3.1. 交叉验证:评估估计器性能#
学习预测函数的参数并在相同数据上测试它是一个方法上的错误:一个仅仅重复它刚刚看到的样本标签的模型将具有完美的得分,但它将无法对尚未见过的样本进行任何有用的预测。这种情况被称为 **过拟合**。为了避免这种情况,在进行(监督)机器学习实验时,通常的做法是将部分可用数据作为 **测试集** X_test, y_test
保留。请注意,“实验”一词并非仅指学术用途,因为即使在商业环境中,机器学习通常也是从实验开始的。以下是模型训练中典型交叉验证工作流程的流程图。最佳参数可以通过 网格搜索 技术确定。
在 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
设置,仍然存在对 *测试集* 过拟合的风险,因为可以调整参数,直到估计器达到最佳性能。这样,关于测试集的知识就会“泄露”到模型中,评估指标不再反映泛化性能。为了解决这个问题,可以将数据集的另一部分作为所谓的“验证集”保留:在训练集上进行训练,然后在验证集上进行评估,当实验似乎成功时,可以在测试集上进行最终评估。
然而,通过将可用数据划分为三个集合,我们大幅减少了可用于学习模型的样本数量,结果可能取决于 (训练,验证) 集合对的特定随机选择。
解决这个问题的方法是称为 交叉验证(简称 CV)的过程。仍然应该保留一个测试集用于最终评估,但在进行 CV 时不再需要验证集。在基本方法中,称为 *k* 折 CV,训练集被分成 *k* 个更小的集合(下面描述了其他方法,但通常遵循相同的原则)。以下过程针对每个 *k* 个“折”进行
使用 \(k-1\) 个折作为训练数据训练一个模型;
将得到的模型在数据的剩余部分上进行验证(即,它被用作测试集来计算性能指标,例如准确率)。
*k* 折交叉验证报告的性能指标是循环中计算的值的平均值。这种方法在计算上可能很昂贵,但不会浪费太多数据(就像固定任意验证集那样),这在样本数量非常少的逆推断问题中是一个主要优势。
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. ])
有关详细信息,请参阅 评分参数:定义模型评估规则。在鸢尾花数据集的情况下,样本在目标类别之间是平衡的,因此准确率和 F1 分数几乎相等。
当 cv
参数为整数时,cross_val_score
默认情况下使用 KFold
或 StratifiedKFold
策略,如果估计器来自 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. ])
另一个选择是使用一个可迭代对象,它生成 (train, test) 拆分作为索引数组,例如
>>> 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. 数据的交叉验证迭代器#
假设某些数据是独立同分布 (i.i.d.),即假设所有样本都来自相同的生成过程,并且假设生成过程没有记忆过去生成的样本。
以下交叉验证器可用于此类情况。
注意
虽然 i.i.d. 数据是机器学习理论中的一个常见假设,但在实践中很少成立。如果知道样本是使用时间相关的过程生成的,则使用 时间序列感知交叉验证方案 更安全。类似地,如果我们知道生成过程具有组结构(从不同主体、实验、测量设备收集的样本),则使用 组交叉验证 更安全。
3.1.2.1.1. K 折叠#
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
不受类别或组的影响。
每个折叠由两个数组构成:第一个数组与训练集相关,第二个数组与测试集相关。因此,可以使用 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 折叠#
RepeatedKFold
重复 K 折交叉验证 n 次。当需要运行 KFold
n 次,并在每次重复中生成不同的拆分时,可以使用它。
2 折 K 折交叉验证重复 2 次的示例
>>> 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]
类似地,RepeatedStratifiedKFold
重复分层 K 折交叉验证 n 次,每次重复使用不同的随机化。
3.1.2.1.3. 留一法 (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\) 折交叉验证相比,从 \(n\) 个样本中构建 \(n\) 个模型,而不是 \(k\) 个模型,其中 \(n > k\)。此外,每个模型都训练了 \(n - 1\) 个样本,而不是 \((k-1) n / k\)。在这两种情况下,假设 \(k\) 不太大并且 \(k < n\),LOO 比 \(k\) 折交叉验证计算量更大。
在准确性方面,LOO 作为测试误差的估计器,通常会导致高方差。直观地说,由于 \(n - 1\) 个样本中的 \(n\) 个样本用于构建每个模型,因此从折中构建的模型彼此之间以及与从整个训练集中构建的模型几乎相同。
但是,如果学习曲线对于所讨论的训练大小很陡峭,那么 5 折或 10 折交叉验证可能会高估泛化误差。
作为一般规则,大多数作者和经验证据表明,5 折或 10 折交叉验证应该优先于 LOO。
参考文献#
http://www.faqs.org/faqs/ai-faq/neural-nets/part3/section-12.html;
T. Hastie, R. Tibshirani, J. Friedman, 统计学习要素, Springer 2009
L. Breiman, P. Spector 回归中的子模型选择和评估:X 随机情况, 国际统计评论 1992;
R. Kohavi, 交叉验证和自举法在准确性估计和模型选择中的研究, 国际人工智能联合会议
R. Bharat Rao, G. Fung, R. Rosales, 关于交叉验证的危险。实验评估, SIAM 2008;
G. James, D. Witten, T. Hastie, R Tibshirani, 统计学习导论, Springer 2013.
3.1.2.1.4. 留 P 法 (LPO)#
LeavePOut
与 LeaveOneOut
非常相似,因为它通过从完整集中删除 \(p\) 个样本创建所有可能的训练/测试集。对于 \(n\) 个样本,这将产生 \({n \choose p}\) 个训练-测试对。与 LeaveOneOut
和 KFold
不同,当 \(p > 1\) 时,测试集将重叠。
在具有 4 个样本的数据集上进行留 2 法的示例
>>> 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. 随机排列交叉验证,也称为洗牌和拆分#
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
不受类或组的影响。
ShuffleSplit
因此是 KFold
交叉验证的一个很好的替代方案,它允许对迭代次数和训练/测试拆分两侧样本比例进行更精细的控制。
3.1.2.2. 基于类标签的分层交叉验证迭代器#
一些分类问题可能在目标类的分布中表现出很大的不平衡:例如,负样本可能比正样本多几倍。在这种情况下,建议使用分层抽样,如 StratifiedKFold
和 StratifiedShuffleSplit
中实现的那样,以确保每个训练和验证折中的相对类频率近似保留。
3.1.2.2.1. 分层 k 折#
StratifiedKFold
是 k 折 的一种变体,它返回 分层 折:每个集合包含与完整集合中每个目标类样本的百分比大致相同的样本百分比。
这是一个在具有来自两个不平衡类的 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)。
这是交叉验证行为的可视化。
RepeatedStratifiedKFold
可用于重复分层 K 折交叉验证 n 次,每次重复使用不同的随机化。
3.1.2.2.2. 分层洗牌拆分#
StratifiedShuffleSplit
是ShuffleSplit的变体,它返回分层拆分,即通过保留与完整集中每个目标类相同的百分比来创建拆分。
这是交叉验证行为的可视化。
3.1.2.3. 预定义折叠拆分/验证集#
对于某些数据集,数据已预先定义为训练和验证折叠,或已定义为多个交叉验证折叠。使用PredefinedSplit
,例如在搜索超参数时,可以使用这些折叠。
例如,当使用验证集时,将所有属于验证集的样本的test_fold
设置为0,将所有其他样本的test_fold
设置为-1。
3.1.2.4. 用于分组数据的交叉验证迭代器#
如果基础生成过程产生依赖样本组,则 i.i.d. 假设将被打破。
这种数据分组是特定于领域的。例如,当从多个患者那里收集医疗数据时,从每个患者那里采集多个样本。并且这种数据很可能依赖于单个组。在我们的示例中,每个样本的患者 ID 将是其组标识符。
在这种情况下,我们想知道在特定组集上训练的模型是否能很好地推广到未见过的组。为了衡量这一点,我们需要确保验证折叠中的所有样本都来自在配对训练折叠中完全没有表示的组。
以下交叉验证拆分器可用于执行此操作。样本的分组标识符通过groups
参数指定。
3.1.2.4.1. 分组 k 折叠#
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
是更好的选择。
这是交叉验证行为的可视化。
与KFold
类似,来自GroupKFold
的测试集将构成所有数据的完整分区。与KFold
不同,GroupKFold
根本没有随机化,而KFold
在shuffle=True
时是随机化的。
3.1.2.4.2. StratifiedGroupKFold#
StratifiedGroupKFold
是一种交叉验证方案,它结合了StratifiedKFold
和GroupKFold
。其想法是尝试在保持每个组在一个拆分内的同时,保留每个拆分中类的分布。当你的数据集不平衡时,这可能很有用,因此仅使用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 时,会发生以下情况
所有组都被洗牌。
使用稳定排序按类的标准偏差对组进行排序。
对排序后的组进行迭代并将其分配到折叠中。
这意味着只有具有相同类分布标准偏差的组才会被洗牌,这在每个组只有一个类时可能很有用。
该算法贪婪地将每个组分配到 n_splits 测试集中,选择使测试集之间类分布方差最小化的测试集。组分配从类频率方差最高到最低的组进行,即在少数几个类上达到峰值的较大组首先被分配。
这种拆分在某种意义上是次优的,因为它即使可能进行完美的分层,也可能会产生不平衡的拆分。如果你在每个组中都有相对接近的类分布,使用
GroupKFold
更好。
以下是针对不均匀组的交叉验证行为的可视化
3.1.2.4.3. 留一组出#
LeaveOneGroupOut
是一种交叉验证方案,其中每个拆分都保留属于一个特定组的样本。组信息通过一个数组提供,该数组对每个样本的组进行编码。
因此,每个训练集都由除与特定组相关的样本之外的所有样本组成。这与n_groups=1
的LeavePGroupsOut
相同,也与n_splits
等于传递给groups
参数的唯一标签数量的GroupKFold
相同。
例如,在多个实验的情况下,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 组出#
LeavePGroupsOut
与 LeaveOneGroupOut
类似,但它会从每个训练/测试集中移除与 \(P\) 个组相关的样本。所有可能的 \(P\) 个组组合都会被移除,这意味着当 \(P>1\) 时,测试集会重叠。
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. 分组随机拆分#
GroupShuffleSplit
迭代器类似于 ShuffleSplit
和 LeavePGroupsOut
的组合,它会生成一系列随机分区,其中每个拆分都会保留一组组。每个训练/测试拆分都是独立进行的,这意味着连续的测试集之间没有保证的关系。
这是一个使用示例
>>> 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]
这是交叉验证行为的可视化。
当需要 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. 时间序列数据的交叉验证#
时间序列数据以时间相近的观测值之间的相关性(自相关)为特征。然而,经典的交叉验证技术,如 KFold
和 ShuffleSplit
假设样本是独立同分布的,这会导致时间序列数据上训练和测试实例之间出现不合理的相关性(导致泛化误差估计不佳)。因此,在时间序列数据上评估模型时,使用与用于训练模型的样本最不像的“未来”观测值来评估模型非常重要。为了实现这一点,TimeSeriesSplit
提供了一种解决方案。
3.1.2.6.1. 时间序列拆分#
TimeSeriesSplit
是k 折的一种变体,它将前 \(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]
这是交叉验证行为的可视化。
3.1.3. 关于洗牌的说明#
如果数据排序不是任意的(例如,具有相同类标签的样本是连续的),则首先对其进行洗牌对于获得有意义的交叉验证结果可能是必不可少的。但是,如果样本不是独立同分布的,则情况可能相反。例如,如果样本对应于新闻文章,并且按其发布时间排序,则洗牌数据可能会导致过度拟合的模型和膨胀的验证分数:它将在与训练样本人工相似的样本(时间接近)上进行测试。
一些交叉验证迭代器,如 KFold
,具有内置选项,可以在拆分数据索引之前对其进行洗牌。请注意
这比直接洗牌数据消耗更少的内存。
默认情况下,不会进行洗牌,包括由
cv=some_integer
指定给cross_val_score
、网格搜索等的(分层)K 折交叉验证。请记住,train_test_split
仍然返回随机拆分。random_state
参数默认为None
,这意味着每次迭代KFold(..., shuffle=True)
时,洗牌都会不同。但是,GridSearchCV
将对每次调用其fit
方法验证的一组参数使用相同的洗牌。要获得每个拆分的相同结果,请将
random_state
设置为整数。
有关如何控制 cv 拆分器的随机性和避免常见陷阱的更多详细信息,请参阅 控制随机性。
3.1.4. 交叉验证和模型选择#
交叉验证迭代器也可用于使用网格搜索直接执行模型选择,以找到模型的最佳超参数。这是下一节的主题:调整估计器的超参数。
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
个模型。因此,它仅适用于小型数据集,对于这些数据集,拟合单个模型非常快。
示例
参考文献#
Ojala 和 Garriga. Permutation Tests for Studying Classifier Performance. J. Mach. Learn. Res. 2010.