7.评价分类结果

1.准确度的陷阱和混淆矩阵

我们之前对于分类问题,一直使用分类的准确度评价分类的结果,但是实际上分类问题的评价比回归问题的评价要复杂很多,相应的指标也多很多。可是之前使用准确度来进行评价不是挺好吗?但其实准确度是有一个很大的问题的,举个例子

我有一个癌症检测系统,通过对人进行体检,那么可以判断这个人是否患有癌症,而这个系统预测的准确度是99%,那么这个系统是好,还是坏呢?按照我们之前的逻辑,准确度都达到99%了,那么肯定是一个好系统了,其实不然。如果这个癌症的发病率只有百分之1呢,换句话说,1000个人里面只有10个癌症患者,健康的人远远高于患有癌症的人。那么不管什么人,系统只要都预测成健康,或者没有患有癌症,那么这个系统的准确度依然能达到99%,也就是说这个系统甚至都不需要机器学习,不管来什么人,只要都预测成健康就可以了,因为健康的人相对于患有癌症的人,比例显然是远远高于的。因此只要都预测成健康,那么准确度依旧会很高,但是这样的系统有用吗?我们的目的是,希望能够检测出来患有癌症的人,尽管这个系统的准确度达到了99%,但是癌症患者是不是一个都没有检测出来啊,所以这便是分类准确度的一个陷阱,或者说一个弊端,那么我们便有了其他的评价指标。所以我们说分类问题的评价指标相较于回归问题,要更复杂一些,毕竟回归问题只要判断误差的大小、或者r2的大小即可

或者我们的例子再举的极端一点,这个癌症的发病率只有百分之0.1,其实癌症的发病率在百分之0.1算是正常的了,百分之1个人觉得有点高了,但是不管了。如果癌症的发病率只有百分之0.1,那么只要系统都预测成健康,那么准确就达到了99.9%,但是这样的系统基本上没有什么价值,因为我们关注的是比较少的那一部分样本,但是这一部分样本,并没有预测出来。

我们之前对于分类问题,一直使用分类的准确度评价分类的结果,但是实际上分类问题的评价比回归问题的评价要复杂很多,相应的指标也多很多。可是之前使用准确度来进行评价不是挺好吗?但其实准确度是有一个很大的问题的,举个例子

这便是数据的极度偏斜(skewed data),对于极度偏斜的数据,只使用分类的准确度是远远不够的。因此我们要引入其他的指标,来判断分类的好坏。

在这里我们先引入一下混淆矩阵。

我们这里的0表示阴性,1表示阳性。在医院里面检测,如果是阴性说明是正常的,但如果是阳性,说明得病了。我们的行代表的是真实值,列代表的是预测值。

按照行列的顺序

  • 如果是[0,0],说明预测negative正确,简称为TN,T表示True,我们预测正确了,预测的结果是negative,说明真实值是negative;
  • 如果是[0,1],那么是FP,F表示False,表示我们预测错了,预测的结果是positive,说明真实值是negative;
  • 如果是[1,0],那么是FN,表示我们预测错了,预测结果是negative,说明真实值是positive;
  • [1,1]的话,那么是TP,表示我们预测对了,预测成了positive,说明真实值也是positive

假设有10000个人,我们的预测结果是这样的。0是阴性,表示健康;1是阳性,表示患病。那我们来描述一下TN,FP,FN,TP。

9978表示我们表示TN,表示有9978个人,我们预测对了,预测成了健康,说明原本就是健康的;12表示FP,表示我们预测错了,预测成了患病,意味着原本是没病的;2表示FN,表示我们预测错了,预测成了健康,意味着这两个人本来是患病的;8表示TP,表示我们预测对了,预测成了患病,说明原本就是患病的。

这便是混淆矩阵

2.精准率和召回率

