ST-GCN骨骼动作识别:时空图卷积网络原理与PyTorch实践 简介本资源是一套完整的基于PyTorch实现的时空图卷积网络ST-GCN骨骼动作识别毕业设计项目面向计算机、人工智能及相关专业本科生解决人体动作识别这一典型时序-结构建模问题适用于毕业设计、课程设计及期末大作业等实践场景。压缩包共90个文件含29个核心Python源码涵盖数据预处理、双流ST-GCN模型构建、离线/实时推理演示、13个YAML配置文件支持NTU-RGBD与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 RGBD数据集一个非常常用的骨骼动作识别基准上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 python3.8 conda activate stgcn pip install torch1.10.0 torchvision0.11.0如果你是NVIDIA显卡可以先确认自己的CUDA版本然后到PyTorch官网选择对应的安装命令。比如CUDA 11.3的安装命令是pip install torch1.10.0cu113 torchvision0.11.0cu113 -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 RGBD还是自建数据做骨骼动作识别最常用的两个开源数据集是NTU RGBD和Kinetics-Skeleton。NTU RGBD有60个动作类别包含单人动作和双人交互动作样本量超过5万是目前学术界的标准benchmark。Kinetics-Skeleton是从Kinetics视频数据集里用OpenPose提取出来的骨架数据类别多达400类但数据质量参差不齐因为视频来源比较杂。我建议做毕设的同学优先用NTU RGBD。原因很朴素数据质量高、动作类别统一、样本划分标准明确。NTU官方给出了两种划分方式——Cross-Subject按人物划分训练集和测试集和Cross-View按摄像机视角划分。你直接用Cross-Subject的划分方式就能和论文里的结果做对比。但NTU RGBD的数据集需要去官网申请审核需要时间如果你是急着做毕设可能等不起。这时候有两条路一是用精简版或别人已经处理过的npy格式数据GitHub上很多ST-GCN开源项目都会附带一个小的示例数据集比如NTU的子集或自采样的数据二是自己用OpenPose提取骨架数据。自己提取骨架的流程是先用OpenPose处理视频帧得到每个人18个关节点或25个关节点的坐标和置信度然后按时间顺序拼接成序列。需要注意的是OpenPose输出的坐标是图像像素坐标直接进模型会有尺寸偏差问题所以要先做归一化。我的做法是找到整个序列里人体所有关节点坐标的均值然后以这个均值为中心做平移再除以坐标标准差让数据分布落在0附近。这一步非常关键不做归一化的话模型很难收敛。3.3 数据划分与标签文件格式ST-GCN源码的数据读取通常依赖一个ntu_gendata.py或feeder模块它读取的是npy格式的关键点数组和对应的标签文件。这里我先说一个很多小白容易懵的地方数据文件里一个样本的形状到底是什么。以NTU RGBD为例原始数据里每个样本是(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 sizeC是通道数3T是帧数300V是节点数25M是人物数2。如果你用的是COCO格式的18个关节点那V就是18M一般就是1。做数据预处理时很多人会踩一个坑把过长的序列截断把过短的序列补零。NTU官方有一个约定超过300帧的裁剪到300帧不足300帧的在末尾补零。但这个补零操作其实对模型很不友好因为补零区域模型不知道怎么处理容易产生虚假的特征。我自己实验下来更推荐的做法是把整个序列均匀采样到固定长度比如100帧而不是简单地截断或补零。采样后的序列虽然帧数变少但时间上的运动趋势保留得相对完整训练出来的模型泛化能力反而更好。如果不会写这个采样逻辑我可以给你一个最简单的版本def uniform_sample(data, target_num_frames100): # 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, stride1, residualTrue): super().__init__() self.A A # (3, V, V) 三个子集的邻接矩阵 self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size1) 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_size1, 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_size9只在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输出通道64stride1不降采样时间长度ST-GCN Block 2-364→64stride1ST-GCN Block 464→128stride2时间维度减半ST-GCN Block 5-6128→128stride1ST-GCN Block 7128→256stride2时间维度再减半ST-GCN Block 8-9256→256stride19个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, dim1) pred_class torch.argmax(probs, dim1).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_locationcpu, weights_onlyFalse)另外老代码里常见的torch.nn.functional.normalize、torch.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、真实类别、预测类别、置信度。这个表格可以直接贴在毕业论文的附录里证明你的实验是真实有效的。很多同学答辩时被问“你怎么证明你的结果不是过拟合”拿出这张表配上可视化视频比你说一百句话都管用。本文还有配套的精品资源点击获取