对学习器的泛化性能进行评估,不仅需要有效可行的实验估计方法,还需要有衡量模型泛化能力的评价标准,这就是性能度量(performance measure)。
性能度量反映了任务需求,在对比不同模型的能力时,使用不同的性能度量往往会导致不同的评判结果,这意味着模型的“好坏”是相对的,什么样的模型是好的,不仅取决于算法和数据,还决定于任务需求。
在预测任务中,给定数据集 D = ( x 1 , y 1 ) , ( x 2 , y 2 ) , . . . , ( x m , y m ) D = {(x_1, y_1), (x_2, y_2), ..., (x_m, y_m)} D=(x1,y1),(x2,y2),...,(xm,ym),其中 y i y_i yi 是示例 x i x_i xi 的真实标记。要估计学习器 f 的性能,就要把学习器预测结果 f ( x ) f(x) f(x) 与真实标记 y 进行比较。
为了说明各性能度量指标,我们以鸢尾花数据集为例,模型选择决策树 CART 算法,通过 train_test_split() 划分数据集,最后评估各项性能指标。
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
# 引入数据集
dataset = load_iris()
data = dataset.data
target = dataset.target
features = dataset.feature_names
# 划分数据集以及模型训练
data_train, data_test, target_train, target_test = train_test_split(data, target, test_size=0.33, random_state=7)
model = DecisionTreeClassifier()
model.fit(data_train, target_train)
错误率与精度
假设现有样例集(数据集) D,则错误率和精度的定义如下:
- 错误率:分类错误的样本数占样本总数的比例。
E ( f ; D ) = 1 m ∑ i = 1 m I ( f ( x i ) ≠ y i ) E(f;D) = \frac{1}{m}\sum_{i=1}^m I(f(x_i) \neq y_i) E(f;D)=m1i=1∑mI(f(xi)̸=yi) - 精度(准确率,Accuracy):分类正确的样本数占样本总数的比例。
a c c ( f ; D ) = 1 m ∑ i = 1 m I ( f ( x i ) = y i ) = 1 − E ( f ; D ) acc(f;D) = \frac{1}{m}\sum_{i=1}^m I(f(x_i) = y_i) = 1 - E(f;D) acc(f;D)=m1i=1∑mI(f(xi)=yi)=1−E(f;D)
错误率和精度是分类问题中最简单也是最直观的两个性能度量指标,往往求出一个即可求得另外一个,例如 300 条测试数据,其中 30 条错误,那么错误率就为 0.1,精度为 1 - 0.1 = 0.9。
【代码实现】:
from sklearn.metrics import accuracy_score
accuracy_score(target_test, model.predict(data_test))
# 输出:0.94
【精度的局限性】:当负样本占 99% 时,分类器把所有样本都预测为负样本也可以获得 99% 的准确率。所以,当不同类别的样本比例非常不均衡时,占比大的类别往往成为影响精度的最主要因素。虽然模型的整体分类精度高,但不代表对占比小的类别的分类精度也高。
解决方案:平均精度——每个类别下的样本精度的算术平均。例如一个二分类问题,分类 0 共有 270 个样本,其中正确的有 240;分类 1 共有 30 个样本,其中正确的有 24。最终计算出的精度为 (240 + 24) / 300 = 0.88,而平均精度为 (240 / 270 + 24 / 30) / 2 = 0.844。可以看出在分类不均衡情况下,平均精度比精度更能反映客观情况。
from sklearn.metrics import balanced_accuracy_score
balanced_accuracy_score(target_test, model.predict(data_test))
# 输出:0.944
查准率、查全率与 F1
假设一个二分类问题,将关注的类别取名为正例(positive),则另一个类别为反例(negative),然后再将样例根据其真实类别与学习器预测类别的组合划分为真正例(true positive)、假正例(false positive)、假反例(false negative)和真反例(true negative)。令 TP、FP、TN、FN 分别表示其对应的样例数,则 TP + FP + TN + FN = 样例总数。分类结果的混淆矩阵(confusion matrix)如下图所示。
鸢尾花数据集是一个三分类问题,因此在用混淆矩阵时,得到的是一个 3 X 3 的矩阵。此时预测结果和真实情况不再以正例、反例命名,而是数据集真实的分类结果。
【代码实现】:
from sklearn.metrics import confusion_matrix
print(confusion_matrix(target_test, model.predict(data_test)))
# 输出:
array([[14, 0, 0],
[ 0, 16, 2],
[ 0, 1, 17]], dtype=int64)
print(set(target))
# 输出: {0, 1, 2}
- 查准率(精确率 Precision):在所有被预测为正类的样本中真实结果也为正类的占比。也就是说,分类正确的正样本个数占分类器判定为正样本的样本个数的比例。
P = T P T P + F P P = \frac{TP}{TP + FP} P=TP+FPTP - 查全率(召回率 Recall):在所有真实结果为正类的样本中预测结果也为正类的占比。也就是说,分类正确的正样本个数占真正的正样本个数的比例。
R = T P T P + F N R = \frac{TP}{TP + FN} R=TP+FNTP
【查准率和查全率代码实现】:
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
print(precision_score(target_test, model.predict(data_test), average='weighted'))
# 输出:0.941
print(recall_score(target_test, model.predict(data_test), average='weighted'))
# 输出:0.94
需要注意的是,precision_score 和 recall_score 方法默认用来计算二分类问题,若要计算多分类问题,则需要设置 average 参数,更多内容请参考官方文档:
查准率和查全率是一对矛盾的度量。一般来说,查准率高时,查全率往往偏低;而查全率高时,查准率往往偏低。
【举例】:若希望将好瓜尽可能多地选出来,则可通过增加选瓜的数量来实现,如果将所有西瓜都选上,那么所有的好瓜也必然都被选上了(查全率高),但这样查准率就会较低;若希望选出的瓜中好瓜比例尽可能高,则可只挑选最有把握的瓜,但这样就难免会漏掉不少好瓜,使得查全率较低。
【P-R 图】:根据学习器的预测结果对样例进行排序,排在前面的是学习器认为“最可能”是正例的样本,排在最后的则是学习器认为“最不可能”是正例的样本。按此顺序逐个把样本作为正例进行预测,则每次可以计算出当前的查全率、查准率。以查准率为纵轴、查全率为横轴作图,就得到查准率-查全率曲线,简称“P-R 曲线”,显示该曲线的图称为“P-R”图。
以信息检索为例,刚开始在页面上显示的信息是用户可能最感兴趣的信息,此时查准率高,但只显示了部分数据,所以查全率低。随着用户不断地下拉,信息符合用户兴趣的匹配程度逐渐降低,查准率不断下降,查全率逐渐上升。当下拉到信息底部时,此时的信息是最不符合用户兴趣,因此查准率最低,但所有的信息都已经展示,查全率最高。
需要注意的是 P-R 曲线只能用于二分类问题,官方文档明确指出:
Note: this implementation is restricted to the binary classification task.
因此我们需要对鸢尾花数据集进行改造,只取其中两分类。
import numpy as np
from matplotlib import pyplot as plt
from sklearn.metrics import precision_recall_curve
from sklearn.metrics import average_precision_score
from sklearn.svm import LinearSVC
from inspect import signature
iris = load_iris()
X = iris.data
y = iris.target
# 添加噪声
random_state = np.random.RandomState(0)
n_samples, n_features = X.shape
X = np.c_[X, random_state.randn(n_samples, 200 * n_features)]
X_train, X_test, y_train, y_test = train_test_split(X[y < 2], y[y < 2], test_size=.5, random_state=random_state)
# 模型训练
classifier = LinearSVC(random_state=random_state)
classifier.fit(X_train, y_train)
y_score = classifier.decision_function(X_test)
average_precision = average_precision_score(y_test, y_score)
precision, recall, _ = precision_recall_curve(y_test, y_score)
# 绘制 P-R 图
step_kwargs = ({'step': 'post'}
if 'step' in signature(plt.fill_between).parameters
else {})
plt.step(recall, precision, color='b', alpha=0.2, where='post')
plt.fill_between(recall, precision, alpha=0.2, color='b', **step_kwargs)
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.ylim([0.0, 1.05])
plt.xlim([0.0, 1.0])
plt.title('2-class Precision-Recall curve: AP={0:0.2f}'.format(average_precision))
具体代码以及 precision_recall_curve 用法可参考官方文档:
P-R 图可以用于学习器的比较:
- 若一个学习器的 P-R 曲线被另一个学习器的 P-R 曲线完全“包住”,则可断言后者的性能优于前者。例如上图中学习器 A 的性能优于学习器 C。
- 若两个学习器的 P-R 曲线发生了交叉,例如学习器 A 与 B,则难以一般性地断言两者孰优孰劣,只能在具体的查准率或查全率条件下进行比较。若要综合性考虑,可使用如下方法。
【交叉时的判断方法】:
- 平衡点(Break-Even Point,简称 BEP):查准率=查全率时的取值。基于该方法则可断言学习器 A 优于学习器 B。
- F1 度量:基于查准率与查全率的调和平均(harmonic mean)定义。
F 1 = 2 × P × R P + R = 2 × T P 样例总数 + T P − T N 1 F 1 = 1 2 . ( 1 P + 1 R ) F1 = \frac{2 \times P \times R}{P + R} = \frac{2 \times TP}{\text{样例总数} + TP - TN} \\ \frac{1}{F1} = \frac{1}{2}.(\frac{1}{P} + \frac{1}{R}) F1=P+R2×P×R=样例总数+TP−TN2×TPF11=21.(P1+R1)
在一些应用中,对查准率和查全率的重视程度有所不同,会相应地添加权重。F1 度量的一般形式——
F
β
F_\beta
Fβ 能表达出查准率/查全率的不同偏好。
F
β
=
(
1
+
β
2
)
×
P
×
R
(
β
2
×
P
)
+
R
F_\beta = \frac{(1+\beta^2) \times P \times R}{(\beta^2 \times P) + R}
Fβ=(β2×P)+R(1+β2)×P×R
其中
β
>
0
\beta > 0
β>0 度量查全率对查准率的相对重要性。
- β = 1 \beta = 1 β=1 时退化为标准的 F1;
- β > 1 \beta > 1 β>1 时查全率有更大影响;
- β < 1 \beta < 1 β<1 时查准率有更大影响。
【F1 代码实现】:
from sklearn.metrics import f1_score
from sklearn.metrics import fbeta_score
# 标准的 F1
print(f1_score(target_test, model.predict(data_test), average='weighted'))
# 输出:0.93995
# 一般形式 F1
print(fbeta_score(target_test, model.predict(data_test), beta=1, average='weighted'))
# 输出:0.93995,可以看到 beta = 1 时,Fb 退化为标准 F1
print(fbeta_score(target_test, model.predict(data_test), beta=2, average='weighted'))
# 输出:0.939859 查全率有更大影响
print(fbeta_score(target_test, model.predict(data_test), beta=0.5, average='weighted'))
# 输出:0.940415 查准率有更大影响
很多时候我们有多个二分类混淆矩阵(鸢尾花数据集就是一个多分类问题,因此上述代码实现中需要添加 average 参数),需要进行多次训练/测试,每次得到一个混淆矩阵;或是在多个数据集上进行训练/测试,希望估计算法的“全局”性能,甚或是执行多分类任务,每两两类别的组合都对应一个混淆矩阵。总之,希望在 n 个二分类混淆矩阵上综合考察查准率和查全率。
【做法 1】:
- 在各混淆矩阵上分别计算出查准率和查全率,记为 ( P 1 , R 1 ) , ( P 2 , R 2 ) , ⋯   , ( P n , R n ) (P_1, R_1), (P_2, R_2), \cdots, (P_n, R_n) (P1,R1),(P2,R2),⋯,(Pn,Rn)。
- 计算平均值。
- 得到宏查准率(macro-P)、宏查全率(macro-R)以及相应的宏F1(macro-F1)。
m a c r o P = 1 n ∑ i = 1 n P i , m a c r o R = 1 n ∑ i = 1 n R i , m a c r o F 1 = 2 × m a c r o P × m a c r o R m a c r o P + m a c r o R macroP = \frac{1}{n}\sum_{i=1}^nP_i, \\ macroR = \frac{1}{n}\sum_{i=1}^nR_i, \\ macroF1 = \frac{2 \times macroP \times macroR}{macroP + macroR} macroP=n1i=1∑nPi,macroR=n1i=1∑nRi,macroF1=macroP+macroR2×macroP×macroR
print(f1_score(target_test, model.predict(data_test), average='macro'))
# 输出:0.9444
【做法 2】:
- 将各混淆矩阵的对应元素进行平均,得到 TP、FP、TN、FN 的平均值,分别记为
T P ‾ , F P ‾ , T N ‾ , F N ‾ \overline{TP} , \overline{FP} , \overline{TN} , \overline{FN} TP,FP,TN,FN - 基于上述平均值计算出微查准率(micro-P)、微查全率(micro-R)和微F1(micro-F1)。
m i c r o P = T P ‾ T P ‾ + F P ‾ m i c r o R = T P ‾ T P ‾ + F N ‾ m i c r o F 1 = 2 × m i c r o P × m i c r o R m i c r o P + m i c r o R microP = \frac{\overline{TP}}{\overline{TP} + \overline{FP}} \\ microR = \frac{\overline{TP}}{\overline{TP} + \overline{FN}} \\ microF1 = \frac{2 \times microP \times microR}{microP + microR} microP=TP+FPTPmicroR=TP+FNTPmicroF1=microP+microR2×microP×microR
print(f1_score(target_test, model.predict(data_test), average='micro'))
# 输出:0.94
关于 F1 和 Fbeta 的更多用法请参考官方文档:
ROC 与 AUC
很多学习器为测试样本产生一个实值或概率预测,根据该实值或概率预测,我们可将测试样本进行排序,“最可能”是正例的排在最前面,“最不可能”是正例的排在最后面。这样,分类过程就相当于在该排序中以某个“截断点”(cut point)将样本分为两部分,前一部分判作正例,后一部分则判作反例。
在不同的应用任务中,可根据任务需求来采用不同的截断点。
- 查准率:选择排序中靠前的位置进行截断。
- 查全率:选择排序中靠后的位置进行截断。
ROC 曲线基于借助排序本身质量好坏来体现综合考虑学习器在不同任务下的“期望泛化性能”的好坏角度出发,研究学习器泛化性能的有力工具。
ROC 全称是“受试者工作特征”(Receiver Operating Characteristic)曲线。根据学习器的预测结果对样例进行排序,按此顺序逐个把样本作为正例进行预测,每次计算出两个重要量的值,分别以它们为横、纵坐标作图。仍然以二分类混淆矩阵为例:
- 纵轴:真正例率(True Positive Rate,简称 TPR)。
T P R = T P T P + F N TPR = \frac{TP}{TP + FN} TPR=TP+FNTP - 横轴:假正例率(False Positive Rate,简称 FPR)。
F P R = F P F P + T N FPR = \frac{FP}{FP + TN} FPR=FP+TNFP
- 对角线:随机猜测模型,猜对猜错都只有 50% 的概率。
- 点(0,1):所有正例排在所有反例之前的“理想模型”。
- 现实任务中通常是利用有限个测试样例来绘制 ROC 图,此时仅能获得有限个坐标对,无法产生平滑的 ROC 曲线。
【绘图过程】:给定 m + m^+ m+ 个正例和 m − m^- m− 个反例,根据学习器预测结果对样例进行排序,然后把分类阈值设为最大,即把所有样例均预测为反例,此时真正例率和假正例率均为 0,在坐标 (0, 0) 处标记一个点,然后,将分类阈值依次设为每个样例的预测值,即依次将每个样例划分为正例。设前一个标记点坐标为 (x, y),当前若为真正例,则对应标记点的坐标为 ( x , y + 1 m + ) (x, y+\frac{1}{m^+}) (x,y+m+1);若为假正例,则对应标记点的坐标为 ( x + 1 m − , y ) (x+\frac{1}{m^-}, y) (x+m−1,y),然后用线段连接相邻点即得。
样本序号 | 真实标签 | 预测正类概率 | 样本序号 | 真实标签 | 预测正类概率 |
---|---|---|---|---|---|
1 | P | 0.9 | 2 | P | 0.8 |
3 | N | 0.7 | 4 | P | 0.6 |
5 | P | 0.55 | 6 | P | 0.54 |
7 | N | 0.53 | 8 | N | 0.52 |
9 | P | 0.51 | 10 | N | 0.505 |
11 | P | 0.4 | 12 | N | 0.39 |
13 | P | 0.38 | 14 | N | 0.37 |
15 | N | 0.36 | 16 | N | 0.35 |
17 | P | 0.34 | 18 | N | 0.33 |
19 | P | 0.30 | 20 | N | 0.1 |
m + m^+ m+ = 10, m − m^- m− = 10,按照绘图过程的步骤,首先将所有样例都预测为反例,则在坐标 (0, 0) 处标记一个点。然后判断第一个样本,将分类阈值设置为当前样本的预测值,即 0.9,同时将该样本当做正例,而恰好当前样本的真实标签是正例,也就是说当前样本属于真正例,那么当前标记点的坐标为 (0, 0 + 1/10) = (0, 0.1)。同理,第二个样本也为真正例,所以坐标为 (0, 0.1 + 1/10) = (0, 0.2)。第三个样本为假正例,故而坐标为 (0 + 1/10, 0.2) = (0.1, 0.2)。依次循环,直到将所有样本的坐标点都计算完毕后,我们根据这些坐标点即可将 ROC 曲线绘制出来,见下图。
ROC 曲线与 P-R 曲线一样,也可以用来比较不同的学习器:
- 若一个学习器的 ROC 曲线被另一个学习器的曲线完全“包住”,则可断言后者的性能优于前者。
- 若两个学习器的 ROC 曲线发生交叉,则难以一般性地断言两者孰优孰劣。此时如果一定要进行比较,则较为合理的判据是比较 ROC 曲线下的面积,即 AUC(Area Under ROC Curve)。
【AUC】:AUC 指的是 ROC 曲线下的面积大小,该值能够量化地反映基于 ROC 曲线衡量出的模型性能。假定 ROC 曲线是由坐标为
{
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
,
⋯
 
,
(
x
m
,
y
m
)
}
\{(x_1, y_1), (x_2, y_2), \cdots, (x_m, y_m)\}
{(x1,y1),(x2,y2),⋯,(xm,ym)} 的点按序连接而形成(
x
1
=
0
,
x
m
=
1
x_1 = 0, x_m = 1
x1=0,xm=1),则 AUC 可估算为
A
U
C
=
1
2
∑
i
=
1
m
−
1
(
x
i
+
1
−
x
i
)
.
(
y
i
+
y
i
+
1
)
AUC = \frac{1}{2}\sum_{i=1}^{m-1}(x_{i+1}-x_i).(y_i + y_{i+1})
AUC=21i=1∑m−1(xi+1−xi).(yi+yi+1)
需要注意的是 roc_curve 同 precision_recall_curve,都只能用于二分类问题,但 sklearn 的 roc_auc_score() 方法支持计算多分类问题的 auc 面积。
Compute Area Under the Receiver Operating Characteristic Curve (ROC AUC) from prediction scores.
Note: this implementation is restricted to the binary classification task or multilabel classification task in label indicator format.
关于 auc、roc_curve 以及 roc_auc_score 的用法请参考官方文档:
问:ROC 曲线相比 P-R 曲线有什么特点?
当正负样本的分布发生变化时,ROC 曲线的形状能够基本保持不变,而 P-R 曲线的形状一般会发生较剧烈的变化。
图 (a) 和图 © 是 ROC 曲线,图 (b) 和图 (d) 是 P-R 曲线。图 © 和图 (d) 是将测试机中的负样本数量增加 10 倍后的曲线图。
真实结果 | 预测结果 | 预测结果 |
---|---|---|
- | 真 | 假 |
真 | 8 | 2 |
假 | 2 | 8 |
P = T P T P + F P = 8 8 + 2 = 0.8 R = T P T P + F N = 8 8 + 2 = 0.8 T P R = T P T P + F N = 8 8 + 2 = 0.8 F P R = F P F P + T N = 2 2 + 8 = 0.2 P = \frac{TP}{TP + FP} = \frac{8}{8 + 2} = 0.8 \\ R = \frac{TP}{TP + FN} = \frac{8}{8 + 2} = 0.8 \\ TPR = \frac{TP}{TP + FN} = \frac{8}{8 + 2} = 0.8 \\ FPR = \frac{FP}{FP + TN} = \frac{2}{2 + 8} = 0.2 P=TP+FPTP=8+28=0.8R=TP+FNTP=8+28=0.8TPR=TP+FNTP=8+28=0.8FPR=FP+TNFP=2+82=0.2
此时,测试集的负样本数量增加 10 倍且新增加的样本遵循原始样本的分布。因此在模型不变的情况下,TN 和 FN 都会等比例增大。
真实结果 | 预测结果 | 预测结果 |
---|---|---|
- | 真 | 假 |
真 | 8 | 2 |
假 | 22 | 88 |
P
=
T
P
T
P
+
F
P
=
8
8
+
22
=
4
15
R
=
T
P
T
P
+
F
N
=
8
8
+
2
=
0.8
T
P
R
=
T
P
T
P
+
F
N
=
8
8
+
2
=
0.8
F
P
R
=
F
P
F
P
+
T
N
=
22
22
+
88
=
0.2
P = \frac{TP}{TP + FP} = \frac{8}{8 + 22} = \frac{4}{15} \\ R = \frac{TP}{TP + FN} = \frac{8}{8 + 2} = 0.8 \\ TPR = \frac{TP}{TP + FN} = \frac{8}{8 + 2} = 0.8 \\ FPR = \frac{FP}{FP + TN} = \frac{22}{22 + 88} = 0.2
P=TP+FPTP=8+228=154R=TP+FNTP=8+28=0.8TPR=TP+FNTP=8+28=0.8FPR=FP+TNFP=22+8822=0.2
可以看到,ROC 曲线的 TPR 和 FPR 没有发生变化,而 P-R 曲线的 P 发生了巨大的变化。
分类报告
scikit-learn 中提供了一个非常方便的工具,可以给出对分类问题的评估报告,Classification_report() 方法能够给出精确率(precision)、召回率(recall)、F1 值(F1-score)和样本数目(support)。
【代码实现】:
from sklearn.metrics import classification_report
print(classification_report(target_test, model.predict(data_test)))
# 输出:
precision recall f1-score support
0 1.00 1.00 1.00 14
1 0.94 0.89 0.91 18
2 0.89 0.94 0.92 18
micro avg 0.94 0.94 0.94 50
macro avg 0.95 0.94 0.94 50
weighted avg 0.94 0.94 0.94 50
因此,我们在处理多分类问题时,不妨先找一个处理速度最快的模型,然后用分类报告查看一下查准率、查全率、f1 值等信息,以便为后续的优化做一个基准。
总结
错误率、精度、查准率、查全率、混淆矩阵以及分类报告都可以用于单个模型的性能度量评估,P-R 曲线以及 ROC 也可以用于单个模型的性能度量评估,但更多的是用作多个模型的直观对比。我们把多个模型的 P-R 曲线或 ROC 曲线绘制出来,然后通过上述所讲的比较方法,就可以轻松地选出最优的模型。因此,我个人认为 P-R 曲线和 ROC 曲线一方面是为了查准率和查全率服务,另一面用于多个模型的性能对比。
本篇博客的所有代码都可以在 传送门 中找到。
参考
- 《机器学习》周志华
- 《百面机器学习》
- sklearn 官方文档:https://scikit-learn.org/stable/modules/classes.html#module-sklearn.metrics