资讯详情

资讯详情

数据不平衡实战指南:从SMOTE采样到阈值优化,拒绝被准确率欺骗

做机器学习这几年数据不平衡是我遇到频率最高、也最容易翻车的问题之一。不管是信贷风控里识别坏客户还是设备预测性维护里抓故障样本都逃不开一个局面你真正关心的那个类别在数据集里只占极小比例。我第一次踩坑时也天真地把原始数据直接丢给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合成少数类样本的原理与边界SMOTESynthetic 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_size0.2, stratifyy, random_state42 ) pipeline Pipeline([ (smote, SMOTE(sampling_strategy0.5, random_state42)), (clf, RandomForestClassifier(n_estimators200, random_state42)) ]) pipeline.fit(X_train, y_train) proba pipeline.predict_proba(X_test)[:, 1]这里有个容易踩的细节如果用sklearn自带的Pipeline重采样步骤会在transform阶段作用到测试集上导致测试集也被SMOTE“污染”。imblearn的Pipeline专门处理了这个问题重采样阶段只会作用于训练集的输入X和标签ypredict阶段什么都不改。所以里面部分方法不要随便换成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或改用SMOTETomek清洗测试集效果突然暴跌重采样泄漏到测试集或验证集确认使用了imblearn的Pipeline检查切分顺序accuracy很高但recall很低模型全预测成多数类换成PR指标评估调低阈值检查class_weight少数类样本被大量误报权重过高或阈值太低降低少数类权重提高判定阈值多分类中个别类效果差类间样本量差异过大对每个类单独计算权重或逐类做采样类别型特征为主时SMOTE效果差插值生成了不存在的类别改用SMOTE-NC或直接依靠class_weight和集成5.3 实操心得踩过的坑和推荐做法先说采样比例。很多论文里都用1:1完全平衡我一开始也照抄过结果F1反而下降了。后来才想明白合成样本虽然有用但毕竟是“造”出来的当它数量超过真实样本时模型的分布认知就开始偏离真实。更稳的做法是把少数类调成多数类的30%到50%让模型既重视少数类又不会因为数据太假而失去对多数类的基本建模能力。这个结论我在好几个推荐场景和风控场景里都验证过。再说数据泄漏。有一次我做交叉验证参数搜索图省事把SMOTE放在整个数据集上先跑了一遍然后再去做K折切分最后验证集F1高得离谱模型上线后效果直接崩了一半。这就是典型的重采样泄漏验证集里混入了训练折的合成样本评估结果完全失真。从那以后我养成了习惯凡是涉及重采样的实验一律用imblearn的Pipeline包进每一折内部绝不手动分开做。最后分享一个小技巧上线后的概率校准。模型调完阈值之后我还要在线上样本上观察预测概率的分布是否偏移了因为生产环境的数据分布和训练期很难保持一致。如果不定期监控少数类带来的概率漂移前面的采样和调参功夫很容易被新数据冲淡。这一点在长期运营的业务场景里比什么都重要。数据不平衡本身不是洪水猛兽它更像是一面镜子逼着你回到业务目标上去思考“到底该让模型最大化什么”。把评价指标、损失权重、采样策略和阈值全部对齐到业务成本上你会发现模型的效果提升往往比换一个更强的算法来得更踏实。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →