sklearn基础篇(二)-- 交叉验证评估模型性能

        训练机器学习模型的关键一步是要评估模型的泛化能力。如果我们训练好模型后,还是用训练集取评估模型的性能,这显然是不符合逻辑的。一个模型如果性能不好,要么是因为模型过于复杂导致过拟合(高方差),要么是模型过于简单导致导致欠拟合(高偏差)。可是用什么方法评价模型的性能呢?这就是这一节要解决的问题,你会学习到两种交叉验证计数,holdout交叉验证和k折交叉验证, 来评估模型的泛化能力。

1 留出法(holdout cross validation)

        评估模型泛化能力的典型方法是holdout交叉验证(holdout cross validation)。holdout方法很简单,我们只需要将原始数据集分割为训练集和测试集,前者用于训练模型,后者用于评估模型的性能。

        不过,在训练模型这一步,我们非常关心如何选择参数来提高模型的预测能力,而选择参数这一步被称为模型选择(model selection,注:不少资料将选择何种模型算法称为模型选择),参数选择是非常重要的,因为对于同一种机器学习算法,如果选择不同的参数(超参数),模型的性能会有很大差别。

         如果在模型选择的过程中,我们始终用测试集来评价模型性能,这实际上也将测试集变相地转为了训练集,这时候选择的最优模型很可能是过拟合的。

        更好的holdout方法是将原始训练集分为三部分:训练集、验证集和测试集。训练集用于训练不同的模型,验证集用于模型选择。而测试集由于在训练模型和模型选择这两步都没有用到,对于模型来说是未知数据,因此可以用于评估模型的泛化能力。下图展示了holdout方法的步骤:

        当然holdout方法也有明显的缺点,它对数据分割的方式很敏感,如果原始数据集分割不当,这包括训练集、验证集和测试集的样本数比例,以及分割后数据的分布情况是否和原始数据集分布情况相同等等。所以,不同的分割方式可能得到不同的最优模型参数。

优点:任务量小,缺点:太不精确,受数据随机性影响

        下一节,我们会学习到一种鲁棒性更好的模型评估方法,k折交叉沿则,即重复k次holdout方法提高鲁棒性。

代码实现:
        下面让我们载入 iris 数据集,并在此数据集上训练出线性支持向量机:

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

X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)    # 分割训练集和测试集

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

2 k折交叉验证(k-fold cross validation)

        k 折交叉验证通过对 k 个不同分组训练的结果进行平均来减少方差,因此模型的性能对数据的划分就不那么敏感。步骤如下:

  • 第一步,不重复抽样将原始数据随机分为 k 份;
  • 第二步,每一次挑选其中 1 份作为测试集,剩余 k-1 份作为训练集用于模型训练;
  • 第三步,重复第二步 k 次,这样每个子集都有一次机会作为测试集,其余机会作为训练集;
  • 第四步,在每个训练集上训练后得到一个模型,用这个模型在相应的测试集上测试,计算并保存模型的评估指标;
  • 第五步,计算 k 组测试结果的平均值作为模型精度的估计,并作为当前 k 折交叉验证下模型的性能指标。

        至于 k 折中的 k 到底设定为多少,这个又是一个调参的过程,当然了,这一步很少有人会调参,一般都是用5或10。但是如果你的数据集特别小,我们当然希望训练集大一点,这时候就要设定大一点的k值,因为 k 越大,训练集在整个原始训练集的占比就越多。但是呢,k 也不能太大,一则是导致训练模型个数过多,二则是k很大的情况下,各个训练集相差不多,导致高方差。要是你的数据集很大,那就把 k 设定的小一点咯,比如5。

        使用 k 折交叉验证来寻找最优参数要比holdout方法更稳定。一旦我们找到最优参数,要使用这组参数在原始数据集上训练模型作为最终的模型。k折交叉验证使用不重复采样,优点是每个样本只会在训练集或测试中出现一次,这样得到的模型评估结果有更低的方法。

        下图演示了10折交叉验证:

        其实呢,在将原始数据集划分为k部分的过程中,有很多不同的采样方法,比如针对非平衡数据的分层采样。分层采样就是在每一份子集中都保持原始数据集的类别比例。比如原始数据集正类:负类=3:1,这个比例也要保持在各个子集中才行。sklearn中实现了分层 k 折交叉验证。

