
前阵子在调一个基于YOLOX改进的小目标检测模型训练到第80个epoch回归分支的loss突然开始剧烈震荡再往上跑几轮直接变NaN。查了一圈数据增强、学习率调度都没问题最后把定位分支的L2损失换成了Smooth L1 Loss顺手用EMA把梯度稳住训练才算回到正轨。这件事让我一直想好好聊聊目标检测里的回归损失函数。很多新入坑的朋友会把目标检测当成一个复杂的分类问题但真正让训练过程变得娇气的往往是边界框回归这一路。这篇就从最经典、也几乎所有检测框架都绕不开的Smooth L1 Loss说起把它的数学定义、梯度行为、工程实现和调参经验一次性讲透适合刚入门目标检测的读者也适合想系统梳理损失函数脉络的老手。1. 回归损失在目标检测里到底在优化什么1.1 检测网络不回归像素坐标而是回归偏移量很多第一次看检测模型代码的人都会困惑网络输出的bounding box回归头明明有四个通道为什么训练标签不是这张图里的(x1, y1, x2, y2)这里需要先建立一个底层认知绝大多数目标检测器不会让网络直接回归绝对像素坐标而是回归一个经过编码的偏移量。Faster R-CNN这类anchor-based方法里回归目标通常是相对anchor的四个值tx (x - x_a) / w_aty (y - y_a) / h_atw log(w / w_a)th log(h / h_a)其中x_a、y_a、w_a、h_a是anchor的中心坐标和宽高x、y、w、h是ground truth框的参数。换成anchor-free方法比如FCOS回归目标变成了当前feature map位置到目标框四条边的距离或者中心点到目标中心再加宽高。但不管形式怎么变本质上都是在做同一个事用一个有限量级的相对量去表示目标框相对于某个参考位置的偏移。为什么要绕这一圈因为绝对坐标的范围波动太大。在一张1920x1080的图上目标可能出现在x50也可能出现在x1800如果我们让网络直接回归坐标值那么同一个回归头要同时处理几十倍范围的数值网络很容易顾此失彼。而编码成偏移量之后大部分正样本的目标值会落在[-1, 1]附近数值范围稳定网络只需要学会输出一个小的修正量压力小很多。这个数值范围正好和Smooth L1 Loss的分段阈值强相关后面会专门说。1.2 正负样本不均衡回归分支天然只对“少数派”负责分类分支的输入是所有anchor或所有候选位置其中有大量负样本需要不同策略去处理。而回归分支不同它只对正样本计算损失——那些负样本根本不会产生回归梯度。这意味着回归分支的训练信号永远来自一小部分与目标框匹配上的位置。这是理解回归损失函数的关键背景。因为回归正样本数量少每个样本的梯度就会被放得很大如果再选一个对异常值敏感的损失函数只要有几个标注得不太准的框训练就可能出问题。Smooth L1在历史上一度成为检测框架标配恰恰是因为它对正样本中的离群值足够稳健不会因为几个异常框就把梯度主导权抢走。1.3 损失函数决定的不只是“能不能收敛”而是“怎么收敛”优化器负责根据梯度更新参数但梯度的大小和方向是由损失函数决定的。同一个检测头用L1 Loss、L2 Loss和Smooth L1 Loss训练出来的前期行为会完全不同。有的损失会让模型在初期快速靠近目标但后期不稳有的会让模型前期动荡但后期收敛细致。所以在进入Smooth L1的公式之前先建立这样一个观念损失函数是对训练过程的“行为约束”。它不只是一个数值指标它直接控制每个样本对参数更新的影响力。这就是为什么目标检测的回归损失一直是研究热点。2. 从 L1 和 L2 的“偏科”看 Smooth L1 的设计动机2.1 L1 的性格稳定输出但接近目标时容易“蒙圈”L1 Loss的表达式是|x - y|也就是误差的绝对值。设x表示预测与目标之间的差值那L1 Loss就是|x|它对x的导数是当x 0时梯度 1当x 0时梯度 -1当x 0时不可导这个性格很鲜明无论误差是10还是0.1梯度的模长都恒定是1。好处是数值极其稳定任何一个离群样本都不会产生特别夸张的梯度训练几乎不会因为某个异常框而爆炸。坏处是当误差已经很小、比如0.01时梯度仍然有1这么大网络无法感知“我已经很接近目标了”只会继续以同样的步长去修正结果就在最优值附近反复横跳很难把loss磨到更低的水平。2.2 L2 的性格快速逼近但面对离群值容易“上头”L2 Loss就是均方误差表达式为x^2导数是2x。误差越大梯度越大误差越小梯度越小。这看起来很好在接近最优值时梯度接近0网络能精细收敛。但问题恰恰出在误差大的地方。目标检测的正样本框千奇百怪很多框在一开始预测得是很离谱的。如果某个样本的误差是5梯度就是10而其他正常样本的梯度可能只有0.5那这一个离谱样本就会把参数更新方向完全带偏导致loss曲线剧烈震荡甚至触发梯度爆炸。换句话说L2对离群值太敏感了一言不合就上头。早期检测训练里很多人遇到NaN一半是learning rate太高另一半就是L2在定位分支作祟。2.3 Smooth L1 的分段表达式0.5x^2 和 |x| - 0.5Smooth L1的设计思路很直接把L1和L2的优点捏在一起。当|x| 1时使用0.5 * x^2误差小时梯度随误差减小而减小保证后期精细收敛当|x| 1时使用|x| - 0.5误差大时梯度模长为1不会像L2那样失控。整个函数在x 1处左侧值是0.5右侧值也是0.5导数左侧是1右侧也是1连续又可导。这个函数在统计学习里也叫Huber Loss是稳健回归的经典选择。Fast R-CNN论文把这个思想引入检测领域之后它就成了框回归的事实标准。它和L1、L2最大的区别是它允许模型在大误差时保持“稳健”在小误差时保持“精细”两种模式自动切换而且切换得很平滑。2.4 阈值 1 从哪来beta 又是什么标准Smooth L1的分段阈值是1。为什么是1而不是0.5或3这取决于回归目标的常见量级。前面提到Faster R-CNN编码后的回归目标通常会被归一化到1附近所以以1作为分段点很自然。如果误差普遍小于1就会进入二次区梯度更平滑如果误差大于1就走线性区梯度恒定。但实际使用中这个阈值不一定要固定为1。PyTorch的F.smooth_l1_loss里有个beta参数可以调整分段位置公式稍有变化当|x| beta时loss 0.5 * x^2 / beta否则loss |x| - 0.5 * beta如果把beta调大二次区的范围变大小误差时梯度会更平缓把beta调小更多误差进入线性区对大误差的抑制更明显。不过大多数检测框架里都用默认beta1因为回归目标已经做了较好的归一化。如果自己的数据分布明显不同beta是值得调的一个点。3. 从梯度视角拆解 Smooth L1 的训练行为3.1 求导连续、可导、有饱和区真正理解Smooth L1要把梯度写出来。设delta x损失函数是smooth_l1(x) 0.5 * x^2, 当 |x| 1smooth_l1(x) |x| - 0.5, 当 |x| 1其一阶导数可以写成d/dx x, 当 |x| 1d/dx sign(x), 当 |x| 1这里有个容易被忽视的细节x 1和x -1处导数连续吗在x 1处左导数是1右导数是1所以连续在x -1处左导数是-1右导数也是-1同样连续。这一点比L1 Loss强L1在x 0处不可导虽然实际工程中可以用subgradient糊弄过去但Smooth L1做得很干净。从导数结构看Smooth L1有明确的两个工作区。当|x| 1梯度模长始终为1这是“饱和区”对误差大小不敏感当|x| 1梯度模长等于|x|这是“线性衰减区”误差越小梯度越小。3.2 训练前期和后期Smooth L1 如何自动切换步调目标检测的训练过程在回归分支上通常是这样的训练早期网络还没学到什么像样的特征预测框可能乱七八糟回归误差普遍很大。如果这时候用L2 Loss个别误差特别大的样本会产生巨大梯度参数更新被异常样本劫持。如果用Smooth L1只要误差大于1梯度模长就固定在1每个样本对更新的影响有上限训练过程会稳定得多。到了训练后期大部分正样本的回归误差已经压缩到1以内模型进入精修阶段。这个时候Smooth L1开始切换成二次函数行为误差大的样本获得更大梯度误差小的样本梯度更小起到了类似“注意力机制”的作用让还需要调整的样本多走几步已经接近目标的样本少动一点。这种前期“求稳”、后期“求准”的模式说得玄乎一点是鲁棒性说白一点就是它不会因为少数坏样本崩掉也不会在接近最优时原地打转。3.3 三种损失梯度对比一个表格就看清了预测误差xL1梯度模L2梯度模Smooth L1梯度模5.011012.01411.01210.5110.50.110.20.1这个表很直观。误差从5降到1的过程中L2的梯度从10降到2依然很大如果batch里混入一个误差为5的样本它的梯度会主导更新方向。L1的梯度全程是1无论误差多大都一样这虽然稳但在误差已经很小的0.1时仍然保持1会导致最优点附近的振荡。Smooth L1的梯度则是误差大于1时锁定单位梯度误差小于1时逐步衰减两头都照顾到了。3.4 极端情况异常标注和大误差场景下的鲁棒性真实训练数据集里ground truth标注不可能百分百准确尤其是一些遮挡目标、边界模糊目标人工标注的框可能偏了几个像素。这些“脏样本”的回归误差普遍偏大。如果用L2 Loss脏样本会拿到很大的梯度为了迎合它们模型参数被强行拉向一个错误方向训练损失虽然能降下来但mAP通常不好看。Smooth L1在这种场景下天然更安全。因为对误差大于1的样本梯度模长被限制为1脏样本的影响力不会超过正常样本太多。这也是为什么早期很多检测库默认框回归损失用Smooth L1而不是L2。它牺牲了一点在良好标注数据上的极限收敛速度换来了对噪声的容忍度这笔买卖在目标检测里非常划算。4. 工程实现细节从数学公式到训练的最后一公里4.1 直接使用 PyTorch 的 F.smooth_l1_loss不要自己造轮子PyTorch已经实现了Smooth L1绝大多数时候不需要自己重写。标准用法import torch import torch.nn.functional as F predicted_boxes model_output # shape: [N, 4] target_boxes target # shape: [N, 4] loss_bbox F.smooth_l1_loss( predicted_boxes, target_boxes, beta1.0, reductionmean )这里注意beta参数的默认值是1.0和标准Smooth L1一致。如果你看过一些老代码会发现里面有人手写了一个smooth_l1_loss函数逻辑也是根据误差绝对值做条件判断。现在完全没必要框架自带实现不仅更快还处理了梯度传播的边界情况。不过值得提醒的是PyTorch的smooth_l1_loss和F.huber_loss在实现上是同一个类名字不同而已。知道这一点之后以后看到某些代码里出现Huber Loss不要觉得陌生它就是Smooth L1的统计学名字。4.2 回归目标为什么要先编码成 tx, ty, tw, th理解了Smooth L1再回来看回归目标编码会更有感触。如果不做编码直接让网络回归原图坐标系下的x、y、w、h对于1920x1080的输入目标值可能动辄上千。网络初始化时预测值通常接近0误差可能达到几百甚至上千。Smooth L1对误差大于1的部分梯度恒为1这确实能避免爆炸但单位梯度过小需要更新很多步才能从“完全不对”进入“大概对”的区间训练效率很低。而经过anchor-based编码后目标值常常在[-1, 1]附近大部分样本从一开始就落在Smooth L1的二次区内梯度能随着误差减小而自适应变化训练过程会舒服得多。换句话说Smooth L1的阈值设计是和回归目标编码方式配套的。有人直接把原图坐标喂给Smooth L1然后发现训练慢、收敛差问题往往不在损失函数而在没有做目标编码。4.3 损失权重、正样本筛选与 Focal Loss 怎么配合目标检测总损失通常是分类损失和回归损失的加权和loss lambda_cls * loss_cls lambda_bbox * loss_bbox很多框架默认lambda_cls和lambda_bbox都是1但这不代表所有场景都适合。如果回归分支的loss量级明显小于分类分支模型会倾向于先优化分类框的位置精度上不来。反过来如果回归loss量级太大分类分支可能被压制检测框位置“很准”但常常把背景误判成目标。在实际项目中我一般会先打印两个分支的loss数值。如果回归loss是0.1量级分类loss是2量级肯定要调整权重让两路的梯度幅度大致匹配。另外回归损失通常只对正样本计算所以在计算时要注意reduction方式。用mean比较稳妥如果对整张图所有anchor做sum正样本越多的图损失越大会和样本数量耦合在一起不利于稳定训练。还有一点很多人会忽略Focal Loss处理的是分类分支的正负样本不均衡它不会自动改善回归分支的样本问题。回归分支仍然只对正样本回传梯度因此正样本质量至关重要。如果你的正样本里混了很多低质量匹配比如说IoU只有0.3的anchor也算正样本Smooth L1再稳健也会被这些低质量样本干扰。这是样本匹配策略的问题换损失函数解决不了。4.4 一个容易让 Smooth L1 翻车的场景坐标尺度不统一我说一个自己踩过的坑。有一段时间做毫米波雷达点云和视觉融合的目标检测雷达坐标系下的目标位置数值范围非常大x可能到几十米y可能到几米。我当时直接把归一化之前的位置作为回归目标输入Smooth L1结果训练非常奇怪大误差样本的梯度永远一样看起来不爆炸但loss下降极慢。后来把回归目标规范到[0, 1]区间之后再算Smooth L1训练明显就顺了。原因就在前面分析的Smooth L1在大误差区是线性单位梯度误差如果从100降到1梯度一直是1模型无法感知“我已经从完全离谱变成比较接近了”于是前期效率很低。所以使用Smooth L1前务必检查回归目标的范围。规范到1附近甚至小于1才能让它的二次区发挥作用。5. 从经典检测器到现代检测器Smooth L1 的演进与局限5.1 它曾是“事实标准”Faster R-CNN、SSD、RetinaNet 时代的默契Fast R-CNN首次把Smooth L1引入检测后它基本上成了后续很多经典模型的默认配置。Faster R-CNN的RPN和最终检测头都用了Smooth L1SSD的loc_loss用的也是Smooth L1RetinaNet在早期版本里同样沿用了它。只要打开这些模型的源码大概率能在损失定义里看到一个SmoothL1Loss。那个时代大家对回归损失的共识是分类用交叉熵定位用Smooth L1。简单、稳定、开源实现多几乎不需要调参就能跑起来。所以很多做应用的人可能没有仔细想过它为什么是Smooth L1只是把它当作一个固定零件。但现在损失函数发展这么快重新审视Smooth L1能让很多问题豁然开朗。5.2 小目标与旋转框Smooth L1 暴露出的结构性短板Smooth L1最大的问题是它把一个边界框拆成四个独立变量分别回归没有衡量这四个变量组合出来的框和ground truth之间的整体重叠程度。一句话概括回归误差小不一定IoU高。在长宽比极端的框上这个问题尤其明显。比如一个宽高比为1:10的长条目标如果tx、ty、tw都很准只有th偏了一点算出来的框和真实框的IoU可能下降很多。在旋转目标检测里角度回归一个周期性问题角度差179度和1度实际上是同一个方向但Smooth L1会认为179度误差巨大完全不符合任务本身的几何意义。小目标检测场景也暴露了Smooth L1的局限。小目标本身的像素面积小几个像素的偏移在绝对数值上很小但相对目标尺寸来说已经很大了。Smooth L1在|x| 1时梯度等于x误差越小梯度越小所以对于像素级小偏移它的修正动力天然不足。这不是Smooth L1设计错误而是它没有“把误差放到目标尺度上衡量”的能力。IoU这类基于重叠度的损失天然具备尺度不变性对小目标更友好这也是后来GIoU、DIoU、CIoU能流行起来的原因之一。5.3 IoU 系损失接管之后Smooth L1 还值得学吗值得而且非常值得。IoU Loss、GIoU、DIoU、CIoU本质上都是在解决Smooth L1“独立回归变量但整体优化目标不匹配”的问题。只有先吃透Smooth L1为什么会出现、为什么在那个时候是进步才能理解IoU系损失到底修正了什么。实际工程中Smooth L1也并没有退出历史舞台。包括很多用IoU损失的项目依然会在初始训练阶段加入Smooth L1作为辅助或者在早停阶段对比两个损失的效果。经典之所以是经典是因为它背后的稳健回归思想是通用的。这也是我这篇“系列一”选择从Smooth L1讲起的原因后续如果继续深入IoU、GIoU、CIoU你会发现很多概念都是在和Smooth L1做对照。6. 围绕 Smooth L1 的调参经验和个人建议6.1 回归损失权重怎么定先看数值量级调回归损失权重时不要盯着loss曲线的大小空想。我建议先打印出训练初期每个分支的loss和梯度模长看两个分支谁在主导更新。具体做法是在第一个epoch之后分别记录loss_cls.backward(retain_graphTrue)之后分类头参数梯度和回归头参数梯度的平均模长。如果分类梯度过大就把lambda_cls调低一点如果回归梯度过大就调低lambda_bbox。多数框架默认还是1:1但这个默认值在改动检测头结构后经常不合适。另一个更粗暴但有效的方法是让回归分支保持Smooth L1分类分支用Focal Loss然后给回归分支配一个0.5到2之间的权重先在验证集上快速跑20个epoch看哪个权重让收敛更平滑。6.2 哪些情况继续用 Smooth L1哪些情况换掉如果你在做一个新数据集标注质量一般框的位置噪声比较大Smooth L1是一个很好的起点因为它足够稳。如果目标框尺度变化不大、回归目标已经做好编码也可以优先尝试Smooth L1省心。如果你的指标在Smooth L1下到了瓶颈尤其是框和目标在高IoU区域上不去可以考虑换成CIoU或DIoU。这类损失能优化框的整体几何关系在密集场景、小目标、长宽比极端目标上通常有明显提升。但要注意IoU系损失的收敛过程没有Smooth L1那么稳建议先用Smooth L1训练足够epoch再用IoU损失做fine-tune或者像很多开源项目那样把两个损失按0.5和0.5加权在一起。我自己的经验是不要频繁切换回归损失函数。损失函数对训练dynamics的影响是长期的至少给它40到50个epoch的机会再看有没有变化。跑十几个epoch就换损失函数大多数时候得到的是随机噪声。6.3 最后提醒一句回归loss下降不代表定位变好有一个容易误导人的地方Smooth L1 loss降到很低并不意味着检测框一定更准。因为mAP评价的是预测框和真值框的IoU而Smooth L1优化的只是编码后数值的绝对差异。我曾经有一个实验把回归权重从1调到3Smooth L1的loss确实从0.08降到0.04但mAP反而掉了1个点。原因就是模型过度拟合了proposal分布牺牲了整体的框几何质量。所以看到Smooth L1曲线很漂亮时先冷静去验证集上看几组典型样本的预测框和真值框的重叠情况。损失函数是手段mAP和应用效果才是目的。等到你真正理解了这一点再回去看Smooth L1在目标检测历史里的位置会看得更明白。关于Smooth L1 Loss我个人能分享的核心经验就是这些。后面计划继续写目标检测回归损失系列的IoU、GIoU、DIoU、CIoU部分把从“逐变量回归”到“整体回归”的演进过程串起来。