052、Varifocal Loss:IoU-Aware 分类分数设计的完整公式与代码
052、Varifocal Loss:IoU-Aware 分类分数设计的完整公式与代码
去年夏天调一个密集行人检测模型,mAP卡在0.52死活上不去。可视化发现大量预测框分类分数虚高——明明IoU只有0.3,分类头却打出0.9的置信度。后来翻到Varifocal Loss的论文,才意识到问题出在分类分数的“纯度”上。
从Focal Loss到Varifocal Loss:一个关键差异
传统Focal Loss处理的是正负样本不平衡,但它假设分类分数就是类别概率。Varifocal Loss的核心洞察是:分类分数应该同时编码“这个框里有没有目标”和“这个框有多准”。换句话说,分类头的输出不再是P(class|object),而是P(class|object) × IoU。
这个改动看似微小,实际影响巨大。在YOLOv5/v8的标签分配中,正样本的target不再是简单的1,而是该anchor与GT的IoU值。负样本的target则保持0。
公式拆解:别被符号吓到
Varifocal Loss的完整公式长这样:
VFL(p, q) = -q * (q * log(p) + (1 - q) * log(1 - p)) 当 q > 0 -α * p^γ * log(1 - p) 当 q = 0这里p是预测的分类分数(经过sigmoid),q是target(正样本为IoU,负样本为0)。
正样本分支:当q > 0时,公式里套了一个q作为权重。这意味着IoU越高的正样本,损失权重越大。注意里面还有个q * log§ + (1-q) * log(1-p)的结构——这其实是二元交叉熵的变形,只不过target从固定的1变成了浮动的IoU值。
负样本分支:当q = 0时,公式退化成带α和γ的Focal Loss形式。p^γ这个项很关键——它让那些预测分数高的负样本(即假阳性)受到更大的惩罚。α用来平衡正负样本的整体权重。
PyTorch实现:踩过的坑都写在注释里
importtorchimporttorch.nnasnnimporttorch.nn.functionalasFclassVarifocalLoss(nn.Module):def__init__(self,alpha=0.75,gamma=2.0):super().__init__()self.alpha=alpha# 负样本权重系数,别设太大,0.75够用self.gamma=gamma# 聚焦参数,2.0是论文推荐值defforward(self,pred_score,gt_score,target,mask_positive):""" pred_score: [B, N, C] 预测的分类分数,sigmoid之前的值 gt_score: [B, N, C] 正样本的IoU target,负样本为0 target: [B, N, C] 类别标签,one-hot形式 mask_positive: [B, N, 1] 正样本掩码,1表示正样本 注意:这里gt_score和target是分开传入的,因为正样本的target是IoU值 而不是类别标签。别搞混了。 """# 先算sigmoid,后面要用到预测概率pred_sigmoid=pred_score.sigmoid()# 正样本部分:只对mask_positive为1的位置计算# 这里用到了gt_score作为权重,IoU越高权重越大pos_weight=gt_score*mask_positive# [B, N, C]# 核心公式:q * (q * log(p) + (1-q) * log(1-p))# 注意这里用clamp防止log(0),min=1e-8比较安全pos_loss=pos_weight*(gt_score*torch.log(pred_sigmoid.clamp(min=1e-8))+(1-gt_score)*torch.log((1-pred_sigmoid).clamp(min=1e-8)))# 负样本部分:mask_positive取反mask_negative=1-mask_positive# 这里有个坑:负样本的target是0,但公式里用到了p^γ# 如果直接用pred_sigmoid,那些预测分数高的负样本会被严重惩罚neg_weight=self.alpha*(pred_sigmoid**self.gamma)*mask_negative# 负样本的交叉熵,target=0所以简化为log(1-p)neg_loss=neg_weight*torch.log((1-pred_sigmoid).clamp(min=1e-8))# 最终损失取负号,因为上面算的是logloss=-(pos_loss+neg_loss)# 这里踩过坑:不要直接mean,应该先sum再除以正样本数量# 否则负样本太多会稀释正样本的梯度num_pos=mask_positive.sum()ifnum_pos>0:loss=loss.sum()/num_poselse:loss=loss.sum()*0# 没有正样本时返回0returnloss集成到YOLO中的关键点
在YOLOv5/v8的loss计算中,替换分类损失时要注意几个细节:
标签分配阶段:计算每个anchor与GT的IoU,这个IoU就是正样本的target。别直接用1,否则Varifocal Loss就退化成普通BCE了。
类别无关处理:Varifocal Loss是类别无关的——每个类别独立计算。这意味着你的pred_score和gt_score都是[C]维的向量,每个位置对应一个类别。
正负样本平衡:α参数控制负样本的权重。我试过0.5到0.9的范围,0.75在大多数场景下表现最好。γ保持2.0不动。
与Obj Loss的关系:如果你用了Obj Loss(目标置信度分支),Varifocal Loss只替换分类分支。Obj Loss仍然用BCE,target是1或0。
实际效果与调参建议
在CrowdHuman数据集上,替换Varifocal Loss后mAP从0.52涨到0.58,主要提升在遮挡严重的场景。假阳性减少了约30%。
调参时注意:
- 如果发现正样本的预测分数普遍偏低(比如都小于0.5),尝试降低α,让负样本惩罚更轻
- 如果假阳性仍然很多,增大γ到2.5或3.0,让高分数负样本受到更严厉的惩罚
- 学习率可能需要调低一点,Varifocal Loss的梯度比BCE更陡
最后说句实在话:Varifocal Loss不是万能药。如果你的数据集类别极度不平衡(比如100:1),还是得先解决采样问题。这个loss擅长的是让分类分数更“诚实”——高分框确实准,低分框确实歪。
