样本不平衡二分类实战:从重采样到阈值调整的完整方案
发布时间:2026/10/22 8:54:29 锦皓数字建站

做二分类的同行应该都有这种体会模型在验证集上收敛得很漂亮准确率刷到95%以上上线跑了两周业务方跑来问一句“你把正样本召回几个”打开混淆矩阵一看少数类几乎全军覆没。这就是典型的样本不平衡问题。我在好几个项目里踩过同样的坑——客户流失预警、设备故障诊断、营销响应预测凡是正样本占比低于10%的任务几乎无一例外会遇到这个坎。所谓样本不平衡指的是二分类任务中两个类别的样本数量相差悬殊少数类占比常常只有5%甚至1%。如果不做任何处理模型会倾向于把一切都预测为多数类因为只要这样整体准确率就能保持很高的数值但这对业务没有任何意义。这篇文章我会从数据侧、算法侧、评估侧三个方向把我在实际项目中用过的重采样、代价敏感学习、阈值调整、指标重构这些方法逐个拆开讲包括每一步的动机、代码实现、优缺点以及那些常规文档里不会写但实际特别坑的细节。无论你是刚入行的数据分析师还是已经有一段时间经验的算法工程师这套方法论都可以直接搬到你的项目里用。1. 先诊断再开药样本不平衡问题的本质与影响边界样本不平衡的根源其实不复杂但很多人上来就套重采样方法结果反而把模型搞得更差。我在项目里的习惯是先花半天时间确认三件事不平衡到底有多严重、少数类样本是否有代表性、业务上对“漏报”和“误报”的容忍度如何。这三件事直接决定了后续方案的选型方向比盲目调参重要得多。1.1 不平衡程度分级与应对原则我把不平衡程度分成三档处理方式完全不同。第一档是轻度不平衡少数类占比在20%到40%之间这时大多数模型都能正常工作顶多把class_weight稍微调整一下就能解决不需要大动干戈地做数据增强。第二档是中度不平衡占比在5%到20%之间这时模型开始出现明显的偏向性需要结合重采样或者代价敏感方法。第三档是重度不平衡占比低于5%比如信用卡欺诈、罕见病筛查这类场景常规手段几乎全部失效需要设计更精巧的方案包括合成数据、单类学习、异常检测思路等。判定方法很简单直接看训练集里类别计数的分布再用collections.Counter或者pandas的value_counts统计一下就行。我见过有团队在不平衡程度只有轻微偏差的情况下就启动SMOTE结果生成了大量重叠样本模型反而过拟合了。这里的原则就是——先量化再动手。1.2 固有性不平衡与采样偏差的区分还有一种细分需要特别警惕有些场景中的不平衡是客观存在的比如产品故障率本就极低而有些场景中的不平衡是数据采集方式造成的比如某个渠道的样本被过量采集。前者要从模型和方法层面解决后者应该先回去修正数据采集策略。如果分不清这两者很容易把时间花在造数据上但实际上真正需要解决的是采样口径问题。我在某个跨平台系统项目里就遇到过这种情况线上点击行为日志本身正负比例接近均衡但因为存储策略只保存了某几类用户的记录导致离线样本里负样本异常多。后来调整了数据刷选逻辑不平衡程度直接从1:50降到了1:3后续建模轻松了很多。所以在做任何技术处理之前先跟业务和数据管道的人确认一下样本来源有时候答案根本不在算法层。2. 数据侧方案重采样策略的选型与实操要点数据侧处理是样本不平衡中最直观的思路样本多了就砍掉一些样本少了就补上一些或者干脆人工构造一些。听起来简单实际做起来有很多细节。我在这个章节把随机欠采样、随机过采样和SMOTE系类方法都拆开讲包括代码和适用边界以及我自己总结的避坑经验。2.1 随机欠采样与过采样的适用边界随机欠采样的逻辑是从多数类中随机抽取一部分样本使其数量与少数类匹配。优点是训练时间变短、内存占用变小很多基于深度学习的场景里这是一种很务实的做法缺点是直接丢弃了大量可能有用的多数类样本如果多数类内部本身有复杂的子结构模型容易出现高方差问题即每次随机抽的样本不同训练出来的模型差异很大。随机过采样则是把少数类样本复制多份直到数量与多数类匹配。这个方法没有丢失信息但风险也很明显模型会把重复样本当作独立样本去学习容易过拟合到少数类的特定样本上。我在做设备故障诊断项目时把故障样本复制了8倍训练集损失曲线很漂亮一测试集上召回率反而不升反降。原因就是模型背下了那几个重复样本的“标准答案”遇到稍微偏移的故障模式就不会泛化。所以这两类方法我只会作为快速基线来用真正比较稳的是下面要说的SMOTE。2.2 SMOTE与边界变体的原理和实现SMOTE的全称是Synthetic Minority Oversampling Technique核心思路是在少数类的样本之间进行插值生成新的合成样本而不是简单复制。具体做法是对每一个少数类样本找到它的K个近邻通常是K5然后从这些近邻中随机挑一个在连线上随机取一点作为新样本。这一步最关键的地方在于新样本不是凭空捏造的而是基于少数类自己的分布特征生成所以能有效缓解过拟合问题。代码实现非常顺手因为有一个现成的库叫imbalanced-learn接口风格和scikit-learn高度一致。下面是我经常使用的一个SMOTE应用片段from imblearn.over_sampling import SMOTE from sklearn.model_selection import train_test_split # 先把数据切好再对训练集做合成千万不要先合成再切分 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy) smote SMOTE(random_state42, k_neighbors5) X_train_res, y_train_res smote.fit_resample(X_train, y_train) print(重采样前后训练集规模, X_train.shape, -, X_train_res.shape)注意我特意在代码里加了个注释一定要先切分训练集和验证集再对训练集做SMOTE。这是新手最容易犯的错误——如果先对整个数据集做合成再切分那么验证集和训练集里会混入来自彼此的样本信息评估结果会虚高得离谱。在真实的线上评估中这种虚高没有任何意义因为这个信息泄漏会让调参方向失真。SMOTE本身也有一些变体值得了解。Borderline-SMOTE专门挑那些处于类别边界的少数类样本做插值因为这些样本往往代表了最难以区分的区域对提升分类边界帮助最大。ADASYN则会根据每个少数类样本的学习难度动态决定生成数量对难分类样本周围生成更多样本。在实际项目中我对中度不平衡数据用标准SMOTE就足够了但如果是重度不平衡Borderline-SMOTE的表现通常会更好。2.3 过采样与欠采样的组合策略还有一个实践心得当数据集比较小时单独用SMOTE容易让生成样本过于密集配合一些欠采样操作往往效果更好。imbalanced-learn有一种组合叫SMOTEENN先做SMOTE合成少数类再用Edited Nearest Neighbours清理那些相互重叠的样本还有SMOTETomek结合了Tomek Links清理策略。这两种组合方法在数据混杂度高的时候很有用。我在处理营销活动响应数据时原始数据里正样本占比只有3%左右单独用SMOTE拟合后F1在0.42左右换成SMOTEENN之后F1升到了0.55。原因在于SMOTE生成的样本中有不少落入到多数类密集区域ENN相当于把这些“混血样本”清理掉边界变得更干净。但要注意清理过程中也可能误删有效样本所以需要配合测试集做效果对照不要一味追求指标数字。数据侧方法的另一个共性问题是可解释性。如果业务方对模型有审计或解释需求用SMOTE生成的样本可能难以追溯到真实来源。这个问题在金融风控领域尤其敏感。我在复盘时通常会单独记录哪些样本是合成的分析它们的分布特征并对比“只用真实样本”和“配合合成样本”两种方案在关键指标上的差异以确认合成并没有带来幻觉式的偏差。2.4 数据侧操作禁忌清单用了这么多年数据侧方法我总结了几条经常被忽略的禁忌简单列一下禁止对验证集和测试集做任何形式的过采样或SMOTE它们必须保持原始分布否则评估结果全面失真。禁止在交叉验证前就一次性生成全部合成样本应该把SMOTE放进每个折的训练流程里。禁止盲目地把少数类合成到与多数类完全相同很多时候合成到1:2或1:3的效果更好。禁止忽略数据重复检查如果原始数据里已经有大量完全相同的样本SMOTE容易在重复点附近生成一堆无效样本。3. 算法侧思路代价敏感学习与阈值调节的实战方案如果说数据侧方案是从“喂给模型什么数据”的角度解决问题算法侧就是从“如何学习、如何判定”的角度做文章。这部分包括样本权重、代价矩阵、以及分类阈值的调优。其中阈值调整是我个人认为性价比最高的手段甚至有时不需要重采样只调阈值就能让业务指标获得显著提升。3.1 类别权重与代价敏感损失函数的本质代价敏感学习的核心思想是少数类的分类错误比多数类的分类错误更“贵”所以要让模型在训练时对少数类的错误付出更高代价。这种代价最直接的体现方式就是给少数类样本一个更大的权重。scikit-learn里很多分类器都有class_weight参数XGBoost里对应的是scale_pos_weightLightGBM里对应is_unbalance或scale_pos_weight。这里有件事我要说清楚class_weight的本质其实是改变了训练时的损失函数让少分类样本的错误梯度被放大从而让模型更倾向去学少数类。但它并不会改变实际预测概率的分布所以通常还要配合阈值调整二者结合效果才最好。我在项目里常常先设置class_weight让模型学到少数类的模式然后用验证集重新寻找判断阈值。3.2 阈值调整无论是谁都能立竿见影的步骤很多分类器输出的其实是一个概率然后默认以0.5作为判断正负类的分界线。但在不平衡数据里0.5几乎总是一个不好的阈值。为什么因为少数类样本的先验概率远远低于0.5模型在预测时很难给出很高的概率值。正确的做法是在验证集上画曲线根据业务需求找到最优阈值。具体的操作流程是这样的训练模型后用predict_proba得到每个样本属于正类的概率然后在一系列候选阈值从0到1每隔0.01或者更密上分别计算精确率、召回率、F1等指标找到最优的那一个点之后用这个阈值替换默认的0.5。from sklearn.metrics import precision_score, recall_score, f1_score import numpy as np # 假设已经拿到了验证集真实标签 y_true 和模型预测概率 y_prob best_threshold 0.5 best_f1 0.0 for threshold in np.arange(0.1, 0.9, 0.02): y_pred (y_prob threshold).astype(int) current_f1 f1_score(y_true, y_pred) if current_f1 best_f1: best_f1 current_f1 best_threshold threshold print(f最优阈值: {best_threshold:.2f}, 最优F1: {best_f1:.4f})这段代码非常朴素但它确实解决过很多实际问题。有一次我在某公司里给推荐系统做用户响应模型默认0.5阈值下F1只有0.3把阈值降到0.2以后F1直接提升到0.57而模型本身一行代码没改。原因就是正常用户点击概率本身就低0.5的门槛设得太高了。3.3 阈值选择的业务导向权衡阈值怎么选不要只盯着F1看还要看业务上的代价。比如在医疗筛查场景漏诊的代价远高于误诊那么就应该把阈值调低一些宁可多召回一些可能误判的样本也不放过任何一个真患者。而在垃圾消息过滤场景误判正常消息为垃圾消息的代价则更高这时候阈值要调高精确率优先。所以要弄清楚模型的输出是给谁用、用在哪里。我发现不少团队在训练阶段花很多时间调模型结构却对预测之后的那套决策逻辑关注不够。实际上阈值调好了模型先天的分类能力也能被充分发挥出来可能比换一个花哨模型更有效。3.4 代价敏感方法在工程中的数据形态问题还有一个经常被忽略的工程细节在sklearn里使用class_weightbalanced时权重会自动根据样本频率计算但如果你需要特殊设定也可以传一个字典。我建议传入自定义权重而不是默认的balanced因为默认权重是基于全局类别比例的当某段时间内比例发生变化模型行为会随之波动。传了一个稳定权重后模型在一个较长时间周期内会更可控。例如在用户流失预警里业务方对召回的要求是至少65%那么我会把权重设成使模型“偏激一些”的方式再在阈值选择时以“满足召回同时最大化精确率”作为条件。这个组合方式比单纯堆模型参数更稳定也更加适配业务目标。4. 评估指标选型别再被准确率指标掩盖真相很多项目出问题一个重要原因是选错了评估指标。准确率在不平衡场景下会骗人当正样本只占1%时一个把所有样本都判成负类的模型准确率依然能达到99%。这个数字看起来亮眼但模型对业务毫无帮助。所以要建立一套适合不平衡场景的评估体系。4.1 混淆矩阵四要素的实际业务含义先看混淆矩阵。预测为正且实际为正的是真正例预测为负但实际为正的是假负例预测为正但实际为负的是假正例预测为负且实际为负的是真负例。这四个数字是精确率、召回率、F1等指标的原料。精确率等于真正例除以所有被预测为正的样本衡量的是“报出来的正例有多少是真的”召回率等于真正例除以所有实际为正的样本衡量的是“真正的正例被找回了多少”。这两个指标天然就是一对矛盾追求召回率就必然牺牲一些精确率追求精确率就容易漏掉更多正例。我在给非技术背景的同事解释这两个概念时喜欢拿报警器做类比精确率高意味着报警器响的时候大概率真有事召回率高意味着有事的时候报警器基本上都会响。前者管“别误报”后者管“别漏报”至于选哪头取决于业务上哪种错误更难受。4.2 F1、PR曲线与AUC的选择策略F1是精确率和召回率的调和平均在不平衡场景里是一个比准确率可靠得多的单值指标。但F1有个弱点它依赖具体的判定阈值。所以更稳妥的做法是看P-R曲线下面积即PR-AUC。PR曲线在正负样本极不平衡时比ROC曲线更敏感因为ROC曲线假正例的占比是被所有负样本归一化后的负样本一多假正例的影响就被稀释了导致曲线看起来很好实际上模型对少数类的区分能力差得感人。具体选择时可以参考这个经验如果业务更关注少数类的绝对识别能力比如风控中的欺诈样本就用PR-AUC来做模型对比如果业务更关注整体的排序能力比如广告点击率的CTR预估ROC-AUC更常用。但最终以滚动业务目标为标准的还是要在验证集上回归精确率和召回率的需求组合。4.3 业务指标与模型指标的挂钩方法我在实际项目中会做一个简单的转化动作把混淆矩阵的四个数翻译成业务语言。比如某个故障预测模型真正例意味着成功预警的故障次数假负例意味着漏掉的故障次数假正例意味着白跑一趟的检修单。然后用业务字段——比如“避免停机损失”和“多付出的检修成本”——计算一个偏业务化的指标Y。这个Y才是我们真正应该优化的目标。定完这个业务指标之后不管是选模型、选阈值还是选重采样策略都可以围绕它来评估。有一段时间里我甚至把模型优化目标直接设成召回率的硬约束条件比如“召回不得低于90%在此前提下最大化F1”。这种约束式优化比单纯“最大化F1”更贴合实际至少做出来的模型不会在该保命的时候沉默。5. 模型策略与集成方案那些真正适合不平衡场景的建模方式除了处理内容和评估建模阶段本身也存在一些更适配不平衡场景的思路。比如可以换用天然支持类别不平衡的模型或者用集成策略把多个偏置模型合并起来。这个部分里面有很多心得是读论文学不到的主要靠一次一次踩坑摸出来。5.1 哪种基础模型更适合不平衡任务以我自己的经验来看基于树的模型在类别权重策略下往往表现不错尤其是梯度提升树模型因为每棵树的节点分裂都自带处理离散特征和缺失值的逻辑配合类别权重能较好地覆盖少数类的模式。逻辑回归这类线性模型也不是不能用但前提是特征工程能把少数类的分布特点刻画清楚。朴素贝叶斯在重度不平衡场景下有时表现意外地好因为它对概率的估计方式天然更偏向依据各特征的条件概率而不会被各类别数量带跑得太远不过这也要看数据形态是否符合条件独立假设。如果是深度学习模型处理思路会不一样比如难样本挖掘、Focal Loss等。但这些方案在中小型表格数据场景里一般不是首选我更倾向于用XGBoost或LightGBM做基线等确认这些传统增强树解决不了的时候再上复杂网络。5.2 单模型与集成在重踩方案里的综合对比如果重采样的目标是让多个模型平均出一个综合结果那么较简单的做法是“Bagging 随机欠采样”。思路是每次从多数类里随机抽与少数类相同数量的样本配上全部少数类样本训练一个基学习器重复做很多轮最后对多个基学习器的输出做平均。这样既保留了多数类多一些信息又引入了多样性不易过拟合。这种做法比直接全局欠采样要稳很多在中等数据规模下相当好用。如果数据量够大也可以尝试“先聚类再抽样”的策略把多数类聚成若干簇然后按簇比例抽取样本。这个方法能避免简单的随机欠采样把某些重要情况整类丢掉在多样性的业务数据里常常有意外收获。5.3 调参时的实际步骤与观察要点在调参上我的建议是不要太早堆模型复杂度。先跑一个默认参数的模型得到混淆矩阵和几个关键指标作为基线再渐进加入类权重、重采样和阈值调整。每一步都看同一套曲线和表格确认每一步带来的都是正向变化而不是A指标涨了B指标崩了。有一次我调了很久结果发现光是把树的max_depth从6降到4F1反而比上所有招式都好——欠拟合状态下的模型反而更能看到真实的数据模式。在这个过程里控制实验的顺序也很重要先数据侧再代价敏感再阈值最后再叠模型结构。这个顺序能保证每一步的可归因性定位到底是谁在带来收益谁在拖后腿。6. 完整工作流搭建与高频踩坑问题清单最后这部分是全文的实战汇总。我把一个标准的样本不平衡二分类项目的完整流程完整走一遍从数据划分到结果评估然后附上几个高频遇到的坑和排查方法。这部分更像是一份快捷参考手册适合直接把项目部署时对照着看。6.1 完整流程从切分到评估的七步工作流先看总体流程我按序列成七步用分层抽样划分训练集、验证集和测试集确保每个集合中的正负比例都与原始数据一致。统计训练集正负样本数量判断不平衡程度并分析少数类样本的质量和代表性。在训练集上做重采样策略实验建议先试SMOTE再看是否需要组合欠采样。设置类别权重配合重采样或单独使用看哪一版在验证集上表现更稳。训练模型后用验证集概率寻找最优决策阈值并依据业务目标调整。在完全未经处理的测试集上评估最终指标对比几组候选方案的业务字段数值。记录全过程输出一份模型行为说明和阈值使用说明给业务方。第7步很多人嫌麻烦会省掉但如果你是给团队或客户交付这条绝不能省。有一次项目交付时对方问“阈值为什么是0.27不是0.5”如果没有当时记录下来的那套分析过程现场会很难看。6.2 高频问题排查分层、泄漏与状态漂移下面这些问题几乎每次出现在样本不平衡项目里我列成表方便对照现象可能原因排查与解决离线指标很高线上效果差重采样或特征工程时发生了数据泄漏检查是否先切分再重采样、有无用全量数据做缩放/编码多次运行结果波动大数据划分或采样过程没有固定随机种子统一设置random_state并在文档中记录下来召回率提不上去少数类和多数类重叠严重或特征信息不足增加特征或转向异常检测思路精确率太低误报很多阈值过低或合成样本失真适当上调阈值检查SMOTE生成样本的邻域重叠情况训练集损失不下降类别权重设置过高梯度不稳定降低少数类权重或使用较温和的scale_pos_weight线上数据分布随时间偏移样本采集口径变化定期监控少数类比例必要时重训或重新采样还有一个高频问题容易被忽视模型上线后少数类比例变化了但阈值还是旧的。从运营第一天开始就要建立监控套餐定期统计线上实际预测的正例比例。如果预测出来的正例占比与训练阶段相差太多往往意味着数据分布已经漂移需要重训或调整阈值。6.3 写到最后的一些工程经验关于工作流我有几个具体工程经验想补充。第一所有数据预处理模块写成函数输入是X_train, y_train输出是重采样后的训练数据保证在交叉验证里能复用。第二记录实验日志时不仅记录指标也要记录阈值和权重设置这能节省后续大量沟通成本。第三对少数类样本的批次来源打标签当发现模型在某一批来源上的表现异常时可以快速定位是该来源的数据质量问题还是模型泛化问题。另外忍不住说一句不要试图一次性把所有技巧都堆到模型里。每次只加一个变量用同一套测试集看效果。重采样、类权重、阈值、集成策略拆开看每一步都有用但叠在一起不一定产生协同甚至可能互相削弱。我的习惯是先跑一个“原始模型阈值调整”作为标配基准再逐项叠加其他方案每叠加一个都做完整的记录和对比。这组工作流我目前已经沿用了超过两年虽然每一步看起来都不算高深但组合在一起足够应对大多数二分类不平衡场景。很多时候你需要的不是更花哨的算法而是把这些基本功按照正确的顺序拼起来再配上一套靠谱的评估方式。最后再分享一个小技巧。如果你最终交付的是一个需要长期运行的服务务必在代码里保留一个备用预测入口——直接输出概率而不是只输出最终的类别标签。这样当业务目标发生变化、需要更换阈值时不需要重新训练模型只要能对历史概率重新切一刀就能快速给出新的预测结果。这个入口非常小但在关键时刻会让你少掉一整轮的开发周期。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。