☰
正负样本不均衡困境:AUC评估与Focal Loss实战指南
2026/10/19 8:27:18 网站建设 项目流程

正负样本不均衡是分类模型迭代中最容易被低估的问题,我最早是在一个风控类的模拟项目里被现实狠狠教育了一顿。当时模型准确率很高,但用AUC一测才发现排序质量很差,后来换了Focal Loss做训练损失,才把少数类召回拉上来。所以今天就把我踩过的坑和最终固定下来的做法拆开聊聊,也希望你能少绕几段弯路。

1. 正负样本不均衡:它到底在制造什么麻烦

1.1 先看一个跑偏的模型长什么样

做分类任务的人,大概率都见过这种场面:训练数据里负样本有90万条,正样本只有900条,比例1000比1。模型跑完之后测试集accuracy有98%,效果看起来好得吓人。但把它放到真实业务里,它很快就原形毕露——遇到真正需要抓出来的那1%对象,几乎一个都抓不到。

这种“准确率虚高、实际一无是处”的情况,我最早在一个风控类的模拟项目里踩过。当时的数据分布是:绝大多数样本是正常用户,欺诈用户只占极少数。模型训练时把所有样本都预测成正常类,就能把loss压得很低,因为错误代价主要集中在少数类错分上,多数类错分几乎不产生惩罚。于是模型学会了“躺平”:不区分样本,直接输出多数类。这类问题就是典型的不均衡问题。所以当我看到有人用accuracy去衡量这种模型时,第一反应是:先看看正负样本比例再说。

核心问题是模型不是学不到少数类特征,而是少数类的“存在感”太弱。在梯度更新时,多数类样本提供的梯度会反复占据主导,正类样本被淹没在几十万个反例里,即使模型偶尔能学到一些正类特征,也会被后续的梯度冲掉。这不只是数据工程问题,更是模型训练机制的问题。

1.2 不均衡从哪来,为什么不能直接忽略

正负样本不均衡在真实世界里是常态,不均衡往往来自业务本身的天然稀疏性。比如点击率预估,用户点击的商品占比通常不到百分之一;比如反欺诈,真实欺诈交易可能只有万分之几;比如医疗筛查,患病样本和健康人群的比例可能达到1比10000。所以不均衡不是数据采集出了问题,而是业务本身就对“某个事件”存在天然的偏向。

如果把这种数据直接喂给常规的分类模型,会出现四个连锁问题:

  • 模型预测偏向多数类,少数类召回率极低。这不是模型笨,而是它在优化全局损失,少数类贡献太小。
  • accuracy不可信,甚至具有误导性。98%的准确率,可能只是把所有样本都判成了负类。
  • 概率输出校准差。模型对少数类样本给出的概率普遍偏低,导致排序和阈值切分都受影响。
  • 少数类的梯度信号太弱,训练后期容易过拟合到噪声上,表现为验证集评估波动大。

很多人最开始觉得“把负样本随机砍一砍”就能解决,但这种方法需要谨慎使用。欠采样会让模型失去大量有效信息,尤其是当多数类本身也带有复杂模式时,方差会变大。过采样则要小心过拟合——正样本被反复复制,模型容易把少数类样本背下来。真正靠谱的做法,是数据层面、模型层面、评估层面一起动手,而不是只靠某一个技巧。

1.3 模型内部发生了什么:梯度被多数类带跑

要理解为什么正负样本不均衡会让模型“学歪”,可以回到损失函数。以二分类交叉熵为例,每个样本的损失是:

L = -y * log(p) - (1 - y) * log(1 - p)

其中p是模型预测为正类的概率。如果负样本有1000条、正样本只有10条,那么累计损失里,负样本贡献了绝大部分梯度。模型发现,只要把p压低,就能让负样本的log(1-p)变小,整体loss下降得最快。至于那10条正样本,它们对总loss的贡献占比太小,“不值得”模型去调整。

于是,模型在训练中会逐渐学习到一个策略:尽量输出很小的正类概率。这种策略在全局loss上确实是“最优”的,但对我们关心的少数类预测毫无帮助。这也是为什么单纯从loss上盯着看,很难发现模型出了问题——因为loss一直在降,看起来一切正常,直到评估阶段才翻车。理解了这层机制,就容易明白为什么需要在评估方式和损失函数上做文章了。

