YOLO26小目标检测涨点技巧:把CIoU换成MPDIoU损失函数 如果你在YOLO26上做过小目标检测大概率遇到过这种情况网络结构换了又换预训练权重也试了好几种可mAP就卡在那里不动。尤其是货物堆叠、密集人群、细胞切片这类目标稠密又互相遮挡的数据集漏检的永远是那些抱在一起的小框。我在这类场景里折腾了大半个月最后才发现问题不在模型而在那个一直没被我正眼看待的损失函数。把默认的CIoU换成MPDIoU之后同样的YOLO26、同样的数据mAP50-95直接涨了将近两个点。这篇文章把我的改法、原理推导和训练过程中的坑完整记录下来给想在YOLO26上做损失函数改进的同学一个可以直接复现的参考。1. 损失函数是密集小目标场景最容易忽略的瓶颈1.1 一次训练卡在0.41的复盘之前我在做一个密集货架场景的检测项目检测目标是大小不一的商品包装很多商品在画面里互相挤压、边缘重叠。模型用的是YOLO26输入分辨率调到640Mosaic、MixUp这些增强全开预训练权重也是最新的。前100轮还挺顺利mAP50-95一路爬到0.4左右但之后就怎么都上不去连续跑了几十轮曲线几乎是一条横线。复盘的时候我把可能的原因都过了一遍标注质量检查过没问题数据增强参数也试过几组没有改变尝试换更大的输入分辨率显存不够换Backbone实验成本太高。最后我在看训练日志的时候才注意到一件事——好几个月以来我从来没有专门看过YOLO26里默认的边界框回归损失用的是哪个函数它里面的具体行为是什么。这个问题其实很普遍。大多数人在做目标检测优化时第一个想到的就是改网络结构、加注意力模块、换Backbone很少有人去动损失函数。但损失函数决定了模型在每一轮训练中“如何调整自己的预测”是比网络结构更底层的东西。尤其是密集小目标场景损失函数的选择直接决定了大框和小框在梯度更新时是否被公平对待这个稍后详细说。1.2 CIoU/DIoU在密集小目标上的三个症结YOLO系列从早期版本开始就默认使用CIoU作为边界框回归损失。CIoU的思路是在IoU的基础上加了中心点距离惩罚和长宽比一致性惩罚公式大致是这样CIoU_Loss 1 - IoU (ρ²(b, bgt) / c²) αv其中ρ²(b, bgt)是预测框和真实框中心点的欧氏距离平方c是最小外接矩形的对角线长度v衡量两个框长宽比的差异α是平衡系数。这个公式在常规场景下表现很好但一旦遇到密集小目标问题就来了。第一个问题是尺度敏感。c是最小外接矩形对角线长度大目标的外接矩形很大c就很大中心点偏移在归一化后会被“稀释”小目标的外接矩形很小c就很小同样像素级别的偏移在归一化后会变成很大的数值。也就是说CIoU在训练时对大目标的梯度偏小、对小目标的梯度偏大这种不稳定对小目标检测的收敛很不利。第二个问题是长宽比惩罚的失效。v这一项是通过反正切函数arctan计算宽高比差异的当两个框都是小框时宽和高的绝对数值都在十来个像素级别数值稍微抖一下v的梯度就会剧烈变化甚至主导了整个更新方向。这样网线很容易被带偏出现“loss在降但mAP不动”的诡异现象。第三个问题最贴合标题里说的“密集重叠”。当两个小目标几乎重叠时中心点的距离本来就非常小CIoU的中心点惩罚项几乎不起作用长宽比惩罚在重叠情况下也和框的位置关系不大。训练数据里真正包含的信息是“这两个框虽然中心点接近但角点之间的错位关系是什么”CIoU没有直接建模这种信息等于把最能区分密集重叠样本的信息给丢掉了。1.3 为什么先动损失函数而不是动网络结构这是我想重点劝大家的一点遇到密集小目标涨不上去时优先排查损失函数的成本远低于改网络结构。改网络结构意味着你至少要做几组消融实验来确认是哪个模块带来了提升训练周期动辄两三百轮还要排除随机种子影响。而换一个损失函数在YOLO26的代码里通常只需要修改一个函数调用跑一轮对比实验基本上就能看到趋势。如果有效你再继续做其他改动也不迟如果没效损失的时间也远比重新训练一个Backbone架构少得多。我自己后来的习惯是任何检测项目开始调试之前先确认使用的回归损失是否适合当前数据分布。数据里小目标多、目标互相重叠多我默认就不再依赖CIoU而是优先考虑专门针对密集场景设计的损失函数MPDIoU就是其中之一。2. MPDIoU的回归逻辑两个顶点距离为什么够用2.1 从中心点到角点一个几何直觉的转变先解释一下MPDIoU的核心思想。CIoU这一系列损失函数的基本出发点是“用某个几何量来刻画预测框和真实框的差异”。CIoU选择的是中心点距离和长宽比而MPDIoU选择的是两个关键点之间的欧氏距离更具体地说是两个框的左上角点和右下角点。一个检测框在坐标表示下有四个数字通常是(x1, y1, x2, y2)也就是左上角和右下角的坐标。MPDIoU做的事情就是分别计算预测框和真实框左上角点之间的欧氏距离以及右下角点之间的欧氏距离然后把这两个距离作为惩罚项叠加到IoU上。为什么只选这两个点就够用你可以这样想一个矩形由四个顶点决定但除了左上角和右下角另外两个顶点的位置其实已经被这两个几何点唯一确定了。就好比搬一张床垫你只要把床垫的两个对角位置对齐了整个床垫的位置和姿态就定型了。相比CIoU还要额外构造长宽比信息MPDIoU直接利用两个关键点把位置、尺度、形状差异一次性编码进去。2.2 MPDIoU公式与计算流程MPDIoU的定义可以写成MPDIoU IoU - (d1² d2²) / (cw² ch²)其中d1是预测框和真实框左上角点的欧氏距离平方d2是预测框和真实框右下角点的欧氏距离平方cw和ch分别是两个框的最小外接矩形的宽和高损失则定义为L_MPDIoU 1 - MPDIoU整个计算流程可以分为四步。第一步把预测框和真实框的坐标统一到同一种表示方式常用的中心点格式(x, y, w, h)先转换成角点格式(x1, y1, x2, y2)。第二步计算两个框的交并比IoU这部分和标准IoU没有区别。第三步计算两个关键点的欧氏距离平方和以及最小外接矩形的对角线长度的平方。第四步按公式合成MPDIoU再通过1减去它得到最终的损失值。这个公式里最值得注意的一点是分母用的是最小外接矩形的宽高平方和也就是对角线的平方。这个归一化方式和DIoU、CIoU里的c²是一致的都是为了把距离惩罚映射到0到1的范围避免不同尺度目标之间的惩罚数值差异过大。但MPDIoU不需要显式计算中心点距离也不需要计算长宽比的反正切函数计算过程非常简单。2.3 相比CIoUMPDIoU在小目标和重叠场景下的优势源源优势可以从几个维度来对比。第一是尺度鲁棒性。MPDIoU的分母也是最小外接矩形的对角线但分子里的两个距离约束的是具体的角点而不是一个中心点。对于小目标而言即使像素偏移很小角点距离的变化也是连续的、平滑的不会出现CIoU那种因为长宽比反正切导致的突变梯度。第二是密集重叠表现。当两个框的中心点几乎重合时CIoU的中心点距离惩罚项会趋近于零失去作用。MPDIoU还保留着四个角点的错位信息d1和d2中至少有一个会比较大因此损失仍然会给模型明确的优化信号。简单说MPDIoU对“中心对齐但边缘错位”的样本更敏感而这恰恰是密集重叠目标最常见的形态。第三是计算开销。MPDIoU只需要计算两组欧氏距离和一组外接矩形宽高比CIoU少了一个反正切和一系列中间变量训练时的额外计算开销几乎可以忽略。我在实际训练中对比过同样的YOLO26配置单轮训练时间差不到1%。3. 在YOLO26中替换MPDIoU完整代码与验证流程3.1 实现mpdiou函数我以YOLO26仓库中常见的代码结构为例给出一个可以直接用的mpdiou实现。这段代码需要放在你代码仓库的损失函数模块里通常文件名是loss.py或者utils/metrics.py。import torch def mpdiou(box1, box2, xywhTrue, eps1e-7): # box1, box2: [N, 4] 的预测框和真实框 if xywh: (x1, y1, w1, h1), (x2, y2, w2, h2) box1.chunk(4, -1), box2.chunk(4, -1) w1_, h1_, w2_, h2_ w1 / 2, h1 / 2, w2 / 2, h2 / 2 b1_x1, b1_y1, b1_x2, b1_y2 x1 - w1_, y1 - h1_, x1 w1_, y1 h1_ b2_x1, b2_y1, b2_x2, b2_y2 x2 - w2_, y2 - h2_, x2 w2_, y2 h2_ else: b1_x1, b1_y1, b1_x2, b1_y2 box1.chunk(4, -1) b2_x1, b2_y1, b2_x2, b2_y2 box2.chunk(4, -1) # 交集面积 inter_w (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) inter_h (torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0) inter inter_w * inter_h # 并集面积 area1 (b1_x2 - b1_x1) * (b1_y2 - b1_y1) area2 (b2_x2 - b2_x1) * (b2_y2 - b2_y1) union area1 area2 - inter eps iou inter / union # 最小外接矩形的宽高和对角线平方 cw torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) ch torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) c2 cw * cw ch * ch eps # 左上角点距离平方 右下角点距离平方 d1 (b1_x1 - b2_x1) * (b1_x1 - b2_x1) (b1_y1 - b2_y1) * (b1_y1 - b2_y1) d2 (b1_x2 - b2_x2) * (b1_x2 - b2_x2) (b1_y2 - b2_y2) * (b1_y2 - b2_y2) return (iou - (d1 d2) / c2).clamp(-1, 1)这段代码的输入和YOLO系列默认的bbox_iou函数一致支持xywh格式和xyxy格式方便你直接替换。3.2 在YOLO26的loss模块里完成替换替换的位置一般在compute_loss或者BboxLoss的forward函数里。YOLO26的代码结构和其他YOLO版本类似你会看到类似下面这样的一行iou bbox_iou(pred_boxes, target_boxes, CIoUTrue)这就是默认损失函数的调用入口。把它改成iou mpdiou(pred_boxes, target_boxes)然后后续的损失计算通常是这样box_loss (1.0 - iou).mean()这种情况不用动其他代码。但有一点要特别注意如果你的仓库里把iou的计算结果同时用于正负样本分配例如SimOTA分配器计算cost时依赖iou那不要直接复用mpdiou的返回值因为MPDIoU不是标准IoU直接拿去和分类分数相乘得到cost逻辑上不严谨。正确做法是单独保留一份标准IoU用于分配只有回归损失用MPDIoU。YOLO26的默认实现里这两者通常已经分开你只需要确认一下你本地代码的走向。如果不确定当前用的配置是哪个损失可以在启动训练前加一行日志打印确认你定义的mpdiou被真正调用了。print(Current bbox loss: MPDIoU)训练起来之后看终端输出确认打印了这行内容再离开避免改了半天代码结果改的不是生效路径。3.3 三步验证确认涨点不是偶然替换完代码不要急着看最终mAP先按这三个步骤来验证。第一步观察训练日志里的box_loss曲线。MPDIoU的初始值和CIoU不太一样通常会略高一点因为多了一个角点距离惩罚项。如果box_loss在前10轮有明显下降趋势说明新损失已经生效。如果box_loss曲线形态和之前完全一致那大概率是替换的代码路径不对。第二步在验证集上同时跑原始模型和新模型的中间checkpoint例如第100轮保存的权重比较mAP50-95和APS小目标AP。MPDIoU对小目标更友好如果有效APS的变化通常比mAP整体更明显。第三步也是最重要的一步用相同的随机种子和训练配置跑两遍取平均值对比。目标检测训练本身有随机性数据增强、多尺度训练都会带来波动单次改动的1到2个点差距很可能只是运气。我习惯同一组配置跑两轮或者至少用两次不同随机种子取平均再下结论。3.4 容易踩到的两个代码坑第一个坑是除零。如果你的数据里出现了宽或高为0的异常框union会变成0导致iou计算不稳定。我的做法是在并集面积后面加一个eps也就是上面代码里的1e-7并在c2里也加eps保证即使两个框完全重合也不会除零。第二个坑是值域。MPDIoU的取值可以小于0因为IoU在0到1之间但距离惩罚可以超过IoU。如果你的loss计算里对iou做了clamp(0, 1)之类的限制会破坏负样本的梯度信号。在YOLO系列里回归损失通常只作用于正样本所以问题不大但还是要检查代码里有没有类似的截断逻辑。4. 实测结果与调参记录哪些改动真正有效4.1 我在密集重叠数据上的对比结果我在自建的密集货架数据集上做了对比实验数据集包含大约8000张图像检测目标是各种尺寸的包装商品目标数量平均每张15个左右部分区域出现目标交叠。训练配置输入分辨率640batch size 32训练200轮优化器默认SGD随机种子固定。对比结果如下表数值保留三位有效数字损失函数mAP50-95mAP50APS小目标重叠子集AP50CIoU默认0.4130.5860.2160.338DIoU0.4050.5780.2070.326MPDIoU0.4350.6040.2490.371可以看到MPDIoU在mAP50-95上比默认CIoU提升了约2.2个点最明显的是APS提升了3.3个点。按标准COCO的定义APS是面积小于32×32像素的目标这个变化说明MPDIoU确实把当前模型在小目标上的表现拉上来了。重叠子集是我从验证集里单独筛出来的一批目标交叠比例较高的图片AP50同样有提升说明密集重叠场景也没拖后腿。后来我也在一组细胞检测数据上复现了类似趋势mAP50-95提升了1.5个点左右虽然没有货架数据那么夸张但方向上是一致的。4.2 调参记录大部分默认值可以不用动很多同学换完损失函数后习惯性地去调一堆超参数我建议先别急。MPDIoU和CIoU在收敛行为上没有本质冲突YOLO26默认的训练配置绝大部分可以保留。我在调参过程中整理了这么几条经验学习率可以维持默认的0.01。MPDIoU的梯度比CIoU平滑不需要特别降低学习率来防止震荡。如果训练后期出现过拟合优先做的是调整weight decay或提前早停而不是动学习率。数据增强上有一点要注意。Mosaic在密集场景下起到了增加样本重叠程度的作用建议保留MixUp会让不同图像线性混合可能模糊小目标的边界如果发现初始下降变慢把MixUp的概率调到0.5以下试试。权重衰减建议从默认的0.0005开始如果box_loss下降很快但验证集mAP停滞说明模型开始过拟合了这时候把weight decay加到0.001通常能稳回来。还有一个很多人忽略的地方MPDIoU收敛得比CIoU快所以早停的时机可能会提前。我当时用的是200轮训练在第170轮左右模型就已经收敛后面30轮基本在浪费算力。建议训练时开启早停策略或者每10轮保存一次checkpoint方便回溯最佳模型。4.3 什么情况下MPDIoU反而会掉点不是所有场景都适合MPDIoU。我在另外几个项目上也试过发现两类情况它不仅不涨点还可能掉点。第一类是目标极度扁长且稀疏的场景比如车牌检测、扫描文档里的横线。这类目标本身长宽比差异极大CIoU的长宽比一致项虽然计算上有些别扭但确实提供了额外约束。MPDIoU只考虑角点距离和IoU缺少对长宽比差异的显式惩罚在某些极端形状上表现反而不如CIoU。第二类是类别极度不平衡的场景。MPDIoU只优化框的回归不解决分类置信度的分布问题。如果数据里90%都是背景或者单一易分类别模型的主要瓶颈在分类损失上这时候换回归损失带来的收益会被淹没甚至因为整体loss权重没调整而出现轻微的负作用。换句话说MPDIoU适合的是“目标小、目标密、目标互相叠”这三种特征至少占两样的场景。如果你的数据是大目标、稀疏分布、形状规整那默认的CIoU就已经够用不需要折腾。5. MPDIoU能发挥真实力的三套配套操作5.1 先检查正负样本分配再谈损失函数这个顺序我认为非常关键。损失函数再优秀它也只是在已经分配好的正样本上做回归。如果正负样本分配器本身有问题比如密集场景下很多小目标没有被分配给任何预测框那损失函数怎么改都救不回来。YOLO26默认使用的分配器基本都能保证每个目标至少匹配一个正样本但在极端密集场景下我遇到过某个目标被多个预测框同时匹配的情况这时候MPDIoU对每一对匹配框都会计算损失可能会导致重复惩罚。我的做法是在训练前单独统计一下密集场景下每个目标平均匹配的正样本数如果明显大于正常值可以考虑适当放宽分配器的阈值或者检查是否需要调整候选框数量。5.2 输入分辨率与标签分配联合调整MPDIoU能提升小目标检测上限但如果输入分辨率太低小目标的像素信息已经丢失损失函数也只能在残存的信息上做文章。我的经验是当数据里小目标占比超过30%时把输入分辨率从640调到960配合MPDIoU收益会叠加。显存不够的话可以采用梯度累积或者把batch size从32降到16同时延长训练轮数弥补收敛速度。我实测下来同样的MPDIoU在960分辨率下比640分辨率下mAP50-95多提升1.2个点左右。这个提升幅度看起来不大但在密集小目标场景里已经是很可贵的收益了。5.3 数据增强的密集目标专项策略最后一个配套操作是数据增强。MPDIoU本身对遮挡有更好的建模能力但如果训练数据里的遮挡情况太少这个优势就发挥不出来。我这里常用的策略是在Mosaic生成时特意控制剪切区域的缩放比例让目标在拼接后能出现部分重叠另外开启Copy-Paste增强把同一张图里的小目标随机复制到附近位置模拟密集叠放效果。这样MPDIoU在训练中被迫频繁处理“两个框非常接近”的样本学到的边界信息会更扎实。我现在的习惯是每次换损失函数之前先花十分钟看数据增强配置里生成的重叠样本比例低于预期就先补增强再训练。法跟网络结构改造比损失函数的改动成本低得多但需要你对它的适用边界心里有数——密集小目标、互相遮挡、像素占比小满足这几个前提MPDIoU才会真正发挥出它的价值。希望这次的分享能帮你少走一点弯路。