news 2026/10/16 10:06:42

不平衡数据集处理全攻略:从SMOTE到阈值调整

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
不平衡数据集处理全攻略:从SMOTE到阈值调整

这篇打卡笔记的第16天,本来只想随手整理一下"不平衡数据集的处理"这个主题,结果越写越长。起因是前几天帮一位学弟排查一个分类模型,他在某高校的数据挖掘课程里复现了一个异常检测任务,正样本占比不到2%,模型验证集准确率跑到98%,他以为大功告成,结果换到真实测试数据上几乎抓不到正样本。这个场景我太熟了——准确率虚高、少数类全被吞、上线即翻车,基本每个做过不平衡任务的人都经历过。

这篇文章就围绕不平衡数据集的处理展开,重点拆解数据侧重采样、算法侧代价敏感、评估指标校准、阈值调整这几条路线的原理和落地细节,最后是我在项目里踩过的一些坑。适合正在做分类任务的算法工程师、数据分析师,也适合刚入门机器学习、被"99%准确率"骗过的同学。

1. 先认清问题:不平衡数据集的判定不能只看样本比例

1.1 一个最典型的"99%准确率翻车"案例

假设我要做一个设备故障预测,采集了1万个样本,其中正常样本9950个,故障样本50个。我偷懒不处理类别不平衡,模型把所有样本都预测成"正常",那我训练集准确率是多少?99.5%。这个数字在汇报时很好看,可实际业务里真正的目标是把那50个故障找出来,全预测成正常等于这模型毫无用处。

这就是不平衡分类最大的坑——准确率这个指标,在正样本占比极低的时候已经完全失真。模型不需要学习任何规律,只要无脑输出多数类,就能拿到很高的准确率,而你的业务核心恰恰集中在少数类上。

1.2 "不平衡"不只是一个比例问题,更是一个代价问题

很多人一上来就问:正负样本比例多少算不平衡?1:1算平衡,1:10算不平衡吗?我个人的看法是,比例只是表象,真正的判断标准是漏检代价。

同样是1:99的不平衡:

  • 如果是判断一封邮件是不是垃圾邮件,少数类漏掉一些,最多是用户看到几封广告邮件,影响不大;
  • 如果是诊断某种罕见病,漏掉一个正样本就是一次医疗事故;
  • 如果是信用卡欺诈检测,漏掉一笔欺诈交易意味着直接的资金损失。

后面这两种场景,即使比例是10:1,只要少数类的业务价值极高,处理难度就一点不比100:1低。

所以拿到一个不平衡数据集,我建议你第一件事不是急着调模型,而是先和业务对齐一个问题:**一个正样本被漏掉,损失是多少?一个负样本被误判,损失又是多少?**这个答案决定了整个方案怎么选。

常规机器学习算法为什么在少数类上翻车,原因也不难理解:模型在训练时通过损失函数衡量预测和真实标签的差异,多数类样本量大,它们的损失在总损失里占绝对主导。梯度下降过程中,模型只要把多数类拟合好,总损失就能降得很低,少数类那点样本根本"说不上话"。树模型类似,分裂点的选择主要被多数类分布主导,少数类在特征空间里很容易被当成噪声埋掉。

要解决这个问题,常规路线有三条:数据侧重采样、算法侧代价调整、评估侧指标校准。这三条不是互斥的,实际上我在项目里经常两三条叠加使用,后面分别展开。

2. 数据侧重采样:SMOTE及其家族的正确打开方式

2.1 最朴素的过采样和欠采样,为什么都不够用

数据侧最简单的方法就两个:随机过采样和随机欠采样。

随机过采样:把少数类样本复制几份,让它的数量跟多数类接近。这种做法在实践里最大的问题是非常容易过拟合。因为复制的样本没有任何新信息,模型等于在反复学习同一批样本,特征空间里少数类区域被"重复记忆",泛化能力很差。

