Python模型调优
了解了Scikit-learn在分类、回归、聚类、降维等领域的各种模型后,在实际应用中,我们还会面对许多新的问题:在众多可用模型中,应该选择哪一个?如何评价一个或多个模型的优劣?如何调整参数使一个模型具有更好的性能?这一类问题属于模型评估和参数调优,Scikit-learn在模型选择子模块model_selection中提供了若干解决方案。
在Scikit-learn体系内有以下三种方法可以用来评估模型的预测质量。在讨论分类、回归、聚类、降维等算法时,我们已经多次使用过这三种方式。
(1)使用估计器(Estimator)的score( )方法。在Scikit-learn中,估计器是一个重要的角色,分类器和回归器都属估计器,是机器学习算法的实现。score( )方法返回的是估计器得分。
(2)使用包括交叉验证在内的各种评估工具,如模型选择子模块model_selection中的cross_val_score和GridSearchCV等。
(3)使用模型评估指标子模块metrics提供的针对特定目的评估预测误差的指标函数,包括分类指标函数、回归指标函数和聚类指标函数等。
估计器得分
在本章的很多例子中,多次使用估计器的score( )方法直接给出了模型的预测精度。对于分类器的评估指标,预测精度自然是分类的准确率。以鸢尾花分类为例,分别使用准确性指标评价函数accuracy_score( )和估计器的score( )方法,可以看出,分类估计器的得分就是分类准确率,代码如下。
>>> from sklearn.datasets import load_iris
>>> from sklearn.neighbors import KNeighborsClassifier
>>> from sklearn.model_selection import train_test_split as tsplit
>>> from sklearn.metrics import accuracy_score
>>> X, y = load_iris(return_X_y=True)
>>> X_train, X_test, y_train, y_test = tsplit(X, y, test_size=0.1)
>>> knn = KNeighborsClassifier()
>>> knn.fit(X_train, y_train)
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=None, n_neighbors=5, p=2,
weights='uniform')
>>> y_pred = knn.predict(X_test)
>>> accuracy_score(y_test, y_pred) # 使用准确性指标评价函数
0.9333333333333333
>>> knn.score(X_test, y_test) # 直接使用测试集对训练效果做出准确性评价
0.9333333333333333
对于回归分析而言,估计器的score( )方法得到的是什么呢?我们以糖尿病数据集的回归分析为例,分别使用均方误差、中位数绝对误差、复相关系数等三个指标评价函数以及估计器的score( )方法,可以看出,回归估计器的得分就是复相关系数,代码如下。
>>> from sklearn.datasets import load_diabetes
>>> from sklearn.svm import SVR
>>> from sklearn.model_selection import train_test_split as tsplit
>>> from sklearn import metrics
>>> X, y = load_iris(return_X_y=True)
>>> X_train, X_test, y_train, y_test = tsplit(X, y, test_size=0.1)
>>> svr = SVR()
>>> svr.fit(X_train, y_train)
SVR(C=1.0, cache_size=200, coef0=0.0, degree=3, epsilon=0.1, gamma='scale',
kernel='rbf', max_iter=-1, shrinking=True, tol=0.001, verbose=False)
>>> y_pred = svr.predict(X_test)
>>> metrics.mean_squared_error(y_test, y_pred) # 均方误差指标评价函数
0.0308798292687418
>>> metrics.median_absolute_error(y_test, y_pred) # 中位数绝对误差指标评价函数
0.14269629155458663
>>> metrics.r2_score(y_test, y_pred) # 复相关系数指标评价函数
0.9331926770628183
>>> svr.score(X_test, y_test) # 直接使用估计器的score()方法
0.9331926770628184
交叉验证
训练—预测,这是监督学习的标准工作模式。衡量一个监督学习模型的优劣的主要指标是模型训练的准确度。如果用全部数据进行模型训练和测试往往会导致模型过拟合,因此,通常会将全部数据分成训练集和测试集两部分,用训练集进行模型训练,用测试集来评估模型的性能。由于测试样本的准确度是一个高方差估计,因此这种准确度的评估方法会依赖不同的测试集,而交叉验证可以很好地解决这个问题。
交叉验证的原理很简单,最常用的是k折交叉验证,就是将样本等分为k份,每次用其中的k-1份作训练集,剩余1份作测试集,训练k次,返回每次的验证结果。本章前面已经多次使用过模型选择子模块model_selection中的cross_val_score这个k折交叉验证器。
但是k折交叉验证隐含着一个风险。以葡萄酒分类数据集为例,其150个样本是按标签排序的,前50个样本是标签为0的,中间50个样本是标签为1的,后50个样本是标签为2的。如果使用3折交叉验证策略,就会出现用其中两个标签的样本训练,用另外一个标签的样本测试的极端情况,代码如下。
>>> from sklearn.datasets import load_wine
>>> X, y = load_wine(return_X_y=True)
>>> y.shape
(150,)
>>> y
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
对此,cross_val_score通过cv参数提供了多种解决方案。例如,可以为cv参数指定一个交叉验证分离器,这个分离器是model_selection.Kfold类的一个实例。下面的代码使用交叉验证分离器指定一个分层的3折交叉验证策略。如果分离器的shuffle参数为False,则退化为常规的3折交叉验证。
>>> from sklearn.tree import DecisionTreeClassifier
>>> from sklearn.model_selection import cross_val_score
>>> from sklearn.model_selection import KFold
>>> dtc = DecisionTreeClassifier() # 实例化决策树分类器
>>> cv = KFold(n_splits=3, shuffle=True, random_state=0) # 实例化交叉验证分离器
>>> cross_val_score(dtc, X, y, cv=cv) # 交叉验证
array([0.92, 0.92, 0.96])
另外还可以使用随机交叉分离器ShuffleSplit类。和k折交叉验证将样本等分为k份不同,ShuffleSplit类不保证每一份都是不同的,并且还可以指定训练样本和测试样本的比例,代码如下。
>>> from sklearn.model_selection import ShuffleSplit
>>> cv = ShuffleSplit(n_splits=3, test_size=.25, random_state=0)
>>> cross_val_score(dtc, X, y, cv=cv) # 交叉验证
array([0.97368421, 0.92105263, 0.92105263])
如果k折交叉验证中k的取值等于样本数量,则意味着每次使用一个样本做验证,这样的交叉验证方法又称为留一法。对于样本数量巨大的数据集,这种验证方法显然不够“聪明”,但对于较小的数据集,有时可能会非常有用。
>>> from sklearn.model_selection import LeaveOneOut
>>> cv = LeaveOneOut()
>>> cross_val_score(dtc, X, y, cv=cv)
array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 1., 1., 1., 1., 0., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 0., 1., 1., 1., 1., 1., 1., 0., 1., 1., 1., 1., 1., 0., 1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 1., 1., 0., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
0., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
>>> cross_val_score(dtc, X, y, cv=cv).mean()
0.94
评价指标
评价一个模型的性能优劣是一件非常复杂且困难的工作。尽管本章多次使用了精度这个概念来评估模型,但即便模型精度高达0.99也不意味着这个模型一定非常优秀。例如,从人群体检数据样本中筛查某一种疾病的患者,如果该疾病在人群中的罹患概率小于1%,那么就算把所有样本都判为阴性(表示没有罹患目标疾病),这个模型的精度也不会低于0.99。
对于二分类问题,习惯上把希望检测出的那一类称为正例(Positive Class),另一类称为负例(Negative Class)。在上面的例子中,阳性的样本就是正例,阴性的样本就是负例。如此一来,二分类结果就有了4种可能:真正例(TP)、假正例(FP)、真负例(TN)和假负例(FN)。可见,评估二分类问题的最好方法是图8-19所示的混淆矩阵。
模型评估指标子模块metrics提供了confusion_matrix混淆矩阵类,下面用它来评估对威斯康星州乳腺癌数据集进行的二分类的结果。
>>> from sklearn.datasets import load_breast_cancer
>>> from sklearn.svm import SVC
>>> from sklearn.metrics import confusion_matrix
>>> from sklearn.model_selection import train_test_split as tsplit
>>> X, y = load_breast_cancer(return_X_y=True)
>>> X_train, X_test, y_train, y_test = tsplit(X, y, test_size=0.1)
>>> svc = SVC() # 实例化支持向量机分类器
>>> svc.fit(X_train, y_train) # 训练
SVC(C=1.0, break_ties=False, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape='ovr', degree=3, gamma='scale', kernel='rbf',
max_iter=-1, probability=False, random_state=None, shrinking=True,
tol=0.001, verbose=False)
>>> y_pred = svc.predict(X_test) # 预测
>>> confusion_matrix(y_test, y_pred) # 返回混淆矩阵
array([[15, 5],
[ 0, 37]], dtype=int64)
>>> svc.score(X_test, y_test) # 模型精度
0.9122807017543859
从结果看,模型的精度为0.91,这不算很突出,但假负例样本为0,假正例样本为5,这意味着没有1例真正的乳腺癌样本被漏判为阴性,有5例样本被误判为阳性。作为筛查乳腺癌的模型,这样的表现值得信赖。
以混淆矩阵为评估手段,模型精度(accuracy)可以定义为分类正确的样本个数(真正例和真负例)占总样本的比例。
模型评估指标子模块metrics提供了若干针对特定目的评估预测误差的指标函数,包括分类指标函数、回归指标函数和聚类指标函数等。针对上面的乳腺癌数据集分类结果,下面的代码使用多个分类指标函数对这个模型做出评价。
>>> from sklearn.metrics import accuracy_score
>>> accuracy_score(y_test, y_pred) # 模型精度
0.9122807017543859
>>> from sklearn.metrics import precision_score
>>> precision_score(y_test, y_pred) # 模型准确率
0.8809523809523809
>>> from sklearn.metrics import recall_score
>>> recall_score(y_test, y_pred) # 模型召回率
1.0
>>> from sklearn.metrics import f1_score
>>> f1_score(y_test, y_pred) # f1分值
0.9367088607594937
在回归模型的评估方法中,mean_squared_error()函数,即均方误差(MSE)最为常用;r2_score()函数,即复相关系数(R2)也经常被用到;median_absolute_error()函数,即中位数绝对误差则较少被用到。关于这三个函数的使用,请参考本书8.8.1小节的例子。
参数调优
通常一个机器学习器模型会有很多参数,其中有些参数可以从学习中得到,而有些参数只能靠经验来设定,这类参数就是超参数。选择最优的超参数是应用机器学习解决实际问题过程中至关重要的一步,一组最优参数可以显著提高模型的泛化能力。那么如何找到最优参数呢?Scikit-learn主要有两种参数调优的方法,分别为网格搜索法和随机搜索法。
网格搜索法是遍历多个参数多个取值的全部组合,使用每一组参数组合做训练和评估测试,记录评估结果,最终找出最优评估结果,该结果对应的参数就是最优参数。显然,训练的时间与数据集大小、训练次数、参数数量以及每个参数的取值数量正相关。当数据集较大时,网格搜索法耗时非常长。因此使用网格搜索法参数调优(调参)时,应尽可能减少参与调参的参数个数,限制每一个参数的取值数量。
随机搜索法类似于网格搜索法,只是不需要给出每个参数的取值,而是给出每个参数的取值范围。该方法会在每个参数的取值范围内随机取值,得到参数组合并对其进行训练和评估。随机搜索法适用于参数取值不确定,需要在较大的范围内寻找最优参数的场合。
我们以支持向量机回归模型的参数调优为例,演示网格搜索法的调参方法。数据仍然使用本书8.5.2小节的曲面数据集,训练样本共1000个,测试样本共2500个。
>>> import numpy as np
>>> x, y = np.mgrid[-2:2:50j,-2:2:50j]
>>> z = x*np.exp(-x**2-y**2)
>>> _x = np.random.random(1000)*4 - 2
>>> _y = np.random.random(1000)*4 - 2
>>> _z = _x*np.exp(-_x**2-_y**2) + (np.random.random(1000)-0.5)*0.1
>>> X_train = np.stack((_x, _y), axis=1) # 训练样本集
>>> y_train = _z # 训练标签集
>>> X_test = np.stack((x.ravel(), y.ravel()), axis=1) # 测试样本集
>>> y_test = z.ravel() # 测试标签集
模型选择子模块model_selection提供了GridSearchCV类实现网格搜索法。使用网格搜索法需要事先指定参数C和参数gamma的若干取值。回归模型使用支持向量机回归模型SVR,kernel参数使用默认的rbf核函数(高斯核函数),拟对误差项的惩罚参数C和核系数gamma做参数调优。
>>> from sklearn.svm import SVR
>>> from sklearn.model_selection import GridSearchCV
>>> args = {
'C': [0.001, 0.01, 0.1, 1, 10, 100, 1000], # 指定参数C的7种取值
'gamma': [0.001, 0.01, 0.1, 1, 10, 100, 1000] # 指定参数C的7种取值
}
>>> gs = GridSearchCV(SVR(), args, cv=5) # 实例化网格搜索器
>>> gs.fit(X_train, y_train)
GridSearchCV(cv=5, error_score=nan,
estimator=SVR(C=1.0, cache_size=200, coef0=0.0, degree=3,
epsilon=0.1, gamma='scale', kernel='rbf',
max_iter=-1, shrinking=True, tol=0.001,
verbose=False),
iid='deprecated', n_jobs=None,
param_grid={'C': [0.001, 0.01, 0.1, 1, 10, 100, 1000],
'gamma': [0.001, 0.01, 0.1, 1, 10, 100, 1000]},
pre_dispatch='2*n_jobs', refit=True, return_train_score=False,
scoring=None, verbose=0)
>>> gs.score(X_test, y_test) # 评估网格搜索器
0.9724349359564686
>>> gs.best_params_ # 当前最优参数
{'C': 1, 'gamma': 1}
网格搜索法的结果显示参数C的最优选择是1,参数gamma的最优选择也是1。使用最优参数对测试集做回归分析,并将结果直观显示出来,效果如图8-20所示。
>>> z_gs = gs.predict(X_test) # 对测试集做回归分析
>>> import matplotlib.pyplot as plt
>>> import mpl_toolkits.mplot3d
>>> ax = plt.subplot(111, projection='3d')
>>> ax.scatter3D(_x, _y, _z, c='r')
<mpl_toolkits.mplot3d.art3d.Path3DCollection object at 0x000001D8B32C2648>
>>> ax.plot_surface(x, y, z_gs.reshape(x.shape), cmap=plt.cm.hsv, alpha=0.5)
<mpl_toolkits.mplot3d.art3d.Poly3DCollection object at 0x000001D8B5590148>
>>> plt.show()