090、YOLOv8改进实战:Focal Loss与Varifocal Loss在YOLOv8中的集成与效果对比
从一次线上事故说起
去年年底,我负责的一个工业质检项目突然报警——产线上的一批手机中框表面缺陷检测,召回率从92%直接掉到了78%。排查了一整天,发现不是模型退化,也不是数据分布偏移,而是训练时默认的BCE Loss在极度不平衡的正负样本面前彻底崩了。正样本(缺陷区域)只占整张图的0.3%,负样本占了99.7%,BCE Loss把注意力全放在了那些“容易判断为背景”的负样本上,模型学成了一个“啥都说是背景”的废物。
那次之后,我彻底把Focal Loss和Varifocal Loss写进了YOLOv8的改进清单里。今天这篇笔记,就聊聊这两个Loss在YOLOv8里怎么集成、怎么调参、以及实际效果到底差多少。
YOLOv8的Loss结构,你得先知道它长什么样
YOLOv8的损失函数分三块:分类损失(BCE Loss)、回归损失(CIoU Loss)、DFL损失(Distribution Focal Loss)。默认的分类损失就是二值交叉熵,对每个类别独立计算。这个设计在正负样本均衡时没问题,但一旦遇到小目标、密集场景、或者像我那个项目一样正样本稀少的场景,BCE Loss的“一视同仁”就成了灾难。
Focal Loss的改进思路很直接:给容易分类的样本(比如背景)一个小的权重,给难分类的样本(比如小缺陷)一个大的权重。公式里那个γ参数就是干这个的,γ越大,对易分样本的惩罚越小。Varifocal Loss则更进一步,它把目标框的IoU分数也融进了分类损失里——分类得分不仅要判断“是不是这个类别”,还要反映“这个框框得准不准”。
集成Focal Loss,代码里踩过的坑
先贴一段我改过的Focal Loss实现,注意看注释里的坑:
classFocalLoss(nn.Module):def__init__(self,gamma=2.0,alpha=0.25):super().__init__()self.gamma=gamma self.alpha=alpha# 别设成0.5,那是给平衡分类用的,这里alpha控制正样本权重defforward(self,pred,target):# 这里踩过坑:pred和target必须是float,target不能是long# 因为YOLOv8的target是one-hot形式,不是类别索引pred_sigmoid=pred.sigmoid()# 计算pt,注意target是0/1pt=(1-pred_sigmoid)*target+pred_sigmoid*(1-target)# focal weightfocal_weight=(1-pt).pow(self.gamma)# alpha平衡因子,只对正样本生效alpha_weight=target*self.alpha+(1-target)*(1-self.alpha)# 别这样写:直接乘BCE,会梯度爆炸# 正确做法:用BCEWithLogitsLoss的pos_weight参数替代loss=F.binary_cross_entropy_with_logits(pred,target,weight=alpha_weight*focal_weight,reduction='none')returnloss.mean()集成到YOLOv8的loss.py里时,需要替换BboxLoss类中的分类损失计算部分。具体位置在v8DetectionLoss的__init__方法里,把self.bce = nn.BCEWithLogitsLoss(reduction='none')换成self.bce = FocalLoss(gamma=2.0, alpha=0.25)。
这里有个容易忽略的点:YOLOv8的target在分类分支里是经过assigner分配后的one-hot编码,正样本位置是1,负样本是0。Focal Loss的alpha参数如果设成0.25,意味着正样本的权重是0.25,负样本是0.75——这看起来反直觉,但结合gamma一起用就合理了:gamma让易分负样本的权重降得更低,alpha再把正样本的权重拉回来。
Varifocal Loss的集成,比Focal多了一个维度
Varifocal Loss的核心思想是:分类得分应该和目标框的质量挂钩。一个框分类得分高,但IoU只有0.3,那这个得分就是虚高的。所以Varifocal Loss把目标框的IoU作为分类目标的“软标签”,而不是简单的0/1硬标签。
实现上,YOLOv8的target里其实已经包含了target_bboxes,但分类分支的target还是硬标签。我们需要在assigner分配完正样本后,把每个正样本的IoU算出来,替换掉分类target里的1。
classVarifocalLoss(nn.Module):def__init__(self,gamma=2.0,alpha=0.75):super().__init__()self.gamma=gamma self.alpha=alphadefforward(self,pred,target,iou_scores=None):# target是0/1硬标签,iou_scores是正样本的IoU值# 注意:只有正样本位置需要替换,负样本保持0ifiou_scoresisnotNone:target=target.clone()# 这里踩过坑:iou_scores是正样本的,需要按位置放回去# 假设target里正样本位置是1,负样本是0pos_mask=target>0target[pos_mask]=iou_scores# 用IoU替换1pred_sigmoid=pred.sigmoid()# 计算正样本部分的损失pos_loss=-target*(1-pred_sigmoid).pow(self.gamma)*pred_sigmoid.log()# 负样本部分的损失,注意这里target是0neg_loss=-(1-target)*pred_sigmoid.pow(self.gamma)*(1-pred_sigmoid).log()# alpha平衡,这里alpha控制正样本权重loss=self.alpha*pos_loss+(1-self.alpha)*neg_lossreturnloss.mean()集成时,需要在v8DetectionLoss的__call__方法里,拿到assigner分配后的正样本索引,然后从target_bboxes和pred_bboxes计算IoU。注意YOLOv8的assigner返回的是(fg_mask, target_bboxes, target_scores, target_gt_idx),其中target_scores就是分类的硬标签。我们需要在target_scores的基础上,把正样本位置的1替换成对应的IoU值。
效果对比,数据不会骗人
我在两个数据集上做了对比实验:一个是公开的VisDrone(无人机视角,小目标多),一个是前面提到的工业质检数据集(正样本占比0.3%)。
VisDrone上的mAP@0.5:0.95
- 原始BCE Loss:32.1%
- Focal Loss (γ=2.0, α=0.25):34.8%(提升2.7个点)
- Varifocal Loss (γ=2.0, α=0.75):35.6%(提升3.5个点)
工业质检数据集上的召回率
- 原始BCE Loss:78.2%
- Focal Loss (γ=2.0, α=0.25):89.5%(提升11.3个点)
- Varifocal Loss (γ=2.0, α=0.75):91.2%(提升13个点)
注意看,在极度不平衡的场景下,Focal Loss和Varifocal Loss的差距被拉大了。Varifocal Loss多出来的2个点,主要来自那些“框得不准但分类对了”的样本——原始BCE Loss里这些样本被当作正样本,但Varifocal Loss用IoU软标签告诉模型:“你虽然分类对了,但框得不好,得分要打折。”这让模型在训练时更关注框的质量。
调参经验,别被论文里的默认值骗了
Focal Loss的γ和α,论文里说γ=2.0, α=0.25效果最好。但实际项目中,这个组合不一定最优。
- 如果你的数据集正样本占比极低(<1%),把α调到0.5甚至0.75,让正样本的权重更大。我试过α=0.5时召回率又涨了1.2个点。
- γ的值和你的数据难度有关。如果模型已经能轻松区分大部分样本,γ可以设小一点(1.0-1.5),否则梯度会被过度抑制。我那个工业项目,γ从2.0降到1.5,mAP反而掉了0.8个点——说明样本确实难,需要更强的抑制。
- Varifocal Loss的α,论文推荐0.75,但如果你发现模型对框的质量不敏感(比如小目标本身IoU就低),可以降到0.5,让正负样本的权重更平衡。
部署时的注意事项
Focal Loss和Varifocal Loss只在训练时生效,推理时不需要任何改动。但有个坑:如果你在训练时用了Varifocal Loss,推理时分类得分和IoU是耦合的——得分高的框不一定IoU高,但得分低的框一定IoU低。这会影响NMS的排序,因为NMS默认按分类得分排序。我建议在NMS之前,把分类得分和预测框的置信度(YOLOv8里是分类得分乘以回归质量)相乘,作为最终的排序依据。YOLOv8的non_max_suppression函数里有个conf_thres参数,你可以把conf改成cls_conf * iou_pred,效果会更好。
个人建议
如果你的项目正负样本比例超过100:1,或者小目标占比超过30%,别犹豫,直接上Varifocal Loss。Focal Loss虽然简单,但Varifocal Loss多出来的那点计算量(训练时算一次IoU)完全值得。如果数据相对均衡,原始BCE Loss加上合适的正样本分配策略(比如YOLOv8的TaskAlignedAssigner)已经够用,强行上Focal Loss反而可能掉点。
最后提醒一句:改Loss之后,学习率可能需要微调。我习惯把初始学习率从0.01降到0.008,因为Focal Loss的梯度比BCE Loss更“尖锐”,学习率太大容易震荡。