4、不平衡数据处理:过采样、欠采样、混合采样与代价敏感学习

做欺诈识别,你第一个要面对的问题,往往不是模型选哪个。

而是数据本身就不平衡。

正常交易几百万笔,欺诈可能就几百笔。比例轻松超过 1000:1。我刚开始做风控那会儿,拿这种数据直接丢进逻辑回归,结果模型学出来全是“正常”——准确率 99.9%,但一个欺诈都抓不到。你说这模型有啥用?

所以,不平衡数据处理,是欺诈建模的必修课。今天咱们就把过采样、欠采样、混合采样、代价敏感学习这四类方法,一次性讲透。

核心思路:不平衡问题的本质,是模型被多数类“带偏”了。解决思路就两条——要么让数据变平衡(采样),要么让模型更关注少数类(代价敏感)。

不平衡数据处理方法总览 不平衡数据 过采样 SMOTE / ADASYN 欠采样 随机欠采样 / Tomek Links 混合采样 SMOTEENN 代价敏感学习 class_weight / 自定义损失 核心目标:让模型不再忽视少数类(欺诈样本) 选择哪种方法?取决于数据量、噪声程度、业务容忍度

4.1 过采样:给少数类“造”点样本

过采样的思路很直接——少数类样本太少?那就复制或者生成一些新的。但直接复制(随机过采样)容易过拟合,所以有了更聪明的做法。

4.1.1 SMOTE:插值生成,不是简单复制

SMOTE 的全称是 Synthetic Minority Over-sampling Technique。它不复制样本,而是在两个少数类样本之间“插值”生成新样本。

具体怎么做的?对每个少数类样本,找到它的 k 个近邻(也是少数类),然后在样本和近邻的连线上随机取一点,作为新样本。

我的经验:SMOTE 在大多数场景下效果都不错。但我记得有一次做信用卡欺诈,欺诈样本本身就有很多噪声,SMOTE 把噪声也“插值”放大了,导致模型反而变差。所以,用 SMOTE 之前,最好先做一下异常值清洗。

from imblearn.over_sampling import SMOTE

smote = SMOTE(random_state=42, k_neighbors=5)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)

print(f"过采样前:{y_train.value_counts().to_dict()}")
print(f"过采样后:{pd.Series(y_resampled).value_counts().to_dict()}")

4.1.2 ADASYN:关注更难学的样本

ADASYN 是 SMOTE 的改进版。它有个核心思想:对于那些“更难学”的少数类样本(周围多数类更多),就多生成一些样本;对于“容易学”的,就少生成一些。

说白了,ADASYN 会根据样本的“困难程度”动态调整生成数量。我个人习惯在数据极度不平衡(比如 1:10000)时优先尝试 ADASYN,因为它能更聚焦在边界区域。

from imblearn.over_sampling import ADASYN

adasyn = ADASYN(random_state=42, n_neighbors=5)
X_resampled, y_resampled = adasyn.fit_resample(X_train, y_train)

注意:ADASYN 对噪声敏感。如果少数类中有离群点,ADASYN 会围绕它生成大量样本,把离群区域“撑大”。我曾经在一个反欺诈项目里踩过这个坑,后来加了预处理才解决。

4.2 欠采样:让多数类“瘦身”

欠采样的思路相反——多数类样本太多?那就删掉一些。优点是数据量变小了,训练更快。缺点是可能丢掉重要信息。

4.2.1 随机欠采样

最简单粗暴的方法。从多数类中随机抽取一部分,让多数类和少数类数量相等。

from imblearn.under_sampling import RandomUnderSampler

rus = RandomUnderSampler(random_state=42)
X_resampled, y_resampled = rus.fit_resample(X_train, y_train)

随机欠采样最大的问题是什么?信息丢失。你想想看,如果多数类有 100 万条,你只保留 1000 条,那 99.9% 的数据都被扔了。模型可能学不到多数类的全貌。

