想象你是餐厅后厨的主厨,正在研发一道以松露为核心的招牌菜。仓库里堆着几千颗土豆,而松露只有屈指可数的几颗。负责试菜的助手是个朴素的统计者:它连尝一百口,九十九口是土豆味,于是认定“这道菜就该是土豆味”,松露被彻底忽略。这就是机器学习里的类别不平衡问题——多数类样本浩如烟海,少数类样本凤毛麟角,而后者往往才是真正的主角:欺诈交易、罕见病、设备故障,全是日常生活中极难碰到的“几颗松露”。 面对这个难题,聪明的厨师有三条路:第一,用现有松露“合成”更多松露片——这是过采样,代表人物是 SMOTE;第二,清理掉一大批贴着松露、容易混淆视听的土豆——这是欠采样,代表方法是 Tomek Links 和 ENN;第三,修改评分规则,让“松露做错”扣双倍分——这是类别重平衡损失。此外,评委的打分方式也得跟着改:不能只看总好评率,要看对松露菜的精确率和召回率。 这篇文章就跟你说说这三条路各自怎么走、什么时候走哪条,配套代码和避坑指南都为你准备好。
真实业务里少数类才是金矿。掌握 SMOTE 与重采样,你就能在欺诈、疾病、故障等极端不平衡数据上训练出对少数类敏感的模型,这是风控、医疗、工业 AI 从业者的核心技能之一。
目录
可视化演示
展示原始不平衡数据分布、SMOTE合成少数类样本、Tomek Links清洗边界与重平衡结果,支持步骤切换与动态演示。
本节配有交互动效(进入视口后自动加载,约 3KB)。
原始不平衡数据分布 多数类样本数量远超少数类 少数类仅占12.5% Imbalance ratio: 1:8 类别不平衡导致分类器偏向多数类
问题与成因:一筐土豆与几颗松露
后厨里,土豆堆成小山,松露只有零星几颗。试菜助手统计完食材后得出结论:“这道菜 99% 是土豆味”,于是一道松露招牌菜被它做成了土豆泥。类别不平衡就是这个“食材比例”问题:训练数据里,多数类样本成千上万,少数类样本寥寥无几,模型学着学着就“偏心”了。
在机器学习的分类任务中,类别不平衡指各类别样本数量悬殊。典型场景包括信用卡欺诈(约 0.1%~0.5%)、罕见病筛查(约 1%)、工业设备故障(约 2%)。直接训练会怎样?以准确率为优化目标时,模型只要把一切判成多数类,就能拿到 99% 以上的“漂亮”准确率——但少数类一个都预测不中。
为什么会这样?因为损失函数被海量多数类样本主导,梯度更新朝着“把多数类分对”的方向倾斜;少数类样本太少,它们的错误在总损失里被稀释得几乎看不见。数学上常用不平衡比率(Imbalance Ratio)刻画严重程度:
看清问题后,解决方案无非三条路:给后厨多造松露(过采样)、把多余土豆清理掉(欠采样)、或者让试菜助手对松露犯错付出更高代价(重平衡损失)。接下来逐一登场。
- N_maj(多数类样本数)数量占优类别的样本总数,相当于仓库里那几千颗土豆。
- N_min(少数类样本数)数量稀少类别的样本总数,相当于仅有的几颗松露。
- IR(不平衡比率)两者相除的结果。IR=100 意味着多数类是少数类的一百倍,模型天然倾向于忽略少数类。
SMOTE过采样:给松露插值出新片
把同一片松露反复端上桌会怎样?试菜助手背下了它的纹理,下次换一片松露就不认识了。随机过采样就是这种“复制粘贴”——把少数类样本复制几份塞回训练集,模型容易把复制品原样背下来,导致过拟合。
SMOTE(Synthetic Minority Over-sampling Technique,合成少数类过采样技术)由 Chawla 等人于 2002 年提出,思路完全不同。对每个少数类样本,先在少数类内部寻找 K 个最近邻(K 通常取 5),随机挑一个邻居,在两者连线上随机取一点作为新样本。新样本是“两片真实松露的中间纹理”——既不是复制品,又贴近真实分布。
数学上,设少数类样本为 $x_i$,选中的近邻为 $x_{z_i}$,随机数 $\lambda$ 在 0 到 1 之间均匀取值,新样本按下式生成:
SMOTE 也有脾气:它只在少数类内部连线,若少数类样本本身带噪声,或特征维度过高(如文本 TF-IDF),合成样本可能“串味”落入多数类区域,反而添乱。这正是下一节 Borderline-SMOTE 想解决的边界问题。
- x_i(基样本)随机挑选的一个少数类样本,是合成的起点。
- x_zi(K 近邻之一)在少数类样本中与 x_i 最近的 K 个样本之一。
- x_zi − x_i(方向向量)从基样本指向邻居的差向量,刻画“往哪个方向补”。
- λ(插值比例)[0,1] 内的随机数,控制新样本落在连线上的哪个位置。
- x_new(合成样本)基样本加上按比例缩放的差值,得到的新少数类样本。
Borderline变体:只在交战区合成
松露与土豆的交界处,才是决定“这道菜算不算松露味”的关键地带。深处再多松露切片,也改变不了边界的判断。分类任务同理:真正难分的是类别边界附近的样本,远离边界的样本无论怎么合成,对决策边界的帮助都很有限。
Borderline-SMOTE 因此只对“处于危险区”的少数类样本做合成。它数每个少数类样本周围 m 个近邻里有多少个多数类:若多数类邻居比少数类邻居多(占比超过一半),说明该样本已处于边界甚至被“包围”,属于 DANGER 集,优先在这里插值。
判定条件可以写成集合形式:
由此衍生出两个版本:BSMOTE1 在 DANGER 样本与少数类近邻之间插值,稳扎稳打;BSMOTE2 允许与任意类近邻连线,但新样本仍要偏向少数类一侧。一句话回扣比喻:只给“贴着土豆的松露”做扩展,因为那里的每一片新松露都能帮试菜助手看清边界;深处合成再多,边界依旧模糊。
- S_min(少数类集合)全部少数类样本。
- n_maj(x_i)(多数类邻居数)样本 x_i 的 m 个最近邻中多数类的个数。
- m/2 < n ≤ m(危险区判据)多数类邻居占多数,说明 x_i 已陷入“敌区”或卡在边界。
- D(DANGER 集)满足条件的边界样本集合,Borderline-SMOTE 只对这些样本合成。
欠采样方法:清理伪装成松露的土豆
另一种思路:不动松露,清理土豆。随机欠采样简单粗暴——随机丢弃多数类样本,直到两类数量接近。但随机丢弃可能丢掉对分类有用的信息,相当于闭着眼把好土豆也扔了。
Tomek Links 更聪明。定义一对异类样本 $(x_a, x_b)$,若它俩互为最近邻,就构成一条 Tomek Link——就像一颗紧贴着松露、纹理被“蹭”得极像的土豆。这条连线正是边界混淆最严重的位置。删除这批配对样本(通常删多数类,或两者都删),边界立刻变干净:
ENN(Edited Nearest Neighbors,编辑近邻)则是“邻居投票”:对每个样本,看它 K 个近邻的类别。若近邻中大多数与它类别不同,说明它是误标或卡在别人地盘里的噪声,删除之。欠采样的代价是丢弃数据——多数类中远离边界的“安全土豆”其实很有价值,误删会损失信息。因此工程上更常见的是“先过采样、再欠采样清理边界”的混合策略。
- y_a ≠ y_b(异类)一对样本分属两个类别,一个是土豆一个是松露。
- x_b ∈ kNN(x_a)(前向最近邻)x_b 是 x_a 的最近邻之一。
- x_a ∈ kNN(x_b)(反向最近邻)x_a 也是 x_b 的最近邻,二者互为最近邻。
- T(Tomek Links 集合)满足条件的所有异类紧挨对,删除后边界变干净。
混合采样:先补松露再清边界
大厨的完整配方通常是两步走:先合成一批松露片(SMOTE),再用 Tomek Links 或 ENN 把边界上的混乱样本清一遍。补与清各司其职,前者解决“松露太少”,后者解决“边界太脏”。
SMOTE-Tomek 组合:SMOTE 之后,在新数据上找 Tomek Links 并移除;SMOTE-ENN 组合:用 ENN 删除被多数邻居“投票出局”的样本。前者主要用于清理边界,删得克制;后者清理得更狠,对噪声重的数据更鲁棒。整体流程可写成:
实战中有条铁律必须记住:重采样只作用于训练集内部。如果先对整个数据集做 SMOTE 再划分,合成样本会偷偷混进测试集——等于让评委提前尝到为它特制的松露,验证指标全是虚假繁荣。
- X_train(原始训练集)划分后未做任何处理的原训练数据。
- SMOTE(X_train)(先补)第一步过采样,生成合成少数类样本。
- T_clean(清理对集合)在新数据上找出的待清理混淆样本对(Tomek 或 ENN 标记)。
- ∖(集合差)把这些边界混淆样本从增强集中移除,得到最终训练集。
重平衡损失:让松露错误翻倍扣分
重采样改的是“食材”,重平衡损失改的是“评分规则”。与其费劲调整数据分布,不如直接修改损失函数:让少数类样本犯错时付出更高的代价,模型自然不敢怠慢。
最直接的做法是类别权重(class weight)。给少数类样本的损失乘上较大的权重 $w_{+}$,多数类乘较小的权重 $w_{-}$,通常取与样本数量成反比。加权交叉熵如下:
Focal Loss 更进一步:不只知道“哪类是重点”,还知道“哪些样本难分”。它的调制因子 $(1-p_t)^{\gamma}$ 让已被模型信心十足分对的样本损失大幅缩小,从而把梯度聚焦在难分样本上($\gamma$ 常取 2):
重平衡损失常与重采样搭配使用:数据层面给松露更多“出场机会”,损失层面给松露错误更高“代价”,双管齐下,模型才不敢偏心。
- p_t(真实类概率)模型对样本真实类别的预测概率,越接近 1 表示分得越有把握。
- (1−p_t)^γ(调制因子)分对且把握大时趋近于 0,该样本的损失被压得很小。
- γ(聚焦参数)常取 2,γ 越大,对难分样本的聚焦越强。
- log p_t(交叉熵核心)保留对“分错”的惩罚能力。
评估与实战:别被平均好评骗了
菜做得好不好,评委不能只看“总好评率”——如果评审团 99% 是土豆爱好者,一道忽略松露的菜也能拿高分。准确率在类别不平衡下就是这种“骗人的平均好评率”。
正确做法是先看混淆矩阵:真正例 TP(松露被认出是松露)、假正例 FP(土豆被误认成松露)、真负例 TN(土豆被认出是土豆)、假负例 FN(松露被漏看成土豆)。由它派生出精确率、召回率与 F1:
多数类占 99% 时,ROC-AUC 会被海量负样本“灌水”而虚高,应改用 PR-AUC(精确率-召回率曲线下面积),它对少数类占比更敏感。验证策略上,用分层 K 折(Stratified K-Fold)保持每折类别比例一致;务必先划分、后重采样,防止泄漏。最终衡量标准回到业务目标:欺诈检测宁可误报也要少漏报(高召回),精准营销则更在意精确率——选择哪个指标,决定了你的“松露菜”是偏足量还是偏纯正。
- TP(真正例)少数类被正确预测为少数类。
- FP(假正例)多数类被误判为少数类,即“误报”。
- FN(假负例)少数类被漏判为多数类,即“漏报”。
- Precision(精确率)预测为正的样本中真正为正的比例。
- Recall(召回率)真实为正的样本中被找出的比例。
- F1(调和平均)同时兼顾精确率与召回率的综合分。
AI 应用场景
信用卡反欺诈
阶段:数据预处理与特征工程
解决的问题:银行每日上千万笔交易,欺诈率不足 0.2%,传统规则引擎只能查出已知模式。直接训练逻辑回归时,模型为了整体准确率几乎不报任何欺诈,漏过大量可疑交易——就像试菜助手只报土豆味。
具体案例:某风控团队用 SMOTE-Tomek 将训练集中的欺诈样本从几千条扩充到数万条并清理边界,再用类别加权损失训练 XGBoost,欺诈召回率从 18% 提升到 67%,每拦截一笔欺诈挽回数千元损失。
罕见病辅助筛查
阶段:模型评估与迭代
解决的问题:罕见病发病率约万分之一,医院积累的正样本极少,而漏诊一名患者后果严重,AI 需要极高的召回率。
具体案例:医疗 AI 团队用 Borderline-SMOTE 对“边界型”病变影像特征插值合成,配合 Focal Loss 训练,在保持 95% 特异度的前提下将单病种召回率提升到 91%;评估时不看准确率,而是用 PR-AUC 判断模型是否真的学会了识别罕见病灶。
工业预测性维护
阶段:建模与上线监控
解决的问题:设备故障是罕见事件,训练数据绝大多数是正常工况,但一次未预报的停机可能带来数十万元损失,漏报比误报更可怕。
具体案例:团队用 SMOTE+ENN 处理传感器时序特征,ENN 负责剔除误标和噪声样本;评估选用更看重召回的 F2 指标上线告警系统,误报率控制在业务可接受范围内。
Python 演示
import numpy as npfrom sklearn.datasets import make_classificationfrom sklearn.neighbors import NearestNeighborsfrom sklearn.model_selection import train_test_splitfrom sklearn.ensemble import RandomForestClassifierfrom sklearn.metrics import f1_score, recall_score def smote(X, y, k=5, ratio=2.0): # 只在少数类内部找近邻 X_min = X[y == 1] n_min = len(X_min) n_need = int(n_min * ratio) nn = NearestNeighbors(n_neighbors=k + 1).fit(X_min) _, idx = nn.kneighbors(X_min) rng = np.random.RandomState(42) synthetic = [] while len(synthetic) < n_need: i = rng.randint(n_min) j = rng.randint(1, k) gap = rng.rand() new = X_min[i] + gap * (X_min[idx[i][j]] - X_min[i]) synthetic.append(new) X_new = np.vstack([X, np.array(synthetic)]) y_new = np.concatenate([y, np.ones(len(synthetic))]) return X_new, y_new def tomek_links(X, y): # 删除互为最近邻的异类对 nn = NearestNeighbors(n_neighbors=2).fit(X) _, idx = nn.kneighbors(X) keep = np.ones(len(X), dtype=bool) for i in range(len(X)): j = idx[i][1] if y[i] != y[j] and j < i: keep[i] = False keep[j] = False return X[keep], y[keep] X, y = make_classification( n_samples=2000, n_features=8, weights=[0.93, 0.07], random_state=42)X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y) X_aug, y_aug = smote(X_train, y_train)X_final, y_final = tomek_links(X_aug, y_aug)print("minority after smote:", int((y_aug == 1).sum()))print("total after tomek:", len(X_final)) clf = RandomForestClassifier(n_estimators=100, random_state=42)clf.fit(X_train, y_train)y_pred = clf.predict(X_test)print("Baseline | recall=%.3f f1=%.3f" % ( recall_score(y_test, y_pred), f1_score(y_test, y_pred))) clf.fit(X_final, y_final)y_pred2 = clf.predict(X_test)print("SMOTE+Tomek | recall=%.3f f1=%.3f" % ( recall_score(y_test, y_pred2), f1_score(y_test, y_pred2)))
weights=[0.93, 0.07]生成 93% 多数类、7% 少数类的不平衡数据集。stratify=y划分训练/测试集时保持两类比例不变,避免测试集比例漂移。NearestNeighbors(n_neighbors=k + 1)只在少数类内部找近邻,+1 是因为第一个邻居是它自己。new = X_min[i] + gap * (X_min[idx[i][j]] - X_min[i])核心插值公式,在基样本与近邻连线上随机取点。if y[i] != y[j] and j < i找互为最近邻的异类对,j < i保证每对只处理一次。clf.fit(X_final, y_final)在重采样后的数据上重新训练分类器。recall_score(y_test, y_pred)以少数类召回率为核心衡量指标,准确率在这里不靠谱。
常见误区
误区常见做法是把 SMOTE 放在预处理最前面,对全量数据合成后再划分训练集和测试集,觉得这样数据更充足。
正解正确顺序是先划分、后重采样,且只处理训练集。合成样本基于少数类插值生成,若混入测试集,等于考试前把答案泄露给考生,验证指标虚高、上线即翻车。比喻成做菜:不能让评委提前尝到为它特制的松露,否则好评没有任何参考价值。
为何常见:初学者把重采样当作“数据增强”,习惯性归入预处理流水线,忽略了它对数据分布的人为修改。
误区以为 SMOTE 跟随机过采样一样,把少数类样本复制几份塞回训练集,只是数量变多而已。
正解随机过采样才是复制;SMOTE 在少数类样本与其近邻之间线性插值,生成的是“两片松露的中间纹理”,位置全新、形态多样,能缓解过拟合。复制会导致模型死记硬背同一片纹理,泛化能力变差。
为何常见:两者都让少数类变多、名字都带“过采样”,流程上看起来很像,容易混淆。
误区在 99:1 的数据上训练,模型准确率高达 99%,于是认为模型很棒,直接准备上线。
正解全猜多数类就能拿到 99% 准确率,但对少数类(欺诈、罕见病)召回率为 0,业务上一文不值。必须看混淆矩阵和 P/R/F1,尤其关注少数类召回率;不平衡场景下用 PR-AUC 替代 ROC-AUC 更可靠。
为何常见:准确率是最早接触的指标,直觉上最有代表性,大脑也容易被高数字安慰。
误区以为 Tomek Links 和随机欠采样一样,随机丢弃多数类样本,让两类数量平衡。
正解Tomek Links 只删除“互为最近邻的异类对”,是精准的边界清理,目标不是平衡数量,而是去掉最容易混淆的样本。随机欠采样才是闭眼盲删,两者信息价值完全不同。
为何常见:二者都属于欠采样,名字里都带“删多数类”,容易被归为一类。
面试题
Q1. 在极端类别不平衡数据上,为什么“预测全部样本为多数类”的模型不可接受?
- A.它对少数类召回率为零← 正确答案
- B.因为它训练时收敛太慢
- C.因为会占用过多内存
- D.因为它无法输出概率
答案:A。全部预测为多数类的模型,虽然准确率可能高达 99%,但对少数类的召回率为 0——欺诈、罕见病等关键少数一个都找不出来,业务上毫无价值。收敛速度、内存、概率输出都与“不可接受”无关。
Q2. SMOTE 生成少数类新样本的核心方式是?
- A.复制少数类样本
- B.近邻间线性插值← 正确答案
- C.删除相邻多数类
- D.增加特征数量
答案:B。SMOTE 在少数类样本与其 K 近邻的连线上随机取点,生成全新样本,属于线性插值。复制是随机过采样的做法;删除多数类属于欠采样;增加特征数量与 SMOTE 无关。
Q3. SMOTE 中寻找 K 近邻的对象集合是?
- A.全体训练样本
- B.全部多数类样本
- C.全部少数类样本← 正确答案
- D.验证集中的样本
答案:C。SMOTE 只在少数类内部找近邻,确保合成样本沿少数类分布方向生成。若在全体样本中找,可能把近邻连到多数类区域,生成“串味”样本;验证集在训练阶段绝不能参与任何处理。
Q4. 在多数类占 99% 的数据上,下列哪个指标能真实反映少数类的表现?
- A.总体准确率 Accuracy
- B.训练集的平均损失
- C.少数类召回率 Recall← 正确答案
- D.样本特征的个数
答案:C。召回率衡量真实少数类中被正确找出的比例,直接反映模型对少数类的敏感度。准确率会被多数类主导而虚高;平均损失只反映整体拟合程度;特征个数与分类表现无关。
Q5. 以下哪些属于欠采样方法?
- A.随机欠采样← 正确答案
- B.Tomek Links← 正确答案
- C.SMOTE 过采样
- D.ENN 近邻清理← 正确答案
答案:ABD。随机欠采样、Tomek Links、ENN 都在减少多数类样本,属于欠采样家族。SMOTE 是合成少数类样本的过采样方法,方向相反。注意 ENN 名为“编辑近邻”,实际删除的是邻居投票不一致的样本。
Q6. 关于 Tomek Links,下列说法正确的是?
- A.删除距离最远的异类对
- B.复制边界上的少数类
- C.互为最近邻的异类对← 正确答案
- D.删除同类样本的噪声
答案:C。Tomek Link 定义为互为最近邻的一对异类样本,删除它们用于清理边界混淆。距离最远的异类对反而互不干扰;复制边界少数类是过采样思路;删同类噪声是 ENN 的职责。
Q7. 使用 SMOTE 之前必须先完成哪一步?
- A.标准化全部特征
- B.划分训练测试集← 正确答案
- C.删除所有多数类
- D.绘制混淆矩阵
答案:B。必须先划分训练集和测试集,并且只对训练集做 SMOTE,否则合成样本会泄漏进测试集,导致评估虚高。标准化可选且应在划分后基于训练集统计量做;删除所有多数类是错误操作;混淆矩阵是评估阶段产物。
Q8. 类别重平衡损失的常见做法包括?
- A.加权交叉熵损失← 正确答案
- B.随机过采样策略
- C.Focal Loss 调制← 正确答案
- D.主成分分析降维
答案:AC。加权交叉熵给少数类更大权重、Focal Loss 用调制因子聚焦难分样本,都属于重平衡损失。随机过采样是数据层面的操作;主成分分析是特征降维,与损失函数无关。
Q9. Focal Loss 中调制因子 (1−p_t)^γ 的作用是?
- A.放大易分样本的贡献
- B.降低易分样本的权重← 正确答案
- C.加速特征提取速度
- D.增加模型参数数量
答案:B。当模型对某样本分得很准(p_t 接近 1)时,(1−p_t)^γ 趋近于 0,该样本损失被压缩;而难分样本 p_t 较小,损失保留较多。效果是让梯度聚焦难分样本,与特征提取、参数数量无关。
Q10. 在类别不平衡场景下,评估模型应优先关注哪些指标?
- A.准确率与训练时间
- B.精确率与召回率← 正确答案
- C.F1 与 PR-AUC 面积← 正确答案
- D.均方误差损失值
答案:BC。精确率、召回率直接刻画少数类表现,F1 是二者调和平均,PR-AUC 对少数类占比敏感,是类别不平衡评估的标准组合。准确率会虚高,训练时间是工程指标而非效果指标,均方误差用于回归任务。