8. Common pitfalls and recommended practices

发布于:2025-06-28 ⋅ 阅读:(13) ⋅ 点赞:(0)

Common pitfalls and recommended practices

8. 常见误区与推荐做法

#

本节是对 scikit-learn 文档中提供内容的一个补充 [链接]。我们将重点介绍一种常见的误用重采样方法而导致的**数据泄漏(data leakage)**问题。由于这种泄漏,模型所报告的性能指标将过于乐观。

8.1. 数据泄漏

正如 scikit-learn 文档中提到的,当建模过程中使用了在预测时无法获取的信息时,就会发生数据泄漏。

在重采样的场景中,一个常见的误区是:在将数据集划分为训练集和测试集之前,就对整个数据集进行了重采样。请注意,这等同于同时对训练集和测试集进行重采样的做法。

此类处理方式会导致两个主要问题:

  • 模型不会在一个类别分布与真实应用场景相似的数据集上进行评估。实际上,通过对整个数据集进行重采样,训练集和测试集都可能变得平衡,而模型应在一个天然不平衡的数据集上进行测试,以评估其潜在的偏差;
  • 重采样过程可能会利用数据集中样本的相关信息来生成或选择某些样本。因此,我们可能会提前使用了原本应保留在测试阶段的信息,从而导致典型的数据泄漏问题。

接下来,我们将演示错误与正确的采样方式,并强调应该使用的工具,帮助大家避免掉入这些陷阱。

我们将使用成人人口普查数据集。为了简化起见,我们仅使用数值型特征。此外,我们还会让数据集的类别分布更加不平衡,以放大因类别不平衡带来的影响:

>>> from sklearn.datasets import fetch_openml
>>> from imblearn.datasets import make_imbalance
>>> X, y = fetch_openml(
...     data_id=1119, as_frame=True, return_X_y=True
... )
>>> X = X.select_dtypes(include="number")
>>> X, y = make_imbalance(
...     X, y, sampling_strategy={">50K": 300}, random_state=1
... )

我们首先查看该数据集中各类别的比例情况:

>>> from collections import Counter
>>> {key: value / len(y) for key, value in Counter(y).items()}
{'<=50K': 0.988..., '>50K': 0.011...}

为了稍后更清楚地展示一些问题,我们会预留一部分数据作为测试集,这部分数据将不会用于模型的评估:

>>> from sklearn.model_selection import train_test_split
>>> X, X_left_out, y, y_left_out = train_test_split(
...     X, y, stratify=y, random_state=0
... )

我们将使用 sklearn.ensemble.HistGradientBoostingClassifier 作为基线分类器。首先,我们将在不做任何预处理的情况下训练并检查该分类器的表现,以体现其对多数类的偏向性。我们通过交叉验证来评估分类器的泛化性能:

>>> from sklearn.ensemble import HistGradientBoostingClassifier
>>> from sklearn.model_selection import cross_validate
>>> model = HistGradientBoostingClassifier(random_state=0)
>>> cv_results = cross_validate(
...     model, X, y, scoring="balanced_accuracy",
...     return_train_score=True, return_estimator=True,
...     n_jobs=-1
... )
>>> print(
...     f"Balanced accuracy mean +/- std. dev.: "
...     f"{cv_results['test_score'].mean():.3f} +/- "
...     f"{cv_results['test_score'].std():.3f}"
... )
Balanced accuracy mean +/- std. dev.: 0.609 +/- 0.024

我们可以看到,分类器在平衡准确率(balanced accuracy)方面的表现并不理想,这主要是由于类别不平衡问题所导致的。

在交叉验证过程中,我们保存了所有折(fold)中训练得到的各个分类器。接下来我们将展示,将这些分类器在保留数据上进行评估时,会得到相近的统计性能:

>>> import numpy as np
>>> from sklearn.metrics import balanced_accuracy_score
>>> scores = []
>>> for fold_id, cv_model in enumerate(cv_results["estimator"]):
...     scores.append(
...         balanced_accuracy_score(
...             y_left_out, cv_model.predict(X_left_out)
...         )
...     )
>>> print(
...     f"Balanced accuracy mean +/- std. dev.: "
...     f"{np.mean(scores):.3f} +/- {np.std(scores):.3f}"
... )
Balanced accuracy mean +/- std. dev.: 0.628 +/- 0.009

现在我们来演示一个在处理类别不平衡问题时错误的做法模式。我们将使用采样器来对整个数据集进行重采样以实现类别平衡,并通过交叉验证来评估分类器的统计性能:

>>> from imblearn.under_sampling import RandomUnderSampler
>>> sampler = RandomUnderSampler(random_state=0)
>>> X_resampled, y_resampled = sampler.fit_resample(X, y)
>>> model = HistGradientBoostingClassifier(random_state=0)
>>> cv_results = cross_validate(
...     model, X_resampled, y_resampled, scoring="balanced_accuracy",
...     return_train_score=True, return_estimator=True,
...     n_jobs=-1
... )
>>> print(
...     f"平衡准确率均值 +/- 标准差: "
...     f"{cv_results['test_score'].mean():.3f} +/- "
...     f"{cv_results['test_score'].std():.3f}"
... )
平衡准确率均值 +/- 标准差: 0.724 +/- 0.042

交叉验证的结果看起来不错,但如果在保留数据上评估分类器,则呈现出不同的结果:

>>> scores = []
>>> for fold_id, cv_model in enumerate(cv_results["estimator"]):
...     scores.append(
...         balanced_accuracy_score(
...             y_left_out, cv_model.predict(X_left_out)
...        )
...     )
>>> print(
...     f"平衡准确率均值 +/- 标准差: "
...     f"{np.mean(scores):.3f} +/- {np.std(scores):.3f}"
... )
平衡准确率均值 +/- 标准差: 0.698 +/- 0.014

我们可以看到此时的性能反而不如交叉验证时的表现。事实上,正如本节前面所述,由于数据泄露的存在,我们之前得到了过于乐观的结果。

现在我们将展示一个正确的使用方式。与 scikit-learn 类似,使用 Pipeline 可以避免任何数据泄露问题,因为重采样将交由 imbalanced-learn 处理,而无需手动干预:

>>> from imblearn.pipeline import make_pipeline
>>> model = make_pipeline(
...     RandomUnderSampler(random_state=0),
...     HistGradientBoostingClassifier(random_state=0)
... )
>>> cv_results = cross_validate(
...     model, X, y, scoring="balanced_accuracy",
...     return_train_score=True, return_estimator=True,
...     n_jobs=-1
... )
>>> print(
...     f"Balanced accuracy mean +/- std. dev.: "
...     f"{cv_results['test_score'].mean():.3f} +/- "
...     f"{cv_results['test_score'].std():.3f}"
... )
Balanced accuracy mean +/- std. dev.: 0.732 +/- 0.019

我们发现,统计性能同样不错。不过现在我们可以检查每次交叉验证中模型的表现,以确保各次结果相近:

>>> scores = []
>>> for fold_id, cv_model in enumerate(cv_results["estimator"]):
...     scores.append(
...         balanced_accuracy_score(
...             y_left_out, cv_model.predict(X_left_out)
...        )
...     )
>>> print(
...     f"Balanced accuracy mean +/- std. dev.: "
...     f"{np.mean(scores):.3f} +/- {np.std(scores):.3f}"
... )
Balanced accuracy mean +/- std. dev.: 0.727 +/- 0.008

可以看出,实际的统计性能与交叉验证的结果非常接近,没有任何过度乐观的迹象。

该篇文章由ChatGPT翻译,如有疑问,欢迎提交Issues


网站公告

今日签到

点亮在社区的每一天
去签到