2. 用AUC评估模型:为什么它比准确率靠谱

2.1 ROC曲线和AUC到底是啥

AUC的全称是Area Under the ROC Curve,也就是ROC曲线下方的面积。ROC曲线由两个基础指标构成:真正例率TPR,也叫召回率;假正例率FPR。TPR表示真实正样本里有多少被找出来了;FPR表示真实负样本里有多少被误判成了正样本。把不同阈值下这两个率画到二维平面,横轴是FPR,纵轴是TPR,连成的曲线就是ROC。

随机猜测的模型对应的是从左下角到右上角的对角线,AUC大概是0.5。如果一个模型AUC是0.9,意思是:随机从正样本里抽一个,再从负样本里抽一个,模型给正样本打高分的概率是90%。AUC衡量的是模型把正样本排在负样本前面的能力,而不是预测绝对概率值的准确性。这是理解AUC最关键的切入点。

AUC还有一个特性:它只关心排序,不关心概率的具体数值。模型把所有预测概率乘以10倍、加上一个常数,只要排序顺序不变,AUC就不变。所以AUC对“概率校准”这件事是无感的。它非常适合用来做模型比较,因为不同训练阶段、不同模型的输出范围可能不一致,用accuracy无法公平对比,但AUC可以。

2.2 AUC为什么对不均衡相对鲁棒

在正负样本比例悬殊的情况下,accuracy会因为“全都预测为多数类”而虚高,ROC曲线却不会。因为TPR只关注正样本中被覆盖的比例,FPR关注负样本中被误报的比例,这两个指标都是按类别独立计算的,不直接受正负样本绝对数量差异的影响。换句话说,ROC空间里,正负样本比例不会改变曲线的形状逻辑。

举个例子,假设测试集有100个正样本、1000个负样本。一个模型把100个正样本全部排在最前面,同时把前900个负样本排在最后面,那么它的TPR在某个阈值下是100%,FPR是0%,AUC接近1。如果另一个模型只会全输出负类,它的TPR在阈值很低时是0%,在阈值很高时也是0%,AUC只有0.5。所以AUC能从“排序质量”这个角度把两类模型区分开,而accuracy对这两种模型的判断很可能是:第一个模型100%准确率,第二个模型90.9%准确率,差距会被缩小。

实际操作中,AUC还有一个好处:评估时不需要提前设定阈值。对于不均衡业务,阈值选多少本身就是一个很麻烦的问题。AUC把阈值选择的环节后置了,让我们可以先评估“模型有没有把正样本排到前面”的能力。这也是它在不均衡场景里能成为评估首选的核心原因。

2.3 只看AUC也有坑:阈值、业务代价

虽然AUC对不均衡相对友好,但只盯AUC也会踩坑。AUC是排序指标,它不关心模型输出的概率是否绝对值可靠。同一个AUC 0.9,可能对应的是一组概率全在0.2到0.3之间的输出,也可能对应一组0.01到0.99的输出。前者如果直接用一个默认的0.5阈值去切,会发现根本切不出正样本,召回率极低;后者则一切就灵。

所以在不均衡场景里,我一般会同时看三样东西:AUC、PR曲线、业务代价矩阵。PR曲线以精确率Precision为纵轴、召回率Recall为横轴,它更关注少数类预测出来的质量。AUC在负样本特别多时会显得“数值很高”,因为FPR分母很大,即使误报了一堆正类,FPR也可能不高;PR曲线则没有这个问题。因此当负样本远多于正样本时,PR曲线往往更能反映少数类的真实表现。

另外一个常被忽略的坑是:AUC对样本分布敏感。测试集如果和训练集分布不一致,比如正样本采样方式不同,AUC会失真。所以用AUC做模型评估,最好在固定分布、固定采样策略下进行横向比较,不要动不动拿不同时间窗口的数据去比。从这些细节能看出来,AUC是评估工具,不是万能指标,模型选择要综合多个维度来看。

3. Focal Loss:让模型把注意力放到“难啃的硬骨头”上

