052、Varifocal LossIoU-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是预测的分类分数经过sigmoidq是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,alpha0.75,gamma2.0):super().__init__()self.alphaalpha# 负样本权重系数别设太大0.75够用self.gammagamma# 聚焦参数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_sigmoidpred_score.sigmoid()# 正样本部分只对mask_positive为1的位置计算# 这里用到了gt_score作为权重IoU越高权重越大pos_weightgt_score*mask_positive# [B, N, C]# 核心公式q * (q * log(p) (1-q) * log(1-p))# 注意这里用clamp防止log(0)min1e-8比较安全pos_losspos_weight*(gt_score*torch.log(pred_sigmoid.clamp(min1e-8))(1-gt_score)*torch.log((1-pred_sigmoid).clamp(min1e-8)))# 负样本部分mask_positive取反mask_negative1-mask_positive# 这里有个坑负样本的target是0但公式里用到了p^γ# 如果直接用pred_sigmoid那些预测分数高的负样本会被严重惩罚neg_weightself.alpha*(pred_sigmoid**self.gamma)*mask_negative# 负样本的交叉熵target0所以简化为log(1-p)neg_lossneg_weight*torch.log((1-pred_sigmoid).clamp(min1e-8))# 最终损失取负号因为上面算的是logloss-(pos_lossneg_loss)# 这里踩过坑不要直接mean应该先sum再除以正样本数量# 否则负样本太多会稀释正样本的梯度num_posmask_positive.sum()ifnum_pos0:lossloss.sum()/num_poselse:lossloss.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仍然用BCEtarget是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擅长的是让分类分数更“诚实”——高分框确实准低分框确实歪。
052、Varifocal Loss:IoU-Aware 分类分数设计的完整公式与代码
发布时间:2026/6/8 13:20:49
052、Varifocal LossIoU-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是预测的分类分数经过sigmoidq是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,alpha0.75,gamma2.0):super().__init__()self.alphaalpha# 负样本权重系数别设太大0.75够用self.gammagamma# 聚焦参数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_sigmoidpred_score.sigmoid()# 正样本部分只对mask_positive为1的位置计算# 这里用到了gt_score作为权重IoU越高权重越大pos_weightgt_score*mask_positive# [B, N, C]# 核心公式q * (q * log(p) (1-q) * log(1-p))# 注意这里用clamp防止log(0)min1e-8比较安全pos_losspos_weight*(gt_score*torch.log(pred_sigmoid.clamp(min1e-8))(1-gt_score)*torch.log((1-pred_sigmoid).clamp(min1e-8)))# 负样本部分mask_positive取反mask_negative1-mask_positive# 这里有个坑负样本的target是0但公式里用到了p^γ# 如果直接用pred_sigmoid那些预测分数高的负样本会被严重惩罚neg_weightself.alpha*(pred_sigmoid**self.gamma)*mask_negative# 负样本的交叉熵target0所以简化为log(1-p)neg_lossneg_weight*torch.log((1-pred_sigmoid).clamp(min1e-8))# 最终损失取负号因为上面算的是logloss-(pos_lossneg_loss)# 这里踩过坑不要直接mean应该先sum再除以正样本数量# 否则负样本太多会稀释正样本的梯度num_posmask_positive.sum()ifnum_pos0:lossloss.sum()/num_poselse:lossloss.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仍然用BCEtarget是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擅长的是让分类分数更“诚实”——高分框确实准低分框确实歪。