ST-GCN骨骼动作识别:时空图卷积网络原理与PyTorch实践
2026/8/31 9:37:39 网站建设 项目流程

简介:本资源是一套完整的基于PyTorch实现的时空图卷积网络(ST-GCN)骨骼动作识别毕业设计项目,面向计算机、人工智能及相关专业本科生,解决人体动作识别这一典型时序-结构建模问题,适用于毕业设计、课程设计及期末大作业等实践场景。压缩包共90个文件,含29个核心Python源码(涵盖数据预处理、双流ST-GCN模型构建、离线/实时推理演示)、13个YAML配置文件(支持NTU-RGB+D与Kinetics数据集灵活切换)、3个预训练模型(.pt格式),以及GIF效果演示、MP4可视化视频、Markdown文档说明和详细注释代码,整体大小为52.61MB。已有306人学习下载,项目为作者手打高分毕设(98分),获导师高度认可;所有模块均经实际运行验证,结构清晰、注释详尽,新手可快速部署并复现完整识别流程,附带工具脚本(如get_models.sh、ntu_gendata.py)与日志分析支持,显著降低入门门槛与调试成本。

1. ST-GCN是什么,这个毕设到底在做什么

先别急着看代码,我先把这事的来龙去脉捋清楚。骨骼动作识别,说白了就是给一段视频里的人物骨骼关节点序列,让模型判断这个人正在做什么动作。比如你输入一段几十帧的骨架序列,模型告诉你这是“挥手”“走路”还是“摔倒”。它不像RGB视频识别那样直接拿图像像素去训练,而是先用姿态估计算法(比如OpenPose)把每一帧的人体关键点坐标提取出来,然后用这些坐标序列去做分类。

那ST-GCN是啥?全称是Spatial Temporal Graph Convolutional Network,时空图卷积网络。这是2018年AAAI上的一篇论文,核心思想是把人体骨骼建模成一张图——关节点是图的顶点,骨骼连接是图的边,然后在这张图上做图卷积,同时把时间维度的帧间变化也卷进去,实现“空间”和“时间”两个维度的联合建模。这个思路在当时很颠覆,因为传统做法都是把骨骼坐标展开成一个向量,然后丢进LSTM或者CNN里,完全丢失了人体关节之间的拓扑结构。ST-GCN直接保留了这个拓扑关系,效果自然更好。

对做毕设的同学来说,这个题目的性价比非常高。第一,它有现成的开源实现,GitHub上星标很高的项目不少,你不用从零造轮子;第二,它涉及图卷积、时间序列建模、深度学习训练推理,知识点覆盖面广,答辩时能讲的点非常多;第三,数据获取相对容易,不像RGB视频那样需要处理复杂的背景和光照,骨骼数据干净、轻量,训练速度也快。无论你是想混个毕业,还是想认真做点研究,ST-GCN都是一个特别合适的选择。

这篇文章我打算按照我自己做这个项目的完整路径来写:从原理拆解到环境搭建,从数据准备到模型训练,再到源码细节和踩坑记录。你会看到很多文档里不会写的实操细节,比如张量维度为什么是N C T V M而不是N C T V,空域卷积的邻接矩阵怎么设计,训练不收敛时先查哪里。这些东西我都是踩过坑才明白的,现在整理出来,希望能让你少走弯路。

2. 核心原理拆解:为什么骨架数据要用图卷积

2.1 人体骨骼的图结构建模

先想一个问题:人体骨骼数据本身是什么形态?假设你有一个人体的18个关键点(OpenPose的COCO格式就是18个点),每个点有x和y坐标,视频一共30帧。那么一个样本就是一个形状为2x18x30的张量——2是坐标维度,18是关节点数,30是时间帧数。如果再加上多个人,那就是2x18x30x人数

但问题是,这18个关节点不是相互独立的。它们通过骨骼连接形成了一种天然的拓扑结构:比如左肘连着左肩,左肩连着脖子,脖子连着头部。这种连接关系如果用传统的全连接层或者普通卷积去处理,完全体现不出来。你想想,如果把“左脚踝”和“左手腕”这两个点在特征空间里当成同等距离的关系来处理,那信息就乱套了。

