深入实战:解决分类任务中的数据不平衡难题

在数据科学和机器学习的实战项目中,你是否遇到过这样的棘手情况:模型的准确率高达 99%,但在实际应用中却表现糟糕?当你仔细检查数据时,可能会发现模型只是盲目地预测了所有的样本为“多数类”,而完全忽略了那个至关重要的少数类。

这种令人沮丧的现象就是我们常说的“类别不平衡问题”。这就像是在大海捞针,如果模型只学会了识别“海水”,那它找到“针”的能力就为零。在这篇文章中,我们将作为实战者,深入探讨如何处理不平衡数据。我们将从为什么会发生这个问题讲起,一步步掌握评估指标、重采样技术以及集成学习这三大法宝。

为什么不平衡数据是个大问题?

在开始动手之前,我们首先需要理解问题的本质。在诸如欺诈检测医疗诊断罕见故障预测等场景中,我们关注的那个核心事件(如欺诈行为、患病样本)往往只占数据集的极小部分。

这就导致了以下几个严重的后果:

  • 模型偏见:机器学习算法通常旨在优化整体准确率。当多数类样本(例如正常交易)占据了 99% 的数据时,模型会发现“不管三七二十一都预测为正常”可以达到 99% 的准确率。于是,模型会向多数类严重倾斜,甚至完全忽略少数类的特征。
  • 误导性的评估指标:正如上面所说,单纯的准确率 在这种情况下是毫无意义的。它会给你一种虚假的安全感。
  • 噪声干扰:少数类样本因为数量稀少,很容易被算法视为噪声或离群点而被忽略。
  • 泛化能力差:由于决策边界严重偏向多数类,模型在面对新的、未知的少数类样本时,往往无法做出正确的判断。

策略一:寻找更可靠的评估指标

既然我们不能只看准确率,那我们应该相信什么?

我们需要一套能穿透表象,反映模型真实性能的指标体系。作为开发者,我们应该重点关注以下三个指标,它们能分别从不同角度揭示模型的优劣。

核心指标解析

  • 精确率:回答了“在模型预测为正例的样本中,有多少是真正的正例?”的问题。

高精确率*意味着模型很少误报(低假阳性 False Positives)。例如,在邮件分类中,高精确率意味着重要的邮件不会被误判为垃圾邮件。

  • 召回率:回答了“在实际为正例的样本中,模型成功找出了多少?”的问题。

高召回率*意味着模型很少漏报(低假阴性 False Negatives)。这在医疗筛查中至关重要,因为我们宁可误判也不愿漏掉一个病人。

  • F1 分数:这是精确率和召回率的调和平均值。

\[ F1 = \frac{2 \times (\text{Precision} \times \text{Recall})}{(\text{Precision} + \text{Recall})} \]

* 为什么用 F1?因为它只有在精确率和召回率同时很高时才会高。如果其中一个很低,F1 分数就会受到惩罚。它是处理不平衡数据集时的黄金标准。

代码实战:全面评估模型

让我们通过一个简单的代码示例,来看看如何在不平衡数据集上计算这些指标。我们将使用 Scikit-learn 的 classification_report,它能一次性展示我们需要的所有信息。

import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix

# 1. 创建一个极度不平衡的数据集 (99% vs 1%)
# weights=[0.99, 0.01] 表示绝大多数是类 0,极少是类 1
X, y = make_classification(
    n_samples=1000, 
    n_features=20, 
    n_classes=2, 
    weights=[0.99, 0.01], 
    random_state=42
)

# 2. 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

# 3. 训练一个基础的逻辑回归模型
model = LogisticRegression(solver=‘lbfgs‘)
model.fit(X_train, y_train)

# 4. 进行预测
y_pred = model.predict(X_test)

# 5. 打印评估报告
print("--- 模型评估报告 ---")
print(classification_report(y_test, y_pred, target_names=[‘多数类 (0)‘, ‘少数类 (1)‘]))

# 6. 打印混淆矩阵,直观地看预测结果
print("混淆矩阵 (行是真实值,列是预测值):")
print(confusion_matrix(y_test, y_pred))

分析输出结果:

当你运行这段代码时,你会发现虽然整体 Accuracy 可能很高,但少数类的 Recall(召回率)可能非常低,甚至为 0。这正是我们引入重采样技术的原因。

策略二:重采样技术 —— 调整天平

既然模型因为数据量差异而产生偏见,那最直观的办法就是人为地调整数据量,让天平重新平衡。我们可以通过两种方式来实现:增加少数类(过采样)或减少多数类(欠采样)。

1. 过采样

原理:通过复制现有的少数类样本或生成新的合成样本,来增加少数类的数量。

  • 优点:不丢失任何信息(所有多数类样本都被保留)。
  • 缺点:如果是简单复制,容易导致模型过拟合(模型只是记住了这些特定的样本)。

2. 欠采样

原理:随机删除一些多数类样本,使其数量与少数类相当。

  • 优点:训练速度变快(数据量变小了),且能减少多数类中的噪声影响。
  • 缺点:可能会丢弃重要信息,导致模型欠拟合

代码实战:对比重采样方法

在下面的代码中,我们将使用强大的 imbalanced-learn 库(简称 imblearn)。这是一个专门处理不平衡数据的工具集。让我们看看如何应用随机过采样和欠采样,并对比数据分布的变化。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from imblearn.over_sampling import RandomOverSampler
from imblearn.under_sampling import RandomUnderSampler
from collections import Counter

# 设置中文字体(如果环境支持,否则可能显示为方框)
# plt.rcParams[‘font.sans-serif‘] = [‘SimHei‘] 

# 生成不平衡数据:10% 的类 0 (作为这里的少数类演示),90% 的类 1
X, y = make_classification(
    n_classes=2, class_sep=2, weights=[0.1, 0.9],
    n_informative=3, n_redundant=1, flip_y=0,
    n_features=20, n_clusters_per_class=1,
    n_samples=1000, random_state=42
)

print(f"原始数据集类别分布: {Counter(y)}")

# --- 方法 1:随机过采样 ---
# sampling_strategy=‘minority‘ 表示只重采样少数类直到等于多数类
oversample = RandomOverSampler(sampling_strategy=‘minority‘, random_state=42)
X_over, y_over = oversample.fit_resample(X, y)
print(f"过采样后类别分布: {Counter(y_over)}")

# --- 方法 2:随机欠采样 ---
# sampling_strategy=‘majority‘ 表示欠采样多数类直到等于少数类
undersample = RandomUnderSampler(sampling_strategy=‘majority‘, random_state=42)
X_under, y_under = undersample.fit_resample(X, y)
print(f"欠采样后类别分布: {Counter(y_under)}")

# 可视化对比(可选)
fig, axs = plt.subplots(1, 3, figsize=(18, 5))

