4.2.3 交叉验证
下面分三部分介绍交叉验证。
1.什么是交叉验证
首先看如下的一个例子:
from sklearn import datasets from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression # 加载数据 bc = datasets.load_breast_cancer() X = bc.data y = bc.target # 构建逻辑回归模型 clf = LogisticRegression() # 划分数据:训练样本量占70%,测试样本量占30% X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=None) # 模型训练 clf.fit(X_train, y_train) # 模型预测得分 clf.score(X_test, y_test)
上述例子是机器学习中常见的数据集划分和训练方式,但有两个主要问题:
1)每次运行上述的代码得到的结果可能都不一样,到底哪次结果才更“靠谱”呢?选最好的一次?这样会有作弊的嫌疑。
2)该划分方式总有一定比例的测试数据无法参与到训练中(此处例子是30%),如果样本量本就不足,那么将损失宝贵的30%的数据,造成浪费。
为了控制随机性,可人为加入随机种子,使每次分隔保持一致(便于复现),例如可在上例train_test_split中设置参数random_state=42。42是一个很有意思的数字,感兴趣的读者可参考Wiki中的解释。但这样做实际加入了人为的偏差,不利于模型性能的公正评估。
为了解决上述两个问题,我们可以很自然地想到,可以通过多次更细粒度的样本划分,多次训练和评估,得到更准确的无偏估计以减少随机性。很多时候我们可以认为,交叉验证就是平均过程,包含了公平评估的思想。它除了可应用在模型的训练和调参中,也能应用在不同算法的评估选择中,这正是交叉验证的评估本质,即评估分类器性能的一种统计分析方法。
初学者常听到一句话,通过交叉验证调整模型参数。这种说法容易让人误以为交叉验证是调参的方法,而实际上交叉验证只是一种评估方法,可用于非调参的场合。
交叉验证可分为如下两个部分。
1)划分数据集:要求保证每次训练的数据集足够多,即一般情况下大于等于50%,即组数大于等于2;尽量保证划分后的数据集独立同分布(分层方法)。
2)训练和评估:与具体的学习算法和评估指标有关。
一般的交叉验证流程描述如下所示。
1)随机混洗、重排数据集。 2)将数据集拆分为K个组(Fold), 即K折交叉验证(K-Fold cross-validation)。 3)对于每个独特的组: a)将该组作为保留或测试数据集; b)将剩余的组作为训练数据集; c)在训练集上拟合模型并在测试集上评估; d)保留评估分数(丢弃模型)。
上述的K折交叉验证有如下两种常见的变体。
1)K×N fold cross-validation,即在K折的基础上再嵌套一层循环,最终取得平均估计,常见的有5×2交叉验证和10×10交叉验证。
2)Least-One-Out cross-validation(LOOCV),留一法,即每次只留一个样例做测试,其余数据作为训练。当数据量极小时,可考虑使用该方法,比如只有100个样本,那么按照LOOCV的方式,将训练100个模型,得到100个评价指标。每个模型使用99个样本训练,使用1个样本测试。
2.交叉验证的实现
上文提到的交叉验证流程方式很简单,完全可以自己实现,当然,这些交叉验证的方式在sklearn中都有接口且十分丰富。
·统一的实现:cross_val_score。
·学习器内自带的CV功能,例如逻辑回归中的LogisticRegressionCV。
·调参网格搜索时也有CV的方法,比如GridSearchCV。
最后,可参考以下示例以更好地理解交叉验证的本质。
1)随机划分数据示例:
from sklearn.model_selection import KFold # 数据示例 X = np.array([1, 2, 3, 4, 5, 6]) # 数据划分准备: n_splits划分组数K,shuffle是否重排打乱数据,随机种子random_state kfold = KFold(n_splits=3, shuffle = True, random_state= 42) # 划分 for train, test in kfold.split(X): print('train: {}, test: {}'.format(X[train], X[test]))
输出3组数据集如下:
train: [3 4 5 6], test: [1 2] train: [1 2 4 5], test: [3 6] train: [1 2 3 6], test: [4 5]
2)使用分层划分,保持y同分布:
from sklearn.model_selection import StratifiedKFold y = np.array([0, 0, 0, 1, 1, 1]) skf = StratifiedKFold(n_splits=3,shuffle = True, random_state= 42) for train_index, test_index in skf.split(X, y): print('train: {}, test: {}'.format(X[train_index], X[test_index]))
输出3组数据集如下:
train: [2 3 5 6], test: [1 4] train: [1 3 4 6], test: [2 5] train: [1 2 4 5], test: [3 6]
前文提到的train_test_split同样实现了数据划分功能。
3)使用cross_val_score做交叉验证:
# 使用上述breast_cancer的数据 from sklearn.model_selection import cross_val_score cross_val_score(clf, X, y, cv=3, scoring='accuracy')
输出3组模型评估如下:
array([0.93684211, 0.96842105, 0.94179894])
3.应用场景示例
下面简述了交叉验证两种常见的应用场景:交叉验证作为调参和模型选择的评估方法。
1)使用交叉验证选择参数。
# 使用上述的breast_cancer 数据 X = bc.data y = bc.target from sklearn.linear_model import LogisticRegressionCV # 此处直接使用了学习器自带的CV # 当然也可以使用统一的接口:cross_val_score lr = LogisticRegressionCV( Cs=[1, 10, 100],cv=5,scoring='accuracy') # 拟合 lr.fit(X, y) # 输出将是 5 x 3 的二维矩阵 lr.scores_[1] # 查看5组平均效果 lr.scores_[1].mean(axis=0) # 输出3个参数5轮平均的accuracy,正则参数C=10的效果最好,所以选择它 # array([0.94559446, 0.95261254, 0.94913428])
2)使用交叉验证选择模型。
相比上述调过参数的逻辑回归来说,KNN算法表现差一些,所以选择逻辑回归。
# 接上,使用同样的数据 from sklearn.neighbors import KNeighborsClassifier # 使用KNN默认参数 knn = KNeighborsClassifier() # 5-fold scores = cross_val_score(knn, X, y, cv=5, scoring='accuracy') # 得分:array([0.88695652, 0.93913043, 0.9380531, 0.94690265, 0.92920354]) scores # 均值:0.9280492497114275 scores.mean()
笔者把交叉验证的方式看作机器学习过程中的一种实验手段,实践中可实现适合自己的实验、测试工具,兼顾效率和公平性。