3.1 先从交叉熵说起:为什么它容易躺平

前面已经提到,二分类交叉熵的损失对每个样本是L = -y*log(p) - (1-y)*log(1-p)。在不均衡场景下,多数类样本数量多,它们每个样本即使已经被分类得很“自信”,损失很小,但数量堆起来之后,对梯度的贡献依然很大。多数类的“简单样本”成了梯度大户,少数类的“困难样本”反而被淹没。这会造成一个结果:模型一直在优化那些已经学得很好的负类样本,而对正类样本的优化动力不足。

Focal Loss的提出者最早在目标检测任务里遇到类似问题:一张图片中的背景框数量巨大,而目标框少,训练时背景框产生的梯度主导了模型更新。于是他们设计了一个新的损失函数,核心思想是:让模型少关注那些“已经被分类得很好的样本”,多关注那些“还不确定、分错的样本”。这个思想放到正负样本不均衡的一般分类问题里,同样适用。

Focal Loss本质上是对交叉熵的一种重构。它给每个样本的损失加了一个调制系数,让简单样本的权重下降,困难样本的权重相对上升。这样即使多数类样本数量很多,如果它们大多是简单样本,整体贡献也会被压下去。少数类里那些难以区分的样本,会获得更多的发言权。

3.2 Focal Loss的公式拆解:α和γ分别管什么

Focal Loss最常见的定义是:

FL = -α * (1 - p)^γ * log(p)(对正样本,y=1)

对负样本,则通常写成:

FL = -(1-α) * p^γ * log(1-p)

为了简洁,也有人写成统一形式:

FL = -α * (1 - p_t)^γ * log(p_t)

其中p_t是模型对正确类别的预测概率。如果样本真实类别是1,p_t就是p;如果真实类别是0,p_t就是1-p。这样一个公式就能覆盖正负样本两种情况。

这里有两个核心超参数。第一个是α,负责处理类别不均衡的数量比例,它相当于给少数类样本一个额外的权重。比如正负样本比例1比1000,α可以设置成0.75甚至0.9,意思是模型更重视正类。第二个是γ,负责处理难易样本的权重分配,它的作用更精妙。当γ=0时,Focal Loss退化成带类别权重的交叉熵;当γ增大时,(1-p_t)^γ会让那些p_t接近1的简单样本权重迅速衰减,而p_t接近0的困难样本权重基本不衰减。

我举一个直观的例子。假设γ=2,一个正样本的真实标签是1,模型给它预测概率p=0.9,那调制系数是(1-0.9)^2=0.01,这个样本对loss的贡献被压到原来的1%。而另一个正样本模型预测概率p=0.6,调制系数是(1-0.6)^2=0.16,权重相对保留较多。模型自然会把优化重心放到p=0.6这类难分样本上。这个机制对正负样本不均衡场景很有价值,因为很多负样本其实非常容易分类,它们的loss被压缩后,就不会反复霸占梯度了。

3.3 PyTorch实现与训练接入

这里给出一个比较实用的PyTorch实现,支持二分类和多分类,方便直接改造成自己的损失函数模块。为了简洁,我假设输入是logits,并且类别标签从0开始。

import torch import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alpha=None, gamma=2.0, reduction='mean'): super().__init__() self.gamma = gamma self.reduction = reduction # alpha可以传入一个标量,也可以传入按类别权重数组 self.alpha = alpha def forward(self, logits, targets): ce_loss = F.cross_entropy(logits, targets, reduction='none') # 得到每个样本被预测为正确类别的概率 p_t pt = torch.exp(-ce_loss) # 等价于 softmax(logits) 在目标类上的概率 # 调制系数 focal_weight = (1 - pt) ** self.gamma if self.alpha is not None: # alpha如果是一个数组,需要按目标类别索引 if isinstance(self.alpha, (list, tuple, torch.Tensor)): alpha_t = torch.tensor(self.alpha, device=logits.device)[targets] else: alpha_t = self.alpha 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

这段代码里pt的计算方式很巧妙:交叉熵在不reduce时,得到的是每个样本的CE loss,而exp(-CE)正好就是该样本被模型分配到正确类别的概率p_t。因此不需要单独拿softmax再gather,既简洁又不容易出错。很多人在尝试Focal Loss时踩过“pt算错”的坑,用这种写法可以规避。

