机器学习算法笔记(二十八):精准率的召回率的平衡、精准—召回率曲线与ROC曲线

上一篇文章我们提到在某些场景中我们更加看重精准率,某些场景我们更加看重召回率,而某些场景需要同时考虑这两者,希望这两者越大越好。其实同时追求精准率和召回率是不现实的,精准率和召回率是互相矛盾的,这时我们就要在两者间取得平衡。

一、精准率的召回率的平衡

为了理解精准率的召回率的平衡,我们先来回顾一下逻辑回归算法:

注意到决策边界=0,我们其实不一定要让决策边界在=0 的位置,完全可以使得( threshold 为常数,也就是阈值)为决策边界,score > threshold 时分类为 1,score < threshold 时分类为 0。通过指定 threshold 可以平移决策边界对应的直线,从而影响分类结果。

假设计算结果大于 threshold 时分类结果为 ★;计算结果小于 threshold 时分类结果为 ●,我们就可以像下图一样设置三条 threshold:

我们可以看到不同的阈值对分类结果、精准率、召回率都有着很大的影响

从上图我们不难发现,精准率和召回率是相互牵制,互相矛盾的两个变量,不能同时增高。阈值增大,精准率提高,召回率随之降低;阈值减小,精准率降低,召回率随之提高。

这时我们相当于给算法加入了一个新的超参数 threshold,我们需要通过调参来找出最佳的阈值。

sklearn 的 LogisticRegression() 类中的 predict() 方法中,默认阈值 threshold 为 0,再根据 decision_function() 方法计算的待预测样本的 score 值进行对比分类:score < 0 分类结果为 0,score > 0 分类结果为 1。我们下面就来编程实现对这个阈值的调整,紧接着上一篇文章工程代码的最后,实现如下代码:

#精准率和召回率的平衡
decision_scores = log_reg.decision_function(X_test)
y_predict_2 = np.array(decision_scores >= 5, dtype='int') #改变 threshold 为 score>=5
print(confusion_matrix(y_test, y_predict_2))
"""
print: 
[[404   1]
 [ 21  24]]
"""
print(precision_score(y_test, y_predict_2)) #prints: 0.96
print(recall_score(y_test, y_predict_2)) #prints: 0.5333333333333333

y_predict_3 = np.array(decision_scores >= -5, dtype='int') #改变 threshold 为 score>=-5
print(confusion_matrix(y_test, y_predict_3))
"""
print: 
[[390  15]
 [  5  40]]
"""
print(precision_score(y_test, y_predict_3)) #prints: 0.7272727272727273
print(recall_score(y_test, y_predict_3)) #prints: 0.8888888888888888

在上面的代码中,我们通过 LogisticRegression() 模块下的 decision_function() 方法得到预测的 score。不使用 predict() 方法,而是重新设定阈值,通过向量转化,直接根据预测得分进行样本分类。

二、精准—召回率曲线绘制

通过上文我们知道,随着阈值 threshold 的变化,精准率和召回率跟着相应变化。这时我们就可以绘制出精准率—召回率曲线来找到精准率与召回率平衡位置的点。紧接着上面的代码,实现如下代码:

from sklearn.metrics import precision_score
from sklearn.metrics import recall_score

precisions = [] #记录精准率
recalls = [] #记录召回率
thresholds = np.arange(np.min(decision_scores), np.max(decision_scores), 0.1) #分割不同的threshold
for threshold in thresholds:
    y_predict = np.array(decision_scores >= threshold, dtype='int')
    precisions.append(precision_score(y_test, y_predict))
    recalls.append(recall_score(y_test, y_predict))

import matplotlib.pyplot as plt
plt.plot(thresholds, precisions) #绘制thresholds—精确率曲线
plt.plot(thresholds, recalls) #绘制thresholds—召回率曲线
plt.show()

plt.plot(precisions, recalls) #绘制Precision-Recall曲线
plt.show()

绘制的thresholds—精确率—召回率和 Precision-Recall 曲线如下:

途中 Precision-Recall曲线开始急剧下降的点,可能就是精准率和召回率平衡位置的点。

当然,sklearn也为我们封装了绘制这些曲线的绘制方法,实现如下:

from sklearn.metrics import precision_recall_curve
precisions, recalls, thresholds = precision_recall_curve(y_test, decision_scores)

plt.plot(thresholds, precisions[:-1])
plt.plot(thresholds, recalls[:-1])
plt.show()

plt.plot(precisions, recalls)
plt.show()

绘制结果应与自己实现的大致相同。

