
简介这是一份面向毕业设计、期末大作业及课程设计的Python实战项目基于时空图卷积网络ST-GCN实现骨骼动作识别。项目代码包含详细注释从数据处理、模型构建到训练推理均有清晰呈现适合具备一定Python基础、希望快速上手深度学习动作识别方向的新手学习者参考。资源包共90个文件以29个Python脚本为核心涵盖模型定义、特征提取、训练与演示流程同时包含13个YAML配置文件、预训练模型pt、演示视频mp4与GIF动图以及说明文档md/txt等便于理解项目结构与复现效果。压缩包大小约52.61MB部署简单下载后按照文档指引即可运行。目前已有189人学习下载认可度较高。项目作者自述获得98分并受到导师认可代码组织规范包含离线演示、实时演示及多种骨骼数据处理工具不仅适合直接作为毕设答辨展示也可作为学习ST-GCN原理与PyTorch工程实践的完整范例。1. 基于 ST-GCN 的骨骼动作识别一个能跑通、能改、能讲清原理的毕设项目骨骼动作识别这几年在毕业设计里算是热度很高的一类选题核心原因不是它比图像识别更简单而是它的技术链路足够完整OpenPose 拿骨架、图卷积做时空建模、NTU-RGB-D 或 Kinetics 做训练验证一套走下来算法深度、工程落地、论文图表全都有了。这份基于时空图卷积 ST-GCN 的 Python 源码项目属于典型的「实验室代码风格 全流程可运行」的毕业设计资源压缩包里除了模型代码和训练脚本还直接给了三个训练好的权重文件拿到手不用从头训就能先跑通 demo这对答辩前时间紧张的学生来说非常关键。下面我按自己拆项目的习惯把这套代码从数据入口到模型推理完整过一遍顺带把最容易踩的坑提前指出来。2. ST-GCN 的原理值得自己捋一遍图卷积为什么能用在骨骼序列上2.1 骨骼数据不是图像用 CNN 硬套会丢掉拓扑结构图像分类用 CNN是因为像素在二维网格上有天然的局部相关性卷积核扫过去就是在做空间特征提取。但骨骼数据不一样——人体骨架是 25 个关键点NTU 是 25Kinetics 用的是 18 个这些点通过骨骼连接构成一张图关节之间并没有规则的网格关系。把骨架坐标排成矩阵塞进 CNN等于强行把图结构拉平成欧几里得结构关节之间的连接关系就丢了。ST-GCN 的核心思路是把骨骼序列建模成时空图空间维度上关节是节点骨骼是边时间维度上同一个关节在相邻帧之间的连线构成时间边。图卷积在这张时空图上做特征聚合既能学到单帧内关节的空间共现关系比如挥手时手腕和手肘的联动也能学到跨帧的时序动态比如下蹲时膝盖角度随时间的变化。这套思路最早由 AAAI 2018 的论文《Spatial Temporal Graph Convolutional Networks for Skeleton-Based Action Recognition》提出后续的很多改进模型都是在它的框架上加注意力、加多流、改分区策略。项目的net/st_gcn.py里实现的就是这个基础结构。我先看它的核心类STGCNclass STGCN(nn.Module): def __init__(self, in_channels, num_class, graph_args, edge_importance_weightingTrue, **kwargs): super().__init__() self.graph Graph(**graph_args) A torch.tensor(self.graph.A, dtypetorch.float32, requires_gradFalse) self.register_buffer(A, A) # 9 层 ST-GCN 块通道数从 3 逐步扩到 256 self.data_bn nn.BatchNorm1d(in_channels * A.size(1)) self.st_gcn_networks nn.ModuleList(( st_gcn_block(in_channels, 64, self.A, 1, residualFalse), st_gcn_block(64, 64, self.A, 1), st_gcn_block(64, 64, self.A, 1), st_gcn_block(64, 128, self.A, 2), st_gcn_block(128, 128, self.A, 1), st_gcn_block(128, 128, self.A, 1), st_gcn_block(128, 256, self.A, 2), st_gcn_block(256, 256, self.A, 1), st_gcn_block(256, 256, self.A, 1), )) self.fc nn.Linear(256, num_class)这段代码里有个参数很关键edge_importance_weightingTrue时每条骨骼边会有一个可学习的权重参数模型在训练过程中会自动调节不同骨骼对动作判别的重要性——长跑时腿部骨骼权重会被拉高弹钢琴时手指骨骼权重会被拉高。这个机制在论文里被称为 edge importance weighting是 ST-GCN 比早期图卷积方法效果好的重要原因很多二区论文的改进点也都挂在这个机制上。2.2 邻接矩阵和分区策略图卷积的数学细节图卷积和普通卷积的差别在于聚合方式。普通卷积用固定大小的卷积核扫过像素邻域图卷积用邻接矩阵 A 定义邻居关系再用度矩阵 D 做归一化。ST-GCN 的Graph类里A不是一张朴素的 0/1 邻接矩阵而是基于分区策略拆成三张子矩阵# 源码里 Graph 类的结构st_gcn.py 的 Graph 类注释中体现 # A.shape (num_subsets, num_joints, num_joints) # num_subsets 3 表示三种分区分别对应 # 0: 关节自身自连接 # 1: 比中心关节点更靠近骨架重心的邻居 # 2: 比中心关节点更远离骨架重心的邻居这被称为 spatial configuration partitioning是 ST-GCN 原文提出的分区方案。模型在每一层图卷积里分别用这三张子矩阵做聚合再把结果拼接相当于同时学习了「离心运动」「向心运动」「静止」三种身体部位的协同特征。Graph类的get_hop_distance和normalize_digraph负责构建这个矩阵。normalize_digraph里的归一化公式是 D^(-1)A也就是对每个节点把所有邻居的贡献按度数取平均而不是像 GCN 原文那样用 D^(-1/2)AD^(-1/2)。这个差异直接影响了梯度传播的稳定性在训练时如果发现 loss 波动特别大可以先检查是不是 A 的归一化方式被改动了。项目里还额外提供了一个改进版权重AddEdgeSTGCN12345.pt从命名可以推测是针对原始 ST-GCN 的邻接矩阵做了边权重叠加的变体——这个在后面第 4 章训练部分我会详细讲它的实现差异。3. 把原始骨骼数据变成模型能吃的张量数据预处理链路的三个关键环节3.1 从 OpenPose 的 JSON 到 Kinetics 格式的 numpy 数组这套源码支持两个数据集NTU-RGB-D 和 Kinetics-skeleton。前者的数据是 Kinect 采集的 3D 骨骼坐标25 个关节点后者是 OpenPose 从视频里提取的 2D 骨骼18 个关节点来自 COCO 关键点格式。两种数据的组织方式不一样但处理后进入模型的格式是统一的(N, C, T, V, M)。kinetics_gendata.py是 Kinetics 数据集的处理脚本。Kinetics-skeleton 的原始标注是以 JSON 行为单位存储的每行一个样本包含frame_index、skeleton、label等字段。skeleton里是每一帧每个人的 18 个关节点坐标和置信度。处理脚本做的事情是读 JSON 行 → 按帧序排列 → 补齐到固定帧数 → 存成.npy特征文件和.pkl标签文件。# 处理 NTU-RGB-D 数据生成训练/验证所需的 npy 文件 python ntu_gendata.py --data_path /path/to/ntu/raw \ --out_folder /path/to/ntu/processed \ --ignored_sample_path ntu_ignored_samples.txtntu_gendata.py的参数含义--data_path指向 NTU 原始.skeleton文件目录--out_folder是输出目录会生成train_data.npy、train_label.pkl、val_data.npy、val_label.pkl--ignored_sample_path是论文作者提供的缺失帧样本列表这些样本在预处理时直接跳过否则会拖低精度。处理后的特征布局是数据维度: (N, C, T, V, M) C 3 # 坐标通道x, y, 置信度NTU 还有 z T 300 # 时序帧数NTU 默认 300 帧 V 25 # NTU 关节数 M 2 # 最多人数注意这里的M2因为 NTU 样本里最多同时出现两个人——不只是单人动作还有交互动作握手、拥抱、推搡。如果只看单人动作的准确率会虚高答辩时被问到多人的情况就容易翻车。3.2 帧对齐和 padding为什么不能把所有样本硬截断到固定长度动作识别里最头疼的问题就是样本长度不齐。挥手的视频可能只有 20 帧打太极的样本可能有 300 帧。粗暴的做法是全部 resize 到固定长度但这样会破坏动作的节奏信息——你无法判断一个 50 帧的动作是被加速了还是本来就这么快。feeder/feeder.py的策略是按比例随机采样先设定总帧数T比如 300然后按random.choice从原序列里采样帧索引。具体逻辑是# feeder/feeder.py 中帧采样逻辑的核心简化 # 设原始帧数为 N目标帧数为 T # 如果 N T: 从 N 帧中等间隔地抽取 T 帧 # 如果 N T: 先均匀重复补齐到 T 帧再做随机偏移# feeder.py 中 __getitem__ 的伪码逻辑与实际源码结构一致 def __getitem__(self, index): data_numpy, label, valid self._load_data(index) if self.input_size 1: # 多帧采样 # 随机选择起始帧保证时序增强 start random.randint(0, max(0, data_numpy.shape[0] - self.input_size)) data_numpy data_numpy[start:start self.input_size] # 对坐标做标准化减均值除标准差按关节分别算 # 返回 (C, T, V, M) 形状的张量这段代码体现的设计思想是训练时用随机裁剪做时序增强让模型对不同起始位置的动作都鲁棒测试时则用固定居中裁剪保证同一段视频每次推理结果一致。我在复现时把input_size从 300 改成 150训练速度快了一倍精度只掉了 1 到 2 个点——如果你的显卡是 6GB 显存这个参数可以优先调。3.3 关键点顺序的映射一个不显眼但致命的细节NTU 的 25 个关节点顺序和 Kinetics 的 18 个关节点顺序完全不同。比如 NTU 的第 0 号是脊柱底部而 COCO 的第 0 号是鼻子。如果你直接拿 Kinetics 预训练权重去 fine-tune NTU 数据第一层就学歪了因为Graph类的self.A是根据关节连接关系构建的关节索引一旦错位邻接矩阵连接的就是完全不同的两个部位。源码的Graph类里内置了NTU和Kinetics两组关节连接规则通过graph_args里的layout参数切换。我在第一次跑通实验时就是在这里翻的车layoutntu-rgb-d和layoutopenpose写反了训练了 20 个 epoch 的精度只有 30%腰都悔断了。后续排查时才发现是关节拓扑搭错相当于用错误的先验图结构去初始化模型。4. 训练配置和双流变体从默认参数到针对性调优4.1 torchlight 训练框架的配置结构这套代码用的是自带的torchlight轻量训练框架配置文件在config/目录下。以st_gcn.twostream的 yaml 为例# config/st_gcn.twostream/st_gcn.yaml 核心配置结构示意 model: name: st_gcn_twostream in_channels: 3 num_class: 60 # NTU-RGB-D 60 类 edge_importance_weighting: True graph_args: layout: ntu-rgb-d # 关节拓扑用 NTU 还是 OpenPose strategy: spatial # 分区策略spatial 是论文里的三分类法 train: optimizer: SGD base_lr: 0.1 # 初始学习率 step: [30, 40] # 在第 30 和 40 个 epoch 衰减学习率 weight_decay: 0.0001 device: [0] # 用第 0 张 GPU test: checkpoint: ./work_dir/recognition/ntu_xsub/twostream/epoch39_model.ptbase_lr: 0.1配合 SGD 是 ST-GCN 原文的默认配置实测在 RTX 3060 上训练 NTU 的 cross-subject 划分60 类动作大概 4 个小时能收敛到 84% 左右的 top-1 准确率。如果你想换 AdamW学习率记得调到 0.001 量级否则前几个 epoch 的 loss 会冲上天。step: [30, 40]表示在训练到第 30 和 40 个 epoch 时学习率乘以 0.1这是原文的阶梯衰减策略不建议改得太激进骨骼数据本身噪声大学习率掉太快会把模型钉在局部最优出不来。启动训练的命令在源码的 README 里有典型的是# 训练双流 ST-GCN骨骼 骨骼运动速度特征 python main.py --config config/st_gcn.twostream/st_gcn.yaml \ --phase train \ --work-dir ./work_dir/recognition/ntu_xsub/twostream--phase支持train、test、demo三个阶段--work-dir存放 checkpoint 和训练日志。日志文件是 txt 格式每行包含 epoch、loss、top1、top5我一般会把这些数据拉出来画 loss 曲线用来判断模型是不是过拟合了。4.2 双流模型把关节坐标和骨骼速度分开建模项目里有st_gcn.py单流和st_gcn_twostream.py双流两个模型。单流输入的是关节坐标(x, y)或者(x, y, z)双流把输入拆成两个分支——第一流输入关节坐标第二流输入骨骼向量相邻两个关节坐标的差(x2-x1, y2-y1)它代表骨骼的长度和方向。两流各自跑一个 ST-GCN最后把特征拼接后过全连接层。# st_gcn_twostream.py 的 forward 核心逻辑 def forward(self, x): # x: 原始骨骼坐标序列 (N, C, T, V, M) # 第一流直接用坐标做空间图卷积 out1 self.st_gcn1(x) # 第二流先对关节做差分计算骨骼特征 x2 x[:, :, 1:, :, :] - x[:, :, :-1, :, :] # 帧间骨骼差分 out2 self.st_gcn2(x2) # 两流特征拼接或加权融合 out torch.cat((out1, out2), dim1) return self.fc(out)双流结构的动机很直观关节坐标丢失了肢体长度信息手臂长度、腿长这些身体结构比例而骨骼向量天然包含这些静力学特征。对人体的动作识别来说「手伸得多远」有时候比「手在哪里」更有区分度。项目自带的AddEdgeSTGCN12345.pt权重实测是在单流基础上改的在Graph类的A矩阵上叠加了一个可学习的边权重矩阵让模型在训练中自动调节骨骼连接强度——这个改进思路在论文里通常叫 adjacency matrix learning实现对骨骼结构未知扰动的自适应甜点是代码改动非常小在st_gcn.py里加一个self.edge_weights nn.Parameter(torch.ones(A.size()))就够了。4.3 推理 demo 和权重文件的使用边界压缩包里给了三个.pt权重文件OriginSTGCN.pt原版单流NTU 上训练的、AddEdgeSTGCN12345.pt改进边权重的单流、kinetics-st_gcn.ptKinetics-skeleton 上训练的适合跑通 demo 但不适合直接用于 NTU 评估。Kinetics 那个权重的类别数是 400如果你拿它加载 NTU 的 60 类模型文件nn.Linear层的大小对不上会直接报错。这不是代码 bug是类别数不匹配换对应权重就行。离线推理命令在processor/demo_offline.py里读取的是 OpenPose 导出的 JSON 骨骼序列。实时推理走demo_realtime.py用摄像头实时抽帧、跑 OpenPose 再进 ST-GCN帧率取决于 GPU 和 OpenPose 的检测速度一般 10 到 15 FPS 是正常的——比这低不是模型的问题是 OpenPose 的耗时占了大头。5. 避坑排查我在复现时踩过的六个真坑5.1 权重加载报错size mismatch for fc.weight现象加载kinetics-st_gcn.pt到 NTU 模型时报维度错误。原因Kinetics 权重是 400 类动作NTU 是 60 类最后的全连接层权重形状不一致。源码里num_class参数是在模型初始化时写死的加载权重时不会自动适配。解决先确认你的任务类别数。如果只是跑 demo 看效果用kinetics-st_gcn.pt且把 yaml 里的num_class: 400改好如果要复现 NTU 的评估结果用OriginSTGCN.pt不要混用。5.2 用 Kinetics 权重初始化 NTU 模型时出现 NaN现象加载预训练权重后前几个 batch loss 直接变成 NaN。原因两类数据的通道数可能不一致。Kinetics 是 2D 坐标(x, y, confidence)三通道NTU 有 3D 坐标加置信度四通道如果in_channels没对上BatchNorm 的均值方差缓冲区和输入不匹配。解决检查 yaml 里in_channels是否和数据集一致。NTU 用 3坐标加置信度还是 4加 z看具体预处理脚本源码的ntu_gendata.py默认输出 3 通道。5.3 demo 结果全是一个类别但训练精度正常现象离线 demo 对所有测试视频输出同一个类别比如全是「挥手」。原因测试时数据预处理和训练时不一致。训练用的feeder做了随机裁剪和标准化demo 里可能漏了标准化步骤或者标准化用的均值和标准差是训练集的但 demo 代码里加载的是默认值。解决翻processor/demo_offline.py确认它调用了feeder的同一套标准化逻辑。不能直接对原始像素坐标做推理必须先做减均值除标准差否则模型看到的输入分布和训练时完全不在一个量级。5.4 训练 loss 降不下去一直在 4 到 5 附近现象SGD 跑了 10 个 epochloss 还在 4.5 上下浮动。原因大概率是学习率策略不对或者 BatchNorm 的 momentum 太小。ST-GCN 原文用的是momentum0.9的 BatchNorm但很多复现代码会顺手改成 PyTorch 默认的0.1直接导致训练不稳定。解决在st_gcn_block里显式传入bn_momentum0.9或者把优化器从 SGD 换成 Adam 并把学习率降到0.001再试。还有一个可能原因是edge_importance_weighting权重初始化过大把它换成nn.init.normal_(self.edge_weights, mean0, std0.1)能缓解。5.5 OpenPose 检测不到人demo 直接中断现象视频里一个人都没有时demo_offline.py报错退出。原因OpenPose 对模糊、遮挡、极端姿态的场景漏检率很高源码里的 demo 没有加空检测保护。解决自己加一行判断——检测到的人数为 0 时跳过当前帧用全零的骨架占位继续跑保证时序长度对齐。这是工程上很常见的「脏数据处理」逻辑很多开源 demo 并不会帮你做。5.6 双流模型比单流精度还低现象st_gcn_twostream.py在 NTU 上比单流低了 0.5 到 1 个点。原因双流模型的第二流输入是骨骼差分但这路输入的数值范围比原始坐标小很多差分值接近于零如果共享了同一套标准化参数小的数值会被当成噪声直接压掉。解决给第二流单独做标准化或者把两流的 loss 分开算再加权求和比如主流 loss 权重 0.7骨骼流 0.3。我实际测试下来分开标准化后双流的收益才出来大约能比单流高 1.5 个点。6. 验证模型的完整顺序从骨架可视化到端到端评测6.1 权重自检用自己的骨架数据做冒烟测试我拿到这套项目的第一个动作不是直接开训练而是先跑一遍 demo 验证环境。在项目的resource/demo_asset/media目录下有自带的测试骨架序列先确认模型能不能跑通、类别输出是否合理python main.py --config config/st_gcn.twostream/st_gcn.yaml \ --phase demo \ --weights ./models/AddEdgeSTGCN12345.pt如果这一步能正常输出动作类别说明依赖环境和模型加载没问题。接下来把demo_realtime.py接上摄像头随便做几个动作试试。注意实时 demo 的输出是类别 ID不是类别名对照resource/kinetics_skeleton/kinetics-motion.txt里的 400 类标签映射表才能知道模型到底在说什么。6.2 用自己的小样本验证模型泛化能力用 OpenPose 提取自己录制的视频骨架存成和 Demo 一样的 JSON 格式然后喂给demo_offline.py。这里要重点观察的是模型在训练集外的表现如何。大部分情况下NTU 训练的模型对校园环境录制的视频效果会打折扣因为训练集里的拍摄视角是固定的而你录的视频可能更侧、更近。这是泛化能力问题不是代码问题——答辩时如果能主动说出「训练集视角固定导致的 domain gap」反而能让老师给你加分。6.3 一个实用的推理提速技巧把demo_realtime.py里的输入裁剪帧数从 300 降到 150用torch.no_grad()包住推理过程再将模型切到 eval 模式整体推理速度能提升 30% 到 40%。这套代码默认没有开半精度推理在支持 AMP 的 GPU 上可以加torch.cuda.amp.autocast()显存占用会进一步减少但对精度有约 1% 的影响需要权衡。从那以后我每次拿到新模型都会强制走一遍「单样本冒烟 → 小批量验证 → 全量评估」这条流程就算只是做个 demo 演示也不例外。这个方法帮我挡掉了不少答辩现场翻车的风险。希望帮到你。本文还有配套的精品资源点击获取