ST-GCN的做法是,先把这18个关节点定义成一个图。图的顶点就是关节点,图的边就是骨骼连接。在数学上,一个图可以用邻接矩阵A来表示,A是一个18x18的矩阵,如果第i个关节点和第j个关节点之间有骨骼连接,那么A[i][j]就等于1,否则为0。比如COCO骨架里,左肘(elbow)连着左肩(shoulder),那么这两个点对应的矩阵位置就是1。

但这里有一个关键问题:直接用这个邻接矩阵做卷积不行。因为人体运动时,不同关节的重要性完全不同。你挥手时主要是肩、肘、腕在动,核心躯干基本不动;你走路时腿和胯是关键,手臂只是自然摆动。如果所有关节一律平等地做卷积,模型很难学到这种差异性。所以ST-GCN在原始邻接矩阵基础上做了分区策略,把每个节点的邻居分成三个子集:节点本身(像心跳一样,保留自身特征)、空间上比它更靠近重心的邻居(向心)、空间上比它更远离重心的邻居(离心)。这样每一次图卷积就相当于同时做了三组独立的卷积,每组有自己的权重矩阵,最后加起来。

2.2 空间图卷积与时间卷积怎么协同

图卷积解决的是“同一帧内关节点之间的关系”,但动作识别光靠单帧肯定不行,你得一帧一帧地看变化。ST-GCN在空间维做完图卷积之后,紧接着在时间维度做一次标准的一维卷积,卷积核大小通常设为9,也就是一次覆盖9帧。

这两个操作是交替进行的。一个ST-GCN模块的典型结构是:先做空间图卷积,然后做时间卷积,中间接BatchNorm和ReLU激活,最后加一个残差连接。残差连接的作用是防止网络过深时出现梯度消失。整个模型的骨干网络就是堆叠9个这样的ST-GCN模块,每个模块的输出通道数逐渐增加,从64到128再到256,空间维度(也就是关节点数)保持不变,时间维度逐步下采样,把序列长度压缩。

我举个直观的例子帮你理解这个流程。假设输入一段30帧、18个关节点的动作序列,第一个ST-GCN模块先把每一帧的18个节点做图卷积,让每个节点的特征融合了它邻居节点的信息。这一步做完,你得到的特征里,“左手腕”这个节点的表示就已经包含了“左肘”“左肩”甚至“左髋”的信息,因为图卷积会沿着边多层传播。然后时间卷积再沿着30帧的方向滑动,捕捉“左手腕在时间轴上的运动轨迹”。空间卷积看到了“谁和谁连在一起”,时间卷积看到了“每个点的运动趋势”,两个信息合力,就能判断这个动作是什么。

2.3 损失函数与评估指标

训练阶段用的损失函数就是标准的交叉熵损失。动作识别本质是一个多分类问题,模型最后通过一个全局平均池化(Global Average Pooling)把特征压缩成一维向量,然后接一个全连接层,输出维度等于动作类别数,再用Softmax转成概率分布,与真实标签计算交叉熵。

评估指标一般看Top-1准确率和Top-5准确率。Top-1就是你预测概率最高的那个类别是否等于真实标签,Top-5就是概率前五的类别里是否包含真实标签。NTU RGB+D数据集(一个非常常用的骨骼动作识别基准)上,ST-GCN在Cross-Subject划分下Top-1准确率大约在81.5%左右,在Cross-View划分下大约在88.3%左右。这个数字你不需要过度追求,毕设能到70%以上就已经能说明模型有效了,但是如果你的数据量小、类别少,跑出85%以上也不是没可能。

3. 环境与数据准备:构建可复现的毕设基础

3.1 Python与PyTorch环境搭建

我建议你直接把环境搭建当成项目的第一步,不要上来就复制代码跑。因为ST-GCN的依赖项比较多,版本不匹配的话你会先被环境折磨三天。

