Python 机器学习 基础 之 模型评估与改进 【网格搜素】的简单说明
目录
Python 机器学习 基础 之 模型评估与改进 【网格搜素】的简单说明
一、简单介绍
二、网格搜索
附录
一、参考文献
一、简单介绍
Python是一种跨平台的计算机程序设计语言。是一种面向对象的动态类型语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越多被用于独立的、大型项目的开发。Python是一种解释型脚本语言,可以应用于以下领域: Web 和 Internet开发、科学计算和统计、人工智能、教育、桌面界面开发、软件开发、后端开发、网络爬虫。
Python 机器学习是利用 Python 编程语言中的各种工具和库来实现机器学习算法和技术的过程。Python 是一种功能强大且易于学习和使用的编程语言,因此成为了机器学习领域的首选语言之一。Python 提供了丰富的机器学习库,如Scikit-learn、TensorFlow、Keras、PyTorch等,这些库包含了许多常用的机器学习算法和深度学习框架,使得开发者能够快速实现、测试和部署各种机器学习模型。
Python 机器学习涵盖了许多任务和技术,包括但不限于:
- 监督学习:包括分类、回归等任务。
- 无监督学习:如聚类、降维等。
- 半监督学习:结合了有监督和无监督学习的技术。
- 强化学习:通过与环境的交互学习来优化决策策略。
- 深度学习:利用深度神经网络进行学习和预测。
通过 Python 进行机器学习,开发者可以利用其丰富的工具和库来处理数据、构建模型、评估模型性能,并将模型部署到实际应用中。Python 的易用性和庞大的社区支持使得机器学习在各个领域都得到了广泛的应用和发展。
二、网格搜索
找到一个模型的重要参数(提供最佳泛化性能的参数)的取值是一项棘手的任务,但对于几乎所有模型和数据集来说都是必要的。由于这项任务如此常见,所以 scikit-learn
中有一些标准方法可以帮你完成。最常用的方法就是网格搜索 (grid search),它主要是指尝试我们关心的参数的所有可能组合。
网格搜索(Grid Search)是一种用于模型超参数优化的技术。它通过遍历给定的超参数空间来寻找最优组合,从而提高模型性能。网格搜索可以与交叉验证结合使用,以确保选择的参数在不同数据集上的表现都很好。
考虑一个具有 RBF(径向基函数)核的核 SVM 的例子,它在 SVC
类中实现。正如之前案例中所述,它有 2 个重要参数:核宽度 gamma
和正则化参数 C
。假设我们希望尝试 C
的取值为 0.001
、0.01
、0.1
、1
、10
和 100
,gamma
也取这 6 个值。由于我想要尝试的 C
和 gamma
都有 6 个不同的取值,所以总共有 36 种参数组合。所有可能的组合组成了 SVM 的参数设置表(网格),如下所示。
C=0.001 | C=0.01 | ... | C=10 | |
gamma=0.001 | SVC(C=0.001, gamma=0.001) | SVC(C=0.01, gamma=0.001) | ... | SVC(C=10, gamma=0.001) |
gamma=0.01 | SVC(C=0.001, gamma=0.01) | SVC(C=0.01, gamma=0.01) | ... | SVC(C=10, gamma=0.01) |
... | ... | ... | ... | ... |
gamma=100 | SVC(C=0.001, gamma=100) | SVC(C=0.01, gamma=100) | ... | SVC(C=10, gamma=100) |
1、简单网格搜索
我们可以实现一个简单的网格搜索,在 2 个参数上使用 for
循环,对每种参数组合分别训练并评估一个分类器:
# 简单的网格搜索实现
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
# 加载数据集
iris = load_iris()
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]:
# 对每种参数组合都训练一个SVC
svm = SVC(gamma=gamma, C=C)
svm.fit(X_train, y_train)
# 在测试集上评估SVC
score = svm.score(X_test, y_test)
# 如果我们得到了更高的分数,则保存该分数和对应的参数
if score > best_score:
best_score = score
best_parameters = {'C': C, 'gamma': gamma}
print("Best score: {:.2f}".format(best_score))
print("Best parameters: {}".format(best_parameters))
Size of training set: 112 size of test set: 38 Best score: 0.97 Best parameters: {'C': 100, 'gamma': 0.001}
2、参数过拟合的风险与验证集
看到这个结果,我们可能忍不住要报告,我们找到了一个在数据集上精度达到 97% 的模型。然而,这种说法可能过于乐观了(或者就是错的),其原因如下:我们尝试了许多不同的参数,并选择了在测试集上精度最高的那个,但这个精度不一定能推广到新数据上。由于我们使用测试数据进行调参,所以不能再用它来评估模型的好坏。我们最开始需要将数据划分为训练集和测试集也是因为这个原因。我们需要一个独立的数据集来进行评估,一个在创建模型时没有用到的数据集。
为了解决这个问题,一种方法是再次划分数据,这样我们得到 3 个数据集:用于构建模型的训练集,用于选择模型参数的验证集(开发集),用于评估所选参数性能的测试集。图 5-5 给出了这 3 个集合的图示:
import mglearn
import matplotlib.pyplot as plt
mglearn.plots.plot_threefold_split()
# plt.tight_layout()
plt.savefig('Images/02GridSearch-01.png', bbox_inches='tight')
plt.show()
利用验证集选定最佳参数之后,我们可以利用找到的参数设置重新构建一个模型,但是要同时在训练数据和验证数据上进行训练。这样我们可以利用尽可能多的数据来构建模型。其实现如下所示:
from sklearn.svm import SVC
# 将数据划分为训练+验证集与测试集
X_trainval, X_test, y_trainval, y_test = train_test_split(
iris.data, iris.target, random_state=0)
# 将训练+验证集划分为训练集与验证集
X_train, X_valid, y_train, y_valid = train_test_split(
X_trainval, y_trainval, random_state=1)
print("Size of training set: {} size of validation set: {} size of test set:"
" {}\n".format(X_train.shape[0], X_valid.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]:
# 对每种参数组合都训练一个SVC
svm = SVC(gamma=gamma, C=C)
svm.fit(X_train, y_train)
# 在验证集上评估SVC
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))
Size of training set: 84 size of validation set: 28 size of test set: 38 Best score on validation set: 0.96 Best parameters: {'C': 10, 'gamma': 0.001} Test set score with best parameters: 0.92
验证集上的最高分数是 96%,这比之前略低,可能是因为我们使用了更少的数据来训练模型(现在 X_train
更小,因为我们对数据集做了两次划分)。但测试集上的分数(这个分数实际反映了模型的泛化能力)更低,为 92%。因此,我们只能声称对 92% 的新数据正确分类,而不是我们之前认为的 97% !
训练集、验证集和测试集之间的区别对于在实践中应用机器学习方法至关重要。任何根据测试集精度所做的选择都会将测试集的信息“泄漏”(leak)到模型中。因此,保留一个单独的测试集是很重要的,它仅用于最终评估。好的做法是利用训练集和验证集的组合完成所有的探索性分析与模型选择,并保留测试集用于最终评估——即使对于探索性可视化也是如此。严格来说,在测试集上对不止一个模型进行评估并选择更好的那个,将会导致对模型精度过于乐观的估计。
3、带交叉验证的网格搜索
虽然将数据划分为训练集、验证集和测试集的方法(如上所述)是可行的,也相对常用,但这种方法对数据的划分方法相当敏感。从上面代码片段的输出中可以看出,网格搜索选择 'C': 10, 'gamma': 0.001
作为最佳参数,而 5.2.2 节的代码输出选择 'C': 100, 'gamma': 0.001
作为最佳参数。为了得到对泛化性能的更好估计,我们可以使用交叉验证来评估每种参数组合的性能,而不是仅将数据单次划分为训练集与验证集。这种方法用代码表示如下:
from sklearn.model_selection import cross_val_score
import numpy as np
for gamma in [0.001, 0.01, 0.1, 1, 10, 100]:
for C in [0.001, 0.01, 0.1, 1, 10, 100]:
# 对于每种参数组合都训练一个SVC
svm = SVC(gamma=gamma, C=C)
# 执行交叉验证
scores = cross_val_score(svm, X_trainval, y_trainval, cv=5)
# 计算交叉验证平均精度
score = np.mean(scores)
# 如果我们得到了更高的分数,则保存该分数和对应的参数
if score > best_score:
best_score = score
best_parameters = {'C': C, 'gamma': gamma}
# 在训练+验证集上重新构建一个模型
svm = SVC(**best_parameters)
svm.fit(X_trainval, y_trainval)
要想使用 5 折交叉验证对 C
和 gamma
特定取值的 SVM 的精度进行评估,需要训练 36×5 = 180 个模型。你可以想象,使用交叉验证的主要缺点就是训练所有这些模型所需花费的时间。
下面的可视化(图 5-6)说明了上述代码如何选择最佳参数设置:
mglearn.plots.plot_cross_val_selection()
# plt.tight_layout()
plt.savefig('Images/02GridSearch-02.png', bbox_inches='tight')
plt.show()
对于每种参数设置(图中仅显示了一部分),需要计算 5 个精度值,交叉验证的每次划分都要计算一个精度值。然后,对每种参数设置计算平均验证精度。最后,选择平均验证精度最高的参数,用圆圈标记。
如前所述,交叉验证是在特定数据集上对给定算法进行评估的一种方法。但它通常与网格搜索等参数搜索方法结合使用。因此,许多人使用交叉验证(cross-validation)这一术语来通俗地指代带交叉验证的网格搜索。
划分数据、运行网格搜索并评估最终参数,这整个过程如图 5-7 所示。
由于带交叉验证的网格搜索是一种常用的调参方法,因此 scikit-learn
提供了 GridSearchCV
类,它以估计器(estimator)的形式实现了这种方法。要使用 GridSearchCV
类,你首先需要用一个字典指定要搜索的参数。然后 GridSearchCV
会执行所有必要的模型拟合。字典的键是我们要调节的参数名称(在构建模型时给出,在这个例子中是 C
和 gamma
),字典的值是我们想要尝试的参数设置。如果 C
和 gamma
想要尝试的取值为 0.001
、0.01
、0.1
、1
、10
和 100
,可以将其转化为下面这个字典:
param_grid = {'C': [0.001, 0.01, 0.1, 1, 10, 100],
'gamma': [0.001, 0.01, 0.1, 1, 10, 100]}
print("Parameter grid:\n{}".format(param_grid))
Parameter grid: {'C': [0.001, 0.01, 0.1, 1, 10, 100], 'gamma': [0.001, 0.01, 0.1, 1, 10, 100]}
现在我们可以使用模型(SVC
)、要搜索的参数网格(param_grid
)与要使用的交叉验证策略(比如 5 折分层交叉验证)将 GridSearchCV
类实例化:
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
grid_search = GridSearchCV(SVC(), param_grid, cv=5)
GridSearchCV
将使用交叉验证来代替之前用过的划分训练集和验证集方法。但是,我们仍需要将数据划分为训练集和测试集,以避免参数过拟合:
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, random_state=0)
我们创建的 grid_search
对象的行为就像是一个分类器,我们可以对它调用标准的 fit
、predict
和 score
方法(用另一个估计器创建的 scikit-learn
估计器被称为元估计器(meta-estimator)。GridSearchCV
是最常用的元估计器,但后面我们将看到更多)。 但我们在调用 fit
时,它会对 param_grid
指定的每种参数组合都运行交叉验证:
grid_search.fit(X_train, y_train)
拟合 GridSearchCV
对象不仅会搜索最佳参数,还会利用得到最佳交叉验证性能的参数在整个训练数据集上自动拟合一个新模型。因此,fit
完成的工作相当于本节开头 In[21] 的代码结果。GridSearchCV
类提供了一个非常方便的接口,可以用 predict
和 score
方法来访问重新训练过的模型。为了评估找到的最佳参数的泛化能力,我们可以在测试集上调用 score
:
print("Test set score: {:.2f}".format(grid_search.score(X_test, y_test)))
Test set score: 0.97
利用交叉验证选择参数,我们实际上找到了一个在测试集上精度为 97% 的模型。重要的是,我们没有使用测试集 来选择参数。我们找到的参数保存在 best_params_
属性中,而交叉验证最佳精度(对于这种参数设置,不同划分的平均精度)保存在 best_score_
中:
print("Best parameters: {}".format(grid_search.best_params_))
print("Best cross-validation score: {:.2f}".format(grid_search.best_score_))
Best parameters: {'C': 10, 'gamma': 0.1} Best cross-validation score: 0.97
同样,注意不要将
best_score_
与模型在测试集上调用score
方法计算得到的泛化性能弄混。使用score
方法(或者对predict
方法的输出进行评估)采用的是在整个训练集上训练 的模型。而best_score_
属性保存的是交叉验证的平均精度,是在训练集上进行交叉验证 得到的。
能够访问实际找到的模型,这有时是很有帮助的,比如查看系数或特征重要性。你可以用 best_estimator_
属性来访问最佳参数对应的模型,它是在整个训练集上训练得到的:
print("Best estimator:\n{}".format(grid_search.best_estimator_))
Best estimator: SVC(C=10, gamma=0.1)
由于 grid_search
本身具有 predict
和 score
方法,所以不需要使用 best_estimator_
来进行预测或评估模型。
1)分析交叉验证的结果
将交叉验证的结果可视化通常有助于理解模型泛化能力对所搜索参数的依赖关系。由于运行网格搜索的计算成本相当高,所以通常最好从相对比较稀疏且较小的网格开始搜索。然后我们可以检查交叉验证网格搜索的结果,可能也会扩展搜索范围。网格搜索的结果可以在 cv_results_
属性中找到,它是一个字典,其中保存了搜索的所有内容。你可以在下面的输出中看到,它里面包含许多细节,最好将其转换成 pandas
数据框后再查看:
import pandas as pd
# 转换为DataFrame(数据框)
results = pd.DataFrame(grid_search.cv_results_)
# 显示前5行
display(results.head())
mean_fit_time | std_fit_time | mean_score_time | std_score_time | param_C | param_gamma | params | split0_test_score | split1_test_score | split2_test_score | split3_test_score | split4_test_score | mean_test_score | std_test_score | rank_test_score | |
0 | 0.000759 | 0.000387 | 0.000798 | 0.000399 | 0.001 | 0.001 | {'C': 0.001, 'gamma': 0.001} | 0.347826 | 0.347826 | 0.363636 | 0.363636 | 0.409091 | 0.366403 | 0.022485 | 22 |
1 | 0.000797 | 0.000398 | 0.000399 | 0.000489 | 0.001 | 0.01 | {'C': 0.001, 'gamma': 0.01} | 0.347826 | 0.347826 | 0.363636 | 0.363636 | 0.409091 | 0.366403 | 0.022485 | 22 |
2 | 0.000599 | 0.000489 | 0 | 0 | 0.001 | 0.1 | {'C': 0.001, 'gamma': 0.1} | 0.347826 | 0.347826 | 0.363636 | 0.363636 | 0.409091 | 0.366403 | 0.022485 | 22 |
3 | 0.000397 | 0.000487 | 0.000797 | 0.000399 | 0.001 | 1 | {'C': 0.001, 'gamma': 1} | 0.347826 | 0.347826 | 0.363636 | 0.363636 | 0.409091 | 0.366403 | 0.022485 | 22 |
4 | 0.000996 | 0.00063 | 0 | 0 | 0.001 | 10 | {'C': 0.001, 'gamma': 10} | 0.347826 | 0.347826 | 0.363636 | 0.363636 | 0.409091 | 0.366403 | 0.022485 | 22 |
results
中每一行对应一种特定的参数设置。对于每种参数设置,交叉验证所有划分的结果都被记录下来,所有划分的平均值和标准差也被记录下来。由于我们搜索的是一个二维参数网格(C
和 gamma
),所以最适合用热图可视化(见图 5-8)。我们首先提取平均验证分数,然后改变分数数组的形状,使其坐标轴分别对应于 C
和 gamma
:
scores = np.array(results.mean_test_score).reshape(6, 6)
# 对交叉验证平均分数作图
# (目前使用绘制会报错暂时不用先)
# mglearn.tools.heatmap(scores, xlabel='gamma', xticklabels=param_grid['gamma'],
# ylabel='C', yticklabels=param_grid['C'], cmap="viridis")
def drawHeatmap(scores):
# 反转scores数组的行顺序,以便(0, 0)在左下角
scores = scores[::-1, :]
# 绘制热图
fig, ax = plt.subplots()
cax = ax.matshow(scores, cmap='viridis')
fig.colorbar(cax)
# 假设param_grid['gamma']和param_grid['C']已经定义
# 这里我们使用字符串列表作为示例
param_grid = {
'gamma': ['0.1', '0.2', '0.3', '0.4', '0.5', '0.6'],
'C': ['1', '2', '3', '4', '5', '6']
}
# 设置xticks和yticks的位置
ticks_x = np.arange(len(param_grid['gamma']))
ticks_y = np.arange(len(param_grid['C']))[::-1] # 反转yticks的顺序以匹配反转的scores
# 设置xticklabels和yticklabels
ax.set_xticks(ticks_x)
ax.set_xticklabels(param_grid['gamma'])
ax.xaxis.set_tick_params(rotation=45)
ax.set_yticks(ticks_y)
ax.set_yticklabels(param_grid['C'])
# 设置x轴和y轴的标签
ax.set_xlabel('gamma')
ax.set_ylabel('C')
# 添加数值到每个单元格
for (i, j), z in np.ndenumerate(scores):
ax.text(j, i, '{:0.2f}'.format(z), ha='center', va='center')
drawHeatmap(scores)
plt.tight_layout()
plt.savefig('Images/02GridSearch-04.png', bbox_inches='tight')
plt.show()
热图中的每个点对应于运行一次交叉验证以及一种特定的参数设置。颜色表示交叉验证的精度:浅色表示高精度,深色表示低精度。你可以看到,SVC
对参数设置非常敏感。对于许多种参数设置,精度都在 40% 左右,这是非常糟糕的;对于其他参数设置,精度约为 96%。我们可以从这张图中看出以下几点。首先,我们调节的参数对于获得良好的性能 非常重要 。这两个参数(C
和 gamma
)都很重要,因为调节它们可以将精度从 40% 提高到 96%。此外,在我们选择的参数范围中也可以看到输出发生了显著的变化。同样重要的是要注意,参数的范围要足够大:每个参数的最佳取值不能位于图像的边界上。
下面我们来看几张图(见图 5-9),其结果不那么理想,因为选择的搜索范围不合适。
fig, axes = plt.subplots(1, 3, figsize=(13, 5))
param_grid_linear = {'C': np.linspace(1, 2, 6),
'gamma': np.linspace(1, 2, 6)}
param_grid_one_log = {'C': np.linspace(1, 2, 6),
'gamma': np.logspace(-3, 2, 6)}
param_grid_range = {'C': np.logspace(-3, 2, 6),
'gamma': np.logspace(-7, -2, 6)}
tempCount = 5
for param_grid, ax in zip([param_grid_linear, param_grid_one_log,
param_grid_range], axes):
grid_search = GridSearchCV(SVC(), param_grid, cv=5)
grid_search.fit(X_train, y_train)
scores = grid_search.cv_results_['mean_test_score'].reshape(6, 6)
# 对交叉验证平均分数作图
#(目前使用这个绘制会报错暂时不用先)
# scores_image = mglearn.tools.heatmap(
# scores, xlabel='gamma', ylabel='C', xticklabels=param_grid['gamma'],
# yticklabels=param_grid['C'], cmap="viridis", ax=ax)
drawHeatmap(scores)
plt.tight_layout()
plt.savefig('Images/02GridSearch-0'+str(tempCount)+'.png', bbox_inches='tight')
plt.show()
tempCount += 1
# plt.colorbar(scores_image, ax=axes.tolist())
第一张图没有显示任何变化,整个参数网格的颜色相同。在这种情况下,这是由参数 C
和 gamma
不正确的缩放以及不正确的范围造成的。但如果对于不同的参数设置都看不到精度的变化,也可能是因为这个参数根本不重要。通常最好在开始时尝试非常极端的值,以观察改变参数是否会导致精度发生变化。
第二张图显示的是垂直条形模式。这表示只有 gamma
的设置对精度有影响。这可能意味着 gamma
参数的搜索范围是我们所关心的,而 C
参数并不是——也可能意味着 C
参数并不重要。
第三张图中 C
和 gamma
对应的精度都有变化。但可以看到,在图像的整个左下角都没有发生什么有趣的事情。我们在后面的网格搜索中可以不考虑非常小的值。最佳参数设置出现在右上角。由于最佳参数位于图像的边界,所以我们可以认为,在这个边界之外可能还有更好的取值,我们可能希望改变搜索范围以包含这一区域内的更多参数。
基于交叉验证分数来调节参数网格是非常好的,也是探索不同参数的重要性的好方法。但是,你不应该在最终测试集上测试不同的参数范围——前面说过,只有确切知道了想要使用的模型,才能对测试集进行评估。
2)在非网格的空间中搜索
在某些情况下,尝试所有参数的所有可能组合(正如 GridSearchCV
所做的那样)并不是一个好主意。例如,SVC
有一个 kernel
参数,根据所选择的 kernel
(内核),其他参数也是与之相关的。如果 kernel='linear'
,那么模型是线性的,只会用到 C
参数。如果 kernel='rbf'
,则需要使用 C
和 gamma
两个参数(但用不到类似 degree
的其他参数)。在这种情况下,搜索 C
、gamma
和 kernel
所有可能的组合没有意义:如果 kernel='linear'
,那么 gamma
是用不到的,尝试 gamma
的不同取值将会浪费时间。为了处理这种“条件”(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]}]
print("List of grids:\n{}".format(param_grid))
List of grids: [{'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]}]
在第一个网格中,kernel
参数始终等于 'rbf'
(注意 kernel
是一个长度为 1 的列表),而 C
和 gamma
都是变化的。在第二个网格中,kernel
参数始终等于 'linear'
,只有 C
是变化的。下面我们来应用这个更加复杂的参数搜索:
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: {:.2f}".format(grid_search.best_score_))
Best parameters: {'C': 10, 'gamma': 0.1, 'kernel': 'rbf'} Best cross-validation score: 0.97
我们再次查看 cv_results_
。正如所料,如果 kernel
等于 'linear'
,那么只有 C
是变化的 :
results = pd.DataFrame(grid_search.cv_results_)
# 我们给出的是转置后的表格,这样更适合页面显示:
display(results.T)
0 | 1 | 2 | 3 | ... | 38 | 39 | 40 | 41 | |
param_C | 0.001 | 0.001 | 0.001 | 0.001 | ... | 0.1 | 1 | 10 | 100 |
param_gamma | 0.001 | 0.01 | 0.1 | 1 | ... | NaN | NaN | NaN | NaN |
param_kernel | rbf | rbf | rbf | rbf | ... | linear | linear | linear | linear |
params | {C : 0.001, kernel : rbf , gamma : 0.001} | {C : 0.001, kernel : rbf , gamma : 0.01} | {C : 0.001, kernel : rbf , gamma : 0.1} | {C : 0.001, kernel : rbf , gamma : 1} | ... | {C : 0.1, kernel : linear } | {C : 1, kernel : linear } | {C : 10, kernel : linear } | {C : 100, kernel : linear } |
mean_test_score | 0.37 | 0.37 | 0.37 | 0.37 | ... | 0.95 | 0.97 | 0.96 | 0.96 |
rank_test_score | 27 | 27 | 27 | 27 | ... | 11 | 1 | 3 | 3 |
split0_test_score | 0.38 | 0.38 | 0.38 | 0.38 | ... | 0.96 | 1 | 0.96 | 0.96 |
split1_test_score | 0.35 | 0.35 | 0.35 | 0.35 | ... | 0.91 | 0.96 | 1 | 1 |
split2_test_score | 0.36 | 0.36 | 0.36 | 0.36 | ... | 1 | 1 | 1 | 1 |
split3_test_score | 0.36 | 0.36 | 0.36 | 0.36 | ... | 0.91 | 0.95 | 0.91 | 0.91 |
split4_test_score | 0.38 | 0.38 | 0.38 | 0.38 | ... | 0.95 | 0.95 | 0.95 | 0.95 |
std_test_score | 0.011 | 0.011 | 0.011 | 0.011 | ... | 0.033 | 0.022 | 0.034 | 0.034 |
16 rows × 42 columns
3)使用不同的交叉验证策略进行网格搜索
与 cross_val_score
类似,GridSearchCV
对分类问题默认使用分层 k 折交叉验证,对回归问题默认使用 k 折交叉验证。但是,你可以传入任何交叉验证分离器作为 GridSearchCV
的 cv
参数,正如 2.1.3 节中的“对交叉验证的更多控制”部分所述。特别地,如果只想将数据单次划分为训练集和验证集,你可以使用 ShuffleSplit
或 StratifiedShuffleSplit
,并设置 n_iter=1
。这对于非常大的数据集或非常慢的模型可能会有帮助。
(1)嵌套交叉验证
在前面的例子中,我们先介绍了将数据单次划分为训练集、验证集与测试集,然后介绍了先将数据划分为训练集和测试集,再在训练集上进行交叉验证。但前面在使用 GridSearchCV
时,我们仍然将数据单次划分为训练集和测试集,这可能会导致结果不稳定,也让我们过于依赖数据的此次划分。我们可以再深入一点,不是只将原始数据一次划分为训练集和测试集,而是使用交叉验证进行多次划分,这就是所谓的嵌套交叉验证 (nested cross-validation)。在嵌套交叉验证中,有一个外层循环,遍历将数据划分为训练集和测试集的所有划分。对于每种划分都运行一次网格搜索(对于外层循环的每种划分可能会得到不同的最佳参数)。然后,对于每种外层划分,利用最佳参数设置计算得到测试集分数。
这一过程的结果是由分数组成的列表——不是一个模型,也不是一种参数设置。这些分数告诉我们在网格找到的最佳参数下模型的泛化能力好坏。由于嵌套交叉验证不提供可用于新数据的模型,所以在寻找可用于未来数据的预测模型时很少用到它。但是,它对于评估给定模型在特定数据集上的效果很有用。
在 scikit-learn
中实现嵌套交叉验证很简单。我们调用 cross_val_score
,并用 GridSearchCV
的一个实例作为模型:
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())
Cross-validation scores: [0.96666667 1. 0.9 0.96666667 1. ] Mean cross-validation score: 0.9666666666666668
嵌套交叉验证的结果可以总结为“SVC
在 iris
数据集上的交叉验证平均精度为 98%”——不多也不少。
这里我们在内层循环和外层循环中都使用了分层 5 折交叉验证。由于 param_grid
包含 36 种参数组合,所以需要构建 36×5×5 = 900 个模型,导致嵌套交叉验证过程的代价很高。这里我们在内层循环和外层循环中使用相同的交叉验证分离器,但这不是必需的,你可以在内层循环和外层循环中使用交叉验证策略的任意组合。理解上面单行代码的内容可能有点困难,将其展开为 for
循环可能会有所帮助,正如我们在下面这个简化的实现中所做的那样:
def nested_cv(X, y, inner_cv, outer_cv, Classifier, parameter_grid):
outer_scores = []
# 对于外层交叉验证的每次数据划分,split方法返回索引值
for training_samples, test_samples in outer_cv.split(X, y):
# 利用内层交叉验证找到最佳参数
best_parms = {}
best_score = -np.inf
# 遍历参数
for parameters in parameter_grid:
# 在内层划分中累加分数
cv_scores = []
# 遍历内层交叉验证
for inner_train, inner_test in inner_cv.split(
X[training_samples], y[training_samples]):
# 对于给定的参数和训练数据来构建分类器
clf = Classifier(**parameters)
clf.fit(X[inner_train], y[inner_train])
# 在内层测试集上进行评估
score = clf.score(X[inner_test], y[inner_test])
cv_scores.append(score)
# 计算内层交叉验证的平均分数
mean_score = np.mean(cv_scores)
if mean_score > best_score:
# 如果比前面的模型都要好,则保存其参数
best_score = mean_score
best_params = parameters
# 利用外层训练集和最佳参数来构建模型
clf = Classifier(**best_params)
clf.fit(X[training_samples], y[training_samples])
# 评估模型
outer_scores.append(clf.score(X[test_samples], y[test_samples]))
return np.array(outer_scores)
下面我们在 iris
数据集上运行这个函数:
from sklearn.model_selection import ParameterGrid, StratifiedKFold
scores = nested_cv(iris.data, iris.target, StratifiedKFold(5),
StratifiedKFold(5), SVC, ParameterGrid(param_grid))
print("Cross-validation scores: {}".format(scores))
Cross-validation scores: [0.96666667 1. 0.96666667 0.96666667 1. ]
(2)交叉验证与网格搜索并行
虽然在许多参数上运行网格搜索和在大型数据集上运行网格搜索的计算量可能很大,但令人尴尬的是,这些计算都是并行的 (parallel)。这也就是说,在一种交叉验证划分下使用特定参数设置来构建一个模型,与利用其他参数的模型是完全独立的。这使得网格搜索与交叉验证成为多个 CPU 内核或集群上并行化的理想选择。你可以将 n_jobs
参数设置为你想使用的 CPU 内核数量,从而在 GridSearchCV
和 cross_val_score
中使用多个内核。你可以设置 n_jobs=-1
来使用所有可用的内核。
你应该知道,scikit-learn
不允许并行操作的嵌套 。因此,如果你在模型(比如随机森林)中使用了 n_jobs
选项,那么就不能在 GridSearchCV
使用它来搜索这个模型。如果你的数据集和模型都非常大,那么使用多个内核可能会占用大量内存,你应该在并行构建大型模型时监视内存的使用情况。
还可以在集群内的多台机器上并行运行网格搜索和交叉验证,不过在写作本书时 scikit-learn
还不支持这一点。
对于 Spark 用户,还可以使用最新开发的 spark-sklearn
包(GitHub - databricks/spark-sklearn: (Deprecated) Scikit-learn integration package for Apache Spark ),它允许在已经建立好的 Spark 集群上运行网格搜索。
附录
一、参考文献
参考文献:[德] Andreas C. Müller [美] Sarah Guido 《Python Machine Learning Basics Tutorial》