做机器学习这几年,数据不平衡是我遇到频率最高、也最容易翻车的问题之一。不管是信贷风控里识别坏客户,还是设备预测性维护里抓故障样本,都逃不开一个局面:你真正关心的那个类别,在数据集里只占极小比例。我第一次踩坑时,也天真地把原始数据直接丢给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 端到端处理流程设计
我一般按照下面的顺序把整个项目串起来,每个环节之间保持清晰边界:
- 探数:统计类别比例、样本总量、缺失情况、特征类型,判断需不需要处理。
- 分层划分:用stratify参数切分训练集和测试集,保证两边类别比例一致。
- 定基线:不加任何重采样和加权,训练一个简单的随机森林或逻辑回归,记录混淆矩阵、F1、PR曲线。
- 数据层实验:跑SMOTE或SMOTETomek,按不同sampling_strategy档位对比。
- 算法层实验:加class_weight或换Balanced集成模型,和第四步结果对比。
- 交叉验证:在训练集内部做5折或10折验证,用网格搜索选出最优组合,注意重采样必须放在每一折内部。
- 阈值优化:用验证集概率选择最优业务阈值。
- 最终评估:固定阈值去预测测试集,输出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包进每一折内部,绝不手动分开做。
最后分享一个小技巧:上线后的概率校准。模型调完阈值之后,我还要在线上样本上观察预测概率的分布是否偏移了,因为生产环境的数据分布和训练期很难保持一致。如果不定期监控少数类带来的概率漂移,前面的采样和调参功夫很容易被新数据冲淡。这一点在长期运营的业务场景里比什么都重要。
数据不平衡本身不是洪水猛兽,它更像是一面镜子,逼着你回到业务目标上去思考“到底该让模型最大化什么”。把评价指标、损失权重、采样策略和阈值全部对齐到业务成本上,你会发现模型的效果提升往往比换一个更强的算法来得更踏实。