{"notes":[{"id":"action-recognition/st-gcn","category":"action-recognition","legacyPath":"","legacySlug":"","legacyPaths":[],"title":"ST-GCN","subtitle":"向心，離心","author":"Z. Yuan","paperDate":"2018-01-23","publishedOn":"2023-12-11","world":"external/st-gcn","paperTitle":"Spatial Temporal Graph Convolutional Networks for Skeleton-Based Action Recognition","paperUrl":"https://arxiv.org/abs/1801.07455","bibliography":{"title":"Spatial Temporal Graph Convolutional Networks for Skeleton-Based Action Recognition","authors":["Sijie Yan","Yuanjun Xiong","Dahua Lin"],"url":"https://arxiv.org/abs/1801.07455","pdfUrl":"https://arxiv.org/pdf/1801.07455v2","codeUrl":"https://github.com/yysijie/st-gcn","preprint":{"id":"1801.07455","submittedOn":"2018-01-23","url":"https://arxiv.org/abs/1801.07455"},"publication":{"venue":"AAAI","year":2018,"kind":"conference","url":"https://ojs.aaai.org/index.php/AAAI/article/view/12328"},"citation":"@misc{actionrecognitionstgcn2018,\n  title = {{Spatial Temporal Graph Convolutional Networks for Skeleton-Based Action Recognition}},\n  author = {Sijie Yan and Yuanjun Xiong and Dahua Lin},\n  year = {2018},\n  eprint = {1801.07455},\n  archivePrefix = {arXiv},\n  url = {https://arxiv.org/abs/1801.07455}\n}\n","citationPath":"/content/papers/citations/action-recognition/st-gcn.d4a6b81c8471.bib"},"thumbnail":{"src":"/content/papers/assets/75fab9132b72202980c799f04d5fa51cf9749352d5b47ce0d19b571aa8a39acc.png","width":868,"height":292},"locale":"zh-hant","path":"/content/papers/zh-hant/action-recognition/st-gcn.d17e446310be.md","headings":[{"id":"定義問題","text":"定義問題","depth":2},{"id":"解決問題","text":"解決問題","depth":2},{"id":"時空圖","text":"時空圖","depth":3},{"id":"圖上的權重要分給誰","text":"圖上的權重要分給誰","depth":3},{"id":"三種分組","text":"三種分組","depth":3},{"id":"實作成矩陣乘法","text":"實作成矩陣乘法","depth":3},{"id":"邊的重要性","text":"邊的重要性","depth":3},{"id":"整個網路","text":"整個網路","depth":3},{"id":"討論","text":"討論","depth":2},{"id":"資料集","text":"資料集","depth":3},{"id":"分組方式的消融","text":"分組方式的消融","depth":3},{"id":"和其他方法比","text":"和其他方法比","depth":3},{"id":"結論","text":"結論","depth":2}],"anchorAliases":{},"guide":null,"relatedReading":[{"id":"graph-learning/gcn","relation":"referenced","topic":""}]},{"id":"graph-learning/gcn","category":"graph-learning","legacyPath":"","legacySlug":"","legacyPaths":[],"title":"GCN","subtitle":"一個參數夠嗎？","author":"Z. Yuan","paperDate":"2016-09-09","publishedOn":"2023-10-01","world":"external/gcn","paperTitle":"Semi-Supervised Classification with Graph Convolutional Networks","paperUrl":"https://arxiv.org/abs/1609.02907","bibliography":{"title":"Semi-Supervised Classification with Graph Convolutional Networks","authors":["Thomas N. Kipf","Max Welling"],"url":"https://arxiv.org/abs/1609.02907","pdfUrl":"https://arxiv.org/pdf/1609.02907v4","codeUrl":"https://github.com/tkipf/gcn","preprint":{"id":"1609.02907","submittedOn":"2016-09-09","url":"https://arxiv.org/abs/1609.02907"},"publication":{"venue":"ICLR","year":2017,"kind":"conference","url":"https://arxiv.org/abs/1609.02907"},"citation":"@misc{graphlearninggcn2016,\n  title = {{Semi-Supervised Classification with Graph Convolutional Networks}},\n  author = {Thomas N. Kipf and Max Welling},\n  year = {2016},\n  eprint = {1609.02907},\n  archivePrefix = {arXiv},\n  url = {https://arxiv.org/abs/1609.02907}\n}\n","citationPath":"/content/papers/citations/graph-learning/gcn.43058ec3e77e.bib"},"thumbnail":{"src":"/content/papers/assets/ded600c9ca8f4eda09d39ffb16b3c9b0ec730780063e55948b29924d00f14794.png","width":1122,"height":400},"locale":"zh-hant","path":"/content/papers/zh-hant/graph-learning/gcn.66a3d9542810.md","headings":[{"id":"定義問題","text":"定義問題","depth":2},{"id":"把圖當成正則項","text":"把圖當成正則項","depth":3},{"id":"解決問題","text":"解決問題","depth":2},{"id":"從頻域一路簡化","text":"從頻域一路簡化","depth":3},{"id":"一層-gcn","text":"一層 GCN","depth":3},{"id":"兩層就夠","text":"兩層就夠","depth":3},{"id":"討論","text":"討論","depth":2},{"id":"實驗設定","text":"實驗設定","depth":3},{"id":"主要結果","text":"主要結果","depth":3},{"id":"哪一種傳播最好","text":"哪一種傳播最好","depth":3},{"id":"疊深一點會怎樣","text":"疊深一點會怎樣","depth":3},{"id":"和-wl-演算法的關係","text":"和 WL 演算法的關係","depth":3},{"id":"四天後的一篇評論","text":"四天後的一篇評論","depth":3},{"id":"論文自己列的限制","text":"論文自己列的限制","depth":3},{"id":"結論","text":"結論","depth":2}],"anchorAliases":{},"guide":null,"relatedReading":[{"id":"graph-learning/spectral-networks","relation":"referenced","topic":""},{"id":"graph-learning/chebnet","relation":"referenced","topic":""}]}],"text":"要辨認一段影片裡的人在做什麼，可以看畫面，也可以只看骨架。\n\n骨架是一串關節座標。用姿態估計工具把每一幀的頭、肩、肘、腕、髖、膝、踝標出來，一段十秒的影片，就變成幾百幀、每幀十幾個點的序列。資料量小，不受背景和光線影響，跳舞、揮手、跌倒這類以肢體為主的動作，光看骨架就有很多線索。\n\n骨架本身就是一張圖。關節是節點，骨頭是邊。香港中文大學的 Sijie Yan、Yuanjun Xiong 與林達華把 GCN 搬到這張圖上，同時處理空間和時間，稱為 ST-GCN。\n\n## 定義問題\n\n在這之前，骨架動作辨識大致有兩類做法。\n\n一類靠人工設計，例如把身體分成幾個部位，手工定義部位之間的關係，或是在關節軌跡上計算手工特徵。換一套資料，規則就得重寫。\n\n另一類用 RNN 或時間卷積，把每一幀所有關節的座標串成一個長向量，再沿時間軸處理。這樣做直接丟掉了骨架的連接結構，右肘和右腕相連、和左膝不相連，模型得自己從資料裡猜。\n\n作者想要的模型，要能直接利用骨架的連接方式，同時學出空間與時間上的模式，不需要人工定義部位。GCN 剛好提供了一個在圖上做卷積的框架，但它是為引用網路這類圖設計的。要用在骨架上，還有一個問題得先解決。\n\n## 解決問題\n\n### 時空圖\n\n給定一段 $T$ 幀、每幀 $N$ 個關節的骨架序列，作者建一張無向的時空圖。節點是每一幀的每一個關節，共 $T\\times N$ 個；每個節點的特徵是座標，加上姿態估計的信心分數。\n\n邊分成兩種。同一幀裡，依人體的自然連接方式把關節連起來；相鄰兩幀之間，把同一個關節連起來。Kinetics 用 OpenPose 估出的 18 個 2D 關節，NTU-RGB+D 用 Kinect 追蹤的 25 個 3D 關節；連接方式由資料決定，不需要手工分部位。\n\n[![原論文 Figure 1：一段骨架序列的時空圖。藍點是關節，同一幀內的骨頭相連，綠色的邊把相鄰幀的同一個關節連起來，右下方的箭頭標示時間方向。](/content/papers/assets/9e7851cb79bcf44e244d3b165ae301cb07adf86ffa6961d683bb9b743c4d2275.png)](/content/papers/assets/9e7851cb79bcf44e244d3b165ae301cb07adf86ffa6961d683bb9b743c4d2275.png)\n\n*原論文 Figure 1（Yan 等人），取自 [**PDF 第 1 頁**](https://arxiv.org/pdf/1801.07455v2#page=1)。圖說未收錄。*\n\n以 Kinetics 為例，一段影片的輸入張量是 $(3, T, 18, 2)$。3 是 $(x, y, \\text{信心})$，18 是關節數，2 是每段影片最多取信心最高的兩個人。作者把每段影片重複播放補到 $T=300$ 幀。\n\n### 圖上的權重要分給誰\n\n先回到一般的 2D 卷積。以 3×3 卷積為例，輸出位置 $x$ 的值是：\n\n$$\nf_{out}(x)=\\sum_{h=1}^{3}\\sum_{w=1}^{3}f_{in}\\left(p(x,h,w)\\right)\\cdot w(h,w)\n$$\n\n作者把它拆成兩個函數。**取樣函數** $p$ 決定看哪些位置，影像上就是周圍的 9 個像素；**權重函數** $w$ 決定每個位置乘上哪一組權重。影像上有固定的網格，左上、正上、右上……9 個位置各自對應 9 組權重，索引一個 $(c, 3, 3)$ 的張量就好。\n\n在圖上，取樣函數不難定義，取距離 1 以內的鄰居就好。權重函數就麻煩了，鄰居沒有「左上」「正上」這種固定的順序。\n\n作者的辦法是把鄰居分成固定的 $K$ 組，每組給一個標籤，同一組共用一組權重：\n\n$$\nf_{out}(v_{ti})=\\sum_{v_{tj}\\in B(v_{ti})}\\frac{1}{Z_{ti}(v_{tj})}\\,f_{in}(v_{tj})\\cdot w\\left(l_{ti}(v_{tj})\\right)\n$$\n\n$l_{ti}$ 把每個鄰居對應到一個組別，$Z$ 是該組的節點數，用來平衡各組的貢獻。照這個說法，3×3 卷積就是把 9 個像素分成 9 組，每組一個像素。\n\n時間方向比較單純。時間軸本身有順序，把空間的組別再依「前幾幀、後幾幀」編號，就能定義時空鄰域上的卷積；時間方向的範圍 $\\Gamma$ 就是時間卷積核的大小，論文用 9。\n\n### 三種分組\n\n分組規則決定了模型能分辨什麼。作者試了三種：\n\n[![原論文 Figure 3：四張小圖。(a) 輸入骨架，紅色虛線圈出 D=1 的鄰域；(b) 不分組，鄰域內所有節點都是綠色；(c) 依距離，自己是綠色、鄰居是藍色；(d) 依空間位置，黑色叉號是骨架重心，自己是綠色，比自己更靠近重心的鄰居是藍色，更遠的是黃色。](/content/papers/assets/75fab9132b72202980c799f04d5fa51cf9749352d5b47ce0d19b571aa8a39acc.png)](/content/papers/assets/75fab9132b72202980c799f04d5fa51cf9749352d5b47ce0d19b571aa8a39acc.png)\n\n*原論文 Figure 3（Yan 等人），取自 [**PDF 第 5 頁**](https://arxiv.org/pdf/1801.07455v2#page=5)。圖說未收錄。*\n\n- **不分組**（uni-labeling，圖 b）：整個鄰域一組，$K=1$。作者指出，這就是 Kipf 和 Welling 的 GCN 傳播規則。每個鄰居乘同一組權重，等於先把鄰居特徵平均起來再做內積；關節之間的相對關係就看不到了。\n- **依距離**（圖 c）：自己一組，距離 1 的鄰居一組，$K=2$。兩組權重不同，就能表示「自己減去鄰居」這類局部差分，例如關節之間的相對位移。\n- **依空間位置**（圖 d）：$K=3$。自己一組；比自己更靠近骨架重心的鄰居一組，稱為**向心**；更遠的一組，稱為**離心**。重心取該幀所有關節座標的平均，$r_i$ 是關節 $i$ 在訓練集上到重心的平均距離。作者的動機是，肢體的動作大致可以分成向內收和往外伸兩類。\n\n拿一個六個關節的簡化骨架，照依空間位置的規則分組：\n\n```python\nimport numpy as np\nnp.set_printoptions(precision=2, suppress=True)\n\n# 簡化的骨架：0 頸、1 右肩、2 右肘、3 左肩、4 左肘、5 骨盆\nnames = [\"頸\", \"右肩\", \"右肘\", \"左肩\", \"左肘\", \"骨盆\"]\nxy = np.array([[0.0, 1.0], [-0.4, 0.9], [-0.6, 0.5], [0.4, 0.9], [0.6, 0.5], [0.0, 0.2]])\nbones = [(0, 1), (1, 2), (0, 3), (3, 4), (0, 5)]\nV = len(xy)\nA = np.zeros((V, V))\nfor i, j in bones:\n    A[i, j] = A[j, i] = 1\n\ncenter = xy.mean(0)                          # 骨架重心：所有關節座標的平均\nr = np.linalg.norm(xy - center, axis=1)      # 每個關節到重心的距離\n\nA_root, A_in, A_out = np.eye(V), np.zeros((V, V)), np.zeros((V, V))\nfor i in range(V):\n    for j in np.nonzero(A[i])[0]:\n        if r[j] < r[i]:\n            A_in[i, j] = 1                   # 向心：鄰居比自己更靠近重心\n        else:\n            A_out[i, j] = 1                  # 離心：鄰居比自己更遠\n\nprint(\"到重心的距離:\", r)\nfor i in (0, 1, 2):\n    print(f\"{names[i]}：向心 {[names[j] for j in A_in[i].nonzero()[0]]}，\"\n          f\"離心 {[names[j] for j in A_out[i].nonzero()[0]]}\")\n```\n\n```text\n到重心的距離: [0.33 0.46 0.62 0.46 0.62 0.47]\n頸：向心 []，離心 ['右肩', '左肩', '骨盆']\n右肩：向心 ['頸']，離心 ['右肘']\n右肘：向心 ['右肩']，離心 []\n```\n\n示範只用一幀的座標，論文用的是整個訓練集上的平均距離。右肩的兩個鄰居被分進不同的組，頸部那一側是向心，手肘那一側是離心；換成不分組，兩者會被平均在一起。\n\n\n> [!TIP]\n> \n> 如果你要讀官方程式，留意一個和論文寫法不同的地方。`net/utils/graph.py` 的 `spatial` 策略，比較的是鄰居與自己到一個**指定中心關節**的圖上跳數，OpenPose 的 18 點骨架用頸部、NTU 用脊椎，而不是到座標重心的距離。跳數相同的鄰居會和自己併成同一組。\n\n\n### 實作成矩陣乘法\n\n實作沿用 GCN 的寫法。不分組時，一層就是：\n\n$$\nf_{out}=\\Lambda^{-\\frac12}(A+I)\\Lambda^{-\\frac12}f_{in}W\n$$\n\n多組時，把 $A+I$ 拆成 $K$ 個矩陣 $A+I=\\sum_jA_j$，每組各自正規化、各自乘一組權重，再加總：\n\n$$\nf_{out}=\\sum_j\\Lambda_j^{-\\frac12}A_j\\Lambda_j^{-\\frac12}f_{in}W_j\n$$\n\n依距離分組時，$A_0=I$、$A_1=A$。有些組可能整列都是 0，例如上面的頸部沒有向心鄰居，所以正規化時在度數上加一個 $\\alpha=0.001$。\n\n接著上面的骨架，把輸入當成 $(C, T, V)$ 的張量，跑一次空間上的圖卷積：\n\n```python\ndef normalize(Aj, alpha=0.001):\n    d = Aj.sum(1) + alpha                    # 論文加上 α，避免整列都是 0\n    return Aj / np.sqrt(np.outer(d, d))\n\nC_in, C_out, T = 3, 8, 4\nx = np.random.default_rng(0).random((C_in, T, V))      # (通道, 幀, 關節)\nWs = [np.random.default_rng(k).normal(size=(C_out, C_in)) for k in range(3)]\n\nout = sum(np.einsum(\"oc,ctv,wv->otw\", Wk, x, normalize(Ak))   # 每組各自正規化、各自一組權重\n          for Wk, Ak in zip(Ws, [A_root, A_in, A_out]))\nprint(\"輸入:\", x.shape, \"→ 輸出:\", out.shape)\n```\n\n```text\n輸入: (3, 4, 6) → 輸出: (8, 4, 6)\n```\n\n真正的 ST-GCN 單元，在這一步之後還會沿時間軸做 $1\\times\\Gamma$ 的卷積，把前後幾幀的同一個關節串起來。\n\n### 邊的重要性\n\n一個關節可能同時屬於好幾個身體部位，在不同部位裡的重要性也不同。作者在每一層的每個鄰接矩陣上，再乘一個可學的遮罩 $M$，逐元素相乘，初始化成全 1。作者也提到，這裡可以換成依資料而變的注意力，留給未來的工作。\n\n### 整個網路\n\n輸入先過一層 BatchNorm，把各關節的座標尺度拉齊；圖卷積在所有節點上共用權重，尺度不一致會很麻煩。接著是 9 個 ST-GCN 單元，輸出通道依序是 64、64、64、128、128、128、256、256、256，時間卷積核大小 9，每個單元都有殘差連接，之後接 0.5 的 dropout。第 4 和第 7 個單元的時間卷積步長設為 2，當作池化。最後做全域池化，得到 256 維的向量，接 softmax 分類。\n\n訓練用 SGD，學習率 0.01，每 10 個 epoch 乘 0.1。在 Kinetics 上，作者改用兩種資料增強代替 dropout。一是隨機仿射轉換並在幀之間內插，模擬攝影機移動；二是訓練時隨機截取片段，測試時用完整序列。\n\n## 討論\n\n### 資料集\n\n**Kinetics** 有約 30 萬段 YouTube 影片、400 個動作類別，每段約 10 秒；訓練集 24 萬段，驗證集 2 萬段。作者把影片縮成 340×256、轉成 30 FPS，用 OpenPose 估出骨架，只拿骨架當輸入。\n\n**NTU-RGB+D** 是在實驗室裡拍的，40 位受試者、三台攝影機同時錄，約 56,000 段、60 個類別，關節由 Kinect 深度感測器追蹤，每人 25 個 3D 關節。評估方式有兩種，X-Sub 依受試者分訓練和測試，X-View 依攝影機視角分。\n\n### 分組方式的消融\n\n在 Kinetics 上比較各種設定的 top-1 與 top-5 準確率：\n\n| 設定 | Top-1 | Top-5 |\n| --- | ---: | ---: |\n| 只做時間卷積的基準（TCN） | 20.3% | 40.0% |\n| 稀疏骨架圖、不共享權重 | 22.0% | 43.2% |\n| 不分組 | 19.3% | 37.4% |\n| 依距離，但兩組權重只差一個負號 | 23.9% | 44.9% |\n| 依距離 | 29.1% | 51.3% |\n| 依空間位置 | 29.9% | 52.2% |\n| 依空間位置 + 邊的重要性 | **30.7%** | **52.8%** |\n\n這張表有幾件事可以看。\n\n第一，不分組的版本，也就是 GCN 式的傳播，比完全不用圖結構的時間卷積基準還低。作者的解釋就是前面說的，平均之後，局部的差分資訊就沒了。\n\n第二，只要把自己和鄰居分開，準確率就從 19.3% 跳到 29.1%。連兩組權重被綁成只差一個負號的版本，都比不分組好，作者認為這說明了分組本身的重要性。\n\n第三，依空間位置分三組，又比兩組好一些；再加上邊的重要性，多了將近 1 個百分點。\n\n另外，時間卷積的基準其實等於在全連接的關節圖上、不共享權重的時空圖卷積。和它相比，ST-GCN 的差別在稀疏的自然連接與共享權重。\n\n### 和其他方法比\n\n| 方法 | Kinetics Top-1 | NTU X-Sub | NTU X-View |\n| --- | ---: | ---: | ---: |\n| Deep LSTM | 16.4% | 60.7% | 67.3% |\n| Temporal ConvNet | 20.3% | 74.3% | 83.1% |\n| C-CNN + MTLN | — | 79.6% | 84.8% |\n| **ST-GCN** | **30.7%** | **81.5%** | **88.3%** |\n\n兩個資料集上，ST-GCN 都是當時骨架方法裡最好的。\n\n不過在 Kinetics 上，只看骨架和看畫面還差很遠，RGB 是 57.0%、光流是 49.5%。作者的解釋是，Kinetics 有很多動作得看人手上拿的東西、身處的場景才分得出來。他們另外挑了 30 個和肢體動作密切相關的類別，在這個子集上，ST-GCN 的平均類別準確率是 72.4%，RGB 是 70.4%、光流是 72.8%，三者差不多。\n\n和 RGB 的雙流模型結合時，加上 ST-GCN 的效果比加上光流好。沒有 ImageNet 預訓練的設定下，RGB 單獨是 70.3%，加光流 71.1%，加 ST-GCN 71.2%，三者全加 71.7%。\n\n## 結論\n\nST-GCN 把骨架序列建成一張時空圖，用 GCN 的形式在上面做卷積。它最重要的設計是鄰居分組；不分組時，模型連不用圖的基準都贏不了，分成兩組就多了近 10 個百分點。\n\n這也替 GCN 在影像網格上只剩一個平均核的問題，給了一個實際的答案。權重能不能依鄰居的角色分開，對有空間結構的圖差別很大。\n\n\n> [!TIP]\n> \n> GCN 的傳播規則，以及它為什麼會讓所有鄰居共用一組權重，可以參考我們的筆記：\n> \n> - **[[16.09] GCN：一個參數夠嗎？](/blog/papers/graph-learning/gcn)**\n\n\n後來不少骨架動作辨識模型都以 ST-GCN 為基礎，例如 [2s-AGCN](https://arxiv.org/abs/1805.07694) 讓鄰接矩陣也能依資料學習。要自己做的話，記得先確認你的姿態估計工具輸出幾個關節、中心關節是哪一個，分組全靠它。\n","imageSizes":{"/content/papers/assets/9e7851cb79bcf44e244d3b165ae301cb07adf86ffa6961d683bb9b743c4d2275.png":{"width":454,"height":364},"/content/papers/assets/75fab9132b72202980c799f04d5fa51cf9749352d5b47ce0d19b571aa8a39acc.png":{"width":868,"height":292}}}
