第 6 章 模型评估和超参数调优的最佳实践

1. 用管道方法简化工作流

1.1 加载数据集

本章将研究 breast cancer dataset,它包含了569个恶性和良性肿瘤细胞样本。数据集的前两列分别存储样本的唯一ID和相应的诊断结果(M=恶性,B=良性)。列3~32包含30个根据细胞核的数字化图像计算出的特征值,可用来建立模型以预测肿瘤是良性还是恶性。威斯康星乳腺癌数据集保存在UCI机器学习存储库

案例数据集: breast cancer dataset

来源: 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

1) 从 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)

df.head()

在这里插入图片描述

df.shape

2)接着,我们把 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_

在数组 y 中对分类标签(诊断)编码,恶性肿瘤用 1 类代表,良性肿瘤用0类代表。通过调用 LabelEncoder 的 transform 方法再对两个虚拟分类标签做映射检查:

le.transform(['M', 'B'])

3)在下一小节构建第一个管道之前,先按照 8:2 的比例把数据集划分成独立的训练数据集和测试数据集:

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)

1.2 集成管道中的转换器和评估器

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(random_state=1))

pipe_lr.fit(X_train, y_train)
y_pred = pipe_lr.predict(X_test)
print('Test Accuracy: %.3f' % pipe_lr.score(X_test, y_test))

\quad make_pipeline 函数可以包括任意多个 scikit-learn 转换器(作为输入对象支持 fit 和 transform 方法),接着是实现 fit 及 predict 方法的 scikit-learn 估计器。前面的代码示例提供了 StandardScaler 和 PCA 两个转换器,以及以 LogisticRegression 估计器作为 make_pipeline 函数的输入,然后以这些对象为基础构建 scikit-learn 的 Pipeline 对象。

\quad 可以把 scikit-learn 的 Pipeline 想象为一个元估计器,或者一个独立转换器和估计器的封装。如果调用 Pipeline 的 fit 方法,数据将通过在中间步骤调用 fit 和 transform 方法来完成一系列转换器的传递,直至到达估计器对象(流水线中最后一个元素)为止。然后用估计器来拟合转换后的训练数据。

\quad 在执行前面示例代码中的 pipe_lr 流水线的 fit 方法时,StandardScaler 首先在训练数据集上调用 fit 和 transform 方法。然后将转换后的训练数据传递给流水线的下一个对象,即 PCA。与前面的步骤类似, PCA 也在缩放后的输入数据基础上调用 fit 和 transform,并将其传递给流水线中的最后一个元素,即估计器。

\quad 最后,经过调用 StandardScaler 和 PCA 完成对训练数据的转换后,用逻辑回归估计器进行拟合。我们再次强调,尽管流水线的中间步骤没有数量限制,但是流水线的最后一个元素必须是估计器。

\quad 类似在流水线上调用 fit 方法,流水线也实现了 predict 方法。如果调用 predict 方法将数据集输入 Pipeline 对象实例,流水线将通过中间步骤调用 transform 完成数据转换。估计器对象在最后一步将返回与转换后的数据相对应的预测结果。

\quad scikit-learn 库的流水线是非常有用的封装工具,本文的其余部分将经常用到。为了确保掌握 Pipeline 对象的工作机制,请仔细观察图6-1,它总结了我们在前面段落中讨论过的内容。

图 6-1
 

2. k 折交叉验证

2.1 holdout 方法(抵抗方法)

holdout 交叉验证是评估机器学习模型泛化性能的一个经典且常用的方法。使用holdout方法把初始数据集划分成独立的训练数据集和测试数据集,前者用于训练模型,后者用来评估模型的泛化性能。然而,在典型的机器学习应用中,我们也对调整和比较不同的参数设置感兴趣,以进一步提高模型在未见过数据上的预测性能。该过程也被称为模型选择,指的是对给定的分类问题,选择最优的调优参数值(也称为超参数)。然而,如果在模型选择时反复使用相同的测试数据集,它将成为训练数据的一部分,这样更容易引起模型的过拟合。 尽管存在着这个问题,许多人仍然使用测试数据集进行模型选择,这是机器学习的一个不良实践。