先说Python版本,我用的是3.8,配合PyTorch 1.10.0。这个组合不是最新,但绝对是最稳的。PyTorch 2.x出来之后很多老项目会有兼容问题,比如torch.nn.functional.conv2d的某些行为变了,或者某些老接口被移除了。如果你的毕设拿到的是老源码,直接上PyTorch 2.6大概率会报错。所以我强烈建议装一个独立的conda环境:

conda create -n stgcn python=3.8 conda activate stgcn pip install torch==1.10.0 torchvision==0.11.0

如果你是NVIDIA显卡,可以先确认自己的CUDA版本,然后到PyTorch官网选择对应的安装命令。比如CUDA 11.3的安装命令是:

pip install torch==1.10.0+cu113 torchvision==0.11.0+cu113 -f https://download.pytorch.org/whl/torch_stable.html

没有GPU的同学也别慌,这个项目用CPU也能跑,就是慢。我用CPU训练过一个5类的小数据集,每个epoch大概要10分钟,20个epoch下来两三个小时能出结果,考虑到毕设的训练周期是可以接受的。但如果你的数据量达到几万样本,强烈建议用GPU,哪怕是个入门级的GTX 1660 Super,速度差距都在10倍以上。

除了PyTorch,还需要装一些辅助库:

pip install numpy matplotlib scikit-learn opencv-python tqdm

如果你的源码用了tensorboard做可视化,再加一个tensorboard。注意不要装tensorflow,只需要tensorboard这个独立的可视化服务就行。

3.2 数据集获取与预处理:NTU RGB+D还是自建数据

做骨骼动作识别,最常用的两个开源数据集是NTU RGB+D和Kinetics-Skeleton。NTU RGB+D有60个动作类别,包含单人动作和双人交互动作,样本量超过5万,是目前学术界的标准benchmark。Kinetics-Skeleton是从Kinetics视频数据集里用OpenPose提取出来的骨架数据,类别多达400类,但数据质量参差不齐,因为视频来源比较杂。

我建议做毕设的同学优先用NTU RGB+D。原因很朴素:数据质量高、动作类别统一、样本划分标准明确。NTU官方给出了两种划分方式——Cross-Subject(按人物划分训练集和测试集)和Cross-View(按摄像机视角划分)。你直接用Cross-Subject的划分方式,就能和论文里的结果做对比。

但NTU RGB+D的数据集需要去官网申请,审核需要时间,如果你是急着做毕设,可能等不起。这时候有两条路:一是用精简版或别人已经处理过的npy格式数据,GitHub上很多ST-GCN开源项目都会附带一个小的示例数据集,比如NTU的子集或自采样的数据;二是自己用OpenPose提取骨架数据。

自己提取骨架的流程是:先用OpenPose处理视频帧,得到每个人18个关节点(或25个关节点)的坐标和置信度,然后按时间顺序拼接成序列。需要注意的是,OpenPose输出的坐标是图像像素坐标,直接进模型会有尺寸偏差问题,所以要先做归一化。我的做法是找到整个序列里人体所有关节点坐标的均值,然后以这个均值为中心做平移,再除以坐标标准差,让数据分布落在0附近。这一步非常关键,不做归一化的话模型很难收敛。

3.3 数据划分与标签文件格式

ST-GCN源码的数据读取通常依赖一个ntu_gendata.pyfeeder模块,它读取的是npy格式的关键点数组和对应的标签文件。这里我先说一个很多小白容易懵的地方:数据文件里一个样本的形状到底是什么。

以NTU RGB+D为例,原始数据里每个样本是(3, 300, 25, 2)——3是通道数(x坐标、y坐标、置信度),300是最大帧数,25是NTU的25个关节点,2是人物数。而ST-GCN模型输入要求的是(3, 300, 25, 2)然后再转成(N, C, T, V, M)。这里的N是batch size,C是通道数(3),T是帧数(300),V是节点数(25),M是人物数(2)。如果你用的是COCO格式的18个关节点,那V就是18,M一般就是1。

