ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数据不平衡实战指南:从SMOTE采样到阈值优化,拒绝被准确率欺骗

数据不平衡实战指南:从SMOTE采样到阈值优化,拒绝被准确率欺骗

做机器学习这几年,数据不平衡是我遇到频率最高、也最容易翻车的问题之一。不管是信贷风控里识别坏客户,还是设备预测性维护里抓故障样本,都逃不开一个局面:你真正关心的那个类别,在数据集里只占极小比例。我第一次踩坑时,也天真地把原始数据直接丢给XGBoost,然后盯着accuracy高达0.98沾沾自喜,直到拿到业务侧验证才发现,模型压根没学会找“少数派”。

这篇文章我会把自己从基线评估、数据重采样、算法侧调整、评价指标选型到工程落地整个链路里的实操经验完整记录下来,很适合两类同学:刚接触不平衡数据,想系统搞清楚处理思路的;或者已经在实战中被准确率骗过,想把这套方法用得明明白白的。

1. 数据不平衡问题的底层逻辑

1.1 什么时候算“不平衡”,什么时候真的需要干预

严格来说,学术界通常把二分类中少数类占比低于10%,或者正负样本比例超过1:10的情况称为类不平衡。但我在实际项目里更看重的是另一件事:比例悬殊是否真的导致模型失效。比如有些场景本身数据量很大,少数类哪怕只有5%,也有上万条样本,类别间特征可分性很好,那默认算法就能学到不错的模式;反过来,有些场景正负比只有1:2,但两类样本在特征空间里像混在一起的两团噪声,模型很容易被带偏。

所以我每次接到这类任务,第一件事是先做探索性分析:统计类别数量、画出特征分布、算一下类别之间的重叠度。只有确认“少数类学不出来”或者“错分代价太高”的时候,才真正进入重采样、加权的环节。另外,多分类也能出现不平衡,比如故障类型有十几种,某些类型只占0.5%以下,处理思路和二分类基本一致,只是要逐类评估,不能笼统地用一个全局指标掩盖问题。

1.2 模型被多数类带偏的本质原因

理解这个问题,得从损失函数说起。大多数分类器训练时都在最小化全体样本的平均损失,逻辑回归用交叉熵、树模型用基尼系数或信息增益,本质都是让整体误差变小。少数类样本占比只有1%的时候,就算模型把少数类全错分,总损失的增量也只有1%左右;而多数类的巨大数量会把决策边界牢牢“拉”到少数类一侧。梯度下降的任务变成“努力把大多数样本分对”,少数类就被牺牲了。

这就像一个班的平均分被99个及格生撑住,那个唯一的挂科生对总分几乎没影响。可如果你要做的事恰恰是找到那个挂科的人,这个平均分就完全没有意义。理解了这一点,后面所有技巧就都好记了:过采样本质是强行提高少数类在损失函数里的“话语权”,相当于告诉优化器,“这些人你不能放弃”;加权损失函数也是同一个逻辑,只是换了个角度实现。

1.3 测试集必须保持原始分布

这是最容易出错但很多人忽略的原则。重采样、合成样本、加权参数,本质是调整训练时的样本分布和损失目标;测试集则要模拟真实业务里的自然分布。如果测试集也跟着过采样了,你评估出来的precision和recall会虚高,因为测试集里人为增加了少数类样本比例,模型“更容易”撞对少数类。

正确的做法是:先用分层采样把原始数据划分成训练集和测试集,保证两边类别比例一致,然后只对训练集做任何重采样或合成操作。交叉验证里同样要注意这个问题,不能图省事把整个数据集先重采样再切折,那样每一折的验证集都被污染了,最终的评估结果没有任何参考价值。这个坑我在早期踩过好几次,后面第五章会展开说。

2. 数据侧处理:重采样方法与实战细节

2.1 随机过采样与欠采样:两条最朴素的路线

随机过采样就是把少数类样本复制多份,直到和多数类数量接近;随机欠采样则是从多数类里随机抽掉一部分,让两边数量对齐。这两条路看起来最简单,实际用起来却要谨慎。

方法优点缺点适用场景
随机过采样保留全部少数类信息,实现简单复制样本不产生新知识,容易过拟合少数类样本量很小、且特征空间相对干净时
随机欠采样训练集变小,训练速度快,能缓解数据量爆炸大量丢弃多数类信息,决策边界容易失真数据量很大、多数类冗余严重时

我个人的经验是,随机过采样一般只作为兜底方案,除非数据量实在小到没法用其他方法。因为它本质上是让模型多看几遍同一批样本,模型会慢慢“记住”这些样本而不是学到模式,训练集F1涨得很高,验证集立刻现原形。随机欠采样则要小心多数类里恰好包含对决策边界很关键的样本,全抽掉了,AUC可能不降,但PR曲线会明显变差。

