一、从一次深夜调试说起

上周在产线部署YOLO检测模型,遇到个诡异现象:模型在测试集上mAP挺漂亮,一到产线实时视频流,小目标漏检率直接飙升。查了一整晚,发现产线场景中待检目标(缺陷)只占图像面积的不到5%,而背景和干扰物占了绝大多数。模型在训练时被海量的负样本“带偏”了,这就是典型的类别不平衡问题。

如果你也在训练集里见过那种标注框寥寥无几、背景铺满全图的场景,今天聊的Focal Loss可能就是你的解药。


二、类别不平衡:不只是数据多少的问题

类别不平衡在目标检测里是个老难题。传统交叉熵损失对每个样本“一视同仁”,当负样本(背景)数量远多于正样本(目标)时,模型会倾向于把所有样本都预测为负类——因为这样整体损失也能降得很低。

举个例子,假如1000个样本里只有5个正样本,模型就算全部预测为负类,准确率也有99.5%。看起来很美,但我们要的正样本召回率却是0。

YOLO本身用了objectness score和类别预测分离的结构,但默认的二元交叉熵损失依然对不平衡数据敏感。尤其在嵌入式端部署时,为了控制计算量,输入分辨率往往较低,小目标本身像素就少,正负样本比例进一步恶化。


三、Focal Loss:让模型“专注”难样本

Focal Loss的提出,最初是为了解决one-stage检测器(如RetinaNet)的正负样本极端不平衡问题。它的核心思想很简单:降低易分类样本的损失权重,让模型更关注难分类的样本

来看公式:

# 标准交叉熵损失
CE(p, y) = -[y * log(p) + (1-y) * log(1-p)]

# Focal Loss 变体
FL(p, y) = -[α * (1-p)^γ * y * log(p) + (1-α) * p^γ * (1-y) * log(1-p)]

这里有两个关键参数:

  • γ(gamma):调节因子。当样本被正确分类且p很大时,(1-p)^γ会很小,从而降低该样本的损失贡献。γ越大,对易分样本的抑制越强。
  • α(alpha):类别权重。可以给正样本一个更高的权重,缓解数量不平衡。

我一般习惯先设γ=2.0,α=0.25作为起点。这个组合在多数检测任务上都能带来稳定提升。


四、在YOLOv11中集成Focal Loss

YOLO的损失通常包含三部分:bbox回归、objectness、分类损失。Focal Loss主要用在objectness和分类损失上。

下面是个简化的实现片段,注意看注释:

class FocalLoss(nn.Module):
    def __init__(self, alpha=0.25, gamma=2.0, reduction='mean'):
        super().__init__()
        self.alpha = alpha
        self.gamma = gamma
        self.reduction = reduction
        
    def forward(self, inputs, targets):
        # 这里踩过坑:inputs需要先过sigmoid,别直接用原始输出
        p = torch.sigmoid(inputs)
        ce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction='none')
        
        # 计算p_t,这里区分正负样本的处理
        p_t = p * targets + (1 - p) * (1 - targets)
        
        # focal weight 计算
        alpha_t = self.alpha * targets + (1 - self.alpha) * (1 - targets)
        focal_weight = alpha_t * (1 - p_t) ** self.gamma
        
        loss = focal_weight * ce_loss
        
        # 实测发现,对focal loss取mean比sum稳定,尤其是batch内正样本极少时
        if self.reduction == 'mean':
            return loss.mean()
        elif self.reduction == 'sum':
            return loss.sum()
        else:
            return loss

在YOLO的head部分替换损失函数时,注意objectness分支和分类分支可以共享同一个Focal Loss实例,但建议对两个分支独立调参。因为objectness的不平衡程度通常比分类更严重。


五、训练时的几个坑点

  1. 学习率需要微调:Focal Loss改变了损失尺度,尤其是γ较大时,整体损失会变小。建议初始学习率比默认值降低20%-30%,否则前期容易震荡。

  2. 正样本定义要一致:YOLO的anchor匹配策略决定了哪些样本是“正”。如果原本的匹配阈值(比如IoU>0.5)太严,正样本数会更少,不平衡问题加剧。可以适当放宽到0.4-0.5之间,但要注意召回率和精度的trade-off。

  3. 验证指标别只看mAP:加上每个类别的AP,特别是小样本类别。有时候整体mAP没变,但少数类的检测率提升了,这对实际项目可能就是关键突破。

  4. γ别盲目调大:有些文章推荐γ=5.0,我在嵌入式场景实测发现,γ>3.0后训练变得极不稳定,建议从2.0开始,每次增加0.5慢慢试。


六、与其他策略的结合

Focal Loss不是银弹,通常需要配合其他手段:

  • 数据层面:适度过采样正样本图像,或者用copy-paste数据增强(把目标随机粘贴到背景中)。
  • 架构层面:YOLOv11的neck部分可以加入SPPF或注意力模块,增强小目标特征。
  • 损失层面:Focal Loss可以和DIoU损失组合,一个解决分类不平衡,一个优化框回归质量。

我在一个缺陷检测项目上的组合是:Focal Loss(γ=2.0, α=0.25) + DIoU Loss + 轻度过采样。最终小目标召回率从63%提升到78%,且没有增加推理时间。


七、一些经验性建议

  1. 先分析再动手:训练前统计一下你的正负样本比例。如果负样本是正样本的100倍以上,Focal Loss大概率有效;如果比例在10:1以内,可能收益不大,反而增加调参成本。

  2. 分阶段训练:先用标准CE损失训几轮,让模型先学到基础特征,再切换到Focal Loss微调。这样比一开始就用FL更稳定。

  3. 关注难样本挖掘:Focal Loss本质是“软性”的难样本挖掘。训练中期可以可视化一下哪些样本损失依然很高,这些可能就是你的数据标注盲区或需要特殊增强的案例。

  4. 部署时注意:Focal Loss只影响训练过程,不会增加推理计算量。这点在嵌入式部署时是个重要优势——性能提升不拖累速度。


八、最后说两句

损失函数改进从来不是换个公式就能一劳永逸的。Focal Loss解决了一部分样本权重问题,但它也引入了新的超参。我的习惯是:每次只改一个变量,做好消融实验记录

实际项目里,我曾见过团队花两周调Focal Loss参数,最后发现问题的根源其实是标注不一致。所以,当模型表现异常时,先检查数据,再检查损失函数,最后再看架构。这个顺序能帮你省下不少深夜调试的时间。

下次我们聊聊损失函数的另一个方向:基于IoU的回归损失优化。那个在密集物体检测和重叠框处理上,又是另一番故事了。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