简体中文 繁體中文 English 日本語 Deutsch 한국 사람 بالعربية TÜRKÇE português คนไทย Français

站内搜索

搜索

活动公告

11-02 12:46
10-23 09:32
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,将及时处理!
10-23 09:31
10-23 09:28
通知:签到时间调整为每日4:00(东八区)
10-23 09:26

掌握Scikit-learn处理不平衡数据集的核心策略从数据重采样到算法调整全面解决机器学习模型性能问题

3万

主题

349

科技点

3万

积分

大区版主

木柜子打湿

积分
31898

三倍冰淇淋无人之境【一阶】财Doro小樱(小丑装)立华奏以外的星空【二阶】⑨的冰沙

发表于 2025-9-17 21:40:06 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

在现实世界的机器学习应用中,不平衡数据集是一个常见且具有挑战性的问题。不平衡数据集指的是在分类问题中,不同类别的样本数量存在显著差异。例如,在欺诈检测中,欺诈交易可能只占总交易的0.1%;在疾病诊断中,罕见疾病的样本可能远少于常见疾病。这种不平衡会导致机器学习模型倾向于多数类,从而对少数类的预测性能较差。

Scikit-learn作为Python中最流行的机器学习库之一,提供了多种处理不平衡数据集的工具和方法。本文将全面介绍如何使用Scikit-learn处理不平衡数据集,从数据重采样技术到算法调整方法,帮助读者掌握解决机器学习模型在不平衡数据上性能问题的核心策略。

不平衡数据集的挑战

不平衡数据集带来的主要挑战包括:

1. 偏向性预测:模型倾向于预测多数类,因为这样可以获得较高的准确率,但对少数类的识别能力较差。
2. 评估指标误导:准确率(Accuracy)在不平衡数据集上可能产生误导。例如,在一个99%负样本和1%正样本的数据集上,一个总是预测负类的模型也能达到99%的准确率,但实际上毫无用处。
3. 决策边界偏移:不平衡数据会导致决策边界向少数类偏移,使得少数类的样本更容易被错误分类。
4. 训练不充分:少数类的样本数量不足,导致模型无法充分学习其特征分布。

偏向性预测:模型倾向于预测多数类,因为这样可以获得较高的准确率,但对少数类的识别能力较差。

评估指标误导:准确率(Accuracy)在不平衡数据集上可能产生误导。例如,在一个99%负样本和1%正样本的数据集上,一个总是预测负类的模型也能达到99%的准确率,但实际上毫无用处。

决策边界偏移:不平衡数据会导致决策边界向少数类偏移,使得少数类的样本更容易被错误分类。

训练不充分:少数类的样本数量不足,导致模型无法充分学习其特征分布。

让我们通过一个简单的例子来说明这个问题:
  1. import numpy as np
  2. from sklearn.datasets import make_classification
  3. from sklearn.model_selection import train_test_split
  4. from sklearn.linear_model import LogisticRegression
  5. from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
  6. # 创建一个不平衡数据集
  7. X, y = make_classification(n_samples=10000, n_features=20, n_classes=2,
  8.                           weights=[0.95, 0.05], random_state=42)
  9. # 划分训练集和测试集
  10. X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
  11. # 训练一个逻辑回归模型
  12. model = LogisticRegression(random_state=42)
  13. model.fit(X_train, y_train)
  14. # 预测
  15. y_pred = model.predict(X_test)
  16. # 评估
  17. print("准确率:", accuracy_score(y_test, y_pred))
  18. print("\n混淆矩阵:\n", confusion_matrix(y_test, y_pred))
  19. print("\n分类报告:\n", classification_report(y_test, y_pred))
复制代码

输出结果可能显示高准确率,但对少数类(类别1)的召回率(Recall)和F1分数很低,这表明模型对少数类的识别能力较差。

评估指标

在不平衡数据集上,传统的准确率指标往往不足以评估模型性能。以下是一些更适合评估不平衡数据集上模型性能的指标:

1. 混淆矩阵(Confusion Matrix):提供真正例(TP)、真负例(TN)、假正例(FP)和假负例(FN)的详细信息。
2. 精确率(Precision):TP / (TP + FP),表示预测为正例的样本中实际为正例的比例。
3. 召回率(Recall)或灵敏度(Sensitivity):TP / (TP + FN),表示实际为正例的样本中被正确预测为正例的比例。
4. F1分数(F1-Score):2 * (Precision * Recall) / (Precision + Recall),精确率和召回率的调和平均数。
5. 特异度(Specificity):TN / (TN + FP),表示实际为负例的样本中被正确预测为负例的比例。
6. ROC曲线和AUC值:ROC曲线绘制真正例率(TPR)与假正例率(FPR)之间的关系,AUC值表示ROC曲线下的面积,值越接近1表示模型性能越好。
7. PR曲线(Precision-Recall Curve):在不平衡数据集中,PR曲线通常比ROC曲线更能反映模型性能,特别是当少数类样本非常少时。

混淆矩阵(Confusion Matrix):提供真正例(TP)、真负例(TN)、假正例(FP)和假负例(FN)的详细信息。

精确率(Precision):TP / (TP + FP),表示预测为正例的样本中实际为正例的比例。

召回率(Recall)或灵敏度(Sensitivity):TP / (TP + FN),表示实际为正例的样本中被正确预测为正例的比例。