随机欠采样:从多数类里随机抽掉一部分,让两类数量接近。问题也很明显:你扔掉了一堆可能的有效信息。特别是多数类内部也有各种子模式时,欠采样很容易让模型只看到多数类的"局部",导致整体精度下降。如果原始数据本来就不多,欠采样基本没法用。

那是不是不能用?不是。我的经验是:如果数据集非常大,比如多数类有几十万甚至上百万条,随机欠采样反而是个性价比很高的方案,因为多数类本身严重冗余,丢一部分不影响大局。但如果数据总量不大,或者多数类多样性很重要,就别用纯随机欠采样。

2.2 SMOTE的原理:在少数类样本之间做"合法插值"

SMOTE(Synthetic Minority Over-sampling Technique)是目前最常用的过采样增强方法,核心思想不是复制样本,而是在少数类样本之间合成新样本。

具体步骤是:

  1. 对少数类里每个样本,找到它的k个近邻;
  2. 从k个近邻里随机选一个邻居;
  3. 在"这个样本"和"选中的邻居"的连线上,随机取一个点,作为新生成的样本。

用数学一点的话说:如果少数类样本是a,选中的邻居是b,那么新样本就是a + λ * (b - a),其中λ是0到1之间的随机数。

这样一来,新样本不再是原样本的复制,而是继承了相邻样本的特征,理论上能缓解随机过采样的过拟合问题。目前处理不平衡分类任务,SMOTE系列是我默认的第一选择。

我用的是imbalanced-learn这个库,跟sklearn的接口非常接近。这里要注意一点,新版本的用法是fit_resample:

from imblearn.over_sampling import SMOTE from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) smote = SMOTE(random_state=42, k_neighbors=5) X_train_resampled, y_train_resampled = smote.fit_resample(X_train, y_train) print(X_train_resampled.shape) print(y_train_resampled.value_counts())

注意我在这里只对训练集做了fit_resample,验证集和测试集保持原始分布。这是一个极其关键的细节,后面会单独说。

2.3 进阶变体怎么选:Borderline-SMOTE、ADASYN、SMOTE-ENN

SMOTE虽然好用,但不是万能的。样本分布重叠严重、噪声多的时候,线性插值很容易把噪声也放大。于是又衍生出一批变体。

我按自己的使用频率做了个对比表格:

方法核心思路适合场景主要风险
SMOTE随机选少数类近邻做插值一般不平衡,噪声较少类别重叠时生成模糊样本
Borderline-SMOTE只对"边界附近"的少数类样本做插值少数类内部有明显边界分布边界样本定义对参数敏感
ADASYN按难度加权生成样本,难分类的生成更多少数类内部难度差异大对噪声极度敏感
SMOTE-ENNSMOTE生成样本后再用ENN删除多数类"附近"的噪声噪声多、边界重叠严重计算量大
SMOTE-TomekSMOTE生成后结合Tomek Links清洗边界希望得到更干净的类别边界清洗过度时信息损失

我在实践里的选型经验偏保守:

  • 数据噪声不多,先试标准SMOTE;
  • 少数类都在边界上,试Borderline-SMOTE;
  • 数据特别乱、重叠严重,直接上SMOTE-ENN,因为生成之后再清洗,能去掉不少"假样本";
  • 高维稀疏数据,比如文本TF-IDF特征,SMOTE系列就不太合适,因为稀疏空间里插值会产生大量不合理取值,这种情况我更倾向于用算法侧方案而不是数据侧方案。

2.4 交叉验证里的重采样泄露,这个坑比想象中更隐蔽

我要特意提一下重采样的数据泄露问题。

很多人知道"验证集不能重采样",但很容易忽略另一个场景:交叉验证中,如果先把整个数据集重采样,再做K折划分,就泄露出去了。因为重采样生成的合成样本,等于已经包含了其他折的信息,验证折里的样本可能跟训练折里生成的合成样本有"血缘关系",评估结果会虚高得离谱。

