1. 项目概述当你的模型“偏爱”了多数类在机器学习的实战里尤其是分类任务我们常常会遇到一个令人头疼的“潜规则”模型会倾向于预测那些数量最多的类别。比如你手头有一个客户流失预测的数据集其中90%的客户是“未流失”只有10%是“已流失”。如果你不做任何处理直接把数据扔给一个逻辑回归或者随机森林模型它很可能会学到一个“偷懒”的策略——把所有客户都预测为“未流失”。这样模型在“未流失”这个类别上的准确率高达90%看起来很美但对于我们真正关心的“已流失”客户它的识别能力是零。这个问题就是典型的类别不平衡。而当类别不止两个比如你要做一个新闻主题分类有“体育”、“科技”、“财经”、“娱乐”、“健康”五个类别其中“体育”新闻占了数据集的60%而“健康”新闻只占5%这就演变成了多分类不平衡问题。模型会疯狂地学习“体育”新闻的特征而对“健康”新闻的特征视而不见导致“健康”类别的召回率Recall惨不忍睹。这绝不是我们想要的结果。一个健康的模型应该对所有类别都保持一定的“敏感度”。“快速解决”这个前缀意味着我们需要一套高效、直接、且在实践中被验证有效的方法论而不是停留在理论探讨。它面向的是那些已经遇到了实际问题需要立刻上手调整模型效果的数据科学家、算法工程师甚至是业务分析师。本文将围绕这个核心拆解从数据层面、算法层面到评估层面的完整应对策略并提供可直接复现的代码示例和避坑指南。2. 核心思路拆解平衡的艺术与策略选择面对多分类不平衡我们的目标不是简单地让每个类别的样本数相等而是让模型在学习过程中能够“公平”地对待每一个类别。这里的“公平”指的是模型对每个类别的学习权重或关注度与其样本数量脱钩。核心思路可以归结为三个层面数据重采样、算法层面调整和评估指标优化。选择哪种或哪几种策略的组合取决于你的数据规模、业务代价和模型复杂度。2.1 数据重采样直接修改训练集分布这是最直观的方法通过增加少数类样本或减少多数类样本使各类别在训练集中的数量达到平衡。它操作简单效果直接尤其适合数据量不是特别巨大的场景。过采样Oversampling增加少数类样本的数量。最经典的方法是SMOTESynthetic Minority Over-sampling Technique。它不是简单地复制少数类样本而是在特征空间中对少数类样本进行插值生成新的“合成”样本。例如对于两个相似的“健康”类新闻样本SMOTE会在连接它们的线段上随机选择一个点作为一个新的合成样本。这样可以有效避免过拟合增加样本多样性。注意SMOTE适用于数值型特征。如果你的数据包含大量分类变量如文本经过One-Hot编码后的稀疏特征直接使用SMOTE可能会生成无意义的样本。此时可以考虑其变种如SMOTE-NC用于混合数据类型。欠采样Undersampling减少多数类样本的数量。最简单的方法是随机删除一些多数类样本。但这种方法会丢失信息如果多数类样本本身就不多风险很大。更高级的方法如NearMiss会选择性地删除那些与少数类样本边界重叠或距离较远的多数类样本保留更有信息量的样本。实操心得我个人的经验是对于中等规模的数据集万级样本优先尝试SMOTE过采样因为它能保留所有原始信息。对于非常大的数据集百万级以上计算SMOTE可能较慢可以考虑结合使用先对多数类进行适度的随机欠采样再对少数类进行SMOTE过采样以达到一个理想的平衡比如1:1或根据业务代价调整。2.2 算法层面调整让模型“看见”少数类我们不必动数据而是直接告诉模型“请格外关注那些数量少的类别。”这通常通过调整模型训练时的**类别权重Class Weight**来实现。大多数机器学习库如scikit-learn, LightGBM, XGBoost的分类器都支持class_weight参数。你可以手动指定一个字典为每个类别赋予一个权重。权重的设置很有讲究按样本数反比权重与类别样本数成反比。这是最常用的方法class_weightbalanced参数会自动计算这个比例。如果一个类别的样本数是另一个的10倍那么前者的权重就是后者的1/10。按业务代价自定义在某些场景下误判少数类的代价远高于误判多数类。例如在医疗诊断中将“患病”少数类误判为“健康”多数类的代价是巨大的。这时你应该根据业务风险来手动设置更高的少数类权重。核心优势这种方法不改变原始数据分布保留了所有信息尤其适合数据量本身就不大无法承受采样信息损失的情况。它直接作用于损失函数从优化目标上纠正模型的偏见。2.3 评估指标优化用对的尺子衡量模型在类别不平衡的场景下准确率Accuracy是彻底失效的指标。一个把所有样本都预测为多数的“笨”模型准确率可能很高但毫无用处。我们必须转向更能反映模型对各类别识别能力的指标。混淆矩阵Confusion Matrix这是所有分析的起点。它能清晰地展示每个类别被预测对了多少又被误判成了其他哪些类别。精确率Precision与召回率Recall对于每一个类别我们都可以计算其精确率预测为该类的样本中真正属于该类的比例和召回率所有属于该类的样本中被模型正确找出来的比例。通常这两者是一对矛盾体。F1-Score精确率和召回率的调和平均数是一个综合性的单值指标。F1 2 * (Precision * Recall) / (Precision Recall)。我们可以计算每个类别的F1然后计算它们的宏平均Macro-averageF1。宏平均F1对每个类别一视同仁无论样本多少其值都同等重要。因此它是衡量多分类不平衡问题模型性能的黄金指标。AUCArea Under ROC Curve对于二分类AUC很有效。对于多分类可以计算每个类别相对于其他类别的“一对多”One-vs-Rest的AUC然后取宏平均。评估策略在模型训练和选择时应该以宏平均F1作为核心优化目标同时辅以混淆矩阵来具体分析哪些类别之间容易混淆。交叉验证时也必须使用分层采样Stratified K-Fold确保每一折的类别分布与整体一致。3. 实战流程从数据到模型的全链路操作下面我们以一个虚拟的“新闻主题五分类”数据集为例展示一个完整的解决流程。假设我们有10000条新闻类别分布如下体育(6000), 科技(2000), 财经(1000), 娱乐(800), 健康(200)。显然“健康”类是严重的少数类。3.1 环境与数据准备首先我们导入必要的库并创建示例数据。import numpy as np import pandas as pd from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix, f1_score from imblearn.over_sampling import SMOTE from imblearn.pipeline import Pipeline import warnings warnings.filterwarnings(ignore) # 1. 创建一个人造的多分类不平衡数据集 # n_classes5, 权重比例大致为 [0.6, 0.2, 0.1, 0.08, 0.02] X, y make_classification(n_samples10000, n_features20, n_informative15, n_redundant5, n_clusters_per_class3, n_classes5, weights[0.6, 0.2, 0.1, 0.08, 0.02], random_state42) # 2. 划分训练集和测试集使用分层划分保持分布 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) print(f训练集形状: {X_train.shape}) print(f测试集形状: {X_test.shape}) print(\n训练集类别分布:) for i in range(5): print(f 类别 {i}: {np.sum(y_train i)} 个样本)3.2 方案一使用类别权重不采样这是最快捷的方法无需安装额外库如imbalanced-learn。# 3. 基线模型不使用任何平衡策略 base_rf RandomForestClassifier(n_estimators100, random_state42) base_rf.fit(X_train, y_train) y_pred_base base_rf.predict(X_test) print( 基线模型无处理性能 ) print(classification_report(y_test, y_pred_base, digits4)) print(宏平均F1: {:.4f}.format(f1_score(y_test, y_pred_base, averagemacro))) # 4. 使用类别权重的模型 # 方法A让库自动计算平衡权重 weighted_rf_auto RandomForestClassifier(n_estimators100, class_weightbalanced, random_state42) weighted_rf_auto.fit(X_train, y_train) y_pred_weighted_auto weighted_rf_auto.predict(X_test) print(\n 使用 class_weightbalanced 模型性能 ) print(classification_report(y_test, y_pred_weighted_auto, digits4)) print(宏平均F1: {:.4f}.format(f1_score(y_test, y_pred_weighted_auto, averagemacro))) # 方法B手动计算并指定权重按样本数反比 from sklearn.utils.class_weight import compute_class_weight classes np.unique(y_train) weights compute_class_weight(class_weightbalanced, classesclasses, yy_train) class_weight_dict dict(zip(classes, weights)) print(f\n计算得到的类别权重: {class_weight_dict}) weighted_rf_manual RandomForestClassifier(n_estimators100, class_weightclass_weight_dict, random_state42) weighted_rf_manual.fit(X_train, y_train) # 预测和评估略结果应与方法A一致实操要点class_weightbalanced和手动计算反比权重在大多数情况下效果等价。使用权重后你会明显发现少数类如类别4的召回率提升了但多数类的精确率可能会略有下降这是用多数类的部分“性能”换取了对少数类的识别能力总体宏平均F1应该上升。3.3 方案二使用SMOTE过采样我们需要安装imbalanced-learn库 (pip install imbalanced-learn)。# 5. 使用SMOTE过采样 # 注意SMOTE要在数据划分后仅对训练集进行绝不能对测试集做任何采样。 smote SMOTE(random_state42) X_train_smote, y_train_smote smote.fit_resample(X_train, y_train) print( SMOTE过采样后训练集分布 ) for i in range(5): print(f 类别 {i}: {np.sum(y_train_smote i)} 个样本) # 此时每个类别的样本数应该大致相等 # 在平衡后的数据上训练模型 rf_smote RandomForestClassifier(n_estimators100, random_state42) rf_smote.fit(X_train_smote, y_train_smote) y_pred_smote rf_smote.predict(X_test) print(\n SMOTE过采样模型性能 ) print(classification_report(y_test, y_pred_smote, digits4)) print(宏平均F1: {:.4f}.format(f1_score(y_test, y_pred_smote, averagemacro)))3.4 方案三结合交叉验证的Pipeline推荐为了更稳健地评估采样方法避免数据泄露最佳实践是将采样器和模型放入一个Pipeline并在交叉验证中使用。# 6. 使用Pipeline和分层交叉验证进行稳健评估 from imblearn.pipeline import make_pipeline # 创建两个对比的pipeline pipeline_base make_pipeline(RandomForestClassifier(n_estimators100, random_state42)) pipeline_smote make_pipeline(SMOTE(random_state42), RandomForestClassifier(n_estimators100, random_state42)) # 使用分层5折交叉验证评估宏平均F1 cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) print(交叉验证结果宏平均F1:) scores_base cross_val_score(pipeline_base, X_train, y_train, cvcv, scoringf1_macro) scores_smote cross_val_score(pipeline_smote, X_train, y_train, cvcv, scoringf1_macro) print(f 基线模型: {scores_base.mean():.4f} (/- {scores_base.std()*2:.4f})) print(f SMOTE模型: {scores_smote.mean():.4f} (/- {scores_smote.std()*2:.4f})) # 根据交叉验证结果选择更好的pipeline在整个训练集上重新拟合 if scores_smote.mean() scores_base.mean(): final_pipeline pipeline_smote print(\n选择 SMOTE 管道进行最终训练。) else: final_pipeline pipeline_base print(\n选择 基线 管道进行最终训练。) final_pipeline.fit(X_train, y_train) y_pred_final final_pipeline.predict(X_test) print(\n 最终模型在测试集上的性能 ) print(classification_report(y_test, y_pred_final, digits4)) print(宏平均F1: {:.4f}.format(f1_score(y_test, y_pred_final, averagemacro))) # 输出混淆矩阵 cm confusion_matrix(y_test, y_pred_final) print(\n混淆矩阵行为真实值列为预测值:) print(cm)4. 进阶策略与技巧当基础方法效果有限时或者面对更复杂的场景可以考虑以下进阶策略。4.1 代价敏感学习与集成方法代价敏感学习这比简单的类别权重更进一步。你不仅可以定义每个类别的权重还可以定义一个代价矩阵Cost Matrix。代价矩阵C[i, j]表示将一个真实类别为i的样本预测为类别j所需要付出的代价。例如在欺诈检测中将欺诈少数类误判为正常多数类的代价远高于将正常误判为欺诈的代价。一些算法如某些决策树变种支持直接优化代价值和。集成方法专门为不平衡数据设计的集成算法往往有奇效。EasyEnsemble 和 BalanceCascade通过多次对多数类进行欠采样生成多个平衡的子训练集分别训练基分类器然后集成结果。这既减少了欠采样造成的信息损失又引入了多样性。RUSBoost 和 SMOTEBoost将采样技术随机欠采样或SMOTE直接集成到Boosting框架如AdaBoost的每一轮迭代中。Boosting本身会给分错的样本加大权重结合采样能更聚焦于难以学习的少数类样本。# 示例使用EasyEnsemble (需要imbalanced-learn) from imblearn.ensemble import EasyEnsembleClassifier ee_clf EasyEnsembleClassifier(n_estimators10, base_estimatorRandomForestClassifier(n_estimators50, random_state42), random_state42) ee_clf.fit(X_train, y_train) y_pred_ee ee_clf.predict(X_test) # 评估...4.2 阈值移动与概率校准对于输出概率的模型如逻辑回归、随机森林的predict_proba默认的决策阈值是0.5二分类或最大概率多分类。我们可以通过移动这个阈值来优化对特定类别的识别。操作步骤在验证集上获取模型对每个样本的预测概率。对于你最关心的少数类观察其概率分布。如果模型对其预测概率普遍偏低比如都小于0.3那么你可以尝试降低将该类判定为正例的阈值。对于多分类阈值移动更复杂通常需要为每个类别单独寻找最优阈值以最大化宏平均F1或根据代价矩阵最小化总代价。这可以通过在验证集上进行网格搜索来实现。概率校准有些模型如SVM、朴素贝叶斯输出的“概率”并非真实的概率其尺度可能有问题。使用CalibratedClassifierCV进行概率校准后阈值移动会更有效。4.3 从特征与问题定义层面思考有时问题出在更上游。特征工程是否为少数类设计了具有区分度的特征例如在新闻分类中“健康”类新闻可能频繁出现某些特定医学术语或机构名这些特征是否被充分提取和利用问题重构极端不平衡时是否可以考虑“异常检测”的思路或者将多个相似的少数类合并成一个“其他”大类亦或将复杂的多分类问题拆解成多个层次化的二分类问题收集更多数据最根本但往往最困难的方法。能否针对少数类进行定向的数据采集或生成5. 常见陷阱与避坑指南在实际操作中我踩过不少坑这里总结几个最关键的陷阱一在划分训练集/测试集前进行重采样这是最严重的错误会导致数据泄露。重采样如SMOTE必须且只能在训练集上进行测试集必须保持原始分布以模拟真实场景。永远使用Pipeline或在交叉验证循环内进行采样来避免此问题。陷阱二盲目追求绝对的样本数量平衡1:1的采样比例并非金科玉律。有时根据业务代价少数类的权重应该更高对应的采样比例也可以超过多数类。需要通过交叉验证来寻找最优的采样比例smote SMOTE(sampling_strategy{0: 5000, 1: 2000, 2: 1500, 3: 1500, 4: 1000})。陷阱三只依赖F1-score不看混淆矩阵宏平均F1提升了但模型可能只是把“健康”新闻全部误判成了“娱乐”。混淆矩阵能告诉你类别间具体的混淆情况帮助你判断问题是特征相关还是采样不足。例如如果“财经”和“科技”新闻总是分不清那可能需要更好的特征来区分这两个领域。陷阱四忽略模型本身的概率输出质量如果模型本身的概率校准很差比如对少数类的预测概率没有区分度那么阈值移动和基于概率的后处理都会失效。训练后先用CalibratedClassifierCV校准一下概率输出往往有意外收获。陷阱五在超大规模数据集上使用SMOTESMOTE需要计算样本间的距离当特征维度和样本量极大时计算开销会变得难以承受。此时class_weight是更高效的选择或者可以考虑在数据子集上应用SMOTE后再训练。一个实用的排查流程建立基线先用原始数据和默认参数训练一个模型记录宏平均F1和混淆矩阵。尝试class_weightbalanced这是成本最低的尝试看F1是否有显著提升。引入SMOTE如果数据量允许用Pipeline交叉验证比较SMOTE与基线或权重法的效果。分析混淆矩阵如果少数类性能仍不佳看它主要被误判为何类。如果是特征问题回头做特征工程如果是样本问题尝试调整SMOTE的采样策略或使用集成方法。概率与阈值如果模型给出了概率绘制少数类的概率分布直方图并尝试在验证集上寻找最优决策阈值。考虑进阶方法如果上述方法都效果平平尝试EasyEnsemble、代价敏感学习或重新审视问题定义。处理多分类不平衡没有银弹它需要你像侦探一样结合数据观察、模型诊断和业务理解进行多轮迭代和实验。从最简单的类别权重开始逐步引入更复杂的方法并用严谨的交叉验证和贴合业务的评估指标来导航你就能快速找到适合当前问题的最佳平衡点。