使用 sklearn 在嵌套交叉验证中使用 GroupKFold

Posted

技术标签:

【中文标题】使用 sklearn 在嵌套交叉验证中使用 GroupKFold【英文标题】:Use GroupKFold in nested cross-validation using sklearn 【发布时间】:2020-07-14 17:51:22 【问题描述】:

我的代码是基于sklearn网站上的例子:https://scikit-learn.org/stable/auto_examples/model_selection/plot_nested_cross_validation_iris.html

我正在尝试在内部和外部 cv 中使用 GroupKFold。

from sklearn.datasets import load_iris
from matplotlib import pyplot as plt
from sklearn.svm import SVC
from sklearn.model_selection import GridSearchCV, cross_val_score, KFold,GroupKFold
import numpy as np

# Load the dataset
iris = load_iris()
X_iris = iris.data
y_iris = iris.target

# Set up possible values of parameters to optimize over
p_grid = "C": [1, 10, 100],
          "gamma": [.01, .1]

# We will use a Support Vector Classifier with "rbf" kernel
svm = SVC(kernel="rbf")

# Choose cross-validation techniques for the inner and outer loops,
# independently of the dataset.
# E.g "GroupKFold", "LeaveOneOut", "LeaveOneGroupOut", etc.
inner_cv = GroupKFold(n_splits=3)
outer_cv = GroupKFold(n_splits=3)

# Non_nested parameter search and scoring
clf = GridSearchCV(estimator=svm, param_grid=p_grid, cv=inner_cv)

# Nested CV with parameter optimization
nested_score = cross_val_score(clf, X=X_iris, y=y_iris, cv=outer_cv, groups=y_iris)

我知道将 y 值放入 groups 参数不是它的用途! 对于此代码,我收到以下错误。

.../anaconda3/lib/python3.7/site-packages/sklearn/model_selection/_validation.py:536: FitFailedWarning: Estimator fit failed. The score on this train-test partition for these parameters will be set to nan. Details: 
ValueError: The 'groups' parameter should not be None.

有人知道如何解决这个问题吗?

提前感谢您的帮助,

索伦

【问题讨论】:

【参考方案1】:

我遇到了一个类似的问题,我发现@Samalama 的解决方案很好。 我唯一需要更改的是fit 电话。我也不得不对groups 进行切片,与火车组的Xy 的形状相同。 否则,我会收到一条错误消息,指出三个对象的形状不相同。这是一个正确的实现吗?

for train_index, test_index in outer_cv.split(x, y, groups=groups):
    x_train, x_test = x[train_index], x[test_index]
    y_train, y_test = y[train_index], y[test_index]

    grid = RandomizedSearchCV(estimator=model,
                                param_distributions=parameters_grid,
                                cv=inner_cv,
                                scoring=get_scoring(),
                                refit='roc_auc_scorer',
                                return_train_score=True,
                                verbose=1,
                                n_jobs=jobs)
    grid.fit(x_train, y_train, groups=groups[train_index])
    prediction = grid.predict(x_test)

【讨论】:

【参考方案2】:

我自己一直在尝试使用 GroupKFold 实现嵌套 CV,还尝试按照您所引用的 sklearn 提供的示例进行操作,并最终遇到与您相同的错误,找到了这个线程。

我认为 ywbaek 的回答没有正确解决问题。

经过一番搜索,我发现在 sklearn Github 上提出了一些问题,与此特定问题或似乎是同一问题的其他形式有关。我认为这与未将 groups 参数传播到所有方法有关(我试图追踪它在脚本中失败的位置,但很快就迷路了)。

这里的问题:

https://github.com/scikit-learn/scikit-learn/issues/7646 https://github.com/scikit-learn/scikit-learn/issues/11429 https://github.com/scikit-learn/scikit-learn/issues/12052

如您所见,这些可以追溯到一段时间(到 2016 年 10 月)。我对开发知之甚少,但显然解决这个问题并不是优先考虑的事情。我想这很好,但是嵌套 CV 的示例特别建议使用 GroupKFold 提供的方法,这是不可能的,因此应该更新。

如果你仍然想用 GroupKFold 做一个嵌套的简历,当然还有其他方法可以做到。 逻辑回归示例:

from sklearn.model_selection import GridSearchCV, GroupKFold