F1分数(F1-Score):2 * (Precision * Recall) / (Precision + Recall),精确率和召回率的调和平均数。

特异度(Specificity):TN / (TN + FP),表示实际为负例的样本中被正确预测为负例的比例。

ROC曲线和AUC值:ROC曲线绘制真正例率(TPR)与假正例率(FPR)之间的关系,AUC值表示ROC曲线下的面积,值越接近1表示模型性能越好。

PR曲线(Precision-Recall Curve):在不平衡数据集中,PR曲线通常比ROC曲线更能反映模型性能,特别是当少数类样本非常少时。

在Scikit-learn中,我们可以使用以下代码计算这些指标:
  1. from sklearn.metrics import precision_score, recall_score, f1_score, roc_auc_score, average_precision_score, roc_curve, precision_recall_curve
  2. import matplotlib.pyplot as plt
  3. # 计算各项指标
  4. precision = precision_score(y_test, y_pred)
  5. recall = recall_score(y_test, y_pred)
  6. f1 = f1_score(y_test, y_pred)
  7. roc_auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1])
  8. pr_auc = average_precision_score(y_test, model.predict_proba(X_test)[:, 1])
  9. print(f"精确率: {precision:.4f}")
  10. print(f"召回率: {recall:.4f}")
  11. print(f"F1分数: {f1:.4f}")
  12. print(f"ROC AUC: {roc_auc:.4f}")
  13. print(f"PR AUC: {pr_auc:.4f}")
  14. # 绘制ROC曲线
  15. fpr, tpr, _ = roc_curve(y_test, model.predict_proba(X_test)[:, 1])
  16. plt.figure(figsize=(12, 5))
  17. plt.subplot(1, 2, 1)
  18. plt.plot(fpr, tpr, label=f'ROC Curve (AUC = {roc_auc:.2f})')
  19. plt.plot([0, 1], [0, 1], 'k--')
  20. plt.xlabel('False Positive Rate')
  21. plt.ylabel('True Positive Rate')
  22. plt.title('ROC Curve')
  23. plt.legend()
  24. # 绘制PR曲线
  25. precision_curve, recall_curve, _ = precision_recall_curve(y_test, model.predict_proba(X_test)[:, 1])
  26. plt.subplot(1, 2, 2)
  27. plt.plot(recall_curve, precision_curve, label=f'PR Curve (AUC = {pr_auc:.2f})')
  28. plt.xlabel('Recall')
  29. plt.ylabel('Precision')
  30. plt.title('Precision-Recall Curve')
  31. plt.legend()
  32. plt.tight_layout()
  33. plt.show()
复制代码

数据重采样技术

数据重采样是处理不平衡数据集的常用方法,主要包括过采样、欠采样和混合采样。

过采样(Oversampling)

过采样通过增加少数类样本的数量来平衡数据集。最简单的过采样方法是随机复制少数类样本,但这可能导致过拟合。更高级的方法包括SMOTE(Synthetic Minority Over-sampling Technique)和ADASYN(Adaptive Synthetic Sampling)。
  1. from sklearn.datasets import make_classification
  2. from sklearn.model_selection import train_test_split
  3. from collections import Counter
  4. # 创建不平衡数据集
  5. X, y = make_classification(n_samples=5000, n_features=20, n_classes=2,
  6.                           weights=[0.95, 0.05], random_state=42)
  7. print("原始数据集类别分布:", Counter(y))
  8. # 划分训练集和测试集
  9. X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
  10. # 随机过采样
  11. from imblearn.over_sampling import RandomOverSampler
  12. ros = RandomOverSampler(random_state=42)
  13. X_resampled, y_resampled = ros.fit_resample(X_train, y_train)
  14. print("过采样后训练集类别分布:", Counter(y_resampled))
  15. # 训练模型
  16. from sklearn.linear_model import LogisticRegression
  17. from sklearn.metrics import classification_report
  18. model = LogisticRegression(random_state=42)
  19. model.fit(X_resampled, y_resampled)
  20. # 预测并评估
  21. y_pred = model.predict(X_test)
  22. print("\n使用随机过采样的分类报告:\n", classification_report(y_test, y_pred))
复制代码

SMOTE通过在少数类样本之间插值来生成合成样本,而不是简单地复制现有样本。
  1. from imblearn.over_sampling import SMOTE
  2. # 应用SMOTE
  3. smote = SMOTE(random_state=42)
  4. X_smote, y_smote = smote.fit_resample(X_train, y_train)
  5. print("SMOTE后训练集类别分布:", Counter(y_smote))
  6. # 训练模型
  7. model_smote = LogisticRegression(random_state=42)
  8. model_smote.fit(X_smote, y_smote)
  9. # 预测并评估
  10. y_pred_smote = model_smote.predict(X_test)
  11. print("\n使用SMOTE的分类报告:\n", classification_report(y_test, y_pred_smote))
复制代码

ADASYN是SMOTE的改进版本,它根据少数类样本的学习难度自适应地生成合成样本。对于更难学习的少数类样本,生成更多的合成样本。
  1. from imblearn.over_sampling import ADASYN
  2. # 应用ADASYN
  3. adasyn = ADASYN(random_state=42)
  4. X_adasyn, y_adasyn = adasyn.fit_resample(X_train, y_train)
  5. print("ADASYN后训练集类别分布:", Counter(y_adasyn))
  6. # 训练模型
  7. model_adasyn = LogisticRegression(random_state=42)
  8. model_adasyn.fit(X_adasyn, y_adasyn)
  9. # 预测并评估
  10. y_pred_adasyn = model_adasyn.predict(X_test)
  11. print("\n使用ADASYN的分类报告:\n", classification_report(y_test, y_pred_adasyn))
