其它章节内容请见机器学习之PyTorch和Scikit-Learn
在前面的章节中,我们学习了用于分类的基本机器学习算法以及如何在喂给这些算法前处理好数据。下面该学习通过调优算法和评估模型表现来构建良好机器学习模型的最佳实践了。本章中,我们将学习如下内容:
- 评估机器学习模型表现
- 诊断机器学习算法常见问题
- 调优机器学习模型
- 使用不同的性能指标评估预测模型
通过管道流程化工作流
在前面章节中应用不同预处理技术时,比如第4章 构建优秀的训练数据集 – 数据预处理中用于特征缩放的标准化,或第5章 通过降维压缩数据中用于数据压缩的主成分分析,我们学习到需要复用在拟合训练数据缩放及压缩新数据时的参数,比如对分离测试集中的样本。本节中,我们会学习极其趁手的工具,scikit-learn中的Pipeline
类。它让我们可以拟合包含任意数量变换步骤的模型并将其用于预测新数据。
加载威斯康星州乳腺癌数据集
本章中,我们会使用威斯康星州乳腺癌数据集,包含有569个恶性和良性肿瘤细胞的样本。数据集中的前两列分别存储样本的唯一ID和对应的诊断结果(M
= 恶性, B
= 良性)。3-32列包含30个通过细胞核数字化图像计算的真实值特征,可用于构建模型预测肿瘤是良性的还是恶性的。威斯康星州乳腺癌数据集存储在UCI机器学习库中,有关数据集更详细的信息请见https://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+(Diagnostic)。
获取威斯康星州乳腺癌数据集
可以在本书代码库中找到一份本数据集(以及本书中使用的其它数据集)的拷贝,以防读者离线操作或是UCI服务器上https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data临时掉线。比如我们从本地目录加载数据集,只需将如下行:
df = pd.read_csv( 'https://archive.ics.uci.edu/ml/' 'machine-learning-databases' '/breast-cancer-wisconsin/wdbc.data', header=None )
替换为:
df = pd.read_csv( 'your/local/path/to/wine.data', header=None )
本节中,我们会读取数据集并分3步将其分成训练集和测试集:
- 我们先使用pandas直接从UCI网站上读取数据数据集:
```
>>> import pandas as pd
>>> df = pd.read_csv('https://archive.ics.uci.edu/ml/'
... 'machine-learning-databases'
... '/breast-cancer-wisconsin/wdbc.data',
... header=None)
```
- 接着将30个特征赋给NumPy数组
X
。使用对象LabelEncoder
,我们将原字符串形式的类标签('M'
和'B'
)转换成整数:
```
>>> from sklearn.preprocessing import LabelEncoder
>>> X = df.loc[:, 2:].values
>>> y = df.loc[:, 1].values
>>> le = LabelEncoder()
>>> y = le.fit_transform(y)
>>> le.classes_
array(['B', 'M'], dtype=object)
```
- 在将类标签(诊断结果)编码到数组
y
中后,恶性肿瘤以类1
进行表示,良性肿瘤以类0
进行表示。可以对手动添加的类标签调用拟合的LabelEncoder
中的transform
方法进行映射的验证:
```
>>> le.transform(['M', 'B'])
array([1, 0])
```
- 在进入下一小节构建第一个模型管道前,我们将数据集分成训练集(占数据的80%)和单独的测试集(占数据的20%):
```
>>> from sklearn.model_selection import train_test_split
>>> X_train, X_test, y_train, y_test = \
... train_test_split(X, y,
... test_size=0.20,
... stratify=y,
... random_state=1)
```
在管道中组合转换器和评估器
在前一章中,我们学习很多学习算法要求输入特征处于同一量级来获取最优性能。因威斯康星州乳腺癌数据集用不同的量级度量,我们要在将其喂给逻辑回归等线性分类器前标准化其中的列。此外,我们假定通过主成分分析(PCA)将数据由初始的30维压缩到二维子空间,这一用于维度下降的特征提取技术在第5章中进行过介绍。
这里就不再单独对训练集和测试集做模型拟合与数据变换了,而是在管道中串起StandardScaler
、PCA
和LogisticRegression
对象:
>>> from sklearn.preprocessing import StandardScaler
>>> from sklearn.decomposition import PCA
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.pipeline import make_pipeline
>>> pipe_lr = make_pipeline(StandardScaler(),
... PCA(n_components=2),
... LogisticRegression())
>>> pipe_lr.fit(X_train, y_train)
>>> y_pred = pipe_lr.predict(X_test)
>>> test_acc = pipe_lr.score(X_test, y_test)
>>> print(f'Test accuracy: {test_acc:.3f}')
Test accuracy: 0.956
make_pipeline
函数可接收任意数量的scikit-learn转换器(支持fit
和transform
方法作为输入的对象),后接实现了fit
和predict
方法的scikit-learn评估器。在前面的示例代码中,我们提供了两个scikit-learn转换器StandardScaler
和PCA
,另有一个LogisticRegression
评估器作为make_pipeline
函数的输入,通过这些对象构建了一个scikit-learn Pipeline
对象。
可以把scikit-learn的Pipeline
看成是元估计器或对独立转换器和评估器的封装。如果调用Pipeline
的fit
方法,数据会通过对中间步骤调用fit
和transform
传递给一系列转换器,直至到达估计器对象(管道中的最后一个元素)。然后评估器对变换后的训练数据进行拟合。
上例中对pipe_lr
管道执行fit
方法时,首先StandardScaler
对训练数据调用fit
和transform
。然后将变换后的数据传入管道中的下一个对象PCA
。类似前一步,PCA
也对缩放后的输入数据执行fit
和transform
,然后传递给管道中的最一个元素,估计器。
最后,训练数据在经过StandardScaler
和PCA
的变换后使用LogisticRegression
估计器进行拟合。同样,我们应注意对管道的中间步数是没有限制的,如果希望使用管道做预测任务,最后一个管道元素就要是估计器。
与对管道调用fit
相似,如果其最后一步是估计器的话管道还实现了一个predict
方法。如果将数据集喂给Pipeline
对象实例的predict
调用,数据会通过transform
调用在中间步骤间传递。最后一步,估计器会返回对变换数据的预测。
scikit-learn库的管道是很强大的封装工具,在本书后续会频繁使用到。为确保诸君很好地掌握了Pipeline
对象原理,请仔细看图6.1,图中总结 了前面的讨论:
图6.1:管道对象的内部工作原理
使用k-折交叉难评估模型表现
本节中,我们会学习常见的交叉验证技术holdout交叉验证和k-折交叉验证,可帮助我们获取模型泛化表现可靠的预估,也就是对未见数据的模型表现。
holdout方法
评估机器学习模型泛化性能的经典且流行的方式是holdout方法。使用holdout方法,我们将初始数据集分成训练集和测试集,前者用于模型训练,后者用于评估泛化性能。但,在典型的机器学习应用中,我们还会调优和比较不同的参数配置以进一步提升未见数据预测的表现。这一过程称为模型选择,这一名称是指我们对给定分类问题我们希望选择调优参数(超参数)最优值。但如果在模型选择期间反复地复用同一测试集,那它就会变成训练集的一部分,进而模型更有可能变得过拟合。虽然有这问题,很多人仍会使用测试集来做模型实践,这并不是一种好的机器学习实践。
使用holdout方法做模型选择更好的方式是将数据分成三个部分:训练集、验证集和测试集。训练集用于拟合不同模型,然后使用验证集的表现做模型选择。测试集的优势是模型在训练和模型选择阶段未见过这部分数据,这样所获取的对新数据的泛化能力的评估偏置更小。图6.2描绘了holdout交叉难的概念,其中我们在通过不同超参数值训练后使用验证集反复评估模型的表现。一旦对超参数值达到满意程度,就开始对测试集评估模型泛化表现:
图6.2:如何使用训练集、验证集和测试集
holdout方法的一个缺点是其性能评估对如何将训练数据集分成训练和验证子集极其敏感,评估随数据样本的不同而不同。在下一小节中,我们会学习更健壮的性能评估技术,k-折交叉验证,其中我们对训练数据的k个子集重复k次holdout方法。
K-折交叉验证
在k-折交叉验证中,我们随机将训练数据集无放回的分成k部分。此处的 k – 1 个折叠,也称为训练折叠,用于做模型训练,而1折叠又称为测试折叠,用于做性能评估。重复这一流程k次,这们便获取了k个模型及性能评估。
有放回和无放回采样
我们在第3章中学习过一个演示有放回和无放回采样的示例。如果读者尚未读取该章或是要复习一下,请参阅通过随机森林组合多个决策树一节中的有放回和无放回抽样补充信息。
然后我们根据不同的独立折叠获取相对holdout方法对测试数据子区分区不那么敏感的性能评估,计算模型的平均性能。通常我们使用k-折交叉验证做模型调优,也即找到产生满意泛化性能的最优超参数值,通过对测试折叠评估模型表现来进行评估。
一旦找到满意的超参数值,就可以对完整训练数据集重新训练模型并使用独立测试数据集获取最终的性能评估。在k-折交叉验证后对整个训练数据集拟合模型的根本原因是,首先我们一般感兴趣的是单个最终模型(对比k个单独模型),其次提供更多的训练样本来学习算法一般能产生更精确、健壮的模型。
因k-折交叉验证是一种无放回的重采样技术,其优势是在每次迭代中,每个样本都只会用到一次,训练和测试部分是无关的。此外,所有测试折叠之间也不相关,也即测试折叠没有重叠部分。图6.3总结了k = 10的k-折交叉验证概念。训练数据集划分为10个部分,在10次迭代中,9个部分用于训练,一个部分用作模型评估的训练数据集。
并且每个部分的预估性能Ei (比如,分类准确度或错误率)之后用于计算评估模型的平均性能E:
图6.3:k-折交叉验证的原理
总之,k-折交叉验证通过校验集比holdout方法更好地使用了数据集,因为k-折交叉验证中所有的数据点都用于了评估。
按经验k-折交叉验证中k的一个很好的标准值是10。比如Ron Kohavi 对真实数据集和各种实验表明10-折交叉验证在偏置和方差间做了最好的权衡(A Study of Cross-Validation and Bootstrap for Accuracy Estimation and Model Selection by Kohavi, Ron, International Joint Conference on Artificial Intelligence (IJCAI) , 14 (12): 1137-43, 1995, **https://www.ijcai.org/Proceedings/95-2/Papers/016.pdf)。
但如果处理的是相对较小的数据集,增加折叠次数会很有用。如果我们增加k的值,每次迭代中会使用更多的训练数据,这会通过平分独立模型评估的泛化性能来逐步降低悲观偏差。但更大的k值也会增加交叉验证算法的运行时并产生更高方差的评估,因为训练折叠彼此更相近。另一方面,如果我们处理的是大数据集,可以选择较小的k值,比如k = 5,这样减少了重复拟合和评估不同折叠模型评估的计算成本,但仍能获得模型平均性能的精准评估。
留一法交叉验证
k-折交叉验证的一种特殊情况是留一法交叉验证 (LOOCV) 。在留一法中,我们会将折叠数量设置为与训练样本数相同(k = n) ,这样每次迭代只有一个训练样本用于测试,在处理非常小的数据集时推荐此法。
对标准k-折交叉验证做部分改进的方法是分层k-折交叉验证,它会产生更好的偏置和方差预估,尤其是针对非均分类,在前面引用的Ron Kohavi论文中也进行了研究。在分层交叉验证中,每个折叠中保存类标签占比以确保各折叠能够代码训练数据集中的类占比,我们使用scikit-learn中的StratifiedKFold
迭代器进行讲解:
>>> import numpy as np
>>> from sklearn.model_selection import StratifiedKFold
>>> kfold = StratifiedKFold(n_splits=10).split(X_train, y_train)
>>> scores = []
>>> for k, (train, test) in enumerate(kfold):
... pipe_lr.fit(X_train[train], y_train[train])
... score = pipe_lr.score(X_train[test], y_train[test])
... scores.append(score)
... print(f'Fold: {k+1:02d}, '
... f'Class distr.: {np.bincount(y_train[train])}, '
... f'Acc.: {score:.3f}')
Fold: 01, Class distr.: [256 153], Acc.: 0.935
Fold: 02, Class distr.: [256 153], Acc.: 0.935
Fold: 03, Class distr.: [256 153], Acc.: 0.957
Fold: 04, Class distr.: [256 153], Acc.: 0.957
Fold: 05, Class distr.: [256 153], Acc.: 0.935
Fold: 06, Class distr.: [257 153], Acc.: 0.956
Fold: 07, Class distr.: [257 153], Acc.: 0.978
Fold: 08, Class distr.: [257 153], Acc.: 0.933
Fold: 09, Class distr.: [257 153], Acc.: 0.956
Fold: 10, Class distr.: [257 153], Acc.: 0.956
>>> mean_acc = np.mean(scores)
>>> std_acc = np.std(scores)
>>> print(f'\nCV accuracy: {mean_acc:.3f} +/- {std_acc:.3f}')
CV accuracy: 0.950 +/- 0.014
首先我们使用训练集中的y_train
类标签初始化了sklearn.model_selection
模块中的StratifiedKFold
迭代器,并通过n_splits
参数指定了折叠的数量。在使用kfold
迭代器遍历k
折叠时,我们使用返回的train
中的索引拟合本章一开始配置的逻辑回归管道。使用pipe_lr
管道,我们保证了每次迭代中样本进行了适当的缩放(比如做标准化)。然后使用test
索引来计算模型的精确度打分,用采集的scores
列表计算平均准确度和预估的标准差。
虽然以上的示例代码对于讲解k-折交叉验证原理很有用,实际上scikit-learn还实现了一个k-折交叉验证打分器,可更简洁地使用分层k-折交叉验证评估模型:
>>> from sklearn.model_selection import cross_val_score
>>> scores = cross_val_score(estimator=pipe_lr,
... X=X_train,
... y=y_train,
... cv=10,
... n_jobs=1)
>>> print(f'CV accuracy scores: {scores}')
CV accuracy scores: [ 0.93478261 0.93478261 0.95652174
0.95652174 0.93478261 0.95555556
0.97777778 0.93333333 0.95555556
0.95555556]
>>> print(f'CV accuracy: {np.mean(scores):.3f} '
... f'+/- {np.std(scores):.3f}')
CV accuracy: 0.950 +/- 0.014
cross_val_score
方法极其有用的特性是可以将不同折叠的评估分发到主机的多个中央处理单元(CPU)上。如果将n_jobs
参数设置为1
,仅有一个CPU用于评估表现,和前面的StratifiedKFold
示例一样。但设置n_jobs=2
时,可以将10轮交叉验证分发到两个CPU上(前提是主机上有两个两个CPU),设置n_jobs=-1
,会使用主机上的所有可用CPU来进行并行计算。
评估泛化表现
有关泛化表现的方差如何评估交叉验证的详细讨论不在本书范畴内,但读者可参考有关模型评估和交叉验证更全面的文章(Model Evaluation, Model Selection, and Algorithm Selection in Machine Learning by S. Raschka),在https://arxiv.org/abs/1811.12808上有分享。这篇文章讨论了多种交叉验证技术,比如.632和.632+自举重采样交叉验证(bootstrap cross-validation)法。
此外,可在https://www.jmlr.org/papers/v6/markatou05a.html阅读M. Markatou与其他人合著的优秀文章(Analysis of Variance of Cross-validation Estimators of the Generalization Error by M. Markatou, H. Tian, S. Biswas, and G. M. Hripcsak, Journal of Machine Learning Research, 6: 1127-1168, 2005)。