3. 边界框回归损失(下):IoU Loss、GIoU Loss、DIoU Loss、CIoU Loss 的深度对比与实战选择

好,咱们接着聊边界框回归损失。上一节我们把 L1、L2、Smooth L1 这些「老前辈」扒了个底朝天。这一节,咱们要聊的才是真正让 YOLO 系列性能起飞的核心——IoU 系列损失函数。

说实话,我刚入行那会儿,大家还在用 Smooth L1。直到有一次,我做一个密集小目标检测的项目,模型怎么调都收敛不好,边界框预测得歪七扭八。后来一查,发现 Smooth L1 对 IoU 这个最终评价指标是「盲人摸象」——它不知道两个框到底重叠了多少。嗯,从那以后,我就彻底倒向了 IoU 系列。

3.1 为什么 Smooth L1 不够用?

你想想看,Smooth L1 是在优化什么?它是在优化四个坐标点的绝对差值。但我们的最终评价指标是 IoU——两个框的交并比。这两者之间有个巨大的鸿沟。

举个例子:

  • 两个框的坐标差相同,但一个是大框,一个是小框,IoU 天差地别。
  • 两个框的 L1 损失很小,但可能一个在左上角,一个在右下角,IoU 为 0。

说白了,Smooth L1 不直接优化 IoU,这就导致训练和评估之间存在「目标不一致」的问题。我见过不少新手调了半天参数,损失降得挺漂亮,一测 mAP 纹丝不动——就是这个原因。

3.2 IoU Loss:最直接的回归损失

IoU Loss 的思路非常朴素:既然最终要看 IoU,那我们就直接优化 IoU 呗。

公式很简单:

IoU Loss = 1 - IoU

其中 IoU 就是两个框的交集面积除以并集面积。

优点:

  • 直接优化评价指标,目标一致。
  • 对尺度不敏感——大框小框一视同仁。

致命缺陷:

  • 当两个框完全不重叠时,IoU = 0,损失恒为 1,梯度为 0。模型根本不知道往哪个方向调整。
  • 无法反映两个框的「距离感」——一个在左边 10 像素,一个在右边 100 像素,IoU 都是 0。
避坑指南: 我曾经在一个航拍图像检测项目里直接用了 IoU Loss,结果模型训练到一半就卡死了。后来发现,因为航拍图像中目标分布稀疏,大量负样本的预测框和真实框完全不重叠,梯度消失,模型直接罢工。所以,纯 IoU Loss 基本没人用了。

3.3 GIoU Loss:给 IoU 加上「惩罚项」

为了解决 IoU 在无重叠时梯度消失的问题,GIoU(Generalized IoU)应运而生。

它的核心思想是:不仅要看两个框的重叠区域,还要看它们的最小外接矩形。

GIoU = IoU - (C - U) / C

其中:

  • C 是两个框的最小外接矩形面积
  • U 是两个框的并集面积
  • (C - U) / C 就是「惩罚项」,表示外接矩形中未被两个框覆盖的比例

GIoU Loss = 1 - GIoU

GIoU 解决了什么?

  • 当两个框不重叠时,IoU = 0,但 GIoU 会变成负数(因为 C - U > 0),损失 > 1,梯度不为 0。
  • 模型会朝着「让两个框靠近」的方向优化,即使它们还没重叠。

GIoU 的局限:

  • 当两个框包含关系时(一个框完全在另一个框内部),C - U = 0,GIoU 退化为 IoU。
  • 收敛速度较慢,因为惩罚项是「间接」的。
我的经验: GIoU 在 YOLOv3 时代用得比较多。我个人习惯在目标尺度差异较大的场景下优先考虑 GIoU——比如同时检测行人和车辆,GIoU 的尺度不变性比 Smooth L1 好太多了。

3.4 DIoU Loss:引入中心点距离

GIoU 虽然解决了梯度消失问题,但它优化的是「外接矩形面积差」,而不是直接优化两个框的位置关系。DIoU(Distance IoU)换了个思路——直接考虑两个框中心点的距离。

DIoU = IoU - ρ²(b, b_gt) / c²

其中:

  • ρ²(b, b_gt) 是预测框和真实框中心点的欧氏距离的平方
  • c² 是最小外接矩形对角线长度的平方
  • ρ² / c² 就是归一化的中心点距离

DIoU Loss = 1 - DIoU

DIoU 的优势:

  • 直接优化中心点距离,收敛更快。
  • 当两个框不重叠时,中心点距离仍然提供梯度。
  • 比 GIoU 更稳定,不会出现「来回震荡」的情况。

DIoU 的不足:

  • 只考虑了中心点,没有考虑宽高比。
  • 如果两个框中心点重合但宽高不同,DIoU 退化为 IoU。

我记得有一次做车牌检测,车牌都是扁平的矩形,预测框经常出现「中心点对了但长宽比不对」的情况。DIoU 在这种场景下就有点力不从心了。

3.5 CIoU Loss:目前 YOLO 系列的「标配」

CIoU(Complete IoU)在 DIoU 的基础上,加上了宽高比的惩罚项。说白了,它想同时优化三个东西:重叠面积、中心点距离、宽高比。