pred_y = []
true_y = []

model = sklearn.linear_model.LogisticRegression()
Cs=[1,10,100]
p_grid='C': Cs

inner_CV = GroupKFold(n_splits = 4)
outer_CV = GroupKFold(n_splits = 4)

for train_index, test_index in outer_CV.split(X, y, groups=group):
    X_tr, X_tt = X[train_index,:], X[test_index,:]
    y_tr, y_tt = Y[train_index], Y[test_index]

    clf = GridSearchCV(estimator=model, param_grid=p_grid, cv=inner_CV)
    clf.fit(X_tr,y_tr,groups=group)

    pred = clf.predict(X_tt)   
    pred_y.extend(pred)
    true_y.extend(y_tt)

然后,您可以根据自己的喜好根据事实评估预测。当然,如果你仍然对比较嵌套和非嵌套分数感兴趣,你也可以收集我在这里没有做过的非嵌套分数。

【讨论】:

【参考方案3】:

对于现在回到这里并像我一样有兴趣将 GroupKFold 交叉验证传递给 cross_val_score() 的任何人...

cross_val_score() 分别接受 cv = GroupKFold() 和 groups 参数。

这对我想要达到的目标起到了作用。

例如:

cv_outer = GroupKFold(n_splits=n_unique_groups)
groups = X['your_group_name'] # or pass your group another way

.... ML Code ...
    
scores = cross_val_score(search, X, y, scoring='f1', cv=cv_outer, groups = groups)

【讨论】:

【参考方案4】:

好的,所以嵌套 CV 的一个简单解决方案是使用 cross_validatefit_params 函数:

nested_score = cross_val_score(clf, X=X_iris, y=y_iris, cv=outer_cv, groups=y_iris, fit_params="groups": y_iris)