我们介绍了混淆矩阵,混淆矩阵在分类任务中是一个非常重要的工具。我们通过混淆矩阵,可以得到更加好的来衡量分类算法的指标。我们下面介绍两个通过混淆矩阵得到的两个指标。

  • 精准率TP / (TP + FP) 

    为什么把这个叫做精准率呢?因为在这种有极度偏斜的样本中,我们关注的是那些比较少的样本?比如癌症患者,那么1代表的就是患有癌症的人;再比如说银行发放信用卡给客户是否有风险,1代表的就是有风险。我们关注是那些样本比较少的,通常把这些样本分类为1。还拿这里的癌症患者举例,精准率就是我们预测为1、并且预测对了的个数除以我们预测为1的个数的值,比如这里的8,表示我们预测对了8个,我们预测为1,并且真实值也为1,这里的12,是我们预测为1但是真实值却不是1。所以8 / (12 + 8),预测为1并且预测对了的个数(8个),除以预测为1的个数(8 + 12个),等于40%,所以精准率就是百分之40.

  • 召回率TP / (TP + FN)

    有了精准率,那么召回率也很好理解。毕竟两者只有分母不一样,精准率是TP / (TP + FP),而召回率是TP / (TP + FN)。TP为8,表示有8个真实值是1,我们也预测为1,FN为2,表示有两个明明也是1,但是我们却预测成了0。

所以:
精准率:真实值为异常、预测值也为异常 / (真实值为异常、预测值也为异常 + 真实值为正常、预测值却为异常。)
召回率:真实值为异常、预测值也为异常 / (真实值为异常、预测值也为异常 + 真实值为异常、预测值却为正常。)

所以想象成抓犯人
如果精准率小于1,说明我们错抓了,明明有不是犯人的,我们却当成了犯人
如果召回率小于1,说明我们漏网了,明明有是犯人的,我们却没有当成犯人

再拿我们之前的癌症患者预测这个例子,假设有10000个人,10个癌症患者,但是我们预测的结果都是健康,那么首先准确度肯定是99.9%,但是它的精准率和召回率又是多少呢? 

9990表示都是健康的,而我们也预测成了健康。但是10表示明明是癌症,我们还是预测成了健康。那么显然召回率是 0 / (10 + 0)=0,而精准率是0 / (0 + 0),尽管结果是0除以0,无意义,但是在统计这一层面上,这里显然还是0。所以尽管分类准确度达到了99.9%,但是它的精准率和召回率都是0。所以这样的系统尽管准确度达到了99.9%,但其实是没有意义的,对我们没有任何的帮助。在这种样本极度偏斜的情况下,我们不看分类准确度,而是看精准率和召回率,能更好的评价我们这个分类系统的好坏。

3.实现混淆矩阵、精准率和召回率

import numpy as np
from sklearn.datasets import load_digits
digits = load_digits()
X = digits.data
y = digits.target.copy()

# 我们需要那种极度偏斜的样本
# 于是我们将手写数字样本的10个特征,变成两个特征。
# 将特征为9的全部变成1,不为9的全部变成0
# 那么我们的关注点就是,对于原来特征为9的样本,能预测出来多少个
y[digits.target == 9] = 1
y[digits.target != 9] = 0
# 先使用逻辑回归进行训练
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)

logistic = LogisticRegression()
logistic.fit(X_train, y_train)
logistic.score(X_test, y_test)  # 0.9755555555555555

可以看到我们使用逻辑回归预测的准确率达到了0.97555,但由于我们的数据是极度偏斜的,因此如果我把所有的样本都预测为0,我们的准确率也能达到百分之90左右,因此我们需要再考察一下其他的性能指标

y_predict = logistic.predict(X_test)
# 计算精准率和召回率
def confusion_matrix(y_true, y_predict):
    # TN:正确预测为正值,说明y_predict和y_test都为0
    TN = np.sum((y_predict == 0) & (y_test == 0)) 
    # FP:错误预测为负值,说明y_predict为1,y_test为0
    FP = np.sum((y_predict == 1) & (y_test == 0)) 
    # FN:错误预测为正值,说明y_predict为0,y_test为1
    FN = np.sum((y_predict == 0) & (y_test == 1)) 
    # TP:正确预测为负值,说明y_predict为1,y_test为1
    TP = np.sum((y_predict == 1) & (y_test == 1)) 
    
    return np.array([
        [TN, FP],
        [FN, TP]
    ])