正确做法是把重采样放进每个折的循环内部:每一折先划分训练集和验证集,只对当折的训练集做fit_resample,验证集始终保持原始分布。

from imblearn.over_sampling import SMOTE from sklearn.model_selection import StratifiedKFold from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import f1_score skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for train_idx, val_idx in skf.split(X, y): X_train_fold, y_train_fold = X[train_idx], y[train_idx] X_val_fold, y_val_fold = X[val_idx], y[val_idx] smote = SMOTE(random_state=42) X_train_resampled, y_train_resampled = smote.fit_resample(X_train_fold, y_train_fold) model = RandomForestClassifier(random_state=42) model.fit(X_train_resampled, y_train_resampled) y_pred = model.predict(X_val_fold) print(f"fold F1: {f1_score(y_val_fold, y_pred):.4f}")

我见过不止一个人在这种地方翻车:全局重采样之后再做交叉验证,F1看着很高,换到真实测试集立刻打回原形。这不是模型不行,是评估流程就错了。

3. 算法侧改造:类别权重与代价敏感的落地姿势

3.1 class_weight的本质:让少数类"犯错代价"变大

数据侧重采样的思路是改变样本分布,算法侧的思路则是不改数据,改学习目标——让模型在训练时更重视少数类。

最直接的实现就是类别权重。逻辑回归、决策树、随机森林、XGBoost、LightGBM这些模型基本都支持class_weight参数。它的本质很简单:少数类样本在计算损失时,乘一个更大的因子,相当于人为放大了少数类的"犯错代价"。

sklearn里计算类别权重的默认公式是:

weight = n_samples / (n_classes * np.bincount(y))

举一个直观例子:1000个样本里正类100个,负类900个。

  • 正类权重 = 1000 / (2 * 100) = 5.0
  • 负类权重 = 1000 / (2 * 900) = 0.56

正样本的权重接近负样本的9倍,训练时梯度更新方向就会向少数类偏移。

实际代码里其实不用自己算,直接用class_weight='balanced':

from sklearn.linear_model import LogisticRegression model = LogisticRegression(class_weight='balanced', max_iter=1000) model.fit(X_train, y_train)

3.2 从权重到代价矩阵:两类错误的代价本来就不相等

类别权重虽然好用,但它只做了"少数类比多数类重要"这一件事。真实业务里,少数类内部也不是铁板一块,多数类内部的错误同样有不同代价。

这就引出了代价敏感学习。核心是构建一个代价矩阵:

预测\真实正样本负样本
预测为正0代价FN(漏报)
预测为负代价FP(误报)0

注意我这里故意把矩阵方向写反了,通常查漏报是"真实为正、预测为负",代价远高于误报。在欺诈检测里,漏掉一笔欺诈交易可能损失几万块,误判一个正常用户最多是客服申诉成本。在重症筛查里,漏报堪比事故。

如果业务方已经给了明确代价,比如漏报一次损失10块,误报一次损失1块,那我建议不要只调class_weight,而是把这个代价比例直接算到训练样本权重里去,或者更稳妥的做法是:先不做任何处理训练一个概率模型,然后在阈值选择阶段,用代价函数去搜索最优阈值。阈值这部分我后面单独讲。

3.3 Focal Loss:极度不平衡和难易样本问题

如果你用的是深度学习模型,数据量又很大,可以关注一下Focal Loss。它最早出现在目标检测场景,处理的是"一张图里绝大多数区域是背景,目标只占很少像素"这样的极端不平衡。

Focal Loss在标准交叉熵基础上加了一个调制因子。公式大意是:

FL = -α * (1 - p)^γ * log(p)

其中α是类别权重,γ是聚焦参数,p是模型对真实类别的预测概率。核心逻辑是:对已经预测得很好的样本,降低它的损失贡献;对预测得不好的难样本,保留甚至放大它的损失贡献。这样模型就不会在大量"简单易分"的多数类样本上自我满足。