复制代码

欠采样(Undersampling)

欠采样通过减少多数类样本的数量来平衡数据集。简单随机欠采样可能会丢失重要信息,因此更高级的方法如Tomek Links和NearMiss被提出。
  1. from imblearn.under_sampling import RandomUnderSampler
  2. # 应用随机欠采样
  3. rus = RandomUnderSampler(random_state=42)
  4. X_rus, y_rus = rus.fit_resample(X_train, y_train)
  5. print("随机欠采样后训练集类别分布:", Counter(y_rus))
  6. # 训练模型
  7. model_rus = LogisticRegression(random_state=42)
  8. model_rus.fit(X_rus, y_rus)
  9. # 预测并评估
  10. y_pred_rus = model_rus.predict(X_test)
  11. print("\n使用随机欠采样的分类报告:\n", classification_report(y_test, y_pred_rus))
复制代码

Tomek Links是指两个不同类别的样本互为最近邻的情况。通过移除Tomek Links中的多数类样本,可以增加两类之间的决策边界。
  1. from imblearn.under_sampling import TomekLinks
  2. # 应用Tomek Links
  3. tl = TomekLinks()
  4. X_tl, y_tl = tl.fit_resample(X_train, y_train)
  5. print("Tomek Links后训练集类别分布:", Counter(y_tl))
  6. # 训练模型
  7. model_tl = LogisticRegression(random_state=42)
  8. model_tl.fit(X_tl, y_tl)
  9. # 预测并评估
  10. y_pred_tl = model_tl.predict(X_test)
  11. print("\n使用Tomek Links的分类报告:\n", classification_report(y_test, y_pred_tl))
复制代码

NearMiss是一组欠采样方法,基于少数类样本选择多数类样本。NearMiss-1选择离少数类样本最近的多数类样本;NearMiss-2选择离少数类样本最远的多数类样本;NearMiss-3使用k近邻算法选择多数类样本。
  1. from imblearn.under_sampling import NearMiss
  2. # 应用NearMiss-1
  3. nm = NearMiss(version=1)
  4. X_nm, y_nm = nm.fit_resample(X_train, y_train)
  5. print("NearMiss-1后训练集类别分布:", Counter(y_nm))
  6. # 训练模型
  7. model_nm = LogisticRegression(random_state=42)
  8. model_nm.fit(X_nm, y_nm)
  9. # 预测并评估
  10. y_pred_nm = model_nm.predict(X_test)
  11. print("\n使用NearMiss-1的分类报告:\n", classification_report(y_test, y_pred_nm))
复制代码

混合采样

混合采样结合了过采样和欠采样的优点,例如SMOTEENN和SMOTETomek。

SMOTEENN首先应用SMOTE进行过采样,然后应用ENN(Edited Nearest Neighbours)进行欠采样,移除被其他类别样本包围的样本。
  1. from imblearn.combine import SMOTEENN
  2. # 应用SMOTEENN
  3. smote_enn = SMOTEENN(random_state=42)
  4. X_smoteenn, y_smoteenn = smote_enn.fit_resample(X_train, y_train)
  5. print("SMOTEENN后训练集类别分布:", Counter(y_smoteenn))
  6. # 训练模型
  7. model_smoteenn = LogisticRegression(random_state=42)
  8. model_smoteenn.fit(X_smoteenn, y_smoteenn)
  9. # 预测并评估
  10. y_pred_smoteenn = model_smoteenn.predict(X_test)
  11. print("\n使用SMOTEENN的分类报告:\n", classification_report(y_test, y_pred_smoteenn))
复制代码

SMOTETomek首先应用SMOTE进行过采样,然后应用Tomek Links进行欠采样。
  1. from imblearn.combine import SMOTETomek
  2. # 应用SMOTETomek
  3. smote_tomek = SMOTETomek(random_state=42)
  4. X_smotetomek, y_smotetomek = smote_tomek.fit_resample(X_train, y_train)
  5. print("SMOTETomek后训练集类别分布:", Counter(y_smotetomek))
  6. # 训练模型
  7. model_smotetomek = LogisticRegression(random_state=42)
  8. model_smotetomek.fit(X_smotetomek, y_smotetomek)
  9. # 预测并评估
  10. y_pred_smotetomek = model_smotetomek.predict(X_test)
  11. print("\n使用SMOTETomek的分类报告:\n", classification_report(y_test, y_pred_smotetomek))
复制代码

算法调整方法

除了数据重采样技术外,我们还可以通过调整算法本身来处理不平衡数据集。

类权重调整