另外,尽量不要追求1:1的完全平衡。实验室论文里常常把数据重采样成完全均衡,但工程场景里,1:1往往会让模型过度偏向少数类,把大量多数类样本误判成少数类。我个人习惯把少数类占比目标定在30%到40%之间,效果通常比50%更稳。

2.2 SMOTE:合成少数类样本的原理与边界

SMOTE(Synthetic Minority Over-sampling Technique)是我用的最多的方法。它的核心思路不是在原始样本上复制,而是在少数类样本之间“插值”生成新样本。具体过程是:对于每个少数类样本,找出它的k个近邻(默认为5),随机挑一个近邻,然后在这两个样本的特征连线上随机取一个点作为新样本。公式可以写成:

x_new = x_i + λ × (x_zi - x_i)

其中λ是0到1之间的随机数,x_i是当前少数类样本,x_zi是随机选出的近邻。因为新样本落在两个真实样本的连线上,多少带了一点“新鲜感”,所以比单纯复制更能缓解过拟合。

但SMOTE也有明显的边界条件。首先,它要求特征是连续数值型,如果是类别型特征或者混合型数据,插值会生成出不存在的“假类别”,需要改用SMOTE-NC。其次,当少数类和多数类在特征空间里本身高度重叠时,SMOTE生成的插值样本很可能落在多数类区域里,反而引入了噪声。这时候更稳妥的做法是用SMOTE结合清洗方法,比如SMOTE-Tomek Links,把落在边界上的可疑样本剔除掉;或者用SMOTE-ENN,把被判错近邻的样本清理掉。

还有一个很多人会忽略的参数:sampling_strategy。它控制合成后少数类占多数类的比例。别一上来就设成1.0,建议从0.2、0.3、0.5、0.7这几个档位里用验证集挑。采样比例越高,合成样本越多,模型被迫拟合这些合成样本的时间也越长,训练开销成倍上升,但收益往往在某个点之后就停滞了。

2.3 用 imbalanced-learn 把采样安全放进训练流程

Python里处理重采样最常用的库是imbalanced-learn,它提供了SMOTE、ADASYN、RandomOverSampler、SMOTETomek等一系列方法。更关键的是它自带了兼容sklearn的Pipeline,可以在交叉验证和网格搜索时自动保证重采样只作用于训练集。

from imblearn.over_sampling import SMOTE from imblearn.pipeline import Pipeline from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) pipeline = Pipeline([ ('smote', SMOTE(sampling_strategy=0.5, random_state=42)), ('clf', RandomForestClassifier(n_estimators=200, random_state=42)) ]) pipeline.fit(X_train, y_train) proba = pipeline.predict_proba(X_test)[:, 1]

这里有个容易踩的细节:如果用sklearn自带的Pipeline,重采样步骤会在transform阶段作用到测试集上,导致测试集也被SMOTE“污染”。imblearn的Pipeline专门处理了这个问题,重采样阶段只会作用于训练集的输入X和标签y,predict阶段什么都不改。所以里面部分方法不要随便换成sklearn的Pipeline来包装。

还要提醒一点:如果数据里有需要标准化的连续特征,要小心SMOTE和StandardScaler的执行顺序。SMOTE是基于距离和k近邻的,如果特征量纲差异很大,欧氏距离会被量纲大的特征主导,近邻选择就会失真。我的习惯是先做标准化,再做SMOTE。但标准化本身只能用训练集的统计量,不能把测试集一起放进去算,否则还是在泄漏。

3. 算法侧调整:代价敏感与集成策略

3.1 加权损失函数与class_weight的落地姿势

数据侧的采样解决的是“样本比例问题”,算法侧解决的是“损失目标问题”。最典型的做法是代价敏感学习,给少数类样本更高的权重,让模型在更新参数时更重视这些样本。

sklearn里很多分类器都支持class_weight参数,设成'balanced'会让模型按类别频率自动计算权重,计算公式大致是:

weight = n_samples / (n_classes × 类别样本数)

举个例子,假设1万条样本里9000条是多数类,1000条是少数类,类别数是2。多数类权重就是10000 / (2 × 9000) ≈ 0.56,少数类权重是10000 / (2 × 1000) = 5.0。也就是说,少数类样本在损失函数里的影响力被放大了接近9倍。XGBoost和LightGBM也有类似的参数,XGBoost里叫scale_pos_weight,直接设置为负样本数除以正样本数即可,也就是9。

但权重不是越高越好。少数类权重过高,模型为了让少数类分类正确,会把很多多数类样本也硬判成少数类,precision会掉得很难看。实际项目里,我会在理论上计算出来的权重附近做网格搜索,比如0.5倍、1倍、2倍、5倍,配合交叉验证选出真正适合数据的那一档。另外,如果训练数据的样本噪声很大,加权会更容易放大噪声的影响,这时候也可以先清洗数据再加权。