我的建议是:如果你的模型是用PyTorch这类框架自己训练的深度模型,Focal Loss值得试;如果用的是XGBoost、LightGBM这类梯度提升树,优先考虑scale_pos_weight或者调整样本权重就够了,不太需要自己去实现Focal Loss,树模型本身就是一种对特征空间做分割的方法,损失函数的改造收益没有神经网络那么明显。

4. 评估体系校准:别让准确率成为唯一尺子

4.1 准确率为什么骗人,一个算例讲清楚

这里我用一个最极端的例子帮大家形成直觉:

假设数据集有1000个样本,其中只有1个正样本,999个负样本。

模型A:什么都学不会,只输出"负样本",准确率99.9%。 模型B:模型学了一些规律,能勉强发现那1个正样本,准确率99.8%。

模型A的准确率居然比模型B还高。如果我拿准确率当作模型好坏的唯一标准,就会得到一个荒谬的结论——这个"复读机"模型比真正做了学习的模型更好。

这个例子看着极端,但放到真实的欺诈检测、故障预测里,原理完全一样。多数类占比越大,准确率的欺骗性越强。

4.2 该看什么:Precision、Recall、F1和PR曲线

在不平衡分类里,我默认至少要看这几个指标:

  • Precision(精确率):预测成正类的样本里,有多少是真正类。衡量的是"抓得准不准"。
  • Recall(召回率):所有真实正类里,有多少被模型找出来了。衡量的是"抓得全不全"。
  • F1-score:两者的调和平均,兼顾准和全。
  • PR曲线:横轴Recall,纵轴Precision,曲线下面积叫AP(Average Precision)。

这里我要多说一句PR曲线和ROC曲线的区别。

ROC曲线在正负样本比例悬殊时依然会显得很乐观,因为它的横纵轴同时考虑了正类和负类,负样本多,模型只要把负样本压得好,ROC曲线就会好看。但PR曲线只关注正类的Precision和Recall,它对少数类的变化更敏感。同样是模型从"漏报一半正类"提升到"只漏报两成正类",PR曲线的变化会比ROC曲线明显得多。

所以在不平衡任务里,我的习惯是:ROC曲线可以画,但做模型选择时以PR曲线为主。

4.3 评估指标的Python落地

sklearn里这些指标都有现成实现,直接看代码:

from sklearn.metrics import classification_report, average_precision_score, precision_recall_curve import matplotlib.pyplot as plt y_pred = model.predict(X_val) y_prob = model.predict_proba(X_val)[:, 1] print(classification_report(y_val, y_pred)) ap = average_precision_score(y_val, y_prob) print(f"AP: {ap:.4f}") precision, recall, thresholds = precision_recall_curve(y_val, y_prob) plt.plot(recall, precision) plt.xlabel("Recall") plt.ylabel("Precision") plt.title("PR Curve") plt.show()

如果项目里希望保留少数类的同时控制误报,可以用F1作为优化目标,在验证集上搜索最佳阈值。这个操作本身也比较简单:

import numpy as np precision, recall, thresholds = precision_recall_curve(y_val, y_prob) f1_scores = 2 * precision[:-1] * recall[:-1] / (precision[:-1] + recall[:-1]) best_idx = np.argmax(f1_scores) best_threshold = thresholds[best_idx] print(f"best threshold: {best_threshold:.4f}, best F1: {f1_scores[best_idx]:.4f}")

这里注意precision_recall_curve返回的precision和recall数组长度要比thresholds多1,所以索引要取[:-1],否则长度对不上会报错。

5. 阈值调整与概率校准:有时候不碰数据也能救

5.1 0.5不是天经地义的阈值

很多模型默认把输出概率大于0.5判成正类。这个0.5本质上是"正负两类对称"的假设,在不平衡数据上根本不成立。

