027、训练策略改进(三):模型EMA、标签平滑与梯度裁剪

张开发
2026/6/8 18:58:05 15 分钟阅读
027、训练策略改进(三):模型EMA、标签平滑与梯度裁剪
一、从一次深夜调试说起上周在部署YOLOv11到边缘设备时遇到一个怪现象验证集mAP波动很大明明训练loss已经收敛但测试时性能忽高忽低。保存的最后一个epoch模型在测试集上比倒数第三个epoch掉了2.3个点。这种“模型抖动”问题在部署时尤其致命——你永远不知道用户拿到的是哪个状态的模型。排查发现问题出在训练末期模型权重在最优解附近震荡。这时候模型EMA指数移动平均就该登场了。今天我们就聊聊EMA顺便把标签平滑和梯度裁剪这两个“训练稳定器”一起讲了。二、模型EMA给权重加个惯性阻尼EMA不是新概念但在目标检测里效果尤其明显。它的核心思想是维护一个“影子权重”这个影子权重更新得比实际模型慢相当于给训练过程加了低通滤波。classModelEMA:def__init__(self,model,decay0.9999):# 这里必须用deepcopy直接赋值是引用会跟着原模型一起变self.ema_modeldeepcopy(model).eval()# 注意eval模式self.decaydecaydefupdate(self,current_model):withtorch.no_grad():# 遍历所有参数做加权平均forema_param,current_paraminzip(self.ema_model.parameters(),current_model.parameters()):# 核心公式影子 decay * 影子 (1 - decay) * 当前ema_param.data.mul_(self.decay).add_(current_param.data,alpha1-self.decay)# 重要BN层的running_mean/var也要更新forema_buffer,current_bufferinzip(self.ema_model.buffers(),current_model.buffers()):ema_buffer.data.mul_(self.decay).add_(current_buffer.data,alpha1-self.decay)几个实战细节decay值怎么设一般0.999到0.9999训练周期越长decay可以越接近1。YOLOv11里我习惯用0.9999相当于给最近10000次迭代加了平均。BN层处理上面代码里更新了BN的running统计量这个很容易漏。漏了的话EMA模型在eval时BN统计量还是初始值效果会差很多。什么时候保存训练结束后保存ema_model而不是model。验证时也用EMA模型这样验证指标才反映最终性能。内存问题EMA需要一份完整的模型副本显存紧张时可以考虑半精度EMA。但要注意混合精度训练时EMA模型最好保持FP32。三、标签平滑给硬标签加点“模糊”分类任务里one-hot标签如[0, 0, 1, 0]有个问题它告诉模型“这个类别100%确定”。但真实世界总有标注误差而且模型过度自信容易过拟合。标签平滑把硬标签变“软”defsmooth_labels(labels,num_classes,smoothing0.1): labels: 原始标签shape[batch_size] 返回平滑后的标签shape[batch_size, num_classes] devicelabels.device# 创建全smoothing/(K-1)的矩阵smoothedtorch.full((labels.size(0),num_classes),smoothing/(num_classes-1),devicedevice)# 对应类别的位置设为1-smoothingsmoothed.scatter_(1,labels.unsqueeze(1),1.0-smoothing)returnsmoothed# 在loss计算时ce_lossnn.CrossEntropyLoss(label_smoothing0.1)# PyTorch 1.10直接支持为什么有效防止过度自信模型不会把预测概率推到极端值如0.999logits输出更平缓有利于泛化。校准效果模型置信度更接近真实准确率这在部署后很重要——你希望模型说“80%把握”时真的接近80%准确率。YOLO里的特殊处理YOLO是多任务分类回归通常只对分类分支做标签平滑。smoothing取0.01~0.1太大反而损害分类能力。踩过的坑早期版本YOLO实现时有人把回归坐标也做了“平滑”结果定位精度直接崩掉。记住只平滑分类标签。四、梯度裁剪给训练装上“限幅器”梯度爆炸是老问题了但在YOLO这种多尺度、多head的网络里梯度突然暴涨的情况还是时有发生。特别是用了大batch size或某些数据增强时。# 两种常用方式# 1. 按值裁剪简单粗暴torch.nn.utils.clip_grad_value_(model.parameters(),clip_value1.0)# 2. 按范数裁剪更推荐torch.nn.utils.clip_grad_norm_(model.parameters(),max_norm10.0,norm_type2)个人偏好用clip_grad_norm_它考虑所有参数的整体梯度范数更符合优化理论。max_norm一般设1.0到10.0可以通过监控梯度范数来调整# 调试时加这段观察梯度大小total_norm0forpinmodel.parameters():ifp.gradisnotNone:param_normp.grad.data.norm(2)total_normparam_norm.item()**2total_normtotal_norm**0.5print(f梯度范数:{total_norm:.4f})什么时候需要裁剪用了RNN/LSTM结构时YOLO一般没有训练loss出现NaN值学习率较大或用了激进的数据增强混合精度训练时FP16容易溢出但注意梯度裁剪不是万能药。如果频繁触发裁剪说明训练过程本身有问题应该调小学习率或检查数据。五、组合使用时的注意事项这三个技巧可以一起用但要注意执行顺序# 训练循环里的大致顺序forbatchindataloader:# 前向计算lossmodel(batch)# 反向传播loss.backward()# 1. 先裁剪梯度在优化器step之前torch.nn.utils.clip_grad_norm_(model.parameters(),max_norm10.0)# 2. 优化器更新optimizer.step()# 3. 更新EMA在优化器更新之后ema.update(model)# 4. 清空梯度optimizer.zero_grad()经验性建议分阶段启用前期可以只用EMA等训练到中期比如50% epoch再加入标签平滑。梯度裁剪可以一直开着但max_norm设大点比如10.0相当于只防爆炸不干扰正常训练。EMA的decay可动态调整训练初期decay可以小点0.999后期加大0.9999。甚至可以用cosine衰减decay。标签平滑别过度目标检测里smoothing0.01~0.05足够。我见过有人设0.2结果小物体分类一塌糊涂。验证集监控EMA模型在验证集上的指标才是部署性能的参考。但注意验证集不要用数据增强否则评估有偏。六、写在最后训练策略改进就像给赛车调悬挂——EMA是减震器让训练曲线更平滑标签平滑是轮胎抓地力防止过拟合打滑梯度裁剪是限速器避免翻车。但调得太死车就跑不快了。实际项目中我通常这样操作先不加任何技巧跑一个baseline观察训练过程的“痛点”在哪里。如果验证集波动大加EMA如果过拟合明显加标签平滑如果出现梯度爆炸加裁剪。别一开始全堆上去否则出了问题都不知道是谁的功劳或黑锅。最后提醒一点这些技巧在论文里可能只提一句但实际实现时细节决定成败。比如EMA更新BN统计量、标签平滑只用于分类head、梯度裁剪的norm_type选择……多写几行代码少熬几个夜。

更多文章