许多Scikit-learn算法允许我们为不同类别设置不同的权重,使得模型在训练过程中更加关注少数类。
  1. from sklearn.linear_model import LogisticRegression
  2. from sklearn.svm import SVC
  3. from sklearn.ensemble import RandomForestClassifier
  4. from sklearn.metrics import classification_report
  5. # 逻辑回归
  6. model_lr = LogisticRegression(class_weight='balanced', random_state=42)
  7. model_lr.fit(X_train, y_train)
  8. y_pred_lr = model_lr.predict(X_test)
  9. print("逻辑回归(平衡类权重)的分类报告:\n", classification_report(y_test, y_pred_lr))
  10. # 支持向量机
  11. model_svc = SVC(class_weight='balanced', random_state=42, probability=True)
  12. model_svc.fit(X_train, y_train)
  13. y_pred_svc = model_svc.predict(X_test)
  14. print("\n支持向量机(平衡类权重)的分类报告:\n", classification_report(y_test, y_pred_svc))
  15. # 随机森林
  16. model_rf = RandomForestClassifier(class_weight='balanced', random_state=42)
  17. model_rf.fit(X_train, y_train)
  18. y_pred_rf = model_rf.predict(X_test)
  19. print("\n随机森林(平衡类权重)的分类报告:\n", classification_report(y_test, y_pred_rf))
复制代码
  1. # 计算自定义类权重
  2. from sklearn.utils.class_weight import compute_class_weight
  3. class_weights = compute_class_weight('balanced', classes=np.unique(y_train), y=y_train)
  4. class_weight_dict = dict(zip(np.unique(y_train), class_weights))
  5. print("自定义类权重:", class_weight_dict)
  6. # 使用自定义类权重
  7. model_custom = LogisticRegression(class_weight=class_weight_dict, random_state=42)
  8. model_custom.fit(X_train, y_train)
  9. y_pred_custom = model_custom.predict(X_test)
  10. print("\n逻辑回归(自定义类权重)的分类报告:\n", classification_report(y_test, y_pred_custom))
复制代码

专门设计用于不平衡数据集的算法

有些算法专门设计用于处理不平衡数据集,例如EasyEnsemble和BalanceCascade。

EasyEnsemble是一种集成学习方法,它从多数类中随机抽取多个子集,每个子集与少数类结合形成一个平衡的训练集,然后训练多个分类器并集成它们的预测结果。
  1. from imblearn.ensemble import EasyEnsembleClassifier
  2. # 应用EasyEnsemble
  3. eec = EasyEnsembleClassifier(random_state=42)
  4. eec.fit(X_train, y_train)
  5. y_pred_eec = eec.predict(X_test)
  6. print("EasyEnsemble的分类报告:\n", classification_report(y_test, y_pred_eec))
复制代码

BalanceCascade是一种级联方法,它训练一系列分类器,每个分类器专注于被前一个分类器错误分类的样本。
  1. from imblearn.ensemble import BalanceCascadeClassifier
  2. # 应用BalanceCascade
  3. bcc = BalanceCascadeClassifier(random_state=42)
  4. bcc.fit(X_train, y_train)
  5. y_pred_bcc = bcc.predict(X_test)
  6. print("BalanceCascade的分类报告:\n", classification_report(y_test, y_pred_bcc))
复制代码

集成学习方法

集成学习方法通过组合多个基学习器的预测结果来提高模型性能,在不平衡数据集上尤其有效。

1. Bagging

Bagging(Bootstrap Aggregating)通过对训练数据进行有放回抽样生成多个训练集,训练多个基学习器,然后通过投票或平均的方式集成预测结果。
  1. from sklearn.ensemble import BaggingClassifier
  2. from sklearn.tree import DecisionTreeClassifier
  3. # 使用Bagging
  4. base_estimator = DecisionTreeClassifier(class_weight='balanced', random_state=42)
  5. bagging = BaggingClassifier(base_estimator=base_estimator, n_estimators=50, random_state=42)
  6. bagging.fit(X_train, y_train)
  7. y_pred_bagging = bagging.predict(X_test)
  8. print("Bagging的分类报告:\n", classification_report(y_test, y_pred_bagging))
复制代码

2. Boosting

Boosting通过迭代训练一系列弱学习器,每个后续学习器专注于前一个学习器错误分类的样本。
  1. from sklearn.ensemble import AdaBoostClassifier
  2. # 使用AdaBoost
  3. adaboost = AdaBoostClassifier(n_estimators=50, random_state=42)
  4. adaboost.fit(X_train, y_train)
  5. y_pred_adaboost = adaboost.predict(X_test)
  6. print("AdaBoost的分类报告:\n", classification_report(y_test, y_pred_adaboost))
复制代码
  1. from sklearn.ensemble import GradientBoostingClassifier
  2. # 使用Gradient Boosting
  3. gb = GradientBoostingClassifier(n_estimators=50, random_state=42)
  4. gb.fit(X_train, y_train)
  5. y_pred_gb = gb.predict(X_test)
  6. print("Gradient Boosting的分类报告:\n", classification_report(y_test, y_pred_gb))
复制代码

3. BalancedRandomForest

BalancedRandomForest是随机森林的变体,它在构建每棵树时对 bootstrap 样本进行平衡,使得每个 bootstrap 样本中少数类和多数类的样本数量相等。
  1. from imblearn.ensemble import BalancedRandomForestClassifier
  2. # 使用BalancedRandomForest
  3. brf = BalancedRandomForestClassifier(n_estimators=50, random_state=42)
  4. brf.fit(X_train, y_train)
  5. y_pred_brf = brf.predict(X_test)
  6. print("BalancedRandomForest的分类报告:\n", classification_report(y_test, y_pred_brf))
复制代码

4. RUSBoost