使用 holdout 方法进行模型选择更好的方式是将数据分为训练数据集三部分、验证数据集和测试数据集三部分。训练数据集用于拟合不同的模型,验证数据集用于在模型选择过程中验证性能。使用模型训练和模型选择步骤4中没见过的测试数据集的好处是,可以对该模型面对新数据的泛化能力有不那么偏颇的评估。图6-2说明了holdout交叉验证的概念,采用不同的参数对模型进行训练后,再在验证数据集上反复评估模型性能。一旦对调优的超参数值感到满意,就开始评估模型在测试数据集上的泛化性能。

图 6-2
 

holdout 方法的缺点是,性能评估可能会对训练数据集划分为训练数据子集和验证数据子集的方式非常敏感,评估的结果将会随不同数据样本的变化而变化。下一节将讨论更强大的性能评估技术:k折交叉验证,即在训练数据的k个子集上反复使用k次holdout方法。

2.2 k 折交叉验证

k 折交叉验证将训练数据集不重复地随机划分成 k 个,其中 k-1 个用于模型训练,1 个用于性能评估。重复该过程 k 次,我们得到 k 个模型和 k 次性能估计。

接下来,我们将根据各次独立测试的结果来计算模型的平均性能,从而获得与 holdout 方法相比对训练数据集不那么敏感的性能评估。我们通常用 k 折交叉验证为模型调优,即寻找最优的超参数值,以获得到令人满意的综合性能。

一旦找到令人满意的超参数值,我们就可以在全部训练数据集上重新训练模型,并使用独立的测试数据集得到最终性能评估。在进行k折交叉验证后,将模型拟合到整个训练数据集的基本原理是,为学习算法提供更多的训练样本通常会使模型更准确,而且更健壮。

由于 k 折交叉验证属于不放回抽样技术,其优点在于每个样本仅用于训练和验证(如测试数据子集部分)一次,与 holdout 方法相比,这将使得模型性能的评估有较小的方差。图 6-3 总结了 k 折交叉验证(k=10)的相关概念。把训练数据的数据集划分为 10 个,在 10 次迭代中,每次迭代都将 9 个用于训练模型,1 个用于评估模型。此外,根据模型在每个数据子集上的性能评估结果 E i E_i Ei(例如分类准确率或误差)来计算模型的估计平均性能 E,如图 6-3 所示。

图 6-3
 

实践证明 k 折交叉验证参数k的最优标准值为 10。 例如,Ren kohavi 通过对各种现实世界数据集的实验表明,10 折交叉验证提供了在偏差和方差之间的最佳平衡

然而,如果训练数据集相对较小,那么增加折数可能有益。如果我们加大 k 值,那么每次迭代都会有更多的训练数据,这样通过计算每个模型评估值的平均来评估泛化性能的偏差就比较小。然而,较大的 k 值也会增加交叉验证算法的计算时间,因为数据子集彼此的相似度更高,所以会产生评估值方差较高。另外,如果数据集的规模比较大,我们可以选择相对较小的 k 值,例如 k=5,这样做仍然可以完成对模型平均性能的准确评估,同时可以降低模型对不同数据子集反复拟合和评估的计算成本。

  • k 折交叉验证的特例是留一交叉验证(leave-one-out cross-validation, LOOCV)法。该方法把 k 值设置为训练样本数(k=n),这样每次迭代只有一个样本用于测试,当数据集非常小时,我们推荐使用该方法。

分层 k 折交叉验证略微改善了标准 k 折交叉验证方法,所产生评估的偏差和方差都比较低,特别是在分类比例不相等的情况下,正如 Ron kohavi 的研究所显示的那样。在分层交叉验证中,分类标签的比例会保留在每个折中,以确保每个折能代表训练数据集中类别的比例,我们将在 scikit-learn 中用 StratifiedKFold 迭代器进行说明:

分类任务中的 K 折交叉验证

import numpy as np
from sklearn.model_selection import StratifiedKFold
    

kfold = StratifiedKFold(n_splits=10,
                        random_state=1).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('Fold: %2d, Class dist.: %s, Acc: %.3f' % (k+1,
          np.bincount(y_train[train]), score))
    
print('\nCV accuracy: %.3f +/- %.3f' % (np.mean(scores), np.std(scores)))

回归任务中的 K 折交叉验证

from sklearn.model_selection import KFold

kfold = KFold(n_splits=10, shuffle=True).split(X_train, y_train)

scores = []
for k, (train, test) in enumerate(kfold):

    model.fit(X_train[train], y_train[train])
    score = mean_absolute_error(model.predict(X_train[test]), y_train[test])
    scores.append(score)
    print('Fold: %2d, score: %.3f' % (k + 1, score))