# 原始数据
colors = [‘#ef476f‘, ‘#ffd166‘]
for i, color in enumerate(colors):
    axs[0].scatter(X[y == i, 0], X[y == i, 1], alpha=0.5, c=color, label=f‘Class {i}‘)
axs[0].set_title(‘Original Imbalanced Data‘)
axs[0].legend()

# 过采样数据
for i, color in enumerate(colors):
    axs[1].scatter(X_over[y_over == i, 0], X_over[y_over == i, 1], alpha=0.3, c=color, label=f‘Class {i}‘)
axs[1].set_title(‘Random Oversampled Data‘)
axs[1].legend()

# 欠采样数据
for i, color in enumerate(colors):
    axs[2].scatter(X_under[y_under == i, 0], X_under[y_under == i, 1], alpha=0.8, c=color, label=f‘Class {i}‘)
axs[2].set_title(‘Random Undersampled Data‘)
axs[2].legend()

plt.show()

进阶技巧:SMOTE

在实际工作中,简单的 RandomOverSampler(复制样本)效果有限。我们通常推荐使用 SMOTE (Synthetic Minority Over-sampling Technique)。它不是简单地复制,而是在特征空间中为少数类生成新的“合成”样本。

如果你想尝试更高阶的技术,可以将代码中的 RandomOverSampler 替换为:

from imblearn.over_sampling import SMOTE

smote = SMOTE(random_state=42)
X_smote, y_smote = smote.fit_resample(X, y)

这通常能让模型学习到更泛化的少数类边界。

策略三:平衡装袋分类器

除了直接修改数据,我们还可以修改算法。集成方法 是处理不平衡数据的利器。

BalancedBaggingClassifier (平衡装袋分类器) 的工作原理非常聪明:

  • 它也是一种 Bagging(自举汇聚)方法,类似于随机森林。
  • 每次它从原始数据中抽取一个子集来训练一个基学习器时,它都会对这个子集进行重采样(通常是欠采样),以确保每个子集中类别都是平衡的。
  • 最后,它将所有基学习器的预测结果进行聚合。

这种方法的优势在于,它让每个“弱”分类器都能在一个公平的视角下学习数据,而不是被淹没在多数类的海洋里。

代码实战:构建平衡集成模型

让我们用一个完整的流程来演示如何使用 BalancedBaggingClassifier,并与标准的随机森林进行对比。你会看到,在处理不平衡数据时,平衡后的模型对少数类的捕获能力有显著提升。

import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
# 注意:BalancedBaggingClassifier 位于 imblearn 库中
from imblearn.ensemble import BalancedBaggingClassifier
from sklearn.metrics import accuracy_score, classification_report

# 步骤 1:创建一个严重不平衡的数据集
# 少数类仅占 5%,多数类占 95%
X, y = make_classification(
    n_samples=2000,  # 增加样本量以使测试更明显
    n_features=20,
    n_classes=2,
    weights=[0.95, 0.05],
    n_informative=2,
    n_redundant=10,
    flip_y=0.01,       # 添加少量噪声
    random_state=42
)

# 步骤 2:划分数据集
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y
)

print("=== 实验 1:标准随机森林 ===")
# 使用标准的随机森林,不做任何平衡处理
rf_standard = RandomForestClassifier(n_estimators=100, random_state=42)
rf_standard.fit(X_train, y_train)
y_pred_std = rf_standard.predict(X_test)

print(classification_report(y_test, y_pred_std, target_names=[‘多数类 (0)‘, ‘少数类 (1)‘]))

print("
=== 实验 2:平衡装袋分类器 ===")
# 使用 BalancedBaggingClassifier
# 这里依然使用决策树作为基估计器,但每个树训练时数据都被平衡了
bbc = BalancedBaggingClassifier(
    estimator=RandomForestClassifier(n_estimators=10, max_depth=5), # 基学习器
    n_estimators=50,      # 集成中的基学习器数量
    sampling_strategy=‘auto‘,
    replacement=False,    # 是否放回抽样
    random_state=42
)

bbc.fit(X_train, y_train)
y_pred_bbc = bbc.predict(X_test)

print(classification_report(y_test, y_pred_bbc, target_names=[‘多数类 (0)‘, ‘少数类 (1)‘]))

结果解读:

仔细观察运行结果。你会发现,标准随机森林虽然在多数类上的表现可能稍好,但在少数类上的 F1 分数往往很低(甚至可能因为几乎不预测少数类而导致 Recall 为 0)。而 BalancedBaggingClassifier 则通过牺牲一点点整体准确率,换取了对少数类极高的召回率和 F1 分数。这正是我们在关键业务场景中所需要的。

总结与最佳实践

在处理不平衡数据时,没有一种“万能药”。我们需要根据具体情况灵活选择策略。

  • 先看数据:在开始建模前,务必先检查类别分布。
  • 不要只信准确率:养成查看混淆矩阵和分类报告的习惯。
  • 尝试多种方法

* 如果数据量不大,优先考虑过采样(如 SMOTE)或修改类权重(如 class_weight=‘balanced‘)。

* 如果数据量巨大,欠采样集成方法(如 BalancedBaggingClassifier)计算效率更高且效果稳定。

  • 交叉验证:在进行重采样时,要确保在交叉验证的循环内部进行重采样,防止数据泄露(即测试集的信息混入了训练集)。

希望这篇实战指南能帮助你更好地应对现实世界中的不平衡数据挑战!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/28414.html
点赞
0.00 平均评分 (0% 分数) - 0