print(confusion_matrix(y_test, y_predict))
"""
[[403   2]
 [  9  36]]
"""
# 计算精准率和召回率
conf_mat = confusion_matrix(y_test, y_predict)
precision_score = conf_mat[1, 1] / np.sum(conf_mat[:, 1])  # TP / (TP + FP)
recall_score = conf_mat[1, 1] / np.sum(conf_mat[1, :])  # TP / (TP + FN)
print(precision_score)  # 0.9473684210526315
print(recall_score)  # 0.8

因此我们便实现了混淆矩阵,以及计算除了精准率和召回率。那么接下来还是老规矩,我们看看sklearn中是如何实现混淆矩阵、精准率以及召回率的。

from sklearn.metrics import confusion_matrix, precision_score, recall_score
print(confusion_matrix(y_test, y_predict))
"""
[[403   2]
 [  9  36]]
"""
print(precision_score(y_test, y_predict))  # 0.9473684210526315
print(recall_score(y_test, y_predict))  # 0.8

# 和我们自己手动实现混淆矩阵、精准率、召回率所得到的结果是一样的。

4.F1 Score

经过之前的学习,我们知道了对于样本极度偏斜的情况,使用精准率和召回率比使用准确度要好一些。

但是这样也有一个问题,那就是这是两个指标。既然是两个指标,就意味着肯定会有冲突,精准率高但召回率低、召回率高但精准率低,这样的话,我们该如何权衡呢?显然还是要业务场景有关。

比如我们进行股票预测,预测是升还是将。这显然是一个二分类问题,如果我们只关心股票升值的话,那么把股票升值预测为1、贬值预测为0,我们预测的就是精准率。因为我们关心的是预测为1的时候,有多少次是预测对的。如果是1,我们就投钱,是0,就不投钱。因此在这种情况下,如果精准率较低,就意味着FP较大,明明是贬值我们预测成了升值,就错误的把钱投进去了。但是召回率的话,对于我们不是很重要,因为召回率偏低意味着FN较大,说明股票明明是升值我们却预测成了贬值,但是对于我们来说,我们并没有投钱,即使召回率偏低的话,我们也不会亏损,尽管少了赚钱的机会。

如果业务场景是患病检测的话,那么我们就关心召回率了。如果召回率偏低的话,说明一些人是患病的,我们却预测成了没病,这是很严重的,会让病人的病情继续恶化下去。但是精准率偏低,意味着一些人没有病,我们却预测成了患病。但是呢?即便如此,再更进一步的检查,也可以检查出来是否真的患病,因此精准率偏低,只是意味着让一些人做了更进一步的检查罢了,这是没什么影响的。我们关注的是,要检测出患病的人,不能有漏网之鱼,这就意味着关注的召回率、而不是精准率。

因此要是场景而定,有些场景,精准率比召回率重要,有些场景,召回率比精准率重要。

然而有些场景,我们是要兼顾两者的,那么这时,就出现了新的指标,F1 Score

F1 Score的目的就是兼顾精准率和召回率

在这里提一个问题,如果让你设计F1 Score你会怎么做呢?一般情况下,会取精准率和召回率两者的平均值。没错,F1 Score也是类似的做法,只不过取的是两者的调和平均值

因此可以发现,当精准率或者召回率有一个为0的话,那么对应的F1 Score也为0。而且F1 Score的范围是0到1的,因为精准率和召回率都是一个小于1的数,那求倒数再相加然后除以二肯定大于1,说明1 / F1是大于1的,那么F1自然小于1。

可为什么要使用调和平均值呢?为什么不直接使用算数平均值呢?我们实际演示一下,就明白了。

def f1_score(precision, recall):
    return 2 * precision * recall / (precision + recall)


print(f1_score(0.5, 0.5))  # 0.5
print(f1_score(0.1, 0.9))  # 0.18000000000000002

可以看到如果使用算数平均值,那么这两者的结果应该是一样的。但是使用调和平均值的一大好处就是,只要有一个低,那么整体的结果依旧会很低。

我们使用上一节的例子来计算一下F1 Score

import numpy as np
from sklearn.datasets import load_digits
digits = load_digits()
X = digits.data
y = digits.target.copy()


y[digits.target == 9] = 1
y[digits.target != 9] = 0
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, precision_score, recall_score
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)