这个方法充分利用了所有样本。但计算比较繁琐,需要训练 k 次,测试 k 次。

        下面的示例展示了如何通过分割数据,拟合模型和计算连续 5 次的分数(每次不同分割)来估计 linear kernel 支持向量机在 iris 数据集上的精度:

from sklearn.model_selection import cross_val_score
clf = svm.SVC(kernel='linear', C=1)
scores = cross_val_score(clf, X, y, cv=5)
print(scores)   # [0.96666667 1.         0.96666667 0.96666667 1.]     

        评分估计的平均得分和 95% 置信区间由此给出:

print("Accuracy: %0.2f (+/- %0.2f)" % (scores.mean(), scores.std()))        # Accuracy: 0.98 (+/- 0.02)

        下面看一下 K-fold 是怎样划分数据的:X 有四个数据,把它分成 2 折,结果中最后一个集合是测试集,前面的是训练集,每一行为 1 折:

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))
    print(X[train], X[test])
    # [2 3]    [0 1]
    # [0 1]    [2 3]

        每一折由两个 arrays 组成,第一个作为 training set ,另一个作为 test set 。 由此,可以通过使用 numpy 的索引创建训练/测试集合。


3 留一法(LOOCV, Leave one out cross validation)

        当 k= n,即样本总数时,就叫做留一法,换言之每次只留下一个样本做测试集,其它样本做训练集,如果有 n 个样本,则需要训练 n 次,测试 n 次。这个方法用于训练的数据只比整体数据集少了一个样本,因此最接近原始样本的分布。但是训练复杂度增加了,因为模型的数量与原始数据样本数量相同。一般在数据缺乏时使用。例如下图,一共10个数据,就交叉验证十次

优点:

  • 适合小样本数据集
  • 利用所有的数据点,因此偏差将很低

缺点:

  • 重复交叉验证过程n次导致更高的执行时间
  • 测试模型有效性的变化大。因为针对一个数据点进行测试,模型的估计值受到数据点的很大影响。如果数据点被证明是一个离群值,它可能导致更大的变化

        LOOCC是保留一个数据点,同样的你也可以保留P个数据点作为验证集,这种方法叫LPOCV(Leave P Out Cross Validation)

同样的数据 X,我们看 LeaveOneOut 后是什么样子,那就是把它分成 4 折,结果中最后一个集合是测试集,只有一个元素,前面的是训练集,每一行为 1 折:

>>> 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]


4 自助采样法(Bootstrapping)

        通过自助采样法,即在含有 n 个样本的数据集中,每次随机挑选一个样本,再放回到数据集中,再随机挑选一个样本,这样有放回地进行抽样 n 次,组成了新的数据集作为训练集。

        这里会有重复多次的样本,也会有一次都没有出现的样本,原数据集中大概有 36.8% 的样本不会出现在新组数据集中。

优点是训练集的样本总数和原数据集一样都是 n,并且仍有约 1/3 的数据不被训练而可以作为测试集。缺点是这样产生的训练集的数据分布和原数据集的不一样了,会引入估计偏差。此种方法不是很常用,除非数据量真的很少。


5 随机二次抽样(Random Subsampling)

        相当于k次Holdout取平均:每次取不一样的验证集(剩下的作为训练集),执行一遍获得指标,最后k次取平均。

参数:训练集、验证集的数据条目数,次数k

应用:在模型之间差异明显,或其他对于结果要求不高的领域,此法比较便捷。

优点:任务量小,缺点:太不精确


6 scikit-learn中的交叉验证