3.2 采样与集成结合:更适合极端场景的组合拳

当少数类占比低到1%以下,或者样本总量本身很小的时候,单独靠SMOTE或class_weight往往不太够用。这时候我倾向于把重采样和集成方法结合,让每一轮训练都能看到均衡的数据。

常见的方案有这么几类。Balanced Bagging和Balanced Random Forest,通过在每棵树的采样过程中分别从每个类别里抽取相同数目的样本,让每棵树都在均衡子集上训练,最后聚合结果,适合数据量中等、特征维度不算太高的情况。Easy Ensemble则把多数类随机切分成多个子集,每个子集分别和少数类组成均衡训练集,各自训练一个基学习器,最后投票。这种方法在1:100甚至更悬殊的比例下,效果往往会比单模型稳定不少。

还有一类就是把SMOTE嵌入到迭代过程里,比如SMOTEBoost,在AdaBoost的每一轮迭代前都生成一些少数类合成样本,专门让后续的基学习器更关注难分的少数类样本。这个方法理论效果不错,但训练开销会明显增加,如果数据量大、线上训练频次高,不一定适合生产环境。我一般倾向于先用Balanced Random Forest和加权XGBoost做对比,达不到业务指标再上更重的集成方案。

3.3 从数据量级判断该用哪套方案

技术方案选型最怕一刀切,我整理了一个粗略的判断逻辑,很多项目里从这个维度去看,方向基本不会错。

数据情况推荐思路
少数类占比5%-20%,样本量充沛class_weight或默认模型,重点做阈值优化
少数类占比1%-5%,特征可分性一般SMOTE采样 + 加权模型,交叉验证调参
少数类占比低于1%,或样本总量很小集成+重采样组合,必要时考虑异常检测思路
特征全是离散类别型优先用class_weight和集成,SMOTE效果有限

3.4 一组普适的实验顺序建议

先说一个小技巧:在动手选复杂方案之前,先跑一个“什么都不处理”的基线模型,哪怕它acc高得离谱、recall低得可怜,也要作为参照物。没有基线,你无法判断后面加的每一个环节到底贡献了多少。我会依次做这几组实验:

第一组:原始数据 + 默认模型,记录PR曲线和F1。第二组:原始数据 + class_weight,看加权带来的变化。第三组:SMOTE采样 + 默认模型。第四组:SMOTE + class_weight,观察叠加效果。第五组:选目前表现最好的组合,再做阈值优化。

这一步跑完,通常就能定位瓶颈到底在样本层面还是算法层面。有些项目里class_weight就能把F1从0.15拉到0.4,那后面再上复杂采样方案的意义就不大了;有些项目里确实是样本信息不够,需要SMOTE这种合成手段才能突破。

4. 评估指标与阈值:别被准确率骗了

4.1 accuracy的陷阱与业务成本

如果只看accuracy,不平衡数据会给你制造一个巨大的幻觉。举一个很常见的例子:10000笔交易里9900笔是正常的,100笔是欺诈。模型把所有交易都预测成正常,accuracy高达99%。但这是业务上最差的结果,因为那100笔欺诈全放过去了,可能造成的损失比几千笔正常交易的利润还高。

这就是我反复强调要看业务成本的原因。不平衡数据真正关心的是少数类的查全和查准,而不是整体的平均表现。所以从项目一开始就要定义清楚:漏掉一个少数类样本的代价是多少,误报一个多数类样本的代价是多少。这两个代价的比值会直接决定你之后阈值怎么调。

4.2 混淆矩阵、PR曲线与ROC的取舍

正式评估不平衡模型的时候,我第一件事永远是输出混淆矩阵。四个基本量会告诉你所有故事:

预测/实际少数类正例多数类负例
预测为少数类TP 真正例FP 假正例
预测为多数类FN 假负例TN 真负例

Precision = TP / (TP + FP),看的是“你报警之后到底准不准”;Recall = TP / (TP + FN),看的是“真实少数类你到底找回了多少”;F1是两者的调和平均,偏向取平衡。如果业务要求宁可多报不能漏,那就把Recall放前面;如果误报成本太高,那就把Precision放前面。

ROC和AUC在极端不平衡场景里有一个问题:ROC曲线的两个轴都包含多数类样本的数量信息,当多数类占据了绝对主导时,曲线往往看起来非常乐观,但实际少数类recall可能还是上不去。PR曲线就不一样,它直接把Precision和Recall画在一起,完全聚焦在少数类的查找效果上。所以我的习惯是:如果这个项目核心就是找出少数类,以PR曲线和PR-AUC为主;只有当业务要求两个类别都要兼顾时,才把AUC当作主要参考。

4.3 阈值优化:把概率变成业务决策的临门一脚