在训练时,把模型输出和标签喂给FocalLoss即可。建议先把原来的CrossEntropyLoss换成FocalLoss,α设成训练集负样本占比的倒数再归一化,γ可以先设2。训练过程记得每个epoch在验证集上算AUC,而不是只看loss。这里给出一个训练循环的骨架:

model = YourModel() criterion = FocalLoss(alpha=0.75, gamma=2.0) optimizer = torch.optim.Adam(model.parameters(), lr=1e-4) for epoch in range(args.epochs): model.train() for x, y in train_loader: optimizer.zero_grad() logits = model(x) loss = criterion(logits, y) loss.backward() optimizer.step() model.eval() valid_scores = [] valid_labels = [] with torch.no_grad(): for x, y in valid_loader: logits = model(x) prob = torch.softmax(logits, dim=1)[:, 1] valid_scores.append(prob.cpu()) valid_labels.append(y.cpu()) # 拼接后调用 sklearn.metrics.roc_auc_score 计算AUC

这里面有个细节:α可以按训练集里负样本占比设置,比如负样本占比0.99时,α取0.75到0.9左右,不一定非要严格等于1减正样本占比。实际操作中,我习惯先固定γ=2,把α从0.5往0.9方向扫,再看验证AUC和PR曲线变化,而不是一次调两个参数。

3.4 多分类和业务场景适配

Focal Loss不仅能用在二分类。多分类场景下,只需在代码里把alpha传成每个类别的权重数组就可以。例如在某个多分类故障识别项目里,类别0有10万条样本,类别1有1万条,类别2只有1000条,那么alpha可以设置成0.1、0.3、0.6这类递增的权重。要注意,alpha要跟着数据分布走,类别越少权重越大,但不能简单让少数类权重高到“扭曲”模型,否则会放大噪声样本的影响。

业务场景适配时要考虑“难易”的定义。在Focal Loss里,“难”的定义是模型预测概率远离正确类别。这个定义在绝大多数分类任务里是合理的。但有些场景里,我们想让模型特别关注特定类别,比如医疗筛查里不想漏掉患病样本,此时除了调高α,还可以辅以阈值移动、定制评估指标,把模型输出概率当作排序值来用,而不是直接当作最终决策。Focal Loss能解决“注意力分配”问题,但决定“哪些样本不能放过”这种业务逻辑,还要靠数据标注和评估规则去约束。

4. 实战调参与避坑:从数据到训练的一整套做法

4.1 在动Focal Loss之前,先试着解决数据层面问题

Focal Loss不是银弹,它再厉害也架不住数据本身太烂。我在实际项目里有一个固定的处理顺序:第一步先看能不能通过数据清洗、特征工程和采样策略把不均衡问题缓解一部分,第二步再引入Focal Loss或样本权重。如果数据里正样本只有几十条,任何损失函数都很难救回来。

数据层面常见的手段有几种。欠采样会在多数类里随机抽样,让正负样本比例接近;优点是训练速度快、内存占用低,缺点是可能丢掉多数类的关键信息。过采样则复制少数类样本,比如SMOTE会在少数类样本之间线性插值生成新样本;它能平衡比例,但要小心生成样本和真实样本混合后带来的过拟合风险。更稳健一点的做法,是先用集成学习方式,把多数类切成多个子集,每个子集和全部少数类训练一个模型,再用bagging把多个模型融合。这种方式对非常不平衡的数据往往比单模型更稳。

我见过一个模拟项目,正负样本比例1比5000,一开始直接上Focal Loss,AUC只有0.72。后来先用聚类对多数类进行下采样,保留有代表性的负样本,再配合Focal Loss,AUC提升到0.85。这说明数据层面的处理是地基,Focal Loss是上层建筑,两者配合而不是互相替代,才有稳定的提升。

4.2 Focal Loss 参数怎么调

