所谓”性能度量“(performance measure)就是衡量模型泛化能力的评价标准。它反映了任务需求,对于不同的任务需求往往使用不同的性能度量。比如,回归任务中常用的“均方误差”(mean squared error),分类任务中常用的“错误率”(error rate)与“精度”(accuracy)。
均方误差
均方误差是回归任务中最常用的性能度量,对于给定数据集D={(x1, y1), (x2, y2), ……, (xm, ym)},其中yi是样本xi对应的真实值。对于学习器f,评估其性能就是把预测值f(xi)与真实值yi进行比较,定义为:
E
(
f
;
D
)
=
1
m
∑
i
=
1
m
(
f
(
x
i
−
y
i
)
)
2
E(f;D)=\frac{1}{m} \sum_{i=1}^m(f(x_i-y_i))^2
E(f;D)=m1i=1∑m(f(xi−yi))2
代码如下
"""
均方误差
"""
import numpy as np
# 构造数据
test_example_number = 100
predict_label = np.random.normal(0, 1, test_example_number)
label = np.random.normal(0, 1, test_example_number)
# 计算均方误差MSE
def mse(predict, label):
"""
- predict : 预测值
- label : 真实值
"""
total_error = 0
for i in range(len(label)):
total_error += (predict_label[i] - label[i]) ** 2
return total_error / len(label)
mse(predict_label, label)
错误率与精度
错误率与精度是分类任务中最常见的两种评价标准,包括二分类和多分类任务。
错误率是分类错误的样本数占样本总数的比例,精度则是分类正确的样本数占样本总数的比例。对数据集D,错误率定义为:
E
(
f
;
D
)
=
1
m
∑
i
=
0
m
(
Ⅱ
(
f
(
x
i
)
≠
y
i
)
)
E(f;D)=\frac{1}{m} \sum_{i=0}^m(Ⅱ(f(x_i)≠y_i))
E(f;D)=m1i=0∑m(Ⅱ(f(xi)=yi))
精度定义为:
a
c
c
(
f
;
D
)
=
1
m
∑
i
=
0
m
(
Ⅱ
(
f
(
x
i
)
=
y
i
)
)
=
1
−
E
(
f
;
D
)
acc(f;D)=\frac{1}{m} \sum_{i=0}^m(Ⅱ(f(x_i)=y_i))\\= 1-E(f;D)
acc(f;D)=m1i=0∑m(Ⅱ(f(xi)=yi))=1−E(f;D)
代码如下
"""
精度
"""
import numpy as np
# 构造数据
classes = 10
test_example_number = 100
predict = np.random.randint(0, classes, test_example_number)
label = np.random.randint(0, classes, test_example_number)
predict = np.eye(classes)[predict]
label = np.eye(classes)[label]
# 计算精度
def acc(predict, label):
"""
- predict : 预测值one-hot
- label : 真实值one-hot
"""
right = len(label) - np.sum((predict - label) ** 2) // 2
return right / len(label)
acc(predict, label)
查准率、查全率与F1
考虑以下问题:农夫手里有一批西瓜,我们用训练好的模型对这些西瓜进行判定,显然,错误率可以衡量出有多少比例的西瓜被判定错误。但是如果我们更关心“挑出来的西瓜中有多少是好的”,或者“所有的好瓜中有多少比例被挑了出来”,那么错误率就不再适用,此时“查准率”(precision)和“查全率”(recall)更适合作为性能度量。
以二分类为例,根据分类结果,构造如下“混淆矩阵”(confusion matrix):
真实情况 | 预测结果 | |
正例 | 反例 | |
正例 | TP(真正例) | FN(假反例) |
反例 | FP(假正例) | TN(真反例) |
代码如下
"""
混淆矩阵
"""
import itertools
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# 构造数据
classes_num = 5
classes = ['A', 'S', 'D', 'F', 'G']
test_example_number = 100
predict = np.random.randint(0, classes_num, test_example_number)
label = np.random.randint(0, classes_num, test_example_number)
predict = np.eye(classes_num, dtype=np.int32)[predict]
label = np.eye(classes_num, dtype=np.int32)[label]
# 计算混淆矩阵
def confusion_matrix(predict, label):
"""
- predict : 预测值one-hot
- label : 真实值one-hot
"""
num, classes_num = label.shape
con_matrix = np.zeros((classes_num, classes_num), dtype=np.int32)
for i in range(num):
con_matrix[np.argmax(label[i]), np.argmax(predict[i])] += 1
return con_matrix
# 绘制混淆矩阵
def plot_confusion_matrix(con_matrix, classes, normalize=False, title='Confusion matrix', cmap=plt.cm.Reds):
"""
- con_matrix : 计算出的混淆矩阵的值
- classes : 混淆矩阵中每一行每一列对应的列
- normalize : True:显示百分比, False:显示个数
"""
if normalize:
con_matrix = con_matrix.astype('float') / con_matrix.sum(axis=1)[:, np.newaxis]
np.set_printoptions(formatter={'float': '{: 0.2f}'.format})
plt.imshow(con_matrix, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(classes))
# plt.xticks(tick_marks, classes, rotation=45)
plt.xticks(tick_marks, classes)
plt.yticks(tick_marks, classes)
fmt = '.2f' if normalize else 'd'
thresh = con_matrix.max() / 2.
for i, j in itertools.product(range(con_matrix.shape[0]), range(con_matrix.shape[1])):
plt.text(j, i, con_matrix[i, j],
horizontalalignment="center",
color="white" if con_matrix[i, j] > thresh else "black")
plt.tight_layout()
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.show()
confusion_matrix = confusion_matrix(predict, label)
plot_confusion_matrix(confusion_matrix, classes)
查准率P与查全率R分别定义为:
P
=
T
P
T
P
+
F
P
R
=
T
P
T
P
+
F
N
P = \frac{TP}{TP+FP}\\R = \frac{TP}{TP+FN}
P=TP+FPTPR=TP+FNTP
从定义中可以看出,查准率是预测正例中预测结果正确的样本所占的比例,查全率是真实正例中预测正确的样本所占的比例。分别对应于“挑出来的西瓜中有多少是好的”和“所有的好瓜中有多少比例被挑了出来”。
一般来说,查准率较高时,查全率往往偏低;而查全率较高时,查准率往往偏低。只有在一些简单任务中,才可能使两者都很高。
把测试样本为正例的可能性按照从高到低排序,并按此顺序逐个把样本作为正例进行预测,则可以得到一系列的查准率和查全率。以查准率为纵轴、查全率为横轴进行作图,可以得到一系列的点,将这些点连成线就得到查准率-查全率曲线,简称“P-R”曲线,显示该曲线的图称为“P-R”图。示意图如下:
在对多个模型进行性能评估时,如果一个模型的P-R曲线被另一个模型的P-R曲线完全“包住”,则说明后者优于前者,例如上图中的A和C。如果两个模型的P-R曲线发生交叉,例如上图中的A和B,此时较为合理的判断依据是“平衡点”。“平衡点”(Break-Even Point, 简称BEP),是“查准率=查全率”时的取值。上图中,模型A的平衡点大于模型B的平衡点,因此可以认为模型A优于模型B。
然而BEP还是过于简化,更常用的是F1度量:
F
1
=
2
×
P
×
R
P
+
R
=
2
×
T
P
样
本
总
数
+
T
P
−
T
N
F1=\frac{2×P×R}{P+R}=\frac{2×TP}{样本总数+TP-TN}
F1=P+R2×P×R=样本总数+TP−TN2×TP
是基于查准率与查全率的调和平均定义的:
1
F
1
=
1
2
⋅
(
1
P
+
1
R
)
\frac{1}{F1} = \frac{1}{2}·(\frac{1}{P}+\frac{1}{R})
F11=21⋅(P1+R1)
以上性能度量都是基于二分类为例进行分析,对于多分类问题,在对不同的类别进行分析时,分别把该类别作为正例其他类别作为反例进行分析,这样便得可到每个类别的性能度量。如果先计算每个类别的查准率和查全率,再计算平均值(宏平均),可得宏-P、宏-R和宏-F1;如果先平均TP、TN、FP、FN,再计算P、R和F1(微平均),可得微-P、微-R和微F1。
宏平均和微平均的对比如下:
-
如果每个类别的样本数量相差不大, 那么宏平均和微平均差异也不大
-
如果每个类别的样本数量相差较大,且:
- 更注重样本量多的类别,使用宏平均
- 更注重样本量少的类别,使用微平均
-
如果微平均远低于宏平均,则要检查样本量多的类别
-
如果宏平均远低于微平均,则要检查样本量少的类别
代码如下
"""
P-R曲线
"""
import numpy as np
import matplotlib.pyplot as plt
# 构造数据
test_example_number = 100
predict = np.random.random(test_example_number)
label = np.random.randint(0, 2, test_example_number)
# 计算混淆矩阵
def confusion_matrix(predict, label):
"""
- predict : 样本为正类的概率
- label : 样本的真实标签值
"""
con_matrix = np.zeros((2, 2), dtype=np.int32)
for i in range(len(label)):
con_matrix[label[i], predict[i]] += 1
return con_matrix
# 绘制P-R图
def P_R(predict, label):
"""
- predict : 样本为正类的概率
- label : 样本的真实标签值
"""
index = np.argsort(-predict)
label = label[index]
n = len(predict)
pre, p, r = np.zeros(n, dtype=np.int32), [], []
for i in range(1, n):
pre[:i] = 1
con_mat = confusion_matrix(pre, label)
p.append(con_mat[1, 1] / (con_mat[1, 1] + con_mat[0, 1]))
r.append(con_mat[1, 1] / (con_mat[1, 1] + con_mat[1, 0]))
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.plot(r, p, c="blue", label=r'P - R', alpha=1, lw=1, zorder=1)
plt.title("P - R图")
plt.legend()
plt.show()
P_R(predict, label)
ROC与AUC
很多模型是为测试样本产生一个实值或概率预测,然后将这个预测结果与一个分类阈值(threshold)进行比较,若大于阈值为正类,小于阈值为反类。这个实值或概率预测结果的好坏,直接决定了模型的泛化能力。根据这个实值或预测概率结果,可按照从大到小的顺序将测试样本进行排序,即“最可能”为正例的样本排在前面,“最不可能”为正例的样本排在后面。这样,分类过程相当于在这个排序中以某个“截断点”(cut point)将样本分类两部分,前一部分预测为正例,后一部分预测为反例。
如果更重视“查准率”,则可选择排序中靠前的位置进行截断;如果更重视“查全率”,则可选择靠后的位置进行截断。
ROC全称是“受试者工作特征(Receiver Operating Characteristic)”曲线,源于“二战”中用于敌机检测的雷达信号分析技术,二十世纪六七十年代开始用于一些心理学、医学检测应用中,此后被引入机器学习领域。根据模型的预测结果对样本进行排序,按此顺序逐个把样本作为正例进行预测,每次计算出“真正例率”和“假正例率”,并以前者为纵轴,后者为横轴,就可得到“ROC曲线”。
基于上面的混淆矩阵,“真正例率”(True Positive Rate, 简称TPR)和“假正例率”(False Positive Rate, 简称FPR)的定义为:
T
P
R
=
T
P
T
P
+
F
N
F
P
R
=
F
P
T
N
+
F
P
TPR = \frac{TP}{TP+FN}\\FPR = \frac{FP}{TN+FP}
TPR=TP+FNTPFPR=TN+FPFP
显示ROC曲线的图称为“ROC图”,示意图如下:
与P-R图相似,在对多个模型进行性能评估时,若一个模型的ROC曲线被另一个模型的ROC曲线完全“包住”,则说明后者的性能优于前者。若两个模型的ROC曲线发生交叉,较为合理的判断依据是比较ROC曲线下的面积,即AUC(Area Under ROC Curve)。如上图所示。其估算为:
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)
形式化地看,AUC考虑的是样本预测的排序质量,因此与排序误差有紧密联系。给定m+个正例和m-个反例,令D+和D-分别表示正、反例集合,则排序“损失”(loss)定义为
l
r
a
n
k
=
1
m
+
m
−
∑
x
+
∈
D
+
∑
x
−
∈
D
−
(
Ⅱ
(
f
(
x
+
)
<
f
(
x
−
)
)
+
1
2
Ⅱ
(
f
(
m
+
)
=
f
(
m
−
)
)
)
l_{rank}=\frac{1}{m^+m^-} \sum_{x^+∈D^+} \sum_{x^-∈D^-}(Ⅱ(f(x^+)<f(x^-))+\frac{1}{2}Ⅱ(f(m^+)=f(m^-)))
lrank=m+m−1x+∈D+∑x−∈D−∑(Ⅱ(f(x+)<f(x−))+21Ⅱ(f(m+)=f(m−)))
即考虑每一对正、反例,若正例的预测值小于反例,则记一个“罚分”,若相等,则记0.5个“罚分”。因此,lrank对应的是ROC曲线上面的面积。因此有
A
U
C
=
1
−
l
r
a
n
k
AUC=1-l_{rank}
AUC=1−lrank
代码如下
"""
ROC曲线
"""
import numpy as np
import matplotlib.pyplot as plt
# 构造数据
test_example_number = 100
predict = np.random.random(test_example_number)
label = np.random.randint(0, 2, test_example_number)
# 计算混淆矩阵
def confusion_matrix(predict, label):
"""
- predict : 样本为正类的概率
- label : 样本的真实标签值
"""
con_matrix = np.zeros((2, 2), dtype=np.int32)
for i in range(len(label)):
con_matrix[label[i], predict[i]] += 1
return con_matrix
# 绘制ROC图
def ROC(predict, label):
"""
- predict : 样本为正类的概率
- label : 样本的真实标签值
"""
index = np.argsort(-predict)
label = label[index]
n = len(predict)
pre, tpr, fpr = np.zeros(n, dtype=np.int32), [], []
for i in range(n):
pre[:i] = 1
con_mat = confusion_matrix(pre, label)
tpr.append(con_mat[1, 1] / (con_mat[1, 1] + con_mat[1, 0]))
fpr.append(con_mat[0, 1] / (con_mat[0, 0] + con_mat[0, 1]))
auc = 0
for i in range(n-1):
auc += 0.5 * (fpr[i+1] - fpr[i]) * (tpr[i] + tpr[i+1])
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.plot(fpr, tpr, c="blue", label=r'ROC图', alpha=1, lw=1, zorder=1)
plt.title("ROC图")
plt.legend()
plt.show()
return auc
ROC(predict, label)