print('\nCV score: %.3f +/- %.3f' % (np.mean(scores), np.std(scores)))

首先,我们用 sklearn.model_selection 模块,在以 y_train 为分类标签的训练数据集上,通过参数 n_splits 指定分区数量来初始化 StratifiedKFold 迭代器。当 kfold 迭代器遍历 k 个分区时,用 train 返回的指标拟合在本章开始部分所建立的逻辑回归流水线。用 pipe_lr 管道来确保每次迭代中样本都得到适当缩放(例如标准化)。然后,用 test 指标来计算模型的准确率,并把这些得分存入 scores 表,用以计算估计的平均准确率和标准差。

尽管前面的代码示例对解释 k 折交叉验证工作机制很有用,但 scikit-learn 也实现了一个 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('CV accuracy scores: %s' % scores)
print('CV accuracy: %.3f +/- %.3f' % (np.mean(scores), np.std(scores)))

cross_val_score 方法极为有用的功能是,可以把不同分区的评估任务分给计算机的多个 CPU。假设把 n_jobs 参数设为 1,那么就只有一个 CPU 会用于性能评估,就像前面 StratifiedKFold 示例所展示的那样。然而,如果设置 n_jobs=2,我们就可以把 10 轮交叉验证任务分给两个 CPU来完成(如果系统有那么多 CPU 的话),如果设置 n_jobs=-1,可以用计算机上所有可用的 CPU 同时进行计算。

3. 用学习和验证曲线调试算法

在本节讨论两种简单而且强大的诊断工具,以帮助提高机器学习算法的性能:学习曲线和验证曲线。在下一小节,我们将讨论如何用学习曲线来诊断学习算法是否面临过拟合(高方差)或者欠拟合(高偏差)的问题。此外,还将研究可以帮助我们解决学习算法常见问题的验证曲线。

3.1 用学习曲线诊断偏差和方差问题

如果模型对于训练数据集过于复杂——模型中有太多的自由度或者参数——就会有过拟合训练数据的倾向,而对未见过的数据泛化能力低下。通常,收集更多的训练样本将有助于缓解过拟合。

然而,在实践中收集更多的数据往往代价高昂或者根本就不可能。通过将模型训练和验证准确率看作是训练数据集规模的函数,并绘制其图像,我们可以很容易看出,模型是否面临高偏差或者高方差问题,以及收集更多的数据是否有助于解决问题。在讨论用scikit-learn绘制学习曲线之前,让我们先通过图6-4讨论模型中常见的两个问题。

图 6-4
 

图6-4的左上图说明模型遇到高偏差问题。该模型的训练和交叉验证准确率均低,这说明模型对训练数据欠拟合。解决该问题的常用办法是增加模型的参数个数,例如,通过收集或构建额外的特征,或者放松正则化要求,就像在SVM或逻辑回归分类器所做的那样。

图 6-4 的右上图说明模型遇到高方差问题,表现是模型在训练和交叉验证的准确率上有比较大的差别。要解决过拟合的问题,可以收集更多的训练数据,减少模型的复杂度,或者增加正则化的参数等。

对于非正则化模型,它也有助于通过特征选择或者特征提取减少特征的数量,从而降低过拟合的程度。更多的训练数据可以降低模型过拟合的概率,但是这么做可能并非屡屡奏效,例如,当训练数据的噪声极大或模型已经非常接近最优的时候。

在 3.2 节中,我们将会看到如何用验证曲线来解决模型问题,但是,在此之前先让我们看看如何利用scikit-learn的学习曲线来评估模型:

import matplotlib.pyplot as plt
from sklearn.model_selection import learning_curve


pipe_lr = make_pipeline(StandardScaler(),
                        LogisticRegression(penalty='l2', random_state=1))

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 samples')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
plt.ylim([0.8, 1.03])
plt.tight_layout()
#plt.savefig('images/06_05.png', dpi=300)
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 函数加入了平均准确率的标准方差信息,用以表示估计的方差。

正如在图6-5所示的学习曲线图中所看到的,如果模型在训练中见过250多个样本,那么该模型在训练和验证数据集上表现得都不错。可以看到,对于样本数量少于250的训练数据集,模型的训练准确率提高,验证和训练准确率之间的差距扩大,这是过拟合程度越来越大的标志。

图 6-5
 

3.2 用验证曲线解决过拟合和欠拟合问题

