如果我们在管道中包含转换器,来自 scikit-learn 的“cross_val_score”和“GridsearchCV”的 k 折交叉验证分数是否有偏差?

Are the k-fold cross-validation scores from scikit-learn's `cross_val_score` and `GridsearchCV` biased if we include transformers in the pipeline?

StandardScaler 等数据预处理程序应该用于 fit_transform 训练集并且只转换(不适合)测试集。我希望相同的 fit/transform 过程适用于交叉验证以调整模型。但是,我发现 cross_val_scoreGridSearchCV fit_transform 整个训练集都带有预处理器(而不是 fit_transform 和 inner_train 集,并转换 inner_validation 放)。我相信这人为地消除了 inner_validation 集合的方差,这使得 cv 分数(GridSearch 用于 select 最佳模型的指标)有偏差。这是一个问题还是我真的错过了什么?

为了证明上述问题,我使用来自 Kaggle 的乳腺癌威斯康星(诊断)数据集尝试了以下三个简单的测试用例。

  1. 我故意用StandardScaler()
  2. 来拟合和变换整个X
X_sc = StandardScaler().fit_transform(X)
lr = LogisticRegression(penalty='l2', random_state=42)
cross_val_score(lr, X_sc, y, cv=5)
  1. 我在 Pipeline 和 运行 cross_val_score
  2. 中包括了 SC 和 LR
pipe = Pipeline([
    ('sc', StandardScaler()),
    ('lr', LogisticRegression(penalty='l2', random_state=42))
])
cross_val_score(pipe, X, y, cv=5)
  1. 与 2 相同,但具有 GridSearchCV
pipe = Pipeline([
    ('sc', StandardScaler()),
    ('lr', LogisticRegression(random_state=42))
])
params = {
    'lr__penalty': ['l2']
}
gs=GridSearchCV(pipe,
param_grid=params, cv=5).fit(X, y)
gs.cv_results_

它们都产生相同的验证分数。 [0.9826087, 0.97391304, 0.97345133, 0.97345133, 0.99115044]

学习预测函数的参数并在相同的数据上对其进行测试是一个方法论错误:一个只会重复它刚刚看到的样本标签的模型会有一个完美的分数,但无法预测任何对尚未见过的数据有用的东西。这种情况称为过拟合。为了避免这种情况,在执行(监督的)机器学习实验时,通常的做法是将部分可用数据作为测试集 X_test、y_test

这个问题的解决方案是称为交叉验证(简称 CV)的过程。测试集仍应保留用于最终评估,但在进行 CV 时不再需要验证集。在称为 k-fold CV 的基本方法中,训练集被分成 k 个较小的集合(其他方法在下面描述,但通常遵循相同的原则)。对于 k 个“折叠”中的每一个都遵循以下过程:

使用折叠作为训练数据训练模型; 生成的模型在数据的其余部分上得到验证(即,它被用作测试集来计算性能度量,例如准确性)。 k 折交叉验证报告的性能度量是循环中计算的值的平均值。这种方法在计算上可能很昂贵,但不会浪费太多数据(固定任意验证集时就是这种情况),这在样本数量非常少的逆向推理等问题中是一个主要优势。

此外,如果您的模型已经从一开始就存在偏差,我们必须通过 SMOTE / 低目标 Variable/Under-sampling 高目标变量的过采样来使其平衡。

不,sklearn 不会对整个数据集进行 fit_transform

为了检查这一点,我将 StandardScaler 子类化以打印发送给它的数据集的大小。

class StScaler(StandardScaler):
    def fit_transform(self,X,y=None):
        print(len(X))
        return super().fit_transform(X,y)

如果您现在替换代码中的 StandardScaler,您会看到第一种情况下传递的数据集大小实际上更大。

但为什么准确度保持完全相同?我认为这是因为 LogisticRegression 对特征尺度不是很敏感。如果我们改为使用对比例非常敏感的分类器,例如 KNeighborsClassifier,您会发现两种情况之间的准确性开始发生变化。

X,y = load_breast_cancer(return_X_y=True)
X_sc = StScaler().fit_transform(X)
lr = KNeighborsClassifier(n_neighbors=1)
cross_val_score(lr, X_sc,y, cv=5)

输出:

569
[0.94782609 0.96521739 0.97345133 0.92920354 0.9380531 ]

而第二种情况,

pipe = Pipeline([
    ('sc', StScaler()),
    ('lr', KNeighborsClassifier(n_neighbors=1))
])
print(cross_val_score(pipe, X, y, cv=5))

输出:

454
454
456
456
456
[0.95652174 0.97391304 0.97345133 0.92920354 0.9380531 ]

精度方面变化不大,但还是有变化。