今日目标 :掌握Focal Loss解决类别不平衡的原理,理解Circle Loss的统一视角,学会在实际项目中选对损失函数
预计阅读 :12分钟 | 动手操作:40分钟
一、为什么标准损失函数不够用?
标准交叉熵损失 (Cross Entropy):
Loss = -log(p_t)
p_t: 模型对正确类别的预测概率
问题场景:
目标检测:一张图有10000个候选框,只有3个有物体
正样本: 3个,负样本: 9997个
负样本占总损失的99.97%!
模型学会了"全部预测为背景" → 损失也很低
但什么也检测不到!
标准CE的三个致命问题:
1. 类别不平衡:正样本被负样本淹没
2. 难易样本不平衡:简单样本占主导
3. 没有区分度:难样本和简单样本被同等对待
解决方案:
Focal Loss: 同时解决类别不平衡 + 难易样本不平衡
Circle Loss: 统一度量学习损失,更具区分度
python
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
torch.manual_seed(42)
二、核心知识点
2.1 Focal Loss:让难样本说话
python
"""
Focal Loss (2017)
论文:https://arxiv.org/abs/1708.02002
核心思想:降低简单样本的权重,让模型关注难样本
标准CE: Loss = -log(p_t)
Focal Loss: Loss = -α_t × (1 - p_t)^γ × log(p_t)
↑ ↑
类别权重 难易调制因子
参数解释:
α_t: 类别权重 (解决类别不平衡)
正样本: α
负样本: 1-α (α=0.25表示正样本权重0.25,负样本0.75)
γ: 聚焦参数 (解决难易样本不平衡)
γ=0: 退化为标准CE
γ=2: 推荐值
(1 - p_t)^γ 的效果:
简单样本 p_t=0.9 → (1-0.9)^2 = 0.01 → 损失缩小100倍!
难样本 p_t=0.1 → (1-0.1)^2 = 0.81 → 损失几乎不变
简单样本的损失被大幅抑制
难样本的损失几乎不变
模型被迫关注难样本!
直观理解:
标准CE:所有人说话一样大声
Focal Loss:简单样本被"静音",难样本的声音被放大
"""
class FocalLoss(nn.Module):
"""
Focal Loss完整实现
两种形式:
1. 多分类:传入logits (未经过softmax)
2. 二分类:传入概率 (经过sigmoid)
"""
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: (N, C) logits (多分类)
targets: (N,) 类别索引
"""
# 计算softmax概率
p = F.softmax(inputs, dim=1) # (N, C)
# 取正确类别的概率
# gather: 从每行取targets指定的列
p_t = p.gather(1, targets.unsqueeze(1)).squeeze(1) # (N,)
# 计算Focal Loss
# 标准CE: -log(p_t)
# Focal: -α_t * (1-p_t)^γ * log(p_t)
ce_loss = -torch.log(p_t + 1e-8)
# 类别权重 α_t
alpha_t = self.alpha * targets.float() + (1 - self.alpha) * (1 - targets.float())
# 简化:多分类下通常用统一的α
# 调制因子 (1-p_t)^γ
focal_weight = (1 - p_t) ** self.gamma
# 最终损失
loss = focal_weight * ce_loss
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
return loss
class FocalLossBinary(nn.Module):
"""
二分类Focal Loss (用于目标检测、分割等)
"""
def __init__(self, alpha=0.25, gamma=2.0):
super().__init__()
self.alpha = alpha
self.gamma = gamma
def forward(self, pred, target):
"""
pred: (N,) 预测概率 (经过sigmoid)
target: (N,) 0或1
"""
# BCELoss -log(p_t)
# p_t = p if target=1 else 1-p
p_t = pred * target + (1 - pred) * (1 - target)
# 类别权重
alpha_t = self.alpha * target + (1 - self.alpha) * (1 - target)
# 调制因子
focal_weight = alpha_t * (1 - p_t) ** self.gamma
return -(focal_weight * torch.log(p_t + 1e-8)).mean()
def focal_loss_demo():
"""演示Focal Loss不同γ的效果"""
p_vals = torch.tensor([0.1, 0.3, 0.5, 0.7, 0.9, 0.99]) # 从难到易
print("Focal Loss不同γ的效果:")
print(f"{'p_t':<8s} {'CE':<8s} {'γ=0.5':<8s} {'γ=1.0':<8s} {'γ=2.0':<8s} {'γ=5.0':<8s}")
print('-' * 48)
for p in p_vals:
ce = -np.log(p.item())
losses = [ce * (1-p.item())**gamma for gamma in [0.5, 1.0, 2.0, 5.0]]
print(f"{p.item():<8.2f} {ce:<8.4f} {losses[0]:<8.4f} {losses[1]:<8.4f} {losses[2]:<8.4f} {losses[3]:<8.4f}")
print("\n观察:")
print(" p_t=0.9 (简单样本): CE=0.105, Focal(γ=2)=0.001 → 损失缩小100倍!")
print(" p_t=0.1 (困难样本): CE=2.302, Focal(γ=2)=1.865 → 损失几乎不变")
focal_loss_demo()
2.2 为什么(1-p_t)^γ能抑制简单样本?
python
"""
数学直观:
1. 简单样本 p_t → 1
(1-p_t) → 0
(1-p_t)^γ → 0 (γ>0)
损失 → 0
简单样本几乎不贡献梯度
2. 困难样本 p_t → 0
(1-p_t) → 1
(1-p_t)^γ → 1
损失 ≈ CE
困难样本正常贡献梯度
3. 中等样本 p_t = 0.5
(1-p_t) = 0.5
(1-p_t)^2 = 0.25
损失 = 0.25 × CE
中等样本贡献部分梯度
这就是"聚焦"的含义:
模型自动聚焦于困难样本
不被大量简单样本带偏
"""
def focal_intuition():
"""Focal Loss的直观理解"""
print("Focal Loss的直觉:")
print(" 想象一个班级考试:")
print(" 标准CE: 每个学生的问题同等重要")
print(" Focal Loss: 只关注成绩差的学生的问题")
print(" 90分的学生(简单样本): 问题不重要,几乎不关注")
print(" 50分的学生(中等样本): 关注一些")
print(" 10分的学生(困难样本): 重点关注!")
print()
print(" 目标检测中的对应:")
print(" 简单样本: 背景区域,模型很确定是背景")
print(" 困难样本: 小物体、遮挡物体、模糊物体")
print(" Focal Loss让模型专注困难样本 → 精度提升!")
focal_intuition()
2.3 Circle Loss:统一度量学习
python
"""
Circle Loss (2020)
论文:https://arxiv.org/abs/2002.10857
度量学习的目标:
让同类样本靠近 (类内紧致)
让异类样本远离 (类间分离)
传统方法 (Triplet Loss):
需要三元组 (anchor, positive, negative)
一个三元组只约束一对关系
采样策略很重要
Circle Loss的改进:
统一视角:所有类内相似度应该 > 所有类间相似度
不是单个三元组,而是全局优化
每个相似度对有自己的权重
Circle Loss的核心公式:
L = log(1 + Σ exp(γ × α_n × (s_n - Δ_n)) × Σ exp(-γ × α_p × (s_p - Δ_p)))
其中:
s_p: 类内相似度 (同类)
s_n: 类间相似度 (异类)
α_p, α_n: 自适应权重
Δ_p, Δ_n: 边界
直观理解:
类内相似度 s_p → 越大越好
类间相似度 s_n → 越小越好
目标:s_p >> s_n (所有类内 > 所有类间)
Circle Loss vs Triplet Loss:
Triplet: 一个三元组约束一对关系
Circle: 全局约束所有关系
Circle Loss收敛更快,效果更好
"""
class CircleLoss(nn.Module):
"""
Circle Loss的简化实现
适用于:人脸识别、ReID、细粒度分类
"""
def __init__(self, scale=32.0, margin=0.25):
super().__init__()
self.scale = scale # γ
self.margin = margin # 边界
def forward(self, features, labels):
"""
features: (N, D) 特征向量 (已归一化)
labels: (N,) 标签
"""
# 计算相似度矩阵
# features已经归一化,点积 = 余弦相似度
sim = features @ features.T # (N, N)
# 类内相似度: 同标签的样本
# 类间相似度: 不同标签的样本
same_label = (labels.unsqueeze(0) == labels.unsqueeze(1)).float()
# 去掉对角线(自己和自己)
same_label = same_label - torch.eye(same_label.shape[0], device=same_label.device)
# 类内相似度
s_p = sim * same_label
# 类间相似度
s_n = sim * (1 - same_label)
# 自适应权重
alpha_p = torch.relu(1 + self.margin - s_p.detach())
alpha_n = torch.relu(s_n.detach() + self.margin)
# Circle Loss
# 简化的统一形式
sp_exp = torch.exp(self.scale * alpha_p * (s_p - (1 - self.margin)))
sn_exp = torch.exp(self.scale * alpha_n * (s_n - self.margin))
loss = torch.log(1 + sn_exp.sum(dim=1) * sp_exp.sum(dim=1))
return loss.mean()
def circle_loss_demo():
print("Circle Loss核心思想:")
print(" 传统Triplet Loss:")
print(" 一个三元组 → 约束一对关系")
print(" 需要精心采样")
print()
print(" Circle Loss:")
print(" 所有类内相似度 > 所有类间相似度")
print(" 全局优化,每个关系有自己的权重")
print(" 收敛更快,区分度更好")
print()
print(" 应用场景:")
print(" 人脸识别、ReID、细粒度分类")
print(" 任何需要"区分不同实例"的任务")
circle_loss_demo()
三、动手实践:损失函数对比实验
python
def loss_comparison():
"""
全面对比各种损失函数
"""
comparison = {
'Cross Entropy': {
'公式': '-log(p_t)',
'解决什么问题': '基础分类',
'类别不平衡': '❌ 不处理',
'难易样本': '❌ 不处理',
'适用场景': '数据均衡的分类',
'端侧推荐': '⭐⭐⭐',
},
'Balanced CE': {
'公式': '-α_t × log(p_t)',
'解决什么问题': '类别不平衡',
'类别不平衡': '✅ 权重',
'难易样本': '❌ 不处理',
'适用场景': '数据不平衡但不需要区分难易',
'端侧推荐': '⭐⭐⭐⭐',
},
'Focal Loss': {
'公式': '-α_t × (1-p_t)^γ × log(p_t)',
'解决什么问题': '类别不平衡 + 难易样本',
'类别不平衡': '✅ α_t',
'难易样本': '✅ (1-p_t)^γ',
'适用场景': '目标检测、不平衡分类',
'端侧推荐': '⭐⭐⭐⭐⭐',
},
'Circle Loss': {
'公式': '全局类内>类间',
'解决什么问题': '度量学习:区分不同实例',
'类别不平衡': '❌',
'难易样本': '✅ 自适应权重',
'适用场景': '人脸识别、ReID',
'端侧推荐': '⭐⭐⭐⭐',
},
'Dice Loss': {
'公式': '1 - 2|X∩Y|/(|X|+|Y|)',
'解决什么问题': '分割的前景-背景不平衡',
'类别不平衡': '✅ 区域级',
'难易样本': '❌',
'适用场景': '语义分割',
'端侧推荐': '⭐⭐⭐⭐',
},
}
print("损失函数全面对比:")
for name, info in comparison.items():
print(f"\n [{name}]")
for k, v in info.items():
print(f" {k}: {v}")
loss_comparison()
四、端侧AI损失函数选择指南
python
def edge_loss_selection():
"""
端侧AI不同任务的损失函数推荐
"""
guide = {
'目标检测 (YOLO)': {
'分类损失': 'Focal Loss (γ=2, α=0.25)',
'框回归损失': 'CIoU Loss',
'理由': '类别不平衡严重,Focal Loss是最佳选择',
},
'语义分割': {
'主损失': 'Dice Loss + BCE',
'辅助损失': 'Focal Loss',
'理由': '前景-背景不平衡,Dice Loss直接优化IoU',
},
'ReID / 人脸识别': {
'主损失': 'Circle Loss 或 ArcFace',
'辅助损失': 'Triplet Loss',
'理由': '需要区分不同实例,度量学习损失效果好',
},
'关键点检测': {
'主损失': 'MSE (热力图回归)',
'辅助损失': 'L1',
'理由': '热力图是回归任务,MSE最直接',
},
'分类 (均衡数据)': {
'主损失': 'Cross Entropy',
'理由': '数据均衡,标准CE就够了',
},
'分类 (不平衡数据)': {
'主损失': 'Focal Loss (γ=1-2)',
'理由': '类别不平衡,Focal Loss自动处理',
},
}
print("端侧AI损失函数选择指南:")
for task, config in guide.items():
print(f"\n [{task}]")
for k, v in config.items():
if k != '理由':
print(f" {k}: {v}")
print(f" {config['理由']}")
edge_loss_selection()
五、常见坑点
坑1:Focal Loss的α和γ调参
python
# Focal Loss有两个超参数:α和γ
# α: 类别权重
# α=0.25 表示正样本权重0.25,负样本0.75
# 为什么不是0.5?因为负样本太多,给负样本更大权重反而不好
# 经验:α=0.25 (论文推荐)
# γ: 聚焦参数
# γ=0: 退化为标准CE
# γ=1: 中等聚焦
# γ=2: 推荐值 (论文最佳)
# γ=5: 过度聚焦,简单样本完全被忽略
# 经验:γ=2.0 (论文推荐)
# 端侧调参建议:
# α=0.25, γ=2.0 (默认,不需要改)
# 如果简单样本太多 → 增大γ到3
# 如果困难样本太多 → 减小γ到1
坑2:Focal Loss和BCE的区别
python
# PyTorch的BCEWithLogitsLoss = sigmoid + BCE
# Focal Loss = sigmoid + BCE + 调制因子
# ❌ 错误:用Focal Loss时输入已经sigmoid过的概率
# loss = FocalLoss()(torch.sigmoid(logits), targets) # 双重sigmoid!
# ✅ 正确:Focal Loss内部自己处理
# 二分类:输入logits或概率(取决于实现)
# 多分类:输入logits
坑3:Circle Loss的特征归一化
python
# Circle Loss要求特征向量归一化
# features = features / ||features|| (L2归一化)
# 归一化后,点积 = 余弦相似度
# ❌ 忘记归一化 → 特征模长影响相似度
# ✅ 先归一化再计算相似度
坑4:损失函数和评估指标的关系
python
# 损失函数 ≠ 评估指标
# 损失函数:训练时优化的目标
# 评估指标:推理时关心的指标
# 例子:
# 分割训练用Dice Loss → 优化IoU相关的目标
# 分割评估用mIoU → 和Dice Loss一致
# 检测训练用Focal Loss → 优化分类
# 检测评估用mAP → 和Focal Loss不完全一致
# 关键:损失函数要和评估指标"对齐"
# 想优化什么指标,就设计什么样的损失函数
六、今日作业
- 手写Focal Loss:实现多分类和二分类Focal Loss,对比不同γ的效果
- 损失函数对比:在同一数据集上对比CE、Balanced CE、Focal Loss的效果
- Circle Loss:理解Circle Loss如何统一度量学习,画出类内/类间关系图
- 打卡 :评论区发你的理解,格式:"Day 39/100 打卡:Focal Loss已掌握!"
今日小结
今天你学会了:
✅ 标准CE的三个问题:类别不平衡/难易样本/无区分度
✅ Focal Loss = -α_t × (1-p_t)^γ × log(p_t)
✅ α_t: 类别权重 (解决类别不平衡)
✅ (1-p_t)^γ: 调制因子 (解决难易样本)
✅ γ=2时,p_t=0.9的损失缩小100倍
✅ Circle Loss: 统一度量学习,全局优化
✅ 所有类内相似度 > 所有类间相似度
✅ 端侧AI六种任务的损失函数推荐
✅ 4个经典坑点
明日预告
Day 40:OHEM与在线难例挖掘
难例挖掘策略、OHEM、在线vs离线挖掘
🔥 关注我,每天解锁一个端侧AI技能!
微信公众号:xxx | 小红书:xxx | CSDN:xxx
评论区打卡,一起坚持100天!
附:小红书图文版
封面标题建议:Focal Loss | 目标检测必备,类别不平衡的终极解法 🔥
P1 --- 封面
标题:Focal Loss
副标题:类别不平衡 / 难易样本 / Circle Loss
P2 --- 标准CE的问题
目标检测:10000个候选框,只有3个有物体
99.97%的损失来自背景!
模型学会"全部预测背景" → 什么也检测不到
三个问题:类别不平衡、难易样本、无区分度
P3 --- Focal Loss公式
Loss = -α_t × (1-p_t)^γ × log(p_t)
α_t: 类别权重
(1-p_t)^γ: 调制因子
p_t=0.9 → 损失缩小100倍
p_t=0.1 → 损失几乎不变
简单样本被"静音",难样本被放大
P4 --- γ参数效果
γ=0: 标准CE
γ=1: 中等聚焦
γ=2: 推荐值
γ=5: 过度聚焦
端侧推荐:γ=2, α=0.25
P5 --- Circle Loss
统一度量学习
所有类内 > 所有类间
全局优化,每个关系有自己的权重
用于人脸识别、ReID
P6 --- 端侧选择指南
检测 → Focal Loss
分割 → Dice Loss + BCE
ReID → Circle Loss
关键点 → MSE
评论区打卡 Day 39/100
标签:#FocalLoss #CircleLoss #目标检测 #损失函数
CSDN发布提示:CSDN版本建议在Focal Loss部分放(1-p_t)^γ随p_t变化的曲线图,在损失函数对比部分放对比表,在端侧选择指南部分放任务-损失函数映射表。