举个例子:正样本占比只有1%,模型输出概率0.3。如果按0.5的阈值,这个样本被判成负类;但在多数类占主导的分布下,0.3这个概率已经偏离先验很多了,很可能意味着模型发现了一些"类似正类"的特征,值得仔细考察。

阈值怎么定?原则是回到业务目标:

  • 如果任务要求尽量不放过正样本(比如故障预警),把阈值往低调,比如0.2甚至0.1,Recall会上去,Precision会降下来;
  • 如果任务要求预测成正类的样本尽量准确(比如精准营销),把阈值往高调,比如0.7、0.8,Precision会上去,Recall会降下来。

调阈值本质上就是在Precision和Recall之间做权衡,没有标准答案,只有业务答案。

我项目里常用的一种方式,是在验证集上计算PR曲线之后,根据业务目标定阈值。比如目标"欺诈召回率不低于90%",那就在验证集上找到召回率不低于90%时,最高精度对应的阈值:

target_recall = 0.9 valid_indices = np.where(recall[:-1] >= target_recall)[0] best_precision_idx = valid_indices[np.argmax(precision[valid_indices])] threshold_for_target = thresholds[best_precision_idx]

5.2 概率校准:让模型输出不只是"相对分数"

很多模型输出的概率并不是真实概率,更像一个排序分数。XGBoost和逻辑回归输出的0.8不代表真的有80%的概率是正类。如果我们拿这个概率直接做业务决策,比如估算预期收益,那是有风险的。

这时候可以用概率校准,sklearn提供了CalibratedClassifierCV:

from sklearn.calibration import CalibratedClassifierCV calibrated_model = CalibratedClassifierCV(model, method='sigmoid', cv=5) calibrated_model.fit(X_train, y_train) y_prob_calibrated = calibrated_model.predict_proba(X_val)[:, 1]

method='sigmoid'对应Platt scaling,适合模型概率单调但不够准确的场景;method='isotonic'是非参数校准,数据量大时效果更好,但需要独立验证集,不能直接用在交叉验证里,容易过拟合。

有一点得提醒:校准不会改变AUC、AP这类排序指标,它只改变概率值本身更接近真实概率。如果你的目标只是选阈值、算F1,那校准不校准对阈值选择的影响有限;如果模型输出概率要直接用于成本核算、风险定价,那建议做一下。

而且要注意:做概率校准要单独切一份数据,不能用训练时的交叉验证结果直接校准,否则又会引入乐观偏差。

6. 项目实战中我踩过的坑:一份可以直接照抄的检查清单

6.1 高发坑位一览

我把这几年处理不平衡数据集踩过的坑统一列出来,每一条都是真金白银换来的:

坑表现正确做法
只看准确率验证集99%准确率,上线抓不到正样本改用F1、AP、PR曲线评估
全局重采样再交叉验证内部验证指标虚高,外部测试崩盘重采样只放进训练集,且必须在CV循环内部
欠采样扔掉太多数据结果方差很大,每次训练不一样数据量少时优先过采样或类别权重
稀疏特征上硬套SMOTE生成大量无意义样本,效果不升反降高维稀疏场景改用class_weight或代价敏感
忽略分层采样K折划分后某些折里正样本为0用StratifiedKFold或train_test_split(stratify=y)
阈值死守0.5少数类召回率极低在验证集上用PR曲线搜索阈值
只调采样不调超参调了几轮指标上不去采样方法和模型超参一起网格搜索

6.2 我现在的标准处理流程

