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
设置,仍然存在在测试集上过拟合的风险,因为可以调整参数直到估计器达到最佳性能。通过这种方式,关于测试集的知识可以“泄漏”到模型中,并且评估指标不再报告泛化性能。为了解决这个问题,可以保留数据集的另一部分作为所谓的“验证集”:在训练集上进行训练,然后在验证集上进行评估,当实验似乎成功时,可以在测试集上进行最终评估。
然而,通过将可用数据划分为三个数据集,我们大大减少了可用于学习模型的样本数量,并且结果可能取决于 (train, validation) 数据集对的特定随机选择。
这个问题的解决方案是一个称为交叉验证(简称 CV)的过程。仍然应该保留一个测试集用于最终评估,但在进行 CV 时不再需要验证集。在基本方法(称为 *k* 折 CV)中,训练集被分成 *k* 个较小的数据集(下面描述了其他方法,但通常遵循相同的原则)。对于每个 *k* 个“fold”,执行以下过程:
使用 \(k-1\) 个 folds 作为训练数据训练模型;
在数据的剩余部分(即用作测试集以计算性能度量,如准确率)上验证生成的模型。
*k* 折交叉验证报告的性能度量是循环中计算的值的平均值。这种方法在计算上可能很昂贵,但不会浪费太多数据(例如,当固定任意验证集时),这在样本数量非常少的逆推断等问题中是一个主要优势。
3.1.1. 计算交叉验证指标#
使用交叉验证最简单的方法是在估计器和数据集上调用 cross_val_score
辅助函数。
以下示例演示如何通过拆分数据、拟合模型和连续计算 5 次得分(每次使用不同的拆分)来估计线性核支持向量机在 iris 数据集上的准确性。
>>> 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
默认情况下,每次交叉验证迭代计算的分数是估计器的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
默认使用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
来保留用于将数据集分割成训练集和测试集的训练和测试索引。
可以将多个指标指定为预定义评分器名称的列表、元组或集合。
>>> 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 次的 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]
类似地,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. 随机排列交叉验证,又名 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
不受类别或分组的影响。
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,对于所有其他样本,设置为 -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
的测试集将构成所有数据的完整分区。
当 shuffle=False
时,GroupKFold
尝试将相同数量的样本放在每个折叠中;当 shuffle=True
时,它尝试将相同数量的不同组放在每个折叠中(但不考虑组的大小)。
3.1.2.4.2. 分层组 k 折#
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\),测试集将重叠。
留 2 组交叉验证示例
>>> 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
设置为整数。
有关如何控制交叉验证拆分器的随机性和避免常见陷阱的更多详细信息,请参阅 控制随机性。
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. 用于研究分类器性能的置换检验. J. Mach. Learn. Res. 2010.