ST-GCN骨骼动作识别:基于PyTorch的图卷积实现与训练避坑指南
2026/9/24 22:45:48 网站建设 项目流程

简介:一套基于时空图卷积(ST-GCN)的骨骼动作识别毕业设计项目,面向计算机视觉方向学生与研究者,覆盖从模型原理、代码实现到项目文档的完整闭环。资源以Python源码为主,包含29个py脚本、13个yaml配置、3个pt预训练模型、9个txt说明等共91个文件,压缩包约52.54MB,清晰划分tools、processor、feeder、models等模块,并附有离线/实时演示与多种骨架数据预处理脚本,便于二次开发。已有175人学习下载。项目采用图卷积建模人体骨骼点时空关系,端到端自动学习动作特征,解决了传统手工特征提取耗时且依赖专家经验的问题。除完整训练好的ST-GCN及改进模型外,还提供项目文档、样例动图与视频,可帮助快速复现NTU-RGB-D等数据集上的骨骼动作识别实验,适合作为课程设计、毕业设计或入门时空图卷积的实践参考。

1. 拿到这个python毕业设计标题,先看它在解决什么问题

做动作识别,多数人第一反应是拿视频帧喂给卷积神经网络。但这个python毕业设计标题里明确写的是骨骼动作识别,输入不是RGB图像,而是一串人体关键点坐标。你不需要关心穿什么衣服、背景多乱、光照好不好,只看骨架的运动轨迹就能区分“挥手”和“走路”。

ST-GCN是这条路线上最有代表性的模型之一。它把骨骼关键点看成一张图,关节是节点,骨骼是边,再用图卷积提取空间特征,用时间卷积捕捉动作时序。作为毕业设计,它的工程量适中、可视化效果好、数据集公开,而且代码框架清晰,适合在PyTorch上一步步跑通。本文按“原理拆解 → 数据预处理 → 训练推理 → 避坑 → 验证进阶”的顺序,把一套可复现的落地过程讲清楚。

2. ST-GCN的核心设计:为什么骨架图卷积能同时吃进空间和时间

做骨骼动作识别,常见做法有三种:把关键点坐标拼成一维向量丢给LSTM、把坐标矩阵当成图像丢给CNN、把骨骼图丢给图卷积网络。LSTM的问题在于它对空间结构不敏感,手和脚的位置在特征里是平的,没有拓扑关系。CNN又依赖固定尺寸的输入,骨架点数一换就得改网络结构。ST-GCN不一样,它把人体骨架看成图结构,用邻接矩阵定义关节间的连接关系,空间卷积在图上做,时间卷积在帧序列上做。

2.1 图卷积层到底做了什么:邻接矩阵、归一化、分区策略

图卷积的核心操作可以理解为“每个节点的特征,等于它自己和邻居节点特征的加权求和”。关键在两点:邻居怎么定义、权重怎么算。ST-GCN用的人体图,把关节按人体的自然连接关系连起来,比如左肘连着左肩、左腕连着左肘。A是邻接矩阵,I是单位矩阵,加上I是在做图卷积时把节点自身也算进聚合范围,不然一层卷完节点自己的信息反而丢了。

import torch import torch.nn as nn class GraphConv(nn.Module): def __init__(self, in_channels, out_channels, A, stride=1): super().__init__() self.A = nn.Parameter(A, requires_grad=False) # 固定的邻接矩阵,不参与训练 self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1) # 1x1卷积只在通道维度变换,不改变空间形状 def forward(self, x): # x: [B, C_in, T, V],B是batch,C是通道,T是帧数,V是节点数 B, C, T, V = x.size() # einsum: 按节点维度做图卷积聚合 x = torch.einsum('bctv,vw->bctw', x, self.A) return self.conv(x)

这里的einsum做了这样一件事:对每个节点t,把v维度上的所有邻居特征按A的权重累加,结果写到w上。A矩阵是VV的方阵,A[v][w]非零表示“关节v到关节w有边”。实际项目中不会直接用原始A,而是做归一化:D^{-1} A或对称归一化D^{-1/2} A D^{-1/2}。不归一化会出现一个问题——度数高的节点(比如躯干中心)聚合了大量邻居,特征数值膨胀,训练容易震荡。

