4. 数据不平衡处理:欠采样/过采样策略、SMOTE及其变体、代价敏感学习、集成方法处理不平衡

数据不平衡,说白了就是正负样本比例严重失调。比如欺诈检测里,正常交易占99.9%,欺诈只有0.1%。你想想看,模型要是学成个“全都判正常”,准确率照样99.9%,但一点用都没有。

我刚开始做风控模型时就吃过这个亏。当时训练集里坏账率不到1%,我直接丢进XGBoost跑了一版,AUC看着还行。上线第一天,坏账全没抓住。嗯,从那以后,数据不平衡就成了我每次建模前必查的第一道关。

核心认知:不平衡不是“数据量少”,而是“少数类信号被淹没”。处理不平衡的本质,是让模型能听到少数类的声音。

数据不平衡处理策略全景图 不平衡数据集 数据层面 算法层面 集成方法 混合策略 欠采样 过采样 代价敏感学习 EasyEnsemble / BalanceCascade SMOTE + 集成 关键选择原则 • 数据量 < 1万:优先考虑过采样 / SMOTE • 数据量 > 10万:欠采样 + 集成更高效 • 噪声较多:避免简单过采样,用SMOTE变体或代价敏感 • 业务可解释性要求高:代价敏感学习优于黑盒采样 • 终极方案:交叉验证 + 多种策略组合调参

4.1 欠采样与过采样:最朴素但最有效

欠采样就是从多数类里随机抽一部分,让正负样本数量接近。过采样则是复制少数类样本,直到比例平衡。听起来简单,但坑不少。

我的经验:欠采样别抽太狠。我曾经把多数类从10万抽到1000,模型倒是平衡了,但整体信息丢失严重,AUC直接掉了0.15。一般建议欠采样后多数类数量保持在少数类的2-5倍,留点冗余。

过采样有个致命问题——直接复制样本容易过拟合。模型记住的是同一个样本的“克隆体”,而不是学到真正的分布规律。我见过一个团队用简单过采样训练了100轮,验证集F1高达0.95,上线后直接掉到0.3。说白了,模型把复制样本的特征当成了真理。

方法 优点 缺点 适用场景
随机欠采样 计算快,减少数据量 可能丢失重要信息 数据量大、多数类冗余多
随机过采样 实现简单,不丢信息 容易过拟合 数据量小、噪声少
NearMiss欠采样 保留边界样本 计算开销大 边界清晰、类别可分性强
Tomek Links 去除噪声和重叠 可能过度清洗 数据质量差、有离群点

4.2 SMOTE及其变体:合成样本的艺术

SMOTE(Synthetic Minority Over-sampling Technique)不是简单复制,而是在少数类样本之间插值生成新样本。具体来说,对每个少数类样本,找到它的k个近邻,然后在样本和近邻的连线上随机取一点作为新样本。

我最早用SMOTE时犯过一个错——没做标准化就直接插值。特征A取值范围0-1,特征B取值范围0-10000,插值出来的样本全被特征B主导。嗯,标准化是SMOTE的前置条件,这个坑我替你们踩过了。

from imblearn.over_sampling import SMOTE
from sklearn.preprocessing import StandardScaler

# 标准化是必须的
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

smote = SMOTE(random_state=42, k_neighbors=5)
X_resampled, y_resampled = smote.fit_resample(X_scaled, y)

# 记得把数据变换回去
X_resampled = scaler.inverse_transform(X_resampled)

SMOTE有几个经典变体,各有各的脾气:

  • Borderline-SMOTE:只对边界上的少数类样本做插值。为什么?因为内部的少数类样本已经很安全了,边界才是分类难点。我在信用评分项目里用过,比原始SMOTE的F2提升了0.08。
  • ADASYN:根据样本密度自适应生成。密度越低的区域,生成越多样本。说白了,哪里少数类越稀缺,就往哪里补。但要注意,ADASYN对噪声敏感,数据质量差时慎用。
  • SMOTE-ENN:先SMOTE过采样,再用Edited Nearest Neighbors清洗。相当于先造一批样本,再把那些“造得不像”的踢掉。我个人习惯用这个组合,鲁棒性最好。

避坑指南:我曾经在文本分类任务里直接用SMOTE,结果生成了大量语义不通的样本。SMOTE假设特征空间是连续的,对离散特征(如词袋向量、one-hot编码)效果很差。遇到这种情况,要么用代价敏感学习,要么在嵌入层之后再做采样。

