4. 置信度损失详解:Focal Loss在YOLO中的应用
正负样本不平衡,这问题在目标检测里太常见了。你想想看,一张图里可能就几个目标,但背景区域占了绝大多数。YOLO在训练时,每个网格会预测多个锚框,其中大部分都是背景——也就是负样本。这些负样本数量多、贡献小,却会主导损失函数,让模型学不到真正重要的东西。
我刚开始做YOLO训练时,就吃过这个亏。模型训练完,loss降得挺漂亮,但一跑推理,全是误检。说白了,模型被海量负样本带偏了,它学会了「说没有」,却没学会「说有什么」。
核心问题:负样本数量远大于正样本,导致损失函数被负样本主导,模型难以聚焦于正样本的学习。
4.1 标准交叉熵的困境
先回顾一下YOLOv1/v2用的标准交叉熵损失。对于二分类的置信度预测,公式长这样:
CE(p, y) = -y * log(p) - (1 - y) * log(1 - p)
其中y是真实标签(1表示有目标,0表示背景),p是模型预测的置信度。
这个公式有什么问题?我举个例子。假设一张图有100个锚框,其中只有1个正样本,99个负样本。每个负样本的损失虽然小,但99个加起来,轻松碾压那1个正样本的损失。模型优化时,会优先把负样本的预测值往0压,正样本反而被忽略了。
嗯,这里要注意:不是所有负样本都「平等」。那些靠近目标但没完全对齐的锚框,预测置信度可能在0.4~0.6之间,它们产生的损失比那些远离目标的锚框大得多。但即便如此,数量优势依然让负样本占据主导。
4.2 Focal Loss的诞生
2017年,何恺明团队在RetinaNet中提出了Focal Loss。我第一眼看到这个公式时,就觉得——这思路太巧妙了。它本质上是对标准交叉熵的「加权改造」:
FL(p, y) = -y * (1 - p)^γ * log(p) - (1 - y) * p^γ * log(1 - p)
关键就在那个 (1 - p)^γ 和 p^γ 上。γ是一个可调的超参数,通常取2。
为什么会这样?你想想看:
- 对于正样本(y=1):如果模型预测p接近1,说明这个样本已经学好了,
(1 - p)^γ接近0,损失被大幅降低。如果p很小(模型预测错了),(1 - p)^γ接近1,损失几乎不变。 - 对于负样本(y=0):如果模型预测p接近0(正确预测为背景),
p^γ接近0,损失被压到几乎忽略。如果p很大(模型误判为目标),p^γ接近1,损失保留。
说白了,Focal Loss就是「让模型更关注那些难分样本,忽略那些已经学好的样本」。这正好解决了正负样本不平衡的问题——大量容易分类的负样本被降权,模型可以把精力集中在少数正样本和难分负样本上。
我的经验:γ=2是最常用的取值。我在项目中试过γ=1.5和γ=3,效果都不如γ=2稳定。但如果你遇到极端不平衡(比如1:10000),可以试试γ=3甚至γ=4。
4.3 Focal Loss在YOLO中的落地
YOLOv3及之后的版本,其实并没有直接使用Focal Loss。为什么?因为YOLO本身已经通过一些设计缓解了正负样本不平衡:
- YOLOv3只对与GT有最高IoU的锚框计算正样本损失
- 忽略那些IoU在0.5~0.7之间的锚框(不参与损失计算)
- 只对IoU小于0.5的锚框计算负样本损失
但我在实际项目中,还是经常把Focal Loss加进去。特别是当你的数据集本身就有严重的长尾分布时——比如工业质检场景,缺陷样本可能只占0.1%。
我曾经在一个PCB缺陷检测项目里,正负样本比例达到了1:5000。用标准交叉熵训练,模型几乎检测不到任何缺陷。后来我把置信度损失替换成Focal Loss,γ设成2.5,mAP直接从0.12跳到了0.67。嗯,这个提升让我印象很深。
4.4 代码实现:自定义Focal Loss
下面是我在YOLOv5基础上修改的Focal Loss实现。注意,这里只替换了置信度损失部分,分类和回归损失保持不变:
import torch
import torch.nn as nn
import torch.nn.functional as F
class FocalLoss(nn.Module):
def __init__(self, gamma=2.0, alpha=None, reduction='mean'):
super(FocalLoss, self).__init__()
self.gamma = gamma
self.alpha = alpha # 类别权重,一般不用
self.reduction = reduction
def forward(self, pred, target):
"""
pred: 预测置信度,shape [N, 1] 或 [N]
target: 真实标签,0或1,shape [N]
"""
# 计算标准交叉熵
ce_loss = F.binary_cross_entropy_with_logits(
pred, target, reduction='none'
)
# 计算pt(预测正确的概率)
pt = torch.exp(-ce_loss) # pt = p if y=1, pt = 1-p if y=0
# 应用Focal Loss调制因子
focal_weight = (1 - pt) ** self.gamma
# 如果使用alpha平衡,可以加上
if self.alpha is not None:
alpha_t = self.alpha * target + (1 - self.alpha) * (1 - target)
focal_weight = alpha_t * focal_weight
# 最终损失
loss = focal_weight * ce_loss
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
else:
return loss
使用时,只需要把YOLO中计算置信度损失的部分替换掉:
# 原来的置信度损失
# loss_conf = F.binary_cross_entropy_with_logits(pred_conf, tconf)
# 替换为Focal Loss
focal_loss = FocalLoss(gamma=2.0)
loss_conf = focal_loss(pred_conf, tconf)
注意:Focal Loss不是万能的。如果你的数据集正负样本比例已经比较均衡(比如1:3以内),加Focal Loss反而可能降低性能。我建议先跑一版基线,确认正负样本比例确实失衡,再考虑使用。
4.5 调参建议与避坑指南
关于γ的取值,我整理了一个经验表:
| 正负样本比例 | 推荐γ值 | 说明 |
|---|---|---|
| 1:10 ~ 1:50 | 1.5 ~ 2.0 | 轻度不平衡,γ=2足够 |
| 1:50 ~ 1:500 | 2.0 ~ 2.5 | 中度不平衡,需要更强调制 |
| 1:500 ~ 1:5000 | 2.5 ~ 3.5 | 严重不平衡,注意不要过拟合 |
| 1:5000以上 | 3.5 ~ 5.0 | 极端情况,建议配合数据增强 |
我曾经犯过一个错误:在正负样本比例1:20的数据集上,直接把γ设成4。结果模型训练时loss降得飞快,但验证集mAP反而下降了。后来分析发现,γ太大导致模型几乎忽略了所有负样本,连那些「应该被学习」的难分负样本也被忽略了。
所以我的建议是:从γ=2开始,观察训练集和验证集的loss曲线。如果训练loss下降很快但验证loss不降,说明γ可能偏大。如果两个loss都降得慢,可以适当增大γ。
4.6 小结
Focal Loss是解决正负样本不平衡的利器,但它不是银弹。我个人习惯把它作为「备选方案」——先用标准交叉熵跑一版,如果发现模型对正样本召回率偏低,再换成Focal Loss。调参时从γ=2起步,根据实际效果微调。
下一章,我会讲分类损失和回归损失的协同优化。这两个损失如果配合不好,模型要么定位不准,要么分类出错。嗯,到时候我会分享一个我踩过的坑——分类损失权重设太大,导致模型变成了「分类器」而不是「检测器」。