6.1 标准k折交叉验证

        scikit-learn 是利用model_selection模块中的cross_val_score函数来实现交叉验证的。cross_val_score函数的参数是我们想要评估的模型、训练数据与真实标签。我们在iris数据集上对LogisticRegression进行评估:

from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression

iris = load_iris()
logreg = LogisticRegression()
scores = cross_val_score(logreg, iris.data, iris.target)
print("Cross-validation scores: {}".format(scores))
# Cross-validation scores: [0.96666667 1.         0.93333333 0.96666667 1.        ]

        默认情况下,cross_val_score执行5折交叉验证,返回5个精度值。可以通过修改cv参数来改变折数。

        总结交叉验证精度的一种常用方法是计算平均值:

print("Average cross-validaton score: {:.2f}".format(scores.mean()))
# Average cross-validaton score: 0.97

        可以从交叉验证平均值中得出结论,我们预计模型的平均精度约为96%。观察5折交叉验证得到的所有5个精度值,我们还可以发现,折与折之间的精度有较大的变化,范围为从100%精度到90%精度。这可能意味着模型强烈依赖于将某个折用于训练,但也可能只是因为数据集的数据量太小。

6.2 分层k折交叉验证

        将数据集划分为k折时,从数据的前k分之一开始划分,这可能并不总是一个好主意。例如,我们来看一下iris数据集:

print("Iris labels:\n{}".format(iris.target))

        数据的前三分之一是类别0,中间三分之一是类别1,最后三分之一是类别2。想象一下在这个数据集上进行3 折交叉验证。第1折将只包含类别0,所以在数据的第一次划分中,测试集将只包含类别0,而训练集只包含类别1和2。由于在3次划分中训练集和测试集中的类别都不相同,因此这个数据集上的3 折交叉验证精度为0。这没什么帮助,因为我们在iris上可以得到比0%好得多的精度。

        由于简单的k折策略在这里失效了,所以scikit-learn 在分类问题中不使用这种策略,而是使用分层k折交叉验证(stratified k-fold cross-validation)。在分层交叉验证中,我们划分数据,使每个折中类别之间的比例与整个数据集中的比例相同,如下图所示。

mglearn.plots.plot_stratified_cross_validation()
当数据按类别标签排序时,标准交叉验证与分层交叉验证的对比

        举个例子,如果90% 的样本属于类别A而10%的样本属于类别B,那么分层交叉验证可以确保,在每个折中90%的样本属于类别A而10% 的样本属于类别B。

sklearn.model_selection.StratifiedKFold()
- n_split折叠数量,默认为3,至少为2.;
- shuffle是否在分割成批次之前打乱每一类的数据集,默认不打乱;
- random_state随机数种子,在进行打乱数据操作时使用.

        使用分层k折交叉验证而不是k 折交叉验证来评估一个分类器,这通常是一个好主意,因为它可以对泛化性能做出更可靠的估计。在只有10% 的样本属于类别B 的情况下,如果使用标准k折交叉验证,很可能某个折中只包含类别A 的样本。利用这个折作为测试集的话,无法给出分类器整体性能的信息。对于回归问题,scikit-learn默认使用标准k折交叉验证。分层法能使验证集更加稳定并且对于小而不平衡的数据集是有奇效的。

1. 对交叉验证的更多控制

        之前看到,可以利用cv 参数来调节cross_val_score 所使用的折数。但scikit-learn允许提供一个交叉验证分离器(cross-validation splitter)作为cv参数,来对数据划分过程进行更精细的控制。对于大多数使用场景而言,回归问题默认的k折交叉验证与分类问题的分层k折交叉验证的表现都很好,但有些情况下你可能希望使用不同的策略。比如说,我们想要在一个分类数据集上使用标准k 折交叉验证来重现别人的结果。为了实现这一点,我们首先必须从model_selection 模块中导入KFold分离器类,并用我们想要使用的折数来将其实例化:

from sklearn.model_selection import KFold
kfold = KFold(n_splits=5)

        然后可以将kfold分离器对象作为cv参数传入cross_val_score:

print("Cross-validation scores:\n{}".format(cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
# Cross-validation scores: [1.         1.         0.86666667 0.93333333 0.83333333]

        通过这种方法,我们可以验证,在iris数据集上使用3折交叉验证(不分层)确实是一个非常糟糕的主意:

kfold = KFold(n_splits=3)
print("Cross-validation scores:\n{}".format(cross_val_score(logreg, iris.data, iris.target, cv=kfold)))   # Cross-validation scores: [0. 0. 0.]

        在iris数据集中每个折对应一个类别,因此学不到任何内容。解决这个问题的另一种方法是将数据打乱来代替分层,以打乱样本按标签的排序。可以通过将KFold的shuffle参数设为True 来实现这一点。如果我们将数据打乱,那么还需要固定random_state以获得可重复的打乱结果。否则,每次运行cross_val_score 将会得到不同的结果,因为每次使用的是不同的划分。在划分数据之前将其打乱可以得到更好的结果:

kfold = KFold(n_splits=3, shuffle=True, random_state=0)
print("Cross-validation scores:\n{}".format(cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
# Cross-validation scores: [0.98 0.96 0.96]

2. 留一法交叉验证
        另一种常用的交叉验证方法是留一法(leave-one-out)。你可以将留一法交叉验证看作是每折只包含单个样本的k 折交叉验证。对于每次划分,你选择单个数据点作为测试集。这种方法可能非常耗时,特别是对于大型数据集来说,但在小型数据集上有时可以给出更好的估计结果:

from sklearn.model_selection import LeaveOneOut
loo = LeaveOneOut()
scores = cross_val_score(logreg, iris.data, iris.target, cv=loo)
print("Number of cv iterations:", len(scores))
print("Mean accuracy: {:.2f}".format(scores.mean()))
# Number of cv iterations: 150
# Mean accuracy: 0.97

3. 打乱划分交叉验证

        另一种非常灵活的交叉验证策略是打乱划分交叉验证(shuffle-split cross-validation)。在打乱划分交叉验证中,每次划分为训练集取样train_size个点,为测试集取样test_size个(不相交的)点。将这一划分方法重复n_iter 次。下图显示的是对包含10个点的数据集运行4 次迭代划分,每次的训练集包含5 个点,测试集包含2 个点(你可以将train_size和test_size 设为整数来表示这两个集合的绝对大小,也可以设为浮点数来表示占整个数据集的比例):

mglearn.plots.plot_shuffle_split()

        下面的代码将数据集划分为50%的训练集和50%的测试集,共运行10次迭代:

from sklearn.model_selection import ShuffleSplit
shuffle_split = ShuffleSplit(test_size=0.5, train_size=0.5, n_splits=10)
scores = cross_val_score(logreg, iris.data, iris.target, cv=shuffle_split)
print("Cross-validation scores:\n{}".format(scores))

Cross-validation scores:
[0.98666667 0.96 0.97333333 0.97333333 0.96 0.97333333
0.96 0.96 0.93333333 0.94666667]

        打乱划分交叉验证可以在训练集和测试集大小之外独立控制迭代次数,这有时是很有帮助的。它还允许在每次迭代中仅使用部分数据,这可以通过设置train_size与test_size之和不等于1 来实现。用这种方法对数据进行二次采样可能对大型数据上的试验很有用。

        ShuffleSplit还有一种分层的形式,其名称为StratifiedShuffleSplit,它可以为分类任务提供更可靠的结果。

4. 分组交叉验证

        另一种非常常见的交叉验证适用于数据中的分组高度相关时。比如你想构建一个从人脸图片中识别情感的系统,并且收集了100 个人的照片的数据集,其中每个人都进行了多次拍摄,分别展示了不同的情感。我们的目标是构建一个分类器,能够正确识别未包含在数据集中的人的情感。你可以使用默认的分层交叉验证来度量分类器的性能。但是这样的话,同一个人的照片可能会同时出现在训练集和测试集中。对于分类器而言,检测训练集中出现过的人脸情感比全新的人脸要容易得多。因此,为了准确评估模型对新的人脸的泛化能力,我们必须确保训练集和测试集中包含不同人的图像。

        为了实现这一点,我们可以使用GroupKFold,它以groups 数组作为参数,可以用来说明照片中对应的是哪个人。这里的groups数组表示数据中的分组,在创建训练集和测试集的时候不应该将其分开,也不应该与类别标签弄混。

        下面这个示例用到了一个由groups数组指定分组的模拟数据集。这个数据集包含12 个数据点,且对于每个数据点,groups 指定了该点所属的分组。一共分成4个组,前3个样本属于第一组,接下来的4 个样本属于第二组,以此类推:

from sklearn.model_selection import GroupKFold
# 创建模拟数据集
X, y = make_blobs(n_samples=12, random_state=0)
# 假设前3个样本属于同一组,接下来的4个属于同一组,以此类推
groups = [0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 3]
scores = cross_val_score(logreg, X, y, groups=groups, cv=GroupKFold(n_splits=3))
print("Cross-validation scores:\n{}".format(scores))
# Cross-validation scores: [0.75       0.6        0.66666667]

        基于这些标签计算得到的划分如下图所示。对于每次划分,每个分组都是整体出现在训练集或测试集中:

mglearn.plots.plot_group_kfold()

        scikit-learn 中还有很多交叉验证的划分策略,适用于更多的使用场景,了解更多请阅读:Cross-validation: evaluating estimator performance——https://scikit-learn.org/stable/modules/cross_validation.html


7 小结

        疑问:对于交叉验证法,如5折交叉验证,那么将会生成5个模型,到底应该选择哪个模型作为最终的模型呢?

        解答:通过阅读博文,发现交叉验证法是用来评估模型性能的,并不参与最后预测模型的生成。

        一、交叉验证法是用于小数据集的。大数据集可直接使用train-validation-test。在研究对比不同算法的泛化性能时,使用测试集来评估,而把训练数据划分为训练集和验证集,基于验证集上的性能来进行模型选择和调参。

        使用train-validation-test的过程:

  • 将数据集划分为training set,validation set,testing set;
  • 对不同的3个算法svm,random forest,logistic regression,分别执行以下三步,得到其最优模型。
    • 对于一个算法,如svm,针对不同的核参数,在training set上训练模型;
    • 在validation set上评估不同核参数对应模型的结果,选择出最优的核参数;
    • 对该最优核参数,重新使用training set+validation set进行训练,得到最终的模型;
  • 在testing set上对这三个不同算法的性能进行评估,用来估计模型在实际使用时的泛化能力。

        二、交叉验证法是用于选择超参数的。如svm中核函数的参数选择,tuning parameter时,使用交叉验证法的平均评估结果来验证哪个参数最好。

        机器学习训练时主要需要学两样东西。

        一样是模型weights,比如coefficients and biases.

        另外一样是模型的超参数,比如学习率,regularisation strength什么的。

        cross validation 主要是为了找到最好的超参数。然后用最好的超参数训练模型,得到最终的模型weights。

        使用交叉验证进行tuning parameter的过程总结:

  1. 将全部数据集划分为training set和 testing set;
  2. 对不同算法,进行tuning parameter:
    • 对不同超参数,使用training set,进行5折交叉验证,得到评估结果;
    • 选择评估结果最好的那个超参数作为最优超参数;
    • 对最优超参数,重新使用training set训练模型;
  3. 使用testing set评估不同算法的泛化能力。

        补充,对于分层验证、重复交叉验证( k-fold cross validation with repetition)、对抗验证(Adversarial Validation)、时间序列的交叉验证(Cross Validation for time series)的介绍,请阅读:https://blog.csdn.net/WHYbeHERE/article/details/108192957


参考

  • 3
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

长路漫漫2021

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值