交叉验证
交叉验证(cross-validation)是一种评估泛化性能的统计学方法,它比单次划分训练集和测试集的方法更加稳定、全面。在交叉验证中,数据被多次划分,并且需要训练多个模型。最常用的交叉验证是 k 折交叉验证(k-fold cross-validation),其中 k 是由用户指定的数字(设置把数据平均分成几份每一部分叫作折(fold))。接下来训练一系列模型。使用第 1 折作为测试集、其他折作为训练集来训练第一个模型,然后在 1 折上评估精度。依次类推,把第2折作为测试集,其他择作为训练集继续训练第二个模型……
scikit-learn 中的交叉验证
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,cv=5)
print(scores)
print(scores.mean())
可以通过修改 cv 参数来改变折数。
可以从交叉验证平均值中得出结论,预计模型的平均精度约为 97%。观察 5 折交叉验证得到的所有 5 个精度值,折与折之间的精度有较大的变化,可能只是因为数据集的数据量太小。
分层k折交叉验证和其他策略
scikit-learn 在分类问题中不使用简单的k折交叉验证策略,而是使用分层 k 折交叉验证(stratified k-fold cross-validation)。在分层交叉验证中,我们划分数据,使每个折中类别之间的比例与整个数据集中的比例相同。
建议使用分层k折交叉验证,而不是k折交叉验证。因为它可以对泛化性能做出更可靠的估计。
对交叉验证的更多控制
可以利用 cv 参数来调节 cross_val_score 所使用的折数。但 scikit-learn允许提供一个交叉验证分离器(cross-validation splitter)作为 cv 参数,来对数据划分过程进行更精细的控制。对于大多数使用场景而言,回归问题默认的 k 折交叉验证与分类问题的分层 k 折交叉验证的表现都很好,但有些情况下可能希望使用不同的策略。
从 model_selection 模块中导入 KFold 分离器类:
from sklearn.model_selection import KFold
kfold = KFold(n_splits=3,random_state=42,shuffle=True)
scores = cross_val_score(logreg,iris.data,iris.target,cv=kfold)
print(scores)
print(scores.mean())
KFold 的shuffle 参数设为 True,表示是将数据打乱来代替分层。还需要固定 random_state 以获得可重复的打乱结果。
留一法交叉验证
另一种常用的交叉验证方法是留一法(leave-one-out)。可以将留一法交叉验证看作是每折只包含单个样本的 k 折交叉验证。对于每次划分,选择单个数据点作为测试集。这种方法可能非常耗时,特别是对于大型数据集来说,但在小型数据集上有时可以给出更好的估计结果:
from sklearn.model_selection import LeaveOneOut
loo = LeaveOneOut()
scores = cross_val_score(logreg,iris.data,iris.target,cv=loo)
print(len(scores))
print(scores.mean())
打乱划分交叉验证
另一种非常灵活的交叉验证策略是打乱划分交叉验证(shuffle-split cross-validation)。每次划分为训练集取样 train_size 个点,为测试集取样 test_size 个(不相交的)点。将这一划分方法重复 n_iter 次。
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(scores)
print(scores.mean())
打乱划分交叉验证可以在训练集和测试集大小之外独立控制迭代次数,它还允许在每次迭代中仅使用部分数据,这可以通过设置 train_size 与 test_size 之和不等于 1 来实现。用这种方法对数据进行二次采样可能对大型数据上的试验很有用。
分组交叉验证
另一种非常常见的交叉验证适用于数据中的分组高度相关时,可以使用 GroupKFold,它以 groups 数组作为参数。数据分组的这种例子常见于医疗应用,可能拥有来自同一名病人的多个样本,但想要将其泛化到新的病人。同样,在语音识别领域,的数据集中可能包含同一名发言人的多条记录,但希望能够识别新的发言人的讲话。
下面这个示例用到了一个由 groups 数组指定分组的模拟数据集。这个数据集包含 12 个数据点,且对于每个数据点,groups 指定了该点所属的分组一共分成 4个组,前 3 个样本属于第一组,接下来的 4 个样本属于第二组,以此类推:
from sklearn.model_selection import GroupKFold
from sklearn.datasets import make_blobs
X,Y = make_blobs(n_samples=12,random_state=0)
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(scores)
网格搜索
网格搜索的目的是找出提升模型的泛化性能的最优参数。
简单网格搜索
可以实现一个简单的网格搜索,在 2 个参数上使用 for 循环,对每种参数组合分别训练并评估一个分类器(在有限范围内穷举所有组合):
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
X_train,X_test,Y_train,Y_test = train_test_split(iris.data,iris.target,random_state=0)
print("size of training set:{},size of test set:{}".format(X_train.shape[0],X_test.shape[0]))
best_score = 0
for gamma in [0.001,0.01,0.1,1,10,100]:
for C in [0.001,0.01,0.1,1,10,100]:
svm = SVC(gamma=gamma,C=C)
svm.fit(X_train,Y_train)
score = svm.score(X_test,Y_test)
if score > best_source:
best_score = score
best_parameters = {'C': C, 'gamma': gamma}
print('best score:{}'.format(best_score))
print('best parameters:{}'.format(best_parameters))
参数过拟合的风险与验证集
简单网格搜索尝试了许多不同的参数,并选择了在测试集上精度最高的那个,但这个精度不一定能推广到新数据上。由于使用测试数据进行调参,所以不能再用它来评估模型的好坏。最开始需要将数据划分为训练集和测试集也是因为这个原因。需要一个独立的数据集来进行评估,一个在创建模型时没有用到的数据集。
为了解决这个问题,一种方法是再次划分数据,这样我们得到 3 个数据集:用于构建模型的训练集,用于选择模型参数的验证集(开发集),用于评估所选参数性能的测试集。
best_score = 0
for gamma in [0.001,0.01,0.1,1,10,100]:
for C in [0.001,0.01,0.1,1,10,100]:
svm = SVC(gamma=gamma,C=C)
svm.fit(X_train,Y_train)
score = svm.score(X_valid,Y_valid)
if score > best_score:
best_score = score
best_parameters = {'C': C, 'gamma': gamma}
svm = SVC(**best_parameters)
svm.fit(X_trainval,Y_trainval)
test_score = svm.score(X_test, Y_test)
print("Best score on validation set: {:.2f}".format(best_score))
print("Best parameters: ", best_parameters)
print("Test set score with best parameters: {:.2f}".format(test_score))
训练集、验证集和测试集之间的区别对于在实践中应用机器学习方法至关重要。任何根据测试集精度所做的选择都会将测试集的信息“泄漏”(leak)到模型中。因此,保留一个单独的测试集是很重要的,它仅用于最终评估。好的做法是利用训练集和验证集的组合完成所有的探索性分析与模型选择,并保留测试集用于最终评估。严格来说,在测试集上对不止一个模型进行评估并选择更好的那个,将会导致对模型精度过于乐观的估计。
带交叉验证的网格搜索
虽然将数据划分为训练集、验证集和测试集的方法是可行的,也相对常用,但这种方法对数据的划分方法相当敏感(如上面的例子,"简单网格搜索"与"参数过拟合的风险与验证集"选出来的最优参数不一样)。
为了得到对泛化性能的更好估计,我们可以使用交叉验证来评估每种参数组合的性能,而不是仅将数据单次划分为训练集与验证集。
best_score = 0
for gamma in [0.001,0.01,0.1,1,10,100]:
for C in [0.001,0.01,0.1,1,10,100]:
svm = SVC(gamma=gamma,C=C)
scores = cross_val_score(svm,X_trainval,Y_trainval,cv=5)
score =scores.mean()
if score > best_score:
best_score = score
best_parameters = {'C': C, 'gamma': gamma}
print('best score:{}'.format(best_score))
print('best parameters:{}'.format(best_parameters))
这次算出来的参数与前面两次又不一样,但这次的更可信一些。
由于带交叉验证的网格搜索是一种常用的调参方法,因此scikit-learn提供了GridSearchCV类,它以估计器(estimator)的形式实现了这种方法。要使用 GridSearchCV类,首先需要用一个字典指定要搜索的参数。然后 GridSearchCV 会执行所有必要的模型拟合。字典的键是要调节的参数名称,字典的值是想要尝试的参数设置。
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
param_grid={
'C':[0.001,0.01,0.1,1,10,100],
'gamma':[0.001,0.01,0.1,1,10,100]
}
X_train,X_test,Y_train,Y_test= train_test_split(iris.data,iris.target,random_state=0)
grid_searchcv = GridSearchCV(SVC(),param_grid=param_grid,cv=5)
grid_searchcv.fit(X_train,Y_train)
print('test set score:{:f}'.format(grid_searchcv.score(X_test,Y_test)))
print('best parameters:{}'.format(grid_searchcv.best_params_))
print('best score:{}'.format(grid_searchcv.best_score_))
print('best model:{}'.format(grid_searchcv.best_estimator_ ))
best_score_ 属性保存的是交叉验证的平均精度,是在训练集上进行交叉验证得到的。
best_params_ 属性保存的是最佳参数
best_estimator_ 属性保存的是最佳参数对应的模型
由于 grid_search 本身具有 predict 和 score 方法,所以一般不需要使用 best_estimator_ 来进行预测或评估模型。
分析交叉验证的结果
将交叉验证的结果可视化通常有助于理解模型泛化能力对所搜索参数的依赖关系。由于运行网格搜索的计算成本相当高,所以通常最好从相对比较稀疏且较小的网格开始搜索。。网格搜索的结果可以在 cv_results_ 属性中找到,它是一个字典,其中保存了搜索的所有内容。
scores = np.array(results.mean_test_score).reshape(6,6)
plt.imshow(scores,cmap='viridis')
plt.xticks(np.arange(len(param_grid['gamma'])),labels=param_grid['gamma'])
plt.yticks(np.arange(len(param_grid['C'])),labels=param_grid['C'])
plt.xlabel("gamma")
plt.ylabel("C")
for i in range(len(param_grid['C'])):
for j in range(len(param_grid['gamma'])):
plt.text(j, i, "{:.2f}".format(scores[i, j]),ha="center", va="center", color="black")
plt.colorbar()
热图中的每个点对应于运行一次交叉验证以及一种特定的参数设置。颜色表示交叉验证的精度:浅色表示高精度,深色表示低精度。
基于交叉验证分数来调节参数网格是非常好的,也是探索不同参数的重要性的好方法。
在非网格的空间中搜索
在某些情况下,尝试所有参数的所有可能组合并不可靠,例如,SVC 有一个 kernel 参数,根据所选择的 kernel(内核),其他参数也是与之相关的。如果 kernel=‘linear’,那么模型是线性的,只会用到 C 参数。如果kernel=‘rbf’,则需要使用 C 和 gamma 两个参数。这种情况下,搜索 C、gamma 和 kernel 所有可能的组合没有意义。
为了处理这种“条件 ”(conditional)参 数,GridSearchCV的param_grid可以是字典组成的列表(a list of dictionaries)。列表中的每个字典可扩展为一个独立的网格。包含内核与参数的网格搜索可能如下所示。
param_grid = [{'kernel':['rbf'],'C':[0.001,0.01,0.1,1,10,100],'gamma':[0.001,0.01,0.1,1,10,100]},
{'kernel':['linear'],'C':[0.001,0.01,0.1,1,10,100]}]
grid_search = GridSearchCV(SVC(),param_grid,cv=5)
grid_search.fit(X_train,Y_train)
print("best parameters:{}".format(grid_search.best_params_))
print("best cross-validation score:{}".format(grid_search.best_score_))
使用不同的交叉验证策略进行网格搜索
与 cross_val_score 类似,GridSearchCV 对分类问题默认使用分层 k 折交叉验证,对回归问题默认使用 k 折交叉验证。但是可以传入任何交叉验证分离器作为GridSearchCV 的cv 参数。
如果只想将数据单次划分为训练集和验证集,可以使用 ShuffleSplit 或 StratifiedShuffleSplit,并设置 n_iter=1。这对于非常大的数据集或非常慢的模型可能会有帮助。
嵌套交叉验证
不是只将原始数据一次划分为训练集和测试集,而是使用交叉验证进行多次划分,这就是所谓的嵌套交叉验证(nested cross-validation)。在嵌套交叉验证中,有一个外层循环,遍历将数据划分为训练集和测试集的所有划分。对于每种划分都运行一次网格搜索(对于外层循环的每种划分可能会得到不同的最佳参数)。然后,对于每种外层划分,利用最佳参数设置计算得到测试集分数。
在 scikit-learn 中实现嵌套交叉验证很简单,调用 cross_val_score,并用 GridSearchCV的一个实例作为模型
from sklearn.model_selection import cross_val_score
scores = cross_val_score(GridSearchCV(SVC(),param_grid,cv=5),iris.data,iris.target,cv=5)
print("Cross-validation scores: ", scores)
print("Mean cross-validation score: ", scores.mean())
交叉验证与网格搜索并行
虽然在许多参数上运行网格搜索和在大型数据集上运行网格搜索的计算量可能很大,但这些计算都是并行的(parallel)。这也就是说,在一种交叉验证划分下使用特定参数设置来构建一个模型,与利用其他参数的模型是完全独立的。这使得网格搜索与交叉验证成为多个 CPU 内核或集群上并行化的理想选择。可以将 n_jobs 参数设置为你想使用的 CPU 内核数量,从而在 GridSearchCV 和 cross_val_score 中使用多个内核。也可以设置 n_jobs=-1 来使用所有可用的内核