{"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-hans","path":"/content/papers/zh-hans/action-recognition/st-gcn.1f5e2bd21c1d.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-hans","path":"/content/papers/zh-hans/graph-learning/gcn.0866efaf54eb.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}}}