做数据预处理时,很多人会踩一个坑:把过长的序列截断,把过短的序列补零。NTU官方有一个约定,超过300帧的裁剪到300帧,不足300帧的在末尾补零。但这个补零操作其实对模型很不友好,因为补零区域模型不知道怎么处理,容易产生虚假的特征。我自己实验下来,更推荐的做法是:把整个序列均匀采样到固定长度(比如100帧),而不是简单地截断或补零。采样后的序列虽然帧数变少,但时间上的运动趋势保留得相对完整,训练出来的模型泛化能力反而更好。

如果不会写这个采样逻辑,我可以给你一个最简单的版本:

def uniform_sample(data, target_num_frames=100): # data shape: (C, T, V, M) current_num_frames = data.shape[1] if current_num_frames == target_num_frames: return data idx = np.linspace(0, current_num_frames - 1, target_num_frames).astype(int) return data[:, idx, :, :]

然后把这个函数应用在每一个样本上,再保存成npy文件。

4. 源码结构与核心实现解析

4.1 项目目录该如何组织

不管你是自己写还是从GitHub上下载,一个清晰的目录结构能让你在写论文时省很多事。我推荐这样组织:

stgcn_project/ ├── config/ # 配置文件(数据集路径、训练参数等) │ ├── train.yaml │ └── test.yaml ├── data/ # 数据文件 │ ├── train_data.npy │ ├── train_label.pkl │ ├── val_data.npy │ └── val_label.pkl ├── feeder/ # 数据读取与增强 │ ├── __init__.py │ ├── feeder.py # DataLoader核心 │ └── augmentation.py # 数据增强 ├── model/ # 模型定义 │ ├── __init__.py │ ├── stgcn.py # ST-GCN主模型 │ ├── graph.py # 图结构定义与邻接矩阵生成 │ └── st_gcn_block.py # 单个ST-GCN模块 ├── processor/ # 训练与评估主逻辑 │ ├── __init__.py │ └── processor.py ├── main.py # 入口 ├── requirements.txt └── README.md

这个结构是参考了开源社区最流行的ST-GCN实现(特别是microsoft和yysijie那版),它的好处是“配置”和“代码”分离。你改数据集路径、改学习率、改batch size,只需要动yaml配置文件,不需要动代码。对毕设来说,这能让你少写很多硬编码的东西,也更符合工程规范。

4.2 邻接矩阵与图结构定义:这是ST-GCN的核心门槛

打开graph.py,你会发现整个模型最重要的代码其实是在构造邻接矩阵。我直接给你讲清楚这部分的实现逻辑。

首先你要定义每个数据集的关节连接关系。以COCO 18点为例,连接关系大概是这样的:

# COCO 18个关键点:0鼻子, 1脖子, 2右肩, 3右肘, 4右手腕, 5左肩, 6左肘, 7左手腕, 8右髋, 9右膝, 10右脚踝, 11左髋, 12左膝, 13左脚踝, 14右眼, 15左眼, 16右耳, 17左耳 num_node = 18 self_link = [(i, i) for i in range(num_node)] # 自连接 neighbor_link = [ (1, 2), (2, 3), (3, 4), # 右臂链路 (1, 5), (5, 6), (6, 7), # 左臂链路 (1, 8), (8, 9), (9, 10), # 右腿链路 (1, 11), (11, 12), (12, 13),# 左腿链路 (1, 0), # 脖子到头 (0, 14), (14, 16), # 右眼链路 (0, 15), (15, 17), # 左眼链路 ]

然后ST-GCN要做的不是简单地生成一个18x18的邻接矩阵,而是生成三个邻接矩阵,分别对应上面提到的三个分区策略。具体实现时,先计算每个节点到重心的距离。对于单人的情况,重心可以用所有关节点坐标的均值来近似。然后对每条边(i, j),根据节点i的重心距离与节点j的重心距离的大小关系,决定这条边属于哪个子集。