4.2.2 Tomek Links:删掉“纠缠”的样本

Tomek Links 是一种更聪明的欠采样方法。它找的是那些“互为最近邻但类别不同”的样本对——这种样本对通常位于决策边界附近,容易造成混淆。

处理方法:删除 Tomek Links 对中的多数类样本(或者同时删除两个)。这样边界就更清晰了。

from imblearn.under_sampling import TomekLinks

tl = TomekLinks()
X_resampled, y_resampled = tl.fit_resample(X_train, y_train)

避坑指南:Tomek Links 通常不单独用,而是作为“清洗”步骤。我习惯先做 SMOTE 过采样,再用 Tomek Links 清理边界上的模糊样本——效果往往比单独用任何一种都好。

4.3 混合采样:过采样 + 欠采样的组合拳

既然过采样和欠采样各有优缺点,那能不能结合起来?当然可以。最经典的就是 SMOTEENN。

4.3.1 SMOTEENN

SMOTEENN 的流程是:先用 SMOTE 过采样少数类,再用 Edited Nearest Neighbors(ENN)清理多数类中的噪声样本。ENN 会删除那些“被多数邻居包围”的样本。

这样做的好处是:既增加了少数类样本,又清理了多数类中的噪声和边界模糊样本。

from imblearn.combine import SMOTEENN

smote_enn = SMOTEENN(random_state=42)
X_resampled, y_resampled = smote_enn.fit_resample(X_train, y_train)

我在一个电商反欺诈项目里用过 SMOTEENN。当时数据量不大,但噪声很多。单独用 SMOTE 效果一般,加上 ENN 清洗后,模型 AUC 提升了 3 个点。嗯,这个组合确实值得一试。

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

前面几种方法都是在数据层面做文章。代价敏感学习不一样——它不碰数据,而是让模型在训练时“更重视”少数类。

怎么做?给少数类的误分类惩罚设得更高。比如,把欺诈样本判为正常的代价,设为正常样本判为欺诈的 100 倍。

4.4.1 在 sklearn 中实现

最简单的做法,就是在模型里设置 class_weight 参数。

from sklearn.linear_model import LogisticRegression

# 自动根据样本比例设置权重
model = LogisticRegression(class_weight='balanced')
model.fit(X_train, y_train)

# 也可以手动指定
model = LogisticRegression(class_weight={0: 1, 1: 100})
model.fit(X_train, y_train)

对于 XGBoost、LightGBM 这类树模型,也有对应的参数:

import xgboost as xgb

model = xgb.XGBClassifier(scale_pos_weight=100)  # 多数类/少数类比例
model.fit(X_train, y_train)

核心要点:代价敏感学习不改变数据分布,所以训练速度不受影响。但需要你根据业务场景合理设置代价比例。比例设得太高,模型会过度敏感,产生大量误报;设得太低,又抓不到欺诈。

4.5 如何选择?我的建议

说了这么多方法,到底该用哪个?我根据经验给个参考:

场景 推荐方法 理由
数据量小,噪声少 SMOTE 或 ADASYN 生成样本质量高,不易过拟合
数据量大,噪声多 SMOTEENN 或 Tomek Links 先增后清,保留有效信息
数据量极大(百万级) 随机欠采样 + 代价敏感 训练速度快,信息损失可接受
业务要求低误报率 代价敏感学习 可精细控制误报与漏报的平衡
不确定选哪个 SMOTE + 代价敏感 组合使用,稳健性高

最后说一句:没有银弹。我见过太多人一上来就无脑用 SMOTE,结果效果还不如不做。建议你每种方法都跑一遍,用验证集 AUC、召回率、精确率来评估,选最适合你业务的那个。

我的习惯:先用代价敏感学习快速跑一个 baseline,再尝试采样方法看能否进一步提升。这样效率最高,也最不容易踩坑。


公众号:蓝海资料掘金营,微信deep3321