RUSBoost结合了随机欠采样(RUS)和AdaBoost算法,在每个迭代中先对多数类进行欠采样,然后应用AdaBoost。
  1. from imblearn.ensemble import RUSBoostClassifier
  2. # 使用RUSBoost
  3. rusboost = RUSBoostClassifier(n_estimators=50, random_state=42)
  4. rusboost.fit(X_train, y_train)
  5. y_pred_rusboost = rusboost.predict(X_test)
  6. print("RUSBoost的分类报告:\n", classification_report(y_test, y_pred_rusboost))
复制代码

实际案例:使用Scikit-learn处理不平衡数据集的完整示例

让我们通过一个完整的实际案例,综合应用前面介绍的方法来处理不平衡数据集。
  1. import numpy as np
  2. import pandas as pd
  3. import matplotlib.pyplot as plt
  4. import seaborn as sns
  5. from sklearn.datasets import make_classification
  6. from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold, GridSearchCV
  7. from sklearn.preprocessing import StandardScaler
  8. from sklearn.linear_model import LogisticRegression
  9. from sklearn.svm import SVC
  10. from sklearn.tree import DecisionTreeClassifier
  11. from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
  12. from sklearn.metrics import (accuracy_score, precision_score, recall_score, f1_score,
  13.                              roc_auc_score, average_precision_score, confusion_matrix,
  14.                              classification_report, roc_curve, precision_recall_curve)
  15. from imblearn.over_sampling import SMOTE, ADASYN, RandomOverSampler
  16. from imblearn.under_sampling import RandomUnderSampler, TomekLinks, NearMiss
  17. from imblearn.combine import SMOTEENN, SMOTETomek
  18. from imblearn.ensemble import EasyEnsembleClassifier, BalancedRandomForestClassifier, RUSBoostClassifier
  19. import warnings
  20. warnings.filterwarnings('ignore')
  21. # 1. 创建不平衡数据集
  22. X, y = make_classification(n_samples=10000, n_features=20, n_informative=15,
  23.                           n_redundant=3, n_classes=2, weights=[0.95, 0.05],
  24.                           flip_y=0.05, random_state=42)
  25. # 2. 数据预处理
  26. # 划分训练集和测试集
  27. X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42)
  28. # 特征标准化
  29. scaler = StandardScaler()
  30. X_train_scaled = scaler.fit_transform(X_train)
  31. X_test_scaled = scaler.transform(X_test)
  32. # 3. 定义评估函数
  33. def evaluate_model(model, X_test, y_test, model_name="Model"):
  34.     """评估模型性能并打印指标"""
  35.     y_pred = model.predict(X_test)
  36.     y_pred_proba = model.predict_proba(X_test)[:, 1] if hasattr(model, 'predict_proba') else None
  37.    
  38.     # 计算各项指标
  39.     accuracy = accuracy_score(y_test, y_pred)
  40.     precision = precision_score(y_test, y_pred)
  41.     recall = recall_score(y_test, y_pred)
  42.     f1 = f1_score(y_test, y_pred)
  43.    
  44.     print(f"{model_name} 评估结果:")
  45.     print(f"准确率: {accuracy:.4f}")
  46.     print(f"精确率: {precision:.4f}")
  47.     print(f"召回率: {recall:.4f}")
  48.     print(f"F1分数: {f1:.4f}")
  49.    
  50.     if y_pred_proba is not None:
  51.         roc_auc = roc_auc_score(y_test, y_pred_proba)
  52.         pr_auc = average_precision_score(y_test, y_pred_proba)
  53.         print(f"ROC AUC: {roc_auc:.4f}")
  54.         print(f"PR AUC: {pr_auc:.4f}")
  55.    
  56.     print("\n分类报告:")
  57.     print(classification_report(y_test, y_pred))
  58.    
  59.     # 绘制混淆矩阵
  60.     cm = confusion_matrix(y_test, y_pred)
  61.     plt.figure(figsize=(8, 6))
  62.     sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
  63.                 xticklabels=['Negative', 'Positive'],
  64.                 yticklabels=['Negative', 'Positive'])
  65.     plt.title(f'{model_name} 混淆矩阵')
  66.     plt.ylabel('真实标签')
  67.     plt.xlabel('预测标签')
  68.     plt.show()
  69.    
  70.     # 绘制ROC和PR曲线
  71.     if y_pred_proba is not None:
  72.         fpr, tpr, _ = roc_curve(y_test, y_pred_proba)
  73.         precision_curve, recall_curve, _ = precision_recall_curve(y_test, y_pred_proba)
  74.         
  75.         plt.figure(figsize=(12, 5))
  76.         plt.subplot(1, 2, 1)
  77.         plt.plot(fpr, tpr, label=f'ROC Curve (AUC = {roc_auc:.2f})')
  78.         plt.plot([0, 1], [0, 1], 'k--')
  79.         plt.xlabel('False Positive Rate')
  80.         plt.ylabel('True Positive Rate')
  81.         plt.title('ROC Curve')
  82.         plt.legend()
  83.         
  84.         plt.subplot(1, 2, 2)
  85.         plt.plot(recall_curve, precision_curve, label=f'PR Curve (AUC = {pr_auc:.2f})')
  86.         plt.xlabel('Recall')
  87.         plt.ylabel('Precision')
  88.         plt.title('Precision-Recall Curve')
  89.         plt.legend()
  90.         
  91.         plt.tight_layout()
  92.         plt.show()
  93.    
  94.     return {
  95.         'accuracy': accuracy,
  96.         'precision': precision,
  97.         'recall': recall,
  98.         'f1': f1,
  99.         'roc_auc': roc_auc if y_pred_proba is not None else None,
  100.         'pr_auc': pr_auc if y_pred_proba is not None else None
  101.     }
  102. # 4. 基准模型(不处理不平衡)
  103. print("="*50)
  104. print("基准模型(不处理不平衡)")
  105. print("="*50)
  106. # 逻辑回归
  107. lr = LogisticRegression(random_state=42)
  108. lr.fit(X_train_scaled, y_train)
  109. lr_metrics = evaluate_model(lr, X_test_scaled, y_test, "逻辑回归")
  110. # 随机森林
  111. rf = RandomForestClassifier(random_state=42)
  112. rf.fit(X_train_scaled, y_train)
  113. rf_metrics = evaluate_model(rf, X_test_scaled, y_test, "随机森林")
  114. # 5. 数据重采样方法
  115. print("="*50)
  116. print("数据重采样方法")
  117. print("="*50)
  118. # 5.1 过采样方法
  119. # SMOTE
  120. smote = SMOTE(random_state=42)
  121. X_train_smote, y_train_smote = smote.fit_resample(X_train_scaled, y_train)
  122. lr_smote = LogisticRegression(random_state=42)
  123. lr_smote.fit(X_train_smote, y_train_smote)
  124. lr_smote_metrics = evaluate_model(lr_smote, X_test_scaled, y_test, "逻辑回归 + SMOTE")
  125. # ADASYN
  126. adasyn = ADASYN(random_state=42)
  127. X_train_adasyn, y_train_adasyn = adasyn.fit_resample(X_train_scaled, y_train)
  128. lr_adasyn = LogisticRegression(random_state=42)
  129. lr_adasyn.fit(X_train_adasyn, y_train_adasyn)
  130. lr_adasyn_metrics = evaluate_model(lr_adasyn, X_test_scaled, y_test, "逻辑回归 + ADASYN")
  131. # 5.2 欠采样方法
  132. # RandomUnderSampler
  133. rus = RandomUnderSampler(random_state=42)
  134. X_train_rus, y_train_rus = rus.fit_resample(X_train_scaled, y_train)
  135. lr_rus = LogisticRegression(random_state=42)
  136. lr_rus.fit(X_train_rus, y_train_rus)
  137. lr_rus_metrics = evaluate_model(lr_rus, X_test_scaled, y_test, "逻辑回归 + 随机欠采样")
  138. # TomekLinks
  139. tl = TomekLinks()
  140. X_train_tl, y_train_tl = tl.fit_resample(X_train_scaled, y_train)
  141. lr_tl = LogisticRegression(random_state=42)
  142. lr_tl.fit(X_train_tl, y_train_tl)
  143. lr_tl_metrics = evaluate_model(lr_tl, X_test_scaled, y_test, "逻辑回归 + TomekLinks")
  144. # 5.3 混合采样方法
  145. # SMOTEENN
  146. smoteenn = SMOTEENN(random_state=42)
  147. X_train_smoteenn, y_train_smoteenn = smoteenn.fit_resample(X_train_scaled, y_train)
  148. lr_smoteenn = LogisticRegression(random_state=42)
  149. lr_smoteenn.fit(X_train_smoteenn, y_train_smoteenn)
  150. lr_smoteenn_metrics = evaluate_model(lr_smoteenn, X_test_scaled, y_test, "逻辑回归 + SMOTEENN")
  151. # SMOTETomek
  152. smotetomek = SMOTETomek(random_state=42)
  153. X_train_smotetomek, y_train_smotetomek = smotetomek.fit_resample(X_train_scaled, y_train)
  154. lr_smotetomek = LogisticRegression(random_state=42)
  155. lr_smotetomek.fit(X_train_smotetomek, y_train_smotetomek)
  156. lr_smotetomek_metrics = evaluate_model(lr_smotetomek, X_test_scaled, y_test, "逻辑回归 + SMOTETomek")
  157. # 6. 算法调整方法
  158. print("="*50)
  159. print("算法调整方法")
  160. print("="*50)
  161. # 6.1 类权重调整
  162. # 逻辑回归 + 平衡类权重
  163. lr_balanced = LogisticRegression(class_weight='balanced', random_state=42)
  164. lr_balanced.fit(X_train_scaled, y_train)
  165. lr_balanced_metrics = evaluate_model(lr_balanced, X_test_scaled, y_test, "逻辑回归 + 平衡类权重")
  166. # 随机森林 + 平衡类权重
  167. rf_balanced = RandomForestClassifier(class_weight='balanced', random_state=42)
  168. rf_balanced.fit(X_train_scaled, y_train)
  169. rf_balanced_metrics = evaluate_model(rf_balanced, X_test_scaled, y_test, "随机森林 + 平衡类权重")
  170. # 6.2 专门设计用于不平衡数据集的算法
  171. # EasyEnsemble
  172. eec = EasyEnsembleClassifier(random_state=42)
  173. eec.fit(X_train_scaled, y_train)
  174. eec_metrics = evaluate_model(eec, X_test_scaled, y_test, "EasyEnsemble")
  175. # BalancedRandomForest
  176. brf = BalancedRandomForestClassifier(random_state=42)
  177. brf.fit(X_train_scaled, y_train)
  178. brf_metrics = evaluate_model(brf, X_test_scaled, y_test, "BalancedRandomForest")
  179. # RUSBoost
  180. rusboost = RUSBoostClassifier(random_state=42)
  181. rusboost.fit(X_train_scaled, y_train)
  182. rusboost_metrics = evaluate_model(rusboost, X_test_scaled, y_test, "RUSBoost")
  183. # 7. 模型比较
  184. print("="*50)
  185. print("模型比较")
  186. print("="*50)
  187. # 收集所有模型的指标
  188. all_metrics = {
  189.     '逻辑回归': lr_metrics,
  190.     '随机森林': rf_metrics,
  191.     '逻辑回归 + SMOTE': lr_smote_metrics,
  192.     '逻辑回归 + ADASYN': lr_adasyn_metrics,
  193.     '逻辑回归 + 随机欠采样': lr_rus_metrics,
  194.     '逻辑回归 + TomekLinks': lr_tl_metrics,
  195.     '逻辑回归 + SMOTEENN': lr_smoteenn_metrics,
  196.     '逻辑回归 + SMOTETomek': lr_smotetomek_metrics,
  197.     '逻辑回归 + 平衡类权重': lr_balanced_metrics,
  198.     '随机森林 + 平衡类权重': rf_balanced_metrics,
  199.     'EasyEnsemble': eec_metrics,
  200.     'BalancedRandomForest': brf_metrics,
  201.     'RUSBoost': rusboost_metrics
  202. }
  203. # 创建比较表格
  204. metrics_df = pd.DataFrame.from_dict(all_metrics, orient='index')
  205. metrics_df = metrics_df.sort_values('f1', ascending=False)
  206. print(metrics_df)
  207. # 可视化比较
  208. plt.figure(figsize=(15, 10))
  209. metrics_to_plot = ['accuracy', 'precision', 'recall', 'f1', 'roc_auc', 'pr_auc']
  210. for i, metric in enumerate(metrics_to_plot, 1):
  211.     plt.subplot(2, 3, i)
  212.     sns.barplot(x=metrics_df.index, y=metrics_df[metric])
  213.     plt.title(metric.upper())
  214.     plt.xticks(rotation=90)
  215.     plt.ylim(0, 1)
  216. plt.tight_layout()
  217. plt.show()
  218. # 8. 最佳模型调优
  219. print("="*50)
  220. print("最佳模型调优")
  221. print("="*50)
  222. # 假设BalancedRandomForest表现最好,我们对其进行调优
  223. param_grid = {
  224.     'n_estimators': [50, 100, 200],
  225.     'max_depth': [None, 10, 20, 30],
  226.     'min_samples_split': [2, 5, 10],
  227.     'min_samples_leaf': [1, 2, 4]
  228. }
  229. cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
  230. grid_search = GridSearchCV(
  231.     estimator=BalancedRandomForestClassifier(random_state=42),
  232.     param_grid=param_grid,
  233.     scoring='f1',
  234.     cv=cv,
  235.     n_jobs=-1,
  236.     verbose=1
  237. )
  238. grid_search.fit(X_train_scaled, y_train)
  239. print("最佳参数:", grid_search.best_params_)
  240. print("最佳F1分数:", grid_search.best_score_)
  241. # 使用最佳参数的模型
  242. best_model = grid_search.best_estimator_
  243. best_metrics = evaluate_model(best_model, X_test_scaled, y_test, "调优后的BalancedRandomForest")
  244. # 9. 结论
  245. print("="*50)
  246. print("结论")
  247. print("="*50)
  248. print("在这个不平衡数据集的案例中,我们比较了多种处理不平衡数据集的方法,包括:")
  249. print("1. 数据重采样技术(过采样、欠采样和混合采样)")
  250. print("2. 算法调整方法(类权重调整和专门设计的算法)")
  251. print("\n通过比较不同方法的性能指标,我们发现BalancedRandomForest在这个数据集上表现最好。")
  252. print("经过参数调优后,模型的性能得到了进一步提升。")
  253. print("\n在实际应用中,应该根据具体的数据集特点和业务需求选择合适的方法。")
  254. print("通常,结合多种方法可能会获得更好的效果。")