代码里通常用一个hop_dis函数计算节点之间的最短路径距离,然后根据这个距离和重心距离来生成三个掩码矩阵。生成完之后,你得到的是一个形状为(3, num_node, num_node)的张量,每次图卷积都拿这三个矩阵分别做一次矩阵乘法,然后拼接起来。

如果你不想理解得太深,只要记住一件事:这个邻接矩阵是在任何训练开始之前就根据人体结构预先算好的,它不参与梯度更新,属于模型的“先验知识”。它的作用是告诉网络,哪些节点之间有直接连接,以及每个节点在卷积时应该如何聚合邻居信息。

4.3 ST-GCN核心模块代码逐行解读

单个ST-GCN模块的实现核心在两行代码上,一空域一时序。我简化一下给你看:

class ST_GCN_Block(nn.Module): def __init__(self, in_channels, out_channels, A, stride=1, residual=True): super().__init__() self.A = A # (3, V, V) 三个子集的邻接矩阵 self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1) self.bn1 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU() self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=(9, 1), stride=(stride, 1), padding=(4, 0)) self.bn2 = nn.BatchNorm2d(out_channels) if residual: self.residual = nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=(stride, 1)), nn.BatchNorm2d(out_channels) ) else: self.residual = lambda x: x def forward(self, x): # x shape: (N, C, T, V, M),先把M合到N里 N, C, T, V, M = x.size() x = x.permute(0, 4, 1, 2, 3).contiguous().view(N * M, C, T, V) res = self.residual(x) # 空间图卷积:A是(3,V,V),x是(N*M,C,T,V) # 做法:先把x变成(N*M*C*T, V),乘以A[k],再变回来 # 实际实现中用 einsum 或 matmul 实现 x = self.conv1(x) # 先做1x1卷积提升通道 x = self.bn1(x) x = self.relu(x) # 图卷积核心:用邻接矩阵加权求和邻居特征 A = self.A.to(x.device) x = torch.einsum('nctv,kvw->nctw', x, A).contiguous() # 上面这一步等价于分别对三个子集做矩阵乘法,然后拼起来 # 时间卷积:kernel_size=9,只在T维度上滑动 x = self.conv2(x) x = self.bn2(x) x += res x = self.relu(x) # 恢复M维度 _, C2, T2, V2 = x.size() x = x.view(N, M, C2, T2, V2).permute(0, 2, 3, 4, 1).contiguous() return x

注意一下,上面的torch.einsum('nctv,kvw->nctw', x, A)其实就是把x在V维上做线性变换。由于A是稀疏的,这个操作的计算量并不大。实际框架里还有人用torch.matmul来实现,效果等价。这里的conv2是时序卷积,卷积核是(9, 1),意思是只在时间维度上卷积,空间维度(节点维度)不做卷积。这个设计很巧妙——空间信息已经在图卷积里处理完了,时序卷积只需要关注帧间变化。

4.4 模型主架构与参数说明

ST-GCN的主模型就是把9个上述模块串起来。我常用的网络结构参数如下:

  • ST-GCN Block 1:输入通道3,输出通道64,stride=1(不降采样时间长度)
  • ST-GCN Block 2-3:64→64,stride=1
  • ST-GCN Block 4:64→128,stride=2(时间维度减半)
  • ST-GCN Block 5-6:128→128,stride=1
  • ST-GCN Block 7:128→256,stride=2(时间维度再减半)
  • ST-GCN Block 8-9:256→256,stride=1

9个block之后接一个全局平均池化,把(N, 256, T, V)压成(N, 256),再过一层全连接,得到(N, num_classes)的logits。

总参数量大约在300万左右,不算大。用GPU训练一个epoch,处理NTU的5万多样本,大约需要5到10分钟(视显卡而定),CPU的话大概1小时起步。如果你的数据集只有几千样本,GPU训练一个epoch几分钟就完事了。

5. 完整实操过程:从训练到可视化结果

5.1 训练流程与关键超参数

这里的训练流程我按照最通用的方式来说,对应的就是GitHub上yysijie那版代码的流程。入口是main.py,它接收一个--config参数指向配置文件。配置文件的完整内容很长,我挑几个最关键的超参数说:

# 数据相关 data_path: ./data/ntu/xsub/train_data.npy label_path: ./data/ntu/xsub/train_label.pkl val_data_path: ./data/ntu/xsub/val_data.npy val_label_path: ./data/ntu/xsub/val_label.pkl # 模型相关 num_class: 60 num_point: 25 num_person: 2 graph: ntu_rgb_d # 图结构定义选择 # 训练相关 batch_size: 64 lr: 0.1 weight_decay: 0.0001 epochs: 80 optimizer: SGD scheduler: cosine

注意这里的lr: 0.1,很多人第一次看到会吓一跳,这不是太高了吗?确实,0.1这个学习率对SGD来说很激进,但关键在后面:配合的是cosine退火学习率调度。前几个epoch用0.1大步快跑,当loss下降变慢时,学习率按余弦曲线逐渐降到接近0,这样既保证了前期收敛速度快,又保证了后期不会震荡。我实测下来,这个配置在NTU上60个epoch左右就能收敛到不错的效果。

但如果你用的是自己的小数据集,学习率要适当调小。我做过一个5类动作的小数据集,用0.1的初始学习率直接爆掉了,loss变成了NaN。降到0.01之后就稳定了。所以我的经验是:如果训练一开始loss就震荡或者变成NaN,第一反应就是调低学习率,不是去调网络结构。

5.2 训练过程中的监控与日志记录

训练的时候,我强烈建议你把日志记录下来。source code里最常见的做法是用tensorboard,在训练命令里加一个--log_dir参数,然后在浏览器里打开http://localhost:6006看曲线。你主要关注三条曲线:训练集loss、验证集loss、验证集准确率。

正常训练时你会看到:训练集loss持续下降,验证集loss也跟着下降,说明模型在学习;验证集准确率稳步上升。但如果出现训练集loss一直降、验证集loss不降反升的情况,说明过拟合了,这时候应该减少epoch数,或者加dropout。ST-GCN的代码里默认在最后一个block后接了一个dropout(概率0.5),如果你自定义数据集特别小,建议把dropout调到0.6甚至0.7。

有一个容易被忽略的细节:训练和验证阶段的数据预处理不同。训练阶段需要做随机旋转、随机裁剪、随机时间偏移等数据增强,但验证阶段只用做标准化,不能加任何随机操作。具体到代码,feeder.py里会有random_move这些函数,只对训练集启用。如果你的代码把验证集也做了增强,那结果会非常不稳定。

5.3 模型评估与保存

每个epoch结束之后,源码会自动在验证集上做一次评估,打印当前的Top-1准确率。如果当前准确率是历史最好,就保存一份模型权重到work_dir下,通常叫best_model.pt。同时保留一份最近的模型,叫latest_model.pt,方便中断后从断点继续训练。

我的习惯是把训练过程拆成两段:先用60个epoch跑一个粗结果,验证模型没有结构性问题;然后再加载latest_model.pt继续训练20个epoch,微调一下。这样如果前面参数设错了,不会浪费太多时间。

5.4 结果可视化:把预测过程展示出来

毕设答辩时,光给出准确率数字是不够的,最好有一张可视化图展示模型是怎么工作的。ST-GCN的可视化一般有两种:

一种是直接在原始视频上把预测的类别和置信度画出来。这个需要你用OpenPose提取骨架后,再把OpenPose画的骨架线叠在原视频上,然后在顶部打印出预测的动作类别和概率。视觉效果很好,答辩时一放,评委立刻能看懂你在做什么。

另一种是可视化模型中间层的特征图。这个稍微复杂一些,需要你在模型的某个block后面加一个hook,把中间层的输出保存下来,然后用matplotlib画出来。实际操作时你会发现,中间层的特征图往往呈现出一种“某些节点被激活,某些节点被抑制”的模式,比如挥手动作时,肩关节、肘关节对应的特征值会特别高。这个可视化如果做得好看,能直接提升你毕设的完成度。