验证曲线是通过解决过拟合和欠拟合问题来提高模型性能的有力工具。虽然验证曲线与学习曲线有关系,但是绘制的不是训练和测试准确率与样本规模之间的函数关系,而是通过调整模型参数来调优,例如逻辑回归中的逆正则化参数C。我们看看如何用scikit-learn来生成验证曲线:

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.tight_layout()
# plt.savefig('images/06_06.png', dpi=300)
plt.show()
图 6-6
 

与learning_curve函数相似,validation_curve函数默认采用分层的k折交叉验证来评估分类器的性能。在validation_curve函数里定义想要评估的参数。在这种情况下,用LogisticRegression分类器的逆正则化参数C(即’logisticregression__C’)访问scikit-learn流水线中的LogisticRegression对象,通过调用参数param_range定义值域。与前面的学习曲线示例类似,我们绘制了平均训练准确率、交叉准确率及相应的标准差。

尽管准确率与不同的C值之间的差别关系很微妙,但是当提高正则化强度(小的C值)时,可以看到模型拟合数据略显不足。然而,较大的C值意味着降低了正则化强度,所以模型往往会略显过拟合。因此,最优C值显然应该在0.01到0.1之间。

4. 通过网格搜索调优机器学习模型

机器学习有两类参数:一类是从训练数据中学习到的参数,例如逻辑回归的权重;另一类是单独优化的算法参数。后者为模型的调优参数,也被称为超参数,例如逻辑回归的正则化参数或者决策树的深度参数。

在 3 节,我们通过调整其中一个超参数,使用验证曲线来改善模型的性能。本节将介绍一种被称为网格搜索的常用超参数优化技术,该技术可以通过寻找超参数值的最优组合来进一步帮助改善模型的性能。

4.1 通过网格搜索调优超参数

网格搜索方法非常简单:它属于暴力穷举搜索类型,我们预先定义好不同的超参数值,然后让计算机针对每种组合分别评估模型的性能,从而获得最优组合参数值:

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,
                  n_jobs=-1)
gs = gs.fit(X_train, y_train)
print(gs.best_score_)
print(gs.best_params_)

用前面的代码从 sklearn.model_selection 模块中初始化 GridSearchCV 对象来训练和优化支持向量机流水线。把 GridSearchCV 的参数 param_grid 设置为字典列表来定义想要调优的各个参数。对于线性支持向量机,只评估逆正则化参数 C;对于 RBF 核支持向量机,则调优参数 svc__C 和 svc__gamma。请注意参数 svc__gamma 只在核 SVM 中适用。

在用训练数据进行网格搜索之后,我们通过调用best_score_属性获得性能最优模型的分数,通过调用best_params_属性访问该模型的参数。在该案例中,RBF核SVM模型以svc_C=100.0得到的最优k折交叉验证准确率为98.5%。最后,用独立的测试数据集评估所选最优模型的性能,并通过调用GridSearchCV对象的best_estimator_属性来实现:

clf = gs.best_estimator_
# clf.fit(X_train, y_train)
print('Test accuracy: %.3f' % clf.score(X_test, y_test))

请注意,在完成网格搜索之后,不需要手动设置 clf.fit(X_train,y_train),在训练数据集上以最佳设置(gs.best_estimator_)拟合模型。GridSearchCV 类有一个 refit 参数,如果我们设置 refit=True(默认值),它将自动在整个训练数据集上重新拟合 gs.best_estimator_。

  • 尽管网格搜索是寻找模型最优参数组合的有力手段,但是评估所有可能参数组合的计算成本也非常昂贵。随机搜索是scikit-learn的另外一种从不同参数组合中抽样的方法。随机搜索通常和网格搜索一样好,但是更具成本效益和时间效率。特别是,如果仅通过随机搜索对60个参数组合进行采样,那么我们已经有95%的概率来获得最优性能5%以内的解。

  • 使用scikit-learn中的RandomizedSearchCV类,我们可以根据指定的预算从样本分布中随机抽取不同的参数组合。更多细节和示例可以从下述网站找到:http://scikit-learn.org/stable/modules/grid_search.html#randomized-parameter-optimization

4.2 通过嵌套式交叉验证选择算法

正如在上一节中所看到的,结合网格搜索进行k折交叉验证,是通过改变超参数值来调优机器学习模型性能的有效方法。如果想要在不同的机器学习算法中进行选择,另一种推荐的方法是嵌套式交叉验证法。通过对误差估计的偏差所做的出色研究,Sudhir Varma和Richard Simon得出这样的结论:当使用嵌套式交叉验证时,估计的真实误差与测试数据集上得到的结果几乎没有差距