从上面绘制的 Precision-Recall(P-R) 曲线我们不难发现,曲线包围坐标轴的形状大致是一个1/4圆:

如上图所示,我们用两组超参数来训练一个模型,绘制出了两条P-R曲线。显然外面这条曲线对应的模型是优于里面这条曲线对应的模型的,因为外面的曲线上每一点 Precision 对应的 Recall 都比里面的 Precision 对应的 Recall 值要大。所以若一个模型的 P-R 曲线更靠外(与坐标轴包围面积更大)的话,通常这个模型就更加好,所以 P-R 曲线还能作为挑选一个模型、算法、超参数的指标。

三、ROC 曲线

前面提到了 P-R 曲线,而本节我们要讨论的是与 P-R 曲线同样重要的 ROC 曲线。ROC 就是英文 Receiver Operation Characteristic Curve 的缩写,是统计学上经常使用的术语,描述的是 TPR(True Positive Rate) 与 FPR(False Positive Rate) 之间的关系。

FPR 与 TPR 的定义,其中 TPR 就是前面提到的召回率。

● TPR(True Positive Rate):真正率;被预测为正的正样本结果数 / 正样本实际数:TPR = TP /(TP + FN)。

● TNR(True Negative Rate):真负率;被预测为负的负样本结果数 / 负样本实际数:TNR = TN /(TN + FP) 。

● FPR(False Positive Rate):假正率;被预测为正的负样本结果数 /负样本实际数:FPR = FP /(TN + FP) 。

● FNR(False Negative Rate):假负率;被预测为负的正样本结果数 / 正样本实际数:FNR = FN /(TP + FN) 。

上面讨论 P-R 曲线时,我们用一张图来表示精准率和召回率之间的联系,在这里我们继续用这张图来描述 TPR 与 FPR 之间的关系:

我们可以发现,随着阈值 threshold 的降低,FPR 和 TPR 都逐渐升高(FPR 和 TPR 之间成正相关的变化趋势),和精准率、召回率之间的关系是正好相反的。其实这也很好理解:若我们要提高 TPR,就要把阈值拉低,拉低后犯 FP 错误的概率也会增高,所以 TPR 增高的同时 FPR 也会增高。所谓的 ROC 曲线就是刻画这两个指标之间的关系。

有了 TPR 和 FPR,我们就能绘制出 ROC 曲线了。我们在自己的MyML包的 metrics.py 文件中实现好之前提到的 TN、FP、FN、TP等函数(点击下载),新建一个工程,导入MyML包,创建一个main.py文件,实现如下代码:

import numpy as np
import matplotlib.pyplot as plt

from sklearn import datasets

digits = datasets.load_digits()
X = digits.data
y = digits.target.copy()

y[digits.target==9] = 1
y[digits.target!=9] = 0

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)

from sklearn.linear_model import LogisticRegression

log_reg = LogisticRegression()
log_reg.fit(X_train, y_train)
decision_scores = log_reg.decision_function(X_test)

from MyML.metrics import FPR, TPR

fprs = []
tprs = []
thresholds = np.arange(np.min(decision_scores), np.max(decision_scores), 0.1)
for threshold in thresholds:
    y_predict = np.array(decision_scores >= threshold, dtype='int')
    fprs.append(FPR(y_test, y_predict)) #将目前阈值对应的FPR存入数组中以便绘制
    tprs.append(TPR(y_test, y_predict)) #将目前阈值对应的TPR存入数组中以便绘制

plt.plot(fprs, tprs)
plt.show()

绘制的图像下:

sklearn中同样为我们封装好了绘制ROC的方法,实现如下:

from sklearn.metrics import roc_curve

fprs, tprs, thresholds = roc_curve(y_test, decision_scores)

plt.plot(fprs, tprs)
plt.show()

绘制结果应该与上图一致。

我们在实际应用中通常以 ROC 曲线与图形边界围成的面积,作为衡量模型优劣的标准,面积越大(FPR 在 0 附近的时候,相应的若 TPR 越高,底下的面积也会被抬得越大),模型越优。这里所说的模型可以是同样算法不同超参数所得的不同模型,也可以是不同算法所得的不同模型。

ROC 曲线包围的面积在[0, 1]之间取值。在这张示意图中,外面那条 ROC 曲线对应的模型较里面那条更好一些。

sklearn也同样为我们提供了求 ROC 曲线包围面积的方法,实现如下:

from sklearn.metrics import roc_auc_score #auc就是area under curve,也就是曲线下面的面积

print(roc_auc_score(y_test, decision_scores)) #prints: 0.98304526748971188

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注