我提供一个简单的推理可视化伪代码思路:

# 加载训练好的模型 model = ST_GCN(...) model.load_state_dict(torch.load('best_model.pt')) model.eval() # 读取一个测试样本 sample = np.load('test_sample.npy') # (3, T, V, M) sample = torch.from_numpy(sample).float().unsqueeze(0) # (1, 3, T, V, M) with torch.no_grad(): logits = model(sample) probs = torch.softmax(logits, dim=1) pred_class = torch.argmax(probs, dim=1).item() pred_score = probs[0][pred_class].item() print(f'预测动作: {class_names[pred_class]}, 置信度: {pred_score:.4f}')

6. 常见问题与排查技巧实录

6.1 训练loss一直是NaN

这个错误我遇到过不下五次,原因通常有三个:学习率太大、数据里有NaN值、模型权重初始化异常。我个人排查的顺序是:先检查数据,把训练数据用np.isnan(np.sum(data))扫一遍,如果有NaN就说明数据预处理出了问题;数据没问题的话,把学习率从0.1降到0.01或者0.001试试;最后再看模型的输入维度是不是不对,比如你把V和M传反了,导致某个维度为0,也会出现NaN。

6.2 准确率很低(比如低于随机水平)

如果模型训练了半天,准确率始终在10%到20%晃悠(假设是60类,随机水平约1.7%),先别急着说模型不行。我遇到过的典型情况是:标签和数据不对齐。比如你的数据原来是按类别文件夹组织的,但在生成label文件时索引写错了,导致样本和标签错位。

另一种情况是归一化没做好。骨骼坐标如果不做归一化,模型输入的数值范围可能是几百到几千,这种尺度差异会严重干扰梯度计算。我见过有的同学直接用原始像素坐标做训练,准确率怎么都上不去,一做归一化之后直接提升20个百分点。

6.3 训练很慢怎么办

CPU训练ST-GCN确实很痛苦,尤其是序列长度为300、节点数为25的时候。我的建议有三条:

  • 把序列长度降到100帧,准确率损失通常不到5个百分点,但训练速度提升3倍。
  • 把batch size减小到16或32,虽然每轮迭代变多,但每次前向传播的耗时减少,整体吞吐量反而可能提升,特别是在显存不够导致swap的情况下。
  • 使用混合精度训练。PyTorch从1.6开始原生支持torch.cuda.amp,代码改动量很小:
scaler = torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): output = model(input) loss = criterion(output, label) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()

我实测能在不损失精度的前提下提升30%到50%的吞吐量。如果用的是30系或40系NVIDIA显卡,加速效果更明显。

6.4 PyTorch版本兼容性问题

最近两年PyTorch版本迭代很快,特别是PyTorch 2.x把很多API的默认行为改了。最典型的一个坑是torch.load函数在2.6版本里把weights_only参数默认改成了True,导致你加载旧模型时直接报错。解决办法是在加载模型时显式指定:

torch.load('model.pt', map_location='cpu', weights_only=False)

另外,老代码里常见的torch.nn.functional.normalizetorch.nn.utils.clip_grad_norm_这些接口在新版本里依然能用,但如果你用的是更老的代码,遇到torch.Tensor.numpy()在GPU张量上报错的,先加一行.cpu()就好。

6.5 数据增强尝试

数据增强这块很多人不在意,但对骨骼动作识别来说效果非常显著。我常用的增强手段有:

  • 随机旋转:对整段骨架序列的坐标做小幅度的2D旋转,旋转角度在-15度到15度之间随机取值
  • 随机缩放:对坐标做0.8到1.2倍的随机缩放
  • 随机时间平移:在序列起始位置随机裁剪掉几帧或补几帧
  • 随机丢弃关节:以一定概率把某些关节点的坐标置为0,模拟遮挡

这些增强操作直接作用在坐标数值上,不用转成图像,实现成本很低。我的实验数据显示,加了随机旋转和缩放之后,测试集准确率大约提高了2到3个百分点。不要小看这2个百分点,在毕设答辩里这就能成为你的创新点之一。

