021、损失函数改进(三):Distribution Focal Loss与不确定性建模

张开发
2026/4/18 1:53:38 15 分钟阅读

分享文章

021、损失函数改进(三):Distribution Focal Loss与不确定性建模
从一次深夜调试说起上周在部署YOLO模型到边缘设备时遇到一个诡异现象同一个检测框在白天光照充足时置信度0.92到了黄昏就掉到0.67。阈值设0.7吧漏检设0.6吧误检满天飞。这让我开始怀疑我们是不是太过信任模型输出的那个单一置信度值了传统检测任务中我们习惯用sigmoid把输出压缩到[0,1]然后当成概率直接使用。但现实世界的模糊性、遮挡、光照变化真的能用一个标量完全表达吗这个问题引出了今天要讨论的核心不确定性建模。传统Focal Loss的局限Focal Loss大家都很熟了解决正负样本不平衡确实有效。但仔细想想它只关心“当前预测与真实标签的差距”没考虑“模型对这个预测有多大把握”。举个例子# 常见的分类头输出cls_outputself.conv(x)# [B, C, H, W]cls_scoretorch.sigmoid(cls_output)# 直接当概率用# Focal Loss计算ce_loss-label*torch.log(score)-(1-label)*torch.log(1-score)ptlabel*score(1-label)*(1-score)fl_loss((1-pt)**self.gamma)*ce_loss问题在哪cls_score这个值既包含了“是什么类别”的信息又隐含了“有多确定”的信息两者混在一起。当模型遇到难样本时它可能不是“预测错了”而是“真的不确定”。Distribution Focal Loss让模型学会说“不知道”DFL的核心思想很直观我们不直接预测一个概率值而是预测一个概率分布。比如原来输出0.7现在输出[0.1, 0.2, 0.4, 0.2, 0.1]表示概率分布在0.7附近。实现细节踩坑记录classDistributionFocalLoss(nn.Module):def__init__(self,bins10,gamma2.0):super().__init__()self.binsbins# 把[0,1]区间分成多少份self.gammagamma# 生成离散的锚点这里注意要均匀分布# 我试过对数间隔效果反而变差self.anchorstorch.linspace(0,1,bins)defforward(self,pred_dist,target_score): pred_dist: [B, bins, H, W] 每个位置预测一个分布 target_score: [B, H, W] 真实标签连续值 # 找到目标值最近的两个锚点# 这里有个坑target_score可能超出[0,1]记得clamp一下target_scoretarget_score.clamp(0,1)# 计算权重idx(target_score*(self.bins-1)).long()weight_righttarget_score*(self.bins-1)-idx.float()weight_left1-weight_right# 提取对应位置的预测概率# 注意维度对齐我在这里debug了半小时pred_leftpred_dist.gather(1,idx.unsqueeze(1)).squeeze(1)pred_rightpred_dist.gather(1,(idx1).clamp(maxself.bins-1).unsqueeze(1)).squeeze(1)# 双线性加权损失loss_left-weight_left*torch.log(pred_left1e-8)loss_right-weight_right*torch.log(pred_right1e-8)# 加上Focal weightingpt_leftpred_left.detach()pt_rightpred_right.detach()focal_weight_left(1-pt_left)**self.gamma focal_weight_right(1-pt_right)**self.gammareturn(focal_weight_left*loss_leftfocal_weight_right*loss_right).mean()实际部署时发现bins数量不是越多越好。我测试过bins5,10,20,50发现bins10在精度和计算量之间取得最好平衡。bins50时训练不稳定容易过拟合到噪声。不确定性怎么用预测出分布后我们得到两个宝贵信息期望值作为最终得分和方差作为不确定性度量。# 计算期望和方差expectation(pred_dist*self.anchors.view(1,-1,1,1)).sum(dim1)variance(pred_dist*(self.anchors.view(1,-1,1,1)-expectation.unsqueeze(1))**2).sum(dim1)# 应用场景1动态阈值base_thresh0.5dynamic_threshbase_thresh-0.3*variance# 不确定性高时降低阈值keep_maskexpectationdynamic_thresh# 应用场景2不确定性加权NMSdefuncertainty_aware_nms(boxes,scores,variances,iou_thresh):# 不确定性大的框权重降低weightstorch.exp(-2*variances)# 简单加权函数weighted_scoresscores*weights# 用加权分数做NMSreturntraditional_nms(boxes,weighted_scores,iou_thresh)在交通场景测试中这种动态阈值策略将黄昏时段的mAP提升了3.2%。特别是对于远处小车辆模型现在更倾向于“有保留地检测”而不是“武断地忽略”。训练技巧血泪教训初始化很重要pred_dist的最后一层初始化用很小的正数我习惯用nn.init.constant_(conv.bias, 0.01)。全零初始化会导致训练初期梯度爆炸。标签平滑的配合使用硬标签0/1不适合DFL。我用的平滑策略# 原来positive1.0, negative0.0# 现在positive0.95, negative0.05# 极端困难样本甚至可以给0.8让模型知道这是模糊情况损失权重需要调DFL损失通常比分类损失小一个数量级。我的经验是分类损失权重1.0DFL损失权重0.1开始根据验证集调整。推理时别忘转换训练时用分布推理时要取期望。这个步骤容易忘我吃过亏——直接取分布最大值结果指标掉点。部署考量边缘设备上DFL增加的计算量主要在于额外卷积层输出bins个通道bins10时增加10倍通道期望计算一次加权求和实测在Jetson Nano上bins10导致推理速度下降约15%。我的优化策略# 训练时用完整分布部署时用简化计算# 技巧用两个高斯分量近似整个分布ifis_training:outputfull_distribution_head(x)else:# 部署时只输出均值和方差meanconv_mean(x)vartorch.sigmoid(conv_var(x))*0.5# 限制方差范围# 需要时再重建近似分布个人建议先在小数据集上验证别直接上COCO。我在VisDrone上先跑通确认有效后再迁移到主数据集节省大量时间。可视化分布变化训练过程中定期可视化难样本的预测分布。我见过三种典型模式尖锐单峰模型很确定平坦分布模型很困惑双峰分布模型在两个答案间犹豫第三种情况最有意思往往对应真实世界的模糊样本。结合具体业务医疗影像中不确定性高的样本应该交给医生复核自动驾驶中不确定性高的检测应该触发保守策略如减速。单纯追求mAP提升可能不是最终目标。别神话DFL它解决的是“认知不确定性”对于数据本身的噪声偶然不确定性还需要其他手段。我现在的方案是DFLMC Dropout一个管认知一个管偶然。最后说句实话改进损失函数就像调参——没有银弹。DFL在我这个项目里有效可能是因为数据集中包含大量“边界情况”。如果你的数据都很清晰传统Focal Loss可能更简单高效。多实验多分析找到适合你问题的那个解法这才是工程师的价值所在。

更多文章