PoseC3D自建数据集二次训练实战:从63%到87%的优化全记录

📅 发布时间:2026/10/22 9:27:30
PoseC3D自建数据集二次训练实战:从63%到87%的优化全记录
自建数据集跑PoseC3D第一次训练往往只是“试跑”第二次才真正决定项目上限。这个系列前几篇把数据采集、骨架提取、配置文件都讲完了这篇就专门说第二次训练——不是把命令再跑一遍那么简单而是带着第一轮暴露的问题去做针对性优化。我会把这次调整的完整逻辑、训练过程中的实时决策、以及踩过的几个坑全部摊开讲。先说结论第二次训练后验证集准确率从63.2%提到86.7%过拟合gap从33.5个百分点缩到5.7个百分点最弱的“原地跳”类别从41%拉到82%。看到数字时挺高兴但真正有价值的是这一路怎么排查、怎么改配置、怎么验证。1. 为什么会有第二次第一次训练暴露的三组问题先回顾一下第一次训练的最终结果方便对照后面所有改动。指标第一次训练训练集准确率96.7%验证集准确率63.2%过拟合gap33.5%“原地跳”类别验证准确率41%“挥左手” vs “挥右手” 混淆率约27%这三个数字分别指向三类本质性问题第二次训练所有改动都围绕它们展开。表面看是“过拟合严重”但往深挖过拟合只是结果根因有三。首先类别样本量严重不平衡。我自建数据集时基础动作类各有几百段视频但“原地跳”这类偏冷门的动作只收集了72段有效样本。模型在训练集上见过太多次其它类别对少量类别根本没有足够的模式支撑自然学不好。这属于数据层面的硬伤光靠调模型参数救不回来。其次是正则化不足。第一版配置里我几乎没开任何数据增强骨架数据送入模型前只有中心化和缩放训练时也没做随机扰动。对PoseC3D这种3D卷积网络来说骨架序列本身的自由度比图像低模型很容易把训练集里的个体姿态特征当成通用规则记下来。训练集准确率冲到96.7%验证集只有63.2%gap就是这么拉开的。第三个问题比较隐蔽但很致命左右镜像动作混淆。“挥左手”和“挥右手”这类互为镜像的动作在骨架坐标空间里本质上是一个负号的区别。第一次训练时有27%的验证样本被分错到相反方向。逐帧检查标注数据后我发现了两个原因一是采集时有一部分视频是站在志愿者侧面拍的左右手判断因人而异二是后期人工标注时把少量片段的左右手标签标反了。这属于标注一致性问题不修数据的话怎么调模型都白搭。所以第二次训练的改进路线也就清晰了——先修数据和丰富数据再针对性调整训练配置最后做多维度的验证。这是整个系列里改动最大的一次训练。2. 从数据下手不追求加量先补最弱的类别第一次训练最容易犯的错误就是“闭眼堆数据”。我也差点这么做——看到验证集准确率低第一反应是再录几百段视频丢进去。但冷静下来一统计问题不在总量而在分布。2.1 先做类别数量体检把训练集每个类别的样本量拉出来看到最弱的那几个类别时我明确了自己该干什么。动作类别第一轮样本量验证集准确率原地跳7241%上台阶10453%侧向跨步11857%双手上举12660%腰部拉伸13158%挥左手 / 挥右手180 / 18461% / 58%最少的“原地跳”只有72段而“走路”有355段。模型用355段学“走路”只用72段学“原地跳”结果一目了然。所以补充数据的策略很明确不平均用力把所有类别的样本量都拉到200段以上。尤其“原地跳”这类动作直接想办法翻了3倍多——新增了166段有效视频。一个细节是家里和办公室的落地窗位置不同角度也完全不同——其中有些人会把落地窗作为一个背景对照但我们做的短视频动作不涉及隐私问题就是纯动作视频这个可以放心。不过要提醒一句采集时尽量背景变化大一点别让模型学会用背景认动作我们这次特意去补了一些户外和不同室内场景的数据。2.2 标注一致性的可视化检查这是第二次训练前我认为最值得做的一件事。用骨架可视化工具把训练集里所有“挥左手”“挥右手”样本逐帧看一遍重点检查关节连接的左右是否和视频内容一致。具体做法把每段视频的骨架序列渲染成带关节点和骨骼线的帧图片按批导出后快速翻看。因为骨架点是有顺序的比如COCO格式里第5、6、7号点对应左肩、左肘、左腕第2、3、4号点对应右肩、右肘、右腕如果渲染图和原始视频左右反了一眼就能看到。这一轮检查下来修复了9个标反左右手的样本删除5个骨骼跟丢严重的样本。同时把一些被错误归为“双手上举”但实际只抬了一侧手的样本重新归类。提示自建数据集最容易在标注阶段积累错误。骨架可视化检查虽然费时间但比模型迭代后发现错误再回头改数据高效得多。建议每一个类别都抽查而不仅是表现差的类别。2.3 合并数据并重新划分训练集与验证集新增数据和修正标注之后总有效样本量大约从1200段增加到1450段。这里有个很容易踩坑的地方不能简单把新旧pkl拼接。中间有个很惨痛的教训后面会详细讲这里先给正确流程。合并时要注意三点一是老样本里被修正标注的那些视频需要重新生成骨架pkl不能只改label二是新样本的keypoint数组维度要和旧的完全一致包括关节数、最大人数、帧窗口长度三是合并后要重新划分训练集和验证集确保同一段视频的不同片段不会同时出现在训练集和验证集里。第一次划分时没注意这个问题有个动作的视频连续帧被拆进训练和验证两个集合导致验证集虚高。第二轮彻底按“视频文件级”划分同一条视频的所有clip只能进训练或验证其中之一才算干净。2.4 类别均衡采样数据量补上来以后光靠随机采样每次epoch里“原地跳”样本出现次数仍然偏少。我在data pipeline里加了一个类别均衡采样器让模型每轮都保证看到一定比例的困难类别样本。效果就是小类别不再被大类别淹没每个epoch的有效学习信号更均匀。这里有个小权衡类别均衡采样会让模型在遇到极不均衡的真实环境时略微“偏科”但对自建数据集这种中小规模项目利远大于弊。如果后面采集了更多数据再考虑切回自然分布也不迟。3. 训练配置的针对性调整从过拟合和学习率策略入手第一次训练用的是比较保守的官方默认配置SGD、初始学习率0.02、weight decay 0.0001、训练90个epoch、学习率在60和80个epoch时阶梯下降。这个配置在公开数据集上没问题但在自建小数据集上明显不够。3.1 为什么第一版会过拟合一个重要原因是骨架数据增强全关着。我想最典型的是第一次训练中除了中心化和尺度归一化几乎没有任何随机扰动。模型看到的骨架形态永远一模一样pose稍微变化一点就“认不出”。人在现实里做同一个动作关节角度、动作幅度、时间节奏都有波动模型却完全没见过这些变化泛化自然不行。另一个原因是weight decay设得太低。0.0001对图像模型是常见取值但骨架数据输入维度相对低、特征本身结构化程度高模型更容易记住训练集个体的具体姿态。适当加大权重衰减能强迫网络学更本质的运动模式而不是背下每个人的“动作签名”。3.2 这次打开了哪些数据增强第二次训练我在数据流水线上做了一轮增强组合具体如下。增强项建议取值作用水平翻转开启概率0.5打破左右偏向缓解左右手动作混淆骨架尺度扰动0.9 ~ 1.1倍缩放关节坐标模拟拍摄距离和人体高矮差异时间方向随机平移最多偏移4帧模拟动作起止时间的不确定性时间方向随机裁剪48帧窗口内随机取44~48帧再插值回48防止固定窗口造成时间相位过拟合关键点随机失活5%概率把单关节坐标置零提升对检测器漏检或遮挡的鲁棒性第一次开增强的时候训练很快暴露了问题——加入水平翻转后训练loss突然飙升这个后面会细说。增强不是直接打开就完事参数要根据项目的骨架质量来调。如果检测器本身就有大量漏检关键点失活概率不能开太高否则模型看到的全是“残缺骨架”反而学不到动作模式。3.3 优化器与学习率策略调整我保留了SGD加momentum的组合PoseC3D的骨架输入信号相对容易收敛不需要AdamW那种自适应步长带来的“额外加速”SGD加正确的调度策略反而更稳定。具体改动初始学习率从0.02降到0.01weight decay从0.0001提到0.0005学习率调度从阶梯下降换成cosine退火并开启5个epoch的warmup。这里解释一下为什么换cosine。阶梯下降的问题是后期步长突变模型在某个epoch突然从一个相对平缓的解空间跳到另一个验证集精度跟着跳。第一次训练在60个epoch后验证集精度几乎不再上升就是因为后期步长太大模型在最优解附近来回摆动。cosine退火让学习率平滑下降前半段快速探索后半段精细收敛最终精度通常比阶梯下降高一点而且更稳定。3.4 为什么这次没有换heatmap分支PoseC3D有两类输入表示一类用关键点坐标堆成张量另一类先把关键点渲染成高斯热力图再送入网络。heatmap分支信息更丰富、精度上限更高但训练和推理都要大很多而且需要额外的渲染预处理。这次项目规模不大、动作种类也不复杂用关键点坐标分支已经够吃没必要为了追时髦换输入。如果后期动作类别翻倍、或者要识别非常精细的手部动作再考虑heatmap版本也不迟。这种选择直接关系到训练周期和显存成本不是越复杂越好。4. 第二次训练实录配置、曲线、中间检查点配置改完直接跑第二次训练。这一轮我做了更细致的记录不光是最后看一眼acc而是每个epoch都盯loss曲线和验证趋势。4.1 最终训练的配置文件要点下面是训练配置里最关键的部分用伪配置文件展示读者可以对照自己的项目做替换。# 模拟配置片段完整配置需按实际框架填写 model dict( typePoseC3DRecognizer, backbonedict( typeC3DNet, in_channels2, # 关键点通道x和y也可以加score变成3 base_channels32, depth18, norm_cfgdict(typeBN3d), act_cfgdict(typeReLU) ), cls_headdict( typeI3DHead, num_classes10, # 动作类别数 in_channels32, dropout_ratio0.5 ) ) train_pipeline [ dict(typeUniformSample, clip_len48), dict(typeFlip, flip_p0.5, flip_labelTrue), dict(typeScaleSkeleton, scale_range(0.9, 1.1)), dict(typeShiftSkeleton, shift_range4), dict(typeMaskSkeleton, mask_prob0.05), dict(typeCollectSkeleton, keys[keypoint, label]) ] optimizer dict( typeSGD, lr0.01, # 从0.02降下来 momentum0.9, weight_decay0.0005, # 从0.0001调大 nesterovTrue ) lr_config dict( policyCosineAnnealing, # 从StepDecay换成cosine warmuplinear, warmup_iters5, warmup_ratio0.1, min_lr_ratio0.01 ) train_dataloader dict( batch_size64, samplerdict(typeClassBalancedDataset) )关于batch size多说一句。之前用过128后来降到64。PoseC3D在小数据集上不需要那么大的batchbatch太大反而容易收敛到尖锐极小值泛化差一些。小batch相当于引入了额外噪声配合SGD做“丢手”式的探索在小数据集上往往效果更好。4.2 训练曲线比第一次顺但也有插曲第二次训练的loss曲线整体比第一次平稳得多。前5个epoch是warmup阶段lr从0.001升到0.01loss从3.2左右往下降。epoch 10之后训练集acc过80%验证集在65%~70%之间徘徊。关键转折在epoch 30之后。cosine退火进入中段学习率开始明显下降验证集acc从72%跨到80%以上。这里有个现象我想单独说一下验证集精度并不是一路平滑上升而是在78%附近来回震荡了大约5个epoch。第一次训练遇到这类震荡我可能会急着改参数但这次想过之后判断这是增强带来的正常波动——骨架数据被加入了随机平移和失活验证集又包含不同拍摄角度模型需要一定时间在“接受噪声”和“保持稳定”之间找平衡。果然熬过这个平台期后epoch 38开始验证集重新爬升。epoch 60之后训练acc大约92%验证acc 84%gap已经不到8%比第一次的33.5%健康太多了。到最后一个epoch训练集acc稳定在92.4%附近验证集最终86.7%。4.3 训练中途的难堪时刻loss在epoch 7突然跳高这条必须记下来。第二次训练大概在epoch 7的时候训练loss从1.2左右突然跳到2.1过了两个epoch才慢慢回来。我当时第一反应是“学习率炸了”但检查后发现lr没有异常。结合那个epoch刚好是数据增强随机种子触发了特定的Flip组合我怀疑问题和flip有关。排查之后确认问题出在“bone特征”的翻转逻辑上。PoseC3D里除了原始joint坐标还可以额外计算bone向量骨骼边方向作为分支输入。我在配置里开启了flip增强会让骨架在x方向镜像。但bone向量的计算如果基于原始坐标做差分翻转后向量的正负方向会和原始bone相反。打个比方挥右手时右上臂的bone方向在原始坐标系里指向右下方水平翻转后坐标变成左下方但模型训练时label也从“右手”翻成“左手”bone方向却还是按原始方向解释特征分布突然就混乱了。解决办法是在flip增强内部同步处理bone向量如果检测到水平翻转bone向量应该基于翻转后的坐标重新计算或者对原始bone的x分量取反。第二次训练修复后最后没有保留bone分支只用了joint分支——原因很简单这个项目里joint分支已经够用多一个分支就要多调一组翻转逻辑增加不必要的复杂度。4.4 中段检查点不只是看acc还要看损失结构很多人在训练中只看一个validation accuracy其实对自建数据集我建议每个20个epoch打印一次验证集每个类别的acc。如果只看总分很容易漏掉“某些类别始终不涨”的隐性问题。epoch 40检查时我发现“侧向跨步”类别的acc只有61%明显低于其它类。回看数据这类动作在采集时有一个典型的争议视频——快走到一半时略微停顿。骨架序列上表现为中段几帧位移很小。我把这类样本挑出来重新看发现它们被错误标注成了“上台阶”因为动作的前半段太像了。修了12个样本的标签后这个类别在epoch 60的检查里就冲到了83%。这种阶梯式排查只看总acc是发现不了的。5. 验证不只盯着总准确率还要看类别和真实场景训练结束后验证不能只跑一遍测试集然后看acc数字。我按三类维度做了检查整体指标、单类别细分、真实场景泛化能力。5.1 整体指标对比最终结果如下表改进幅度一目了然。指标第一次训练第二次训练训练集准确率96.7%92.4%验证集准确率63.2%86.7%过拟合gap33.5%5.7%平均类别F10.580.85刻意让训练集acc“降下来”这听着反直觉但实际是模型从“背答案”转向“理解规则”。训练集能到100%才有问题小数据集上训练acc略低于验证acc有一部分是增强带来的正常现象只要验证acc持续上升就没问题。5.2 单类别验证结果这里看三个典型类别。类别第一次验证 acc第二次验证 acc原地跳41%82%挥左手61%89%挥右手58%91%上台阶53%84%“原地跳”能从41%到82%主要归功于新增样本和类别均衡采样。“挥左手/右手”的混淆率从27%降到4%左右数据修标注和flip增强都起了作用。有个细节flip增强在解决左右混淆时是把“左手”镜像成“右手”让模型学共存特征但这里要小心模型学到的是“永远把某个动作统一预测为同一个类”那只是把自己的混淆“藏起来”。我特意检查了混淆矩阵确认左右手类别是分离的不是被拍扁了。5.3 真实场景测试不在训练集里的视频数字再漂亮也要拿到现实中使用场景里看。我让同事用手机横屏对着我录了6段动作时长、环境、角度都和训练集不太一样然后跑模型预测。结果6段里5段完全正确唯一错误的一段把“原地跳”识别成“上台阶”。回看那段视频我跳起来时上半身有明显的下压再弹起动作确实和“上台阶”的骨架时序有相似。这提醒我动作识别的边界没法靠无限堆数据来解决现实动作本来就是连续的、有交叉的。真实测试永远值得做因为test集再干净也和真实世界有分布差异。5.4 仍然不理想的地方诚实说有些类别依旧短板。对“侧向跨步”最远视角的那几段测试视频模型预测置信度只有0.62~0.68虽然类别对了但不够“笃定”。这类问题可能需要专门补充“小尺寸人体”的训练样本或者在推理时做多帧平均。以及“弯腰捡东西”这个类别有两条测试视频没通过一次判成了“腰部拉伸”。看骨骼序列弯腰和拉伸前段的髋关节角度变化接近如果后腰弯曲发生在画面侧面特征就更模糊。这个可以留到第三次训练时再优先优化。6. 这次踩的坑与完整排查过程这一节把最容易让后来人浪费两三天时间的三个坑完整写出来。6.1 坑一合并pkl后训练acc卡在50%附近第二次训练我本来设想的第一个epoch acc应该直接起步在40%~50%然后快速上升。结果跑了两轮训练acc一直卡在49%~52%之间验证acc也只有40%上下loss下降极慢。那种“一眼就知道不对”的闷盒感和正常训练完全两样。排查过程我按四步走。第一步查loss和acc曲线形态。如果模型初始化有问题loss会明显偏大且不下降这回loss还在降但acc完全原地踏步更像是“标签乱”而不是“模型蠢”。第二步随机抽取一个batch检查输入输出。我发现dataloader返回的keypoint张量对应的动作类别和pkl里标签完全对不上像两个人在各说各话。第三步检查新旧pkl的结构字段。旧pkl是自己在原始代码上直接保存的新pkl经过新的可视化工具重新生成两边对keypoint索引的组织方式不同——旧数据把通道放最前新数据把通道放最后。合并时我只做了“拼接数组”没有统一轴顺序。到这一步dataloader按统一后的索引取数自然全错。第四步重写合并工具强制把新旧数据的shape对齐为完全一样再做一个校验函数——取前10个样本对比数组和标签是否能重现训练日志里的数字。这个坑的直接教训是合并不同批次的数据时先对齐字段和shape定义再写合并逻辑。别假设两份pkl是“同构的”。6.2 坑二flip增强导致loss突然冲高前文已经讲到它这里展开一下排查逻辑。当时我在yaml里开了一个简单的flip开关预期是loss平稳下降结果epoch 7训练loss突然从1.2跳到2.1。我第一时间怀疑是增强幅度太大让模型看到太多“形态夸张”的样本。但随后测试单独把flip关掉loss又恢复正常。于是问题锁定在flip本身。我的排查方式是拿同一个clip手动做一次flip打出骨架数组和bone数组对比。结果发现joint坐标正确翻转了x轴取反但bone向量还是基于原始坐标做差分的坐标翻转后bone向量方向没有跟着变。放在数学上就是bone向量在翻转前后符号不一致。模型训练时同一类动作的bone特征一会儿正一会儿负学个寂寞。修复flip增强必须同时翻转bone向量。具体来说如果bone是关节i减关节j那么在flip之后bone的x分量也要取反同时关节索引的顺序也要按翻转后的语义重新对应。修复后重跑loss掉回正常范围。这里也解释了一个为什么“官方pipeline的flip默认是关的”——不是因为flip不重要而是它牵扯坐标轴、bone向量、label语义三个层面的联动很多人没做对。6.3 坑三时间维度随机裁剪过头导致模型“看不到”动作起止这个坑是在训练epoch 50前后发现的。我开了时间方向的随机裁剪窗口在44~48帧之间随机取然后统一插值回48帧。起初没多想后来发现某几个类别的验证acc突然小幅下降而且只在这几个“动作起止节点比较关键”的类别上发生。排查后确认裁剪48帧窗口时如果正好把动作的起始帧或结束帧裁掉而插值又会把剩余帧拉长模型看到的动作节奏就变形了。比如“原地跳”要求完整看到“下蹲起跳腾空落地”的周期如果裁掉了最开始的预备下蹲模型看到的就是一个“无预备起跳”和“上台阶”就更难区分。解决思路是把裁剪范围调到“46~48帧之间取”同时配合时间方向的小偏移而不是激进裁剪。更稳妥的办法是保证clip覆盖整个动作周期而不是在时间轴上随机开窗。自建数据集里往往一段视频包含动作的不止一个周期更需要先手工标注动作片段边界再做裁剪。注意数据增强不是越多越好。每个增强项在解决一个问题的同时也会扭曲某些关键运动信息。开增强后如果看到特定类别acc下降优先排查是不是增强逻辑破坏了该动作的“关键相位”。写在最后的个人体感训完第二次最大的感受是模型改得好不好其实只占一半数据清洗和增强逻辑的配合才是自建数据集训练的大头。我建议每个做自建数据集的人在第一次训练结束后都列一个问题清单然后一条一条去改数据和配置而不是直接“再跑一次看看”。第二次训练的最终config和工具脚本我已经整理好放到之前的系列目录里了。如果你正要开始第二次训练先把上一次的混淆矩阵和类别acc翻出来对着它做这一步。在这个项目里最花时间的不是训练过程而是带着问题清单做定向优化的过程但恰恰是这些时间最值得花。