7. 从ST-GCN出发:你能做的扩展与改进方向

如果只是把ST-GCN跑通然后写论文,说实话这个毕设只能算及格,因为它太经典了,2018年到现在已经过了好几年。但如果你想让项目更有亮点,有几个非常自然的扩展方向。

第一个方向是替换或者改进图结构。ST-GCN的图结构是预先定义的,不会随着训练更新。但是不同动作的关节依赖关系其实是不一样的,比如“鼓掌”这个动作双手之间的关节点根本没有物理骨骼连接,但它们在空间上高度相关。基于这个思路,后来的工作(比如2s-AGCN)提出了自适应图卷积,让邻接矩阵也参与训练,模型能自己学到每一层应该关注哪些关节之间的关系。这个修改在代码上并不复杂,只需要把A从固定常量改成可学习的参数,但效果提升很明显,在NTU上能比ST-GCN高出3到5个百分点。

第二个方向是双流融合。ST-GCN只用到了关节坐标信息(Joint流),但你还可以把骨骼的长度和角度作为第二路输入(Bone流),两个流分别训练,最后把softmax得分相加。这个做法在2s-AGCN里被证明非常有效,因为它同时利用了关节位置信息和骨骼运动信息。实现上只需要写一个简单的数据预处理脚本,把关节坐标转成骨骼向量,然后训练两个结构相同的模型。

第三个方向是结合姿态估计做端到端系统。ST-GCN的输入是骨骼数据,但真实场景里你需要先有人体检测和姿态估计,才能拿到骨骼数据。如果你在毕设里把整个pipeline打通——输入视频,输出动作类别——那这个项目的实用性就会大幅提升。开源的力量很强大,OpenPose和MediaPipe都是免费的姿态估计工具,你只需要做简单的坐标映射,把OpenPose的25点映射到ST-GCN需要的25点,或者把MediaPipe的33点减少到18点,就可以直接用。

我个人在实际项目中最推荐的扩展方向是前两个:自适应图卷积和双流融合。它们改动量小、效果提升明显、论文里也容易写出“创新点”来。我帮一个学弟改过一个版本,把这两个扩展都加进去,在NTU 60的Cross-Subject上从81%提到了87%,这个成绩已经能超过很多硕士学位论文的水平了。你不需要动太多源码的核心结构,只需要在ST_GCN_Block里把A从固定值换成可学习参数,再把数据预处理改一下,两个星期就能搞定。

如果你时间更充裕,还可以试试当前更新的方法,比如CTR-GCN、PosEn等,它们在NTU上的准确率已经突破90%。但那些代码的复杂度比ST-GCN高了不止一个档次,我不建议作为毕设的第一选择——先把ST-GCN吃透、跑通、做可视化,再考虑要不要升级模型架构。毕设的本质是让你完整地走一遍技术流程:定义问题、设计方法、实现、实验、分析。ST-GCN的优雅之处在于,它把图卷积、时间序列、动作识别三个知识点完美结合,信息量足够支撑一篇高质量毕业论文,同时实现难度又是可控的。

我自己的实际操作体会是,这个项目最花时间的其实不是模型训练,而是数据准备和Debug。数据格式不对、标签错位、设备不兼容,这些问题占了我整个项目周期的六成时间。所以如果你现在刚开始做,先把数据流程跑通,再用小数据集快速验证模型能过拟合,最后才正式训练。如果一上来就拿完整数据集训练,等了三小时发现准确率只有1%,那个心态崩的程度我是体会过的。

最后再分享一个小技巧:训练完模型之后,记得把模型在几个典型样本上的预测结果导出成表格,比如样本ID、真实类别、预测类别、置信度。这个表格可以直接贴在毕业论文的附录里,证明你的实验是真实有效的。很多同学答辩时被问“你怎么证明你的结果不是过拟合”,拿出这张表配上可视化视频,比你说一百句话都管用。

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

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

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

立即咨询