资讯动态

深度学习如何处理不平衡数据?从采样到Focal Loss的实战指南

发布时间:2026/10/17 1:46:20 来源:尧图企业网站定制
1. 不平衡数据为什么让深度学习模型翻车先从一个最常见的场景说起你在做一个信用卡欺诈识别模型正负样本比例大概是1:1000训练完之后准确率高达99.5%你兴冲冲地拿去上线结果发现真正出现欺诈交易时模型几乎全部放行预警率低到没法看。这样的问题我见过太多次了根本原因就一句话——准确率在极度不平衡的数据上没有任何意义因为模型只要把所有样本都预测成多数类就能拿到一个看起来很漂亮的准确率而它实际上什么都没学会。不平衡数据Imbalanced Data在深度学习任务里无处不在除了金融风控还有医疗诊断罕见病患者远少于健康人、工业质检正常产品远多于缺陷品、推荐系统曝光点击率通常不到1%等等。它的核心矛盾是我们真正关心的那一类样本——少数类往往数量太少导致模型在训练过程中被多数类淹没了。这里要先澄清一个底层机制。深度学习的本质是拟合一个从输入到输出的映射函数优化的目标是让损失函数最小化。如果你的损失函数是常规的交叉熵每个样本的贡献是平等的那么少数类样本虽然重要但在数量上占比极小整体损失几乎完全由多数类主导。用个通俗的类比你在教孩子认识动物给他看了1000张猫的图片、只有1张老虎的图片最后考试他只要把所有动物都答成猫正确率也能到99.9%。模型就是这个孩子它找到了一个极其省力的偷懒解而且这个解在传统指标下看起来还挺好。还有另一个容易被忽视的问题——即使你硬着头皮去优化少数类样本的梯度信号也太稀疏了。深度模型依赖大量的梯度更新来调整参数而少数类样本本身就少加上它们的分布往往和多数类高度重叠模型很难从这些少量样本里学到区分的边界最终会把少数类当成噪声忽略掉。这也就解释了为什么很多人在不平衡数据集上直接训练一个深层网络效果还不如传统的逻辑回归——小样本根本撑不起大模型的容量。另外不平衡数据的不平衡程度也不是一个简单比例就能说清的。1:10和1:1000是两个完全不同量级的问题。比例在1:10以内时很多常规方法稍微调一下就能解决一旦到了1:100甚至更高就需要从数据、算法、评价指标多个层面同时动手。我习惯把一个任务的不平衡程度分成三个等级轻度1:10以内、中度1:10到1:100、重度1:100以上不同等级对应的处理策略完全不同下面每一章我都会按这个分级来展开。2. 数据层面的处理采样与增广先解决数量不够的问题处理不平衡数据最直接的想法就是让少数类的样本数量补起来或者让多数类的样本数量降下去。这就是数据层面Data-level的方法思路朴素但在很多场景下非常管用尤其是当不平衡比例不算特别极端时。2.1 随机过采样与随机欠采样最朴素的两板斧随机过采样就是对少数类样本进行有放回地抽取复制直到少数类和多数类的数量接近随机欠采样则是从多数类里随机丢弃一部分样本让两类数量平衡。先说欠采样。它的最大优势是训练数据量变小训练速度明显加快尤其是在ImageNet这种大规模数据集上能省不少算力。但缺点也很致命——丢弃的多数类样本可能携带重要信息导致模型对多数类的判断能力下降。我做过一个用户流失预测的项目多数类样本被随机砍掉了70%结果模型的整体表现确实在少数类流失用户上提升了但对多数类留存用户的误判率暴增AUC反而是下降的。所以随机欠采样更适合多数类样本本身就存在大量冗余的情况比如日志数据、点击流数据里几乎重复的内容很多丢掉一些损失不大。随机过采样则正好相反它不损失信息但容易带来严重的过拟合问题。因为复制出来的样本和原始样本一模一样模型反复看到同一个样本就会把这条样本的噪声也学进去。过度过采样后模型在训练集上表现很好一到验证集就崩。这也是很多初学者容易踩的坑。这里补充一个实操中的小技巧不要盲目地把少数类过采样到和多数类完全等量。我一般会设置一个目标比例比如1:2到1:3而不是1:1。原因在于完全平衡虽然让模型看到了同样多的两类样本但也破坏了真实世界的先验分布。测试时数据分布依然是严重不平衡的模型在完全平衡的训练集上学习到的决策边界可能过于激进产生大量假阳性。把比例控制在1:2或1:3相当于给模型留了一点先验拉扯的空间实际效果往往比完全均衡好。2.2 SMOTE及其变体让少数类生成新样本随机过采样的最大问题是简单复制不产生新信息而SMOTESynthetic Minority Over-sampling Technique解决的就是这个问题。它的思路是在少数类样本之间进行插值——随机选一个少数类样本A找到它的k个近邻从中随机挑一个样本B然后在A和B的连线上随机取一个点当作新生成的少数类样本。SMOTE的优势在于生成了原样本附近的新样本而不是简单复制能缓解一部分过拟合问题。但它也有固有的缺陷它假设少数类样本之间的线性插值仍然属于少数类这在样本重叠严重的场景里不成立。比如在医学影像数据里少数类和多数类的特征空间高度纠缠SMOTE生成的点可能直接落在了多数类的区域内反而引入了噪声。后来出现的Borderline-SMOTE专门处理边界样本只对靠近分类边界的少数类样本进行合成ADASYN则根据样本的学习难度来分配生成数量对难学样本生成更多。这些变体的核心思想都是关注困难样本在实践里可以优先试一下Borderline-SMOTE它对边界重叠型数据的效果通常比标准SMOTE更稳。不过在真正做深度学习时我很少直接用SMOTE处理图片数据——图像的特征空间是高维且非结构化的在像素空间里插值没有任何语义意义。SMOTE更适用于表格型数据特别是特征都是数值型、且经过了标准化之后的情况。如果你在用卷积神经网络处理图像不平衡分类问题数据层面更靠谱的方向是数据增强这一点下面单独说。2.3 数据增强图像和文本领域最有效的数据补全手段对于图像数据深度学习社区早就把数据增强当成了默认操作。随机裁剪、水平翻转、旋转、色彩抖动这些操作能让一张图变成几十张语义相同但外观多样的图对少数类同样适用。核心逻辑在于图像分类任务中这些几何变换不改变物体的语义类别所以增强后样本的标签依然是可靠的。很多人在少数类上不敢做太重度的增强担心引入噪声我的经验恰恰相反——少数类样本在极度不平衡时反而值得用更强的增强策略比如Mixup和CutMix。Mixup把两张图的像素按比例混合标签也按比例混合CutMix则是把一张图的一块区域直接贴到另一张图上标签按面积比例混合。这两类方法不仅能扩充数量还能让模型学到更平滑的决策边界在含量稀少的数据上效果显著。文本领域同样如此回译把中文翻译成英文再翻译回来、同义词替换、随机删除/交换词都是扩充少数类样本的常见做法。但要注意文本增强容易改变语义尤其是涉及否定词、数字、专有名词的场景做了增强后一定要人工抽查一批增强样本确认标签没有漂移。我在一个短文本分类项目里就吃过亏——随机替换同义词把不喜欢增强成了不讨厌语义直接反转导致模型训出一堆错误。2.4 生成模型VAE和GAN用来合成少数类样本的尝试近几年用生成模型GAN、VAE、扩散模型来合成少数类样本也成了一个研究方向。思路是训练一个生成器让它可以产出符合少数类分布的逼真样本。这个方向听起来很美好但我要泼一盆冷水在大多数中小规模的表格数据或非极端复杂图像任务上生成模型的不稳定性带来的麻烦远大于收益。GAN训练本身就存在模式坍塌问题——生成器可能只学会生成少数几类样本多样性不够这在数据增强场景里是致命的因为你要的是覆盖少数类整个分布而不是几簇典型样本。此外用G生成的样本直接加入训练集还会受到hubness problem的影响——生成样本分布偏离真实样本较远时可能会让最近邻算法出现偏差反而降低分类性能。我的判断是当你手里的少数类样本少到连SMOTE都无能为力、且数据形态足够规整比如人脸、数字、特定物体时才值得考虑用预训练好的扩散模型或StyleGAN来做生成。如果只是普通业务表数据与其折腾GAN不如把精力放在特征工程和算法层面的损失函数调整上性价比高得多。3. 算法层面的处理让模型学会看重少数类数据层面的方法相当于改变模型看到的世界算法层面的方法则是改变模型看世界的方式——不增加数据而是让模型在优化时自动更关注少数类。这两者并不互斥实际操作中经常一起用。3.1 加权损失函数给少数类样本更高的话语权加权损失函数Class Weighting是代价敏感学习最简单也最直接的实现。做法就是在计算损失时给不同类别的样本乘上不同的权重系数。比如二分类交叉熵损失函数原本是L -[y·log(p) (1-y)·log(1-p)]引入类别权重后变为L -[w_pos·y·log(p) w_neg·(1-y)·log(1-p)]权重怎么设置最常用的方法是按类别频率的反比来设w_pos N_neg / N_totalw_neg N_pos / N_total如果正样本极其稀缺比如正负比是1:999那么正样本的权重就会非常大。这种方法在PyTorch里实现成本极低只需要在CrossEntropyLoss里设置weight参数import torch import torch.nn as nn # 假设train_loader里class 0有10000个样本class 1有100个样本 n_class0 10000 n_class1 100 # 计算每个类别的权重使用总样本数/类别样本数 weight_class0 (n_class0 n_class1) / n_class0 weight_class1 (n_class0 n_class1) / n_class1 class_weight torch.tensor([weight_class0, weight_class1], dtypetorch.float32) criterion nn.CrossEntropyLoss(weightclass_weight)这样设置的效果是少数类样本的每一个梯度更新都相当于多数类样本的n_class0/n_class1倍。模型必须认真对待每一个少数类样本因为犯错的代价变高了。对于轻度不平衡这个方法几乎是一键解决中度不平衡时也值得先用它跑一个基线。但加权损失也有自己的副作用。当不平衡比例非常极端时比如1:10000正样本的权重会变得极大接近10000这会导致模型在训练初期梯度爆炸收敛非常不稳定。我的做法是对权重做个软化处理w_clipped torch.clamp(class_weight, max10.0)把权重上限限制在10倍以内避免极端权重带来的震荡。当然这个上限需要根据任务调节不同数据集最优值不太一样通常5到20之间值得都试一遍。3.2 Focal Loss专门为极度不平衡设计的损失函数Focal Loss是目标检测领域处理极度前景背景不平衡时的里程碑式工作后来被广泛迁移到各种不平衡分类任务中。它的核心贡献在于不仅给少数类加权重还让模型把注意力集中在难分样本上忽略那些已经分得很对的易分样本。传统的交叉熵里模型哪怕已经把一个样本以0.9的概率预测正确了它依然会贡献一部分可观梯度Focal Loss引入了一个调制因子(1-p_t)^γ当p_t越接近1样本分得越好时这个因子趋近于0对应的梯度贡献也被压缩。公式是FL(p_t) -(1-p_t)^γ·log(p_t)γ一般取2。举一个直观的例子某个样本被模型以0.99的概率正确分类传统交叉熵的损失是-log(0.99)≈0.01005而Focal Loss在γ2时把它压缩到(0.01)^2×0.01005≈0.000001差了五个数量级。相反一个0.5概率的模糊样本传统损失是0.693Focal Loss下是0.25×0.693≈0.173虽然相对也减小了但减小的幅度远小于易分样本所以难分样本的相对贡献被放大了。Focal Loss还有一个可以调节的alpha加权项实际使用上最好和γ一起调。我的经验是α和γ的取值不能拆开单独搜索——α偏大时γ应该调小两者搭配着调才有效果。具体代码实现也很简单Focal Loss可以基于交叉熵自己写也可以在torchvision或各类开源库里找到现成实现。class FocalLoss(nn.Module): def __init__(self, alpha0.25, gamma2.0): super().__init__() self.alpha alpha self.gamma gamma def forward(self, logits, targets): ce_loss nn.functional.cross_entropy(logits, targets, reductionnone) pt torch.exp(-ce_loss) # 预测正确的概率 focal_loss self.alpha * (1 - pt) ** self.gamma * ce_loss return focal_loss.mean()注意这里alpha是针对正样本少数类的权重实际使用中如果有多个少数类需要改成每个类别一个权重向量。另外Focal Loss在batch size比较小时容易震荡建议同步调小学习率并配合warm-up策略使用。3.3 阈值移动训练时不必调整预测时再扳回来还有一个非常实用但经常被忽略的方法阈值移动Threshold Moving。它不去改动训练过程而是训练完模型后在预测阶段重新调整分类阈值。拿到一个训练好的模型输出的是一个概率值。默认情况下概率≥0.5判为正类0.5判为负类。但如果不平衡数据中少数类的先验概率只有千分之一模型输出的概率值在整个分布上都会系统性偏低——哪怕模型对某个样本的预测概率是0.3这个样本可能已经是极其像少数类的了。此时把阈值强行保持0.5就会导致大量少数类被误判为多数类。正确的做法是在验证集上重新寻找一个最优阈值。我没有用默认的0.5而是在验证集上遍历所有可能的阈值选择让F1分数或业务上定义的评估指标最大的那个。实现也不复杂from sklearn.metrics import f1_score import numpy as np # val_scores: 验证集预测概率, val_labels: 验证集真实标签 thresholds np.arange(0.1, 0.9, 0.01) best_f1 0 best_thr 0.5 for thr in thresholds: preds (val_scores thr).astype(int) cur_f1 f1_score(val_labels, preds) if cur_f1 best_f1: best_f1 cur_f1 best_thr thr print(f最优阈值: {best_thr:.2f}, 最佳F1: {best_f1:.4f})阈值移动常常能带来令人惊喜的效果因为它完全不需要重新训练。有研究把阈值调优看作一种免费的午餐我在实际项目中也见过在很多任务上调整阈值后的F1提升幅度甚至超过换损失函数带来的提升。而且阈值移动和前面提到的加权损失、Focal Loss可以叠加使用组合拳效果更稳。3.4 从算法选型看多少样本量够用在极度不平衡且少数类绝对数量也很小时比如只有几十条样本任何深度学习方法的想象空间都有限因为模型容量和样本数量之间有硬约束。这时候与其硬撑着上深度模型不如考虑降级到传统机器学习方法比如梯度提升树XGBoost/LightGBM它们在小型表格数据上的表现往往优于深度模型而且更容易控制过拟合。如果你坚持要用深度学习我的建议是先确认少数类的绝对数量是否超过500——如果低于这个量级先把精力放在数据收集和增强上算法层面的优化收益会越来越小。这并非理论论证而是我在多个项目里反复得到的经验值500条左右的少数类样本配合加权损失和数据增强才能让一个中小型网络学到大致可靠的决策边界低于这个样本量模型的方差大到难以评价。4. 评价指标与验证策略用错指标一切白干很多人处理不平衡数据时第一步就错了——还在用准确率Accuracy衡量模型。准确率在不平衡场景下几乎毫无参考价值我之前在第一部分也说了。所以这一章专门讲清楚应该用什么指标以及怎么正确地做验证。4.1 为什么准确率不可靠以及该换成什么假设10000个样本里有9900个负样本、100个正样本。把模型设计成永远输出负样本,准确率是99%,看起来很高,但实际上这个模型一点用都没有。这就是典型的准确率悖论。在不平衡分类任务里我更常用的指标是这几个Precision精确率预测为正类的样本中有多少是真的正类。它衡量的是模型报出来的正类里含金量有多高在误报代价高的场景比如风控拦截尤其重要。Recall召回率真实的正类样本中有多少被正确找出来了。它衡量的是模型有没有漏掉关键少数类在漏报代价高的场景比如癌症筛查里几乎是最重要的指标。F1-ScorePrecision和Recall的调和平均数在两者间取平衡。调和平均数的特性决定了它会惩罚那个偏低的指标所以F1高意味着P和R都不错。AUC-ROCROC曲线下面积衡量模型在所有阈值下区分正负类的能力对类别不平衡相对不敏感适合用来做模型选型和超参调优。PR曲线Precision-Recall曲线在极度不平衡数据里比ROC曲线更敏感。原因是ROC曲线受负样本数量影响大而PR曲线只关注正类的表现。当正样本极度稀少时ROC可能看起来还挺好但PR曲线会很快暴露问题。用个实际的对比表来说明指标对少数类变化的敏感度适合场景说明Accuracy极低类别均衡数据集均衡时可参考Precision中误报代价高关注预测为正类的可信度Recall高漏报代价高关注少数类是否被漏检F1高综合平衡Precision和Recall的调和均值AUC-ROC中模型选择与调参受负样本数量影响大PR-AUC极高极度不平衡更关注少数类推荐我目前处理不平衡数据任务时训练阶段主要看AUC-ROC做模型选择和调参因为它对不平衡程度不敏感能比较稳定地反映模型区分度最终评估模型是否上线则根据具体业务场景看F1或者Recall。比如风控场景我更看重Precision宁可少拦截也不能误伤太多正常用户而医疗筛查我优先保Recall漏掉一个病人可能酿大祸。4.2 分层采样与K折验证防止验证集看起来很好但没用不平衡数据下的验证集划分比均衡数据要讲究得多。如果简单把数据随机切分很可能出现验证集中正类样本只有个位数的情况指标波动巨大完全无法真实评价模型效果。正确做法是使用分层采样Stratified Sampling——确保切分出来的训练集和验证集都保持与原数据集一致的类别比例。sklearn里stratify参数就是干这个的from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, stratifyy, random_state42 )在K折交叉验证时也是一样直接用StratifiedKFold替代普通KFold。这不仅仅是个细节习惯当少数类样本量很少的时候随机划分甚至可能让某一折验证集中完全没有正类样本指标直接失效。另一个容易踩的坑是过采样操作必须在划分完训练集和验证集之后再进行。很多人习惯对全量数据做过采样再切分训练和验证集这会导致验证集里出现训练集中复制的样本造成非常严重的数据泄漏评估指标虚高。我在好几个项目里见过百思不得其解的训练AUC只有0.75验证AUC却有0.95——多半就是这个原因。正确流程是先把数据划分为训练集和验证集然后只对训练集做过采样/增强处理验证集始终保留真实分布的原始数据。4.3 处理极度不平衡时如何评估少数类的绝对性能比例低于1:1000的重度不平衡场景下只看F1也会遇到问题——正样本总共只有几十条F1波动非常大。我习惯同时记录混淆矩阵的四个值TP、TN、FP、FN尤其是TP和FN直接看模型有没有找到那些少数类样本、漏掉了哪些。这样评价更透明也更方便排查系统性问题。另外对小样本少数类的评估应该采用Bootstrapping自助采样或置信区间估计。做法是对验证集做多次重采样每次计算一次AUC或F1得到指标的分布和置信区间。比如我的一个项目里验证集中只有80个正样本单次F10.72通过1000次bootstrap重采样后发现F1的95%置信区间是[0.61, 0.80]。这个置信区间让0.72和0.75哪个更好这种问题变得有意义起来了——0.03的差距可能完全落在噪声区间里根本说明不了问题。5. 两类常见踩坑数据泄漏与少数类过拟合我见过的处理不平衡数据的失败案例里大部分不是方法选错了而是踩了两个经典的坑数据泄漏和少数类过拟合。这两个坑隐蔽性强很难一眼发现但后果非常严重。5.1 数据泄漏的常见来源除了上一章提到的过采样时序问题数据泄漏还有好几个隐蔽来源。正则化如PCA、标准化时如果先对整个数据集拟合参数均值、方差、PCA主成分再划分训练集验证集的信息就已经混进预处理阶段了。正确做法是把标准化器和特征选择器都fit在训练集上再transform验证集和测试集。在时间序列场景下如果不按时间切分而是随机切分未来的数据就会泄漏到训练集里。我在一个时序预测项目里吃过三次亏最终养成了习惯时间类数据一律按时间点切分训练集只用过去的数据验证集和测试集必须严格在时间维度之后。还有个容易被忽视的来源是重复样本重复用户、重复设备——如果在数据清洗阶段没有去掉同一身份的数据可能同时出现在训练集和验证集里模型实际上见过验证集的答案了。做数据清洗时如果发现用户ID列一定要检查一下是否存在一个用户对应多条样本的情况并做group-level的划分。5.2 少数类过拟合的痕迹和应对手段过拟合的表现是训练集指标极高验证集指标明显回落。在不平衡场景里少数类的过拟合因为样本量太少而格外严重——模型很容易把几十个少数类样本背下来而不是学到泛化规律。这时候增加Dropout、提高L2正则强度、做Early Stopping都是行之有效的手段。另外我需要强调一点过采样方法包括SMOTE在深度学习里天然容易加重过拟合——因为合成样本毕竟和真实样本有差异模型在训练集上过度贴合合成分布验证时面对真实分布会出现落差。所以用SMOTE一类方法时建议合成比例不要太高比如合成后比例不超过多数类的30%并且配合更强的正则化。我在实践中常把过采样比例和正则项强度一起做网格搜索往往能找到比固定参数明显更好的组合。还有一个容易被忽略的细节类别权重设得太高也会诱发过拟合。权重过高意味着模型为了少数类样本不惜牺牲大量多数类样本最终学出一个极其激进、只在少数类样本上过拟合的边界。所以我一般会先调整别的手段数据增强、阈值移动等把类别权重保持在一个相对保守的范围再逐步增加观察。6. 实操路线图从数据集到决策的完整流程讲了这么多方法很多人会问到底该先用哪个我的回答是没有一个放之四海而皆准的组合但有一套相对稳定的流程可以帮你快速找到最有效的方法组合。第1步先做诊断别急着上方法。计算数据集的类别比例、少数类的绝对数量、特征重叠程度。如果少数类样本不到几百条先考虑补充标注数据或强数据增强如果比例低于1:1000做好走极端手段的心理准备。第2步建立一个认真但朴素的基线。用一个不过采样的普通交叉熵损失训练一个简单模型比如浅层网络或者逻辑回归记录AUC和F1。这个基线看起来笨但它是你判断后续所有改进是否有效的参照物——很多改进方法可能还不如这个基线如果没有基线你根本发现不了。第3步按性价比顺序叠加改进。我自己的优先级顺序是先做类别加权改动最小收益往往不小在验证集上做阈值移动免费午餐顺手就做加数据增强或SMOTE提升数据多样性换Focal Loss或改进版损失函数针对重度不平衡结合模型集成或重采样策略进一步调优每个步骤只改一个变量保留AUC和F1的记录最后回头对比。第4步验证阶段做分层采样、bootstrap置信区间用PR-AUC辅助评估。第5步把模型预测结果和业务逻辑绑定起来确定最终决策阈值——这一步必须由业务方来拍板因为阈值本质是一个业务决策比如宁可误报也不能漏报和宁可漏也不能全拦截在阈值选择上的方向完全不同。下面是一个选型参考表我把不同不平衡程度对应的推荐策略整理了一下不平衡程度典型比例推荐策略轻度≤1:10类别加权 阈值移动中度1:10~1:100类别加权/SMOTE 数据增强 阈值移动重度≥1:100Focal Loss 重度数据增强 类别加权 阈值移动 必要时降低模型复杂度这个表格不是教条核心思路是不平衡程度越重需要叠加的手段越多但每一层手段都要验证有效做加法时不要盲目堆。最后分享两个我在实战里用惯了的小技巧。一是所有对训练数据的改动都必须写到代码注释和实验记录里如果不写一周后再跑实验你根本分不清当前结果是在什么数据上得到的。另一个是我几乎所有不平衡数据项目里第一个尝试的强势改动都是先跑一遍阈值移动——它几乎不花时间却经常带来几个点的F1提升这种付出产出比极高的改动值得每次优先尝试。说白了处理不平衡数据没有银弹但也不是玄学。把数据层面的补充、算法层面的加权、评价指标的正确选择三者组合起来绝大多数任务都能有一个可靠的解法。关键是每一步都基于验证和比较做决策而不是凭感觉堆方法。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