其它章节内容请见机器学习之PyTorch和Scikit-Learn
查看不同性能评估指标
在前面的章节中,我们使用预测准确率来评估各机器学习模型,通常这是用于量化模型表现很有用的指标。但还有其他几个性能指标可以用于衡量模型的相关性,例如精确率、召回率、F1分数和马修斯相关系数(MCC)等。
读取混淆矩阵
在我们深入讨论各评分指标之前,先看一下混淆矩阵,这是一种展示学习算法性能的矩阵。
混淆矩阵是一个简单的方阵,报告分类器真正(TP)、真负(TN)、假正(FP)和假负(FN)的预测次数,如图6.9所示:
图6.9:混淆矩阵
虽然可以通过手动比较实际和预测类标签来轻松计算这些指标,但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)
[[71 1]
[ 2 40]]
执行代码后返回的数组提供了有关分类器在测试数据集上产生的不同类型错误的信息。我们可以使用Matplotlib的matshow
函数将这一信息映射到图6.9中的混淆矩阵图中:
>>> 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
模块中导入,如以下代码片段所示:
>>> 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
作为评价指标):
>>> 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
验证器中的折叠数减少到三。代码如下所示:
>>> 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
函数:
>>> pre_scorer = make_scorer(score_func=precision_score,
... pos_label=1,
... greater_is_better=True,
... average='micro')
处理类不平衡
本章中,我们多次提到了类不平衡的问题,但实际上还没有讨论如果恰当地处理这种情况。类不平衡是在处理现实世界数据时很常见的问题,即数据集中一个类别或多个类别的样本过多。可以设象几个可能会发生这种情况的领域,例如垃圾邮件过滤、欺诈检测或疾病筛查。
假设我们在本章中使用的威斯康星乳腺癌数据集中有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
89.92443324937027
因此,在对这样的数据集拟合分类器时,与其比较不同模型的准确率,我们更应该关注其他指标,如精确度、召回率、ROC曲线等,具体取决于我们在应用中最关心的内容。例如,我们的优先级可能是识别出大多数患有恶性癌症的患者以便推荐额外的筛查,那么召回率应该是我们选择的指标。在垃圾邮件过滤中,如果系统不确定我们便不希望将邮件标记为垃圾邮件,那么精确率可能是更合适的指标。
除了评估机器学习模型之外,类不平衡还会影响模型拟合过程中的学习算法。由于机器学习算法通常优化拟合过程中计算为训练样本加和的奖励或损失函数,决策规则很可能会偏向于多数类别。
换句话说,算法会隐式地学习一个模型,优化基于数据集中最常见类别的预测,以在训练中最小化损失或最大化奖励。
在模型拟合过程中处理类不平衡占比的一种方法是对少数类的错误预测赋予更大的惩罚。通过scikit-learn,调整这种惩罚非常方便,只需将class_weight
参数设置为class_weight='balanced'
,大多数分类器都实现了这个功能。
处理类不平衡的其他常用策略包括增加少数类别的样本、减少多数类别的样本以及生成合成训练样本。可惜并没有一种在不同问题领域中都表现最佳的普适解决方案或技术。因此,在实践中,建议尝试不同的策略,评估结果,并选择最合适的技术。
scikit-learn库实现了一个简单的resample
函数,可以通过对数据集有放回地抽取新样本来帮助增加少数类别的样本。以下代码将从我们的不均衡的威斯康星州乳腺癌数据集中获取少数类(这里是类1
),并反复从中抽取新样本,直到它包含与类标签0
相同数量的样本为止:
>>> 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
子集合并起来,获得一均衡的数据集,代码如下所示:
>>> 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
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,我们进一步对模型进行了调优。然后,我们使用混淆矩阵和各种性能指标来评估和优化模型在特定问题任务上的性能。最后,我们通过讨论处理不平衡数据的各种方法来结束本章,这是许多实际应用中的常见问题。现在,读者应该具备了成本构建分类监督机器学习模型的基础技术。
下一章中,我们将学习集成方法:这些方法允许我们将多个模型和分类算法组合起来,以进一步提升机器学习系统的预测性能。