2. 边界框回归损失(上):L1 Loss、L2 Loss、Smooth L1 Loss 的原理、公式推导与代码实现
大家好,欢迎来到YOLO损失函数系列的第二讲。
今天咱们聊聊边界框回归损失。说白了,就是让模型学会「框得准」。你想想看,YOLO预测出来的边界框,跟真实框之间到底差了多少?这个差距怎么量化?怎么让模型朝着正确的方向优化?这就是边界框回归损失要干的事。
我个人习惯把这类损失函数分成两派:一派是今天要讲的「标准派」——L1、L2、Smooth L1;另一派是下节课要讲的「进阶派」——IoU系列。今天先打好基础。
2.1 为什么需要边界框回归损失?
YOLO网络最后输出的特征图上,每个网格会预测若干个边界框。每个框包含四个值:(x, y, w, h)。训练时,我们需要让这些预测值尽可能接近真实标注框。
那怎么衡量「接近」呢?
最简单的想法:算差值。预测值和真实值之间的差越小,说明预测越准。这个差值怎么算?这就引出了三种经典的距离度量方式。
核心思想:边界框回归损失 = 度量预测框与真实框在坐标/尺寸上的差异。
2.2 L1 Loss(平均绝对误差)
2.2.1 原理与公式
L1 Loss,也叫MAE(Mean Absolute Error)。公式很简单:
L1 = |y_pred - y_true|
对于边界框的四个参数,我们分别计算差值,然后求和或取平均。
举个例子:预测框的x坐标是10,真实框是12,那损失就是|10-12|=2。就这么直接。
2.2.2 梯度特性
L1的梯度是常数:
dL1/dx = 1 (当 x > 0)
dL1/dx = -1 (当 x < 0)
这意味着什么?不管误差多大,梯度始终是±1。不会因为误差大而梯度爆炸,也不会因为误差小而梯度消失。
嗯,这里要注意:在误差接近0的时候,梯度仍然不是0。这会导致模型在最优解附近「震荡」,难以收敛到精确值。
我的经验:我在做早期YOLOv3的调参时,试过用纯L1 Loss做回归。结果发现训练初期收敛很快,但到了后期,边界框总是差那么一两个像素,精度上不去。后来换成Smooth L1才解决。
2.2.3 代码实现
import torch
import torch.nn as nn
def l1_loss(pred, target):
"""
L1 Loss 实现
pred: 预测值 [N, 4]
target: 真实值 [N, 4]
"""
loss = torch.abs(pred - target)
return loss.mean() # 取平均
# 或者直接用PyTorch内置函数
criterion_l1 = nn.L1Loss()
loss = criterion_l1(pred_boxes, target_boxes)
2.3 L2 Loss(均方误差)
2.3.1 原理与公式
L2 Loss,也叫MSE(Mean Squared Error)。公式:
L2 = (y_pred - y_true)²
说白了就是差值的平方。还是刚才的例子:预测x=10,真实x=12,损失就是(10-12)²=4。
2.3.2 梯度特性
dL2/dx = 2 * (y_pred - y_true)
梯度跟误差成正比。误差越大,梯度越大;误差越小,梯度越小。
这有什么问题?我遇到过这种情况:训练初期,预测框离真实框很远,梯度非常大,直接导致loss爆炸,模型参数一下子飞出去,再也回不来了。这就是L2对异常值敏感的原因。
避坑指南:我曾经在YOLOv4的某个实验里,不小心把回归损失设成了纯L2。结果训练到第10个epoch,loss突然变成NaN。排查了半天,发现是某个边界框的预测值偏差太大,梯度爆炸了。后来加了梯度裁剪才稳住。
2.3.3 代码实现
def mse_loss(pred, target):
"""
L2 Loss 实现
"""
loss = (pred - target) ** 2
return loss.mean()
# PyTorch内置
criterion_mse = nn.MSELoss()
loss = criterion_mse(pred_boxes, target_boxes)
2.4 Smooth L1 Loss
2.4.1 为什么需要它?
你想想看,L1在最优解附近震荡,L2对异常值敏感。能不能把两者的优点结合起来?
当然可以。Smooth L1 Loss就是干这个的。它由Fast R-CNN论文提出,后来被YOLO系列广泛采用。
2.4.2 公式与推导
Smooth L1(x) =
0.5 * x² / β, 当 |x| < β
|x| - 0.5 * β, 当 |x| ≥ β
其中 x = y_pred - y_true,β是一个超参数(通常取1)。
说白了就是:
- 当误差小的时候(|x| < β),用L2的平方形式,梯度平滑,收敛稳定
- 当误差大的时候(|x| ≥ β),用L1的线性形式,梯度恒定,防止爆炸
2.4.3 梯度分析
dSmoothL1/dx =
x / β, 当 |x| < β
sign(x), 当 |x| ≥ β
你看,这个梯度设计很巧妙:
- 小误差时,梯度随误差线性减小,不会像L1那样震荡
- 大误差时,梯度恒为±1,不会像L2那样爆炸
为什么YOLO喜欢用Smooth L1?因为边界框回归中,大部分预测框在训练初期误差很大,后期误差很小。Smooth L1正好能适应这种「先粗后精」的训练过程。
2.4.4 代码实现
def smooth_l1_loss(pred, target, beta=1.0):
"""
Smooth L1 Loss 实现
"""
diff = torch.abs(pred - target)
loss = torch.where(
diff < beta,
0.5 * diff ** 2 / beta,
diff - 0.5 * beta
)
return loss.mean()
# PyTorch内置(注意:PyTorch的SmoothL1Loss默认beta=1)
criterion_smooth = nn.SmoothL1Loss(beta=1.0)
loss = criterion_smooth(pred_boxes, target_boxes)
2.5 三种损失函数的对比
| 特性 | L1 Loss | L2 Loss | Smooth L1 Loss |
|---|---|---|---|
| 对异常值敏感度 | 低 | 高 | 低 |
| 最优解附近收敛 | 震荡 | 稳定 | 稳定 |
| 梯度稳定性 | 恒定 | 线性变化 | 分段控制 |
| YOLO使用情况 | 较少 | 极少 | 常用 |
2.6 实际使用建议
我个人在YOLO项目中,边界框回归损失几乎只用Smooth L1。原因很简单:
- 训练稳定:不会因为个别异常框导致loss爆炸
- 精度高:在最优解附近能精细调整
- 调参简单:β默认取1,基本不用动
一个小技巧:如果你发现模型训练初期loss下降很慢,可以尝试把β调小(比如0.5),让「大误差区」更早进入线性梯度,加速收敛。但注意别调太小,否则就退化成L1了。
2.7 小结
今天咱们聊了三种最基础的边界框回归损失:
- L1 Loss:简单粗暴,梯度恒定,但最优解附近震荡
- L2 Loss:平方惩罚,梯度随误差变化,但对异常值敏感
- Smooth L1 Loss:取两者之长,分段控制,YOLO首选
下节课,我们会进入更高级的IoU系列损失函数。你会发现,直接算坐标差其实不够「智能」,IoU系列才是真正理解「框得好不好」的度量方式。
嗯,今天就到这里。代码我都放在上面了,建议你动手跑一跑,看看三种损失在训练过程中的梯度变化。实践出真知嘛。