做二分类模型的人,十有八九都会撞上样本不平衡:正样本几万条,负样本才几百条,模型“聪明”到直接全预测成正样本,准确率还高得吓人。我在实际项目里吃过不少这种亏,信贷违约、工业质检、异常流量识别,几乎每个场景都要先过这道坎。今天这篇不打算罗列技巧,而是把我解决问题时的完整分析链拆开讲,从失衡类型诊断、评估指标选择,到采样方法、模型改造和实战排坑一步步走通。如果你已经会跑分类模型,但一遇到这类数据就心里没底,直接看这篇就够了。
1. 先别急采样:把失衡的“类型”和“水位”摸清楚
很多学习者拿到不平衡数据,第一反应就是“上SMOTE”或者“调class_weight”。我的建议是,先忍一忍。因为你连失衡到什么程度、少数类到底有多少有效样本都没搞清楚,后面的方法大概率是乱枪打鸟。这一节不聊具体手段,先把诊断做完。
1.1 失衡水位怎么定,对应的处理重心完全不同
样本不平衡不是只有一种形态。从“轻度不均衡”到“极度不均衡”,处理思路差异非常大。我一般会先把比例量化出来,再决定采用什么策略。
| 失衡程度 | 常见正负比例 | 处理重心 |
|---|---|---|
| 轻度失衡 | 1:2 至 1:10 | 多数情况下不采样,设一下类权重就够 |
| 中度失衡 | 1:10 至 1:50 | 可以考虑SMOTE或组合采样,配合阈值移动 |
| 重度失衡 | 1:50 至 1:200 | 需要组合方法,比如SMOTE-ENN或集成方案 |
| 极端失衡 | 超过1:200 | 单纯分类器往往不是最优解,要考虑异常检测思路 |
为什么要分这么细?因为不同水位下,样本噪声的破坏力不一样。轻度失衡时,少数类的模式仍然能被模型学出来,你只需让损失函数稍微关注少数类即可。到了重度失衡,少数类的信息被大量多数类淹没,随机采样引入的噪声会被放大,需要更精细地筛选和生成少数类样本。到了极端失衡,比如反欺诈场景中正常交易和欺诈交易可能达到1:1000以上,少数类内部模式极其稀疏,这时哪怕是SMOTE生成的点也未必能覆盖真实分布,继续死磕标准分类器反而会绕远路。
1.2 少数类绝对量太小,再多的方法也难变出信息
比例只是一个维度,我还特别关注少数类的绝对数量。假设你有10万样本,正负比1:100,那就是大约1000个正样本。如果正样本只有几十个呢?情况和前者完全不同。
绝对数量小意味着你在交叉验证时会遇到一个尴尬局面:训练集里的正样本怎么切都切不够,测试集可能只有十几个正样本,模型输出结果稍微变一下,召回率就从0.6跳到0.9,评估指标根本不稳定。这种情况下,该优先考虑的不是精致的采样算法,而是能不能补充标注、引入外部数据,或者通过业务规则先筛出高置信度的负样本,把问题规模降下来再谈建模。
我在实操中见到过太多反面案例:少数类就80条,硬生生用SMOTE生成到1600条,模型在验证集上F1漂亮得不行,上线后新来的少数类形态稍有变化,直接全线崩盘。原因很简单——插值只能在线段上做文章,生成不了真正新的知识。
1.3 失衡背后的成因,影响你选哪个方案
失衡的成因大致分成两类:一类是天然分布如此,比如罕见疾病在筛查人群中的发生率就是很低;另一类是采样偏差,比如你发布了调查问卷,老年用户很少填,导致样本比例失真。
天然分布不均的情况下,你要做的是让模型适应这个分布,并通过阈值控制去平衡精确率和召回率。采样偏差则需要先修正采集过程,而不是靠算法硬掰。如果是因为数据漏标导致某一类看起来特别少,优先去清洗数据,否则在脏数据上做任何重采样都是在放大错误。
我习惯在项目开始时把这个判断写进备忘录:失衡成因是什么、是否可通过采集侧修正、是否需要考虑时间维度上的漂移。很多模型上线后效果退化,不是因为算法不好,而是因为“缺失少数类”的成因变了,但模型还停留在旧逻辑里。
2. 评估体系先立住:准确率在不平衡场景里基本是骗人的
在模型还没训练之前,先把评估指标定下来。如果你继续用准确率(Accuracy)做评估,那后面对“样本不平衡问题已经解决”的判断就是海市蜃楼。多数类占99%时,模型什么都不学,全预测成多数类也能拿到99%准确率,场面看着风光,实际毫无用处。
2.1 看一眼混淆矩阵和三个关键指标
我拿到不平衡数据后的第一件事,是把混淆矩阵当作仪表盘来用。模型输出四类数字:真正例(TP)、假正例(FP)、真负例(TN)、假负例(FN)。只看准确率远远不够,重点关注三个衍生指标:
- 精确率:预测为正类的样本中,真的正类占多少。
- 召回率:真实的正类样本中,模型能召回多少。
- F1分数:精确率和召回率的调和平均,能在两者之间做一个平衡。
这三者的业务含义比任何抽象公式都直白。比如信贷违约识别,如果你担心放过坏账,就盯召回率;如果你担心误杀太多好客户,就盯精确率。指标选择不是数学问题,是业务成本问题。我的原则很简单:先把业务代价换算成指标偏好,再让模型去优化这一个目标。
2.2 为什么我更依赖PR曲线而不是ROC
很多教材喜欢讲ROC曲线和AUC值,但在一类别明显不平衡时,PR曲线通常更能反映模型的实际水平。核心区别在于:ROC曲线横轴是假正例率,纵轴是真正例率,它受到数量占优的负样本影响比较大;PR曲线横轴是召回率,纵轴是精确率,直接聚焦正类的表现。
举个例子,一个极不平衡的数据集里,负样本很多,哪怕模型产生大量误报,假正例率也可能很低,ROC曲线看起来依然优美,AUC值照样高。但PR曲线会把这类误报毫不留情地暴露出来,精确率会明显掉下来。这就是为什么我在论文复现和真实项目里,大都以PR曲线、F1、以及带业务约束的召回率为主要评估对象,ROC作为辅助参考。
2.3 分层交叉验证和业务阈值设定
选择交叉验证方式时,需要注意不能使用普通的随机K折。不平衡数据里少数类本来就不多,随机划分很可能让某一折一个正样本都没有,训练和评估都会失真。建议使用分层K折(StratifiedKFold),保证每一折的正负比例与整体分布一致。
除了划分方式,还要明确一个原则:所有阈值调整、超参搜索,都应该在验证集上完成,测试集只能用于最终评估。我自己会把数据拆成训练、验证、测试三份,训练集做重采样和模型拟合,验证集用来搜索阈值,测试集只碰最后一次。这么做可以尽量避免信息泄漏带来的虚高表现,后面我会专门讲这个坑。
3. 数据层面放大招:采样方法的取舍与组合拳
当失衡水位明确、评估指标定好之后,数据层面的采样才正式开始。采样方法分为欠采样、过采样和混合采样。它们各有适用场景,你需要能选到合适的组合。
3.1 随机欠采样和随机过采样:效果有限但胜在简单
随机欠采样是从多数类里随机丢弃样本,把正负比例拉到接近1:1。直观高效,但信息损失严重,尤其当多数类本身也不多时,丢掉大量负样本会导致模型视野变窄。随机过采样则是简单复制少数类样本,让模型重复看见相同点,容易过拟合,而且它只是放大已有特征,并没有增加真正的多样性。
这两个简单方法现在仍然有用,但更适合作为快速基线。比如在百万级数据上先跑一个回归测试,我会用随机欠采样快速验证特征是否有效。基线确认后,再投入更复杂的SMOTE类方法,避免前期消耗过多算力。
3.2 SMOTE类方法到底在干什么,边界在哪里
SMOTE的核心思想是:对每个少数类样本,找出它的K个近邻(通常是K=5),然后在少数类样本和随机近邻的连线上,随机生成新的样本点。这样生成的样本不是简单复制,而是像在特征空间里做插值,从而缓解过拟合问题。
这种做法的前提假设是,少数类样本在特征空间里分布相对连贯,近邻之间确实存在真实可插值的结构。如果少数类样本高度稀疏、被多数类分割成孤立的小岛,或者特征空间严重离散,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_size=0.2, stratify=y, random_state=42 ) # 只在训练集上做SMOTE sm = SMOTE(sampling_strategy=0.6, k_neighbors=5, random_state=42) X_train_res, y_train_res = sm.fit_resample(X_train, y_train)sampling_strategy不一定要设成1.0,通常设到0.5或0.6就够了,也就是少数类占多数类的50%到60%,没必要完全平衡。完全平衡有时会使决策边界过度偏向少数类,线上反而误报严重。
3.3 我更常用的组合策略与顺序提示
单项采样都有局限,实际项目里我多数时间走的是组合路线。最常用的两个组合是:
- SMOTEENN:先用SMOTE生成少数类样本,再用 edited nearest neighbours 把那些类别混乱、边界模糊的噪声样本清洗掉。
- SMOTETomek:先用SMOTE扩样,再用Tomek Link找出互相靠近但类别相反的样本对,并删除其中更容易引起边界重叠的样本。
两者都强调同一件事:生成样本之后还要清噪。SMOTE只是放大了少数类区域,噪声同时也会被放大,组合清洗流程能在数据层面抑制这种副作用。
我一般先跑SMOTEENN,如果数据量太大、运算时间太长,退一步用SMOTETomek。但无论用哪种,都要配合上一节说的分层交叉验证,不然你无法判断性能提升是来自算法还是来自验证划分的偶然性。
3.4 数据处理的边界意识:分训练/验证/测试再重采样
这是数据层面最容易被忽视的纪律:重采样必须在切分之后进行,且只能作用于训练集。验证集和测试集要保持原始分布,不得做任何过采样或欠采样。
原因很直白:模型要模拟的线上环境是天然的不平衡分布,你测试时也应该用天然分布来评估。如果把验证集也采样成平衡分布,所有指标都建立在“线上会有那么多正样本”的错误假设上。后面6.1节我会再展开一个具体翻车案例,这里先记住这个边界:切分先行,采样随后,测试集绝不参与任何数据生成。
4. 算法层面对症下药:让模型自己“偏向”少数类
数据层面解决的是样本数量和分布的问题,算法层面解决的是模型在损失函数中对不同类别的“关注度”问题。两者常常配合使用,也可以单独生效。
4.1 类权重惩罚是从根上改变损失函数
很多模型在损失函数中都支持样本权重。比如逻辑回归的损失函数,会给每个样本的误差乘一个类别权重,少数类的误差权重更大,模型为了降低整体损失,就会在少数类上尽量预测正确。
实操中,我通常先不手动指定复杂的权重表,直接用库内置的平衡设置。比如scikit-learn里在逻辑回归或支持向量机中设置class_weight='balanced',它会按样本数量的反比自动计算权重;随机森林也可以设置同样的参数。这个设置的数学本质就是让少数类的错误惩罚放大,是最省事的算法层手段。
4.2 阈值移动:模型输出的0.5原本就不该是默认
很多分类模型输出的是概率,然后默认把超过0.5的结果判为正类。但在不平衡数据里,概率分布整体偏向负类,0.5这个门槛十有八九是不合适的。阈值移动是我认为“性价比”最高的一步。
具体操作是:在验证集上算出Positive和Negative两类样本的预测概率分布,然后遍历一系列阈值(比如从0.1到0.9,步长0.05),分别计算精确率、召回率、F1,选出最符合业务目标的那个阈值。如果你更看重召回率,就把阈值调低一点;如果你更在意精确率,就把阈值调高。这一步不改变模型,却能直接在决策层改善业务表现。
4.3 从损失函数上下功夫,焦点损失的思路可以借鉴
如果类权重解决的是“类别不平等”,那焦点损失解决的是“样本难易不均匀”。它的核心思路是降低那些被模型预测得很好的“易分样本”的损失贡献,把学习重点放到难分样本上。
原始公式里有一个调制因子,让模型对高置信度样本的loss自动打折。这个思路在目标检测中非常常见,后来被大量引用到不平衡分类任务中。对二分类任务,你可以改造现有的损失函数,对每个样本按预测置信度和真实标签动态调整权重。不过这个改造需要较深厚的模型功底,我建议普通项目先尝试类权重和阈值移动,依然不满足时再考虑改损失函数。
4.4 集成模型里怎么配参才不上火
如果你用的是XGBoost、LightGBM这类梯度提升树,那么它的原生参数里也有针对不平衡的开关。
以XGBoost为例,scale_pos_weight是一个常见参数,推荐取值是负样本数除以正样本数。这个参数的本质也是放大少数类的梯度,与类权重原理一致。LightGBM里除了scale_pos_weight,还有is_unbalance,效果也类似。epoch 有人认为把它设成1:1的比例就万事大吉,其实还要配合早停(early stopping)和交叉验证,否则很容易在验证集上过拟合少数类样本。我在实践中的配参顺序是:先把树本身的深度、学习率调稳,再引入scale_pos_weight,最后用网格搜索微调,避免多个参数同时乱跑。
5. 组合策略和更高阶的选择:从单个模型走向“系统方案”
前面的方法都默认在一个模型上做文章。但实际业务里,单模型的稳定性往往有限,尤其是当少数类模式非常杂的时候。更好的做法是将多个平衡处理过的子模型组合成一个系统,发挥集成的优势。
5.1 重新采样的Bagging思路
传统Bagging是通过自助采样生成多个子训练集,再对多个子模型的结果做投票。在不平衡任务中,可以在每个子模型里对训练数据做“平衡化采样”,比如每个子模型都从负类中随机抽取与正类相似数量的样本,再独立训练一个模型。
这种做法的优势在于,每个子模型虽然只看到了多数类的局部信息,但多个子模型合在一起,多数类信息被尽量保留,同时每个子模型内部都有相对平衡的训练分布。最终投票时,少数类的决策权重不会被多数类淹没。代码层面可以直接用BalancedBaggingClassifier这类现成封装,也可以自己循环训练多个模型,再对预测概率做平均。
5.2 并行采样的集成策略
除了Bagging,还有一类专门为不平衡设计的并行集成,比如EasyEnsemble和BalanceCascade。EasyEnsemble的思路是多次从多数类中独立随机抽取子集,每次抽取的数量与少数类相近,然后分别训练子模型,最后把多个子模型预测结果平均。BalanceCascade则更进一步:每轮训练一个模型后,把已经被正确分类的多数类样本从下一轮中移除,让后续模型真正聚焦到更难处理的样本上。
我在重度失衡的数据集上用过类似策略,效果通常明显好于单次采样加单模型。代价是训练时间更久、模型数量更多,需要做好推理时的工程化优化。如果项目延迟要求很高,我一般会把这类集成模型的推理结果做成离线预计算或定时更新。
5.3 从“二分类”跳到“异常检测”:极端不平衡时的思维转换
当失衡比例超过1:200,少数类内部又没有稳定模式时,我会建议团队真正考虑一种思维转换:从“学习少数类长什么样”变成“学习多数类长什么样”。
分类器需要两类样本共同训练,而异常检测只建模多数类的正常分布,凡是偏离这个分布的就是异常。这种方法在极端失衡时反而更稳健,因为它不再试图从零星样本中挖掘模式,而是对正常逻辑建模。工业质检、A类流量识别、数据库异常操作检测等场景里,这种思路相当好用。不过它的弱点也很明显:多数类模式不是单一高斯分布时会误报频发,需要不断过滤和更新正常样本库。
6. 这些坑我基本都踩过,提前写给你
最后几个坑是我实际项目里踩过的,单独拎出来。它们不一定会出现在代码报错里,却能在线上让效果断崖式下跌。
6.1 本地验证指标高得一片光明,上线就崩?重采样泄漏是头号元凶
有个我印象深刻的案例:某项目组朋友建模时,先对整个数据集做了SMOTE,再拆分训练集和测试集,验证集F1高达0.9,模型上线后召回率直接掉到0.3。问题出在他们把SMOTE放在切分之前,测试集里“混”入了大量由训练集插值生成的样本,这些样本和训练集高度相似,评估指标自然虚高。
正确流程永远是:先切分,再对训练集采样。测试集必须保持原始的、无任何插值的分布。这是一条铁律,违反了它,后面所有调参都是在沙滩上建塔。
6.2 在测试集上反复调阈值,其实也是在作弊
有些人把测试集当作一个可以反复尝试的大草稿,今天跑一个阈值,明天改一个参数,最后挑最好的一组结果写进报告。这种“测试集复用”会产生隐性过拟合:你把测试集的模式抄进了模型选择过程,最终指标反映的不是泛化能力,而是你记忆测试集的能力。
我现在的习惯是:测试集只在最终评估时碰一次。阈值、超参、特征筛选全部在验证集上做。如果你觉得验证集不够稳定,可以采用多折交叉验证,把每一轮的验证结果取平均,再决定最终模型,但最后一次的测试评估仍然只做一次。
6.3 过采样带来的“重复样本”,会让模型记得太牢
随机过采样和SMOTE的边界要清晰:简单复制少数类样本,会让模型在见到少数类时给出一条过于确定的决策边界。尤其在神经网络或深层模型里,大量重复样本会加速模型对训练数据的记忆,验证集上不确定性反而被掩盖。
规避方法有两个:一是刻意控制过采样后的比例,不要让少数类和多数类完全等量,保留一定的多数类优势;二是在模型训练里引入更强的正则化和早停。我在树模型里通常不会特别担心这个问题,但在神经网络模型里会非常小心。
6.4 概率校准与业务成本:分数分布比标签本身更有用
最后一个经验是,不要把模型输出的标签直接当成业务决策的唯一依据。很多业务方需要的不是“是不是正类”,而是“这个样本的正类概率有多大”。概率分数可以用于排序,比如把预测概率最高的前1000个样本优先分配给人工处理。正因如此,概率的绝对值是否准确也很重要。
如果发现模型输出的概率整体偏高或偏低,可以考虑做概率校准,常见方式包括Platt Scaling和Isotonic Regression。校准之后,你再用业务成本矩阵去定阈值:误报一次损失多少、漏报一次损失多少,两者不对称时,阈值应该定在让“期望成本最小”的位置,而不是机械地落在某个F1最高点。这是从学术指标走向真实业务的一步,也是样本不平衡问题最终落地的关键一环。
处理二分类样本不平衡,说实话没有银弹。比例、绝对样本量、数据成因、业务代价会组合出各种变体,照搬他人方案大概率水土不服。把诊断、评估、采样、算法改造和系统设计串成完整链路,再配合几个关键的避坑经验,会让你的模型扎实很多,也更能经得起线上检验。