3.1. 交叉验证:评估估计器性能#

学习预测函数的参数并在同一数据上进行测试是一种方法上的错误:一个只会重复它刚刚看到的样本标签的模型会得到满分,但在尚未见过的数据上却无法做出任何有用的预测。这种情况被称为过拟合(overfitting)。为了避免这种情况,在进行(有监督的)机器学习实验时,通常的做法是留出一部分可用数据作为测试集(test set) X_test, y_test。请注意,“实验”一词并非仅指学术用途,因为即使在商业环境中,机器学习通常也是从实验开始的。以下是模型训练中典型交叉验证工作流程的流程图。最佳参数可以通过网格搜索技术确定。

Grid Search Workflow

在scikit-learn中,可以使用train_test_split辅助函数快速计算训练集和测试集的随机拆分。让我们加载iris数据集,并在其上拟合一个线性支持向量机

>>> import numpy as np
>>> from sklearn.model_selection import train_test_split
>>> from sklearn import datasets
>>> from sklearn import svm

>>> X, y = datasets.load_iris(return_X_y=True)
>>> X.shape, y.shape
((150, 4), (150,))

现在我们可以快速抽取一个训练集,同时保留40%的数据用于测试(评估)我们的分类器

>>> X_train, X_test, y_train, y_test = train_test_split(
...     X, y, test_size=0.4, random_state=0)

>>> X_train.shape, y_train.shape
((90, 4), (90,))
>>> X_test.shape, y_test.shape
((60, 4), (60,))

>>> clf = svm.SVC(kernel='linear', C=1).fit(X_train, y_train)
>>> clf.score(X_test, y_test)
0.96

当评估估计器(例如必须为SVM手动设置的C设置)的不同设置(“超参数”)时,仍然存在在测试集上过拟合的风险,因为可以对参数进行调整,直到估计器表现最佳。这样,关于测试集的知识就会“泄露”到模型中,评估指标不再报告泛化性能。为了解决这个问题,可以再留出数据集的另一部分作为所谓的“验证集(validation set)”:在训练集上进行训练,然后对验证集进行评估,当实验看起来成功时,可以在测试集上进行最终评估。

然而,通过将可用数据划分为三个集合,我们大幅减少了可用于学习模型的样本数量,并且结果可能取决于(训练集、验证集)对的特定随机选择。

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

  • 使用\(k-1\)个折叠作为训练数据来训练模型;

  • 在剩余的数据部分上验证生成的模型(即,将其用作测试集来计算性能指标,例如准确率)。

然后,通过k折交叉验证报告的性能指标是循环中计算值的平均值。这种方法计算成本可能较高,但不会浪费太多数据(不像固定任意验证集的情况),这在样本数量非常小的逆向推理等问题中是一个主要优势。

A depiction of a 5 fold cross validation on a training set, while holding out a test set.

3.1.1. 计算交叉验证指标#

使用交叉验证最简单的方法是调用cross_val_score辅助函数,传入估计器和数据集。

以下示例演示了如何通过拆分数据、拟合模型并计算分数5次(每次拆分不同)来估计鸢尾花数据集上的线性核支持向量机的准确率。

>>> from sklearn.model_selection import cross_val_score
>>> clf = svm.SVC(kernel='linear', C=1, random_state=42)
>>> scores = cross_val_score(clf, X, y, cv=5)
>>> scores
array([0.96, 1. , 0.96, 0.96, 1. ])

平均分数和标准差因此由下式给出

>>> print("%0.2f accuracy with a standard deviation of %0.2f" % (scores.mean(), scores.std()))
0.98 accuracy with a standard deviation of 0.02

默认情况下,在每次CV迭代中计算的分数是估计器的score方法。可以通过使用scoring参数来更改此设置

>>> from sklearn import metrics
>>> scores = cross_val_score(
...     clf, X, y, cv=5, scoring='f1_macro')
>>> scores
array([0.96, 1., 0.96, 0.96, 1.])

有关详细信息,请参阅评分参数:定义模型评估规则。在Iris数据集的情况下,样本在目标类别之间是平衡的,因此准确率和F1分数几乎相等。

cv参数是一个整数时,cross_val_score默认使用KFoldStratifiedKFold策略,如果估计器派生自ClassifierMixin,则使用后者。

也可以通过传入交叉验证迭代器来使用其他交叉验证策略,例如

>>> from sklearn.model_selection import ShuffleSplit
>>> n_samples = X.shape[0]
>>> cv = ShuffleSplit(n_splits=5, test_size=0.3, random_state=0)
>>> cross_val_score(clf, X, y, cv=cv)
array([0.977, 0.977, 1., 0.955, 1.])

另一种选择是使用一个可迭代对象,该对象以索引数组的形式生成(训练,测试)拆分,例如

>>> def custom_cv_2folds(X):
...     n = X.shape[0]
...     i = 1
...     while i <= 2:
...         idx = np.arange(n * (i - 1) / 2, n * i / 2, dtype=int)
...         yield idx, idx
...         i += 1
...
>>> custom_cv = custom_cv_2folds(X)
>>> cross_val_score(clf, X, y, cv=custom_cv)
array([1.        , 0.973])
使用保留数据进行数据转换#

正如对保留在训练之外的数据进行预测器测试很重要一样,预处理(例如标准化、特征选择等)和类似的数据转换也应从训练集学习并应用于保留数据进行预测

>>> from sklearn import preprocessing
>>> X_train, X_test, y_train, y_test = train_test_split(
...     X, y, test_size=0.4, random_state=0)
>>> scaler = preprocessing.StandardScaler().fit(X_train)
>>> X_train_transformed = scaler.transform(X_train)
>>> clf = svm.SVC(C=1).fit(X_train_transformed, y_train)
>>> X_test_transformed = scaler.transform(X_test)
>>> clf.score(X_test_transformed, y_test)
0.9333

一个Pipeline使组合估计器变得更容易,在交叉验证下提供了这种行为

>>> from sklearn.pipeline import make_pipeline
>>> clf = make_pipeline(preprocessing.StandardScaler(), svm.SVC(C=1))
>>> cross_val_score(clf, X, y, cv=cv)
array([0.977, 0.933, 0.955, 0.933, 0.977])

请参阅管道和复合估计器

3.1.1.1. cross_validate函数和多指标评估#

cross_validate函数在两个方面不同于cross_val_score

  • 它允许指定多个指标进行评估。

  • 除了测试分数之外,它还返回一个包含拟合时间、评分时间(以及可选的训练分数、拟合估计器、训练-测试拆分索引)的字典。

对于单个指标评估,其中scoring参数是字符串、可调用对象或None,键将是 - ['test_score', 'fit_time', 'score_time']

对于多个指标评估,返回值是一个包含以下键的字典 - ['test_<scorer1_name>', 'test_<scorer2_name>', 'test_<scorer...>', 'fit_time', 'score_time']

return_train_score默认设置为False以节省计算时间。要同时评估训练集上的分数,需要将其设置为True。通过设置return_estimator=True,您也可以保留在每个训练集上拟合的估计器。类似地,您可以设置return_indices=True以保留用于将数据集拆分为训练集和测试集的训练和测试索引,用于每个cv拆分。

可以以预定义评分器名称的列表、元组或集合的形式指定多个指标

>>> from sklearn.model_selection import cross_validate
>>> from sklearn.metrics import recall_score
>>> scoring = ['precision_macro', 'recall_macro']
>>> clf = svm.SVC(kernel='linear', C=1, random_state=0)
>>> scores = cross_validate(clf, X, y, scoring=scoring)
>>> sorted(scores.keys())
['fit_time', 'score_time', 'test_precision_macro', 'test_recall_macro']
>>> scores['test_recall_macro']
array([0.96, 1., 0.96, 0.96, 1.])

或者以将评分器名称映射到预定义或自定义评分函数的字典形式

>>> from sklearn.metrics import make_scorer
>>> scoring = {'prec_macro': 'precision_macro',
...            'rec_macro': make_scorer(recall_score, average='macro')}
>>> scores = cross_validate(clf, X, y, scoring=scoring,
...                         cv=5, return_train_score=True)
>>> sorted(scores.keys())
['fit_time', 'score_time', 'test_prec_macro', 'test_rec_macro',
 'train_prec_macro', 'train_rec_macro']
>>> scores['train_rec_macro']
array([0.97, 0.97, 0.99, 0.98, 0.98])

这是使用单个指标的cross_validate示例

>>> scores = cross_validate(clf, X, y,
...                         scoring='precision_macro', cv=5,
...                         return_estimator=True)
>>> sorted(scores.keys())
['estimator', 'fit_time', 'score_time', 'test_score']

3.1.1.2. 通过交叉验证获取预测结果#

函数cross_val_predict具有与cross_val_score类似的接口,但它返回输入中每个元素在它位于测试集中时获得的预测结果。只能使用将所有元素分配给测试集恰好一次的交叉验证策略(否则,会引发异常)。

警告

关于cross_val_predict不当使用的注意事项

cross_val_predict的结果可能与使用cross_val_score获得的结果不同,因为元素的组合方式不同。函数cross_val_score对交叉验证折叠进行平均,而cross_val_predict只是返回来自几个不同模型的标签(或概率),没有区分。因此,cross_val_predict不是衡量泛化误差的合适指标。

cross_val_predict函数适用于
  • 可视化从不同模型获得的预测结果。

  • 模型融合:当一个有监督估计器的预测结果用于在集成方法中训练另一个估计器时。

可用的交叉验证迭代器将在下一节介绍。

示例

3.1.2. 交叉验证迭代器#

以下各节列出了可用于根据不同交叉验证策略生成数据集拆分的索引的实用程序。

3.1.2.1. 用于i.i.d.数据的交叉验证迭代器#

假设某些数据是独立同分布(Independent and Identically Distributed,i.i.d.)意味着所有样本都源自相同的生成过程,并且假设生成过程没有过去生成样本的记忆。

在这些情况下可以使用以下交叉验证器。

注意

虽然i.i.d.数据是机器学习理论中的常见假设,但在实践中很少成立。如果知道样本是使用时间依赖过程生成的,那么使用时间序列感知的交叉验证方案会更安全。类似地,如果我们知道生成过程具有组结构(从不同受试者、实验、测量设备收集的样本),那么使用按组划分的交叉验证会更安全。

3.1.2.1.1. K折交叉验证(K-fold)#

KFold将所有样本划分为\(k\)个大小相等的样本组,称为折叠(如果\(k = n\),则等同于留一法策略)。使用\(k - 1\)个折叠来学习预测函数,剩下的折叠用于测试。

对包含4个样本的数据集进行2折交叉验证的示例

>>> import numpy as np
>>> from sklearn.model_selection import KFold

>>> X = ["a", "b", "c", "d"]
>>> kf = KFold(n_splits=2)
>>> for train, test in kf.split(X):
...     print("%s %s" % (train, test))
[2 3] [0 1]
[0 1] [2 3]

这是交叉验证行为的可视化。请注意,KFold不受类别或组的影响。

../_images/sphx_glr_plot_cv_indices_006.png

每个折叠由两个数组组成:第一个与训练集相关,第二个与测试集相关。因此,可以使用numpy索引创建训练集/测试集

>>> X = np.array([[0., 0.], [1., 1.], [-1., -1.], [2., 2.]])
>>> y = np.array([0, 1, 0, 1])
>>> X_train, X_test, y_train, y_test = X[train], X[test], y[train], y[test]

3.1.2.1.2. 重复K折交叉验证(Repeated K-Fold)#

RepeatedKFoldKFold重复\(n\)次,每次重复产生不同的拆分。

重复2次的2折K折交叉验证示例

>>> import numpy as np
>>> from sklearn.model_selection import RepeatedKFold
>>> X = np.array([[1, 2], [3, 4], [1, 2], [3, 4]])
>>> random_state = 12883823
>>> rkf = RepeatedKFold(n_splits=2, n_repeats=2, random_state=random_state)
>>> for train, test in rkf.split(X):
...     print("%s %s" % (train, test))
...
[2 3] [0 1]
[0 1] [2 3]
[0 2] [1 3]
[1 3] [0 2]

类似地,RepeatedStratifiedKFoldStratifiedKFold重复\(n\)次,每次重复采用不同的随机化。

3.1.2.1.3. 留一法(Leave One Out,LOO)#

LeaveOneOut(或LOO)是一种简单的交叉验证。每个学习集都是通过取除一个样本之外的所有样本创建的,测试集就是被留下的那个样本。因此,对于\(n\)个样本,我们有\(n\)个不同的训练集和\(n\)个不同的测试集。这种交叉验证过程不会浪费太多数据,因为训练集中只移除了一个样本

>>> from sklearn.model_selection import LeaveOneOut

>>> X = [1, 2, 3, 4]
>>> loo = LeaveOneOut()
>>> for train, test in loo.split(X):
...     print("%s %s" % (train, test))
[1 2 3] [0]
[0 2 3] [1]
[0 1 3] [2]
[0 1 2] [3]

LOO模型选择的潜在用户应权衡一些已知的注意事项。与\(k\)折交叉验证相比,LOO从\(n\)个样本中构建\(n\)个模型,而不是\(k\)个模型,其中\(n > k\)。此外,每个模型都在\(n - 1\)个样本上进行训练,而不是\((k-1) n / k\)。从这两种方式来看,假设\(k\)不是太大且\(k < n\),LOO的计算成本高于\(k\)折交叉验证。

在准确性方面,LOO作为测试误差的估计器通常会导致高方差。直观地说,由于\(n\)个样本中的\(n - 1\)个用于构建每个模型,因此由折叠构建的模型彼此之间以及与从整个训练集构建的模型几乎相同。

然而,如果对于所讨论的训练大小,学习曲线很陡峭,那么5或10折交叉验证可能会高估泛化误差。

作为一般规则,大多数作者和经验证据表明,应首选5或10折交叉验证而不是LOO。

参考文献#

3.1.2.1.4. 留P法(Leave P Out,LPO)#

LeavePOutLeaveOneOut非常相似,因为它通过从完整集中移除\(p\)个样本来创建所有可能的训练集/测试集。对于\(n\)个样本,这将产生\({n \choose p}\)个训练-测试对。与LeaveOneOutKFold不同,测试集对于\(p > 1\)将重叠。

对包含4个样本的数据集进行留2法(Leave-2-Out)的示例

>>> from sklearn.model_selection import LeavePOut

>>> X = np.ones(4)
>>> lpo = LeavePOut(p=2)
>>> for train, test in lpo.split(X):
...     print("%s %s" % (train, test))
[2 3] [0 1]
[1 3] [0 2]
[1 2] [0 3]
[0 3] [1 2]
[0 2] [1 3]
[0 1] [2 3]

3.1.2.1.5. 随机排列交叉验证,又名洗牌拆分(Shuffle & Split)#

ShuffleSplit迭代器将生成用户定义的独立训练/测试数据集拆分数量。样本首先被洗牌,然后被拆分为一对训练集和测试集。

可以通过显式种子random_state伪随机数生成器来控制随机性以重现结果。

这是一个使用示例

>>> from sklearn.model_selection import ShuffleSplit
>>> X = np.arange(10)
>>> ss = ShuffleSplit(n_splits=5, test_size=0.25, random_state=0)
>>> for train_index, test_index in ss.split(X):
...     print("%s %s" % (train_index, test_index))
[9 1 6 7 3 0 5] [2 8 4]
[2 9 8 0 6 7 4] [3 5 1]
[4 5 1 0 6 9 7] [2 3 8]
[2 7 5 8 0 3 4] [6 1 9]
[4 1 0 6 8 9 3] [5 2 7]

这是交叉验证行为的可视化。请注意,ShuffleSplit不受类别或组的影响。

../_images/sphx_glr_plot_cv_indices_008.png

ShuffleSplit因此是KFold交叉验证的一个很好的替代方案,它允许对迭代次数和训练/测试拆分两侧的样本比例进行更精细的控制。

3.1.2.2. 基于类别标签分层的交叉验证迭代器#

某些分类任务自然会存在稀有类别:例如,负样本观察值可能比正样本观察值多出几个数量级(例如医疗筛查、欺诈检测等)。因此,交叉验证拆分可能会生成不包含特定类别的训练或验证折叠。这通常会导致未定义的分类指标(例如ROC AUC),在尝试调用fit时引发异常,或者在对不同折叠训练的多类别分类器的predict_probadecision_function方法的输出中缺少列。

为了缓解这些问题,像StratifiedKFoldStratifiedShuffleSplit这样的拆分器实现了分层抽样,以确保每个折叠中相对类别频率近似保留。

注意

scikit-learn中引入分层抽样是为了解决上述工程问题,而不是解决统计问题。

分层使交叉验证折叠更同质化,因此隐藏了拟合具有有限数量观测值的模型所固有的某些可变性。

因此,分层可以人为地缩小跨交叉验证迭代测量的指标的传播:折叠间的可变性不再反映分类器在存在稀有类别时的性能不确定性。

3.1.2.2.1. 分层K折交叉验证(Stratified K-fold)#

StratifiedKFoldK-fold的一种变体,它返回分层的折叠:每个集合包含的每个目标类别的样本百分比与完整集合大致相同。

这是对具有两个不平衡类别的50个样本数据集进行分层3折交叉验证的示例。我们显示每个类别中的样本数量,并与KFold进行比较。

>>> from sklearn.model_selection import StratifiedKFold, KFold
>>> import numpy as np
>>> X, y = np.ones((50, 1)), np.hstack(([0] * 45, [1] * 5))
>>> skf = StratifiedKFold(n_splits=3)
>>> for train, test in skf.split(X, y):
...     print('train -  {}   |   test -  {}'.format(
...         np.bincount(y[train]), np.bincount(y[test])))
train -  [30  3]   |   test -  [15  2]
train -  [30  3]   |   test -  [15  2]
train -  [30  4]   |   test -  [15  1]
>>> kf = KFold(n_splits=3)
>>> for train, test in kf.split(X, y):
...     print('train -  {}   |   test -  {}'.format(
...         np.bincount(y[train]), np.bincount(y[test])))
train -  [28  5]   |   test -  [17]
train -  [28  5]   |   test -  [17]
train -  [34]   |   test -  [11  5]

我们可以看到StratifiedKFold在训练集和测试数据集中都保留了类别比例(大约1 / 10)。

这是交叉验证行为的可视化。

../_images/sphx_glr_plot_cv_indices_009.png

RepeatedStratifiedKFold可用于重复分层K折交叉验证n次,每次重复采用不同的随机化。

3.1.2.2.2. 分层洗牌拆分(Stratified Shuffle Split)#

StratifiedShuffleSplitShuffleSplit的一种变体,它返回分层的拆分,通过保留与完整集相同的每个目标类别的百分比来创建拆分。

这是交叉验证行为的可视化。

../_images/sphx_glr_plot_cv_indices_012.png

3.1.2.3. 预定义折叠拆分/验证集#

对于某些数据集,数据到训练集和验证折叠或到几个交叉验证折叠的预定义拆分已经存在。使用PredefinedSplit可以使用这些折叠,例如在搜索超参数时。

例如,当使用验证集时,对于属于验证集的所有样本,将test_fold设置为0,对于所有其他样本,设置为-1。

3.1.2.4. 用于分组数据的交叉验证迭代器#

如果底层生成过程产生相互依赖的样本组,则i.i.d.假设就会被打破。

这种数据分组是特定于领域的。一个例子是当有从多个患者收集的医疗数据,每个患者采集多个样本时。这样的数据很可能依赖于单个组。在我们的例子中,每个样本的患者ID将是其组标识符。

在这种情况下,我们想知道在一个特定组集上训练的模型是否能很好地泛化到未见过的组。为了衡量这一点,我们需要确保验证折叠中的所有样本都来自未在配对训练折叠中表示的组。

以下交叉验证拆分器可用于执行此操作。样本的分组标识符通过groups参数指定。

3.1.2.4.1. 组K折交叉验证(Group K-fold)#

GroupKFold是K折交叉验证的一种变体,它确保同一个组不会同时出现在测试集和训练集中。例如,如果数据是从不同受试者获得的,每个受试者有多个样本,并且如果模型足够灵活以至于可以从高度特定于个人的特征中学习,它可能无法泛化到新的受试者。GroupKFold使得检测这种过拟合情况成为可能。

假设您有三个受试者,每个受试者都有一个从1到3的关联数字

>>> from sklearn.model_selection import GroupKFold

>>> X = [0.1, 0.2, 2.2, 2.4, 2.3, 4.55, 5.8, 8.8, 9, 10]
>>> y = ["a", "b", "b", "b", "c", "c", "c", "d", "d", "d"]
>>> groups = [1, 1, 1, 2, 2, 2, 3, 3, 3, 3]

>>> gkf = GroupKFold(n_splits=3)
>>> for train, test in gkf.split(X, y, groups=groups):
...     print("%s %s" % (train, test))
[0 1 2 3 4 5] [6 7 8 9]
[0 1 2 6 7 8 9] [3 4 5]
[3 4 5 6 7 8 9] [0 1 2]

每个受试者都在一个不同的测试折叠中,并且同一个受试者永远不会同时出现在测试集和训练集中。请注意,由于数据不平衡,折叠的大小不完全相同。如果必须在折叠中平衡类别比例,StratifiedGroupKFold是一个更好的选择。

这是交叉验证行为的可视化。

../_images/sphx_glr_plot_cv_indices_007.png

KFold类似,来自GroupKFold的测试集将构成所有数据的完整分区。

shuffle=False时,GroupKFold尝试将相同数量的样本放入每个折叠中,而当shuffle=True时,它尝试将相同数量的不同组放入每个折叠中(但不考虑组大小)。

3.1.2.4.2. 分层组K折交叉验证(StratifiedGroupKFold)#

StratifiedGroupKFold是一种结合了StratifiedKFoldGroupKFold的交叉验证方案。其思想是在每个拆分中尝试保留类别的分布,同时将每个组保持在单个拆分内。当您有一个不平衡的数据集时,这可能很有用,因为仅使用GroupKFold可能会产生倾斜的拆分。

示例

>>> from sklearn.model_selection import StratifiedGroupKFold
>>> X = list(range(18))
>>> y = [1] * 6 + [0] * 12
>>> groups = [1, 2, 3, 3, 4, 4, 1, 1, 2, 2, 3, 4, 5, 5, 5, 6, 6, 6]
>>> sgkf = StratifiedGroupKFold(n_splits=3)
>>> for train, test in sgkf.split(X, y, groups=groups):
...     print("%s %s" % (train, test))
[ 0  2  3  4  5  6  7 10 11 15 16 17] [ 1  8  9 12 13 14]
[ 0  1  4  5  6  7  8  9 11 12 13 14] [ 2  3 10 15 16 17]
[ 1  2  3  8  9 10 12 13 14 15 16 17] [ 0  4  5  6  7 11]
实现说明#
  • 在当前的实现中,在大多数情况下无法进行完全洗牌。当 shuffle=True 时,会发生以下情况:

    1. 所有组都被洗牌。

    2. 使用稳定排序(stable sort)按类别的标准差对组进行排序。

    3. 迭代排序后的组并将它们分配给折叠。

    这意味着只有具有相同类别分布标准差的组才会被洗牌,这在每个组只有一个类别时可能很有用。

  • 该算法贪婪地将每个组分配给n_splits个测试集中的一个,选择使跨测试集类别分布方差最小的测试集。组分配从类别频率方差最高到最低的组开始,即主要集中在一个或少数类别的较大组首先被分配。

  • 这种拆分在某种意义上是次优的,即使可以进行完美分层,它也可能产生不平衡的拆分。如果每个组中的类别分布相对接近,使用GroupKFold会更好。

这是不均匀组的交叉验证行为的可视化

../_images/sphx_glr_plot_cv_indices_005.png

3.1.2.4.3. 留一分组法(Leave One Group Out)#

LeaveOneGroupOut是一种交叉验证方案,其中每个拆分保留属于一个特定组的样本。组信息通过一个数组提供,该数组编码每个样本的组。

因此,每个训练集由除与特定组相关的样本之外的所有样本组成。这与LeavePGroupsOutn_groups=1相同,也与GroupKFoldn_splits等于传递给groups参数的唯一标签数量相同。

例如,在多个实验的情况下,LeaveOneGroupOut可用于创建基于不同实验的交叉验证:我们使用除一个实验之外的所有实验的样本来创建训练集

>>> from sklearn.model_selection import LeaveOneGroupOut

>>> X = [1, 5, 10, 50, 60, 70, 80]
>>> y = [0, 1, 1, 2, 2, 2, 2]
>>> groups = [1, 1, 2, 2, 3, 3, 3]
>>> logo = LeaveOneGroupOut()
>>> for train, test in logo.split(X, y, groups=groups):
...     print("%s %s" % (train, test))
[2 3 4 5 6] [0 1]
[0 1 4 5 6] [2 3]
[0 1 2 3] [4 5 6]

另一个常见的应用是使用时间信息:例如,组可以是样本收集的年份,从而允许针对基于时间的拆分进行交叉验证。

3.1.2.4.4. 留P分组法(Leave P Groups Out)#

LeavePGroupsOut类似于LeaveOneGroupOut,但为每个训练集/测试集移除与\(P\)个组相关的样本。所有可能的\(P\)个组组合都被移除,这意味着对于\(P>1\),测试集将重叠。

留2分组法(Leave-2-Group Out)示例

>>> from sklearn.model_selection import LeavePGroupsOut

>>> X = np.arange(6)
>>> y = [1, 1, 1, 2, 2, 2]
>>> groups = [1, 1, 2, 2, 3, 3]
>>> lpgo = LeavePGroupsOut(n_groups=2)
>>> for train, test in lpgo.split(X, y, groups=groups):
...     print("%s %s" % (train, test))
[4 5] [0 1 2 3]
[2 3] [0 1 4 5]
[0 1] [2 3 4 5]

3.1.2.4.5. 组洗牌拆分(Group Shuffle Split)#

GroupShuffleSplit迭代器的行为是ShuffleSplitLeavePGroupsOut的组合,并生成一个随机分区序列,其中每个拆分都保留了一部分组。每个训练/测试拆分都是独立执行的,这意味着连续测试集之间没有保证的关系。

这是一个使用示例

>>> from sklearn.model_selection import GroupShuffleSplit

>>> X = [0.1, 0.2, 2.2, 2.4, 2.3, 4.55, 5.8, 0.001]
>>> y = ["a", "b", "b", "b", "c", "c", "c", "a"]
>>> groups = [1, 1, 2, 2, 3, 3, 4, 4]
>>> gss = GroupShuffleSplit(n_splits=4, test_size=0.5, random_state=0)
>>> for train, test in gss.split(X, y, groups=groups):
...     print("%s %s" % (train, test))
...
[0 1 2 3] [4 5 6 7]
[2 3 6 7] [0 1 4 5]
[2 3 4 5] [0 1 6 7]
[4 5 6 7] [0 1 2 3]

这是交叉验证行为的可视化。

../_images/sphx_glr_plot_cv_indices_011.png

当需要LeavePGroupsOut的行为,但组的数量足够大,以至于生成所有可能的保留\(P\)个组的分区成本过高时,此类很有用。在这种情况下,GroupShuffleSplit提供了一个由LeavePGroupsOut生成的训练/测试拆分的随机样本(有替换)。

3.1.2.5. 使用交叉验证迭代器拆分训练集和测试集#

上述组交叉验证函数也可用于将数据集拆分为训练子集和测试子集。请注意,便利函数train_test_split是对ShuffleSplit的包装,因此只允许进行分层拆分(使用类别标签),而不能考虑组。

要执行训练集和测试集拆分,请使用交叉验证拆分器的split()方法输出的生成器所产生的训练子集和测试子集的索引。例如

>>> import numpy as np
>>> from sklearn.model_selection import GroupShuffleSplit

>>> X = np.array([0.1, 0.2, 2.2, 2.4, 2.3, 4.55, 5.8, 0.001])
>>> y = np.array(["a", "b", "b", "b", "c", "c", "c", "a"])
>>> groups = np.array([1, 1, 2, 2, 3, 3, 4, 4])
>>> train_indx, test_indx = next(
...     GroupShuffleSplit(random_state=7).split(X, y, groups)
... )
>>> X_train, X_test, y_train, y_test = \
...     X[train_indx], X[test_indx], y[train_indx], y[test_indx]
>>> X_train.shape, X_test.shape
((6,), (2,))
>>> np.unique(groups[train_indx]), np.unique(groups[test_indx])
(array([1, 2, 4]), array([3]))

3.1.2.6. 时间序列数据的交叉验证#

时间序列数据的特点是时间上相近的观测值之间存在相关性(自相关性)。然而,经典的交叉验证技术,如KFoldShuffleSplit,假设样本是独立同分布的,并且会在时间序列数据上导致训练实例和测试实例之间不合理的相关性(产生对泛化误差的较差估计)。因此,评估时间序列数据模型时,对“未来”观测值进行评估非常重要,这些观测值与用于训练模型的观测值最不相似。为了实现这一点,TimeSeriesSplit提供了一种解决方案。

3.1.2.6.1. 时间序列拆分(Time Series Split)#

TimeSeriesSplitk-fold的一种变体,它将前\(k\)个折叠作为训练集,将第\((k+1)\)个折叠作为测试集返回。请注意,与标准交叉验证方法不同,连续的训练集是它们之前训练集的超集。此外,它将所有多余的数据添加到第一个训练分区中,该分区始终用于训练模型。

此类可用于对按固定时间间隔观测的时间序列数据样本进行交叉验证。实际上,折叠必须代表相同的持续时间,以便跨折叠具有可比较的指标。

对包含6个样本的数据集进行3次拆分时间序列交叉验证的示例

>>> from sklearn.model_selection import TimeSeriesSplit

>>> X = np.array([[1, 2], [3, 4], [1, 2], [3, 4], [1, 2], [3, 4]])
>>> y = np.array([1, 2, 3, 4, 5, 6])
>>> tscv = TimeSeriesSplit(n_splits=3)
>>> print(tscv)
TimeSeriesSplit(gap=0, max_train_size=None, n_splits=3, test_size=None)
>>> for train, test in tscv.split(X):
...     print("%s %s" % (train, test))
[0 1 2] [3]
[0 1 2 3] [4]
[0 1 2 3 4] [5]

这是交叉验证行为的可视化。

../_images/sphx_glr_plot_cv_indices_013.png

3.1.3. 关于洗牌的说明#

如果数据顺序不是任意的(例如,具有相同类别标签的样本是连续的),则首先洗牌可能对于获得有意义的交叉验证结果至关重要。然而,如果样本不是独立同分布的,情况可能相反。例如,如果样本对应于新闻文章,并按出版时间排序,那么洗牌数据很可能会导致模型过拟合和验证分数膨胀:它将在与训练样本人为相似(时间上接近)的样本上进行测试。

一些交叉验证迭代器,例如KFold,具有内置选项,可以在拆分数据索引之前洗牌它们。请注意,

  • 这比直接洗牌数据消耗更少的内存。

  • 默认情况下不会发生洗牌,包括通过指定cv=some_integercross_val_score、网格搜索等执行的(分层)K折交叉验证。请记住,train_test_split仍然返回随机拆分。

  • random_state参数默认为None,这意味着每次迭代KFold(..., shuffle=True)时洗牌都会不同。然而,GridSearchCV将为通过单个调用其fit方法验证的每组参数使用相同的洗牌。

  • 要使每次拆分都获得相同的结果,请将random_state设置为整数。

有关如何控制cv拆分器随机性以及避免常见陷阱的更多详细信息,请参阅控制随机性

3.1.4. 交叉验证和模型选择#

交叉验证迭代器也可用于直接使用网格搜索(Grid Search)进行模型选择,以找到模型的最佳超参数。这是下一节的主题:调整估计器的超参数

3.1.5. 排列检验分数#

permutation_test_score提供了另一种评估预测器性能的方法。它提供了一个基于排列的p值,表示观察到的估计器性能偶然获得的概率。此检验中的零假设是估计器无法利用特征和目标之间的任何统计依赖关系对保留数据进行正确预测。permutation_test_score通过计算n_permutations次不同的数据排列来生成零分布。在每次排列中,目标值都会被随机洗牌,从而消除了特征和目标之间的任何依赖关系。输出的p值是交叉验证分数大于或等于未排列目标时的真实分数的分数。对于可靠的结果,n_permutations通常应大于100,cv应在3-10折之间。

低p值提供了证据,表明数据集包含特征和目标之间的某些真实依赖关系并且估计器能够利用这种依赖关系获得良好的结果。相反,高p值可能是由于以下任一原因造成的:

  • 特征和目标之间缺乏依赖关系(即,没有系统的关系,任何观察到的模式很可能是由于随机机会造成的)

  • 或者因为估计器无法使用数据中的依赖关系(例如,因为它欠拟合)。

在后一种情况下,使用更合适的、能够利用数据结构的估计器将导致较低的p值。

交叉验证通过估计估计器预期分数的范围来提供有关其泛化能力的信息。然而,在没有结构的高维数据集上训练的估计器仍然可能在交叉验证上表现得比预期要好,这仅仅是偶然的。这通常发生在样本量小于几百个的小数据集上。permutation_test_score提供了有关估计器是否找到了特征和目标之间真实依赖关系的信息,并且可以帮助评估估计器的性能。

值得注意的是,该检验已被证明即使数据中只有微弱结构,也会产生低p值,因为在相应的排列数据集中完全没有结构。因此,该检验只能显示模型是否可靠地优于随机猜测。

最后,permutation_test_score是使用暴力计算的,并在内部拟合(n_permutations + 1) * n_cv个模型。因此,它只适用于拟合单个模型非常快的小数据集。使用n_jobs参数可以并行化计算,从而加快速度。

示例

参考文献#