嵌套式交叉验证有一个k折交叉验证的外部循环,负责把数据拆分为训练块和测试块,而内部循环在训练块上使用k折交叉验证选择模型。模型选择后,用测试块来评估模型的性能。图6-7说明了仅有5个外部模块及2个内部模块的嵌套式交叉验证概念,可用于对计算性能要求高的大型数据集;这种特殊类型的嵌套式交叉验证也被称为5x2交叉验证。

图 6-7
 

在scikit-learn中,可以通过如下方式进行嵌套式交叉验证:

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('CV accuracy: %.3f +/- %.3f' % (np.mean(scores),
                                      np.std(scores)))

代码返回的交叉验证准确率平均值对模型超参数调优的预期值绘出了很好的估计,且使用该值优化过的模型能够预测未知数据。

例如,我们可以用嵌套式交叉验证方法来比较SVM模型和简单的决策树分类器;为简单起见,这里只调优其深度参数:

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('CV accuracy: %.3f +/- %.3f' % (np.mean(scores), 
                                      np.std(scores)))

正如我们所看到的,SVM模型(97.4%)的嵌套式交叉验证性能明显要优于决策树(93.4%)的,因此可以预期,SVM对来自某个特定数据集同一样本空间的新数据进行分类可能是更好的选择.

5 了解不同的性能评估指标

5.1 分析混淆矩阵

混淆矩阵用来展示算法性能指标的矩阵

混淆矩阵是一个简单的方阵,用于展示一个分类器预测的结果——真正(TP)、真负(TN)、假正(FP)和假负(FN)——的数量,如图 6-8 所示。

图 6-8
 

虽然可以很容易通过对真实标签和预测标签的比较来手工计算这些指标,但是scikit-learn提供了一个方便使用的confusion_matrix函数,示例如下:

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)

执行代码所返回的数组提供了分类器在测试数据集上出现的不同类型的错误信息。可以调用matshow把这些信息表示为图6-8所示的混淆矩阵形式:

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')

plt.xlabel('Predicted label')
plt.ylabel('True label')

plt.tight_layout()
#plt.savefig('images/06_09.png', dpi=300)
plt.show()

假设类1(恶性)是该例子中的正类,模型正确地把71个样本分类到类0(TN),把40个样分类到类1(TP)。然而,模型也错误地把属于类1的两个样本分到类0(FN),并把其中一个本为良性的肿瘤样本(FP)预测为恶性。在下一节将学习如何利用这些信息来计算各种误差指标.

5.2 优化分类模型的精度和召回率

预测的误差(error, ERR)和准确率(accuracy, ACC)提供了误分类样本数量的相关信息。可以把误差理解为所有错误预测之和除以预测总数,而准确率的计算方法为正确预测之和除以预测总数:

可以直接根据误差计算预测准确率如下:

真正率(TPR)和假正率(FPR)是对非平衡分类问题特别有效的性能
指标:

例如,在肿瘤诊断中,我们更关心恶性肿瘤的检测,以便帮助病人进行适当的治疗。然而,减少恶性肿瘤(FP)的误诊率对减少病人的不必要担忧也很重要。与FPR相反,TPR提供关于部分正(或相关)的样本被从正池(P)中正确地识别出来的有用信息。

精度(PRE)和召回率(REC)是与真正率和真负率相关的性能指标,事实上,REC是TPR的同义词:

回顾恶性肿瘤检测的示例,优化召回率有助于最大限度地减少漏测恶性肿瘤的机会。然而,这是以误测健康患者(大量FP)患恶性肿瘤作为代价。相反,如果我们优化精度,就会强调所预测患者患有恶性肿瘤的正确性。但是这将以更大机会漏测恶性肿瘤作为代价(大量FN)。在实践中,通常采用PRE和REC的组合,即所谓的F1分数:

from sklearn.metrics import precision_score, recall_score, f1_score

print('Precision: %.3f' % precision_score(y_true=y_test, y_pred=y_pred))
print('Recall: %.3f' % recall_score(y_true=y_test, y_pred=y_pred))
print('F1: %.3f' % f1_score(y_true=y_test, y_pred=y_pred))

