简介这是一份面向计算机、数学、电子信息类专业学生及研究者的骨骼动作识别Python源码项目基于时空图卷积网络ST-GCN实现从骨骼关键点序列到动作类别的端到端识别可直接用于课程设计、期末大作业或毕业设计参考。整个压缩包共91个文件约52.6MB以Python源码29个py为主辅以yaml配置文件、pyc预编译模块、pt模型权重、mp4/gif演示动画及md/txt说明文档涵盖数据预处理、模型构建、训练与推理全流程整体目录结构清晰便于按模块查阅。目前已有275人学习下载。附带预训练模型与演示素材便于快速验证效果代码结构清晰包含双流ST-GCN、实时演示等扩展适合进阶调试与二次开发。下载解压即可运行能帮助理解时空图卷积在动作识别中的实际应用为论文写作与答辩提供有力支撑。1. 骨骼动作识别为什么绕不开时空图卷积ST-GCN骨架是图不是像素你在检索栏里敲“动作识别”的时候大概率会碰到这组关键词时空图卷积ST-GCN、骨骼动作识别、python源码。骨架方案这几年之所以能成为动作识别的主流路子之一核心就一句话人体关键点坐标天然不受光照、衣着和背景的干扰。基于时空图卷积ST-GCN的骨骼动作识别把每一帧的人体连接关系当成一张图用图卷积在关节之间交换特征时间维度再用卷积并行扫过。整套思路比LSTM直观比直接拿CNN处理关节坐标要贴切得多。市面上用python源码跑的ST-GCN毕业设计项目很常见结构清晰、改造成本低适合做行为分析、康复评估和运动姿态校验。这篇笔记会按落地顺序把图怎么建、卷积怎么算、代码怎么跑、参数怎么调讲透最后给一条验证习惯能帮你少走不少弯路。2. 从骨骼数据到时空图卷积图怎么建、卷积怎么算、分区策略为什么决定精度上限这一章先把原理讲透。很多源码包拿到手就能训练但如果不知道图是怎么构造的后面换数据集、调分支、调精度会处处受制。2.1 骨架数据的本质一张写好人际拓扑的图骨骼动作识别的输入是一段时序骨架序列总共有 T 帧每一帧有 V 个关键点每个关键点是 C 维坐标。NTU RGBD 数据集每帧 25 个关键点覆盖头、颈、双肩、双肘、双腕、双手、躯干、双髋、双膝、双踝和双脚Kinetics 上常用的 COCO 格式是 18 个点。节点之间有天然的物理连接——脊柱连肩膀肩膀连手肘手肘连手腕。这种连接关系本身就是先验知识。如果用图像 CNN 来处理就得把 25 个关键点拉平成一个向量。问题是卷积核在空间上共享权重而拉平之后“左肘”和“右肘”明明是不同的关节却会被同一个位置的卷积权重处理等于把人体左右对称结构当成平移不变性来学前置知识完全错配。RNN/LSTM 能建模时序但沿帧方向串行计算前面帧的信息很难有效地流到后面关键时刻训练速度也慢。ST-GCN 的做法是把骨架数据建模成图节点集合就是 V 个关键点空间边 Es 是人体骨骼连接时间边 Et 是同一个关键点在相邻帧之间的对应连接。输入张量组织成 (C, T, V) 三个维度C3 时分别存 x、y、z 坐标。图卷积沿 Es 在关节之间传递特征时间卷积沿 Et 在帧与帧之间建模运动两者各管各的backbone 结构非常干净。2.2 空间图卷积的计算邻接矩阵、自环和度归一化图卷积的一层传播公式可以写成f_out D^(-1/2) (A I) D^(-1/2) f_in W其中 A 是邻接矩阵A[i][j] 1 表示关节 i 和关节 j 之间有骨骼连接I 是单位矩阵用来给每个节点加自环这样节点在一次更新里既能聚合邻居信息也能保留自身特征D 是度矩阵D[i][i] 等于节点 i 的邻居数量加上自环后要 1W 是这层卷积的可学习权重。这个公式里最关键的是度归一化。颈部、髋部这类中心关节邻居多聚合之后特征范数天然偏大手脚这类末端关节邻居少特征会偏小。如果不做归一化模型学出来的特征尺度会被中心关节点带偏训练过程容易不稳。D^(-1/2) A D^(-1/2) 是对称归一化比直接除以度数值更平滑这也是图神经网络里的标准做法。但 ST-GCN 没有止步于对整张图做一次卷积。它把邻接矩阵按分区策略拆成多个子矩阵 A_1, A_2, ..., A_k每一份子矩阵配一个独立的权重 W_k最后把 k 个分支的结果加起来。这样做等于让同一个模型拥有多种“关系视角”一个视角看自己一个视角看邻居一个视角看邻居相对身体重心的方向。参数多了表达能力也上去了。2.3 三种分区策略为什么空间构型划分最常用ST-GCN 论文里提出了三种分区策略源码里都有现成实现但选哪个直接决定模型上限。单一标签分区Uni-labeling所有邻居节点加上自己归为一类k1相当于全局共享权重的最简图卷积。优点是参数最少、不容易过拟合缺点很明显模型分不清邻居是靠近身体还是远离身体对“挥手”和“放下手”这类方向敏感的动作几乎要靠时序分支硬猜。距离划分Distance partitioning根节点自己一类1-hop 邻居一类k2。模型能区分“自己”和“别人”但还是分不清这个邻居是向心还是离心对肢体相对躯干的运动方向仍然不敏感。空间构型划分Spatial configuration partitioning以骨架重心为参考点把邻居节点分成向心离重心更近和离心离重心更远两类根节点自己单独一类一共 k3 类。这是动作识别场景里最常用的策略因为“手向身体移动”和“手离开身体”是挥拳、举手、投掷这类动作的核心判据。三个子矩阵分别建模三个语义最后一层相加代价是权重数量变成三份。完整的 ST-GCN backbone 是 9 个时空块堆叠。每个块内部结构是先做空间图卷积接 BatchNorm、ReLU、Dropout再做时间维卷积。时间维卷积用的是标准 Conv2d把卷积核放在帧维度上扫NTU 场景下 kernel_size9 比较稳小了感受不到帧间时序大了参数膨胀明显。通道数从 3 开始经过 64、128、256 三个量级递进第 4 和第 7 块在时间维做 stride2 的下采样降低帧率换取更大感受野。最后接全局平均池化和线性分类器输出动作类别概率。3. 把 python 源码跑通环境配置、数据整理与第一次训练原理立住之后这个项目就变成纯工程问题了。源码包解压之后会看到什么、依赖怎么装、数据怎么喂进去、训练命令长什么样我按实际操作的顺序写。3.1 拿到代码先别训练目录结构、依赖版本和 python 环境配置解压 zip 之后常见源码包的目录结构大概长这样st-gcn/ ├── config/ # 训练与测试的 yaml 配置 ├── graph/ # 人体图结构定义ntu / kinetics ├── nets/ # st_gcn.py 网络定义 ├── processor/ # 数据加载、训练循环、识别入口 ├── main.py # 命令行入口 ├── data/ # 预处理后的 npy 数据 └── pretrained/ # 预训练权重可能为空动手跑之前先把 python 环境配好。网上 python 安装教程很多核心要点只有两条用 conda 单独建一个虚拟环境别动系统自带的解释器torch 的版本必须和本机 CUDA 版本对得上否则程序能跑但显卡不干活。配环境时 vscode 或 pycharm 都行选解释器时认准刚才创建的那个环境。# 用 conda 建一个干净环境python 3.8~3.10 都可以 conda create -n stgcn python3.9 conda activate stgcn # 装 torch按你自己的 CUDA 版本选择 cu118 / cu121 等 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 其余依赖 pip install numpy opencv-python pyyaml tqdm这里有个常见的坑有人直接pip install cv2必报错。opencv 的包名是opencv-python导入名才是cv2。装完之后在终端里敲一句验证python -c import torch, cv2, numpy; print(torch.__version__, cv2.__version__)能打印出版本号这张环境就过了。只有 CPU 的机器也能跑通流程只是训练速度慢 10 倍以上毕业设计阶段建议至少找一块 8G 显存的显卡。3.2 骨骼数据是怎么进模型的从 NTU 命名规则到 npy 数组NTU RGBD 原始数据是一堆.skeleton文本文件文件名带编码规则。比如S001C001P001R001A001.skeleton表示第 1 个被拍摄者、第 1 台相机、第 1 个动作类别。S是 subject人物C是 camera视角P是 performer演员编号R是 replication重复次数A是 action动作类别。这个命名规则很重要cross-subject 划分按S分cross-view 划分按C分代码里解析文件名用的就是这个规则。skeleton 文本格式也不复杂文件头写总帧数之后每一帧先写该帧关节数然后每个关节一行数字前三个字段是 x、y、z 坐标。我一般建议自己写一遍解析函数后面换数据集时就不用依赖别人的预处理脚本了import numpy as np def parse_skeleton(path): 把 NTU 的 skeleton 文本文件解析成 (T, V, 3) 数组 with open(path, r) as f: lines [line.strip() for line in f.readlines()] idx 0 num_frames int(lines[idx]) idx 1 frames [] for _ in range(num_frames): num_joints int(lines[idx]) idx 1 joints [] for _ in range(num_joints): parts lines[idx].split( ) # 前 3 个是 x, y, z其余是颜色、深度信息这里丢弃 joints.append([float(parts[0]), float(parts[1]), float(parts[2])]) idx 1 frames.append(joints) return np.array(frames) # 形状 (T, V, 3)这段代码只用了 python 基础语法逻辑很清楚先读帧数再循环读每一帧。注意split( )按空格切分NTU 文件里字段之间恰好是一个空格如果从别的数据集复制数据来要先确认分隔符是空格还是逗号。解析完成后源码包里的 processor 会把 (T, V, 3) 转置成 (3, T, V) 输入网络再按人物编号划分训练集和测试集存成 npy 加速后续加载。3.3 训练与验证配置文件参数表、终端命令和日志判断源码包的 config 目录下会按数据集和划分方式放几个 yaml 文件训练之前先把参数过一遍。需要改的核心参数就这些参数常见取值作用lr0.1SGD初始学习率ST-GCN 配 SGD 比 Adam 稳momentum0.9SGD 动量配合 nesterov 加速收敛weight_decay0.0001L2 正则防过拟合nesterovtrue打开 Nesterov 动量batch_size64显存 8G 建议 32~64显存小就调低num_epoch80完整训练轮数小数据集 50 轮可先看趋势temporal_kernel_size9时间卷积核大小NTU 场景推荐 9gpus0使用哪张卡多卡写 0,1配置文件确认之后训练就一条命令# 训练 python main.py --config config/st_gcn/nturgbd-cross-view/train.yaml # 用训练好的权重做测试 python main.py --config config/st_gcn/nturgbd-cross-view/test.yaml \ --weights pretrained/checkpoint.pt训练日志每个 batch 会打印一次 loss。前 5 个 epoch loss 应该从初始值一般在 3~4快速掉到 1.5 以下到第 20 个 epoch 左右开始在 0.1~0.3 区间震荡这是正常现象。如果 50 个 epoch 了 loss 还在 1.0 以上不下来先怀疑学习率和数据预处理别急着调网络结构。单卡 1080Ti 级别训练 NTU 60 的 cross-view 划分80 个 epoch 大概 5~8 小时中途中断了不用担心checkpoint 每 10 个 epoch 存一次完整体验一把“后悔药”。4. 避坑记录ST-GCN 的五个翻车现场、原因与解决这一章全部是实际操作里的血泪经验。每一条我都按“现象 → 原因 → 解决”的线索写照着排查比翻 issue 快得多。4.1 loss 变成 NaN或者训着训着直接崩掉现象训练到某个 epoch终端突然打出一堆nan然后 loss 永远回不来。或者从第一个 epoch 就是 NaN但日志前面完全正常。原因最常见的是学习率太激进。ST-GCN 用 SGD 加 nesterov 动量时lr0.1 在权重初始化不理想的情况下确实会出现梯度爆炸。另一个隐蔽原因是数据里有无效值比如 NTU 数据集中骨骼缺失时某些坐标会被填成 0如果你用的预处理脚本没有做 mask网络会把缺失点当作真实坐标参与图卷积特征里混入超大异常值。解决先查数据把加载后的数组跑一遍np.isnan(data).any()和np.isfinite(data).all()确认输入干净。数据没问题再降学习率lr 从 0.01 起步前 20 个 epoch 用 warmup 逐步升到 0.1或者干脆全程用 0.01 训练牺牲一点收敛速度换稳定性。顺便把梯度裁剪打开torch.nn.utils.clip_grad_norm_(model.parameters(), 5)一行代码的事能挡住绝大多数梯度爆炸。4.2 显存 OOMbatch_size 怎么调都不行现象显存 8G 的卡batch_size 设 64跑到第 3 个 epoch 直接CUDA out of memory。改成 16 之后勉强能跑但训练速度肉眼可见地变慢。原因很多人以为 ST-GCN 输入很小25 个点显存一定省。实际上时间维卷积核是 9中间特征图是 (B, 256, T, V)T 在 300 帧时单张特征图就有 200 万左右个元素全连接之前还要做全局池化显存消耗大头全在中间特征上不在输入。解决先把 batch_size 降到 32 确认能跑通再从数据端省显存。NTU 原始数据每段序列 300 帧很多样本的动作主体在前 150 帧就结束了可以把序列长度先裁到 150 帧预热训练稳定后再恢复到完整长度。另外数据加载默认每个 step 从磁盘读 npyIO 会把 CPU 占满建议预处理时把训练集一次性载入内存虽然吃内存但训练速度能快三分之一以上。4.3 验证集精度比论文低十几个点现象完整训完 80 个 epoch验证集 top-1 准确率只有 75% 左右而论文表里写的是接近 90%差距大到让人怀疑代码是不是有 bug。原因这个落差九成不是 bug而是你只跑了一个 stream。ST-GCN 论文里 89% 以上的精度是四个 stream 集成出来的——关节坐标joint、骨骼向量bone、关节运动joint motion、骨骼运动bone motion每个 stream 单独训练最后把 softmax 分数相加。你单独跑 joint streamcross-view 也就 82% 左右cross-subject 大概 78%这是正常的单流 baseline不是模型坏了。解决先确认自己复现的是单流 baseline 还是四流集成。如果是毕设建议把四个 stream 全训出来再做分数融合融合代码在 processor 里一般都有现成实现没有的话自己写也很简单加载四个权重分别输出每个类别的概率取平均后 argmax。这是整个项目里性价比最高的一次精度提升。4.4 换了一个验证集模型精度直接崩到不可用现象训练日志显示 acc 稳定在 85% 以上换一个划分方式比如把 cross-view 换成 cross-subject重新测直接掉到 20%跟随机猜差不多。原因数据划分泄漏了。cross-subject 和 cross-view 的文件名解析逻辑不一样如果你在 config 里选了 cross-view 的划分文件但训练时不小心用了 cross-subject 的预处理结果或者测试集里混入了训练集里同一个人的样本模型学到的就是“认人”而不是“认动作”。这种泄漏很隐蔽因为 loss 曲线完全正常acc 还特别好看——好看到不正常。解决训练前先打印训练集和测试集的文件名前缀确认同一 subject 或同一 camera 的样本只出现在一边。NTU 官方划分是枚举出人的编号列表代码里通常写死了一组常量换成自己的数据时一定要自己检查拆分逻辑不要直接复用别人的train.txt/val.txt。4.5 自定义数据集上完全乱来换 18 点骨架就崩现象不用 NTU改用 OpenPose 输出的 18 点骨架之后网络训练 loss 能降但识别结果几乎没有语义几个类别来回误判。原因关节编号对不上。NTU 的 25 点拓扑和 COCO 的 18 点拓扑不是简单删几个关节的关系NTU 里有髋中心、脊柱、颈部这些中轴点COCO 里没有独立的髋中心左右髋直接连到骨盆中点。直接把 18 点数据塞进为 25 点定义的图里邻接矩阵从第一层就指错边消息传递的物理含义全乱了。解决换数据集时graph 目录下对应数据集的邻接矩阵定义要重写。最稳的做法是写一个脚本把新骨架点位映射到 NTU 的关节索引体系映射不了的关节坐标置 0 并在图里断开连接。不要怕麻烦这一步不做好后面所有调参都是在黑匣子里瞎猜。5. 先画出来再谈训练可视化骨架与预测结果的验证习惯最后一章分享一个我每次拿到新骨骼数据都先做的事把骨架画回视频帧上用眼睛确认数据没毛病再谈训练。可视化代码不复杂核心是用 cv2 画点和连线。关键点坐标是归一化到 [0, 1] 区间的画之前要乘回图像的宽高import cv2 import numpy as np def draw_skeleton(frame, keypoints, edges): keypoints: (V, 2) 的归一化坐标 edges: [(i, j), ...] 骨骼连接列表 h, w frame.shape[:2] for (i, j) in edges: x1, y1 int(keypoints[i, 0] * w), int(keypoints[i, 1] * h) x2, y2 int(keypoints[j, 0] * w), int(keypoints[j, 1] * h) cv2.line(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) for (x, y) in keypoints: cx, cy int(x * w), int(y * h) cv2.circle(frame, (cx, cy), 4, (0, 0, 255), -1) return frame这段代码里的edges就是你在 graph 目录里定义的那份邻接列表。把第 2 章讲过的边数据直接拿过来用可视化出来的骨架如果和视频里的人物对不上比如左手连到了右肩、腿画到了天上那一定是关节索引映射错了趁早回头改图定义如果对得上再开始训练。预测结果也要可视化。推理的时候把 top-5 类别和置信度画在图像左上角和原视频逐帧合成一个 demo 视频。这一步看着简单但它能暴露一个很隐晦的问题模型可能学到了场景特征而不是动作特征比如只要画面里出现某种背景就输出“跑步”画出来一眼就能发现。我自己的习惯是预处理脚本、图定义、可视化脚本这三样永远放在训练之前写好。可视化脚本不用多精致哪怕只是把 10 帧连起来存成 gif 都行但它能在你投入几小时训练之前拦住数据错误这比任何调参技巧都省钱。ST-GCN 这个方向本身不难难的是养成“先验证输入再相信输出”的工程习惯。希望帮到你。本文还有配套的精品资源点击获取