经过这些坑之后,我现在拿到不平衡数据集的固定套路大概是这样的:

  1. 先定业务指标。明确漏报一次损失多少,误报一次损失多少,确定优先优化F1还是限定Recall。
  2. 用分层划分切出验证集。train_test_split一定要加stratify=y,交叉验证用StratifiedKFold。
  3. 先跑一个baseline。不做任何处理,用逻辑回归或随机森林先看一版指标,弄清楚当前主要矛盾是分类器太弱,还是数据不平衡导致的偏置。
  4. 小规模试采样/权重方案。数据量小、特征稠密,试SMOTE;数据量大,试随机欠采样或类别权重;树模型优先试class_weight。
  5. 统一用PR曲线和AP评估。记录Precision、Recall、F1、AP,不单独看准确率。
  6. 在验证集上搜最优阈值。根据业务目标选阈值,而不是默认0.5。
  7. 上线前再看一次错误分布。把误差分析里误报、漏报的样本各抽几十个出来人工看一遍,这一步经常能发现特征工程的新思路。

说一个比较反直觉的体会:在很多项目里,特征工程和业务规则的改进,比任何采样算法带来的提升都大。比如故障预测任务里,如果能构造出"最近十分钟内传感器变化率"这类强特征,正负样本的区分度会大幅提升,SMOTE和类别权重反而只是锦上添花。所以别把所有希望都寄托在采样方法上,先问自己:有没有特征是被我漏掉的?

另外一个容易忽略的小技巧是,如果少数类的样本本身质量很差,比如标签标注错误、特征字段大量缺失,那重采样只会把噪声放大。我习惯先做一轮异常值清洗和缺失值处理,再进入重采样环节。我的经验是,对于不平衡数据,干净的小样本通常好过污染的大样本。

最后说一句:做不平衡分类没有所谓"万能模板"。有些项目里SMOTE效果显著,有些项目里纯调阈值就够用了,关键是你得建立一套自己的评估和判断流程。这篇笔记算是给我第16天打卡的一个阶段总结,如果对你也有一点点帮助,那今天的整理就没白写。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 10:05:50

从零手搓SNMP温湿度采集:OID定位、MIB解析与Python自动化实战

1. 为什么我要从零手搓一套SNMP采集机房动环监控这个事,说大不大,说小不小。你如果只是盯着几台服务器看温度,那随便找个带屏幕的温湿度计往机柜上一贴就完事了。但一旦机柜数量上到十几个、几十个,分布在不同的楼层甚至不同的楼宇…

作者头像 李华
网站建设 2026/10/16 10:05:18

AmcacheParser:Windows终端取证中Amcache.hve程序痕迹解析工具

终端取证响应工具之 Windows Amcache 文件解析工具 AmcacheParser 搞过 Windows 终端取证的朋友都知道,拿到一台涉案机器或者做应急响应,第一件事就是找程序执行痕迹。系统里能留存程序执行记录的地方不少,比如 Prefetch、UserAssist、ShimCa…

作者头像 李华
网站建设 2026/10/16 10:04:18

Qt5无人机地面站地图交互实战:坐标转换、瓦片加载与航线绘制

简介:这是一份面向Qt开发者与无人机地面站方向学习者的实战型源码资源,基于Qt5与opmapcontrol组件实现互动地图与地面站核心效果,支持谷歌、必应、雅虎、GIS等多厂商地图切换,适合具备一定C与Qt基础、希望深入地图交互与航点管理的…

作者头像 李华
网站建设 2026/10/16 10:04:14

思H264解码库实践:软解选型、参数配置与性能调优

简介:这是一份面向C# WinForm开发者的海思H264解码库资源包,适用于视频监控客户端、本地播放器等需要调用原生解码库实现H.264视频流解码的场景,可为具备一定C#基础的中级开发者提供从库集成到画面显示的完整参考。压缩包共9个文件&#xff0…

作者头像 李华
网站建设 2026/10/16 10:03:52

CTF热身签到题解题指南:从查源码到拿flag的完整思路

一次偶然的机会,有位刚开始接触CTF的朋友问我“热身签到-ctfshow”这道题到底该怎么入手,他说自己连题目界面都没看明白。这让我意识到,很多新人面对这类入门题,并不是不会技术,而是不知道CTF解题该从哪里开始想。我决…

作者头像 李华