其它章节内容请见机器学习之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临时掉线。比如我们从本地目录加载数据集,只需将如下行:
123456 df = pd.read_csv('https://archive.ics.uci.edu/ml/''machine-learning-databases''/breast-cancer-wisconsin/wdbc.data',header=None)替换为:
1234 df = pd.read_csv('your/local/path/to/wine.data',header=None)
本节中,我们会读取数据集并分3步将其分成训练集和测试集:
- 我们先使用pandas直接从UCI网站上读取数据数据集:
12345>>> 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'
)转换成整数:
1234567>>> 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
方法进行映射的验证:
12>>> le.transform(['M', 'B'])array([1, 0]) - 在进入下一小节构建第一个模型管道前,我们将数据集分成训练集(占数据的80%)和单独的测试集(占数据的20%):
123456>>> 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
对象:
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> 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
对象。
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-折交叉验证中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
迭代器进行讲解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
>>> 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-折交叉验证评估模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> 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)。
使用学习曲线和验证曲线调试算法
本节中,我们来看两个非常简单但强大的诊断工具,可帮助我们提升学习算法的性能:学习曲线和验证曲线,在接下的小节中,我们会讨论如何使用学习曲线诊断学习算法是否有过拟合(高方差)或欠拟合(高偏置)的问题。另外,我们还会学习验证曲线,它辅助我们处理学习算法中的常见问题。
通过学习曲线诊断偏置和方差问题
如果模型对给定训练数据集过于复杂,比如非常深的决策树,模型会倾向于过度拟合训练数据,但对未见数据的泛化不佳。通常,收集更多的训练样本可降低过拟合的程度。
但在实践中,采集更多数据通常很昂贵或根本就不可行。通过将模型训练和验证精度按训练数据集大小的函数进行绘制,我们可以轻易地监测出模型是否存在高方差或高偏置,以及收集更多的数据是否有助于解决这一问题。
但在讨论如何在scikit-learn中绘制学习曲线之前,我们通过下图先讨论这两种常见的模型问题:
图6.4:常见模型问题
左上角的图显示模型有高偏置。这个模型的训练精度和交叉验证精度都低,说明对训练数据是欠拟合的。处理这一问题的常见方式是增加模型参数,比如通过采集或构建更多的特征或在支持向量机(SVM) 或逻辑回归分类器中降低正则化程度。
右上角的图显示模型存在高方差问题,可通过训练精度和交叉验证精度之间的大间隔看出。要解决过拟合问题,我们可以收集更多的训练数据,减少模型的复杂度或增加正则化参数。
对于非正则化模型,可以通过特征选择(第4章)或特征提取(第5章)来降低过拟合程序来减少特征数。虽然采集更多的训练数据通常能降低过拟合的机率,但并不总是有效,比如,如果训练数据噪音极高或是模型已经非常接近最优。
在下面的小节中,我们会学习如何使用验证曲线来处理这些模型,但我们先来学习如何使用scikit-learn中的学习曲线函数来评估模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
>>> import matplotlib.pyplot as plt >>> from sklearn.model_selection import learning_curve >>> pipe_lr = make_pipeline(StandardScaler(), ... LogisticRegression(penalty='l2', ... max_iter=10000)) >>> train_sizes, train_scores, test_scores =\ ... learning_curve(estimator=pipe_lr, ... X=X_train, ... y=y_train, ... train_sizes=np.linspace( ... 0.1, 1.0, 10), ... cv=10, ... n_jobs=1) >>> train_mean = np.mean(train_scores, axis=1) >>> train_std = np.std(train_scores, axis=1) >>> test_mean = np.mean(test_scores, axis=1) >>> test_std = np.std(test_scores, axis=1) >>> plt.plot(train_sizes, train_mean, ... color='blue', marker='o', ... markersize=5, label='Training accuracy') >>> plt.fill_between(train_sizes, ... train_mean + train_std, ... train_mean - train_std, ... alpha=0.15, color='blue') >>> plt.plot(train_sizes, test_mean, ... color='green', linestyle='--', ... marker='s', markersize=5, ... label='Validation accuracy') >>> plt.fill_between(train_sizes, ... test_mean + test_std, ... test_mean - test_std, ... alpha=0.15, color='green') >>> plt.grid() >>> plt.xlabel('Number of training examples') >>> plt.ylabel('Accuracy') >>> plt.legend(loc='lower right') >>> plt.ylim([0.8, 1.03]) >>> plt.show() |
注意我们在实例化LogisticRegression
对象(默认迭代1000次)时传递了一个额外参数max_iter=10000
,来避免小数据集或极端正则化参数值的收敛问题(在下一节中会讲解)。成功执行以上代码后,会获得如下的学习曲线图:
图6.5:展示训练和验证数据集精度对训练样本数的学习曲线
通过learning_curve
函数中的train_sizes
参数,我们可以控制用于生成学习曲线的训练样本绝对或相对数。这里,我们设置train_sizes=np.linspace(0.1, 1.0, 10)
来使用对训练数据集大小做10个空间上均分的相对间隔。默认,learning_curve
函数使用分层k-折交叉验证计算分类器的交叉验证精度,我们通过cv
参数设置k = 10用于10-折分层交叉验证。
然后,我们通过返回的训练数据集不同大小的交叉验证训练和测试分数来计算平均精度,使用Matplotlib的plot
函数进行了绘制。此外,我们添加了平均精度的标准差绘图,使用fill_between
函数来表明评估的方差。
在上面的学习曲线图中可以看出,在训练时如果250个以上的样本时模型对训练和验证数据集的表现都很好。还可以看到样本小于250个时训练数据集的训练精度会上升,验证精度和训练精度的间隔会拉大,这表示过拟合的程度在上升。
处理验证曲线的过拟合和欠拟合
验证曲线是通过解决过拟合或欠拟合问题提升模型表现很有用的工具。验证曲线与学习曲线相关,但它绘制的是样本大小对训练和测试精度的函数,我们变化模型参数的值,比如逻辑回归中的逆向正则化参数C
。
我们下面学习如何通过scikit-learn创建验证曲线:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
>>> from sklearn.model_selection import validation_curve >>> param_range = [0.001, 0.01, 0.1, 1.0, 10.0, 100.0] >>> train_scores, test_scores = validation_curve( ... estimator=pipe_lr, ... X=X_train, ... y=y_train, ... param_name='logisticregression__C', ... param_range=param_range, ... cv=10) >>> train_mean = np.mean(train_scores, axis=1) >>> train_std = np.std(train_scores, axis=1) >>> test_mean = np.mean(test_scores, axis=1) >>> test_std = np.std(test_scores, axis=1) >>> plt.plot(param_range, train_mean, ... color='blue', marker='o', ... markersize=5, label='Training accuracy') >>> plt.fill_between(param_range, train_mean + train_std, ... train_mean - train_std, alpha=0.15, ... color='blue') >>> plt.plot(param_range, test_mean, ... color='green', linestyle='--', ... marker='s', markersize=5, ... label='Validation accuracy') >>> plt.fill_between(param_range, ... test_mean + test_std, ... test_mean - test_std, ... alpha=0.15, color='green') >>> plt.grid() >>> plt.xscale('log') >>> plt.legend(loc='lower right') >>> plt.xlabel('Parameter C') >>> plt.ylabel('Accuracy') >>> plt.ylim([0.8, 1.0]) >>> plt.show() |
使用以上的代码,我们获取参数C
的验证曲线图:
图6.6: SVM超参数C的验证曲线图
类似于learning_curve
函数,validation_curve
函数默认使用分层k-折交叉验证来评估分类器的表现。在validation_curve
函数内,我们指定待评估的参数。本例中为C
,LogisticRegression
分类器的逆向正则化参数,写作logisticregression__C
,用于访问s通过param_range
参数设置的指定范围值 scikit-learn管道中的LogisticRegression
对象。类似前一节中的学习曲线示例,我们绘制了训练和交叉验证精度与相关的标准差。
虽然不同C
值的精度判别分别小,我们还是可以看出在增加正则化强度(小C
值)时模型会有轻微的欠拟合。但在C
为大值时,就表示降低正则化强度,因此模型会趋向对数据轻微的过拟合。本例中,最佳点位于C
值在0.1和1.0之间。
通过网格搜索调优机器学习模型
在机器学习领域,有两种类型的参数:通过训练数据学习的参数,如逻辑回归中的权重,和单独优化的学习算法参数。后者是模型的调优参数(或超参数),如逻辑回归中的正则化参数或决策树中的最大深度参数。
在上一节中,我们使用验证曲线通过调优一种超参数来提升模型的性能。本节中,我们会研究一种流行的超参数优化技术,称为网格搜索,可通过查找超参数值的最优组合来提升模型表现。
通过网格搜索调优超参数
网格搜索方法相当简单:是一种暴力穷举搜索范式,可指定不同超参数的值列表,然后计算机评估每种组合的模型表现来获取集合中值的最优组合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
>>> from sklearn.model_selection import GridSearchCV >>> from sklearn.svm import SVC >>> pipe_svc = make_pipeline(StandardScaler(), ... SVC(random_state=1)) >>> param_range = [0.0001, 0.001, 0.01, 0.1, ... 1.0, 10.0, 100.0, 1000.0] >>> param_grid = [{'svc__C': param_range, ... 'svc__kernel': ['linear']}, ... {'svc__C': param_range, ... 'svc__gamma': param_range, ... 'svc__kernel': ['rbf']}] >>> gs = GridSearchCV(estimator=pipe_svc, ... param_grid=param_grid, ... scoring='accuracy', ... cv=10, ... refit=True, ... n_jobs=-1) >>> gs = gs.fit(X_train, y_train) >>> print(gs.best_score_) 0.9846153846153847 >>> print(gs.best_params_) {'svc__C': 100.0, 'svc__gamma': 0.001, 'svc__kernel': 'rbf'} |
使用以上代码,我们初始化了sklearn.model_selection
模块中的GridSearchCV
对象来训练和调优SVM管道。我们将GridSearchCV
的param_grid
参数设置为一个字典列表,以指定希望调优的参数。对于线性SVM,我们只评估逆正则化参数C
,对于径向基函数(RBF)核SVM,我们调优svc__C
及svc__gamma
参数。注意svc__gamma
是核SVM特有的。
GridSearchCV
使用k-折交叉验证对比不同超参数设置所训练的模型。通过设置cv=10
,会执行10-折交叉验证及(通过scoring='accuracy'
) 计算10个折叠的平均精度来评估模型表现。我们设置n_jobs=-1
,这样GridSearchCV
可以使用所有的处理器核心来通过并行拟合各折叠加速网格搜索,你也可以设置n_jobs
来仅使用单处理器。
在使用训练数据执行网格搜索后,我们通过best_score_
属性获取到了最佳表现模型的分数,通过best_params_
属性可获取到相应的参数。在本例中,RBF核SVM模型在svc__C = 100.0
时产生了最佳的k-折交叉验证精度:98.5%。
最后,我们使用独立测试数据集来评估最优选模型的表现,可通过GridSearchCV
对象的best_estimator_
属性获取:
1 2 3 4 |
>>> clf = gs.best_estimator_ >>> clf.fit(X_train, y_train) >>> print(f'Test accuracy: {clf.score(X_test, y_test):.3f}') Test accuracy: 0.974 |
请注意在训练集上使用最佳设置(gs.best_estimator_
) 拟合模型,完成网格搜索后手动设置clf.fit(X_train, y_train)
并非必需的。GridSearchCV
类有一个refit
参数,如果设置refit=True
(默认值)会自动对整个训练集重新拟合gs.best_estimator_
。
使用随机搜索更广泛地探索超参数配置
因网格搜索是一种穷举搜索,只要用户指定参数网格中就一定能持续到最优超参数配置。但指定大型超参数网格在实践中是开销很大。一种采样不同参数组合的替代方法是随机搜索。在随机搜索中,我们通过分布(或离散集合)随机提取超参数配置。这样让我们能以成分和时间更高效的方式探索超参数值设置更广泛的范围。这一概念见图6.7,它显示了网格搜索和随机搜索的9个超参数设置固定网格:
图6.7: 分别采样9个不同参数配置的网格搜索与随机搜索对比
重点是虽然网格搜索只探讨离散的、用户指定的选项,但如果搜索空间太稀疏,可能会错过良好的超参数配置。感兴趣的读者可以在以下文章中找到有关随机搜索的详细信息和实证研究: 《用随机搜索优化超参数》,J. Bergstra, Y. Bengio, 《机器学习研究杂志》,2012年,第281-305页,https://www.jmlr.org/papers/volume13/bergstra12a/bergstra12a。
我们来看如何使用随机搜索调优SVM。Scikit-learn实现了一个RandomizedSearchCV
类,类似于我们在前面的小节中使用的GridSearchCV
。主要的区别在于,我们可以将分布指定到参数网格中,并指定要评估的超参数配置的总数。例如,我们考虑前面小节网格搜索示例中用于调优SVM的几个超参数的范围:
1 2 3 |
>>> import scipy.stats >>> param_range = [0.0001, 0.001, 0.01, 0.1, ... 1.0, 10.0, 100.0, 1000.0] |
注意,虽然RandomizedSearchCV
可以接收类似的离散值列表作为参数网格的输入,这在考虑分类超参数时非常有用,但其主要优势在于我们可以用分布来替换这些列表进行采样。因此,例如我们可以使用SciPy中的以下分布来替换前面的列表:
1 |
>>> param_range = scipy.stats.loguniform(0.0001, 1000.0) |
例如,使用对数均匀分布而非常规均匀分布将确保在足够数量的试验中,从[0.0001, 0.001]中抽取的样本数量与从[10.0, 100.0]中抽取的样本数量相同。为检查其行为,我们可以通过rvs(10)
方法从该分布中抽取10个随机样本,如下所示:
1 2 3 4 5 |
>>> np.random.seed(1) >>> param_range.rvs(10) array([8.30145146e-02, 1.10222804e+01, 1.00184520e-04, 1.30715777e-02, 1.06485687e-03, 4.42965766e-04, 2.01289666e-03, 2.62376594e-02, 5.98924832e-02, 5.91176467e-01]) |
指定分布
RandomizedSearchCV
支持任意分布,只要我们可以通过调用rvs()
方法从中进行采样即可。scipy.stats
包含的所有分布列表请见:https://docs.scipy.org/doc/scipy/reference/stats.html#probability-distributions。
下面我们实际查看RandomizedSearchCV
并像前一节中的GridSearchCV
那样调优SVM:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
>>> from sklearn.model_selection import RandomizedSearchCV >>> pipe_svc = make_pipeline(StandardScaler(), ... SVC(random_state=1)) >>> param_grid = [{'svc__C': param_range, ... 'svc__kernel': ['linear']}, ... {'svc__C': param_range, ... 'svc__gamma': param_range, ... 'svc__kernel': ['rbf']}] >>> rs = RandomizedSearchCV(estimator=pipe_svc, ... param_distributions=param_grid, ... scoring='accuracy', ... refit=True, ... n_iter=20, ... cv=10, ... random_state=1, ... n_jobs=-1) >>> rs = rs.fit(X_train, y_train) >>> print(rs.best_score_) 0.9670531400966184 >>> print(rs.best_params_) {'svc__C': 0.05971247755848464, 'svc__kernel': 'linear'} |
根据这段示例代码,可以看出其使用与GridSearchCV
非常相似,区别是我们可以使用分布来指定参数范围以及通过设置n_iter=20
来指定迭代次数为20次。
通过successive halving实现资源更高效的超参数搜索
进一步拓展随机搜索的思想,scikit-learn实现了一个连续减半的变种HalvingRandomSearchCV
,在查找适合的超参数配置时更高效。连续减半是指在给定一组候选配置时,连续地淘汰较差的超参数配置,直到只剩下一个配置为止。我们可以将其流程总结为如下步骤:
- 通过随机抽样获取一组大量的候选配置。
- 使用有限资源来训练模型,例如,使用训练数据的一个小子集(而不是使用整个训练集)。
- 根据预测表现,丢弃最差的50%。
- 返回步骤2,并增加可用资源的数量。
重复以上步骤,直到只剩下一个超参数配置。注意,网格搜索变种还有一个连续减半的实现,称为HalvingGridSearchCV
,在步骤1中使用了所有指定的超参数配置,而不是随机样本。
在scikit-learn1.0中,HalvingRandomSearchCV
仍然是实验性的,因此我们需要先启用它:
1 |
>>> from sklearn.experimental import enable_halving_search_cv |
(以上代码可能无法运行或者在未来的版本不再支持)
在启用了实验性支持后,我们可以使用持续减半的随机搜索,如下:
1 2 3 4 5 6 7 8 |
>>> from sklearn.model_selection import HalvingRandomSearchCV >>> hs = HalvingRandomSearchCV(pipe_svc, ... param_distributions=param_grid, ... n_candidates='exhaust', ... resource='n_samples', ... factor=1.5, ... random_state=1, ... n_jobs=-1) |
resource='n_samples'
(默认设置)指定训练集大小,作为在每轮之间变化的资源。通过factor
参数,我们可以决定每轮淘汰多少候选样本。例如,设置factor=2
会淘汰一半的样本,而设置factor=1.5
表示只有100% / 1.5 ≈ 66% 的样本进入下一轮。与RandomizedSearchCV
选择固定迭代次数不同,我们设置n_candidates='exhaust'
(默认值),它会采样超参数配置的数量,以便在最后一轮使用最大数量的资源(此处为训练样本)。
然后,我们可以执行类似RandomizedSearchCV
的搜索:
1 2 3 4 5 6 7 8 |
>>> hs = hs.fit(X_train, y_train) >>> print(hs.best_score_) 0.9617647058823529 >>> print(hs.best_params_) {'svc__C': 4.934834261073341, 'svc__kernel': 'linear'} >>> clf = hs.best_estimator_ >>> print(f'Test accuracy: {hs.score(X_test, y_test):.3f}') Test accuracy: 0.982 |
如果将前两个小节中GridSearchCV
和RandomizedSearchCV
的结果与HalvingRandomSearchCV
模型进行比较,可以看到后者在测试集上的表现略好一些(准确率为98.2%,大于97.4%)。
使用hyperopt进行超参数调优
另一个用于超参数优化的流行库是hyperopt(https://github.com/hyperopt/hyperopt),它实现了几种不同的超参数优化方法,包括随机搜索和树状结构Parzen 估计器(TPE)方法。TPE是一种贝叶斯优化方法,它基于过往超参数评估和相关性能评分不断更新概率模型,而不是将这些评估视为独立事件。读者可以在《超参数优化算法》 Bergstra J, Bardenet R, Bengio Y, Kegl B. NeurIPS 2011. 第2546–2554页, https://dl.acm.org/doi/10.5555/2986459.2986743中了解有关TPE的更多信息。
虽然hyperopt提供了一个通用的超参数优化接口,但还有一个针对scikit-learn的特定包,称为hyperopt-sklearn,更为便利:https://github.com/hyperopt/hyperopt-sklearn。
使用嵌套交叉验证进行算法选择
用k-折交叉验证结合网格搜索或随机搜索是一种很有用的方法,可以通过改变超参数的值来调优机器学习模型的性能,在前面的小节中我们也看到了。但如果我们希望在不同的机器学习算法之间进行选择,另一种推荐的方法是嵌套交叉验证。在一项对误差估计偏差的深入研究中,Sudhir Varma和Richard Simon得出结论,使用嵌套交叉验证时,预估的真实误差相较测试数据集几乎是无偏的(《使用交叉验证做模型选择时误差估计的偏差》,S. Varma和R. Simon,BMC Bioinformatics,7(1): 91,2006,https://bmcbioinformatics.biomedcentral.com/articles/10.1186/1471-2105-7-91)。
在嵌套交叉验证中,有一个外部的k-折交叉验证循环,将数据分割为训练折叠和测试折叠,并使用内部循环对训练折叠上使用k-折交叉验证选择模型。在选取模型之后,测试折叠用于评估模型的表现。图6.8说明了仅包含五个外部折叠和两个内部折叠的嵌套交叉验证的概念,这对于计算性能为重的大型数据集会很有用;这种特定类型的嵌套交叉验证也被称为5×2交叉验证:
图6.8:嵌套交叉验证的概念
在scikit-learn中,我们可以通过网格搜索执行嵌套交叉验证,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
>>> param_range = [0.0001, 0.001, 0.01, 0.1, ... 1.0, 10.0, 100.0, 1000.0] >>> param_grid = [{'svc__C': param_range, ... 'svc__kernel': ['linear']}, ... {'svc__C': param_range, ... 'svc__gamma': param_range, ... 'svc__kernel': ['rbf']}] >>> gs = GridSearchCV(estimator=pipe_svc, ... param_grid=param_grid, ... scoring='accuracy', ... cv=2) >>> scores = cross_val_score(gs, X_train, y_train, ... scoring='accuracy', cv=5) >>> print(f'CV accuracy: {np.mean(scores):.3f} ' ... f'+/- {np.std(scores):.3f}') CV accuracy: 0.974 +/- 0.015 |
返回的平均交叉验证准确率为我们提供了一个很好的评估,可以预测到调优模型超参数并用于未知数据时所得到的结果。
例如,我们可以使用嵌套交叉验证方法来比较SVM模型和简单的决策树分类器;为了简化起见,我们只调整其深度参数:
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> from sklearn.tree import DecisionTreeClassifier >>> gs = GridSearchCV( ... estimator=DecisionTreeClassifier(random_state=0), ... param_grid=[{'max_depth': [1, 2, 3, 4, 5, 6, 7, None]}], ... scoring='accuracy', ... cv=2 ... ) >>> scores = cross_val_score(gs, X_train, y_train, ... scoring='accuracy', cv=5) >>> print(f'CV accuracy: {np.mean(scores):.3f} ' ... f'+/- {np.std(scores):.3f}') CV accuracy: 0.934 +/- 0.016 |
可以看到,SVM模型嵌套交叉验证的表现(97.4%)明显优于决策树的表现(93.4%),因此,我们预测对来自与该特定数据集相同的总体的新数据进行分类时,SVM模型可能是更优选。
查看不同性能评估指标
在前面的章节中,我们使用预测准确率来评估各机器学习模型,通常这是用于量化模型表现很有用的指标。但还有其他几个性能指标可以用于衡量模型的相关性,例如精确率、召回率、F1分数和马修斯相关系数(MCC)等。
读取混淆矩阵
在我们深入讨论各评分指标之前,先看一下混淆矩阵,这是一种展示学习算法性能的矩阵。
混淆矩阵是一个简单的方阵,报告分类器真正(TP)、真负(TN)、假正(FP)和假负(FN)的预测次数,如图6.9所示:
图6.9:混淆矩阵
虽然可以通过手动比较实际和预测类标签来轻松计算这些指标,但scikit-learn提供了一个方便的confusion_matrix
函数供我们使用,如下所示:
1 2 3 4 5 6 7 |
>>> from sklearn.metrics import confusion_matrix >>> pipe_svc.fit(X_train, y_train) >>> y_pred = pipe_svc.predict(X_test) >>> confmat = confusion_matrix(y_true=y_test, y_pred=y_pred) >>> print(confmat) [[71 1] [ 2 40]] |
执行代码后返回的数组提供了有关分类器在测试数据集上产生的不同类型错误的信息。我们可以使用Matplotlib的matshow
函数将这一信息映射到图6.9中的混淆矩阵图中:
1 2 3 4 5 6 7 8 9 10 |
>>> fig, ax = plt.subplots(figsize=(2.5, 2.5)) >>> ax.matshow(confmat, cmap=plt.cm.Blues, alpha=0.3) >>> for i in range(confmat.shape[0]): ... for j in range(confmat.shape[1]): ... ax.text(x=j, y=i, s=confmat[i, j], ... va='center', ha='center') >>> ax.xaxis.set_ticks_position('bottom') >>> plt.xlabel('Predicted label') >>> plt.ylabel('True label') >>> plt.show() |
通过以下添加标签的混淆矩阵图,应该能更容易理解结果:
图6.10:我们数据的混淆矩阵
假设本例中,类别1
(恶性)是正类,我们的模型分别正确地将71个样本分类为类别0
(TN)以及40个样本分类为类别1
(TP)。但我们的模型也错误地将两个属于类别1
的样本分类为类别0
(FN),并且它预测有一个样本是恶性,但实际上它是良性肿瘤(FP)。在下一小节中,我们将学习如何使用这些信息来计算各种错误指标。
预测错误率(ERR)和准确率(ACC)都提供了关于有多少示例被错误分类的总体信息。错误率可以理解为所有错误预测的总和除以总预测次数,而准确率则是正确预测的总和除以总预测次数,分别计算如下:
优化分类模型的准确率及召回率
预测错误率(ERR)和准确率(ACC)都提供了有关多少样本分类错误的整体信息。错误率可看作所有错误预测的总和除以总预测次数,而准确率则是正确预测的总和除以总预测次数,分别计算如下:
然后通过错误率直接计算出预测准确率:
真正例率(TPR) 和假正例率 (FPR) 是对不均衡分类问题尤为有用的性能指标:
在肿瘤诊断中,我们更关注对恶性肿瘤的检测,以帮助患者进行适当的治疗。但降低误将良性肿瘤分类为恶性肿瘤(FP)也很重要,以免给患者带来不必要的恐慌。与FPR相反,TPR提供了所有阳性样本(P)中正确识别的阳性(或相关)样本比例的有用信息。
性能指标准确率(PRE)和召回率(REC)与TP和TN率相关,实际上REC与TPR是近义词:
换句话说,召回率量化了有多少相关记录(阳性样本)被正确识别为阳性(真阳性)。准确率量化了被预测为相关(真阳性和假阳性之和)的记录中实际上相关的(真阳性)占多少:
重新回顾一下恶性肿瘤检测的案例,优化召回率有助于最小化未能检测到恶性肿瘤的几率。但代价是会预测健康患者(高FP)为恶性肿瘤。而如果优化精确率,我们着重患者患有恶性肿瘤时的正确性。但代价是会更频繁地漏掉恶性肿瘤(高FN)。
为了平衡优化PRE和REC的优缺点,使用PRE和REC的调和平均数,即所谓的F1分数:
精确率和召回率的扩展阅读
如果读者对精确率和召回率等不同性能指标的更深入讨论感兴趣,请阅读David M. W. Powers的技术报告Evaluation: From Precision, Recall and F-Factor to ROC, Informedness, Markedness & Correlation,可在https://arxiv.org/abs/2010.16061上免费阅读。
最后,汇总混淆矩阵的度量指标是MCC,它在生物研究领域特别受欢迎。MCC的计算如下:
与PRE、REC和F1分数不同,MCC的取值范围在-1到1之间,并且它考虑了混淆矩阵的所有元素,例如F1分数不涉及TN。虽然MCC值比F1分数更难计算,但它被看作是一种更优秀的度量指标,如下文所述:《二元分类评估中马修斯相关系数(MCC)较F1分数和准确率的优势》by D. Chicco and G. Jurman,BMC Genomics,2012年,281-305页,https://bmcgenomics.biomedcentral.com/articles/10.1186/s12864-019-6413-7。
这些评分指标都在scikit-learn中有实现,并可以通过sklearn.metrics
模块中导入,如以下代码片段所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
>>> from sklearn.metrics import precision_score >>> from sklearn.metrics import recall_score, f1_score >>> from sklearn.metrics import matthews_corrcoef >>> pre_val = precision_score(y_true=y_test, y_pred=y_pred) >>> print(f'Precision: {pre_val:.3f}') Precision: 0.976 >>> rec_val = recall_score(y_true=y_test, y_pred=y_pred) >>> print(f'Recall: {rec_val:.3f}') Recall: 0.952 >>> f1_val = f1_score(y_true=y_test, y_pred=y_pred) >>> print(f'F1: {f1_val:.3f}') F1: 0.964 >>> mcc_val = matthews_corrcoef(y_true=y_test, y_pred=y_pred) >>> print(f'MCC: {mcc_val:.3f}') MCC: 0.943 |
此外,在GridSearchCV
中,我们可以设置scoring参数使用与准确率不同的评分指标。可以在http://scikit-learn.org/stable/modules/model_evaluation.html上找到scoring参数所接收值的完整列表。
请记住,在scikit-learn中,正类是标记为类别1
的类别。如果我想指定其它正类标签,可以使用make_scorer
函数构建自己的评分器,然后以参数形式直接提供给GridSearchCV
的scoring
(本例中,使用f1_score
作为评价指标):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
>>> from sklearn.metrics import make_scorer >>> c_gamma_range = [0.01, 0.1, 1.0, 10.0] >>> param_grid = [{'svc__C': c_gamma_range, ... 'svc__kernel': ['linear']}, ... {'svc__C': c_gamma_range, ... 'svc__gamma': c_gamma_range, ... 'svc__kernel': ['rbf']}] >>> scorer = make_scorer(f1_score, pos_label=0) >>> gs = GridSearchCV(estimator=pipe_svc, ... param_grid=param_grid, ... scoring=scorer, ... cv=10) >>> gs = gs.fit(X_train, y_train) >>> print(gs.best_score_) 0.986202145696 >>> print(gs.best_params_) {'svc__C': 10.0, 'svc__gamma': 0.01, 'svc__kernel': 'rbf'} |
绘制ROC曲线
接收者操作特征(Receiver Operating Characteristic,ROC)曲线是根据其在假正率(FPR)和真正率(TPR)选择分类模型很有用的工具,这些指标通过改变分类器的决策阈值来计算。ROC曲线的对角线可理解为随机猜测,对角线以下的分类模型看作比随机猜测还要差。完美的分类器将位于图形的左上角,其TPR为1、FPR为0。基于ROC曲线,我们可以计算所谓的曲线下面积(ROC AUC),用于描述分类模型的性能。
类似于ROC曲线,我们可以对分类器的不同概率阈值计算精确率-召回率曲线。scikit-learn中也实现了绘制这些精确率-召回率曲线的函数,参见官方文档http://scikit-learn.org/stable/modules/generated/sklearn.metrics.precision_recall_curve.html。
执行以下示例代码,我们将绘制一个ROC曲线,该分类器仅使用威斯康星州乳腺癌数据集中的两个特征来预测肿瘤是良性还是恶性。尽管我们使用之前定义的逻辑回归管道,但这次只使用两个特征。这样做是为了使分类任务对分类器更具挑战性,通过隐藏其他特征中包含的有用信息,使得到的ROC曲线视觉上更有趣。出于类似的原因,我们还将StratifiedKFold
验证器中的折叠数减少到三。代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
>>> from sklearn.metrics import roc_curve, auc >>> from numpy import interp >>> pipe_lr = make_pipeline( ... StandardScaler(), ... PCA(n_components=2), ... LogisticRegression(penalty='l2', random_state=1, ... solver='lbfgs', C=100.0) ... ) >>> X_train2 = X_train[:, [4, 14]] >>> cv = list(StratifiedKFold(n_splits=3).split(X_train, y_train)) >>> fig = plt.figure(figsize=(7, 5)) >>> mean_tpr = 0.0 >>> mean_fpr = np.linspace(0, 1, 100) >>> all_tpr = [] >>> for i, (train, test) in enumerate(cv): ... probas = pipe_lr.fit( ... X_train2[train], ... y_train[train] ... ).predict_proba(X_train2[test]) ... fpr, tpr, thresholds = roc_curve(y_train[test], ... probas[:, 1], ... pos_label=1) ... mean_tpr += interp(mean_fpr, fpr, tpr) ... mean_tpr[0] = 0.0 ... roc_auc = auc(fpr, tpr) ... plt.plot(fpr, ... tpr, ... label=f'ROC fold {i+1} (area = {roc_auc:.2f})') >>> plt.plot([0, 1], ... [0, 1], ... linestyle='--', ... color=(0.6, 0.6, 0.6), ... label='Random guessing (area=0.5)') >>> mean_tpr /= len(cv) >>> mean_tpr[-1] = 1.0 >>> mean_auc = auc(mean_fpr, mean_tpr) >>> plt.plot(mean_fpr, mean_tpr, 'k--', ... label=f'Mean ROC (area = {mean_auc:.2f})', lw=2) >>> plt.plot([0, 0, 1], ... [0, 1, 1], ... linestyle=':', ... color='black', ... label='Perfect performance (area=1.0)') >>> plt.xlim([-0.05, 1.05]) >>> plt.ylim([-0.05, 1.05]) >>> plt.xlabel('False positive rate') >>> plt.ylabel('True positive rate') >>> plt.legend(loc='lower right') >>> plt.show() |
在上面的示例代码中,我们使用了scikit-learn中已经熟悉的StratifiedKFold
类,并在每次迭代中使用sklearn.metrics
模块中roc_curve
函数计算了pipe_lr
流水线中LogisticRegression
分类器的ROC性能。此外,我们使用从NumPy导入的interp
函数对三个折叠的平均ROC曲线进行了插值,并通过auc
函数计算了曲线下面积。得到的ROC曲线表明不同折叠之间存在一定的变化,而平均ROC AUC(0.76)介于完美得分(1.0)和随机猜测(0.5)之间:
图6.11:ROC曲线图
注意如果只对ROC AUC分数感兴趣,也可以直接从sklearn.metrics
子模块中导入roc_auc_score
函数,该函数类似于前几节介绍的其他评分函数(例如precision_score
)。
将分类器的性能报告为ROC AUC可以进一步了解分类器在不均衡样本方面的性能。但虽然准确率分数可解释为ROC曲线上的一个截止点,但A. P. Bradley指出ROC AUC和准确率指标大多数情况下是一致的:《ROC曲线下面积在机器学习算法评估中的应用》,A. P. Bradley,《模式识别》,30(7): 1145-1159, 1997,https://reader.elsevier.com/reader/sd/pii/S0031320396001422。
多类分类的评分指标
到目前为止,我们讨论的评分指标都是针对二分类系统的。可是,scikit-learn还实现了一对多(OvA)分类的宏平均和微平均方法,将这些评分指标扩展到多类别问题上。微平均通过系统的单独TP、TN、FP和FN计算得出的。例如,在一个k-类系统中,精确度分数的微平均计算如下:
宏平均可简单地计算为不同系统的平均分数:
如果我们想要均等地权衡每个实例或预测,微平均很有用,而宏平均将所有类别平等加权,以评估分类器对最常见的类标签的整体性能。
在scikit-learn中,如果我们使用二分类性能指标来评估多类分类模型,默认会使用宏平均的归一化或加权变种。加权宏平均通过将每个类标签的得分乘以在计算平均时的真实实例数来进行加权。如果我们处理类别不均衡(即不同标签的实例数不同),加权宏平均很有用。
虽然加权宏平均是scikit-learn中多类别问题的默认方法,但我们可以通过从sklearn.metrics
模块导入的不同评分函数中的average
参数来指定平均方法,例如precision_score
或make_scorer
函数:
1 2 3 4 |
>>> pre_scorer = make_scorer(score_func=precision_score, ... pos_label=1, ... greater_is_better=True, ... average='micro') |
处理类不平衡
本章中,我们多次提到了类不平衡的问题,但实际上还没有讨论如果恰当地处理这种情况。类不平衡是在处理现实世界数据时很常见的问题,即数据集中一个类别或多个类别的样本过多。可以设象几个可能会发生这种情况的领域,例如垃圾邮件过滤、欺诈检测或疾病筛查。
假设我们在本章中使用的威斯康星乳腺癌数据集中有90%的健康患者。这时,只需对所有样本预测多数类别(良性肿瘤),而不借助机器学习监督算法,就可以在测试数据集上实现90%的准确率。因此,在这样的数据集上训练模型并实现大约90%的测试准确度意味着我们的模型没有从该数据集提供的特征中学到任何有用的信息。
本节中,我们将简要介绍一些可用于处理不平衡数据集的技术。但在讨论解决这个问题的各种方法之前,我们先从数据集中创建一个不平衡数据集,原始数据集中包含357个良性肿瘤(类0
)和212个恶性肿瘤(类1
):
1 2 |
>>> X_imb = np.vstack((X[y == 0], X[y == 1][:40])) >>> y_imb = np.hstack((y[y == 0], y[y == 1][:40])) |
在这段代码中,我们取了所有357个良性肿瘤样本,并与前40个恶性肿瘤样本放在一起,创建了一个明显的类别不平衡。如果我们计算的模型准确率总是预测多数类别(良性,类0
),就会获得约90%的预测准确率:
1 2 3 |
>>> y_pred = np.zeros(y_imb.shape[0]) >>> np.mean(y_pred == y_imb) * 100 89.92443324937027 |
因此,在对这样的数据集拟合分类器时,与其比较不同模型的准确率,我们更应该关注其他指标,如精确度、召回率、ROC曲线等,具体取决于我们在应用中最关心的内容。例如,我们的优先级可能是识别出大多数患有恶性癌症的患者以便推荐额外的筛查,那么召回率应该是我们选择的指标。在垃圾邮件过滤中,如果系统不确定我们便不希望将邮件标记为垃圾邮件,那么精确率可能是更合适的指标。
除了评估机器学习模型之外,类不平衡还会影响模型拟合过程中的学习算法。由于机器学习算法通常优化拟合过程中计算为训练样本加和的奖励或损失函数,决策规则很可能会偏向于多数类别。
换句话说,算法会隐式地学习一个模型,优化基于数据集中最常见类别的预测,以在训练中最小化损失或最大化奖励。
在模型拟合过程中处理类不平衡占比的一种方法是对少数类的错误预测赋予更大的惩罚。通过scikit-learn,调整这种惩罚非常方便,只需将class_weight
参数设置为class_weight='balanced'
,大多数分类器都实现了这个功能。
处理类不平衡的其他常用策略包括增加少数类别的样本、减少多数类别的样本以及生成合成训练样本。可惜并没有一种在不同问题领域中都表现最佳的普适解决方案或技术。因此,在实践中,建议尝试不同的策略,评估结果,并选择最合适的技术。
scikit-learn库实现了一个简单的resample
函数,可以通过对数据集有放回地抽取新样本来帮助增加少数类别的样本。以下代码将从我们的不均衡的威斯康星州乳腺癌数据集中获取少数类(这里是类1
),并反复从中抽取新样本,直到它包含与类标签0
相同数量的样本为止:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> from sklearn.utils import resample >>> print('Number of class 1 examples before:', ... X_imb[y_imb == 1].shape[0]) Number of class 1 examples before: 40 >>> X_upsampled, y_upsampled = resample( ... X_imb[y_imb == 1], ... y_imb[y_imb == 1], ... replace=True, ... n_samples=X_imb[y_imb == 0].shape[0], ... random_state=123) >>> print('Number of class 1 examples after:', ... X_upsampled.shape[0]) Number of class 1 examples after: 357 |
重采样后,我们可以将原始的类0
样本与扩充采样的类1
子集合并起来,获得一均衡的数据集,代码如下所示:
1 2 |
>>> X_bal = np.vstack((X[y == 0], X_upsampled)) >>> y_bal = np.hstack((y[y == 0], y_upsampled)) |
因此,使用多数投票预测规则只能达到50%的准确率:
1 2 3 |
>>> y_pred = np.zeros(y_bal.shape[0]) >>> np.mean(y_pred == y_bal) * 100 50 |
类似地,我们可以通过从数据集中删除训练样本来减少多数类的采样。使用resample
函数缩减采样,我们只需将此前的示例代码中类1
标签与类0
交换。
生成新的训练数据以解决类不平衡问题
处理类不平衡的另一种技术是生成合成训练样本,这不在本书的范畴内。用于生成合成训练数据的最常用算法可能是人工少数类过采样法(SMOTE),读者可以在Nitesh Chawla等人的原始研究文章中了解更多该技术的更多信息:《SMOTE: 人工少数类过采样法》,《人工智能研究学报》,16:321-357,2002年,可在https://www.jair.org/index.php/jair/article/view/10302上获取。也强烈建议查看imbalance-learn,这是一个专注于不平衡数据集的Python库,包含SMOTE的实现。可以在https://github.com/scikit-learn-contrib/imbalanced-learn上了解更多关于imbalance-learn的信息。
小结
在本章的开头,我们讨论了如何在方便的模型流水线中链接不同的转换技术和分类器,这有助于我们更高效地训练和评估机器学习模型。然后,我们使用这些管道执行了k-折交叉验证,这是模型选择和评估的重要技术之一。使用k-折交叉验证,我们绘制了学习曲线和验证曲线,来诊断学习算法的常见问题,如过拟合和欠拟合。
使用网格搜索、随机搜索和successive halving,我们进一步对模型进行了调优。然后,我们使用混淆矩阵和各种性能指标来评估和优化模型在特定问题任务上的性能。最后,我们通过讨论处理不平衡数据的各种方法来结束本章,这是许多实际应用中的常见问题。现在,读者应该具备了成本构建分类监督机器学习模型的基础技术。
下一章中,我们将学习集成方法:这些方法允许我们将多个模型和分类算法组合起来,以进一步提升机器学习系统的预测性能。