简介:基于时空图卷积(ST-GCN)的骨骼动作识别项目,是一套评审98分的高分毕业设计源码包,主要面向计算机专业正在准备毕设的学生,以及需要人体骨架动作识别实战练习的学习者。项目采用ST-GCN与双流ST-GCN结构,完整实现从骨骼数据处理、图建模、模型训练到识别推理与离线/实时演示的流程,支持NTU-RGB-D与Kinetics等常见数据集,并附带完整的项目说明,能够帮助快速掌握图卷积在动作识别中的工程落地方法。压缩包共88个文件,包含29个Python源码、13个YAML配置、预训练pt模型、MP4演示视频、GIF运行效果图以及项目说明文档等,整体大小约52.56MB,目录按tools、processor、feeder、models等模块清晰划分,方便按需阅读与二次开发。已有424人学习下载,项目说明详细且附带运行配置,可作为毕业设计、课程设计或期末大作业的高质量参考。
1. 骨骼动作识别为什么选择 ST-GCN:从骨架序列到手势级判别的第一步
做动作识别的人都会遇到一个选择:直接用 RGB 视频做端到端,还是先抽骨架再做分类。我最初做这个方向时也纠结过,直到在一个公开数据集上用 ST-GCN 复现了一次。同样 10 个动作类、同样一套训练资源,ST-GCN 只输入 25 个人体关键点的坐标序列,识别精度比当时用双流 CNN 的方案高了近 6 个点,而且推理时完全不依赖背景信息。这套基于时空图卷积(ST-GCN)的骨骼动作识别 python 源码,解决的就是“把一堆坐标点变成动作类别”这件事:它把每一帧的人体关节当成图节点,在空间上聚合相邻关节点,在时间上做时序卷积,从而同时抓住动作的姿态特征和运动节奏。适合正在做毕设复现、动作识别方向科研预研、以及想给传统视频理解方案找轻量替代的从业者。
2. 时空图卷积的建模逻辑:用 Python 构建邻接矩阵与 ST-GCN 块的数据流
ST-GCN 的全称是 Spatial Temporal Graph Convolutional Networks,它的核心主张是:人体动作的本质不是图像纹理,而是骨骼关节点之间的空间构型随时间的演化。所以它放弃了网格形式的卷积核,改用图结构来描述人体。这一章先把“图卷积”这件事讲透,再给出一段能直接跑的邻接矩阵构建代码。
2.1 骨架数据为什么是“图结构”而不是“网格结构”
一张 224x224 的 RGB 图像,像素是规则排列的网格,卷积核可以直接用 3x3 的窗口滑动。但 25 个骨骼关键点不是规则网格:以 NTU 这类骨架数据为例,每一帧包含 25 个关节点(头、颈、肩、肘、腕、髋、膝、踝等),它们在空间中呈树状结构连接,每个节点的邻居数量不同——手腕只有肘一个相邻节点,而髋关节同时连着躯干和两条腿。如果在这样一个图上套普通 CNN,卷积核会滑到不准的位置,或者说,网格卷积根本找不到“上下左右”的定义。图卷积要做的,就是定义“邻居”和“聚合”规则,让卷积操作能在不规则的连接关系上跑起来。
这里的关键是把人体骨架的先验连接关系写进模型:相邻节点之间有一条物理“骨骼”,这条边决定了信息怎么流动。实现时,这个先验被编码在一个邻接矩阵 A 里,A[i][j]=1 表示节点 i 和节点 j 直接相连,加上自连接后,就构成了一次图卷积的聚合范围。
2.2 两种邻接矩阵写法:单位矩阵加法与归一化拉普拉斯
常见的写法有两种。第一种最简单:在原始连接矩阵 A 上叠加单位矩阵 I,得到 A_hat = A + I。自连接的意义是让每个节点在聚合时保留自身特征,否则每次消息传递都会丢失节点自己的信息,层数一深特征就容易被邻居“淹没”。第二种是归一化拉普拉斯:D^{-1/2} (A + I) D^{-1/2},其中 D 是度矩阵,D[i][i] 表示节点 i 的邻居数量(含自连接)。归一化的目的是防止度数高的节点特征被放大、度数低的节点被压缩——本质上和图像里做像素归一化是一个道理。ST-GCN 原文用的是划分策略(把邻居分成向心、离心、根节点三类),但工程复现时,如果你只是跑分类任务,先用归一化拉普拉斯版本,稳定且不容易出 NaN。
下面这段 python 代码可以直接在本地把两种邻接矩阵都构建出来,这也是“python 构建邻接矩阵”最常见的实现模板:
import numpy as np # 以 25 点骨架为例,先定义物理连接关系。 # 这里用 7 点简演示意,换成 25 点时只改 num_node 和 edges 即可。 edges = [(0, 1), (1, 2), (1, 3), (3, 5), (2, 4), (4, 6)] num_node = 7 # 构建原始邻接矩阵 A:无向图,所以要对称赋值 A = np.zeros((num_node, num_node), dtype=np.float32) for i, j in edges: A[i, j] = 1.0 A[j, i] = 1.0 # 写法一:加自连接的 A_hat I = np.eye(num_node, dtype=np.float32) A_hat = A + I # 写法二:归一化拉普拉斯,等价于 D^{-1/2} A_hat D^{-1/2} degree = A_hat.sum(axis=1) # 每个节点的度(含自连接) d_inv_sqrt = np.power(degree, -0.5) # 度矩阵的 -1/2 次方 d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0 # 处理度为 0 的孤立节点 D_norm = np.diag(d_inv_sqrt) A_norm = D_norm @ A_hat @ D_norm print("A_hat:\n", A_hat) print("A_norm:\n", A_norm)这段代码的关键在于:edges 列表定义了人体的骨骼连接,这是从生理结构来的先验,不能用学习替代;A_hat 让每个节点都能“看到自己”,是保底写法;A_norm 做了尺度归一化,训练更稳。实际用 25 点骨架时,把 num_node 改成 25、edges 换成完整连接表即可,代码逻辑完全不用动。numpy 的这些操作在 python 3.8 环境下都很常规,如果还没装,按 python 安装 numpy 库的方法直接 pip install numpy 就能跑。
2.3 ST-GCN 块里的维度变化:从 (N, C, T, V) 到分类输出
站在模型视角,输入的骨骼序列被组织成一个五维张量 (N, C, T, V, M),N 是 batch 大小,C 是特征通道数(通常是坐标 x、y 加上置信度),T 是帧数(时间维),V 是关节点数(空间维),M 是人数。ST-GCN 块的核心操作分两步:先用图卷积在 V 维度上做空间聚合,再用普通 1D 卷积在 T 维度上做时序建模。
可以这样理解:图卷积的作用是把“某一帧里每个关节点的特征”混合成“考虑了邻居之后每个关节点的特征”,反复几次,模型就学到了“手肘抬到肩膀高度”这类局部空间构型;紧接着的时序卷积则负责回答“这个构型是快速挥拳还是缓慢抬手”。步长大于 1 时,时间维被压缩,特征不断抽象;最后接一个全局平均池化和全连接层,输出每个动作类别的分数。
一个标准的 ST-GCN 块参数通常是这样的:空间卷积 kernel_size = 1(图卷积实现为 1x1 卷积乘以邻接矩阵),时序卷积 kernel_size = 9、stride = 1,残差连接在通道数变化时用 1x1 卷积对齐。工程里很多人一上来就把 kernel_size 调大或把层数堆到 10 层以上,结果显存先炸了,精度反而没涨——这个尺度问题在避坑章会细说。
3. 从原始视频到模型输入:骨架提取、序列统一与坐标归一化实现
ST-GCN 本身不负责从视频里找关节点,它消费的是骨架序列。所以一个完整的识别系统,前面还得接一个姿态估计模型。这一章把“视频 → 骨架 → 张量”这条管线拆开,给出我在本地跑通的最小步骤。很多复现失败的案例,问题不出在模型,而在这条预处理管线。
3.1 骨架提取方案怎么选:25 点、17 点还是 18 点
常见的人体姿态估计工具输出三种关节点规格:OpenPose 的 25 点(BODY_25)、COCO 的 17 点、MPII 的 15/16 点。选哪个不只看精度,更看和模型的匹配度。ST-GCN 原文的实验基于 18 点(OpenPose 早期版本)和 25 点,很多公开的骨骼动作识别数据集也提供了现成的骨架坐标,比如 NTU RGB+D 每帧给 25 点。
我一般建议:数据集给你什么关键点,就保持什么关键点,不要轻易做关键点重投影。因为重投影意味着你要自己配骨骼连接表,一旦连错,模型学到的是错误拓扑。比如 COCO 的 17 点里,左耳和右耳分别连接头部,而 OpenPose 的 25 点里鼻子、颈、髋是关键中枢,两种拓扑完全不同。如果数据集没给骨架,就固定用 OpenPose 抽 25 点,理由一是 OpenPose 的 COCO 模型速度快,二是 25 点保留了更多躯干与四肢细节,对跌倒检测这类依赖躯干姿态的动作更友好。MediaPipe 也可以,但它在遮挡场景下输出的关键点顺序和 OpenPose 不一致,换库就要连带改连接表,容易埋雷。
如果关键点带置信度通道(C=3),归一化时置信度不要动,单独保留。有些实现会把低置信度关节点直接置零,我试过之后觉得副作用很大:同一个人在不同帧间关键点缺失模式不同,模型会把这种“缺失”当特征学进去。保留原始置信度,让模型自己判断要不要信这个点,更稳。
3.2 序列长度统一:等间隔采样与尾部填充
视频长度不同,但网络的 T 维必须固定。常见做法是设一个超参 num_frames,比如 64 或 128。两种策略:第一,等间隔采样,把每一段视频均匀抽成 num_frames 帧,适合整体动作节奏相对均匀的场景;第二,尾部填充,把长的截断、短的补零或复制最后一帧,适合动作有明确起止的短片段。
这里要特别注意:填充的帧在后续时序卷积里会产生伪特征。如果你发现模型对“长时间静止后突然动作”的样本特别容易误判,多半是尾部填充帧参与梯度更新导致的。一个缓解办法是给填充帧做一个 mask,或者在 loss 计算时忽略对应位置的输出。等间隔采样虽然也会丢失信息,但不会引入虚假的静止帧,工程上更稳妥。
注意:固定 num_frames 时,宁可采样稀疏一点,也不要让同一段视频里出现大量重复帧。重复帧会让时序卷积学到的“运动速度”失真。
3.3 坐标归一化与数据增强:一份可直接落地的 Python 预处理脚本
拿到原始关键点坐标后,第一件事不是喂模型,而是做归一化。常见做法是以人体质心(所有关节点坐标的均值)为中心做平移,然后除以一个尺度因子(比如肩宽或脊柱长度),把不同身高、不同画面远近的人拉到同一尺度。这样模型不会把人高马大和娇小身材误判成不同动作。
下面给出一段我在本地直接能跑的预处理脚本,输入是 (T, V, C) 的原始坐标序列,输出是模型可用的归一化张量:
import numpy as np def normalize_skeleton(seq): # seq shape: (T, V, C),C 通常为 2(x,y)或 3(x,y,confidence) T, V, C = seq.shape # 基于全部关节点的均值计算质心,作为平移基准 centroid = seq[..., :2].mean(axis=(0, 1), keepdims=True) # (1, 1, 2) seq_centered = seq.copy() seq_centered[..., :2] -= centroid # 用两肩中点与两髋中点的平均距离做尺度归一化 # 索引按 OpenPose 25 点习惯:肩 2/5,髋 8/11,换成 COCO 时对应调整 l_shoulder = seq_centered[0, 2, :2] r_shoulder = seq_centered[0, 5, :2] l_hip = seq_centered[0, 8, :2] r_hip = seq_centered[0, 11, :2] trunk = 0.5 * (np.linalg.norm(l_shoulder - r_shoulder) + np.linalg.norm(l_hip - r_hip) + 1e-6) seq_norm = seq_centered seq_norm[..., :2] /= trunk return seq_norm # 数据增强:对坐标加噪声和轻微旋转 def augment_skeleton(seq, angle=0.05, noise=0.01): T, V, C = seq.shape aug = seq.copy() theta = np.random.uniform(-angle, angle) cos_t, sin_t = np.cos(theta), np.sin(theta) rot = np.array([[cos_t, -sin_t], [sin_t, cos_t]]) aug[..., :2] = aug[..., :2] @ rot.T aug[..., :2] += np.random.normal(0, noise, size=(T, V, 2)) return aug归一化的关键在于:质心和尺度因子都要从同一段序列里算,千万不要用整个数据集的均值去替代。不同视频里人的位置和尺度差异太大,全局均值会把个体差异全部抹平,模型会变得对绝对坐标极其敏感。旋转和噪声的幅度我给的是经验值:angle=0.05 弧度约 3 度,noise=0.01 是坐标归一化后的量级。如果原始坐标像素值在几百量级,这两个参数要同步缩放,否则增强就变成了破坏。骨骼数据增强很容易做过头,因为关节点的微小抖动在时序卷积里会被放大成虚假的运动模式。
4. 读透这套 python 源码:模型文件、训练闭环与可视化复现
拿到一套 ST-GCN 源码工程,不要急着跑 train.py。先花十分钟看目录结构,搞清楚数据从哪个文件进来、模型定义在哪、配置参数在哪改。这一章按一个典型 pytorch 工程的常见组织方式来讲,你把它对应到自己手里的 zip 工程上,就能少走很多弯路。
4.1 源码工程通常会拆成哪些模块:一个目录对照表
一套规范的源码包,通常会有这么几个文件(具体命名可能不同,但职责基本一致):
| 常见文件名 / 目录 | 职责 | 你需要改什么 |
|---|---|---|
| config.py 或 yaml 配置 | 学习率、batch、epoch、类别数 | 数据路径、num_classes、device |
| graph.py | 构建邻接矩阵与划分策略 | 关键点数量、连接表 |
| st_gcn.py 或 model.py | 定义图卷积层、时序卷积、残差 | 层数、通道数、dropout |
| feeder.py / dataset.py | 读取骨架数据、做归一化与采样 | 数据路径、num_frames |
| train.py | 训练主循环、验证、保存权重 | batch_size、lr、epoch |
| test.py / demo.py | 加载权重做推理 | checkpoint 路径 |
先改配置,再改 graph,最后动模型结构,这是最不容易翻车的顺序。不少人在 st_gcn.py 里改通道数,却忘了同步 graph.py 里的节点数,结果维度对不上,报错信息还不直观。
4.2 核心模型代码精读:图卷积层与残差连接的参数设置
ST-GCN 模型主体并不复杂,它的图卷积层在 pytorch 里通常就是几个卷积和 einsum 的组合。下面这段代码是一个精简可运行的图卷积层示例,它把邻接矩阵当作一个固定权重,和 1x1 空间卷积的输出做矩阵乘法:
import torch import torch.nn as nn class SpatialGraphConv(nn.Module): def __init__(self, in_channels, out_channels, A): super().__init__() # 固定邻接矩阵:requires_grad=False 表示不参与训练 # 想改成自适应邻接矩阵时,把 False 改成 True 即可 self.A = nn.Parameter(A, requires_grad=False) self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1) self.bn = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) def forward(self, x): # x: (N, C, T, V) N, C, T, V = x.shape x = self.conv(x) # (N, out_channels, T, V) # 图聚合:把每个节点的特征按邻接矩阵加权求和到邻居 x = torch.einsum('nctv,vw->nctw', x, self.A) return self.relu(self.bn(x))forward 里的 einsum 就是图卷积的灵魂:它把每个节点的特征按邻接矩阵加权求和到邻居上。很多人第一次看会问为什么不用 torch.mm——因为这里要保留 batch 和帧两个维度,einsum 一步完成批量矩阵乘法,代码最简洁,也最省显存。如果你把 requires_grad 改成 True,A 就变成可学习的自适应邻接矩阵,这是后面进阶章要用的核心改动。
接下来是时序卷积部分。常见做法是用一个 2D 卷积,核大小设为 (kernel_size, 1),即只在时间维上滑动,空间维保持 1。这样时序卷积不会跨关节点混合信息——跨关节点的事已经由图卷积干完了。kernel_size=9 是原文常用值,stride=2 用在网络后半段压缩时间维。残差连接的做法是:如果输入输出通道数不一致,先过一个 1x1 卷积改变通道数,然后与主路径输出相加。
4.3 训练主循环:dataloader、损失函数与学习率调度的调用链
训练脚本的骨架大致如下,STGCN 的完整类定义对照 4.2 节去模型文件里找。注意 dataloader 的 num_workers 设置很关键:骨骼数据本身很轻量,瓶颈往往在读取姿态文件而不是计算,num_workers 设成 4 或 8 可以显著加速。但如果你开了 dataloader 的预取队列,内存不足时优先调小 batch 而不是调小 num_workers。
import torch import torch.nn as nn from torch.utils.data import DataLoader # model 为完整 STGCN 模型,num_classes 与数据集类别数一致 model = STGCN(num_classes=10) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1) for epoch in range(epochs): model.train() for x, y in train_loader: x, y = x.to('cuda'), y.to('cuda') out = model(x) loss = criterion(out, y) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() acc = evaluate(model, val_loader) # 验证集准确率 print(f'epoch {epoch}, acc: {acc:.4f}')这里的参数经验:SGD + momentum=0.9 在 ST-GCN 上通常比 Adam 稳定,学习率从 0.1 起、每 30 个 epoch 衰减 10 倍是原文路线;如果你用的是小数据集,学习率建议改成 0.01 起步,否则前几个 epoch loss 就会震荡。CrossEntropyLoss 在类别均衡时直接用没问题,如果动作类别数量差距大,可以给 loss 传入一个 class_weight 张量,给样本少的类更高的权重。
4.4 训练曲线可视化:一张能进论文的 accuracy 图怎么画出来
训练过程中把每个 epoch 的 train acc 和 val acc 记录到列表里,最后用 matplotlib 画出来。一个常见坑是横坐标太密集:epoch 一多,刻度标签全部叠在一起,整张图没法看。解决方法是只显示指定间隔的刻度,至少让每个刻度之间有足够空隙:
import matplotlib.pyplot as plt plt.plot(train_accs, label='train') plt.plot(val_accs, label='val') plt.xticks(range(0, len(train_accs), 10)) # 每 10 个 epoch 显示一个刻度 plt.xlabel('epoch') plt.ylabel('accuracy') plt.legend() plt.savefig('acc_curve.png', dpi=150) plt.show()画完图之后要做的一件事是保存最佳权重,不要等训练全部结束才存最后的 checkpoint。常见做法是每个 epoch 验证一次,val acc 创新高就把 state_dict 存成 best_model.pth,这样即使后面训练崩了,你还有“后悔药”可以回滚。很多人忽略的是:检查点文件里除了 model_state_dict,最好连预处理参数(均值、尺度因子)一起存,或者用 json 存一份,否则推理阶段很容易出现 5.3 节说的“换机器掉点”。
5. ST-GCN 高频踩坑与排查清单:从 Loss 不降到换机器精度掉分
这一章写给已经跑起来但结果不对的人。下面几条是我自己复现 ST-GCN 时真踩过的坑,按“现象 → 原因 → 解决”三条写,方便你直接对着排查。
5.1 Loss 停在 1.09 附近不动:先查这三处
现象:训练跑了几十个 epoch,loss 始终在 log(num_classes) 附近徘徊。比如 10 类动作时 loss 约 2.30,20 类时约 3.00,仿佛模型在瞎猜。
原因:loss 值接近 -ln(1/num_classes) 说明网络根本没有学到区分性特征。最常见的原因是学习率太大或太小:lr 过大会导致 loss 早期震荡、后期困在局部;lr 过小则是纯爬不动。第二个常见原因是 BatchNorm 的 momentum 在骨骼数据上过于敏感,数据分布波动大时 BN 的滑动均值滞后。第三个原因很多人会忽略:输入没做中心化,坐标量级在几百,模型第一层很快被大数值特征冲垮。
解决:先把 lr 调到 0.01 级别,用 SGD 跑 5 个 epoch 看趋势;如果 loss 还是平的,打印一帧输入数据,看坐标量级是不是在 0 附近;最后确认 dataloader 的 shuffle=True。shuffle=False 会让每个 batch 都是同一段视频,梯度方向单一,loss 会呈现周期性震荡而不是平滑下降。
5.2 训练集 95% 验证集 60%:过拟合的四步干预
现象:训练集准确率爬到 95% 以上,验证集卡在 60% 上下。这是典型的过拟合,在骨骼动作识别里比图像任务来得更早、更猛,因为骨架数据的特征维度本来就低,模型很容易背下来。
原因:数据量小(几百到几千样本)加上模型容量大(ST-GCN 可以轻松上百万参数),再加上数据增强不到位或 dropout 没开,三个因素叠加,验证集自然上不去。
解决:第一步,把 dropout 打开,一般加在 ST-GCN 块的输出后,rate 从 0.5 试起。第二步,增强强度往上调,噪声从 0.01 增到 0.03、旋转角从 3 度增到 8 度,验证集通常立刻有反应。第三步,把网络后半段的通道数减半,比如 256 降到 128,减少记忆容量。第四步,如果还不行,回看训练集和验证集的划分方式——动作识别数据集如果按人物划分 train/val,跨人的泛化难度本来就远高于随机划分,这时 60% 可能并不是代码问题,而是任务本身难度。你可以换成按样本随机划分试试,如果精度明显上升,说明模型没问题,是数据划分引入了额外挑战。
5.3 同一套权重换机器掉 3 个点:预处理管线不一致
现象:在 A 机器上训练到 val acc=85%,把 best_model.pth 原封不动拿到 B 机器上推理,同样测试集掉到 82% 左右,有时候掉更多。
原因:训练和推理的预处理管线上有细微差别。最常见的三个差异:一是骨架提取工具版本不同,OpenPose 不同版本输出的关键点坐标存在像素级差异;二是归一化时质心和尺度的计算方式变了,比如训练时用整段序列均值、推理时用了单帧均值;三是数据增强没关,测试阶段还开着旋转和噪声。
解决:把预处理封装成同一个函数,训练和测试共用;权重文件传过去之后,先用一条已知样本做“冒烟测试”,打印归一化前后的坐标值,和训练时存的样本对比:
import numpy as np sample = np.load('sample_skeleton.npy') # train_norm 是训练时记录下来的归一化输出 train_norm = normalize_skeleton(sample) # infer_norm 是推理时实际执行的归一化输出 infer_norm = normalize_skeleton(sample) assert np.allclose(train_norm, infer_norm, atol=1e-5), \ "预处理偏差过大,检查归一化和增强开关"这个问题属于那种“怎么查都查不出 bug、但精度就是不对”的玄学,其实只要把管线锁死,用同一份代码、同一个函数跑训练和推理,就再也不会犯。
5.4 梯度爆炸与 NaN Loss:图归一化的隐藏问题
现象:训练第几十个 iter 时 loss 突然变成 nan,后续无法恢复。
原因:图卷积层的邻接矩阵如果有孤立节点或度数为 0 的节点,聚合时会出现除零;或者归一化拉普拉斯里的 d_inv_sqrt 在度为 0 时为 inf,inf 乘上特征直接溢出。另一个少见但真实的原因是分类层的 logits 在 fp16 混合精度下溢出,骨骼坐标虽然量级小,但经过多层累加后照样会爆。
解决:构建邻接矩阵时检查每个节点的度,孤立节点补一个自连接;d_inv_sqrt 用 np.isinf 把 inf 替换成 0,代码在 2.2 节已经给了;混合精度训练时给 loss scaler 设置更大的 growth interval,或者干脆在 ST-GCN 这种小模型上关掉 AMP,训练速度影响不大,但稳定性能提升一个档次。这些排查都不需要改模型结构,但对训练稳定性影响极大。
6. 让识别精度再上一个台阶:自适应邻接矩阵与双流融合的换血改造
当基础版 ST-GCN 已经能跑到 85% 但还想冲高时,我一般会做两个方向的小改造,都不复杂,但对精度提升很显著。
6.1 自适应邻接矩阵:把“图结构”从写死变成可学习
把 graph.py 里的 nn.Parameter(A, requires_grad=False) 改成 requires_grad=True,并给这个参数单独设置学习率(通常比主网络小 10 倍),模型就能根据数据调整关节点连接权重。需要注意的是:初始值从归一化拉普拉斯开始,而不是从 0 或随机开始,否则训练早期梯度会乱;另外加一个平滑约束,防止自适应矩阵退化成恒等映射。改完之后可以用 TensorBoard 把 A 打出来看,你能直观看到模型把哪些关节连接权重提上去了。
6.2 双流融合与超参修正:joint + bone 的常见做法
双流输入的思路是把“关节点坐标”和“骨骼向量(相邻关节点坐标差)”当成两个模态,分别输入两个共享参数的 ST-GCN,最后把两个分支的 softmax 分数相加或拼接。骨骼特征对肢体长度和方向更敏感,关节特征对整体姿态更敏感,两个视角天然互补。我自己的做法是直接复用一套模型定义,加载两份预处理后的数据,loss 取两个分支的平均,实践下来比单流能涨 2-3 个点。
训练超参我也想分享一条心得:小数据集(2000 以下)把 weight_decay 从 1e-4 提到 5e-3,能明显压住过拟合;而 batch size 不要贪大,ST-GCN 对 BN 的依赖很重,batch=64 时的跨域稳定性比我试过的 128、256 都耐看。如果你的数据里有长视频,把 T 从 64 提到 128 之前先确认显存足够,否则序列加长带来的收益会被频繁的 OOM 抵消。我最早跑这套工程时,一上来就把 batch 拉到 256,结果直接在 BN 层爆了显存,后来老老实实从 32 起步逐级加才找到平衡点。这些坑都是实际跑过才留下的,希望帮到你。
本文还有配套的精品资源,点击获取