复制代码

最佳实践和策略选择

在处理不平衡数据集时,没有一种方法适用于所有情况。以下是一些最佳实践和策略选择的建议:

1. 理解数据和业务需求

在选择处理不平衡数据集的方法之前,首先要理解数据和业务需求:

• 不平衡程度:数据集的不平衡程度如何?是轻微不平衡(如4:1)还是严重不平衡(如1000:1)?
• 样本数量:数据集的总样本量是多少?如果样本量很少,欠采样可能会导致信息丢失。
• 特征维度:特征维度高还是低?高维数据可能需要不同的处理方法。
• 业务目标:业务更关注精确率还是召回率?例如,在疾病诊断中,可能更关注召回率(不漏诊),而在垃圾邮件过滤中,可能更关注精确率(不误判正常邮件)。

2. 评估指标选择

根据业务需求选择合适的评估指标:

• 如果两类都重要,使用F1分数或G-Mean(几何平均数)。
• 如果更关注少数类的识别,使用召回率。
• 如果更关注预测的准确性,使用精确率。
• 如果需要综合考虑,使用ROC AUC或PR AUC。

3. 策略选择指南

根据不同情况选择合适的策略:

• 推荐方法:欠采样或混合采样
• 理由:数据量大,欠采样不会导致严重信息丢失,同时可以减少训练时间
• 具体技术:RandomUnderSampler、TomekLinks、SMOTETomek