很多分类器默认用0.5作为预测阈值,但在不平衡数据里,0.5几乎总是次优的。调阈值对最终效果的影响,常常比换模型还要大,而且几乎零成本。做法是拿到模型输出的概率值,然后在验证集上遍历一组阈值,找到最优的判定点。

import numpy as np from sklearn.metrics import precision_recall_curve precisions, recalls, thresholds = precision_recall_curve(y_test, proba) f1_scores = 2 * (precisions[:-1] * recalls[:-1]) / (precisions[:-1] + recalls[:-1] + 1e-9) best_idx = np.argmax(f1_scores) best_thr = thresholds[best_idx]

这段代码会输出使F1最大的阈值。如果目标是最大化查全率,可以把阈值压到0.2甚至更低;如果目标是控制误报、保证Precision,就把阈值抬到0.6、0.7。关键一步是必须在验证集上选阈值,选完再固定下来去测测试集,不然阈值本身会和测试集做过拟合。我在生产项目里发现,默认0.5和调优后的阈值之间,F1常常差出0.1以上,这属于白捡的收益。

5. 完整落地流程与踩坑实录

5.1 端到端处理流程设计

我一般按照下面的顺序把整个项目串起来,每个环节之间保持清晰边界:

  1. 探数:统计类别比例、样本总量、缺失情况、特征类型,判断需不需要处理。
  2. 分层划分:用stratify参数切分训练集和测试集,保证两边类别比例一致。
  3. 定基线:不加任何重采样和加权,训练一个简单的随机森林或逻辑回归,记录混淆矩阵、F1、PR曲线。
  4. 数据层实验:跑SMOTE或SMOTETomek,按不同sampling_strategy档位对比。
  5. 算法层实验:加class_weight或换Balanced集成模型,和第四步结果对比。
  6. 交叉验证:在训练集内部做5折或10折验证,用网格搜索选出最优组合,注意重采样必须放在每一折内部。
  7. 阈值优化:用验证集概率选择最优业务阈值。
  8. 最终评估:固定阈值去预测测试集,输出P/R/F1和业务侧可解释的结论。

这套流程看起来步骤多,但每一步都有明确产出,不会白跑。大多数项目卡在第六步和第七步之间,很多人交叉验证做完了模型就收工,阈值优化这一步直接被跳掉,其实非常可惜。

5.2 常见问题速查表

我把这几年遇到的高频问题整理成一张表,碰到类似情况可以直接从这里找排查方向:

现象可能原因解决思路
训练F1高、验证F1低重采样比例过高,模型过拟合合成样本降低sampling_strategy,或改用SMOTE+Tomek清洗
测试集效果突然暴跌重采样泄漏到测试集或验证集确认使用了imblearn的Pipeline,检查切分顺序
accuracy很高但recall很低模型全预测成多数类换成PR指标评估,调低阈值,检查class_weight
少数类样本被大量误报权重过高或阈值太低降低少数类权重,提高判定阈值
多分类中个别类效果差类间样本量差异过大对每个类单独计算权重,或逐类做采样
类别型特征为主时SMOTE效果差插值生成了不存在的类别改用SMOTE-NC,或直接依靠class_weight和集成

5.3 实操心得:踩过的坑和推荐做法

先说采样比例。很多论文里都用1:1完全平衡,我一开始也照抄过,结果F1反而下降了。后来才想明白:合成样本虽然有用,但毕竟是“造”出来的,当它数量超过真实样本时,模型的分布认知就开始偏离真实。更稳的做法是把少数类调成多数类的30%到50%,让模型既重视少数类,又不会因为数据太假而失去对多数类的基本建模能力。这个结论我在好几个推荐场景和风控场景里都验证过。

再说数据泄漏。有一次我做交叉验证参数搜索,图省事把SMOTE放在整个数据集上先跑了一遍,然后再去做K折切分,最后验证集F1高得离谱,模型上线后效果直接崩了一半。这就是典型的重采样泄漏,验证集里混入了训练折的合成样本,评估结果完全失真。从那以后我养成了习惯:凡是涉及重采样的实验,一律用imblearn的Pipeline包进每一折内部,绝不手动分开做。

最后分享一个小技巧:上线后的概率校准。模型调完阈值之后,我还要在线上样本上观察预测概率的分布是否偏移了,因为生产环境的数据分布和训练期很难保持一致。如果不定期监控少数类带来的概率漂移,前面的采样和调参功夫很容易被新数据冲淡。这一点在长期运营的业务场景里比什么都重要。

数据不平衡本身不是洪水猛兽,它更像是一面镜子,逼着你回到业务目标上去思考“到底该让模型最大化什么”。把评价指标、损失权重、采样策略和阈值全部对齐到业务成本上,你会发现模型的效果提升往往比换一个更强的算法来得更踏实。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进