8. 常见陷阱和推荐做法#
本节是对scikit-learn中提供的文档的补充 [这里]。 实际上,我们将强调误用重采样的问题,导致 数据泄露。由于这种泄露,报告的模型性能 将会过于乐观。
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
我们看到分类器在平衡准确率方面表现不佳,主要是由于类别不平衡问题。
在交叉验证中,我们存储了所有折的不同分类器。我们将展示,在留出数据上评估这些分类器将给出接近的统计性能:
>>> 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"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.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"Balanced accuracy mean +/- std. dev.: "
... f"{np.mean(scores):.3f} +/- {np.std(scores):.3f}"
... )
Balanced accuracy mean +/- std. dev.: 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
我们看到统计性能与我们进行的交叉验证研究非常接近,没有任何过度乐观结果的迹象。