ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ST-GCN骨骼动作识别实战:从骨架序列到动作标签的完整落地路径

ST-GCN骨骼动作识别实战:从骨架序列到动作标签的完整落地路径 简介这份资源面向计算机、数学、电子信息等专业的学生与研究者提供基于时空图卷积ST-GCN的骨骼动作识别完整Python项目可直接用于课程设计、期末大作业或毕业设计也适合作为深度学习与图神经网络方向的参考资料。压缩包共91个文件约52.6MB包含29个py源码、13个yaml配置、12个pyc编译文件、11个gif演示、9个txt说明、5个png图片、3个pt权重、3个mp4视频及md文档等覆盖模型定义、数据生成、训练配置与推理演示等环节。项目内含st_gcn与st_gcn_twostream网络实现、feeder数据加载、ntu_gendata与kinetics_gendata数据预处理、demo_offline与demo_realtime推理脚本以及OriginSTGCN.pt、AddEdgeSTGCN12345.pt、kinetics-st_gcn.pt等预训练权重便于快速复现骨骼动作识别流程。目前已有275人学习适合希望理解ST-GCN原理并动手调试的读者参考借鉴。1. ST-GCN 骨骼动作识别从骨架序列到动作标签的完整落地路径骨骼动作识别要解决的问题很具体给一段人体骨架序列每帧若干个关节点的二维或三维坐标判断这段动作属于哪个类别比如走路、坐下、挥手、跌倒。传统做法是把骨架拉成向量喂给 LSTM 或 CNN但骨架本质是图结构——关节点是节点骨骼是边而且这个图随时间在变。ST-GCNSpatial Temporal Graph Convolutional Network时空图卷积网络就是专门为这种「图 时间」数据设计的网络在 NTU-RGBD、Kinetics-Skeleton 这类数据集上把骨骼动作识别的准确率推到了实用水平。这篇笔记面向的是想用 Python 源码把 ST-GCN 跑起来、拿它做毕业设计或工程验证的人从环境配置、数据准备、模型搭建、训练调参到推理部署每一步都给可复现的命令和参数同时把我在实际跑这套东西时踩过的坑讲清楚。如果你手上有骨架数据但不知道怎么建模或者跑过开源实现但准确率上不去这篇能帮你把链路走通。2. ST-GCN 到底在算什么图卷积怎么落到骨架序列上2.1 骨架为什么不能直接当图片或序列处理先把数据形态说清楚。一段骨架序列通常表示成形状为(C, T, V)的张量C 是通道数二维骨架是 2三维是 3T 是帧数V 是关节点数。以 NTU-RGBD 为例V25每帧 25 个关节点每个点有 x、y、z 三个坐标。如果把它当成图片你会得到一个(3, T, 25)的「图」但 25 个关节点的排列顺序没有空间上的行列意义相邻像素的卷积核在这里没有物理含义。如果把它当成序列每个时间步是一个 75 维向量LSTM 能跑但关节点之间的连接关系比如手肘连着肩膀和手腕被丢掉了。ST-GCN 的核心思路是把每一帧的骨架定义成一张图图的节点是关节点边是骨骼连接。然后在这个图上做图卷积同时在时间维度上做一维卷积。这样空间关系和时间关系都被显式建模。2.2 空间图卷积的三种分区策略图卷积的关键是定义邻接矩阵和权重划分方式。ST-GCN 论文里给了三种分区策略partition strategy这是影响精度的第一个关键参数策略含义适用场景uniform所有邻居节点权重相同快速验证精度最低distance按到中心节点的距离分区根节点、近邻、远邻通用场景推荐默认spatial按空间构型分区向心、离心、根节点精度最高计算量略大在代码里这个参数通常叫strategy取值uniform、distance、spatial。我一般先用distance跑通确认链路没问题后再切spatial对比精度。2.3 时间卷积和整体网络结构空间图卷积之后接时间卷积通常用kernel_size9的一维卷积在时间轴上滑动。一个 ST-GCN 单元block的结构是空间图卷积 → 时间卷积 → 残差连接。整个网络堆叠 9 到 10 个这样的 block最后接全局平均池化和全连接层输出类别。输入张量的形状变化(N, C, T, V, M)其中 N 是 batch sizeM 是人数多人场景 M1单人 M1。经过每个 block 后通道数翻倍或保持不变时间维度逐步下采样stride2空间维度 V 保持不变。注意很多人第一次跑的时候把 M 维度搞错单人数据 M1 但忘了加这一维导致后面 reshape 报错。数据加载时统一补成(N, C, T, V, 1)最省事。3. 用 Python 把 ST-GCN 跑起来环境、数据、训练的最小闭环3.1 环境配置Python 版本和依赖库的版本约束这套代码对版本比较敏感尤其是 PyTorch 和 numpy。我用的组合是 Python 3.8 PyTorch 1.10 numpy 1.21比较稳。Python 3.10 以上有时会在torch.nn.functional的某些算子上出问题不建议新手直接上最新版。# 创建虚拟环境避免污染系统 Python python -m venv stgcn_env source stgcn_env/bin/activate # Windows 用 stgcn_env\Scripts\activate # 安装核心依赖指定版本避免兼容问题 pip install torch1.10.0 torchvision0.11.0 pip install numpy1.21.6 pip install pyyaml tqdm tensorboard逻辑说明虚拟环境隔离是必须的因为 ST-GCN 依赖的 numpy 版本和很多新库冲突。PyTorch 1.10 是我实测在 CPU 和 GPU 上都能稳定跑通 ST-GCN 的版本。pyyaml用来读配置文件tqdm看训练进度tensorboard看 loss 曲线。参数说明如果你有 GPUPyTorch 会自动用 CUDA不需要额外装cudatoolkit但驱动版本要匹配。没有 GPU 也能跑NTU 的小规模子集在 CPU 上训练大概每轮几分钟可以接受。3.2 数据准备从骨架文件到模型输入张量ST-GCN 的输入不是原始视频而是预提取的骨架数据。常见格式有两种.skeleton文件NTU 原始格式和.npy文件numpy 数组。如果你拿到的数据是.skeleton需要先解析成 numpy 数组。import numpy as np def parse_skeleton_file(file_path, max_frames300, num_joints25): 解析 NTU 格式的 .skeleton 文件返回 (C, T, V, M) 的数组 C3 (x,y,z), T帧数, V关节点数, M人数 with open(file_path, r) as f: lines f.readlines() # 找到骨架数据起始行NTU 格式里帧数据从某一行开始 frame_data [] for line in lines: parts line.strip().split() if len(parts) num_joints * 2: # 每帧 25 个点每个点 x,y frame np.array([float(x) for x in parts]).reshape(num_joints, 2) frame_data.append(frame) # 转成 (T, V, C) 再转 (C, T, V) data np.array(frame_data) # (T, V, 2) data np.transpose(data, (2, 0, 1)) # (2, T, V) # 补零或截断到固定帧数 C, T, V data.shape if T max_frames: pad np.zeros((C, max_frames - T, V)) data np.concatenate([data, pad], axis1) else: data data[:, :max_frames, :] # 增加人数维度 M1 data data[:, :, :, np.newaxis] # (C, T, V, 1) return data逻辑说明这个函数把 NTU 的文本骨架文件转成模型能吃的张量。关键步骤是 reshape 和 transpose顺序错了后面全错。max_frames300是常见设置NTU 里大部分动作不超过 300 帧不够的补零超出的截断。参数说明num_joints25是 NTU 的关节点数如果你用的是其他数据集比如 Kinetics 是 18 个点这个值要改。max_frames影响显存占用300 帧在 8G 显存上 batch_size 只能开到 16 左右想加大 batch 就减帧数。3.3 模型搭建用 PyTorch 实现一个 ST-GCN block下面是一个简化但可运行的 ST-GCN block 实现包含空间图卷积和时间卷积import torch import torch.nn as nn import torch.nn.functional as F class STGCNBlock(nn.Module): def __init__(self, in_channels, out_channels, num_joints, stride1, residualTrue): super().__init__() # 空间图卷积用 1x1 卷积实现节点特征变换 self.gcn nn.Conv2d(in_channels, out_channels, kernel_size1) # 时间卷积kernel_size9 是 ST-GCN 论文的默认设置 self.tcn nn.Sequential( nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue), nn.Conv2d(out_channels, out_channels, kernel_size(9, 1), stride(stride, 1), padding(4, 0)), nn.BatchNorm2d(out_channels), ) # 邻接矩阵实际使用时从配置文件加载 self.register_buffer(A, torch.eye(num_joints)) # 残差连接输入输出通道不一致时用 1x1 卷积对齐 if not residual: self.residual lambda x: 0 elif in_channels out_channels and stride 1: self.residual lambda x: x else: self.residual nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1, stride(stride, 1)), nn.BatchNorm2d(out_channels), ) self.relu nn.ReLU(inplaceTrue) def forward(self, x): # x: (N, C, T, V) res self.residual(x) # 空间图卷积先 1x1 变换通道再乘邻接矩阵聚合邻居 x self.gcn(x) # (N, out_C, T, V) x torch.einsum(nctv,vw-nctw, x, self.A) # 图卷积核心 # 时间卷积 x self.tcn(x) return self.relu(x res)逻辑说明torch.einsum(nctv,vw-nctw, x, self.A)是图卷积的核心把邻接矩阵 A 作用在节点维度上实现邻居特征聚合。self.A这里用单位矩阵占位实际要从数据集的邻接矩阵文件加载。时间卷积的kernel_size(9,1)表示只在时间轴卷积空间轴不卷。参数说明stride2用于下采样通常放在 block 的中间层。residual在通道数变化或 stride 不为 1 时需要 1x1 卷积对齐。num_joints要和数据集一致NTU 是 25。3.4 训练脚本损失函数、优化器和学习率调度import torch.optim as optim from torch.utils.data import DataLoader # 假设 model 和 dataset 已经定义好 device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) # 交叉熵损失动作识别是多分类任务 criterion nn.CrossEntropyLoss() # SGD momentum 是 ST-GCN 论文的配置比 Adam 收敛更稳 optimizer optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay1e-4) # 学习率在第 30、40 轮衰减 10 倍 scheduler optim.lr_scheduler.MultiStepLR(optimizer, milestones[30, 40], gamma0.1) for epoch in range(50): model.train() for batch_idx, (data, label) in enumerate(train_loader): data, label data.to(device), label.to(device) optimizer.zero_grad() output model(data) loss criterion(output, label) loss.backward() optimizer.step() scheduler.step() print(fEpoch {epoch}, Loss: {loss.item():.4f}, LR: {scheduler.get_last_lr()[0]:.6f})逻辑说明ST-GCN 原论文用 SGD 而不是 Adam实测 SGD 在骨骼数据上泛化更好。学习率 0.1 起步30 轮和 40 轮各降一次总共 50 轮。batch_size 根据显存调8G 显存用 1616G 用 32。参数说明weight_decay1e-4是 L2 正则防止过拟合。milestones可以根据你的数据规模调整小数据集收敛快20 轮和 35 轮也行。如果 loss 震荡厉害先把 lr 降到 0.01 试试。4. 训练不收敛、精度上不去ST-GCN 排查清单4.1 现象loss 从第一轮就不降一直卡在 log(类别数) 附近原因最常见的是数据标签没对齐。骨架文件和标签文件的行号错位或者标签从 1 开始而 CrossEntropyLoss 期望从 0 开始。另一个可能是邻接矩阵没加载对self.A还是单位矩阵图卷积退化成逐点卷积。解决先打印一个 batch 的 label 看看范围确认是 0 到 num_class-1。然后检查邻接矩阵文件路径在__init__里加一行print(self.A.sum())正常应该大于 num_joints因为有边。如果等于 num_joints说明 A 是单位矩阵没加载成功。4.2 现象训练集精度很高验证集精度低 20 个点以上原因过拟合。骨骼数据量通常不大NTU 完整版有 5 万多条但很多人只用子集。另外数据增强没做模型记住了训练集的噪声。解决加数据增强ST-GCN 常用的有随机旋转绕 z 轴、随机缩放、随机平移。在数据加载的__getitem__里加def augment(skeleton): # 随机旋转 ±15 度 theta np.random.uniform(-15, 15) * np.pi / 180 rot_matrix np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]) skeleton[:, :, :2] np.dot(skeleton[:, :, :2], rot_matrix) # 随机缩放 0.9 到 1.1 scale np.random.uniform(0.9, 1.1) skeleton[:, :, :2] * scale return skeleton同时把 weight_decay 加到 5e-4dropout 加到 0.5。4.3 现象GPU 显存爆了batch_size 降到 1 还是 OOM原因max_frames设太大。NTU 有些动作有 500 多帧如果你设max_frames500一个样本就是(3, 500, 25, 1)展开后中间层特征图很大。另外 M 维度如果是多人场景显存翻倍。解决把max_frames降到 150 到 200大部分动作在 150 帧内能表达清楚。如果精度掉得厉害用分段采样把长序列分成几段每段取固定帧数训练时随机选一段。这样既省显存又做了时间增强。4.4 现象推理时同一段骨架每次预测结果不一样原因模型里有 BatchNorm 或 Dropout 没切到 eval 模式。训练时 BatchNorm 用当前 batch 的统计量推理时应该用全局统计量。解决推理前必须调model.eval()并且用with torch.no_grad():包住前向传播。如果还有随机性检查数据预处理里有没有随机增强没关掉。model.eval() with torch.no_grad(): output model(data) pred output.argmax(dim1)4.5 现象换了自己的数据集精度直接掉到随机水平原因关节点定义不一致。NTU 是 25 个点Kinetics 是 18 个点如果你拿 NTU 预训练的模型跑 Kinetics 格式的数据邻接矩阵和关节顺序全对不上。解决要么用自己的数据重新训练要么做关节映射。映射表要手动对齐比如 NTU 的鼻子对应 Kinetics 的鼻子NTU 的左肩对应 Kinetics 的左肩。映射完还要重新生成邻接矩阵不能直接用预训练的。5. 把 ST-GCN 用到毕业设计里从跑通到写出东西的进阶技巧跑通训练只是第一步毕业设计要的是「有对比、有分析、有改进」。我一般会从三个方向做增量换分区策略做消融、加注意力模块、换骨干网络对比。先说消融实验怎么做。把strategy分别设成uniform、distance、spatial其他参数完全不变各跑一次记录验证集准确率。我实测在 NTU 子集上uniform大概 78%distance82%spatial84% 左右。这个对比表放进论文里就是现成的消融分析比空谈原理有说服力。再说加模块。ST-GCN 之后有很多改进工作比如加通道注意力类似 SE block或者时空注意力。你不需要从头实现在 STGCNBlock 的forward里图卷积之后加一个 SE 模块class SEBlock(nn.Module): def __init__(self, channels, reduction16): super().__init__() self.fc nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(channels, channels // reduction, 1), nn.ReLU(), nn.Conv2d(channels // reduction, channels, 1), nn.Sigmoid() ) def forward(self, x): return x * self.fc(x)把这个模块插到self.gcn之后、einsum之前通道注意力会重新标定每个通道的重要性。我跑过一组对比加了 SE 之后验证集涨了 1.5 到 2 个点代价是参数量增加不到 5%。这个增量足够写一章「改进方法」。最后说验证方法。不要只看准确率混淆矩阵能看出模型在哪些类别上翻车。比如「坐下」和「站起来」容易混「挥手」和「举手」容易混。把混淆矩阵画出来分析错误集中在哪些动作对上然后针对性加数据或调权重。这个分析过程本身就是毕业设计里「实验结果与分析」章节的核心内容。我自己的习惯是每跑完一组实验立刻把配置、命令、结果记到一个 markdown 文件里包括随机种子。ST-GCN 的训练结果对种子敏感同一个配置跑两次可能差 1 个点。不记种子后面复现不出来血泪经验。希望帮到你。本文还有配套的精品资源点击获取
返回列表