调参是多数人最容易纠结的部分。我的建议是不要同时调两个超参,会分不清是α还是γ贡献了提升。先把γ固定在2,接下来只扫α,从0.5到0.9,步长0.1,每个值在固定验证集上记录AUC、精确率、召回率。等α确认了,再固定α,扫γ,γ范围可以从1.0到3.0。如果γ太大,比如超过3,会发现训练loss非常难降,甚至梯度震荡,这是因为困难样本的权重被抬得太高,模型一开始就被少数极端样本带着走。

另外,Focal Loss对学习率更敏感。交叉熵训练时学习率1e-3可能没问题,换成Focal Loss后同样学习率可能让训练不稳定。因为Focal Loss整体数值量级比交叉熵小,梯度更新幅度也会变化。我通常会先把学习率降到原来的二分之一或三分之一,然后看loss曲线的下降速度。如果前几个batch loss不降反升,大概率是学习率太大或者γ太大,先降学习率再降γ。

还有一个很容易忽略的点:类别权重α不要和Focal Loss叠加得太猛。有些人先给每个样本乘一个class_weight,又用Focal Loss,还把γ设得很大,最后训练集loss居高不下。简单样本被压太狠,模型反而学不到稳定的类别边界。正确做法是,要么用class weight,要么用Focal Loss里的α,不要同时上两套强权重。真需要叠,也要把数值调温和一点。

4.3 常见问题和排查技巧实录

这里整理了一份我在实际调Focal Loss时遇到的典型问题速查表,可以对着排查。

现象可能原因操作建议
训练loss不降γ太大、学习率太高、模型本身欠拟合降低γ到1或2,降低学习率,确认特征和数据是否有效
验证AUC低于0.5标签错乱、训练验证数据泄漏、正负样本定义混乱先检查标签对齐,重新切分数据,确保验证集分布与线上一致
预测概率普遍接近0模型输出不是概率校准值,Focal Loss让数值分布偏移不要直接拿概率当置信度,用温度缩放或Platt缩放校准后再定阈值
正样本比例极低,AUC仍上不去数据量太少,噪声大,Focal Loss无法拯救信息量缺失回到数据层面,尝试迁移学习、半监督,或补充标注数据
加了alpha后训练震荡明显alpha过大,模型被少数极端样本带偏降低alpha,配合早停和梯度裁剪

我碰到过一个比较隐蔽的问题:训练时用了Focal Loss验证AUC很高,但线上效果很差。排查后发现,验证集里的正样本被重复采样过,导致验证集分布已经失真。所以验证AUC再高,也不能代表真实场景。解决办法是单独保留一份“纯净”的验证集,正负样本比例和线上保持一致,并且不做任何过采样。只有这样的验证AUC才有参考价值。

还有一个经验:当类别不均衡很严重时,除了看AUC,要额外关注PR曲线下的面积。有时候AUC提升0.02,但PR-AUC提升0.1,说明模型对少数类实际更有用了。有时候AUC提升了,PR-AUC反而下降,说明模型只是把容易的负样本排得更靠后,但对少数类的精准区分没有帮助。所以我在项目复盘里,会同时记录AUC和PR-AUC两个数字,避免被单一指标误导。

4.4 从离线到在线:阈值选择与效果验收

评估和训练都搞定之后,还差最后一步:怎么把模型输出变成业务决策。这时候需要选定阈值,或者设计一套线上打分规则。对不均衡问题来说,阈值不能默认用0.5,因为模型输出的概率分布通常会整体偏移。比如用Focal Loss训练出的模型,少数类概率可能普遍低于0.5,但排序依然很好。这时可以去验证集上遍历阈值,找到精确率和召回率平衡点,或者根据业务代价函数计算期望收益最大化对应的阈值。

在线效果验收也不能只看指标是不是变好,还要看坏样本覆盖收益、误伤成本这些业务指标。某个风控项目里,离线AUC提升了0.03,但线上坏账率没有下降,最后发现是阈值选错了:旧模型阈值0.5对应的召回率很低,新模型适合阈值0.3,但线上还沿用0.5,把提升卡在门外。调整阈值之后,效果才真正体现出来。这种“最后一公里”的问题很容易被忽视,但它往往是项目成败的关键。