logistic = LogisticRegression()
logistic.fit(X_train, y_train)
y_predict = logistic.predict(X_test)

print("准确率:", precision_score(y_test, y_predict))  
print("召回率:", recall_score(y_test, y_predict))  
# f1 score和精准率以及召回率一样,只需要传入y_test和y_predict即可
print("F1 Score:", f1_score(y_test, y_predict))  
"""
准确率: 0.9473684210526315
召回率: 0.8
F1 Score: 0.8674698795180723
"""

5.精准率和召回率的平衡

我们之前说过,精准率和召回率。我们都希望这两个指标越高越好,但其实这两个指标有时是相互矛盾的,精准率提高那么召回率就会不可避免的降低,所以我们有了F1 Score,我们要找到的就是精准率和召回率之间的平衡。

还记得逻辑回归中的决策边界吗?我们可以类比一下

如果我们以中间的竖线作为决策边界,在右边的我们预测为1,左边的预测为0,我们关注的是五角星。那么我们预测为1的样本有五个,但是对的有4个,所以精准率就是4 / 5 = 0.8,而五角星总共有6个,所以召回率是4 / 6 = 0.67。如果我们以右边的竖线作为决策边界的话,那么显然精准率是百分之百,而召回率是 2 / 6 = 0.33。同理以左边的竖线作为决策边界的话,精准率就是6 / 8 = 0.75,因为我们预测8个为1,而真正为1的有6个;召回率则是6 / 6 = 1,因为尽管有两个错的,但是五角星我们都正确的识别出来了。

因此从这里我们可以看出,如果想要精准率提高的话,我们可以把阈值设置的高一些,对那些特别有把握的我们才分类为1,这也就导致了有些样本明明也为1,但是由于没有把握,我们分类为0,从而导致召回率降低。同理如果我们期望召回率高的话,我们可以把阈值设置的低一些,比如癌症患者,哪怕患病的概率只有百分之10,我们也归类为1,这样就很难有漏网之鱼了。然而这样做的代价就是有些不是癌症患者的,我们也分类为癌症患者,从而导致精准率降低。

  • 精准率:正确识别为1的样本数 / 总共识别的样本数
  • 召回率:正确识别为1的样本数 / 所有特征为1的样本数

程序演示一下

# 我们如何改变一下决策边界呢?
# predict默认是以0作为决策边界的
# 但是对于逻辑回归来说,有这么一个函数,从名字也能看出来,是决策函数
decision_score = logistic.decision_function(X_test)
print(np.max(decision_score), np.min(decision_score))  # 19.88956556899649 -85.68605733401593


# 我们不按照predict默认的以0作为决策边界,而是自定义把大于等于n的分类为1,小于n的分类为0
def func(n):
    y_predict2 = np.array(decision_score >= n, dtype=np.int)
    from sklearn.metrics import recall_score, precision_score, f1_score
    print("精准率", precision_score(y_test, y_predict2))  
    print("召回率", recall_score(y_test, y_predict2))
    print("f1_score", f1_score(y_test, y_predict2))
    
    
func(5)
"""
精准率 0.96
召回率 0.5333333333333333
f1_score 0.6857142857142858
"""

func(10)
"""
精准率 1.0
召回率 0.28888888888888886
f1_score 0.4482758620689655
"""

func(-50)
"""
精准率 0.1174934725848564
召回率 1.0
f1_score 0.2102803738317757
"""
# 可以看到阈值设置的高,精准率也高,召回率会低
# 阈值设置的低,精准率也低,但召回率会高

6.精准率和召回率曲线

我们来绘制一下,随着阈值的变化,精准率和召回率的变化曲线

import numpy as np
from sklearn.metrics import precision_score, recall_score
import matplotlib.pyplot as plt

precision_score_list = []
recall_score_list = []
def func(n):
    y_predict2 = np.array(decision_score >= n, dtype=np.int)
    precision_score_list.append(precision_score(y_test, y_predict2))
    recall_score_list.append(recall_score(y_test, y_predict2))
    

threshold = np.arange(np.min(decision_score), np.max(decision_score), 0.1)
for t in threshold:
    func(t)

