简介

在机器学习建模中,我们常常会用准确率,MSE等指标来衡量模型的优劣,但是在复杂的实际需求上,仅仅依靠这些指标是不够的,最典型的就是不平衡学习问题。这种问题往往是二分类问题,通常标签分布极不平衡,并且我们会重点关注其中一个类别的预测表现。例如,银行贷款问题,通常数据都是通过贷款的占大多数,并且我们重点关注未通过贷款的预测准确率,毕竟拒绝一个会准时还款的人对银行影响不大,接受一个不会准时还款的人,那对银行可就是实打实的损失了。

对于这类问题,我们可以使用一系列的指标来对模型进行评估,如混淆矩阵,F1,P-R曲线,ROC曲线,AUC等,听着好像很多很复杂,但其实这些指标本质都是相通的,只要理解了混淆矩阵,剩下的就水到渠成了。至于标题为什么加上了ROC曲线呢?因为这是最常用于衡量模型一种工具,我觉得值得放在标题里面。

混淆矩阵

在介绍混淆矩阵之前,我们首先需要做一点铺垫。在一个数据集被模型预测之后,其可以被分为四个集合,分别为

TP(True Positives):正样本,且被预测为正,也就是被正确预测了

FP(False Positives):负样本,但被预测为正,也就是被错误预测了

TN(True Negatives):负样本,但被预测为负,也就是被正确预测了

FN(False Negatives):正样本,但被预测为负,也就是被错误预测了

这样看着可能有点乱,我们将其总结为下图

image-20240403151220801

将上述表格写成一个2×22 \times 2的矩阵就是混淆矩阵,一般在sklearn中我们绘制出的混淆矩阵图长这样

image-20240403152059597

精确率与召回率

下面用TP, FP, TN, FN分别代表其对应集合中的样本数量,我们可以计算出各种指标。

精确率

也叫查准率,定义为

P=TPTP+FPP = \frac{TP}{TP + FP}

其代表被划分到正类的样本中,划分正确的样本占比,例如上述的银行贷款场景,我们就可以要求精确率尽可能的高。

召回率

也叫查全率,定义为

R=TPTP+FNR = \frac{TP}{TP + FN}

其代表所有正类样本中,被划分到正类的样本占比,例如作物初筛,我们不希望浪费达到标准的作物,并且后续处理过程中还会有其他筛选工序可以继续筛选劣质的作物,那我们就可以要求召回率尽可能的高。

精确率与召回率的关系

一般来说,精确率与召回率是相互约束的。大多数模型用于分类时,其输出会是该样本被分到一个类别的概率,然后再根据我们所设定的阈值被划分到不同的类别。虽然在二分类问题中,这个阈值一般会被设置为0.5,但是在不平衡学习问题中,可以根据我们的需求调整这个阈值。

例如还是上述的银行贷款问题,记通过的为1,不通过的为0,模型输出为样本被分为1的概率,我们就可以将阈值上调,如上调至0.7。这时精确率与召回率会发生什么变化呢?首先可以确定的是,召回率一定会变小,因为其分母TP+FNTP + FN是一个定值,就是所有真值为1的样本嘛,其此调高阈值必然会导致TPTP,也就是被划分到正类的样本数下降,因为被划分的正类的门槛更高了。而精确率在一般情况下则会上升,因为高门槛也会过滤掉更多被错误分类至正类的样本,可以结合下图理解一下。

image-20240403165523013

因此,如果同时拥有高精确率与高召回率的模型,则其性能是非常优秀的,也就是说这个模型可以把样本分的很开,正类样本都很接近1,负类样本都很接近0。我们可以用一个统一的指标来衡量模型的精确率与召回率,这就是F1值,其为精确率与召回率的调和平均,定义为

1F1=12(1P+1R)F1=2PRP+R\frac1{F1}=\frac12 \left(\frac1P+\frac1R\right) \quad \Rightarrow \quad F1 = \frac{2PR}{P + R}

当然,当我们对精确率与召回率的重视程度不一样时,我们可以通过权重来控制,定义为

1Fβ=11+β2(1P+β2R)Fβ=(1+β2)PRβ2P+R\frac{1}{F_{\beta}}=\frac{1}{1+\beta^{2}} \left(\frac{1}{P}+\frac{\beta^{2}}{R}\right) \quad \Rightarrow \quad F_{\beta} = \frac{(1+\beta^2)PR}{\beta^2P + R}

β>1\beta > 1时,召回率RR有更大的影响,反之,精确率PP有更大影响。

P-R曲线

在模型已经完成训练后,我们也可以通过精确率与召回率来选择合适的阈值,具体操作为,将所有样本的概率值从小到大排序,然后从0开始,将每两个相邻样本的概率值均值依次作为阈值,并计算一次精确率与召回率,最后以PPyy轴,RRxx轴,绘制散点图并连接成曲线,这也就是P-R曲线,可以结合下图理解一下

image-20240403202407963

注:其实阈值的增大并不用那么严格,就算是从0开始,每次加0.1加到1为止都是可以的,画出来的学习曲线没什么区别

最终画出来的P-R曲线大概长这样

image-20240404101444021

通过P-R曲线我们可以根据自己的实际需求,选择合适的阈值。除此之外,我们也常常用P-R曲线包住面积的大小来衡量模型的优劣,而在模型的P-R曲线有交叉时,除了根据我们的需求选择模型,还可以通过平衡点处的取值来衡量模型的优劣。平衡点就是指P与R取值相同时,模型的P或R的取值(反正是一样的嘛),这也被称为BEP,当然实际使用中还是F1用的多一点。

ROC曲线

TPR与FPR

在介绍ROC曲线之前,我们要先来看看TPR(True Positive Rate)与FPR(False Positive Rate),这也是由TP, FP, TN, FN计算出的指标,其具体定义为

