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。原因很简单:

  1. 训练稳定:不会因为个别异常框导致loss爆炸
  2. 精度高:在最优解附近能精细调整
  3. 调参简单:β默认取1,基本不用动

一个小技巧:如果你发现模型训练初期loss下降很慢,可以尝试把β调小(比如0.5),让「大误差区」更早进入线性梯度,加速收敛。但注意别调太小,否则就退化成L1了。

2.7 小结

今天咱们聊了三种最基础的边界框回归损失:

  • L1 Loss:简单粗暴,梯度恒定,但最优解附近震荡
  • L2 Loss:平方惩罚,梯度随误差变化,但对异常值敏感
  • Smooth L1 Loss:取两者之长,分段控制,YOLO首选

下节课,我们会进入更高级的IoU系列损失函数。你会发现,直接算坐标差其实不够「智能」,IoU系列才是真正理解「框得好不好」的度量方式。

嗯,今天就到这里。代码我都放在上面了,建议你动手跑一跑,看看三种损失在训练过程中的梯度变化。实践出真知嘛。