plt.figure(figsize=(10, 8))
plt.plot(threshold, precision_score_list, color="pink", label="precision_score")
plt.plot(threshold, recall_score_list, color="green", label="recall_score")
plt.legend()
plt.grid()
plt.show()

从这个曲线,我们很明显能够看出,一开始阈值比较低,从而导致召回率高、精准率低,这是因为阈值低导致我们把太多的样本都分类为1了,不管是1或者不是1,我们都分成1了,从而精准率低,召回率高。但是随着阈值的增大,精准率提高,因为阈值变高导致分类为1的样本数变少了。而召回率没有变化,因为在阈值变化的一定范围内,我们还是把所有标签为1的样本全找出来了。

然鹅当阈值继续增大,召回率开始下降,这是因为阈值高了,说明分类为1的样本,都是非常有把握的。而有些样本标签虽然也为1,但是因为阈值或者说门槛提高了,通俗的说,就是变严格了,宁可错过一千,也不看错一个。比如股票,必须要有百分之90以上的把握预测会增长,如果没有90%以上的把握,就不买。说明有一些为1的,因为把握不够,我们漏掉了,从而导致召回率变低。而精准率显然是不断升高的,因为阈值越高,我们分类的就越有把握,阈值过高,可能最终我们就只选了10个样本,而这两个样本使我们能百分之百确定为1的,所以精准率为1。阈值再高,精准率同样是没有变化的,10个样本都为1,那么阈值提高,也只是从这10个样本中再去掉把握相对比较低的。比如其中5个样本我们99%确信是1,但是剩余5个我们只有95%的可信度。显然阈值提高就把具有95%可信度的样本去掉了,但是精准率依旧是1,然而召回率就比较可怜了。

我们可以从图像上观察,找到一个相对满意的位置,而且sklearn也为我们提供了方法,我们来看看

from sklearn.metrics import precision_recall_curve
# 只需要传入,至于要传入y_test, 和decision_function(X_test)即可
# 返回三个元素,看名字也知道分别是什么了
precision_score_list, recall_score_list, threshold = precision_recall_curve(y_test, decision_score)

# 但是注意的是,threshold的元素个数是比precision_score_list,recall_score_list要少1个的
# 这是因为,sklearn把threshold的最大值给去掉了,因为threshold最大值对应的精准率为1,召回率为0
plt.figure(figsize=(10, 8))
plt.plot(threshold, precision_score_list[: -1], color="pink", label="precision_score")
plt.plot(threshold, recall_score_list[: -1], color="green", label="recall_score")
plt.legend()
plt.grid()
plt.show()

可以看到图像稍微有些变化,这是因为sklearn选择了它认为比较重要的部分。但是我们仍然可以看出来,这两者是相互制约的。

同理我们还可以将精准率作为x轴,召回率作为y轴,绘制图像。

plt.figure(figsize=(10, 8))
plt.plot(precision_score_list, recall_score_list, color="pink")
plt.grid()
plt.show()

从图像上我们可以找出一个平衡的位置,通常这样的曲线都会有一个急剧下降的部分,显然就是精准率接近1的时候。

我们还可以使用精准率和召回率曲线(PR曲线)来衡量模型的好坏,因为PR曲线总体呈递减的趋势,我们不妨把图像画的平滑一些。

可以看到,图像越靠外,那么对应的模型就越好,因为这样对应的精准率和召回率就越高。或者我们说图像与两个轴围成的面积越大,那么模型效果越好。这样做是可以的,但虽然如此,可大多数情况下我们不用PR曲线的面积来衡量模型的优劣,而是使用另外一种曲线与XY轴围成的面积来衡量模型的优劣,也就是非常著名的ROC曲线。

7.ROC曲线

ROC:Receiver Operation Characteristic Curve,是一个统计学上经常使用的术语,它描述的是TPR和FPR之间的关系。

  • TPR:TP / (TP + FN),就是我们预测为1并且预测对了的数量,除以所有标签确实为1的样本数量,所以大家可能发现了,这个TPR其实就是recall
  • FPR:FP / (TN + FP),就是我们预测为1但是预测错了的数量,除以所有标签为0的样本数量,这个FPR和TPR正好是相反的

