☰
PoseC3D二次训练实战:从61%到82%的调优记录
2026/10/22 4:20:00 网站建设 项目流程

二次训练这件事,听起来比第一次平淡,实际上才是真正磨数据、磨配置、磨耐心的阶段。第一次训练我在自建数据集上跑完 PoseC3D,验证集 Top-1 只有 61.3%,训练集却冲到 98.7%——典型过拟合,而且不是一般的过拟合,是那种数据、划分、采样策略全都有问题的过拟合。第六篇就专门讲讲第二次训练,怎么从 61% 这种“能跑但没法用”的状态,一步步调回到 82% 左右的稳定水平。

这篇内容适合正在自建动作数据集、用 PoseC3D 或者类似骨架模型做训练的人。如果你已经跑通了第一遍全流程,但对结果不满意,这篇就是给这种“半成品训练”擦屁股的实战记录。

1. 复盘第一次训练:成绩单与错误点

1.1 第一次训练的配置与结果

先把第一次训练的配置摆出来,后面对比起来才清楚。

我当时的数据情况是:自己采集并标注了 8 类动作,每类样本数量很不均匀。少的类别只有 60 多段,多的类别接近 700 段,总共 1500 段训练片段、200 段验证片段。骨架数据用的是 17 个关键点的 COCO 格式,输入时序长度固定为 30 帧,主干是 PoseC3D 默认的 3D 卷积网络。

第一次训练我图省事,直接套了类似图像分类的套路:

  • 优化器:Adam,初始学习率 1e-3
  • 损失函数:普通交叉熵
  • 训练轮数:40 epoch
  • 批大小:64
  • 数据增强:只做了轻微的关键点坐标归一化,没有任何扰动
  • 训练集和验证集随机按“片段”划分,没有严格隔离人员

这个配置跑完,训练集 Top-1 是 98.7%,验证集 Top-1 是 61.3%。我第一反应是模型太简单、数据太少,后来一项项排查才发现,问题根本不在模型,而在好几个环节都有隐藏坑。

1.2 为什么第一次训练会过拟合

很多人一看到训练集 98%、验证集 61%,第一反应就是“加数据增强”“加 Dropout”“换小模型”。但 PoseC3D 这种基于骨架的模型,和图像分类的过拟合来源并不完全一样。

我复盘后认为,第一次训练的过拟合主要有三个来源。

第一个来源是数据划分泄漏。我当时是按“片段”随机分训练和验证,但同一个人的同一组动作往往被采集了好几段,这些片段之间只是拍摄角度、位移略微不同,骨架序列高度相似。于是验证集里混进了大量训练集的“近亲样本”。这种情况下,模型即使没学到真正的动作泛化特征,只要死记了这些相似序列的倾向,训练集指标也会很好,验证集上那些“稍微新一点”的同类样本反而识别不准。

第二个来源是长尾类别问题。数量最少的类别“跌倒”只有 60 多段,在 40 个 epoch 的训练里被反复看过很多遍,模型很容易把这几个样本彻底背下来。普通交叉熵对每个样本一视同仁,头部类别梯度很大,尾部类别过拟合到几个具体样本上,验证集遇到新样本就崩。

第三个来源是时序采样没做好。我第一版是直接把每段视频均匀抽成 30 帧。但自建数据采集的视频长短不一,动作速度也不同,均匀采样会把一个 2 秒的动作压成 30 帧,也可能把一个 5 秒的动作硬截断成 30 帧,导致同一类动作的关键点时序模式差异极大,模型只能记住那些“采得巧”的训练样本。

1.3 必须纠正的三个关键错误

复盘之后,我把问题收敛成三个必须纠正的关键错误:

第一,训练集和验证集必须按照“人员”或者“场景”划分,不能简单按片段随机切。

第二,类别不平衡必须显式处理,不能指望模型自己从交叉熵里悟出来。

第三,帧采样策略不能一刀切,要根据动作时长分布,设计合理的采样方式。

这三个问题都和数据有关,而不是模型结构。这也给了第二次训练一个很明确的调优方向:先动数据,再动训练策略,模型结构本身不需要大改。

2. 数据层面的修正:比调参更难察觉的细节

2.1 骨架数据的再清洗策略

第一版骨架数据是直接跑的 2D 姿态估计模型输出的关键点坐标序列,并没有做太多后处理。我第二次训练前专门写了一个清洗脚本,重点检查三件事:

一是单帧关键点置信度。正常情况下每个关键点有一个置信度分数,自建数据里经常出现背面、遮挡、快速运动模糊导致某几个关键点置信度极低。如果直接把这类帧灌进训练,相当于给模型喂了错误的位置信息。我的处理方式是:置信度低于阈值的帧,如果连续少于 3 帧,用前后帧线性插值补上;如果连续超过 10 帧,直接把这段从样本里剔除。

二是整个人体的尺度变化。同一个动作,离镜头近的人和远的人,关键点坐标绝对数值差异很大。第一次训练只做了简单归一化,相当于把所有关键点减均值除标准差,但没有按人体中心跨度和肩宽做尺度归一。第二次训练改为:以髋部中心为原点,把所有关键点坐标减去中心点,再除以人体左右肩宽的平均值。这样处理后,距离、身高、体型的影响被大幅压低。

三是关键点的“抖动”噪声。姿态估计模型输出的逐帧关键点天然存在抖动,尤其手部、脚腕这些末端点,每帧跳动几个像素是常态。我对关键点坐标做了滑动窗口平滑,窗口大小 3 帧,用简单均值滤波。这一步不能做太狠,窗口大了会把动作细节磨没,3 帧是我试下来比较稳的值。

2.2 帧采样策略:从单一均匀抽帧到分段随机采样

第一版均匀抽帧的问题前面说过了。第二次训练我改成 PoseC3D 更常用的 TSN 式分段采样思路:

对每段视频,先计算有效动作帧区间。因为自建视频一般开头和结尾都有多余的站立、走动、准备帧,全部纳入训练只会让模型更难学。我用一个简单规则截取有效区间:双手或双脚关键点位移速度连续超过阈值的帧段,取最大连续区间的起点和终点。

有效区间确定后,把它分成 30 个等长段,训练时每段随机抽一帧,验证时每段取中间帧。这样做的好处是,即使动作快慢不同,模型也能看到动作不同阶段的关键帧,而不是被固定的均匀抽帧节奏带偏。

对于明显太短的视频片段,比如只有 20 帧以内的动作,我的处理是周期性重复补齐到 30 帧。这里注意不要简单复制最后一帧,否则会引入一段静止状态,让模型误以为所有动作最后都是“停顿”。

2.3 训练/验证划分重新设计

数据划分是我这次改得最狠的部分。我把全部采集人员分成 5 组,第 1、2、3 组作为训练集,第 4 组作为验证集,第 5 组作为测试集,确保验证集里出现的人的骨架序列,训练集完全没见过。

这种按人划分的方式会让验证集指标比原来“难看”一些,因为没有了近亲样本,泛化难度真实暴露出来了。但它才是自建数据集训练的正确姿势。

划分完之后我重新统计了类别的分布,发现验证集中某些类别的样本数量更少,于是后续引入了类别权重。

3. 第二次训练的模型与超参数配置

3.1 主干与输入规格

第二次训练我没有换主干结构,仍然是 PoseC3D 默认的 3D 卷积。输入规格上,我只做了一个重要调整:把时序长度从固定 30 帧放宽到“输入 30 帧,但采样时允许随机偏移”。简单说,训练时每段样本可以从有效动作区间的多个起点开始采样,而不是死板地从头开始。这相当于给时序维度加了一层免费的平移增强。

关键点通道数保持 17,每个关键点坐标是 (x, y) 两维,所以模型输入形状是 (B, 34, 30, 17, 1)。如果你用官方 mmaction2 的 Skeleton 数据格式,还需要把坐标重新组织成 (B, M, T, V, C) 的格式,其中 M 表示人物数量,单人的时候设为 1 即可。

3.2 优化器、调度器与损失配置

第一次用的 Adam 在小数据集上收敛很快,但也容易过拟合。第二次我换成了 SGD,动量为 0.9,初始学习率 1e-2,权重衰减 5e-4。这里说下原因:PoseC3D 这种骨架模型输入的维度并不高,Adam 的自适应学习率在训练后期容易让权重在小梯度方向反复横跳,泛化性反而不如 SGD 配合退火来得稳。

学习率调度我用了余弦退火,并且加了 5 个 epoch 的线性热身。具体是这样设置的:

  • 前 5 个 epoch,学习率从 0 线性升到 1e-2
  • 第 5 到第 40 个 epoch,学习率按余弦曲线从 1e-2 降到 0
  • 总训练轮数 40 epoch