TPR=TPTP+FNFPR=FPFP+TNTPR = \frac{TP}{TP + FN} \quad FPR = \frac{FP}{FP + TN}

从定义我们可以看出,TPR代表真值为正的样本被正确分类的概率,其定义与召回率是一样的。FPR代表真值为负的样本被错误分类的概率。

ROC曲线

与P-R曲线相同,我们可以通过调整阈值的方式,得到多组TPR与FPR以得到ROC曲线,示例图如下

image-20240403204529771

我们当然希望TPR尽可能高,FPR尽可能低,那么自然就是ROC曲线越靠近左上角则代表模型的效果越好,因此也经常使用曲线盖住的面积来衡量模型的效果,这被称为AUC值,也就是图中图例的部分。另外,图中的对角线代表随机猜测下的结果,ROC曲线离这条线越远越好。

注:ROC曲线靠近左上角最好,那什么情况最差呢?靠近右下角?错的,其实是与虚线重合,如果ROC曲线靠近右下角其实跟靠近左上角一样,是很好的结果,这说明模型学反了,但是学的很好,把结果反过来看就可以了,当然实际不太可能出现这种情况。

sklearn代码绘制

最后我们来说明一下如何使用sklearn库实现我们上述讲到的内容,其主要都在sklearn.metrics中,下面导入需要的库

1
2
3
4
5
6
7
8
9
10
11
12
13
import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import load_breast_cancer # 乳腺癌数据集
from sklearn.ensemble import RandomForestClassifier # 随机森林分类
from sklearn.model_selection import train_test_split # 切分数据

from sklearn.metrics import precision_score, recall_score, f1_score # P,R,F1
from sklearn.metrics import confusion_matrix # 混淆矩阵
from sklearn.metrics import precision_recall_curve, average_precision_score # P-R曲线与AP
from sklearn.metrics import roc_curve, auc # ROC曲线与AUC

import warnings
warnings.filterwarnings("ignore")

我们使用乳腺癌数据集作为示例数据集,下面我们导入数据并完成模型的训练

1
2
3
4
5
6
7
8
9
data = load_breast_cancer()
train = data.data
target = data.target

Xtrain, Xtest, Ytrain, Ytest = train_test_split(train, target, test_size=0.1, random_state=1008)

clf = RandomForestClassifier(n_estimators=100, random_state=1008)
clf.fit(Xtrain, Ytrain)
clf.score(Xtest, Ytest)

首先我们来计算精准率,召回率与F1值,使用函数precision_scorerecall_scoref1_score,参数也很简单,填入真值与预测值即可,其余参数都不算很常用。

1
2
3
4
5
6
# 计算P,R,F1
Ypred = clf.predict(Xtest)
P = precision_score(Ytest, Ypred)
R = recall_score(Ytest, Ypred)
f1 = f1_score(Ytest, Ypred)
print("P=", P, "R=", R, "F1=", f1)

然后是绘制混淆矩阵,使用函数confusion_matrix,当然这个函数只负责算出混淆矩阵,绘图还是需要用motplotlab写,参数同样是填入真值与预测值,其返回值就是混淆矩阵。

1
2
3
4
5
6
7
8
9
#绘制混淆矩阵
cm = confusion_matrix(Ytest, Ypred)

plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='g', cmap='Blues',
xticklabels=['Predicted 0', 'Predicted 1'],
yticklabels=['Actual 0', 'Actual 1'])
plt.title('Confusion Matrix')
plt.show()

image-20240403214644671

接着是绘制P-R曲线,使用函数precision_recall_curve,需要注意的是,其参数填入的是真实值与每个样本的预测概率值,这可以通过模型的predict_proba方法计算,这会获得每个样本被分到0与1的概率,我们只需要样本被分到1的概率。函数的返回值有三个,分别为P,R与每一次计算的阈值,一般我们不需要阈值,这里使用_代替,表示不需要该返回值。

使用average_precision_score函数可以计算AP值。这里的AP值是P-R曲线面积的一种精度更高的算法,使用的时候理解为面积即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 绘制P-R曲线
P, R, _ = precision_recall_curve(Ytest, clf.predict_proba(Xtest)[:, 1])
ap = average_precision_score(Ytest, clf.predict_proba(Xtest)[:, 1])

plt.figure(figsize=(8, 6))
plt.plot(R, P, color='darkorange', lw=2, label='P-R curve (AP = %0.2f)' % ap)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('Precision Recall Curve')
plt.legend(loc="lower right")
plt.xlim(0, 1)
plt.ylim(0, 1.05)

image-20240404121225542

最后是绘制ROC曲线,使用函数roc_curve,其参数同P-R曲线,填入真值与概率值,返回值为FPR,TPR与每一轮计算的阈值。

使用auc可以计算曲线包住的面积,填入FPR与TPR即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
#绘制ROC曲线
fpr, tpr, _ = roc_curve(Ytest, clf.predict_proba(Xtest)[:, 1])
roc_auc = auc(fpr, tpr)

plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC curve (AUC = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC) Curve')
plt.legend(loc="lower right")
plt.xlim(0, 1)
plt.ylim(0, 1.05)

image-20240403214809096

补充:上述提到的绘图sklearn都进行了封装,这里也一并给出示例代码

1
2
3
4
5
6
7
8
9
10
11
# 混淆矩阵
from sklearn.metrics import plot_confusion_matrix
plot_confusion_matrix(clf, Xtest, Ytest)

# P-R曲线
from sklearn.metrics import plot_precision_recall_curve
plot_precision_recall_curve(clf, Xtest, Ytest)

# ROC曲线
from sklearn.metrics import plot_roc_curve
plot_roc_curve(clf, Xtest, Ytest)