此外,可以在GridSearchCV通过评分参数使用与准确率不同的各种评分指标。可以在下面的网页链接中找到评分参数可接受的不同值的完整列表:http://scikit-learn.org/stable/modules/model_evaluation.html

请记住在scikit-learn中,正类的标签为类1。如果想指定一个不同的正标签,那就需要通过调用make_scorer函数构建自己的评分器,然后将其作为参数直接提供给GridSearchCV的scoring参数(在这个例子中,使用f1_score作为指标):

from sklearn.metrics import make_scorer

scorer = make_scorer(f1_score, pos_label=0)

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']}]

gs = GridSearchCV(estimator=pipe_svc,
                  param_grid=param_grid,
                  scoring=scorer,
                  cv=10,
                  n_jobs=-1)
gs = gs.fit(X_train, y_train)
print(gs.best_score_)
print(gs.best_params_)

5.3 绘制ROC曲线

ROC(Receiver Operating Characteristic,受试者工作特征)曲线是选择分类模型的有用工具,它以FPR和TPR的性能比较结果为依据,通过移动分类器的阈值完成计算。ROC的对角线可以解释为随机猜测,如果分类器性能曲线在对角线以下,那么其性能就比随机猜测还要差。TPR为1且FPR为0的完美分类器会落在图的左上角。基于ROC曲线,可以计算所谓的ROC曲线下面积(Area Under the Curve,AUC)以描述分类模型的性能。

与ROC曲线相似,可以计算分类器在不同概率阈值下的精度与召回率曲线。scikit-learn也实现了绘制精度与召回率曲线的功能,详细文档可以参考下述网页:http://scikit-learn.org/stable/modules/generated/sklearn.metrics.precision_recall_curve.html

执行下面的代码示例将绘制分类器ROC曲线,该分类器只用威斯康星乳腺癌数据集的两个特征来预测肿瘤为良性或恶性。虽然将复用先前定义的逻辑回归流水线,但是完成这个分类任务的分类器更具挑战性,从而使所绘制的ROC曲线在视觉上变得更有趣。出于类似的考虑,也把StratifiedKFold验证器的分块数量减少为三个。具体代码如下:

from sklearn.metrics import roc_curve, auc
from scipy import interp

pipe_lr = make_pipeline(StandardScaler(),
                        PCA(n_components=2),
                        LogisticRegression(penalty='l2', 
                                           random_state=1, 
                                           C=100.0))

X_train2 = X_train[:, [4, 14]]
    

cv = list(StratifiedKFold(n_splits=3, 
                          random_state=1).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='ROC fold %d (area = %0.2f)'
                   % (i+1, roc_auc))

plt.plot([0, 1],
         [0, 1],
         linestyle='--',
         color=(0.6, 0.6, 0.6),
         label='random guessing')

mean_tpr /= len(cv)
mean_tpr[-1] = 1.0
mean_auc = auc(mean_fpr, mean_tpr)
plt.plot(mean_fpr, mean_tpr, 'k--',
         label='mean ROC (area = %0.2f)' % mean_auc, lw=2)
plt.plot([0, 0, 1],
         [0, 1, 1],
         linestyle=':',
         color='black',
         label='perfect performance')

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.tight_layout()
# plt.savefig('images/06_10.png', dpi=300)
plt.show()

前面的代码示例采用已经很熟悉的scikit-learn的StratifiedKFold类,在pipe_lr流水线上调用sklearn.metrics模块的roc_curve函数,每次迭代分别计算逻辑回归分类器的ROC性能。此外,调用来自SciPy的interp函数,把三个块的平均ROC曲线插入图中,并且调用auc函数计算曲线下的面积。ROC曲线的结果表明不同的块之间存在着一定的方差,平均ROC AUC(0.76)介于理想分数(1.0)和随机猜测(0.5)之间,如图6-10所示

图 6-10
 

以 ROC AUC 来报告分类器的性能,可以对不平衡样本分布下分类器的性能产生更加深刻的认识。然而,准确率分数可以解释为ROC曲线上的一个截点,A. P. Bradley发现ROC AUC和准确率指标在大多数时间是相互一致的

5.4 多元分类评分指标

本节所讨论的评分指标是针对二元分类系统的。然而,scikit-learn 也实现了宏观和微观的平均方法,以把二元分类的评分指标通过一对所有(OvA)扩展到解决多元分类问题。微平均是根据各个系统的TP、TN、FP和FN计算出来的。例如,k类系统的精度分数的微平均可以计算如下:

宏平均值只是计算不同系统的平均分数:

如果每个实例或预测的权重相同,那么微平均是有用的,而宏平均则同样地给予所有类相同的权重,以评估分类器在最频繁分类标签上的整体性能。

如果用scikit-learn的二元分类模型的性能指标来评价多元分类模型的性能,默认使用归一化或者宏平均的加权变体。加权的宏平均是在计算平均值时,以真实实例的数量作为每个分类标签评分的权重计算而来的。如果处理的类不均衡,即每个标签的样本数量不同,那么加权宏平均值更有用。

对多元分类问题,scikit-learn默认支持加权宏平均值,可以调用sklearn.metrics模块的不同评分函数通过参数average来指定平均的
方法,例如precision_score或make_scorer函数:

pre_scorer = make_scorer(score_func=precision_score, 
                         pos_label=1, 
                         greater_is_better=True, 
                         average='micro')

5.5 处理类不均衡问题

本章多次提到了类的不均衡问题,但还没有讨论过如何适当地处理。在现实世界中,类的不均衡是个常见问题,即当数据集的一个或多个类的样本被过度代表的时候。我们可以想到可能出现该问题的几个场景,如垃圾邮件过滤、欺诈检测或疾病筛查。

想象一下本章用过的包括90%健康病人的乳腺癌威斯康星数据集。在这种情况下,可以在无监督机器学习算法的帮助下,通过预测所有样本的多数类(良性肿瘤),在测试数据集上达到90%的准确率。因此,在这样的数据集上训练一个模型以达到大约90%的测试准确率,将意味着模型还没有从这个数据集所提供的特征中学到任何有用的东西。

本节将简要地介绍一些有助于处理数据集中类不均衡的技术。但是,在讨论处理这个问题的不同方法之前,我们先用乳腺癌数据集创建一个不均衡的数据集,该数据集最初包括357个良性肿瘤(类0)和212个恶性肿瘤(类1)样本:

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%的预测准确率:

y_pred = np.zeros(y_imb.shape[0])
np.mean(y_pred == y_imb) * 100

因此,在这样的数据集上拟合分类器,当比较不同模型的精度、召回率、ROC曲线时,无论在应用中最关心什么,都要将注意力集中在准确率以外的其他指标上。例如,如果优先级是找出大多数恶性肿瘤患者,然后推荐他们做额外的筛查,那么召回率应该是要选择的指标。在垃圾邮件过滤中,如果系统并不是很确定的话,并不想把邮件标记为垃圾邮件,那么精度就可能是更合适的度量指标。

除了评估机器学习模型外,类的不均衡会影响到模型拟合过程中的学习算法。由于机器学习算法通常优化奖励或代价函数,这些函数计算在训练过程中所看到的训练样本的总和,决策规则很可能偏向于多数类。

换句话说,该算法隐式学习构建模型,模型基于数据集中最丰富的类来优化预测,以便在训练中最小化代价或最大化奖励。

在模型拟合过程中,处理不均衡类比例的一种方法是对少数类的错误预测给予更大的惩罚。在scikit-learn中,只要把参数class_weight设置成class_weight=‘balanced’,就可以很方便地加大这种惩罚的力度,大多数的分类器都是这么实现的。

处理类不均衡问题的其他常用策略包括对少数类上采样,对多数类下采样以及生成合成训练样本。不幸的是,没有万能的最优解决方案,没有对所有问题都最有效的技术。因此,我们建议在实践中对给定问题尝试不同的策略,通过评估结果选择最合适的技术。

scikit-learn库实现了简单的resample函数,可以通过从数据集中有放回地提取新样本来帮助少数类上采样。下面的代码将从不均衡的乳腺癌数据集中提取少数类(这里,类1),并反复从中提取新样本,直到包含与分类标签0的样本数量相同为止:

from sklearn.utils import resample

print('Number of class 1 samples before:', X_imb[y_imb == 1].shape[0])

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 samples after:', X_upsampled.shape[0])

重采样可以把原来的类0样本与上采样的类1样本叠加以获得均衡的数据
集:

X_bal = np.vstack((X[y == 0], X_upsampled))
y_bal = np.hstack((y[y == 0], y_upsampled))

因此,多数票预测规则只能达到50%的准确率:

y_pred = np.zeros(y_bal.shape[0])
np.mean(y_pred == y_bal) * 100
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值