CIoU = IoU - ρ²(b, b_gt) / c² - αv

其中:

  • v = (4/π²) * (arctan(w_gt/h_gt) - arctan(w/h))² —— 衡量宽高比差异
  • α = v / ((1 - IoU) + v) —— 平衡因子,当 IoU 较小时,α 较小,优先优化重叠和距离;当 IoU 较大时,α 较大,重点优化宽高比

CIoU Loss = 1 - CIoU

为什么 CIoU 是「完全版」?

  • 三个维度全覆盖:重叠、距离、形状。
  • α 的自适应机制很巧妙——不同阶段关注不同目标。
  • 在 YOLOv4、YOLOv5、YOLOv8 等主流版本中都是默认配置。
实战建议: 如果你在做通用目标检测,直接用 CIoU Loss 基本不会出错。YOLOv8 默认就是 CIoU,我自己的项目也几乎全部用 CIoU。只有在极特殊场景下(比如所有目标都是正方形),我才会考虑换成 DIoU 来减少计算量。

3.6 四种损失函数的深度对比

损失函数 优化目标 无重叠梯度 包含情况 收敛速度 适用场景
IoU Loss 交并比 ❌ 梯度消失 正常 几乎不用
GIoU Loss 交并比 + 外接矩形 ✅ 有梯度 退化为 IoU 较慢 尺度差异大
DIoU Loss 交并比 + 中心距离 ✅ 有梯度 退化为 IoU 较快 中心点敏感
CIoU Loss 交并比 + 中心距离 + 宽高比 ✅ 有梯度 正常 最快 通用场景

3.7 实战选择指南

说了这么多理论,到底怎么选?我给大家一个「决策树」:

  1. 通用场景(90% 的情况):无脑选 CIoU Loss。YOLOv5/v8 默认配置,经过大量验证,稳定可靠。
  2. 目标尺度极不均匀:比如同时检测蚂蚁和汽车,GIoU 的尺度不变性更好,可以试试。
  3. 目标形状单一:比如都是正方形或圆形,DIoU 就够了,省去宽高比的计算开销。
  4. 训练不稳定:如果 CIoU 训练时损失震荡严重,可以回退到 DIoU 或 GIoU,牺牲一点精度换取稳定性。
  5. 极端稀疏场景:比如遥感图像,目标间距很大,GIoU 或 DIoU 比 CIoU 更合适——因为宽高比惩罚在无重叠时意义不大。
一个小技巧: 我习惯在训练初期用 GIoU 或 DIoU,让模型先把框「拉」到目标附近,训练中期再切换到 CIoU 精调宽高比。这种「课程学习」策略有时候能带来 1-2 个点的 mAP 提升。当然,这需要写一点自定义损失函数的代码,后面我会专门讲。

3.8 代码实现要点

最后,给大家一个 CIoU Loss 的 PyTorch 实现片段,方便你直接拿去用:

def ciou_loss(pred_boxes, target_boxes):
    # pred_boxes, target_boxes: [N, 4] (x1, y1, x2, y2)
    
    # 计算 IoU
    # ... (省略 IoU 计算代码)
    
    # 计算中心点距离
    pred_center = (pred_boxes[:, :2] + pred_boxes[:, 2:]) / 2
    target_center = (target_boxes[:, :2] + target_boxes[:, 2:]) / 2
    center_dist = (pred_center - target_center).pow(2).sum(dim=1)
    
    # 计算最小外接矩形对角线
    enclose_x1 = torch.min(pred_boxes[:, 0], target_boxes[:, 0])
    enclose_y1 = torch.min(pred_boxes[:, 1], target_boxes[:, 1])
    enclose_x2 = torch.max(pred_boxes[:, 2], target_boxes[:, 2])
    enclose_y2 = torch.max(pred_boxes[:, 3], target_boxes[:, 3])
    enclose_diag = (enclose_x2 - enclose_x1).pow(2) + (enclose_y2 - enclose_y1).pow(2)
    
    # DIoU 项
    diou_term = center_dist / (enclose_diag + 1e-7)
    
    # 宽高比惩罚项
    pred_w = pred_boxes[:, 2] - pred_boxes[:, 0]
    pred_h = pred_boxes[:, 3] - pred_boxes[:, 1]
    target_w = target_boxes[:, 2] - target_boxes[:, 0]
    target_h = target_boxes[:, 3] - target_boxes[:, 1]
    
    v = (4 / (torch.pi ** 2)) * (torch.atan(target_w / (target_h + 1e-7)) - 
                                  torch.atan(pred_w / (pred_h + 1e-7))).pow(2)
    alpha = v / ((1 - iou) + v + 1e-7)
    
    ciou = iou - diou_term - alpha * v
    return 1 - ciou

嗯,代码里我加了一些 1e-7 防止除零,这是实战中容易忽略的细节。你直接复制这段代码,稍微改改输入格式就能用。

好了,边界框回归损失就讲到这里。下一节我们聊聊分类损失——Focal Loss 是怎么解决正负样本不平衡的。到时候我会分享一个我在 YOLOv5 里改 Focal Loss 参数的血泪史,保证让你少踩坑。