正如精准率和召回率一样,TPR和FPR之间也有紧密的关系

当直线在中间的时候,我们预测为1预测对了4个,预测为1预测错了1个,而标签为1的有6个,为0的也有6个,所以TPR = 4 / 6,FPR = 1 / 6;当直线在右边的时候,预测为1预测对了两个,预测为1预测错了0个,所以TPR = 2 / 6,FPR = 0 / 6;当直线在左边的时候,预测为1预测对了6个,预测为1预测错了2个,所以TPR = 6 / 6,FPR = 2 / 6

很明显样本总数是固定的,对于TPR和FPR影响的因素只有分母,当然threshold较小的时候,预测为1的个数就会变多,TPR比较大,但同时"误杀"的情况也会更严重,不是1的我们也分类为1了,threshold越小,错误分类为1的可能就越大,所以FPR也会比较大。但随着threshold增大的时候,门槛高了,有些是1的,我们分类为0了,所以TPR会减少,同时误杀的情况也变小了,毕竟特征是1的,都看走眼、分类为0了,更何况特征本来就是0的样本。所以threshold增大,TPR和FPR都会减小。

from sklearn.metrics import roc_curve
fprs, tprs, threshold = roc_curve(y_test, decision_score)
plt.plot(fprs, tprs)
plt.show()

可以发现,面积的范围是不超过1的,这是因为TPR和FPR都不超过1。那么我们如何求面积呢?sklearn也为我们提供了相关的接口

from sklearn.metrics import roc_auc_score
# 参数和roc_curve是一致的
area = roc_auc_score(y_test, decision_score)
print(area)  # 0.9830452674897119

结果还是蛮大的,可以看出来,对于那些有偏斜的情况不是那么的敏感。我们以ROC围成的面积的大小作为模型好坏的评判标准。

8.多分类问题中的混淆矩阵

import numpy as np
from sklearn.datasets import load_digits
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_score, confusion_matrix
digits = load_digits()
X = digits.data
y = digits.target

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

logistic = LogisticRegression()
logistic.fit(X_train, y_train)
y_predict = logistic.predict(X_test)
# 对于精准率、召回率、f1 score,sklearn默认只能解决2分类
# 如果是多分类,需要加上average="micro"参数
print(precision_score(y_test, y_predict, average="micro"))  # 0.9555555555555556

但是混淆矩阵,则是天然的支持多分类问题

print(confusion_matrix(y_test, y_predict))
"""
[[45  0  0  0  0  1  0  0  0  0]
 [ 0 37  0  0  0  0  0  0  3  0]
 [ 0  0 49  1  0  0  0  0  0  0]
 [ 0  0  0 49  0  1  0  0  3  0]
 [ 0  1  0  0 47  0  0  0  0  0]
 [ 0  0  0  1  0 36  0  0  1  0]
 [ 0  0  0  0  0  1 38  0  0  0]
 [ 0  0  0  0  0  0  0 42  0  1]
 [ 0  2  0  0  0  0  0  0 46  0]
 [ 0  1  0  1  1  1  0  0  0 41]]
"""

我们可以将矩阵转化为图像,在matplotlib中专门可以将矩阵转化为图像

cfm = confusion_matrix(y_test, y_predict)
# 调用matshow
plt.matshow(cfm, cmap=plt.cm.gray)
plt.show()

越亮的地方,说明值越大。但是对角线的地方是我们预测对了的情况,我们的关注点不在预测对了的上面。我们关注的是犯错误的地方。

cfm = confusion_matrix(y_test, y_predict)
# 对每一行求和,计算每一个格子所占的百分比
row_sums = np.sum(cfm, axis=1)
err_percent = cfm / row_sums
# 我们关注的不是预测对了的,而是错了的,所以可以把对角线的部分全部变为0
np.fill_diagonal(err_percent, 0)
# 调用matshow
plt.matshow(err_percent, cmap=plt.cm.gray)
plt.show()

我们可以看到越亮的地方,说明我们犯的错误就越大。可以针对犯错误比较大的地方进行微调。

猜你喜欢

转载自www.cnblogs.com/traditional/p/11517919.html