我个人在做这类项目时,习惯把AUC、PR-AUC、阈值、成本收益四个维度一起固化到模型评测脚本里。每次迭代模型,先跑这四个指标的对比,再决定是否上线。这个习惯帮我避免了很多次“指标好看但业务不买账”的尴尬。

5. 进阶补充:Focal Loss之外,不均衡问题还能怎么打

5.1 常见损失函数的选型对比

Focal Loss确实好用,但也不是唯一选择。在具体项目里,我还用过带权重交叉熵、Dice Loss以及梯度调和损失。它们的适用场景不太一样,先列一个表方便大家选型。

损失函数核心机制适用场景
普通交叉熵每个样本权重一致数据相对均衡,或者类别不均衡但特征区分度很高
带类别权重的交叉熵按类别比例放大少数类loss类别不均衡明确,但难易样本差异不大
Focal Loss调制难易样本权重,兼顾类别权重类别极不均衡,且存在大量容易分类的多数类样本
Dice Loss / Focal Dice按区域重叠度计算损失分割任务、小目标检测、像素级类别不均衡
梯度调和损失按梯度密度给样本加权样本噪声较大,希望通过梯度分布自动调节权重

选型时我看两步:第一是少数类是不是“容易被学”,第二是多数类是不是“噪音很大”。如果多数类本身很干净,用带权重的交叉熵就够;如果多数类里有大量和少数类边界模糊的样本,Focal Loss会更合适;如果任务本身就是像素级预测,比如图像分割,Dice类损失往往更贴近评估指标。

5.2 组合策略的思路

Focal Loss和其他策略组合时,顺序很重要。我比较推荐“先修数据,再调损失,最后定阈值”的路线。数据层面可以先做聚类欠采样,把多数类里和少数类高度相似的簇保留下来,减少无意义样本;模型层面再引入Focal Loss,让难分样本获得更多权重;最后在验证集上做阈值搜索,找到业务收益最高的切分点。

组合使用时要格外小心过拟合。比如先对少数类做过采样,又用大γ的Focal Loss,模型会拼命去拟合被重复生成的少数类样本,训练集指标可以做到很好看,验证集AUC反而会掉。我通常会在组合前先观察正样本之间的相似度,如果相似度很高,就少用SMOTE,多依赖Focal Loss本身;如果正样本来源多样,则可以适当用合成样本增强特征空间覆盖。

还有一个思路是集成。把训练数据切成多个子集,每个子集使用不同的多数类采样比例,训练多个模型,最后对预测概率做平均或加权投票。这个策略和Focal Loss可以兼容:每个基模型都使用Focal Loss,最终集成后的概率排序往往比单模型稳定。缺点是训练成本成倍增加,但考虑到不均衡项目的试错成本,多花点算力通常是值得的。

5.3 一套可复用的实验评估模板

最后分享一套我固定用的实验评估模板,你可以直接搬到自己的项目里。每次实验都要记录:数据切分方式、正负样本比例、特征版本、模型结构、损失函数类型、α和γ的取值、学习率、验证AUC、PR-AUC、最优阈值、精确率、召回率、以及业务收益估算。没有这些记录,后面调参和复盘都会变成玄学。

在代码层面,train和valid切分时保持stratified策略,确保验证集里正样本比例和真实场景接近;训练过程中至少保存三类信息:每个epoch的训练loss、验证AUC、验证PR-AUC。不要只保存最后一个epoch的结果,否则没法回溯最佳模型。模型选择时,我一般不看最后一轮权重,而是选择验证AUC最高的那一个epoch,再做一次早停。

这个模板看起来简单,但真能坚持执行的团队不多。很多时候大家只盯着一个acc或AUC看,忽略了阈值和代价矩阵,导致模型迭代评判标准前后不一致。把模板固定下来之后,实验对比的效率会提升一大截,线上出问题也能快速定位是数据、模型还是阈值的问题。

我在实际项目中最大的感受是,AUC和Focal Loss不是万能药,但它们确实解决了我最头疼的两件事:用对尺子看清模型,以及把训练火力对准真正难分的少数类。如果只记住一个原则,那就是任何时候都先想清楚要评估什么,再去谈用什么损失函数。顺序对了,结果通常不会太差。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询