简介这份资源是基于时空图卷积网络ST-GCN的骨骼动作识别完整项目面向计算机、人工智能等相关专业的本科生与研究生尤其适合作为课程设计、毕业设计或初期项目演示的参考实现。项目覆盖数据处理、模型训练、离线与实时推理等完整流程并附带多组预训练模型与演示视频能够帮助学习者快速理解图卷积在动作识别中的应用思路。压缩包共90个文件以Python源码29个py、配置文件13个yaml、预训练权重3个pt及演示动画与视频为主整体体积52.55MB结构按feeder、processor、net、tools等模块组织便于对照代码逐层拆解。目前已有230人学习下载适合具备一定深度学习基础、希望动手实践时空图卷积模型的读者。内容包含NTU-RGB-D与Kinetics数据集的预处理脚本、双流ST-GCN网络实现、模型说明文档及运行脚本可支撑从数据准备到结果可视化的完整实验闭环具有较高的工程参考价值。1. 骨骼动作识别为什么绕不开 ST-GCN安防摄像机里一个人背对镜头挥拳人脸完全看不清但骨架姿态足以判断动作——这是基于时空图卷积ST-GCN的骨骼动作识别最典型的落地场景。它把人体关键点在空间上织成图、在时间上串成序列用图卷积同时学到「谁和谁连着」和「动作怎么演化」两件事。相比直接跑 RGB 视频骨骼数据不吃像素、不看光照、对隐私也友好得多代价是必须把图结构建模这件事想清楚。下面按你拿到这个 zip 之后的真实操作顺序来写先讲建模原理再给数据准备、训练命令和踩坑记录。这篇文章适合两类人想用骨骼动作识别落地动作检测的工程同学以及拿到 python 源码后不知道怎么改参数、不知道坑在哪的入门者。2. 从关节点到时空图ST-GCN 的核心建模思路ST-GCN 最早是 Yan 等人在 2018 年提出的核心就一句话把每一帧的人体关节坐标当成图节点把骨骼连接当成边把时间轴上的相邻帧连接起来然后用图卷积在这个时空图上提取特征。图像卷积之所以好使是因为图像是规则的二维网格卷积核平移过去就能对齐骨骼点是不规则分布的没有平移不变性可以依赖所以必须显式告诉模型节点之间怎么连。2.1 骨骼数据怎么织成一张图拿 NTU 数据集来说每帧标注 25 个关节点每个点有 x、y 坐标加一个置信度共 3 个通道。这 25 个点之间天然存在物理连接头连脖子、脖子连肩膀、肩膀连手肘、手肘连手腕、髋部连膝盖。这些连接关系预先给定构成空间图的邻接矩阵 AA 的形状是 25×25A[i][j]1 表示关节点 i 和 j 之间有一根骨头。图的构建有三件套邻接矩阵 A、自连接矩阵 I、度矩阵 D。自连接是给每个节点加一条指向自己的边让图卷积在聚合邻居信息时保留自身特征。度矩阵 D 是一个对角阵对角线上的值是每个节点的邻居数量含自己用来做归一化。常见做法是先构造 A_hat A I再计算归一化矩阵import numpy as np def normalize_adjacency(A): n A.shape[0] A_hat A np.eye(n) # 加自连接保留节点自身信息 D np.diag(np.sum(A_hat, axis1)) # 度矩阵每个节点邻居数 D_inv_sqrt np.linalg.inv(np.sqrt(D)) return D_inv_sqrt A_hat D_inv_sqrt这段代码在绝大多数 ST-GCN 开源实现里都会出现只是有人把它预处理好存成 npy有人在模型 forward 里实时算。D 矩阵做的是对称归一化效果是让邻居多的关节点比如躯干上的点特征不会被过度放大邻居少的点手尖、脚尖也能正常参与聚合。空间图只描述单帧姿态动作是时序演化所以还要在时间维度上把相邻帧的同一个关节点连起来。最终每个节点的邻居有两类空间上同一帧里骨骼连接的点以及时间上前后几帧的同一个点。图卷积负责空间聚合时间卷积负责跨帧聚合两者交替堆叠成网络主体。2.2 图卷积、可学习掩码与三种图划分策略图卷积的公式看起来和 CNN 很像但有个关键差异。单层图卷积做的事情是把每个节点的特征和它所有邻居的特征加权求和。用矩阵写法就是 Y A_norm X W其中 A_norm 是归一化后的邻接矩阵X 是当前层的特征W 是权重。但直接拿整张邻接矩阵去乘有个问题所有邻居不分远近权重完全一样表达力不够。原论文的做法是把邻居关系按物理语义分组每组用一张独立的邻接矩阵。论文给出了三种划分策略源码里通常用第三种划分策略分组数分组逻辑适用场景统一划分1所有邻居一组节点少的简易图几乎没人用距离划分2节点自身一组、相邻点一组区分自环和邻居空间构型划分3自身、向心邻居、离心邻居ST-GCN 默认区分肢体向身体靠拢还是离开空间构型划分里的「心」是指整个骨架的重心常见做法是取髋部中心关节点坐标作为重心。然后判断一个邻居节点比当前节点离重心近还是远更近就算向心更远就算离心。这样一来举手、踢腿这类肢体远离身体的动作和收手、屈膝这类肢体靠近身体的动作在特征空间里天然分离。代码里这 3 组邻接矩阵一般拼成一个形状为 [3, 25, 25] 的张量模型里用可学习掩码和它做逐元素相乘import torch import torch.nn as nn class STGCNConv(nn.Module): def __init__(self, A, in_channels, out_channels): super().__init__() self.A nn.Parameter(torch.from_numpy(A).float(), requires_gradFalse) # 可学习掩码初始化为全 1逐元素乘到 A 上 self.M nn.Parameter(torch.ones_like(self.A)) self.W nn.Conv2d(in_channels, out_channels, kernel_size1) def forward(self, x): # x: [N, C, T, V]先把 V 维做图卷积 A_norm self.A * self.M # 掩码逐元素作用 x torch.einsum(nctv,kvw-nctw, x, A_norm) return self.W(x)M 是模型自己学出来的注意力掩码初始全 1训练过程中它会学会放大哪些连接、压低哪些连接。比如「挥手」这个动作里肩-肘连接和肘-腕连接会被放大腿部的连接会被压低。这个设计是 ST-GCN 比普通 GCN 好用的一个重要原因。2.3 ST-残差块通道数、层数与时间卷积的配合网络主体是 9 个 ST-GCN 残差块堆叠每个块内部结构是图卷积 → 批归一化 → ReLU → 时间卷积 → 批归一化 → ReLU → 残差相加。通道数按 64-64-64-128-128-128-256-256-256 的规律走第 4 层和第 7 层的时间卷积 stride 设为 2把时序长度逐级减半和图像分类网络里空间下采样的思路完全一致。时间卷积用的是普通 1D 卷积kernel size 常见取 9作用在 T 维上。输入 300 帧的序列经过第 4 层变成 150 帧第 7 层变成 75 帧最后接全局平均池化把 [N, C, T, V] 压成 [N, C]再过全连接分类。训练时最需要留意的是这个时域下采样。源码里 T 的初始值不是写死在模型里的而是根据输入数据自动推断。你如果换数据集的时候没有保持训练和验证的帧数一致前向传播到池化层之前维度就对不上报错信息通常是一长串 size mismatch很少有人第一眼能看出是帧数问题。3. 拿到 zip 先读项目说明数据格式与 NTU 转换脚本这个 zip 里带了一份项目说明文档这是判断一份 python 源码能不能直接跑的第一步。很多人把它当 README 扫一眼就丢其实它比代码本身更重要。项目说明里写清楚了环境、数据格式、训练入口照着它走能省下大半天的试错时间。3.1 项目说明文档的四个优先项我拿到任何一份深度学习源码会按固定顺序翻项目说明里的四个地方。第一是环境依赖清单看它写了哪些包和版本号重点关注 torch 和 cuda 的版本组合第二是数据目录结构看训练和验证数据放在哪个目录、是什么格式是 .npy 还是 .pkl每个文件对应什么协议NTU 的 cross-subject 还是 cross-view第三是训练入口确认主程序是 main.py 还是 train.py命令行参数长什么样第四是预训练权重和 License有没有提供权重文件、能不能商用这决定了你要从头训还是直接微调。这四个信息的优先级是递减的。环境版本不对只会报错但数据格式不对会训练不报错、精度崩掉是最隐蔽的问题。预训练权重有没有则直接决定你第一周能不能看到结果。项目说明文档如果把这四块写清楚这个源码的质量就过关了一半。3.2 先搞懂 [N, C, T, V, M]骨骼数据的形状约定ST-GCN 的输入张量形状是五维这个五维在很多人的代码里被写死换数据时最容易出乱子。五个维度依次是N 批次大小、C 特征通道数、T 时序帧数、V 单帧关节点数、M 人数上限。C 在 NTU 上取 3对应 x、y 坐标和置信度T 由采样策略决定常见取 60、150 或 300V 在 NTU 上固定 25M 是每帧最多出现的人数NTU 上训练集最多 2 人单人动作和双人交互混在一起所以 M 取 2。维度顺序别搞反PyTorch 的习惯是通道维靠前所以是 [N, C, T, V, M]不是 [N, T, V, C, M]。M 维的位置有讲究。放在最后一位模型通过逐点卷积把两个人各自的特征提取出来再用全局池化去融合如果放在 C 后面等于把「第一个人」和「第二个人」当成两组特征通道效果完全不同。这一点在后面避坑章节会展开。3.3 把 NTU 原始数据转成 npy转换脚本与归一化NTU 官方发布的是 mat 格式文件每个样本一个文件里面包含骨架关节点坐标。不同预处理工具产出的字段名不一样常见的关键字有 data、joints、skeleton取值形状一般是 (3, T, V, M)。转换脚本的核心逻辑是先读 mat、再截断或补齐帧数和人数、最后按 [N, C, T, V, M] 重排import numpy as np import scipy.io as sio def mat_to_npy(mat_path, joint_count25, max_frame60, max_person2): mat sio.loadmat(mat_path) # 不同工具链产出的字段名不一样先打印 mat.keys() 确认 data mat.get(data, mat.get(joints)) data data[:3] # 只取 x, y, confidence T min(data.shape[1], max_frame) data data[:, :T, :, :max_person] out np.zeros((3, max_frame, joint_count, max_person), dtypenp.float32) out[:, :T, :, :data.shape[-1]] data return out这段代码做了两件关键的事帧数不够 60 的用 0 补齐、人数只有 1 的补一行 0保证输出形状严格是 [3, 60, 25, 2]。很多人忽略这个补齐直接拿原始长度去做 batch到模型里维度对不上才回头改浪费不少时间。原始坐标的数值量级是像素级可能从 0 到 1080 分布。直接丢给模型训练loss 会降得极慢因为梯度被坐标尺度放大了。我一般会在预处理阶段就做归一化而不是丢给 dataloader 实时算def normalize_pose(frame, center_joint1, scale_joints(0, 1)): center frame[:, center_joint] # 以脊柱中段为原点常见的还有取脊柱底部 frame frame - center[:, None] length np.linalg.norm( frame[:, scale_joints[0]] - frame[:, scale_joints[1]] ) frame frame / (length 1e-6) # 除以躯干长度消除身高差异 return frame除以躯干长度这一步很多人会省略但摄像头远近不同、人的身高不同关节坐标的绝对尺度差异很大。除以躯干长度之后特征变成相对量模型对不同体型的人才有一致性。加 1e-6 是防止躯干长度为 0 导致除零。4. 用 Python 跑通 ST-GCN 训练最小命令与六个关键参数环境装好之后训练一个能用的模型并不复杂。常见的开源实现入口是 main.py 或 train.py用命令行参数控制训练和推理。这一章给一套最小可跑通的命令再逐个解释影响精度和时间的关键参数。4.1 python 环境配置conda、CUDA 与依赖清单环境问题是最容易卡住新手的地方。推荐用 conda 建一个独立环境避免把系统 python 搞乱。PyTorch 的安装命令要特别注意 CUDA 版本常见组合是 CUDA 11.8 配 PyTorch 2.1.0这个组合在大多数显卡驱动下都稳定conda create -n stgcn python3.9 -y conda activate stgcn pip install torch2.1.0 torchvision --index-url https://download.pytorch.org/whl/cu118 pip install numpy scipy opencv-python tqdm tensorboard装完先验证一句python -c import torch; print(torch.cuda.is_available())输出 True 再往下走。这里有个经验不要用 conda install 装 pytorchconda 源里的版本经常落后而且依赖解析容易拖进一堆无关包。pip 加--index-url指定 PyTorch 官方源版本可控得多。4.2 最小训练命令从 main.py 入口说起数据转成 npy 之后按项目说明里约定的目录结构放好训练命令长这样python main.py --phase train \ --dataset NTU --protocol xsub \ --batch-size 64 --lr 0.1 \ --weight-decay 0.0001 --dropout 0.5 \ --epochs 80 --num-worker 8 --device cuda:0--protocol xsub指定 NTU 的评测协议NTU 60 有 cross-subject按人物 ID 划分训练测试和 cross-view按摄像头视角划分两种精度标准不一样论文里报告的结果也分开说。--phase train是主线入口跑完会自动转 validation不需要再敲一遍测试命令。第一次跑建议把--epochs临时改小比如 5确认前向、反向、权重更新整个链路通顺后再挂长训。很多源码里还有--resume参数断点续训用的长任务必开。4.3 六个关键参数照着调就能复现八成精度参数常见值作用调参方向batch_size64每步样本数影响 BN 统计量显存不够降到 32BN 抖动调大lr0.1初始学习率SGD 配 warmuploss 震荡就降到 0.01weight_decay0.0001L2 正则强度过拟合明显时调到 0.0005采样帧数 T60输入时序长度帧数少训练快精度略降dropout0.5全连接层前随机丢弃数据量小调大到 0.7图划分策略空间构型邻接矩阵分组方式一般不动改它等于改模型采样帧数 T 是最值得讲的一个。NTU 原始序列长度从几十帧到三百帧不等常见做法是均匀采样到固定长度。T60 在大部分动作上精度损失很小训练速度快一倍T150 精度略高但显存和训练时间显著上涨。我用过的项目里 60 帧和 150 帧在 NTU 上的 top-1 差距一般在 1 到 2 个点之间要根据你自己的算力预算去权衡。训练过程怎么判断有没有跑对看两个信号loss 初始值在 4.0 附近60 类分类的随机交叉熵是 ln60 约 4.0980 轮内降到 0.5 以下验证集 top-1 在 NTU 60 cross-subject 协议下能爬到八成上下这是原论文公开的水平。如果你的曲线在 3.0 附近就下不去了大概率是数据预处理的问题不是网络结构的问题先回第 3 章检查归一化。5. 训练骨骼动作识别最容易翻车的 5 个坑骨骼动作识别看着简单关键点能画出来、模型能跑通但精度上不去的时候问题往往藏在数据形状和预处理细节里。这一章把我在 NTU 和自采数据集上踩过的坑按现象、原因、解决来写。5.1 M 轴位置不对训练不报错精度崩给你看现象训练 loss 正常下降验证集 top-1 卡在 40% 上下怎么调参都上不去。原因预处理时数据形状写成了 [N, C, M, T, V] 或 [N, C, T, M, V]把人数维度放到了通道维或时序维后面。模型没有报错因为这五个维度的顺序是合法的只是把「第二个人」当成了额外的特征通道语义完全错了。解决启动训练前先打印 dataloader 输出的 shape确认是 [N, 3, T, 25, 2]。保险的做法是在数据集类的__getitem__里加一行断言assert x.shape (3, T, 25, 2)形状不对直接抛异常宁可训练前崩溃也不要训完了才发现。5.2 坐标没归一化loss 卡在 4.0 附近不动现象loss 初始在 4.0 左右看着正常但几十轮下来只降到 3.8、3.9验证精度跟随机猜差不多。原因关节坐标是原始像素值0 到 1080 的量级。网络第一层图卷积输出会被坐标尺度放大BN 层虽然能缓解一部分但梯度在数值上仍然不稳定模型学得很慢甚至学不动。常见做法是把坐标以脊柱中点为原点做平移再除以躯干长度做尺度归一化。解决回第 3.3 节把归一化加到数据预处理里。注意归一化要在拿到完整一帧的坐标之后做不能逐点归一化因为尺度因子依赖整帧的躯干长度。归一化参数建议存到项目说明里方便推理阶段用完全一致的处理方式。5.3 帧数采样不一致验证精度凭空掉 5 个点现象训练用 60 帧验证时直接拿完整 300 帧序列前向测试精度比训练时低 5 个点以上。原因时间卷积的感受野是基于固定帧数假设的。训练时模型见过的时序模式长度在 60 帧以内推理时突然给 300 帧时序卷积的输出分布变了池化结果也会偏。很多人以为是过拟合其实只是推理数据和训练数据分布不一致。解决训练和验证用同一套采样逻辑帧数相同、采样方式相同。在项目说明里明确记录「输入帧数 60均匀采样」推理脚本里严格复用。换数据集时帧数可以变但训练和验证必须同步变。5.4 自采数据类别不平衡全部预测成高频类别现象自己采集的数据集里「站立」占 90%训练完模型把几乎所有样本都预测成站立整体准确率还挺高但少数类动作全部误报。原因交叉熵损失在大类别样本多的情况下模型学到的最优策略就是全预测成大类别因为这样平均损失最低。NTU 官方数据每个类别样本数接近不会暴露这个问题但自采数据几乎必然不平衡。解决先看混淆矩阵别只看 top-1 准确率。然后做类别加权采样让每个 batch 里各类别样本数接近或者给少数类的 loss 加权。我用得比较多的是给 dataloader 换WeightedRandomSampler改动小、效果直观。5.5 CUDA 与 PyTorch 版本不匹配cuda error 排查顺序现象训练跑到一半报CUDA error: device-side assert triggered或者启动时报CUDA out of memory。原因设备侧断言错误大概率是输入里有 NaN 或类别标签越界比如标签写了 60而类别只有 60 个从 0 到 59。CUDA out of memory常见原因除了显存真不够还可能是别人在同一个显卡上占了显存或者 batch_size 太大。解决遇到这类问题按顺序查三件事。先确认torch.cuda.is_available()返回 True排除装了 CPU 版 PyTorch 的情况再打印训练数据的标签最大值和最小值确认没有越界最后把 batch_size 减半重试。这三步能解决九成以上的 CUDA 报错。6. 让模型真正可用双流融合、可视化验证与提速6.1 双流融合关节坐标加骨骼向量精度再涨 2 个点单流 ST-GCN 只输入关节坐标还有一条信息被浪费了骨骼向量。骨骼向量是相邻关节坐标的差比如肩到肘的向量、肘到腕的向量它描述的是肢体朝向和关节位置是互补的。常见的做法是训练两个独立模型一个吃关节坐标一个吃骨骼向量推理时把两路 softmax 分数加权相加score 0.5 * softmax(joint_logits) 0.5 * softmax(bone_logits)权重可以按验证集调但 0.5 对 0.5 已经很稳。这个融合方式在 NTU 上通常能比单流再涨 2 到 4 个点。代价是训练时间翻倍我先用单流验证整个流程确认精度正常再开双流。6.2 训练后验证与加速画回视频、合并 BN、导出 ONNX训练完不要只看准确率数字把预测结果画回视频里看一眼永远值得。用 OpenCV 加载原始视频帧把预测的关节点和骨骼连线画上去再标上动作类别import cv2 def draw_skeleton(frame, joints, edges, action_name, score): for u, v in edges: cv2.line(frame, (int(joints[u, 0]), int(joints[u, 1])), (int(joints[v, 0]), int(joints[v, 1])), (0, 255, 0), 2) cv2.putText(frame, f{action_name} {score:.2f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) return frame我第一次用这套方案做跌倒检测精确率看着不低画回视频里才发现一个穿大衣的阿姨连续误报因为大衣遮住了腿部关节点数据质量本身有问题。这种事看指标看不出来只有画回视频才能发现。部署阶段想提速最便宜的一招是合并 BN。推理时把 BN 的参数融合进前面的卷积权重再导出 ONNXCPU 上能快 20% 到 30%精度几乎无损。如果还要更快可以把模型转成半精度跑但不能只看推理速度要回测一遍验证集的精度变化。我现在的习惯是凡是输入带 M 轴的先打印样本再训凡是换数据集的先跑 5 个 epoch 看曲线再挂长训凡是换相机位置的先画回视频看漏检。这三个习惯帮我躲过不少坑希望帮到你。本文还有配套的精品资源点击获取