这会将组下推到GridSearchCV。但是,由于您的方法存在一些概念性问题,您正在做的事情仍然会引发一堆异常(这在某种程度上扩展和补充了 @ywbaek 's answer)。让我们检查一下:

    因此,当您执行GroupKFold 时,它将确保一组中的所有样本都在训练或测试中。您将这些组设置为 iris 数据集中的三个目标类 ([0,1,2])。

    这意味着outer_cv(与n_splits=3)将创建一个折叠,其中两个类在训练中,其余类在测试中。

    for train_idx, test_idx in outer_cv.split(X_iris, y_iris, groups=y_iris):
        print(np.unique(y_iris[test_idx]))
    

    这实际上没有任何意义,因为模型不会了解有关测试数据的任何信息。但是让我们继续一下:

    inner_cv 中,我们将只有两个类,这将始终打破GroupKFold(n_splits=3),因为我们将只有两个可能的组。

    所以让我们暂时将inner_cv 设置为GroupKFold(n_splits=2)。这解决了之前的问题。但是我们将只有一堂训练课和一堂测试课。在这种情况下,分类器会抱怨训练数据中只有一个类,它什么都学不到。

因此,总的来说,虽然上面基于 fit_params 参数的解决方案允许您进行嵌套交叉验证,但它并不能解决您在方法中遇到的概念问题。我希望我的解释有助于更清楚地说明这一点。

【讨论】:

【参考方案5】:

正如您从documentation 中看到的GroupKFold, 当您想要K-fold 的非重叠组时,您可以使用它。 这意味着除非在创建 K-fold 时需要分离不同的数据组,否则不要使用此方法。

话虽如此,对于给定的示例,您必须手动创建groups, 它应该是一个类似数组的对象,其形状与您的 y 相同。 和

不同组的数量必须至少等于 折叠次数

以下是文档中的示例代码:

import numpy as np
from sklearn.model_selection import GroupKFold
X = np.array([[1, 2], [3, 4], [5, 6], [7, 8]])
y = np.array([1, 2, 3, 4])
groups = np.array([0, 0, 2, 2])
group_kfold = GroupKFold(n_splits=2)
group_kfold.get_n_splits(X, y, groups)

可以看到groupsy的形状一样, 并且它有两个不同的组0, 2,这与折叠的数量相同。

已编辑:GroupKFold 对象的get_n_splits(groups) 方法返回交叉验证器中的拆分迭代次数,我们可以将其传递为cv 关键字到 cross_val_score 函数的参数。

clf = GridSearchCV(estimator=svm, 
                   param_grid=p_grid, 
                   cv=inner_cv.get_n_splits(groups=y_iris))

nested_score = cross_val_score(clf, X=X_iris, y=y_iris, 
                               cv=outer_cv.get_n_splits(groups=y_iris))

【讨论】:

谢谢,这就是为什么我使用 y 作为示例。您的示例使其更加清晰,但没有解决嵌套交叉验证中的异常。 我看不出这里编辑的解决方案是如何工作的。 cv=outer_cv.get_n_splits(groups=y_iris) 意味着只会将一个 int 传递给 cv,因此您最终会得到常规的 cv 拆分,而不是分组。【参考方案6】:

对于该聚会的后来者,此技术不需要更改 sklearn 对象(例如 LogisticRegressionCV、GridSearchCV),并且可以在多个 sklearn 设置中透明地工作。

我模拟了一个包含组的 cv 对象:

class PseudoGroupCV:
    def __init__(self, cv_obj, groups):
        self.cv = cv_obj
        self.groups=groups
    def split(self, X,y, groups=None):
        return self.cv.split(X,y, groups=self.groups)
    def get_n_splits(self, X, y, groups):
        return self.cv.get_n_splits(X,y, groups)

然后您可以将其传递给例如像这样的GridSearchCV:

kfold = GroupKFold(n_splits=5) # desired CV object
clf = GridSearchCV(estimator=svm, 
                    param_grid=p_grid, 
                    cv=PseudoGroupKFold(kfold, groups)
                   )

这应该可以正常工作。它也适用于 Pipeline 对象。

唯一的缺点是您需要在类声明时提供组(即groups 需要匹配用于拟合的(X, y))。

【讨论】:

【参考方案7】:

@Martin Becker 的回答是正确的。 GridSearchCVGroupKFold 一起使用时,不仅期望得到 Xy,而且在 fit 方法中得到 groups。要传递该参数,您需要使用cross_val_score 函数的fit_params 参数。

这是一个例子。为了简单起见,我将GroupKFold 替换为LeaveOneGroupOut

import numpy as np
from sklearn.base import BaseEstimator
from sklearn.model_selection import \
    LeaveOneGroupOut, cross_val_score, GridSearchCV

# Create 12 samples and 4 groups [0, 1, 2] [3, 4, 5], ...
X = np.arange(12)
y = np.random.randint(0, 1, len(X))
groups = X // 3

class DummyEstimator(BaseEstimator):
    """Estimator that just prints given folds."""
    def fit(self, X, y=None):
        print('Trained on', np.unique(X // 3))
        return [0]*len(X)
    def score(self, X, y):
        print('Tested on', np.unique(X // 3))
        return 0

logo = LeaveOneGroupOut()
clf = GridSearchCV(DummyEstimator(), param_grid=, cv=logo)
cross_val_score(
    clf, X, y, 
    cv=logo, groups=groups, fit_params='groups': groups,
    n_jobs=None)

代码生成以下训练/验证/测试组:

Trained on [2 3]  <-- First inner loop (Test fold=0, Train=1, 2, 3)
Tested on  [1]
Trained on [1 3]
Tested on  [2]
Trained on [1 2]
Tested on  [3]
Trained on [1 2 3]  <-- fit best params on the whole training data
Tested on  [0]      <-- Score on the test fold 0
Trained on [2 3]  <-- Second inner loop (Test fold=1, Train=0 2 3)
Tested on  [0]
Trained on [0 3]
Tested on  [2]
Trained on [0 2]
Tested on  [3]
Trained on [0 2 3]  <-- fit best params on the whole training data
Tested on  [1]      <-- Score on the test fold 1
... and so one

【讨论】:

以上是关于使用 sklearn 在嵌套交叉验证中使用 GroupKFold的主要内容,如果未能解决你的问题,请参考以下文章

有没有办法使用 SKlearn 获得滑动嵌套交叉验证?

SKlearn中具有嵌套交叉验证的分类报告(平均值/个体值)

sklearn中的交叉验证+决策树

如何在 sklearn 中编写自定义估算器并对其使用交叉验证?

如何在 python 的 sklearn 中使用交叉验证执行 SMOTE

在 Sklearn 管道和交叉验证中使用缩放器