• 推荐方法:过采样或类权重调整
• 理由:数据量小,欠采样会导致信息丢失,过采样可以增加少数类样本
• 具体技术:SMOTE、ADASYN、类权重调整

• 推荐方法:混合采样或专门设计的算法
• 理由:结合过采样和欠采样的优点,或使用专门处理不平衡数据的算法
• 具体技术:SMOTEENN、SMOTETomek、BalancedRandomForest、RUSBoost

• 推荐方法:类权重调整或简单采样方法
• 理由:复杂的采样方法或集成方法可能会降低模型的可解释性
• 具体技术:类权重调整、RandomOverSampler、RandomUnderSampler

4. 组合策略

在某些情况下,组合多种策略可能会获得更好的效果:

• 数据重采样 + 类权重调整:先对数据进行重采样,然后在训练模型时使用类权重调整。
• 数据重采样 + 集成学习:使用重采样技术创建多个平衡的训练集,然后训练多个基学习器并集成。
• 特征选择 + 数据重采样:先进行特征选择,然后对数据进行重采样。

5. 验证策略

在不平衡数据集上,使用合适的验证策略非常重要:

• 分层K折交叉验证:确保每折中各类别的比例与原始数据集相同。
• 重复分层K折交叉验证:多次运行分层K折交叉验证,获得更稳定的评估结果。
• 自定义验证集:确保验证集的不平衡程度与测试集相似。

