做目标检测训练的人,十有八九都遇到过这种诡异情况:模型在验证集上的分类精度很高,但画出来的框总差那么一点,要么整体向左滑了大半个身位,要么边界没能贴合目标轮廓。我以前的第一反应是调anchor、改NMS阈值,折腾几轮发现收益有限。直到有天闲着没事,把YOLO26默认的CIoU损失换成MPDIoU,一整套检测框突然就“规矩”了。损失函数这个入口,虽然只占训练脚本几行代码,却实实在在地决定了一个模型如何定义“预测框离真框有多远”。这篇记录就是围绕MPDIoU展开的:它的几何含义、和主流IoU损失的差异,以及在YOLO26里替换和调参的具体做法,适合那种已经跑通YOLO训练、接下来想榨干定位精度的人。
1. 边界框回归损失函数在目标检测里的位置与演进
1.1 分类准不等于定位准,回归损失才是“最后一厘米”
目标检测任务看起来是分类加回归,但绝大多数时候分类分支收敛得比回归分支快得多。你随便跑一个训练任务,前几十个epoch里类别loss大概率已经降到很低,可检测框还是歪歪扭扭的。原因很简单:分类分支告诉模型“这里有什么”,回归分支告诉模型“这个东西具体在哪”。实际应用里,框偏一点都是不能接受的。
比如行人检测,框如果整体往左偏半个身位,下游的任务去做轨迹预测,这个误差会被无限放大;工业质检里,瑕疵框贴不准边缘,判断就可能由“良品”变成“缺陷”。我发现很多同学在做YOLO系列改进时,注意力全放在backbone、注意力模块、neck结构上,很少去动损失函数。这个思路可以理解,毕竟结构改进看着更“硬核”,但损失函数其实决定了模型优化的根本方向。
YOLO26这类模型,分类任务通常用BCE或者交叉熵,回归任务却经历过很多代变化。早期用Smooth L1,后来发现IoU族损失更适合目标检测。原因也简单:Smooth L1是在像素坐标层面计算距离,对尺度特别敏感;同样的偏移,大目标看起来是微小的,小目标看起来就是巨大的。IoU族损失天生做了归一化,把重叠程度映射到0到1之间,尺度鲁棒性好很多。
我在实际调试中还有一个体会:当mAP卡在某个阈值附近上不去时,先别急着加模块,看看回归损失曲线是不是已经平了。如果验证集上loss还在降但mAP不动,那大概率是分类分支已经饱和,瓶颈就在边界框回归上。这时候换一种返回损失,往往比换一个backbone更直接。
1.2 从IoU到CIoU:解决一个旧问题,又引入一个新问题
IoU本身很好理解,就是交并比。它最大的优点是尺度不变,不管目标多大,IoU都落在0到1之间。缺点同样明显:预测框和真实框完全没有交集时,IoU等于0,梯度也直接消失,模型根本学不到往哪个方向调整。哪怕是有交集,IoU也无法区分“两个框交叉重叠”和“一个框包含另一个框”,更无法度量对齐程度。
GIoU试图用最小外接矩形的面积差来弥补这个缺陷。它的想法是:除了看重叠面积,还要看两个框之间“多出来没有覆盖到的空白区域”。这样即便IoU为0,外接框带来的差值还能提供梯度。问题是GIoU收敛很慢,尤其当两框是包含关系时,外接框面积几乎等于大框面积,惩罚项作用变得非常微弱。
DIoU进一步引入中心点距离,直接把“两个框中心差多远”作为惩罚项。这个改动效果很明显,因为它提供了明确的方向信息,模型知道该朝哪个方向移动中心点。CIoU则在DIoU基础上加了一个长宽比一致性惩罚,希望模型不仅把中心挪过去,还顺手把宽高比例也对齐。理论上是更完整了,但实际用下来不一定好使。
CIoU有个绕不开的问题:模型预测框和真实框的中心已经完全重合时,长宽比惩罚项是否还能起正面作用?在某些等比例缩放的情况下,惩罚项的梯度可能变得很弱,回归会停滞。而且宽高比的梯度在计算时互相耦合,一个方向上微调,另一个方向也跟着变,这就导致训练过程不稳定。EIoU把宽高距离拆开单独计算,角度上再考虑SIoU,却把公式越做越复杂,超参也多起来。
所以MPDIoU的出发点让我眼前一亮:既然一个轴对齐框由左上角和右下角唯一确定,直接用这两个角点的欧氏距离作为几何惩罚,不是更直接吗?它绕开了CIoU中宽高比的耦合问题,也不需要像EIoU那样引入额外的加权超参。这就是它在收敛速度和回归精度上能超过CIoU的底层原因。
2. MPDIoU的核心思想与几何原理
2.1 两个角点定义了整个框,角点对齐框就对齐
在目标检测的轴对齐框设定下,一个矩形可以由左上角坐标 (x1, y1) 和右下角坐标 (x2, y2) 唯一决定。中心点加宽高也是等价的表示,但角点更贴近边框本身。MPDIoU的做法是计算预测框和真实框这两组角点的距离,再叠加到IoU上。
假设预测框为 Bp = (x1, y1, x2, y2),真实框为 Bgt = (x1_gt, y1_gt, x2_gt, y2_gt)。先算两个角点的欧氏距离平方:
d1^2 = (x1 - x1_gt)^2 + (y1 - y1_gt)^2
d2^2 = (x2 - x2_gt)^2 + (y2 - y2_gt)^2
然后用预测框和真实框的最小外接矩形宽度 w 和高度 h 做归一化。w^2 + h^2 就是最小外接矩形对角线长度的平方。损失定义写成:
L_MPDIoU = 1 - IoU + d1^2 / (w^2 + h^2) + d2^2 / (w^2 + h^2)
等价地,MPDIoU = IoU - (d1^2 + d2^2) / (w^2 + h^2)。如果两个角点完全重合,后面的惩罚项为0,损失变成 1 - IoU,此时框的位置和尺寸已经完全一致,只剩IoU本身的重叠度评价。如果两个框错开,角点距离就会变大,惩罚项增大,模型必须同时优化“重叠面积”和“边框对齐”两个方向。
为什么不用四个角点距离?其实对于轴对齐框,左上角和右下角一旦对齐,剩下两个角点自然也就对齐了。只取两个对角点已经足够,实现和计算都省一半。而CIoU的中心点距离加宽高比,本质上是把这个信息拆成了两部分,还拆得不彻底,所以MPDIoU在梯度回传上更干净。
我最初看公式时还想过:分目标检测里的坐标不是全部归一化,尺度不一怎么办?分母的 w^2+h^2 本身就是尺度归一化项,点距离被对角线长度除过之后,变成了相对量。只要最小外接矩形不是极端小,这个归一化就是合理的。
2.2 相比CIoU、EIoU、SIoU,优势与局限都在哪
可以从几何约束的完整度和实现复杂度两个维度来看。CIoU约束的是中心距离和宽高比,但宽高比并不是一个独立变量;EIoU虽然把宽高距离拆开了,公式更复杂;SIoU甚至考虑了角度,代价是引入更多计算。MPDIoU只用了两个角点的距离,实现非常简单,算法复杂度低,收敛速度往往还更快。
我用下面的表格做过一次快速对比:
| 损失 | 额外几何约束 | 约束形式 | 实现难度 | 主要特点 |
|---|---|---|---|---|
| IoU | 无 | 重叠面积 | 低 | 无重叠时梯度消失 |
| GIoU | 最小外接框面积 | 外接区域差异 | 低 | 收敛慢,包含关系退化 |
| DIoU | 中心点距离 | 中心点位置 | 低 | 梯度方向明确 |
| CIoU | 中心点距离、宽高比 | 中心点+形状 | 中 | 长宽比梯度耦合 |
| EIoU | 中心点、宽高距离 | 中心点+宽高独立 | 中高 | 超参略多 |
| MPDIoU | 双角点距离 | 角点对齐 | 低 | 位置和尺寸同时约束 |
原论文作者在公开目标检测数据集上做了大量对比,结论是MPDIoU在收敛速度和最终mAP上都优于传统IoU系损失。我自己复现时的体感是:大目标和中等目标上的提升比较稳定,因为分母尺度较大,惩罚项数值分布更合理。
但它不是没有局限。极端长条形的目标会让角点距离在某一方向上非常敏感。比如一个横向很长、纵向很窄的目标,竖直方向的微小偏移也会被角点距离惩罚放大,这在小目标本来就密集的场景里容易导致训练震荡。另外,如果预测框与真实框中心重合,但宽高比例完全不同,IoU部分会很小,角点距离惩罚项也会很大,这时模型要同时解决重叠和形状对齐两个问题,压力比CIoU更集中。所以MPDIoU适合作为默认候选,但遇到特定数据集仍需调整。
3. 在YOLO26中集成MPDIoU的实操过程
3.1 从一行替换开始,找到损失函数的入口
YOLO26的代码结构依然沿用“模型、损失、优化器”三个大模块。损失相关代码通常集中在项目的model/loss.py或engine/loss.py里,不要整个重写,只改边框回归部分就行。多数情况下你能找到类似这样的调用:
iou = bbox_iou(pred_boxes, target_boxes, xywh=True, CIoU=True)这就是入口。先自己实现一个MPDIoU函数,为了兼容不同的坐标格式,我建议直接写一个同时支持xywh和xyxy的函数,方便以后在多个项目里复用。
import torch def bbox_mpdiou(pred, target, xywh=True, eps=1e-7): # 默认输入为xywh格式,先转换到xyxy if xywh: px, py, pw, ph = pred.chunk(4, dim=-1) tx, ty, tw, th = target.chunk(4, dim=-1) pred_x1, pred_y1 = px - pw / 2, py - ph / 2 pred_x2, pred_y2 = px + pw / 2, py + ph / 2 target_x1, target_y1 = tx - tw / 2, ty - th / 2 target_x2, target_y2 = tx + tw / 2, ty + th / 2 else: pred_x1, pred_y1, pred_x2, pred_y2 = pred.chunk(4, dim=-1) target_x1, target_y1, target_x2, target_y2 = target.chunk(4, dim=-1) # 计算交集面积 inter_w = torch.min(pred_x2, target_x2) - torch.max(pred_x1, target_x1) inter_h = torch.min(pred_y2, target_y2) - torch.max(pred_y1, target_y1) inter = inter_w.clamp(0) * inter_h.clamp(0) # 计算并集面积 pred_area = (pred_x2 - pred_x1) * (pred_y2 - pred_y1) target_area = (target_x2 - target_x1) * (target_y2 - target_y1) union = pred_area + target_area - inter + eps iou = inter / union # 最小外接矩形 enclose_x1 = torch.min(pred_x1, target_x1) enclose_y1 = torch.min(pred_y1, target_y1) enclose_x2 = torch.max(pred_x2, target_x2) enclose_y2 = torch.max(pred_y2, target_y2) w = enclose_x2 - enclose_x1 h = enclose_y2 - enclose_y1 # 两个角点的距离平方 d1 = (target_x1 - pred_x1).pow(2) + (target_y1 - pred_y1).pow(2) d2 = (target_x2 - pred_x2).pow(2) + (target_y2 - pred_y2).pow(2) # 归一化分母 den = w.pow(2) + h.pow(2) + eps mpdiou = iou - (d1 + d2) / den return mpdiou代码里关键点是:交集面积一定要clamp到0以上,否则会出现负面积;归一化分母加了eps防止除以0;MPDIoU和IoU一样是越大越好,训练时最终返回的损失是 1 - mpdiou 的均值。
修改调用处时,最简单的做法是这样:
iou = bbox_mpdiou(pred_boxes, target_boxes, xywh=True)如果怕影响原来的训练流程,可以加一个开关:
iou = bbox_mpdiou(pred_boxes, target_boxes, xywh=True) if use_mpdiou else bbox_iou(pred_boxes, target_boxes, xywh=True, CIoU=True)我建议第一次实验时保留CIoU分支,方便做对照,也能在训练过程中随时切换回去。
3.2 跑一轮对比,别被单点mAP骗了
替换完之后,我拿了一个自己标注的道路目标数据集做对照实验。同一份数据、同样的优化器、固定随机种子,训练到第140 epoch。
默认CIoU的mAP@0.5:0.95大概在0.612,MPDIoU变体大约0.627,看起来只提升了一个多点。但把预测结果可视化后,感受完全不一样:CIoU模型画出来的框喜欢“往目标中心缩”,边框离目标边缘有缝隙;MPDIoU模型则明显更贴合轮廓,尤其在车辆和行人类别上,边界紧实了很多。这个差异在mAP上只能体现一小部分,但换成框的中心点误差统计,差距能到5%以上。
我还记录了一个有意思的现象:前20个epoch两者损失曲线几乎重叠,一度以为改了个寂寞。到了第35个epoch左右,MPDIoU的回归损失下降曲线开始更平稳,验证集上的定位差距逐渐拉开。到训练后期,CIoU的回归损失还有一个回弹,MPDIoU则相对稳定。这个“前期无差别、中期逐步拉开”的走势,很符合角点惩罚在大尺度框上逐步生效的特征。
另一份遥感数据集上的结果完全不同:MPDIoU比CIoU掉了将近一个点。那个数据集小目标和极端长条物体特别多,前面说的问题正好踩中。所以这种改进一定要带自己的数据做验证,不能照搬结论。
3.3 训练参数与常见手感
损失函数替换后,默认学习率通常还能接着用,但我习惯把初始学习率降到原计划的0.8,并保留原来的warmup轮数。原因很简单:MPDIoU的角点惩罚项在训练早期可能会让部分预测框的损失数值比CIoU偏大,如果学习率还在原来的档位,回归分支容易震荡。
损失权重不用专门调整。YOLO系列里box loss gain通常在0.05左右,这个值是配合默认损失设计的,MPDIoU的数值范围还是在0到1之间,直接沿用问题不大。
如果真想微调,先看你的数据分布:大目标占多数,可以稍微调高回归权重,让模型更在意定位;小目标占多数,反而要降低角点惩罚项的权重,或者把分母加一个固定下界,这个处理放在后面说。
4. MPDIoU在实操中的常见问题与排查技巧
4.1 训练到一半loss变成NaN怎么办
替换MPDIoU之后遇到NaN,第一反应先检查分母。如果预测框和真实框几乎完全重叠,或者目标本身就特别小,w^2+h^2会非常接近0。即使加了eps,在某些FP16混合精度训练场景下,eps太小照样会溢出。
解决办法是对分母做一次强制下限:
den = torch.clamp(w.pow(2) + h.pow(2), min=1e-4) + eps如果你的坐标是归一化到0到1之间的,甚至可以把min设为1e-3。这样能挡住绝大多数除以0的问题。如果改了之后还是NaN,那就去打印MPDIoU值看哪里出现了inf。我遇到过一次是回归分支输出极端坐标,导致角点距离平方值巨大,这其实和优化器梯度爆炸有关,调低学习率、加EMA之后就自然消失了。还有一次是BatchNorm统计量在初期不稳定,和损失函数本身无关。
4.2 小目标场景越改越差,怎么救
小目标占比高的场景,MPDIoU的原始公式容易出问题。原因还是分母:小目标的w^2+h^2很小,角点距离哪怕只有几个像素,在归一化之后也会被放大,相当于把小目标的位置偏差放得过大,模型被迫把大量精力花在“让角点绝对对齐”上,反而忽视了整体的分类质量。
我试过最直接的办法是给角点惩罚项加一个衰减系数alpha。在浅层负责小目标的特征层上,把alpha设成0.5,损失函数变成:
mpdiou = iou - alpha * (d1 + d2) / denalpha等于1就是原版,等于0就退化回普通IoU。这个系数需要在小数据集上做几次网格搜索,通常0.5到0.8之间比较常见。另一种不用超参的稳法是把分母做下限:
den = torch.clamp(w.pow(2) + h.pow(2), min=1.0)当坐标归一化到0到1时,分母最小为1,角点惩罚项最多和点距离平方同量级,不会无限放大。我实测这种改法能让小目标数据上的训练稳定很多,代价是收敛速度比原版慢一些。两种方案选哪个,取决于你是更怕调参,还是更怕训练震荡。
4.3 注意与正样本分配策略的耦合
现在很多YOLO版本从静态anchor分配转向了动态分配,比如task-aligned assigner。动态分配器在选择正样本时,会同时考虑分类分数和回归损失,把回归损失作为cost的一部分。你在这里把CIoU换成MPDIoU,本质上改变了cost的数值分布,正样本数量可能会明显变化。
我踩过这个坑:某次替换后训练曲线整体怪怪的,一查统计,正样本数量平均少了15%,模型欠拟合。解决办法很简单,把分配器的cost计算里也同步替换成MPDIoU,让“选样本”和“训练样本”用同一个质量标准。如果项目里用的是静态匹配策略,这个可以不用管,但动态策略必须检查。
判断方法也很朴素:替换损失函数前后各跑30个epoch,统计每张图正样本数量。如果波动超过10%,就要去看分配器的回归代价项。这一条在常规博客里很少被提到,但实际影响非常大。
4.4 不能只盯mAP,还要看框的偏移方向
损失函数效果到底好不好,只看mAP容易误判。mAP是按IoU阈值划分的,比如0.5、0.75,只要预测框和真框重叠超过阈值就算对,它不敏感于“框整体往左偏了两个像素”这种细粒度偏差。但很多业务场景恰恰需要这种细粒度准确。
我的习惯是训练完成后做三件事:第一,可视化一批验证集图片,把预测框和真框同时画出来,用肉眼感受边缘贴合程度;第二,统计所有预测框和对应真框的中心点欧氏距离平均值,这个数值对定位精度很敏感;第三,分析不同尺寸目标的漏检和误检情况。
实验里最直观的感受是:CIoU模型喜欢把框往目标中心缩,MPDIoU模型更喜欢把边框贴合目标边缘。如果你的业务对边缘要求高,比如边缘检测、文字检测、医学病灶定位,这个损失函数完全值得投入时间实验。
5. 经验总结与后续扩展
5.1 我的一点心得
损失函数不是一个可以随便选个“涨点神器”换上去就完事的模块,它定义了模型的学习偏好。MPDIoU用两个角点距离作为几何约束,把回归目标从“只看重叠面积”提升到了“位置和尺寸同时对齐”,理论上更严苛,收敛也更快。但它的缺点同样来自这份严苛:小目标、长条物体和极端尺度分布下,需要额外做稳定化处理。
我现在的一般选择策略是这样的:第一候选就是MPDIoU,先在小数据集上跑几个epoch看回归损失曲线;如果目标整体偏小,就在角点惩罚项上做alpha折减;如果数据里长条物体多,就把分母加上限或下限做保护。这样一套下来,基本能规避MPDIoU的大多数坑。
5.2 后续可以继续做的方向
如果你想让MPDIoU更贴近自己的数据,可以把角点距离拆成x方向和y方向两个独立项,分别乘以不同权重。比如你的目标大多是横向的长条物体,那对y方向误差的惩罚可以适当降低。这样改虽然多一两个超参,但能让损失函数真正“跟随数据分布”。
更进一步的组合是MPDIoU加IoU-aware分类分支,让分类分支也感知定位质量。常规做法是用预测框和真框的IoU作为分类目标的一部分,如果回归损失本身已经更准,分类分支也能学到更可靠的质量信号。另一个思路是把MPDIoU当“预训练损失”用,训练后期切回CIoU做细微调整。我在一次实验中试过这个组合,mAP还能再涨零点几个点,但这不是稳定规律,只能算一个备选方案。
最后说一个比较感性的体会:训练目标检测模型,瓶颈往往不是“网络不够深”,而是损失函数没有把任务目标翻译准确。分类告诉模型看到了什么,回归才告诉模型在哪里。MPDIoU这几行代码,是我目前觉得性价比最高的回归改进之一。如果看完文章你打算动手,先把训练日志里loss曲线的纵轴范围记下来,然后再替换,等你看到验证集上那些框突然变得服帖,就会明白这种“小改动大收益”的快乐了。