ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

L1、L2与Smooth L1损失函数全面对比:从梯度行为到目标检测回归实战

L1、L2与Smooth L1损失函数全面对比:从梯度行为到目标检测回归实战 前阵子有个做检测的朋友突然找我说训练时损失一直“过山车”甚至直接跳到inf模型最后输出全是同一个值。我问他损失用的什么他说MSE。我让他把坐标回归那一支换成Smooth L1训练马上稳了下来。后来他自己把三种损失都试了一遍回来跟我说“原来L1、L2、Smooth L1不是随便选的这里面门道太深了。”如果你也在做回归任务或者正在看目标检测、姿态估计、深度估计这类项目那你一定绕不开这三个名词L1 loss、L2 loss、Smooth L1 loss。很多人背公式背得熟但真到了选型、调参、画损失曲线的时候就开始懵。这篇文章我就从回归任务的角度把这三个损失函数彻底拆开讲包括数学本质、梯度行为、统计意义、工程选型以及我在真实项目中踩过的坑。1. 回归任务里损失函数到底在解决什么问题1.1 回归不是分类我们优化的不是一个边界而是一个数值先搞清楚一个最基本的问题回归任务和分类任务在“优化目标”上的区别。分类任务输出的是离散类别比如“猫”还是“狗”网络最后一层通常接softmax把输出变成概率分布然后用交叉熵衡量预测分布和真实分布的差异。这时候损失函数关心的是“分对了没有”和“分得有多自信”。回归任务输出的是连续数值比如房价、物体中心点坐标、人体关键点的x/y坐标、深度图里的像素距离。它的优化目标是让预测数值尽可能接近真实数值这时候损失函数必须回答一个问题预测值偏离真值越远损失应该多大梯度应该指向哪个方向这不是简单定义一个“差多少”就行的事情。损失函数不仅要告诉我们当前预测有多差还要通过梯度告诉优化器“下一步往哪个方向走、走多大步”。这个“量尺和方向盘”的角色就是损失函数的意义和作用。1.2 一个最小例子从y_true10开始假设真值是10预测值是8。L1 loss计算的是绝对误差L1 |10 - 8| 2L2 loss计算的是平方误差L2 (10 - 8)^2 4同样是差2L2给出的损失是L1的2倍。如果误差是5那么L1 5 L2 25L2的惩罚直接变成了25倍。这就是“平方”带来的非线性放大效应。很多新手会忽略这一点但恰恰是这个放大效应决定了L2对离群点极度敏感也决定了它在大误差场景下可能梯度爆炸。1.3 损失函数不是越低越好要能“学得动”我在实际训练里有过一个很深的体会某些任务里MSE的loss数值降得很漂亮但预测结果一塌糊涂。相反有时候损失曲线看着抖得厉害模型却越训越准。原因在于损失函数不仅要反映任务目标还要配合优化器形成稳定的梯度流。一个优秀的回归损失在大误差时梯度不能大到爆炸在小误差时不能小到停滞这样才能让网络持续学到东西。这点在后面的梯度分析中会看得非常清楚。2. L1 loss和L2 loss公式司空见惯行为天差地别2.1 数学表达式与统计视角均值vs中位数先列出最常见的公式。假设预测值是y_pred真实值是y_true记残差x y_true - y_pred。L1 loss也叫MAE平均绝对误差L1 |x|L2 loss也叫MSE均方误差L2 x^2在批量训练中通常取所有样本损失的平均作为该batch的loss。从统计学的角度看这两个损失对应着不同的噪声假设L2 loss 等价于在“高斯噪声”假设下对似然函数取负对数。最小化MSE最终得到的是目标变量的条件均值。L1 loss 等价于在“拉普拉斯噪声”假设下取负对数。最小化MAE最终得到的是目标变量的条件中位数。这个区别很重要。如果你的数据里存在少量极端离群点L2会拼命去拟合那些离群点因为平方误差会让离群点占据压倒性的梯度。而L1的损失是线性增长的离群点带来的惩罚相对小所以模型会更倾向于拟合大多数正常样本得到中位数附近的稳健结果。2.2 梯度行为常数梯度与线性梯度的博弈对损失函数求导能够直接看出训练时的行为差异。L1的梯度x不等于0时d(L1)/d(y_pred) -sign(y_true - y_pred)也就是说无论误差多大L1的梯度大小始终是1方向是朝向真值的方向。这种恒定梯度有个好处不容易被离群点带偏训练更稳。但也有个坏处当预测值已经很接近真值时梯度仍然不变模型会在最优值附近反复震荡难以精细收敛。L2的梯度d(L2)/d(y_pred) -2 * (y_true - y_pred) -2x梯度大小和误差成正比。误差大时梯度大收敛看似快但一旦出现一个极端离群点梯度的绝对值可能变得非常大导致参数更新过大loss爆炸误差小时梯度也趋近于0收敛速度越来越慢。这也是很多新手在训练初期看到MSE在几轮内快速下降后期却“蜗牛爬行”的原因之一。下面用一张表总结区别对比维度L1 loss (MAE)L2 loss (MSE)Smooth L1 loss数学形式|x|x^20.5x^2, |x|1; |x|-0.5, 否则对离群点敏感度低鲁棒高会被极端值主导中大误差时不受影响梯度大小恒定为1随误差线性增大小误差时线性增大大误差时恒为1收敛稳定性大误差稳定小误差震荡小误差稳定大误差易爆炸两者平衡最优解统计含义中位数均值介于两者之间2.3 一个10分钟实验带离群点的线性回归光说理论有点空。我建议你亲手做一个实验用一组带离群点的数据分别用L1和L2做线性回归。import numpy as np import matplotlib.pyplot as plt # 生成带离群点的数据 np.random.seed(42) x np.linspace(0, 10, 50) y 2 * x 1 np.random.randn(50) * 2 y[45] 60 # 人为加入一个离群点 # 用梯度下降拟合 y w * x b def fit(loss_type, lr0.01, epochs200): w, b 0.0, 0.0 history [] for _ in range(epochs): y_pred w * x b err y - y_pred if loss_type l1: grad_w -np.sign(err).dot(x) / len(x) grad_b -np.sign(err).mean() else: grad_w -2 * err.dot(x) / len(x) grad_b -2 * err.mean() w - lr * grad_w b - lr * grad_b loss np.abs(err).mean() if loss_type l1 else (err ** 2).mean() history.append(loss) return w, b, history w1, b1, h1 fit(l1) w2, b2, h2 fit(l2) plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.scatter(x, y) plt.plot(x, w1 * x b1, labelL1, linewidth2) plt.plot(x, w2 * x b2, labelL2, linewidth2) plt.legend() plt.subplot(1, 2, 2) plt.plot(h1, labelL1 loss curve) plt.plot(h2, labelL2 loss curve) plt.legend() plt.show()运行之后你大概率会看到L2的拟合线被离群点拉歪了而L1的拟合线基本贴合正常数据。这就是线性损失和平方损失在真实任务里的直观差异。3. Smooth L1 loss在回归和鲁棒性之间找平衡3.1 什么是Smooth L1 loss它为什么能火既然L1和L2各有毛病能不能做个“缝合怪”把两者的优点结合起来Smooth L1 loss就是这种思路的典型代表。它的定义是一个分段函数设x y_true - y_predsmooth_l1(x) 0.5 * x^2, 如果 |x| 1 smooth_l1(x) |x| - 0.5, 其他情况在误差绝对值小于1的时候它和L2一样用二次函数在误差绝对值大于等于1的时候它和L1一样用线性函数。这个设计的动机很好理解误差小的时候我们希望梯度随误差变小而变小让模型能够精细逼近真值所以用二次曲线。误差大的时候我们希望梯度不要超过1避免离群点把整个训练带崩所以用线性曲线。分段点处两个表达式得到的值和导数都相等曲线光滑可导反向传播不会在切换点出现梯度突变。3.2 为什么是0.5和1这两个常数很多初学者会问为什么二次部分要写成0.5 * x^2分段点为什么是1先说0.5。这个系数纯粹是为了求导方便。如果f(x)0.5*x^2那么f(x)x这样前面L2的公式里那个2就消失了梯度表达式干净利落。当然用x^2也可以但梯度会多一个2倍因子对学习率的要求就会变化。虽然神经网络里的学习率本来就要调但能少一个系数就少一份麻烦。再说分段点1。这是一个默认的超参数。它表示“误差超过1就切换成线性”。但在工程里这个1不一定是绝对的最优因为不同任务的真值量纲差异极大。比如预测温度时误差可能都是0.5以内预测坐标偏差时误差可能达到几十。所以很多框架提供的Huber loss支持你自定义delta参数而Smooth L1 loss在PyTorch里也支持beta参数。你可以根据实际误差范围去调整这个切换点不要死守1。3.3 梯度推导与一个可直接用的实现对三个区段分别求导可以得到Smooth L1的梯度f(x) x, 如果 |x| 1 f(x) sign(x), 其他情况这里的sign(x)在x为正时取1为负时取-1。也就是说预测值和真值之间的差距很大时梯度大小被限制在1不会再随误差线性放大。下面是一个PyTorch实现可以方便地赋给beta参数import torch import torch.nn.functional as F def smooth_l1_loss(pred, target, beta1.0): diff torch.abs(pred - target) loss torch.where(diff beta, 0.5 * diff.pow(2) / beta, diff - 0.5 * beta) return loss.mean()这个实现和PyTorch里默认的SmoothL1Loss行为一致。注意当beta不为1时0.5 * diff^2 / beta能够保证在切换点两侧的函数值和导数都连续。如果你不想自己写直接用F.smooth_l1_loss(pred, target, beta1.0)也行。但理解内部实现永远比调API更重要尤其是在你面对复杂的多任务损失时你才能知道该改哪里。4. 项目实战目标检测框回归中三种损失的选型与调参记录4.1 为什么目标检测的bbox回归常用Smooth L1目标检测是回归损失应用最典型的场景之一。你在Faster R-CNN、SSD、RetinaNet这些经典模型里经常能看到Smooth L1的身影。为什么不用MSE原因很简单框回归的标签是物体边界框的坐标数据里经常会有标注噪声和离群样本。比如一张图里目标被严重遮挡或者标注框本身就不准出现若干个“极端误差”是很常见的。如果此时用L2 loss那些极端误差会平方放大梯度甚至会超过正常样本的几十倍直接主导一次迭代的方向让整个训练变得很不稳定。如果只有L1可选倒是抗离群了但小误差时梯度始终为±1模型很难把框精修到很高的IoU。Smooth L1正好在两个极端之间做了一个切换大误差时梯度封顶为1不会被离群样本带飞小误差时梯度可以平滑降到0允许精细收敛。在早期的YOLO系列中坐标回归部分也类似地用了带权重系数的MSE或Smooth L1变体目的是在“定位准确性”和“分类置信度”之间做一个权衡。现在的YOLO版本普遍引入了更复杂的IoU类损失但理解Smooth L1仍然是基础。4.2 我在训练里亲历的loss爆炸与修复过程去年做一个行人检测项目数据里有大量遮挡场景。训练时我一开始图省事坐标回归用MSE结果每训练几千步就会突然出现一次loss尖峰严重的时候直接inf。排查步骤我记录在这里希望对你有帮助。第一步先画损失曲线。我把每个batch的loss都打到了日志里发现loss是“正常-突变-正常”的模式而不是一直偏高。这通常说明有个别样本的梯度太大而不是全局学习率有问题。第二步检查输入数据和标签。我随机打印了几组训练数据发现确实有标注框出界的样本比如框的坐标超出了图像宽度。这种离群标签放到L2里误差可能上百平方后就是上万一个batch的loss自然爆炸。第三步把L2换成Smooth L1。改完之后即便还有出界标签梯度也最多为1loss尖峰基本消失训练稳定了许多。后来我又额外加了数据清洗把完全无效的标签过滤掉收敛效果更好了。这个经历让我养成一个习惯遇到回归训练不稳定先别急着调学习率先看一眼用的是哪种损失函数。很多不稳定问题不是优化器的问题而是损失函数对离群样本的响应过于剧烈。4.3 yolov8画损失函数曲线图怎么验证损失是否真的收敛热搜词里有个“yolov8画损失函数曲线图”说明不少人在跑完YOLOv8之后想知道怎么把训练日志里的损失画出来看趋势。这个方法其实不限制具体模型只要你有训练日志就能很简单地画出曲线。比如你用了Ultralytics的YOLOv8训练训练完后results.csv里会记录train/box_loss等字段。你可以这样读取并绘制import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/train/exp/results.csv) # 列名里可能带空格先清理 df.columns [c.strip() for c in df.columns] plt.figure(figsize(10, 6)) plt.plot(df[train/box_loss], labeltrain box loss) plt.plot(df[val/box_loss], labelval box loss) plt.xlabel(epoch) plt.ylabel(loss) plt.title(Box Regression Loss) plt.legend() plt.grid(True) plt.show()看曲线的时候我一般关注三个信号训练loss和验证loss一起缓慢下降说明模型还在正常学习。验证loss下降后反弹说明开始过拟合可以考虑提前停止或加大正则化。训练loss突然出现一个尖峰但很快回落通常是个别异常样本引起的梯度抖动需要结合数据检查不能只调学习率。如果画出的loss曲线整体偏高或者来回震荡先不要急着怀疑模型结构先确认你的return任务选用的损失函数是否匹配数据分布。这一点在线性回归、框回归、关键点回归里都适用。5. 进阶思考从Smooth L1到更顺滑的回归损失以及两个常见误区5.1 Smooth L1和Huber loss其实是同一个东西如果你搜“Huber loss”会发现它的定义和Smooth L1非常像huber_delta(x) 0.5 * x^2, 如果 |x| delta huber_delta(x) delta * |x| - 0.5 * delta^2, 否则当delta等于1时Huber loss就是Smooth L1 loss。PyTorch里单独提供了HuberLoss允许指定delta参数而SmoothL1Loss默认固定为1。所以你在不同代码库里看到这两个名词可以说它们是同一个家族。实际使用中如果数据误差范围明显不是1附近可以调delta。比如某个回归任务里误差通常在0.1到0.5之间那就可以把delta调小到0.2或0.3让模型更早进入二次收敛区如果误差动辄几十delta太大反而会让阈值内梯度像L2一样高这时候把delta调小会更稳。5.2 常见误区一损失函数数值越小越好这是一个很经典的误区。我见过有人把L2 loss从100降到0.1很开心但模型输出结果却不理想。原因可能是他不停调学习率、调batch size却从未考虑过当前loss和任务是否匹配。回归任务里损失函数值的绝对大小和模型能力没有直接关系。L2 loss通常比L1 loss数值大因为它用平方衡量误差Smooth L1又介于两者之间。不同损失之间比较loss数值没有意义真正有意义的是在同一损失函数下的收敛趋势以及最终在验证集上的指标RMSE、MAE、IoU、关键点距离等。另一个相关误区是“不看梯度只看损失曲线”。有些时候损失曲线下降了但梯度几乎消失模型实际不再更新。我建议你在debug时不仅打印loss也打印第一层卷积的梯度范数。如果loss还在降但梯度已经非常小可能是网络的局部最优或者梯度消失问题换个激活函数或损失函数都比硬调学习率有效。5.3 常见误区二MSE是万能回归损失MSE是很多教材里最先介绍的回归损失容易给人一种“回归任务就该用MSE”的印象。实际上MSE只适合残差分布接近高斯、离群点少的场景。一旦数据里有长尾噪声比如深度估计中远处像素误差极大、自动标注中偶尔出现错误标签MSE就会让模型陷入“过拟合离群点”的糟糕状态。回到选择问题我给出一条很实用的判断逻辑如果你的数据干净误差基本集中在小范围且你希望梯度在后期平滑变小用L2/MSE。如果你的数据有较多离群样本且你更关注大多数正常样本用L1/MAE。如果你两者都想要优先用Smooth L1或Huber loss并调整delta/beta来适应数据误差尺度。如果你的任务是框回归或IoU优化还可以在基础损失之上叠加GIoU、DIoU、CIoU等针对空间几何的损失但那是另一个话题。这些选择不是互斥的很多模型会同时用多个损失比如分类用交叉熵回归用Smooth L1关键点用L1或Wing loss。核心思路永远是每个损失必须对应当前任务最容易优化的“度量尺度”。6. 最后分享一点个人经验损失函数看起来只是训练代码里的一行但它决定了模型每一个step的梯度质量。我在实际项目中多次发现排查优化问题最快的路径不是动网络结构而是回头检查损失函数和数据的匹配度。如果你现在正要开始一个回归任务我建议你花十分钟做两件事第一把预测目标的误差分布画出来看看是集中在0附近还是有长尾第二分别用L1、L2和Smooth L1跑几十个epoch把三条损失曲线放在一起看。这种对比实验比纯粹背理论更能建立直觉。我自己现在做回归任务时默认先上Smooth L1如果发现小误差下的收敛不够精细再考虑换回MSE或者调整beta值。这个习惯帮我避开了很多“loss突然爆炸”的坑也希望这篇关于L1 loss、L2 loss和Smooth L1 loss的拆解能让你在做回归任务时少走一段弯路。
返回列表