4.3 代价敏感学习:不改数据,改损失函数

代价敏感学习的思路完全不同——不碰数据,直接让模型对少数类分错付出更高代价。比如在逻辑回归里,给少数类的误分类权重设为10,多数类设为1。模型为了降低总损失,自然会偏向少数类。

实现方式主要有三种:

  1. 样本权重:在fit()时传入sample_weight参数。sklearn里大部分模型都支持。
  2. 类别权重:设置class_weight='balanced',让模型自动根据样本比例调整权重。
  3. 自定义损失函数:在深度学习里自己写加权交叉熵。
from sklearn.linear_model import LogisticRegression

# 方法一:自动平衡权重
model = LogisticRegression(class_weight='balanced')
model.fit(X_train, y_train)

# 方法二:手动指定权重(比如欺诈检测,误放一个欺诈损失100倍)
weights = {0: 1.0, 1: 100.0}
model = LogisticRegression(class_weight=weights)
model.fit(X_train, y_train)

代价敏感学习的好处是不改变数据分布,训练集还是原来的分布,模型上线后不会因为数据分布变化而崩掉。但有个前提——你得知道误分类的真实代价。比如在医疗诊断里,漏诊的代价是误诊的10倍还是100倍?这个需要业务方给出来,不是拍脑袋定的。

我的习惯:先用class_weight='balanced'跑一版baseline,然后跟业务方对齐代价比例。很多时候业务方自己也没算清楚,我会建议他们做一次“代价敏感性分析”——画出不同权重下的精确率-召回率曲线,让业务方选一个可接受的平衡点。

4.4 集成方法处理不平衡:多个模型比一个强

集成方法处理不平衡,核心思路是“分而治之”。把多数类切成多份,每份跟少数类组合成一个平衡子集,然后训练多个模型,最后投票或平均。

EasyEnsemble:从多数类里随机采样多个子集,每个子集跟少数类组合训练一个分类器。我做过实验,用10个子集训练10个逻辑回归,集成后的AUC比单模型用SMOTE还高0.03。而且每个子模型都很轻量,训练速度快。

BalanceCascade:比EasyEnsemble更聪明一点。它是有监督地采样——每轮训练后,把那些被正确分类的多数类样本丢掉,只保留被分错的。下一轮再从剩下的多数类里采样。说白了,每一轮都在攻克上一轮没搞定的难点。

from imblearn.ensemble import EasyEnsembleClassifier, BalancedRandomForestClassifier

# EasyEnsemble:默认10个子集
eec = EasyEnsembleClassifier(n_estimators=10, random_state=42)
eec.fit(X_train, y_train)

# 平衡随机森林:每棵树内部自动采样
brf = BalancedRandomForestClassifier(n_estimators=100, random_state=42)
brf.fit(X_train, y_train)

平衡随机森林(Balanced Random Forest)是另一种思路——每棵决策树在分裂时,对当前节点的样本做自助采样,强制让正负样本比例平衡。我对比过,在高度不平衡的数据上(比例超过100:1),平衡随机森林比普通随机森林的召回率高出30%以上。

避坑指南:集成方法虽然效果好,但可解释性差。如果你做的是金融风控或医疗诊断,监管要求解释每个样本的预测理由,那集成方法可能不是最佳选择。这时候我建议用代价敏感的逻辑回归或决策树,配合SHAP值做解释。

4.5 实战选择:到底该用哪个?

说了这么多,你可能会问:到底该用哪个?我的建议是分三步走:

场景 推荐方案 理由
数据量 < 5000,比例 < 10:1 SMOTE + 代价敏感 数据少,SMOTE能有效扩充;代价敏感防止过拟合
数据量 > 50000,比例 > 100:1 EasyEnsemble / BalanceCascade 数据量大,集成方法效率高,不丢失信息
特征维度高(> 1000) 代价敏感学习 高维空间下SMOTE插值容易产生噪声
深度学习模型 加权损失函数 + 数据增强 深度学习对采样敏感,加权损失更稳定
业务要求可解释 代价敏感逻辑回归 权重透明,每个特征贡献可解释

最后说一句,没有银弹。我见过有人把SMOTE、欠采样、代价敏感、集成全堆在一起,结果模型反而变差了。处理不平衡,不是方法越多越好,而是找到最适合你数据特点的那一个。先跑一版简单的baseline,再逐步加复杂度,这才是工程上的正道。