6. 实际应用中的注意事项

在实际应用中,还需要注意以下几点:

• 数据泄露:确保在数据重采样之前划分训练集和测试集,避免测试集信息泄露到训练过程。
• 计算成本:某些方法(如SMOTE在大数据集上)可能计算成本较高,需要考虑实际应用场景的计算资源限制。
• 模型复杂度:在不平衡数据集上,简单的模型可能比复杂的模型表现更好,因为复杂模型更容易过拟合多数类。
• 阈值调整:除了上述方法外,还可以通过调整预测阈值来优化模型性能。例如,降低预测少数类的阈值可以提高召回率。

结论

处理不平衡数据集是机器学习中的一个常见挑战,Scikit-learn和imbalanced-learn库提供了丰富的工具和方法来应对这一挑战。本文全面介绍了从数据重采样到算法调整的各种策略,并通过实际案例展示了如何应用这些方法。

关键要点总结:

1. 评估指标选择:在不平衡数据集上,准确率往往不是合适的评估指标,应选择精确率、召回率、F1分数、ROC AUC或PR AUC等指标。
2. 数据重采样技术:过采样(如SMOTE、ADASYN)通过增加少数类样本来平衡数据集。欠采样(如RandomUnderSampler、TomekLinks)通过减少多数类样本来平衡数据集。混合采样(如SMOTEENN、SMOTETomek)结合了过采样和欠采样的优点。
3. 过采样(如SMOTE、ADASYN)通过增加少数类样本来平衡数据集。
4. 欠采样(如RandomUnderSampler、TomekLinks)通过减少多数类样本来平衡数据集。
5. 混合采样(如SMOTEENN、SMOTETomek)结合了过采样和欠采样的优点。
6. 算法调整方法:类权重调整(如设置class_weight=‘balanced’)使模型在训练过程中更加关注少数类。专门设计用于不平衡数据集的算法(如EasyEnsemble、BalancedRandomForest、RUSBoost)。
7. 类权重调整(如设置class_weight=‘balanced’)使模型在训练过程中更加关注少数类。
8. 专门设计用于不平衡数据集的算法(如EasyEnsemble、BalancedRandomForest、RUSBoost)。
9. 策略选择:根据数据集特点(不平衡程度、样本数量、特征维度)和业务需求选择合适的策略。
10. 组合策略:在某些情况下,组合多种策略可能会获得更好的效果。
11. 验证策略:使用分层K折交叉验证等合适的验证策略,确保评估结果的可靠性。

评估指标选择:在不平衡数据集上,准确率往往不是合适的评估指标,应选择精确率、召回率、F1分数、ROC AUC或PR AUC等指标。

数据重采样技术:

• 过采样(如SMOTE、ADASYN)通过增加少数类样本来平衡数据集。
• 欠采样(如RandomUnderSampler、TomekLinks)通过减少多数类样本来平衡数据集。
• 混合采样(如SMOTEENN、SMOTETomek)结合了过采样和欠采样的优点。

算法调整方法:

• 类权重调整(如设置class_weight=‘balanced’)使模型在训练过程中更加关注少数类。
• 专门设计用于不平衡数据集的算法(如EasyEnsemble、BalancedRandomForest、RUSBoost)。

策略选择:根据数据集特点(不平衡程度、样本数量、特征维度)和业务需求选择合适的策略。

组合策略:在某些情况下,组合多种策略可能会获得更好的效果。

验证策略:使用分层K折交叉验证等合适的验证策略,确保评估结果的可靠性。

在实际应用中,处理不平衡数据集往往需要尝试多种方法,并根据具体情况进行调整。通过本文介绍的方法和策略,读者可以更好地应对不平衡数据集带来的挑战,提高机器学习模型在不平衡数据上的性能。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

频道订阅

频道订阅

加入社群

加入社群

联系我们|TG频道|RSS

Powered by Pixtech

© 2025 Pixtech Team.