ST-GCN原论文对邻居做了三种分区策略:节点自身是一类、比自身更靠近重心的邻居是一类、比自身更远离重心的邻居是第三类。一张邻接矩阵被拆成三张,分别做聚合再拼接。这样做是因为“手靠近躯干”和“手远离躯干”在动作语义上完全不同,一套权重算不明白。

2.2 时间卷积:在帧序列上滑动一维卷积

空间关系处理完,还要处理时间维。一段视频有几十帧,每帧都有V个节点坐标,时间维T就是帧数。ST-GCN在每个图卷积层之后跟一个时间卷积层,用kernel_size=9的一维卷积沿T方向滑动。9帧左右的窗口能覆盖一个基本动作单元。如果时间核太小,看不出动作的起承转合;拉得太大,参数暴涨还容易过拟合。

class STGCNBlock(nn.Module): def __init__(self, in_channels, out_channels, A, stride=1, temporal_kernel=9): super().__init__() self.gcn = GraphConv(in_channels, out_channels, A) self.tcn = nn.Sequential( nn.Conv2d(out_channels, out_channels, (temporal_kernel, 1), stride=(stride, 1), padding=((temporal_kernel - 1) // 2, 0)), nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True), ) def forward(self, x): return self.tcn(self.gcn(x))

temporal_kernel控制时间感受野,毕业设计直接用9就行,不需要调。stride用于在浅层到深层之间逐步压缩时间长度——前三层步长为1,先充分提取短程时序特征,从第四层开始步长变2,把时间维折半,扩大感受野。通道数跟着翻倍,从64到128再到256。整个网络结构可以理解为九个ST-GCNBlock串联,通道配置是[64, 64, 64, 128, 128, 128, 256, 256, 256],最后接全局平均池化加全连接分类头。

2.3 为什么选固定邻接矩阵而不是让它学习

有个容易被问到的细节:self.Arequires_grad=False,也就是说邻接矩阵在训练中不更新。原因很简单,骨骼连接关系是先验知识——肘关节不可能在训练中自己长到头部去。让它参与训练,反而可能学出不符合人体结构的连接,可解释性变差,在小数据集上容易过拟合。如果你做改进型毕设,常见做法是加入一个可学习的残差图A + α * BB初始化为零,让网络自己发现数据中的额外关联。这个改进很小,但答辩时能讲的东西多不少。

3. 把骨骼序列变成ST-GCN能吃的张量:数据组织与预处理

模型结构搞清楚之后,数据处理是最容易卡壳的地方。ST-GCN的输入是一个五维张量:[B, C, T, V, M],其中C是通道数,每个关节有x、y坐标(有的还有置信度,那就是3个通道),T是帧数,V是关键点数量,M是画面中的人数。处理数据集时最常见的困难就是你的原始标注和这个形状对不上。

3.1 认识两套主流数据格式:NTU坐标系与OpenPose输出

NTU RGB+D数据集每条样本包含25个关节点的三维坐标,同时提供.skeleton格式文件,用matlab或python解析成numpy数组都能用。Kinetics-skeleton是用OpenPose从视频里提取的二维坐标,18个关节点,格式是(C, T, V, M),其中C的3个通道分别是x、y、置信度。你要做毕业设计时,先把数据归一化成统一形状:[通道, 帧数, 关节数, 人数]

关节数量的差异不会让你改模型结构。ST-GCN的图卷积层依赖邻接矩阵A的形状,你用25个关键点就生成25×25的矩阵,用18个关键点就生成18×18。网络中间的图卷积层节点数在通道维度上操作,不改变V的个数,所以只要把A换成对应大小的矩阵,其他代码一行不用动。这也是图卷积相对CNN的一个天然优势。

import numpy as np def build_adjacency(num_nodes, edges): """根据骨骼连接关系生成邻接矩阵""" A = np.zeros((num_nodes, num_nodes), dtype=np.float32) for i, j in edges: A[i, j] = 1 A[j, i] = 1 # 自连接 A = A + np.eye(num_nodes, dtype=np.float32) # 对称归一化:防止度数高的节点特征过大 D = np.sum(A, axis=1) D_inv_sqrt = np.diag(np.power(D, -0.5)) A_norm = D_inv_sqrt @ A @ D_inv_sqrt return A_norm

edges是手工定义的关节连接列表,比如[(0, 1), (1, 2)]表示鼻子连脖子、脖子连右肩。这是唯一需要人工介入的部分,定义错了整个网络看到的人体结构就是错的。画图检查一遍再拿去训练,别凭感觉写。

3.2 时间对齐:不同长度的动作怎么统一成固定帧数

视频长度从几十帧到几百帧都有,但ST-GCN要求一个batch内的时间维一致。常见做法是采样固定帧数,比如统一采样到100帧或64帧。你可能会想直接 resize 或插值,但这会让动作速度快慢发生变化。更合理的做法是分段采样:先把整段序列等分成T段,每段随机抽一帧。这样既能统一长度,又保留动作节奏的稳定性,还能起到轻微的数据增强作用。

def temporal_sample(sequence, num_frames=100): """把任意长度的骨骼序列采样到固定帧数""" length = sequence.shape[0] if length >= num_frames: # 均匀分段,每段随机取一帧 indices = np.linspace(0, length, num_frames, endpoint=False).astype(int) return sequence[indices] else: # 序列太短就复制末帧补齐 pad = np.tile(sequence[-1:], (num_frames - length, 1, 1)) return np.concatenate([sequence, pad], axis=0)

训练时用随机采样,让每轮看到的时间点不完全一样,相当于一种时序增强;验证时用固定等间距采样,保证评估结果稳定可复现。如果你的数据大部分长度相近,把num_frames设成它们的均值附近就行,太大的话后面的时间卷积层感受野覆盖不了全局,太小则动作信息被截断。

3.3 数据增强怎么做才算对

骨骼序列的增强和图像不太一样,翻转、缩放、旋转都不能无脑套。左右翻转是安全的,因为动作语义基本不会因为镜像改变——“左手挥手”翻转后变成“右手挥手”,标签不变。但旋转就要小心,有些动作带有方向性,比如“投掷”从左上到右下,旋转后可能变成别的角度。实际项目中我一般只保留三个操作:随机旋转小角度(±15度以内)、随机缩放(0.9到1.1倍)、时间维随机裁剪。

一个容易被忽视的点是处理多人场景。训练时每个动作可能有多个人出现,输入张量的M维大于1。ST-GCN的处理方式是取置信度最高的那个人作为主要目标,其余人直接丢弃。在数据集里挑出每帧中得分最高的那个人的骨骼序列,简单有效。如果你做的是室外自采数据,这一步要写实一点,不然训练和推理时的输入分布不一致,模型表现会明显变差。

4. 用python把ST-GCN训练和推理落地:核心代码与参数设置

数据处理完毕,接下来就是训练模型。这一步的关键不是把代码跑通,而是理解训练脚本里每个部分为什么这样设计。很多同学的失败经验是:照着开源仓库把命令一敲,loss确实在降,但验证准确率上不去,从头到尾不知道为什么。

4.1 训练循环:损失函数、优化器、学习率调度

分类任务用交叉熵损失,这一点没有争议。优化器选择上,SGD搭配Nesterov动量在ST-GCN上表现比Adam稳定,见过的多份实验记录都显示SGD最终准确率更高。学习率初始0.1,配合余弦退火或每30轮降10倍都行。batch size在4到32之间,取多少取决于显存——ST-GCN输入的[T, V]本身不大,一般16就能跑得动。

逻辑说明:图卷积训练的长尾风险在于梯度消失,所以残差连接在每层之间都会加一遍。另一个细节是BatchNorm2d的参数统计在训练和推理时不一样,训练时用当前batch统计,推理时用滑动平均,所以net.eval()必须调用,否则验证结果会莫名波动。

model = build_stgcn(num_class=10, in_channels=3) optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9, nesterov=True) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) criterion = nn.CrossEntropyLoss() for epoch in range(total_epochs): model.train() for batch in dataloader: x, y = batch['skeleton'], batch['label'] x = x.cuda() y = y.long().cuda() out = model(x) loss = criterion(out, y) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 每个epoch结束后跑一次验证 validate(model, val_loader)

这里x的形状是[B, C, T, V, M]。如果你的数据集按[B, T, V, C]存,训练前要做一次permute,这个维度顺序错了网络也能跑,因为图卷积不强制检查输入维度,但结果一定不对。建议在数据加载器里固定输出维度顺序,并写一行断言在第一个epoch前检查。

4.2 从源码组织到跑通:文件结构怎么设计

一份能跑的毕业设计代码,至少要有dataset.pymodel.pytrain.pyinference.py四个文件,外加训练好的权重和一张结果可视化图。很多已有的开源仓库已经把逻辑写好了,但要跑通自己的数据,核心改动集中在dataset.py,因为每个数据集的解析方式都不同。

project/ ├── dataset.py # 数据加载、采样、增强 ├── model.py # ST-GCN网络定义(图卷积+时间卷积) ├── train.py # 训练循环 ├── inference.py # 加载权重做单样本预测 ├── weights/ │ └── best_model.pth # 训练好的模型权重 └── docs/ └── 项目文档.md # 毕业设计文档

inference.py有一个容易踩坑的地方。模型输出的out直接是softmax之前的值,要正确拿到类别,得先torch.softmaxargmax。有人会把argmax直接做在原始输出上,结果类别概率排名全乱了,因为最大logit并不等价于最高概率,尤其当类别数量多时。更隐蔽的问题是dropout和BatchNorm在推理时的影响,如果忘记model.eval(),每次推理结果会抖动,这个在项目文档里写清楚会加分。

4.3 训练到多少轮才够:早停与模型选择策略

骨架数据维度低,不像图像那样需要动辄200轮的训练。NTU RGB+D这种大数据集通常要训练上百轮,但如果你自己做的是小规模数据集,每类几百条样本,50轮以内基本就能收敛。我一般观察两条曲线:训练损失还在降、验证损失开始回升的拐点就是模型开始过拟合的信号。

best_acc = 0 for epoch in range(total_epochs): # ...训练... acc = validate(model, val_loader) if acc > best_acc: best_acc = acc torch.save(model.state_dict(), 'weights/best_model.pth') print(f"epoch {epoch} saved, acc={acc:.4f}")

多保存一个last_model.pth作为兜底,省得训练中断时连后悔药都没有。使用best_model.pth做最终评估,因为它是在验证集上选的模型,泛化能力相对有保障。至于是不是测试集也要评估一次,看你们学校的规范——有的导师要求只用验证集调参、测试集只能碰一次,这个要提前问清楚。

5. ST-GCN训练避坑:五个常见问题和排查顺序

图卷积网络在实现上比普通CNN多一点“玄学”成分。训练不起来或效果差,很多情况下不是模型结构写错了,而是数据处理和训练细节出了偏差。下面五条按出现频率排序,都是我实际排查过的坑,每条按“现象 → 原因 → 解决”列清楚。

5.1 损失不下降,准确率稳定在随机水平

现象:loss在前几个epoch里从3.0掉到2.5左右就再也不动,验证准确率一直在10%、20%之间像心电图一样晃。

原因排查顺序:先看输入数据归一化是否做了。骨骼坐标如果直接拿原始像素值,数值范围从0到720,图卷积的聚合会把数值放大,梯度一上来就乱掉。再看标签类别是否从0开始连续编号,CrossEntropyLoss要求标签是[0, num_classes-1]的整数,如果数据集的类别从1开始,loss前期就会乱。

解决:把骨架坐标除以视频宽高,归一化到[0, 1]区间,并检查y.unique()确认类别编号。如果归一化做了还不行,把学习率从0.1调到0.01,再看前几个batch的loss是不是在正常下降。

5.2 验证准确率比训练低一大截,且差距越来越大

现象:训练集准确率到了95%,验证集只有60%,而且每过一个epoch差距都会扩大。

原因:这是典型的过拟合。骨骼数据维度和信息量远小于图像,模型容量本来就不需要那么大。几个ST-GCNBlock的参数对单类几百条样本来说绰绰有余。加上数据增强基本只有翻转和缩放,模型很容易把训练样本的噪声细节记住。

解决:第一批要做的是把中间三层通道数从128压到64,减少参数量。然后是增加dropout,原论文在全连接层前加0.5的dropout,残差块内部也可以加0.2到0.3。最后一个技巧是时间维的随机采样增强,如果之前没做,补上这一步往往提升最明显。

5.3 显存不够,或者跑着跑着内存溢出

现象:batch size不敢开到16,一开就OOM;有时显存没爆,CPU内存先爆了。

原因:一个隐形雷在数据加载器里。如果dataloadernum_workers设得很大,而数据预处理全部在__getitem__里做,每个worker都会复制一份数据。骨骼数据本身不大,但如果你用OpenPose跑自采视频,骨骼点里面还带着每帧的置信度图或原始特征,内存就撑不住了。

解决:离线把数据全部预处理成numpy数组存盘,加载时只做切片和采样,不要再在__getitem__里算特征。batch_size=8对小规模数据集完全够,一个ST-GCN模型权重才几十MB,显存瓶颈远没有图像模型那么严重。

5.4 动作类别在混淆矩阵里永远分不开

现象:训练完看混淆矩阵,“挥手”和“招手”这类动作互相混淆,“坐下”和“蹲下”也经常错判。

原因:两个动作在前几帧空间结构完全一样,区别只在后续时序变化。ST-GCN的时间卷积窗口虽然覆盖了9帧,但如果数据预处理时把帧数采样得太稀疏,中间的动作过渡细节就丢了。另一个可能是这两个类别在数据集中本身标注不干净,采集时动作执行得模棱两可。

解决:把num_frames从64提高到128,时间卷积的感受野覆盖更多帧。更直接的方法是像2s-AGCN那样把骨骼的长度特征(即骨骼向量的方向和大小)作为第二路输入,分类结果会明显拉开差距。毕设阶段加一路特征,工作量不大,答辩时还能讲成是“双流特征融合”的改进。

5.5 推理时单条样本预测结果和训练时不一致

现象:训练时验证集准确率挺高,一加载训练好的权重做单条样本推理,同样的样本每次结果不同,甚至前一帧后一帧差别很大。

原因:大概率是model.eval()被漏掉了。BatchNorm在训练模式下用batch内的均值和方差,推理模式下用累积的全局统计量。忘了切换会让同一帧数据因为batch不同而产生不同的归一化结果。还有一处隐蔽的问题是数据预处理中的随机旋转和随机缩放没有关,推理时也在做增强。

解决:推理脚本里在加载模型后立即调用model.eval(),并确认数据增强在推理时不执行。建议代码里用一个全局变量is_train控制增强开关,别在图里写死。

6. 模型诊断与可视化:在验证集上找到证据而不是猜

训练完成不等于项目结束。毕业设计答辩时最能拉开差距的部分,是把模型“为什么能工作”用可视化的方式讲出来。ST-GCN在这方面的优势很明显:它本身就有图结构,把注意力权重可视化出来,比图片模型画热力图更直观。

先做一个最基础的骨架动作可视化。把输入序列和预测结果画成逐帧的关节点折线图,每帧之间用连线表示骨骼关系,帧序号标在下方,缓慢播放或导出成GIF。这一步的代码量很小,但已经能让评审知道你做的不是黑匣子。

import matplotlib.pyplot as plt def plot_skeleton_sequence(sequence, joints, save_path=None): """sequence: [T, V, 2],把多帧骨骼轨迹画在一张图里显示时空变化""" for t in range(sequence.shape[0]): plt.clf() frame = sequence[t] for i, j in joints: plt.plot([frame[i, 0], frame[j, 0]], [frame[i, 1], frame[j, 1]], 'b-', linewidth=1.5) plt.scatter(frame[:, 0], frame[:, 1], c='red', s=8) plt.xlim([-0.1, 1.1]) plt.ylim([-0.1, 1.1]) plt.title(f'frame {t}') if save_path: plt.savefig(f"{save_path}/frame_{t:03d}.png", dpi=100)

再说混淆矩阵的可视化。训练完保存所有验证样本的预测类别和真实类别,画一张混淆矩阵热力图,重点检查对角线上哪个位置颜色偏淡。比如“站立”类别被大量预测成“行走”,说明模型对双腿配合的时序特征区分能力弱,这时候可以去数据层面找原因——是不是这两类在采集时的动作幅度太接近?或者标注本来就不清晰?有了混淆矩阵,你可以把问题定位到“某两个类别之间的判别性不足”,而不是笼统地调参。

最后的进阶方向是双流融合。把关节坐标作为第一路输入,骨骼向量(相邻关节坐标差)作为第二路输入,两个分支各自过一套ST-GCN,最后的特征拼接后送分类头。这个改进的代码量不大,但通常能提升3到5个百分点的准确率,而且在文档里可以单独拎出来写一章“模型改进”,对毕业设计来说性价比极高。动手前先确认原版单流模型复现成功,保存好权重做一个基线,再动新结构——有基线才能证明你的改进确实有效。这个习惯我带过很多项目都验证过:先把基线版本完整跑通、留下记录,再开始“改良”,项目返工率能低很多,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询