损失函数改成了带类别权重的交叉熵。权重计算方式我采用了类频率倒数的平方根,而不是直接倒数。直接倒数的权重会把“跌倒”这样的少样本类别抬得太高,导致模型为了少样本类别牺牲大部分类别的精度。平方根形式是一种折中,既提高了少样本类别的梯度贡献,又不至于喧宾夺主。

3.3 配置修改前后对比表

我把两次训练的核心配置放在一起,方便直接对照:

配置项第一次训练第二次训练
优化器AdamSGD(momentum 0.9)
初始学习率1e-31e-2
学习率调度固定/无5 epoch 热身 + 余弦退火
训练轮数4040
批大小6432
损失函数普通交叉熵类别权重交叉熵(频率倒数平方根)
数据划分片段随机按人员划分
帧采样均匀抽帧分段随机采样 + 随机偏移
数据增强无关键点抖动、尺度、平移、遮挡模拟
骨架归一化简单减均值除标准差髋部中心化 + 肩宽尺度归一 + 平滑滤波

这张表基本就是第二次训练的“处方”。我没有改任何模型结构参数,所有的提升都来自数据和训练策略的修正,这一点也侧面说明,自建数据集训练时,数据管线的坑往往比模型本身更大。

4. 完整训练流程:从数据处理到训练脚本运行

4.1 准备训练与验证列表

第二次训练前,我用脚本把数据整理成了 mmaction2 需要的格式。每个样本是一行,包含:

相对路径/xxx.pkl 30 1

其中第一个数字表示有效帧长度,第二个数字是类别标签。训练列表 train_list.txt 和验证列表 val_list.txt 分开生成。实际运行前一定要检查两个列表的人员是否有交叉,我写了一个简单脚本统计每段视频对应的采集人员 ID,一旦发现验证集出现训练集的同人样本就立刻剔除。

骨架数据结构方面,PoseC3D 官方要求每个样本是包含 keypoint、keypoint_score 的 numpy 数组。我建议直接效仿官方动作识别工具包的数据处理方式,用如下结构保存:

{ "keypoint": np.zeros((M, T, V, C)), "keypoint_score": np.zeros((M, T, V)) }

我用的单人数据集,M 就是 1。如果视频里有两人同框,需要额外处理人员匹配问题,这个我在第五篇里已经提过,这里不再展开。

4.2 实际训练脚本的关键配置

如果你用的是基于 mmaction2 二次开发的训练脚本,第二次训练我直接在配置里改了这几个字段:

# optimizer optimizer = dict(type='SGD', lr=0.01, momentum=0.9, weight_decay=5e-4) optimizer_config = dict(grad_clip=None) # lr scheduler lr_config = dict( policy='CosineAnnealing', min_lr=0, warmup='linear', warmup_iters=5, warmup_ratio=0.1 ) # loss loss_cls = dict( type='CrossEntropyLoss', loss_weight=1.0, class_weight=class_weight_array ) # data augmentation train_pipeline = [ dict(type='UniformSampleFrames', clip_len=30, num_clips=1), dict(type='PoseDecode'), dict(type='PoseCompact', hw_ratio=1.0, allow_imgpad=True), dict(type='Resize', scale=(-1, 64)), dict(type='RandomResizedCrop', area_range=(0.8, 1.0)), dict(type='Resize', scale=(56, 56), keep_ratio=False), dict(type='Flip', flip_ratio=0.5), dict(type='GeneratePoseTarget', with_kp=True, with_limb=False) ]

这里我特别说明两点。第一,UniformSampleFrames配合num_clips=1就是分段随机采样;第二,翻转增强对于左右对称的动作类别很有效,但对“左右手分别做动作”的类别会造成标签歧义,需要根据自己的类别体系决定要不要开。

4.3 训练过程中的检查点与验证策略

我在训练脚本里设置了每个 epoch 结束都跑一次验证,并保存验证集 Top-1 最高的检查点。之所以每轮都验证,是因为小数据集训练时最优模型往往不是最后一个 epoch 的模型,而是中间某个时刻。

训练过程中我观察到了几个现象。前 5 个 epoch 由于学习率从 0 热身,loss 下降看起来比较慢,但这时模型权重还在稳定收敛,不用慌张。大约第 10 个 epoch,验证集 Top-1 涨到 70% 以上。到第 20 个 epoch 左右,验证集 Top-1 开始稳定在 78% 到 81% 之间波动。

