机器学习类别不平衡问题:从SMOTE到代价敏感学习的实战指南

📅 2026/8/12 9:50:37
机器学习类别不平衡问题:从SMOTE到代价敏感学习的实战指南
1. 项目概述当少数派的声音需要被听见在数据科学和机器学习的实战中我们常常会遇到一个看似简单却影响深远的问题数据集里的各类样本数量相差悬殊。比如在金融风控场景中99%的交易可能是正常的只有1%是欺诈交易在医疗影像诊断中患有某种罕见疾病的样本可能只占千分之一。这种“多数类”样本远多于“少数类”样本的现象就是类别不平衡问题。它绝不是一个可以忽略的统计细节而是直接关系到模型能否真正“学会”识别我们最关心的那些少数类样本。我见过太多项目在训练集上准确率高达99%上线后却对真正的风险如欺诈、故障、疾病视而不见沦为“多数类的投票机器”。问题的核心在于大多数机器学习算法如逻辑回归、决策树、乃至深度学习模型的默认优化目标如最小化整体错误率在面临极度不平衡的数据时会天然地倾向于偏向多数类。因为只要把所有样本都预测为多数类就能轻松获得一个很高的准确率但这对于解决实际问题毫无价值。因此“解决类别不平衡问题”不是一个可选的高级技巧而是构建一个可靠、公平且实用的预测模型的基石。它贯穿于从数据理解、预处理、算法选择到模型评估的整个流程。本文将系统性地梳理从数据层面、算法层面到评估层面的各类方法并结合我多年的调参踩坑经验为你提供一份可直接落地的“不平衡数据生存指南”。2. 核心思路拆解三层防御体系对抗不平衡面对类别不平衡我们不能指望单一方法一招制胜。一个稳健的策略是构建一个多层次、协同工作的防御体系。我的经验是将其分为三个层面层层递进共同作用。2.1 第一层数据重采样——从源头调整兵力分布这是最直观、应用最广泛的一层核心思想是直接修改训练数据集使各类样本数量趋于平衡。它又分为两大流派上采样和下采样。上采样Oversampling增加少数类样本的数量。最朴素的方法是随机复制已有的少数类样本。但这种方法容易导致模型过拟合因为它只是简单重复没有提供新的信息。更高级的方法是SMOTESynthetic Minority Over-sampling Technique及其变种。SMOTE的基本原理是在少数类样本之间进行插值生成新的“合成”样本。例如对于两个少数类样本A和BSMOTE会在其连线上随机选择一个点作为新的合成样本。这相当于为模型创造了更多“略有不同”的少数类场景有助于模型学习到更鲁棒的决策边界。注意SMOTE并非万能。在数据分布非常复杂或存在大量噪声时盲目插值可能会生成一些没有实际意义的“坏样本”反而会干扰模型学习。对于类别内部方差很大的情况比如少数类样本本身就分属于几个不同的子簇需要谨慎使用。下采样Undersampling减少多数类样本的数量。最简单的是随机丢弃一部分多数类样本。但这样做会损失大量潜在有用的信息特别是当多数类样本本身也蕴含重要模式时。因此启发式的下采样方法更受青睐如NearMiss选择那些与少数类样本最“接近”的多数类样本保留下来移除那些远离决策边界的多数类样本。这相当于聚焦于分类的“前沿阵地”。Tomek Links识别并移除那些是Tomek Link的样本对一个属于多数类一个属于少数类且彼此是对方最近邻。这有助于清理类别边界附近模糊或噪声的点使边界更清晰。实操心得我通常的策略是“先下后上组合使用”。对于数据量极大的情况先使用NearMiss或聚类方法对多数类进行智能下采样将规模降至可管理的水平同时尽量保留信息。然后如果少数类样本量仍然不足比如少于1000条再考虑使用SMOTE进行适度的上采样。绝对不要一开始就对一个只有几十条样本的少数类进行大幅度的SMOTE那很可能是在制造幻觉。2.2 第二层算法层面调整——赋予模型不平衡视角如果不想或不能改动数据我们可以从模型本身入手改变它对不同类别的“重视程度”。代价敏感学习Cost-Sensitive Learning这是最核心的算法层思路。其理念是为不同类别的错误分类赋予不同的“代价”Cost。例如将少数类误判为多数类漏报的代价要远高于将多数类误判为少数类误报。几乎所有分类算法都可以通过设置class_weight参数来实现这一点。在Scikit-learn中可以设置为‘balanced’让算法自动根据类别频率反比设置权重也可以手动指定一个字典如{0: 1, 1: 10}表示将类别1假设是少数类的权重设为类别1的10倍。原理阐释以逻辑回归为例其损失函数通常是交叉熵。当我们设置类别权重后损失函数中每一项都会乘以对应样本类别的权重。少数类样本的损失项被放大因此模型在优化过程中会“更努力”地去减少对少数类样本的预测错误从而将决策边界向多数类方向推移。集成学习方法一些集成算法天然对不平衡数据有较好的适应性。EasyEnsemble 和 BalanceCascade这类方法通过多次对多数类下采样生成多个平衡的子训练集分别训练基分类器最后集成。它既降低了下采样的信息损失风险又通过集成获得了更好的泛化能力。梯度提升树如XGBoost, LightGBM这类模型在训练过程中可以非常方便地设置scale_pos_weight参数例如设置为多数类样本数/少数类样本数来实现代价敏感学习。同时其基于梯度的训练机制本身对异常值和噪声有一定的鲁棒性在不平衡数据上往往有出色表现是我的首选算法之一。2.3 第三层评估指标革命——抛弃虚伪的准确率这是最容易犯错也最至关重要的一层。在不平衡数据上准确率Accuracy是一个具有严重误导性的“虚荣指标”。一个将95%样本预测为多数的模型准确率就是95%但这毫无意义。我们必须转向更能反映模型对少数类识别能力的指标精确率Precision与召回率Recall精确率在所有被模型预测为少数类的样本中真正是少数类的比例。它关注的是“预测的准不准”。召回率在所有真实的少数类样本中被模型成功找出来的比例。它关注的是“找的全不全”。 这两者通常相互制约需要根据业务需求权衡。在金融欺诈中我们可能追求高召回率宁可错杀不可放过在内容推荐中可能更看重高精确率宁可少推也要推准。F1-Score精确率和召回率的调和平均数是一个综合指标。当精确率和召回率都重要且需要平衡时使用。PR曲线Precision-Recall Curve和AUC-PR比ROC曲线更适合不平衡数据。ROC曲线在正负样本比例悬殊时其AUC值可能会虚高因为横坐标假正率的一点变化在多数类样本海量时显得微不足道。而PR曲线直接聚焦于我们关心的正类少数类其曲线下的面积AUC-PR能更真实地反映模型在少数类上的性能。混淆矩阵Confusion Matrix永远是最直观的工具。一眼就能看出模型在每一类上的具体表现特别是漏报和误报的数量。评估策略在训练和验证阶段我固定使用F1-Score作为主要优化指标同时辅以PR曲线和混淆矩阵进行诊断。在最终测试集上会业务方一起基于混淆矩阵的具体数字如“我们找到了80%的欺诈交易但同时误报了2000个正常用户”来做出是否上线的决策。3. 方法详解与实操要点3.1 数据重采样的技术细节与陷阱理解了基本思路后我们深入看看具体操作时有哪些“魔鬼细节”。SMOTE家族的选择与参数调优 基础的SMOTE假设少数类样本空间是连续且均匀的但现实往往骨感。因此衍生出了多种变体Borderline-SMOTE只对那些处于“边界线”附近的少数类样本进行过采样。因为这些样本更容易被误分类是提升模型性能的关键。这比盲目地对所有少数类样本过采样更高效。SMOTE-NC用于处理混合数据类型数值型类别型。对于类别型特征它采用众数而不是插值。ADASYN根据少数类样本的密度分布进行自适应过采样。在分布稀疏的区域生成更多样本在密集区域生成较少样本旨在减轻基础SMOTE可能带来的分布扭曲。实操配置示例使用imbalanced-learn库from imblearn.over_sampling import SMOTE, BorderlineSMOTE from imblearn.under_sampling import NearMiss from imblearn.pipeline import Pipeline # 使用管道组合采样和模型 # 方案1 Borderline-SMOTE smote BorderlineSMOTE(kindborderline-1, # 边界线模式 k_neighbors5, # 用于生成新样本的最近邻数 random_state42) X_resampled, y_resampled smote.fit_resample(X_train, y_train) # 方案2 下采样上采样管道推荐用于极大不平衡 pipeline Pipeline([ (undersample, NearMiss(version3, n_neighbors3)), # 先下采样 (oversample, SMOTE(sampling_strategy0.5, random_state42)), # 再上采样到0.5的比例 (classifier, RandomForestClassifier(n_estimators100)) ])关键参数解析sampling_strategy 控制重采样后的类别比例。例如0.5表示让少数类的数量达到多数类的50%‘auto’默认表示让两类数量相等。k_neighbors SMOTE中用于插值的最近邻数量。太小如2可能导致过拟合太大可能生成无意义的样本通常取5。kindin BorderlineSMOTE‘borderline-1’和‘borderline-2’决定了哪些边界样本被选中通常先用‘borderline-1’。必须避开的坑数据泄露绝对不能在包含测试集数据的整个数据集上应用SMOTE重采样必须且只能在训练集上进行。正确的流程是先划分训练集和测试集然后在训练集上做重采样用未经过任何采样的原始测试集来评估模型。这是一个高频错误会导致模型性能评估严重失真。评估失真如果你使用了重采样那么交叉验证也需要在采样的数据上进行。但要注意采样过程应该放在交叉验证的每一次折叠循环内部而不是外部。也就是说对每一折的训练部分进行重采样验证部分保持原样。imblearn提供了专门的StratifiedKFold和管道来支持这一点。类别不止两类对于多类不平衡问题策略需要调整。通常是为每一个少数类分别针对其与多数类的组合进行处理或者使用“一对多”的策略。imblearn中的大多数采样器原生支持多类。3.2 代价敏感学习的权重计算艺术设置class_weight看似简单但权重的具体数值如何确定是一门实践艺术。自动计算class_weight‘balanced’是最常用的选择。其计算公式为n_samples / (n_classes * np.bincount(y))。这意味着某个类别的样本数越少其权重就越大。这是一个很好的起点。手动调优当业务对精确率和召回率有明确的不对称要求时需要手动调整。例如在癌症筛查中漏诊的代价远高于误诊我们可能需要给正类癌症一个极高的权重。一个实用的网格搜索策略先使用‘balanced’作为基线。观察模型在验证集上的混淆矩阵特别是少数类的召回率Recall。如果召回率过低按比例提高少数类的权重例如从反比权重提高到其2倍、5倍。同时监控精确率因为权重的提高通常会以精确率的下降为代价。使用交叉验证下的AUC-PR或F1-Score来最终确定最佳权重。示例在随机森林中调优类别权重from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV # 定义权重参数网格 param_grid { class_weight: [ balanced, {0: 1, 1: 5}, # 少数类(1)权重为5 {0: 1, 1: 10}, {0: 1, 1: 20} ], max_depth: [10, 20, None] } rf RandomForestClassifier(n_estimators100, random_state42) grid_search GridSearchCV(estimatorrf, param_gridparam_grid, scoringf1, # 使用F1分数作为评估标准 cv5, # 5折交叉验证 n_jobs-1) grid_search.fit(X_train, y_train) print(f最佳参数: {grid_search.best_params_}) print(f最佳交叉验证F1分数: {grid_search.best_score_:.4f})3.3 集成方法XGBoost/LightGBM的不平衡处理实战对于树模型尤其是梯度提升框架处理不平衡数据有其独特而高效的方式。核心参数scale_pos_weight这个参数是XGBoost和LightGBM中用于平衡正负类别权重的利器。一个经验性的设置是scale_pos_weight 负样本数 / 正样本数。这相当于告诉模型“一个正样本的错误其严重程度相当于这么多负样本的错误”。LightGBM实操示例import lightgbm as lgb from sklearn.metrics import classification_report # 计算样本权重比例 num_negative (y_train 0).sum() num_positive (y_train 1).sum() scale_pos_weight num_negative / num_positive # 创建数据集 train_data lgb.Dataset(X_train, labely_train) # 设置参数 params { objective: binary, # 二分类 metric: binary_logloss, # 也可以使用‘auc’或自定义评估函数 boosting_type: gbdt, scale_pos_weight: scale_pos_weight, # 关键参数 num_leaves: 31, learning_rate: 0.05, feature_fraction: 0.9, verbose: -1 } # 训练并指定评估集 gbm lgb.train(params, train_data, num_boost_round1000, valid_sets[train_data], callbacks[lgb.early_stopping(stopping_rounds50)]) # 预测与评估 y_pred_proba gbm.predict(X_test) y_pred (y_pred_proba 0.5).astype(int) # 默认阈值为0.5 print(classification_report(y_test, y_pred))高级技巧自定义损失函数与评估函数如果业务场景非常特殊标准的对数损失不合适可以自定义损失函数。例如我们可以定义一个代价敏感的损失函数给少数类错误更高的惩罚。def custom_asymmetric_loss(y_true, y_pred): weight 10.0 # 少数类错误的权重 loss - (y_true * np.log(y_pred) * weight (1 - y_true) * np.log(1 - y_pred)) return loss # 在LightGBM中需要通过fobj参数传入自定义损失函数这需要一定的实现功底。更常见且推荐的做法是使用feval参数自定义一个与业务更贴切的评估函数如F1-Score并让模型以此为目标进行早停间接优化我们关心的指标。4. 完整项目流程与方案选型纸上得来终觉浅我们把这些方法串起来形成一个端到端的实战流程。假设我们面对一个信用卡欺诈检测数据集正负样本比为1:99。4.1 第一步探索性数据分析与评估基准建立在动手处理之前必须先彻底了解数据。查看类别分布用value_counts()或可视化确认不平衡程度。检查特征与标签的关系对于少数类样本其特征分布是否有显著异常是否存在明显的聚类建立愚蠢基线训练一个总是预测多数的Dummy Classifier它的准确率是99%但召回率是0。这是我们所有聪明模型必须超越的底线。建立简单模型基线在不做任何不平衡处理的情况下用一个简单的模型如逻辑回归或浅层决策树在原始数据上训练并记录其准确率、召回率、精确率、F1和AUC-PR。这个结果将作为我们后续所有优化努力的对比基准。4.2 第二步分层抽样与数据划分这是保证后续评估有效性的关键一步。from sklearn.model_selection import train_test_split # 使用stratify参数进行分层抽样保证训练集和测试集中的类别比例与原始数据集一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy # 关键 )确保测试集是“纯净”的没有经过任何采样污染它代表了真实世界的分布。4.3 第三步构建并对比多种处理管道不要只尝试一种方法。我通常会并行构建3-4条技术路线在验证集上进行对比。管道A纯算法调整代价敏感学习模型RandomForestClassifier(class_weight‘balanced’)评估5折交叉验证主要看F1-Score和AUC-PR管道B数据重采样SMOTE步骤在训练集上应用Borderline-SMOTE模型RandomForestClassifier(class_weightNone)或XGBoost评估同样使用交叉验证注意交叉验证要在采样步骤之后进行。管道C集成下采样EasyEnsemble使用imblearn.ensemble.EasyEnsembleClassifier它内部集成了多个平衡的子集和分类器。直接调用比较省心适合作为基准集成方法。管道D梯度提升树内嵌权重模型LGBMClassifier(scale_pos_weight负样本数/正样本数)评估使用自定义的feval函数计算F1进行早停。使用交叉验证的结果选择在F1-Score和AUC-PR上综合表现最好的管道。通常管道DLightGBM/XGBoost scale_pos_weight和管道A代价敏感随机森林是强有力的竞争者且不需要额外处理数据流程更简洁。4.4 第四步阈值调优——模型校准的最后一步模型输出的通常是概率值如0.78我们默认用0.5作为阈值来划分正负类。但在不平衡问题中0.5通常不是最优阈值。 我们可以根据业务需求在验证集上寻找最佳阈值如果追求高召回率可以降低阈值如0.3这样更多样本会被判为正类召回率上升但精确率会下降。如果追求高精确率可以提高阈值如0.7这样只有非常确信的样本才会被判为正类精确率上升但召回率会下降。寻找最佳F1阈值的代码from sklearn.metrics import f1_score, precision_recall_curve # 获取模型在验证集上的预测概率 y_val_proba best_model.predict_proba(X_val)[:, 1] # 计算不同阈值下的精确率、召回率 precisions, recalls, thresholds precision_recall_curve(y_val, y_val_proba) # 计算每个阈值对应的F1分数 f1_scores 2 * (precisions * recalls) / (precisions recalls 1e-8) # 避免除零 # 找到使F1最大的阈值 optimal_idx np.argmax(f1_scores) optimal_threshold thresholds[optimal_idx] print(f最佳F1阈值: {optimal_threshold:.4f}) print(f该阈值下的验证集F1分数: {f1_scores[optimal_idx]:.4f}) # 在测试集上应用新阈值 y_test_pred_optimized (best_model.predict_proba(X_test)[:, 1] optimal_threshold).astype(int)5. 常见陷阱、问题排查与进阶思考5.1 高频问题排查清单问题现象可能原因排查与解决思路模型召回率始终为01. 类别权重设置错误或未设置。2. 模型过于简单无法捕捉少数类模式。3. 少数类特征与多数类完全重叠难以区分。1. 检查class_weight或scale_pos_weight参数尝试大幅提高少数类权重。2. 使用更复杂的模型如更深的树、集成模型。3. 进行特征工程尝试构造能区分两类的新特征。检查特征重要性看是否有特征对预测少数类有帮助。过拟合严重训练集F1高验证集骤降1. SMOTE过度使用生成了大量不现实的合成样本。2. 模型复杂度太高如树深度太大。3. 数据量本身太少。1. 减少SMOTE的采样比例(sampling_strategy)或换用Borderline-SMOTE。2. 增加正则化如剪枝、降低树深、增加min_samples_leaf。3. 考虑使用下采样或集成方法而非上采样。精确率极低误报太多1. 为提升召回率阈值设得过低。2. 少数类权重设置得过高导致模型过于“敏感”。1. 在PR曲线上寻找精确率和召回率的平衡点或根据业务成本调整阈值。2. 适当降低少数类的权重或尝试使用能同时优化精确率和召回率的指标如F_beta Score beta1时更看重精确率。不同的重采样方法结果差异巨大1. 数据本身噪声大或边界模糊。2. 不同采样方法对数据分布的假设不同。1. 在采样前尝试清洗数据移除明显的噪声点如使用Tomek Links。2. 使用交叉验证稳健地评估多种采样方法选择方差最小的。可视化采样后的数据分布通过PCA或t-SNE降维观察是否合理。5.2 容易被忽略的细节与进阶策略训练、验证、测试集的一致性这是一个原则性问题。任何基于数据分布的处理如重采样、权重计算都只能从训练集中获取信息。计算scale_pos_weight的比例要用训练集的SMOTE只能拟合训练集。验证集用于调参和选择阈值测试集用于最终、不可动摇的性能报告。三者必须严格隔离。“中度不平衡”与“极度不平衡”的策略差异中度不平衡如1:10到1:100代价敏感学习class_weight和集成树模型scale_pos_weight往往效果最好且最简单。极度不平衡1:1000以上可能需要组合拳。例如先使用NearMiss或聚类方法将多数类下采样到一个合理比例如1:50再使用SMOTE轻微上采样少数类最后结合代价敏感学习。同时考虑异常检测算法如Isolation Forest, One-Class SVM是否更适合因为此时问题可能更接近于“在正常数据中找异常点”。特征工程的力量很多时候模型性能的瓶颈不在于不平衡处理技巧而在于特征本身。能否构造出对少数类有强区分度的特征至关重要。例如在交易欺诈中“本次交易金额与过去30天平均交易金额的比值”、“交易发生地与常用地的距离”等衍生特征可能比原始特征有效得多。业务代价的量化如果可能尝试与业务方沟通量化一次“漏报”False Negative和一次“误报”False Positive带来的实际成本如金钱、信誉损失。将这个代价比直接代入到代价敏感学习的权重设置中或者用于计算一个自定义的、与商业价值直接挂钩的评估指标如“最小化期望损失”这样得到的模型才是最符合业务需求的。处理类别不平衡问题没有银弹。它要求我们从数据、算法、评估三个维度系统性地思考和实践。从建立一个可靠的评估基准开始尝试不同的技术路径优先尝试简单的代价敏感学习谨慎地使用重采样技术并避免数据泄露最后通过阈值调优来对齐业务目标。记住最终目标是让模型在现实世界中可靠地工作而不是在训练集上获得一个漂亮的数字。在这个过程中混淆矩阵和业务逻辑是你的罗盘不断地质疑和验证你的方法是前进的动力。