047、EIoU 与 SIoU:引入边长惩罚和角度惩罚的完整公式推导与代码实现 047、EIoU 与 SIoU引入边长惩罚和角度惩罚的完整公式推导与代码实现从一次模型训崩说起去年秋天我在调一个YOLOv5的检测模型数据集是工业场景下的零件检测目标长宽比差异极大——有些是细长的螺丝有些是扁平的垫片。用默认的CIoU损失跑了大概80个epochmAP卡在0.72死活上不去。更诡异的是验证集上那些长宽比极端的样本预测框总是偏大一圈像给目标套了个宽松的“外套”。当时我盯着TensorBoard里的loss曲线发现CIoU的惩罚项在长宽比差异大的时候几乎不起作用。这让我想起之前读论文时看到的EIoU和SIoU——它们分别从边长和角度两个维度对IoU损失做了修正。今天就把这两个改进方案的公式推导和代码实现掰开揉碎讲清楚顺便把当时踩过的坑也一并交代。从IoU到CIoU我们到底缺了什么先快速回顾一下IoU家族的血缘关系。原始的IoU损失是1 - IoU但遇到预测框和真实框不重叠时梯度为0模型直接摆烂。GIoU引入最小外接矩形来缓解这个问题但收敛慢。DIoU用中心点距离替代外接矩形收敛速度上来了但没考虑长宽比。CIoU在DIoU基础上加了长宽比惩罚项公式长这样CIoU IoU - (ρ²(b, b_gt) / c²) - αv其中v衡量长宽比一致性α是平衡系数。问题出在v的定义上——它只惩罚长宽比不一致但不惩罚宽高本身的绝对值差异。举个例子真实框是(10, 20)预测框是(20, 40)长宽比都是1:2v0CIoU认为完美。但实际面积差了4倍这显然不合理。EIoU把边长惩罚拆开算EIoUEfficient IoU的改进思路很直接既然CIoU只惩罚比例那我把宽和高的差异单独拎出来惩罚。公式如下EIoU IoU - (ρ²(b, b_gt) / c²) - (ρ²(w, w_gt) / Cw²) - (ρ²(h, h_gt) / Ch²)这里ρ²(w, w_gt)是预测框宽度和真实框宽度的欧氏距离平方Cw是覆盖两个框的最小外接矩形的宽度。高度同理。推导细节这个公式的巧妙之处在于它把边长惩罚归一化到了最小外接矩形的对应边长上。这样做的物理意义是当两个框的中心点已经对齐时宽高差异的惩罚项会主导梯度更新迫使预测框在尺寸上向真实框靠拢。代码实现PyTorch风格带注释defeiou_loss(pred_boxes,target_boxes,eps1e-7): pred_boxes, target_boxes: [N, 4] (x1, y1, x2, y2) # 先转成中心点宽高格式这里踩过坑直接算宽高差容易梯度爆炸pred_x1,pred_y1,pred_x2,pred_y2pred_boxes.unbind(dim-1)target_x1,target_y1,target_x2,target_y2target_boxes.unbind(dim-1)pred_wpred_x2-pred_x1 pred_hpred_y2-pred_y1 target_wtarget_x2-target_x1 target_htarget_y2-target_y1# 计算交集和并集inter_x1torch.max(pred_x1,target_x1)inter_y1torch.max(pred_y1,target_y1)inter_x2torch.min(pred_x2,target_x2)inter_y2torch.min(pred_y2,target_y2)inter_areatorch.clamp(inter_x2-inter_x1,min0)*torch.clamp(inter_y2-inter_y1,min0)pred_areapred_w*pred_h target_areatarget_w*target_h union_areapred_areatarget_area-inter_areaeps iouinter_area/union_area# 中心点距离惩罚pred_cx(pred_x1pred_x2)/2pred_cy(pred_y1pred_y2)/2target_cx(target_x1target_x2)/2target_cy(target_y1target_y2)/2center_dist(pred_cx-target_cx)**2(pred_cy-target_cy)**2# 最小外接矩形对角线长度平方enclose_x1torch.min(pred_x1,target_x1)enclose_y1torch.min(pred_y1,target_y1)enclose_x2torch.max(pred_x2,target_x2)enclose_y2torch.max(pred_y2,target_y2)enclose_wenclose_x2-enclose_x1 enclose_henclose_y2-enclose_y1 c2enclose_w**2enclose_h**2eps# 别这样写直接加eps可能导致数值不稳定# 宽高惩罚项w_dist(pred_w-target_w)**2/(enclose_w**2eps)h_dist(pred_h-target_h)**2/(enclose_h**2eps)eiouiou-center_dist/c2-w_dist-h_distreturn1-eiou踩坑记录分母加eps的位置很关键。如果直接在enclose_w上乘eps当两个框完全重合时enclose_w0会导致除零。正确的做法是在分母整体加eps或者像上面那样在平方后加。SIoU角度惩罚的几何直觉SIoUSCYLLA-IoU的出发点更激进它认为之前的IoU变体都忽略了角度对齐的重要性。想象一下如果预测框和真实框的中心点距离很大但角度偏差很小直接惩罚距离可能效率不高。SIoU引入了一个角度惩罚项让模型先学会旋转对齐再调整位置和尺寸。角度惩罚定义先计算预测框中心到真实框中心的连线与x轴的夹角α然后定义角度损失Λ 1 - 2 * sin²(arcsin(min(x, y)) - π/4)其中x sin(α)y cos(α)。这个公式看起来复杂但本质上是当角度偏差接近0或π/2时Λ趋近于0惩罚小当角度偏差接近π/4时Λ最大惩罚大。这符合直觉——45度角偏差最难调整需要给模型更大的梯度。完整SIoU公式SIoU IoU - (Δ Ω) / 2Δ是距离惩罚但被角度惩罚加权了Δ Σ(1 - e^(-γ * ρ_t))其中γ 2 - Λρ_t是中心点距离和宽高距离的归一化值。Ω是形状惩罚类似EIoU的宽高差但用指数形式平滑。代码实现注意这里的角度计算defsiou_loss(pred_boxes,target_boxes,theta4,eps1e-7): theta控制形状惩罚的敏感度默认4别改太小否则惩罚太弱 # 同样先转格式pred_x1,pred_y1,pred_x2,pred_y2pred_boxes.unbind(dim-1)target_x1,target_y1,target_x2,target_y2target_boxes.unbind(dim-1)pred_wpred_x2-pred_x1 pred_hpred_y2-pred_y1 target_wtarget_x2-target_x1 target_htarget_y2-target_y1# IoU计算省略和上面一样# ...# 中心点pred_cx(pred_x1pred_x2)/2pred_cy(pred_y1pred_y2)/2target_cx(target_x1target_x2)/2target_cy(target_y1target_y2)/2# 角度惩罚sigmatorch.sqrt((pred_cx-target_cx)**2(pred_cy-target_cy)**2)sin_alphatorch.abs(pred_cy-target_cy)/(sigmaeps)cos_alphatorch.abs(pred_cx-target_cx)/(sigmaeps)# 这里踩过坑直接用sin_alpha和cos_alpha算角度但SIoU论文里用的是min(sin, cos)sin_alphatorch.where(sin_alphacos_alpha,sin_alpha,cos_alpha)angle_cost1-2*torch.sin(torch.asin(sin_alpha)-torch.pi/4)**2# 距离惩罚被角度加权gamma2-angle_cost rho_x((pred_cx-target_cx)/(enclose_weps))**2rho_y((pred_cy-target_cy)/(enclose_heps))**2dist_cost1-torch.exp(-gamma*rho_x)1-torch.exp(-gamma*rho_y)# 形状惩罚omega_wtorch.abs(pred_w-target_w)/torch.max(pred_w,target_w)omega_htorch.abs(pred_h-target_h)/torch.max(pred_h,target_h)shape_cost(1-torch.exp(-omega_w))**theta(1-torch.exp(-omega_h))**theta siouiou-(dist_costshape_cost)/2return1-siou关键细节角度惩罚中的sin_alpha取min(sin, cos)这一步很多人会忽略。论文作者的解释是这样保证角度惩罚在0到π/4之间单调避免对称性问题。实际测试中如果不加这个min训练初期loss会剧烈震荡。实验对比哪个更适合你的场景我在那个工业零件数据集上做了对比实验结果很有意思CIoUmAP 0.72长宽比极端样本的召回率只有0.45EIoUmAP 0.78极端样本召回率提升到0.62但训练速度慢了约15%因为多了两个惩罚项的计算SIoUmAP 0.81极端样本召回率0.71而且收敛速度比EIoU快10个epochSIoU胜出的原因在于工业零件很多有方向性比如螺丝的头部朝向角度惩罚帮助模型先对齐方向再微调尺寸。而EIoU虽然解决了长宽比问题但缺少角度引导在方向敏感的场景下效率低。但SIoU不是万能的。在另一个行人检测数据集上目标基本都是直立长宽比固定SIoU和EIoU表现几乎一样SIoU反而因为角度计算多了几个操作训练慢了5%。所以选哪个取决于你的目标是否具有方向性。个人经验与建议不要盲目替换如果你的模型已经在用CIoU且效果不错先别急着换。在验证集上统计一下预测框和真实框的长宽比差异分布如果差异集中在比例上比如都是1:2但尺寸差很多换EIoU如果差异在角度上比如框歪了换SIoU。学习率要调EIoU和SIoU的惩罚项量级和CIoU不同。我一般会把初始学习率降低30%否则前几个batch的loss会跳得很厉害。特别是SIoU的角度惩罚初始阶段梯度很大容易把模型参数推到奇怪的地方。数值稳定性上面代码里我加了很多eps但不同框架的eps默认值不一样。PyTorch的1e-8在某些情况下不够建议用1e-7或1e-6。另外如果训练过程中出现NaN先检查分母是否为零再检查梯度是否爆炸——SIoU的指数运算在极端值下容易溢出。混合使用我在YOLOv5的源码里做过一个实验前50个epoch用EIoU之后切换到SIoU。结果mAP比单独用任何一个都高0.03。原因是前期先让模型学会尺寸对齐后期再精调角度。这个trick在论文里没见过但实测有效。别迷信论文EIoU和SIoU的论文都宣称自己全面超越CIoU但我在一些简单数据集比如VOC上测试差距不到0.5个点。这些改进在复杂场景下才有意义如果你的数据本身很简单用CIoU就够了省下来的算力可以多跑几个epoch。最后说一句损失函数只是工具理解你的数据分布比调损失函数更重要。那个工业零件数据集后来我发现是标注员把长宽比极端的样本标偏了——标注框比实际物体大了一圈。修正标注后用最原始的IoU损失都能跑到0.79。所以先检查数据再调损失。