ST-GCN自适应图卷积人体动作识别系统解析
发布时间:2026/10/4 6:00:47 作者:尧图编辑部 阅读量:1,286

简介基于ST-GCN自适应图卷积网络的人体动作识别系统Python源码面向计算机、人工智能、大数据等相关专业的学生、教师与开发人员适用于动作识别算法研究、课程设计和初期项目演示。系统利用时空图卷积网络捕捉人体关键点随时间的动态变化代码结构完整且经测试运行通过并附有使用说明便于按文档掌握训练、推理与自定义数据流程。压缩包共476个文件、约43.69MB核心包含258个Python脚本、60个YAML配置文件另有演示GIF、MP4样例视频、TXT说明、模型数据等既可直接运行查看效果也可作为二次开发基础。资源已有284人学习下载作为毕设或大作业可通过文档快速搭建环境替换自有动作数据以开展针对性实验。下载解压后建议将目录重命名为英文字符串再运行避免路径问题。1. 这套ST-GCN自适应图卷积人体动作识别系统先别急着跑听我说完关键再动手我要拆的是一套基于ST-GCN即时空图卷积网络的人体动作识别系统源码它的核心不是去“看整张画面”而是先把人体的关节点提取出来连成一张图再沿时间维度做图卷积。这个思路最大的价值在于识别动作时模型只关心骨架拓扑和关节运动轨迹背景、光照、衣服颜色这些干扰直接被甩掉。适合三类人做毕设或课设的学生、刚开始接触图卷积的入门者、想基于动作识别做二次开发的从业者。我拆包后的第一感觉是这份代码本身能跑通但真正决定你能不能复现的反而不在网络结构而在数据对齐和几个不起眼的坑。下文我会把这套系统从原理到落地完整过一遍包括参数怎么设、哪里会翻车、以及怎么把它改成你自己的动作识别器。2. 从骨架序列到自适应图卷积模型的核心结构和选型理由2.1 为什么动作识别要用图卷积而不是普通卷积或LSTM人体动作识别的早期方案有两条路线一是把视频帧直接喂给2D/3D卷积网络让模型自己学时空特征二是把关键点坐标展平成向量扔给LSTM这类序列模型。这两条路线都有明显短板。普通卷积假设数据分布在规则的网格上但人体关键点天然是非欧几里得结构手腕和肩膀之间是骨骼连接关系不是像素邻域关系。强行把关节坐标伪造成网格等于把拓扑信息丢了。LSTM虽然能建模时间依赖但对关节之间的空间关系建模很弱它默认把所有关节特征平等地串成一个序列忽略了“手肘和手腕相连”这种强先验。ST-GCN的做法是把人体骨架建模为一张图节点是关节边是骨骼连接。以Kinetics数据集的常见设定为例一张骨架图包含18个关节点节点特征通常是三维坐标加置信度。整个视频序列的输入张量是[B, C, T, V, M]B是batch sizeC是特征通道数T是时间帧数V是关节点数M是帧内人数。图卷积在空间维度上聚合邻域关节的信息时间卷积再沿着T方向做特征融合两者叠加就构成了时空图卷积模块。这个设计的好处是模型的归纳偏置和人体结构一致参数效率比3D卷积高而且对背景变化完全鲁棒。2.2 空间图卷积和时间卷积是怎么叠成一个ST-GCN块的空间图卷积的核心公式是所有GCN变体的基础。设X为[N, C]的节点特征矩阵N是节点数A为原始邻接矩阵维度是[V, V]A[i][j]表示关节i和关节j之间是否有骨骼相连。为了在聚合时引入自身节点信息并做归一化通常会加上单位矩阵并做对称归一化A_tilde D^(-1/2) * (A I) * D^(-1/2)其中D是度矩阵I是单位矩阵。图卷积的输出为Y A_tilde * X * WW是可学习的特征变换矩阵。经过这一步每个关节的特征都聚合了它的相邻关节的信息。在实际实现中A_tilde通常被预计算并固化下来但ST-GCN还会在后来的版本里增加一个可学习的适配矩阵见2.3小节。空间卷积做完后张量维度从[B, C, V, M]变成[B, C_out, T_frames, V, M]这类形态接下来需要沿时间轴做卷积。常见做法是使用一个kernel size为9的一维卷积作用于T维度也可以把它实现为PyTorch里的Conv2d卷积核为[1, 9]即空间维度上kernel为1只在时间维度上滑。我一般会这么写# 时间卷积只沿时间轴做卷积空间维度kernel为1 self.temporal_conv nn.Sequential( nn.Conv2d(in_channelsout_channels, out_channelsout_channels, kernel_size(1, 9), stride(1, 1), padding(0, 4)), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue) )这段代码里的kernel_size第0维对应空间维度、第1维对应时间维度。把时间卷积核设为9覆盖9帧的运动变化padding设为4是为了保持T不缩短。如果你的数据集动作速率很快可以适当把kernel size缩到5反之如果动作非常缓慢可以增大到11或13。完整的ST-GCN block由空间图卷积、时间卷积、BatchNorm、ReLU和残差连接组成。残差连接会把输入直接加到输出上避免网络加深后梯度退化。训练时如果发现深层网络的loss降不下去优先检查残差分支的维度是否对齐这是最容易忽略的地方。2.3 自适应邻接矩阵为什么固定的骨骼连接表不够用原始ST-GCN里的邻接矩阵A是由人体骨骼连接表手工定义的。这个人工先验看起来很合理但只要细想就有问题不同的动作中关节之间的耦合关系根本不是恒定的。比如“挥手”这个动作右手腕和右肩的关联很强而“走路”动作中左臂和右腿的交叉耦合更关键。固定邻接矩阵无法表达这种动态变化的依赖关系。自适应图卷积网络的做法是在原始邻接矩阵上叠加一个可学习的适配矩阵通常写成如下形式A_adaptive A alpha * A_learnedA_learned的初始值一般设为全零矩阵维度同样是[V, V]但它的每个元素都是可训练的。训练过程中网络会自动学习出在做某个动作时哪些关节之间的信息传递应该被加强。alpha是一个可学习的缩放系数它的作用类似注意力机制中的temperature参数避免一开始A_learned的噪声过大干扰原始拓扑。有的实现更进一步把A_learned改成输入相关的动态形式即根据当前帧的特征动态计算关节相似度。这种做法的公式是A_dynamic softmax(X * X^T / d_k)其中X是当前帧的节点特征矩阵d_k是特征维度的缩放因子。这样A的权重就变成了样本相关的同一个关节对不同动作的响应不同。从工程角度看动态版本更灵活但更吃显存固定版本则更稳定。# 自适应邻接矩阵的PyTorch实现骨架 class AdaptiveAdjacency(nn.Module): def __init__(self, num_nodes, in_channels): super().__init__() # 可学习适配矩阵初始化为0 self.A_learned nn.Parameter(torch.zeros(num_nodes, num_nodes)) self.alpha nn.Parameter(torch.tensor(0.0)) # 特征变换矩阵W self.W nn.Parameter(torch.randn(in_channels, in_channels)) def forward(self, X, A_original): # X: [B, C, V, T]这里把空间特征转成二维后做动态聚合 B, C, V, T X.shape X_reshaped X.permute(0, 3, 2, 1).reshape(B * T, V, C) # 动态相似度矩阵 A_dynamic torch.softmax(X_reshaped X_reshaped.transpose(1, 2) / C**0.5, dim-1) A_combined A_original self.alpha * self.A_learned A_dynamic Y A_combined X_reshaped self.W return Y.reshape(B, T, V, C).permute(0, 3, 2, 1)这段代码的forward里用了两个矩阵self.A_learned是全局可学习的适配A_dynamic是当前样本动态计算的相似度。初始化时A_learned全零、alpha为0等于模型一开始退化为原始GCN训练过程逐步放开这是一种很实用的稳定训练技巧。参数上num_nodes要和你的骨架点数对齐Kinetics是18NTU RGBD是25改数据集时这个超参数是第一个要改的地方。对比项原始GCN自适应GCN邻接矩阵来源固定骨骼连接表固定表 可学习适配 动态相似度表达能力依赖人工先验能从数据中学习关节耦合变化训练难度低略高需要学习率配合适合场景动作类别固定、关节标准动作多样、遮挡频繁的复杂场景另外给出一个常见误用提醒有的人收到源码后直接把A_learned初始化为随机值这会导致训练初期loss剧烈震荡。好的做法是像我上面那样初始化为零矩阵并把alpha设为0等网络稳定后再让它自己学。3. 把环境装好、数据备齐依赖安装与预处理环节3.1 运行环境和依赖拆包之后第一件事不是跑而是装环境这套源码是Python写的依赖主要围绕PyTorch生态。动手之前我强烈建议你用conda单独建一个虚拟环境不要图省事直接装在base环境里。因为ST-GCN的依赖版本比较敏感装错了一个版本可能导致后面编译报错或者模型推理结果异常。conda create -n stgcn python3.8 -y conda activate stgcn pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install numpy opencv-python tqdm scipy scikit-learn第一行是创建Python 3.8的虚拟环境ST-GCN这系列源码不需要太新的Python版本3.8对应主流PyTorch版本兼容性最好。第二行激活环境。第三行的torch安装命令里cu118指的是CUDA 11.8版本如果你的显卡是30系或40系这个版本基本都能覆盖如果你只有CPU环境把这段换成pip install torch torchvision即可但训练速度会慢很多。第四行安装的是图像处理和训练辅助库。opencv-python负责视频帧读取和可视化tqdm用来显示训练进度scikit-learn用于混淆矩阵等评估指标。装完以后我在验证环境时通常会跑一条极短的命令确认CUDA可用省得训练时才发现用的是CPUpython -c import torch; print(torch.__version__, torch.cuda.is_available())输出里如果torch.cuda.is_available()为False说明CUDA版本或驱动有问题后续训练速度会慢到让人怀疑人生。这时优先检查nvidia-smi输出的驱动版本再对应选择合适的PyTorch版本。3.2 文件清单和目录结构先把gpu_nms和main.css隔离出去项目压缩包解压后你会看到一堆文件名比较“杂”的东西main.css、gpu_nms.cu、nms_kernel.cu、demo_video.gif、pull_ups_w.gif。第一次看到这个清单的人容易懵以为所有文件都要参与编译。实际情况是main.css是Web可视化页面用的样式表跑命令行训练和推理时完全用不到gpu_nms.cu和nms_kernel.cu是CUDA版本的NMS算子服务于动作检测里的候选框去重如果源码的主要入口是做动作分类这两个文件也不是必需依赖。我建议解压后先把目录整理干净再操作mv stgcn_project stgcn_work cd stgcn_work mkdir extra mv main.css gpu_nms.cu nms_kernel.cu extra/ ls -la这段命令做的事儿有三个第一把项目根目录重命名为纯英文这是摘要里明确强调的注意事项实测中文路径在OpenCV读视频和PyTorch保存权重时都会出问题第二创建extra临时目录第三把三个暂时用不到的文件挪进去防止后面误以为它们是主流程的一部分。目录整理好之后你的主入口脚本、models目录、data目录应该一目了然。如果你发现源码结构比较乱不妨按自己的习惯把data、models、utils这三个目录归置一下这不会影响代码运行因为你最终都是通过main入口指定路径的。3.3 骨架数据准备从视频到关键点序列的完整管线和关键细节模型的输入是骨架序列不是原始视频。所以数据准备阶段的任务是把每一帧的人体关节点坐标提取出来组织成[T, V, C]的数据结构。这一步有两个来源一是公开数据集比如NTU RGBD和Kinetics-Skeleton二是自己拍的视频用姿态估计器提取关键点。公开数据集的骨架数据已经处理好了缺的只是清洗和归一化。自己拍视频就要多一步姿态估计。我以自拍视频为例给出一个抽取关键点的流程骨架import cv2 import numpy as np import json def extract_skeleton(video_path, detector, out_path, sample_interval2): cap cv2.VideoCapture(video_path) frames [] frame_idx 0 while True: ret, frame cap.read() if not ret: break if frame_idx % sample_interval 0: # 姿态估计器输出关键点常见格式为 [V, C]C为(x, y, score) keypoints detector(frame) frames.append(keypoints) frame_idx 1 cap.release() # 统一补齐到固定帧数不足部分用零填充 frames np.array(frames) T, V, C frames.shape if T target_frames: padded np.zeros((target_frames, V, C), dtypenp.float32) padded[:T] frames frames padded np.save(out_path, frames)这段代码里的sample_interval是采样间隔设为2表示每两帧取一帧可以显著降低计算量同时保留足够的运动信息。target_frames是模型期望的时间帧数一般在训练配置里写死常见取值是64或128。如果你的视频时长不足等宽填充零帧是通用的兜底方案如果视频过长则采用均匀采样截取中间段。这里有一个重要的细节姿态估计器输出的关节顺序必须和训练时一致。Kinetics的18关节约定顺序与OpenPose的COCO 18点顺序基本一致NTU的25点又是另一套顺序。如果混用了识别效果会直接崩掉。坐标归一化也是预处理里绝对不能跳过的环节。常见做法是把所有坐标除以图像的短边让特征值落到0到1的区间再减均值除以标准差。置信度分数通常单独留一个通道作为模型的辅助输入。归一化的好处是让模型在分辨率不同的视频上都有相近的输入分布这也是第5章里训练不收敛的一个重要排查点。4. 训练和跑通demo从命令行到推理脚本4.1 训练入口与超参数配置先用小配置跑通再谈收敛拿到源码后第一件事永远是跑通训练流程而不是直接上大batch和大模型。ST-GCN系列源码的训练入口一般是main.py或者train.py通过argparse接收命令行参数。我先把最常用的一组参数列出来python main.py \ --dataset kinetics \ --batch-size 8 \ --epochs 60 \ --lr 0.1 \ --weight-decay 1e-4 \ --device 0这里--dataset指定数据集类型不同数据集的关节数和骨骼连接表不同--batch-size设为8是24G显存环境下的稳妥选择如果显存只有12G就降到4--lr初始学习率0.1配合momentum 0.9是ST-GCN源码里比较常见的组合--weight-decay是L2正则系数防止过拟合--device 0表示使用第0张GPU。如果你的训练数据量很大把epochs加到100以上但初期跑通验证时60轮足够看到收敛趋势。训练过程中我习惯每5个epoch记录一次训练loss和验证集top-1准确率。如果验证集top-1在20轮之内能超过50%说明数据管线和模型结构没有问题。反之如果loss一直卡在某个值附近不动先别急着调模型重点检查数据归一化和学习率两个因素。4.2 推理新视频从视频帧到动作标签的完整流程模型训练好后推理阶段的链路比训练更直观但也更容易踩坑。拉取一段视频先采样帧再做姿态估计然后把骨架序列滑窗分段逐段过模型最后聚合结果。推理脚本大致长这样import cv2 import numpy as np import torch def infer_video(model, video_path, pose_detector, window_size64, overlap16, devicecuda): cap cv2.VideoCapture(video_path) skeleton_frames [] frame_idx 0 while True: ret, frame cap.read() if not ret: break if frame_idx % 2 0: kps pose_detector(frame) # [V, C] skeleton_frames.append(kps) frame_idx 1 cap.release() # 构建滑动窗口窗口长度64帧相邻窗口重叠16帧 results [] step window_size - overlap for start in range(0, len(skeleton_frames) - window_size 1, step): window np.array(skeleton_frames[start:start window_size]) window_tensor torch.tensor(window, dtypetorch.float32, devicedevice) window_tensor window_tensor.unsqueeze(0).permute(0, 3, 1, 2) # [1, C, T, V] with torch.no_grad(): logits model(window_tensor) probs torch.softmax(logits, dim-1) results.append(probs.cpu().numpy()) # 对所有窗口的softmax结果取均值 final_probs np.mean(results, axis0) action_id np.argmax(final_probs) return action_id, final_probs这段代码里有几个容易被忽略的参数window_size取64表示一次模型输入64帧的骨架序列这个值必须和训练时的时间长度一致overlap设为16让相邻窗口共享16帧避免动作正好被窗口边界切断导致预测断裂最后对窗口结果取均值而非取最大值是为了抑制偶发的抖动预测。整个推理过程在CPU上也能跑但速度大约是GPU的十分之一实时性要求高就上GPU。demo_video.gif就是跑通这段逻辑后把每一帧的预测结果渲染回视频生成的效果图你可以用它来验证自己的推理链路是否正确。4.3 输出结果的可视化把预测标签画回视频帧对于毕设展示或者项目演示可视化几乎是刚需。通常的做法是推理时把每个窗口的预测类别和概率存下来然后再读一遍原视频把标签写到画面的顶部最后用OpenCV的VideoWriter导出mp4或gif。pull_ups_w.gif这个文件本质上是引体向上动作的输出可视化样例。如果你要做演示可以参考下面这个极简的可视化代码片段def draw_label(frame, label, prob, pos(10, 40)): text f{label}: {prob:.2f} cv2.putText(frame, text, pos, cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) return frame画标签时注意putText不支持中文显示如果你希望最终视频上显示中文动作名要么先映射成英文要么自己用PIL加载中文字体再绘制。这部分不影响模型效果但直接影响演示观感建议提前准备好。5. 避坑与常见问题路径、NMS编译和数据顺序5.1 gpu_nms编译报错不需要它就直接隔离现象import时报错ModuleNotFoundError: No module named gpu_nms或者编译时提示gpu_nms.cu文件里AT_CHECK未定义。原因这份源码里的gpu_nms.cu是为旧版PyTorch写的C扩展新版本PyTorch的API早就变了AT_CHECK宏已经被TORCH_CHECK取代而且编译C扩展需要本地有匹配的NVCC编译器很多人环境里根本没配。解决如果你的任务只是动作分类NMS压根不属于主链路。先去源码里找到import gpu_nms的位置看它是否被条件性地调用如果是直接import就改成try/except结构异常时fallback到纯Python实现的一套nms函数。不要为了一个辅助算子去折腾CUDA编译环境这是在浪费时间。5.2 中文路径导致的诡异报错现象解压后直接运行训练或推理脚本报错说找不到文件或者视频读出来的每一帧全是黑的也有的报错信息指向cp950编码错误。原因PyTorch和OpenCV底层用的是C/C运行时对中文字符路径支持很糟糕脚本里如果还拼接了相对路径而当前工作目录不对叠加起来就会出现各种莫名其妙的错误。解决把项目目录重命名为英文整个路径中不要带中文然后cd到项目根目录再执行命令。这是摘要里反复强调的坑我在实测中碰到过不止一次新手最容易忽视。5.3 自拍视频识别效果差关节顺序与缺失值处理现象自带的demo视频识别效果良好但换成自己的视频后准确率骤降。原因极大概率是姿态估计器输出的关节编号顺序和训练时不同或者画面里人物遮挡导致某些关键点坐标为0这些无效坐标被当成真实位置输入网络。解决先做可视化调试把姿态估计器的关键点画在画面上逐一核对编号对于缺失的节点用mask机制处理而不是直接填0。下面这段是通用处理逻辑def handle_missing_keypoints(kps, valid_mask, feat_dim2): # valid_mask标记每个关节是否有效shape为[V] # 无效关节的坐标置零同时增加一个可见性通道 kps kps * valid_mask[..., None] visibility valid_mask.astype(np.float32)[..., None] return np.concatenate([kps, visibility], axis-1)这段代码把原本的(x, y)坐标扩展成了(x, y, visible)三通道。visible为1表示该关节正常0表示被遮挡或超出画面。模型的输入通道数也因此从2变成3训练时就要按这个通道数设置。如果你是在已有模型上做迁移需要调整第一层图卷积的输入维度。这个细节直接决定迁移学习的成败我见过太多人卡在这一步。5.4 显存OOM和loss不下降现象设置batch size为64时报CUDA out of memory或者loss一直卡在4.2附近震荡。原因ST-GCN输入张量是[B, C, T, V, M]五维结构中间特征图在V和T都大时很占显存loss不降则通常是因为坐标没有归一化输入的特征值范围从几像素到几千像素量化不均梯度被极端的坐标值带偏。解决batch size先降到8或者4同时把解码器里的窗口帧数从128降到64坐标先除以短边归一化再做一次减均值除标准差的标准缩放。另外强烈建议加载公开数据集预训练的权重做初始化而不是从随机权重开始训练这能让收敛时间缩短数倍。6. 进阶把预训练模型改造成自己的动作识别器当你把demo跑通、理解了整条链路之后一个很自然的诉求就是让这套系统识别你自己定义的动作比如“举手”“下蹲”“挥拍”。这个场景与训练公开数据集不完全一样但流程是固定的采集正负样本视频提取骨架序列定义类别映射然后微调模型。首先采集数据。每个动作至少准备5段以上的视频每段时长控制在3到10秒尽量让两位以上不同体型的人录制避免模型过拟合到个人体型上。负样本也很重要如果你只录正样本模型大概率会把所有动作都判成你定义的那一类。然后按第3章的骨架提取管线把视频转成npz文件组织成训练集和验证集目录。类别映射这一步的关键是调整模型最后的全连接层把输出维度从预设类别数改成你的动作数加一多出来的那一类就是背景或未知动作。微调时不要从头训练。加载预训练权重后把最后一个全连接层替换掉其余层的学习率设为原来的十分之一。这样做的逻辑是骨干网络已经学到了通用的关节运动特征只需要在顶层适配你的动作分布。优化器仍然用SGD初始学习率建议0.001batch size按显存调到8左右训练20到30轮基本就能看到效果。如果你发现验证集准确率上不去最优先的排查项不是网络结构而是骨架数据里关节顺序是否统一以及窗口切分是否把动作截断。验证时别只看一个整体准确率我习惯把混淆矩阵打出来。混淆矩阵能直接告诉你哪些动作之间互相打架比如“挥手”和“举手”如果频繁混淆多半是两者的运动幅度区分度不够这时可以增加窗口长度让模型看到更长的运动轨迹。ST-GCN这类模型的一个特点是窗口越长对慢动作的区分度越好但也会引入更多冗余帧所以64到128帧区间是实用性较好的折中。从那以后我每次拿到别人的项目都强制自己先走一遍固定的流程看文件清单找出真正的主入口把NMS、CSS这类旁路文件隔离跑一遍最小实验确认数据管线通然后才谈得上改模型。这个习惯帮我避掉了很多无谓的踩坑也让我在帮别人排查问题时能快速定位到是环境问题、数据问题还是模型问题。希望帮到你。本文还有配套的精品资源点击获取