到了第 32 个 epoch,验证集 Top-1 达到了最终的 82.4%,之后没有明显提升。

4.4 中间被忽略的评估细节

同类模型在验证时有“多线程平均”的说法。我第一次只用单片段验证,第二次训练最终评分采用了多片段投票:把每段视频分段随机采样 10 次,分别推理并取平均概率作为最终预测。这个操作通常能再提升 1 到 2 个点的 Top-1。我的测试集最终成绩比验证集高一点,也和这个机制有关。

5. 试验对比:第二次训练收获了什么

5.1 准确率指标对比

我直接用表格呈现两次训练的结果差异:

指标第一次训练第二次训练
训练集 Top-198.7%93.2%
验证集 Top-161.3%82.4%
测试集 Top-1未测83.7%
验证集损失持续震荡稳步下降后收敛
最优模型出现 epoch第 39 轮第 32 轮

第二次训练训练集 Top-1 反而比第一次低,这一点很容易被误解。我自己的理解是,第一次那种 98.7% 是“记住样本”,第二次的 93.2% 才是“学会动作”。验证集和测试集的大幅提升说明泛化能力是真的变强了。

5.2 单类别准确率的改善与剩余问题

我重点看了混淆矩阵里几个过去容易错的动作。第一次训练里,“弯腰”和“蹲下”经常互相误判,因为两者的腿部关键点角度变化很接近。第二次训练修正数据划分和尺度归一化后,这两类的混淆率明显下降。原因在于,尺度归一化让腿部坐标的肩膀宽度比例更一致,模型不再被“离镜头远近不同造成的腿部绝对位移差异”干扰。

“挥手”和“拍手”仍然有少量交叉,这是合理的,因为二者手部路径有重叠,单靠骨架序列区分本来就困难。我没有强行追求百分百分清,后续如果真的要区分这类近似动作,只能再采集更多包含肩肘角度差异的样本来补强。

5.3 这套配置的可复现性说明

如果你也想复现第二次训练的结果,我的建议是直接照搬这一套配置:

  • 骨架数据必须经过清洗、平滑、尺度归一
  • 训练/验证必须按人员或场景划分
  • 类别权重采用频率倒数平方根
  • SGD + 5 epoch 热身 + 余弦退火
  • 每轮验证并保存最优检查点
  • 验证阶段使用 10 次多段投票

这套配置我在类似规模的另一组数据上也跑过一次,结论是相似的。它不一定是最优解,但作为自建小数据集的起点,远比上来就用复杂随机搜参更可靠。

6. 常见问题与排查清单

每次训练都会遇到一些奇怪的错,第二次训练也不例外。我把踩过的坑整理成速查表:

现象可能原因解决办法
验证集 Top-1 一直卡在 60% 左右数据划分泄漏或帧采样方式错误按人员划分、改用分段随机采样
训练集快速到 99%,验证集 50%无数据增强 + 学习率过大加入关键点扰动增强、降低初始学习率
少样本类别完全不识别类别权重缺失或权重过大使用频率倒数平方根做类权重
验证集曲线剧烈震荡学习率调度没有热身加 5 epoch 线性热身
骨架坐标数值跨度极大只减均值没做尺度归一按肩宽归一化关键点坐标
输入形状报错M、T、V、C 顺序错误检查是否转成 (B, M, T, V, C)
中断后无法恢复训练没有保存最近 epoch 检查点设置每 epoch 保存,并记录优化器状态

排查顺序我个人的习惯是:先看数据划分有没有泄漏,再看帧采样和输入格式对不对,最后才是学习率这些训练参数。因为数据层面的错误往往会让模型陷入“怎么调都无效”的怪圈。

根据我的经验,如果你第一次训练结果很差,先别急着换模型结构,把这篇里提到的数据清洗、数据划分、采样策略和优化器配置都过一遍,大概率能涨回 15 到 20 个点。自建数据集的项目,真正值钱的往往不是网络改得多花哨,而是把数据管线里的细节打磨到什么程度。

最后再分享一个实操体会:二次训练时,我会额外存一份“最后一次 epoch 的模型”和“验证集最优模型”,不要只留最优模型。因为有一些后续推理场景需要复现训练曲线,只保留最优模型会让你丢失中间过程的调试信息。这个小习惯在后续继续数据扩增时特别有用。

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

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

立即咨询