课程8. 质量指标
质量指标
今天我们将讨论一个乍一看似乎微不足道的问题——如何衡量已经构建的分类器的质量?
每个人首先想到的可能是我们在整个课程中经常使用的指标。让我们从他开始吧。
准确度
准确度指的是分类器正确答案的比例
最明显、最直观的指标。为了计算准确度(Accuracy),我们采用由 N N N 个对象组成的原始数据集,并查看其中有多少个对象被正确分类。令这个数字为 M M M。然后: A c c u r a c y = M N Accuracy = \frac{M}{N} Accuracy=NM
看起来,还需要什么呢?
这是最客观地显示我们算法有效性的指标!
事实证明,准确性有其自身的问题,这常常使得该指标缺乏代表性,有时甚至会产生误导!
让我们看一个例子。
例子
我们的任务是将疑似患有严重疾病的患者分为需要医生进行进一步检查的患者和不需要医生进行进一步检查的患者。
每个病人都由某个向量 x ⃗ \vec{x} x 描述,让该向量的第一个坐标指定有关病人健康状况的投诉的存在。
让我们定义一个合成数据集如下:
- 假设我们有 950 名健康人和 50 名病人。
- 让50个病人中有40个感觉不好,也就是说,他们描述的向量的第一个坐标将等于1。
- 假设 950 名健康人中有一个疑病症患者,也就是说,那些虽然健康,却认为自己生病了的人。假设它们的数量为 100 左右。
import warnings
import numpy as np
from random import shuffle
warnings.filterwarnings('ignore')
# 确定数据的维度
x_shape = 3
# 健康人群数据
health_X = np.random.randn(950, x_shape)
health_f = [0] * 850 + [1] * 100
shuffle(health_f)
health_X[:, 0] = health_f
# 病人数据
ill_x = np.random.randn(50, x_shape)
ill_f = [0] * 10 + [1] * 40
shuffle(ill_f)
ill_x[:, 0] = ill_f
# 合并数据集
X = np.concatenate((health_X, ill_x))
# 为合并的数据创建标签
Y = np.concatenate((np.zeros(950), np.ones(50)))
print(f"Size X: {X.shape}")
print(f"Amount of health data: {np.sum(Y == 0)}")
print(f"Number of sick data: {np.sum(Y == 1)}")
输出:
Size X: (1000, 3)
Number of health data: 950
Number of sick data: 50
数据集已准备好。现在让我们定义 2 个分类器。
让他们其中一个更聪明,另一个更愚蠢。
- 分类器 1 将根据患者的健康状况做出预测
- 分类器 2 会将所有人分配到健康组
classifier_1 = lambda x: (x[:,0] == 1).astype(int) # 与幸福感的标志进行比较
classifier_2 = lambda x: np.zeros_like(x[:,0]) # 恒定预测
y_1 = classifier_1(X) # 第一个分类器的预测
y_2 = classifier_2(X) # 第二个分类器的预测
from sklearn.metrics import accuracy_score
accuracy_1 = accuracy_score(y_1, Y)
accuracy_2 = accuracy_score(y_2, Y)
print(f"Accuracy of classifier #1 (smart): {accuracy_1}")
print(f"Accuracy of classifier #2 (constant): {accuracy_2}")
输出:
Accuracy of classifier #1 (smart): 0.89
Accuracy of classifier #2 (constant): 0.95
这个结果并不令人意外。让我们看看我们各组的分布情况。
import matplotlib.pyplot as plt
import seaborn as sns
labels = [
"Disturbed in vain",
"Disturbed not in vain",
"Truly not disturbed",
"Unfound sick",
]
values = [
(y_1 * (1 - Y)).sum(),
(y_1 * Y).sum(),
((1 - y_1) * (1 - Y)).sum(),
((1 - y_1) * Y).sum(),
]
pie = plt.pie(values, labels=labels)
输出:
labels = ["Truly not disturbed", "Unfound sick"]
values = [((1 - y_2) * (1 - Y)).sum(), ((1 - y_2) * Y).sum()]
pie = plt.pie(values, labels=labels)
输出:
也就是说,分类器 2 的正确答案比例实际上更高,因为它没有白白打扰任何一个健康人。但他没有发现一个病人。
显然,第二个分类器对我们来说是绝对无用的,甚至有害的。这意味着准确性并不总是能帮助我们区分好的分类器和坏的分类器。
准确度指标可能面临的问题之一是类别不平衡,即数据集中的类别分布不均匀的情况,某些类别的数量明显较多,而其他类别的数量明显较少。
准确率和召回率
在我们继续描述这些指标之前,让我们先介绍一下误差矩阵的概念。
误差矩阵
让我们有一个二元分类问题,数据集 { ( x i , y i ) } i = 1 N \{{(x_i,y_i)}\}_{i=1}^N {(xi,yi)}i=1N
设有一个分类器
a
(
x
)
a(x)
a(x)。
然后,每次我们将算法
a
a
a 应用于对象
x
i
x_i
xi 时,我们有 4 个选项:(0是感觉好、1是感觉不好)
- a ( x i ) = y i = 0 a(x_i) = y_i = 0 a(xi)=yi=0 - 这种情况称为真阴性(TN)
- a ( x i ) = y i = 1 a(x_i) = y_i = 1 a(xi)=yi=1 - 这种情况称为真阳性(TP)
- a ( x i ) = 1 ≠ y i = 0 a(x_i) = 1 \neq y_i = 0 a(xi)=1=yi=0 - 这种情况称为假阳性(FP)
- a ( x i ) = 0 ≠ y i = 1 a(x_i) = 0 \neq y_i = 1 a(xi)=0=yi=1 - 这种情况称为假阴性(FN)
这种结构通常写成矩阵形式:它们计算每个类别中有多少个分类。
在我们的案例中,对应关系如下:
- TP = Disturbed not in vain(标签是得病,预测是得病)
- FP = Disturbed in vain(标签是健康,预测是生病)
- TN = Truly not disturbed(标签是健康,预测是健康)
- FN = Unfound sick(标签是生病,但预测是健康)
import pandas as pd
TP = (y_1 * Y).sum()
FP = (y_1 * (1 - Y)).sum()
TN = ((1 - y_1) * (1 - Y)).sum()
FN = ((1 - y_1) * Y).sum()
TP_2 = (y_2 * Y).sum()
FP_2 = (y_2 * (1 - Y)).sum()
TN_2 = ((1 - y_2) * (1 - Y)).sum()
FN_2 = ((1 - y_2) * Y).sum()
df = pd.DataFrame({"a(x) = 1": [TP, FP], "a(x) = 0": [FN, TN]}, index=["y=1", "y=0"])
df
输出:
df_2 = pd.DataFrame({"a(x) = 1": [TP_2, FP_2], "a(x) = 0": [FN_2, TN_2]}, index=["y=1", "y=0"])
df_2
输出:
现在我们将考虑反映分类质量不同方面的两个指标。
准确率
第一个叫做准确率(Precision)
分类准确率定义如下:
P
r
e
c
(
X
,
a
(
x
)
)
=
T
P
T
P
+
F
P
Prec(X, a(x)) = \frac{TP}{TP + FP}
Prec(X,a(x))=TP+FPTP
换句话说,精确度表明我们因正当理由打扰了多少比例的受邀检查患者。精度越高,患者受到的干扰就越少。准确度表示分类器分配给类别 1 的对象实际上属于该组的置信度。
召回率
第二个指标是召回率
R
e
c
(
X
,
a
(
x
)
)
=
T
P
T
P
+
F
N
Rec(X, a(x)) = \frac{TP}{TP + FN}
Rec(X,a(x))=TP+FNTP
完整性向我们展示了我们实际邀请了多少名需要我们帮助的患者。回忆表明我们有信心第 1 类物体不会从我们的视线中消失。
让我们计算一下本例中的准确率和召回率
precision_1 = TP / (TP + FP + 1e-5)
precision_2 = TP_2 / (TP_2 + FP_2 + 1e-5)
recall_1 = TP / (TP + FN)
recall_2 = TP_2 / (TP_2 + FN_2 + 1e-5)
print(f"Accuracy of smart algorithm: {precision_1}")
print(f"Accuracy of the constant algorithm: {precision_2}")
print(f"Completeness of the smart algorithm: {recall_1}")
print(f"Completeness of the constant algorithm: {recall_2}")
输出:
Accuracy of smart algorithm: 0.2857142653061239
Accuracy of the constant algorithm: 0.0
Completeness of the smart algorithm: 0.8
Completeness of the constant algorithm: 0.0
如果我们有一个恒定的算法,但该算法认为每个病人都生病了,那该怎么办?
classifier_3 = lambda x: np.ones_like(x[:,0]) # 恒定预测
y_3 = classifier_3(X)
TP_3 = (y_3 * Y).sum()
FP_3 = (y_3 * (1 - Y)).sum()
TN_3 = ((1 - y_3) * (1 - Y)).sum()
FN_3 = ((1 - y_3) * Y).sum()
precision_3 = TP_3 / (TP_3 + FP_3 + 1e-5)
recall_3 = TP_3 / (TP_3 + FN_3 + 1e-5)
print(f'Accuracy of the panic algorithm: {precision_3}')
print(f'Completeness of the panic algorithm: {recall_3}')
输出:
Accuracy of the panic algorithm: 0.0499999995
Completeness of the panic algorithm: 0.99999980000004
我们需要在这两个指标之间做出妥协
F1 测量
F1 测量(F1-measure)
F1 测量尝试将这两个指标合二为一,遵循以下愿望:
-
在同样的 r e c a l l recall recall下, p r e c i s i o n precision precision越高的算法,其 f 1 f1 f1值应该越高。
-
在同样的 p r e c i s i o n precision precision条件下, r e c a l l recall recall越高的算法,其 f 1 f1 f1值应该越高。
-
对于在最大化 p r e c i s i o n precision precision和 r e c a l l recall recall的同时保持两者平衡的算法, f 1 f1 f1应该更大。
p r e c i s i o n precision precision 和 r e c a l l recall recall 算法的调和平均值完美地满足了这些要求: f 1 = 2 p r e c i s i o n ∗ r e c a l l ( p r e c i s i o n + r e c a l l ) f1 = 2\frac{precision*recall}{(precision + recall)} f1=2(precision+recall)precision∗recall
f1 = lambda prec, recall: 2 * prec * recall / (prec + recall + 1e-5)
print(f'f1 smart algorithm: {f1(precision_1, recall_1)}')
print(f'f1 constant algorithm: {f1(precision_2, recall_2)}')
print(f'f1 of the panic algorithm: {f1(precision_3, recall_3)}')
输出:
f1 smart algorithm: 0.4210487313377906
f1 constant algorithm: 0.0
f1 of the panic algorithm: 0.09523718640304021
当然,我们不必每次都手写这些指标。我们可以从“sklearn”获取它们
from sklearn.metrics import precision_score, recall_score, f1_score
print(f'f1 smart algorithm: {f1_score(y_1, Y)}')
print(f'f1 constant algorithm: {f1_score(y_2, Y)}')
print(f'f1 of the panic algorithm: {f1_score(y_3, Y)}')
输出:
f1 smart algorithm: 0.42105263157894735
f1 constant algorithm: 0.0
f1 of the panic algorithm: 0.09523809523809523
软分类质量指标
软分类是一种分类方法,其中算法返回给我们的不是明确的解决方案(即类 1 或 0),而是对象属于类 1 的概率(如逻辑回归的情况)。
在这种情况下,我们不仅要考虑答案的正确性,还要考虑模型对这些答案的信心。
令 b ( x i ) b(x_i) b(xi) 为对象 x i x_i xi 属于第 1 类的估计值。我们如何对该对象进行分类?
我们将这个估计值 b ( x i ) b(x_i) b(xi) 与值 0.5 进行比较,即 a ( x i ) = [ b ( x i ) > 0.5 ] a(x_i) = [b(x_i) >0.5] a(xi)=[b(xi)>0.5]
在这种情况下选择值 0.5 是因为我们将 b ( x i ) b(x_i) b(xi) 视为一个概率。让我们回想一下,当我们谈论 S 型函数(即逻辑回归算法的输出)时,我们说我们可以将 S 型函数的输出解释为一个概率,但没有人保证它就是一个概率。由此,我们可以假设 0.5 不一定是我们与算法的输出进行比较的最佳值。
也就是说,在一般情况下,决策算法如下所示: a t ( x i ) = [ b ( x i ) > t ] a_t(x_i) = [b(x_i) > t] at(xi)=[b(xi)>t] 其中 t t t 是某个预先确定的阈值。
现在让我们有一个样本 X X X= { ( x i , y i ) } i = 1 N \{(x_i,y_i)\}_{i=1}^N {(xi,yi)}i=1N。让我们按照估计值 b ( x ) b(x) b(x) 的升序排列对象 x ( 1 ) . . . x ( N ) x_{(1)} ... x_{(N)} x(1)...x(N)。
b ( x ( 1 ) ) ≤ . . . ≤ b ( x ( N ) ) b(x_{(1)}) \leq ... \leq b(x_{(N)}) b(x(1))≤...≤b(x(N))
我们将所有这些值作为分类阈值
t
1
=
b
(
x
(
1
)
)
t_1 = b(x_{(1)})
t1=b(x(1))
.
.
.
...
...
t
N
=
b
(
x
(
N
)
)
t_N = b(x_{(N)})
tN=b(x(N))
让我们考虑一组分类器 a t 1 ( x ) . . . a t N ( x ) a_{t_1}(x) ... a_{t_N}(x) at1(x)...atN(x),它们具有上面指出的分类阈值和相同的函数 b ( x ) b(x) b(x)。
PR曲线
对于每个阈值 t i t_i ti,我们计算算法 a t i ( x ) a_{t_i}(x) ati(x) 的 p r e c i s o n precison precison 和 r e c a l l recall recall。然后每个阈值 t i t_i ti 将对应于 p r e c i s i o n − r e c a l l precision-recall precision−recall 坐标中的一个点。
让我们标记所有这些点并按顺序连接它们,得到一条分段线性曲线(学校意义上的断裂)。
# 让我们自己构建一个 PR 曲线并计算其下的面积,并使用 sklearn 中的函数
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import precision_score, recall_score, f1_score
X, y = load_breast_cancer(return_X_y = True)
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
sc = StandardScaler()
sc.fit(X_train)
X_train, X_test = sc.transform(X_train), sc.transform(X_test)
LR = LogisticRegression().fit(X_train, y_train)
让我们导出测试样本中每个对象属于类别“1”的概率表,以及按类别划分的实际分布。
import pandas as pd
import numpy as np
probas = LR.predict_proba(X_test)[:, 1]
infotable = pd.DataFrame({'proba': probas, 'y_true': y_test})
infotable
输出:
现在让我们看看在这个集合中有多少个属于类“1”的对象的唯一概率。
set(infotable.proba)
输出:取了个中间段
相当多。每个概率都需要作为阈值进行尝试。
for prob in set(infotable.proba):
infotable[f"predicted_threshold_{prob}"] = (infotable.proba.values >= prob).astype(int)
infotable
输出:
对于每个阈值,我们计算
p
r
e
c
i
s
i
o
n
precision
precision和
r
e
c
a
l
l
recall
recall。
precisions = [
precision_score(infotable["y_true"], infotable[f"predicted_threshold_{prob}"])
for prob in set(infotable.proba)
]
recalls = [
recall_score(infotable["y_true"], infotable[f"predicted_threshold_{prob}"])
for prob in set(infotable.proba)
]
prec_rec_table = pd.DataFrame({"precision": precisions, "recall": recalls})
prec_rec_table = prec_rec_table.sort_values(
by=["recall", "precision"], ascending=[True, False]
)
prec_rec_table
输出:
太好了,现在我们可以在平面上绘制这些点。
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme()
plt.figure(figsize=(15, 12))
plt.plot(prec_rec_table.recall, prec_rec_table.precision)
plt.scatter(prec_rec_table.recall, prec_rec_table.precision, c="r", s=10)
输出:
在该图中,用点标记了在不同分类阈值下得到的
p
r
e
c
i
s
i
o
n
precision
precision和
r
e
c
a
l
l
recall
recall的值。我们可以看到,它们中的大多数完全不适合解决这个问题,因为它们在一个方向上产生了明显的偏差,但在这里我们看到,有可能通过固定大多数阈值的第二个指标来改进其中一个指标,以及这些参数的一些相当好的组合(在右上角)。正是这些良好组合的存在,决定了该PR曲线下的面积相当大。
通过使用“sklearn”中的函数,可以更简单地完成所有这些操作:
from sklearn.metrics import precision_recall_curve, roc_curve
precision, recall, thresholds = precision_recall_curve(y_test, probas)
plt.figure(figsize=(15, 12))
plt.plot(recall, precision)
plt.scatter(recall, precision, c="r", s=10)
plt.gca().set_aspect("equal")
plt.xlim([0.0, 1.1])
plt.ylim([0.65, 1.1])
plt.show()
输出:
请注意,这种曲线下的面积明显小于“合理”算法曲线下的面积。我们知道,这并不是巧合。
让我们尝试通过这个指标比较两种算法。让我们训练“KNNClassifier”。
现在让我们计算相似曲线的面积,但现在针对测试样本。我们将使用函数 sklearn.metrics.auc
来实现这一点,该函数允许我们通过节点计算折线的面积,我们还将使用函数 sklearn.metrics.precision_recall_curve
,这样就不用再次自己组合
p
r
e
c
i
s
i
o
n
precision
precision 和
r
e
c
a
l
l
recall
recall。
# 让我们计算 pr_auc
from sklearn.metrics import auc, precision_recall_curve
KNN = KNeighborsClassifier().fit(X_train, y_train)
knn_scores = KNN.predict_proba(X_test)[:, 1]
precision, recall, thresholds = precision_recall_curve(y_test, knn_scores)
knn_auc_pr = auc(recall, precision)
lr_scores = LR.predict_proba(X_test)[:, 1]
precision, recall, thresholds = precision_recall_curve(y_test, lr_scores)
lr_auc_pr = auc(recall, precision)
print(f"KNN auc pr = {knn_auc_pr}")
print(f"LR auc pr = {lr_auc_pr}")
输出:
KNN auc pr = 0.9866993077310161
LR auc pr = 0.9984125182621637
很容易验证该曲线具有以下性质:
- 曲线的左点始终为 (0,1)
- 如果样本完全可分离,则曲线将经过点 (1,1)
- 曲线下面积越大越好
因此,PR曲线下的面积(Precision-Recall)可以作为软分类质量的指标。它被称为AUC_PR。
ROC 曲线
我们先来介绍一下真阳性率和假阳性率的概念:
-
假阳性率
(
F
P
R
)
假阳性率 (FPR)
假阳性率(FPR)
F P R = F P F P + T N FPR = \frac{FP}{FP + TN} FPR=FP+TNFP
该值的含义是遇到“0”类对象错误分类的概率。
-
真阳性率(
T
P
R
)
真阳性率(TPR)
真阳性率(TPR)
T P R = T P T P + F N TPR = \frac{TP}{TP + FN} TPR=TP+FNTP
这个值的含义是遇到正确分类为“1”类的对象的概率。
现在让我们定义一条类似于 PR 曲线的曲线,但现在采用 T P R − F P R TPR-FPR TPR−FPR 坐标。
这条曲线被称为ROC曲线(接收器操作特性)。
import matplotlib.pyplot as plt
from sklearn.metrics import roc_auc_score, roc_curve
# 获取测试数据的预测概率
predictions_lr = LR.predict_proba(X_test)
predictions_knn = KNN.predict_proba(X_test)
plt.figure(figsize=(10, 7))
# 确定 LR 模型的 ROC 曲线
fpr_lr, tpr_lr, thresholds_lr = roc_curve(y_test, predictions_lr[:, 1])
plt.plot(fpr_lr, tpr_lr, color="darkorange", lw=3, label="ROC-curve for LR")
# 定义 KNN 模型的 ROC 曲线
fpr_knn, tpr_knn, _ = roc_curve(y_test, predictions_knn[:, 1])
plt.plot(fpr_knn, tpr_knn, color="blue", lw=3, label="ROC-curve for KNN")
# 添加随机算法的ROC曲线
fpr_random, tpr_random, _ = roc_curve(
y_test, np.random.uniform(0, 1, y_test.shape[0])
)
plt.plot(fpr_random, tpr_random, color="green", lw=3, label="ROC-curve for Random")
# 填充 ROC 曲线下的区域
plt.fill_between(fpr_lr, tpr_lr, color="papayawhip")
plt.fill_between(fpr_knn, tpr_knn, color="aquamarine", alpha=0.5)
plt.plot([0, 1], [0, 1], color="navy", lw=1, linestyle="--")
# 设置图表轴和边框
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.0])
# 添加轴标签和标题
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.title("Receiver Operating Characteristic")
# 添加图例
plt.legend()
# 显示图表
plt.show()
输出:
from sklearn.metrics import RocCurveDisplay
RocCurveDisplay.from_estimator(LR, X_test, y_test)
RocCurveDisplay.from_estimator(KNN, X_test, y_test)
输出:
您可以使用函数sklearn.metrics.roc_auc_score
计算roc_auc
from sklearn.metrics import roc_auc_score
knn_preds = KNN.predict(X_test)
lr_preds = LR.predict(X_test)
auc_lr = roc_auc_score(y_test, lr_preds)
print(f"AUC score for LR model: {auc_lr}")
输出:AUC score for LR model: 0.9801406192179597
auc_knn = roc_auc_score(y_test, knn_preds)
print(f"AUC score for KNN model: {auc_knn}")
输出:AUC score for KNN model: 0.9536203281115087
特性:
- 左点 (0,0)
- 右点 (1,1)
- 对于完全可分离的样本,曲线将穿过(0,1)
- 曲线下面积越大越好
- 随机猜测的情况下,曲线下面积为 0.5
与ROC曲线下面积相对应的质量指标称为AUC-ROC。
多类分类
到目前为止,我们主要考虑了二元分类问题。同时,这种限制是相当强的。目前,我们甚至无法解决经典鸢尾花数据集(Fischer’s Irises)的分类问题,也无法评估分类质量,尽管我们已经遇到过很多次了。
让我们对多类分类问题的想法进行概括。
多类分类问题的解决是基于二分类问题的重复解决。有两种方法可以解决此问题。
让我们使用已知的 Fisher Iris 数据集的示例来考虑这个问题。
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
import pandas as pd
import seaborn as sns
sns.set_theme()
# 加载数据
iris = load_iris()
data = pd.DataFrame(iris.data, columns=iris.feature_names)
data["species"] = iris.target
# 创建 StandardScaler 实例
sc = StandardScaler()
# 我们将数据分为训练和测试样本
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, shuffle=True)
# 标准化数据
X_train = sc.fit_transform(X_train)
X_test = sc.transform(X_test)
# 使用pairplot进行数据可视化
sns.pairplot(data=data, hue="species")
输出:
我们可以看出,这个问题是一个典型的多类分类问题。
One VS All
在这种情况下,手头有 N N N 个类,我们将构建 N N N 个分类器 a 1 . . . a N a_1 ... a_N a1...aN,每个分类器将解决二元分类问题,预测一个对象是否属于第 i i i 个类。
在这种情况下,我们对僵化的分类算法并不满意,因为在这种情况下,当几个分类器预测某个对象属于它们的类别时,我们不清楚该怎么做。
如果我们正在使用的算法产生属于第 i i i 个类的概率,我们可以选择所提出的概率中的最大值。
在“一对多”方法中,我们必须在整个样本(显然存在类别不平衡)上训练 N N N 个分类器。
在“一对多”方法中,我们将训练 3 个分类器,以预测一个对象是否属于这 3 个类别中的每一个。
y_0_train = (y_train == 0).astype(int)
y_1_train = (y_train == 1).astype(int)
y_2_train = (y_train == 2).astype(int)
y_0_test = (y_test == 0).astype(int)
y_1_test = (y_test == 1).astype(int)
y_2_test = (y_test == 2).astype(int)
LR_0 = LogisticRegression().fit(X_train, y_0_train)
LR_1 = LogisticRegression().fit(X_train, y_1_train)
LR_2 = LogisticRegression().fit(X_train, y_2_train)
因此,我们训练了 3 个分类器。 LR_0
预测某个物体属于类 0,LR_1
预测该物体属于类 1,LR_2
预测该物体属于类 2。
现在让我们使用这些分类器来决定任意对象最终属于这三个类别之一。
x = X_test[0]
x
输出:
array([1.22403303, 0.05310022, 0.91485647, 1.16218368])
# 第一个分类器的响应
p_0 = LR_0.predict_proba([x])[0][1]
p_0
输出:
np.float64(0.06128449143298947)
# 第二个分类器的响应
p_1 = LR_1.predict_proba([x])[0][1]
p_1
输出:
np.float64(0.23439331039793057)
# 第三个分类器的答案
p_2 = LR_2.predict_proba([x])[0][1]
p_2
输出:
np.float64(0.8858534247270644)
让我们选择属于“x”概率最大的类
prediction = np.argmax([p_0, p_1, p_2])
prediction
输出:
np.int64(2)
让我们看看分类是否正确
y_test[0] == prediction
输出:
np.True_
让我们检查整个测试样本
p_0 = LR_0.predict_proba(X_test)[:,1]
p_1 = LR_1.predict_proba(X_test)[:,1]
p_2 = LR_2.predict_proba(X_test)[:,1]
probas = np.stack([p_0, p_1, p_2])
preds = np.argmax(probas, axis=0)
preds
输出:
array([2, 1, 0, 2, 0, 1, 1, 0, 2, 1, 0, 0, 1, 2, 1, 1, 0, 0, 1, 2, 2, 0,
0, 1, 1, 2, 1, 1, 2, 0, 2, 1, 0, 2, 2, 1, 0, 2, 0, 1, 2, 1, 1, 1,
1])
accuracy_score(preds, y_test)
输出:0.9777777777777777
All VS All
在这种情况下,对于每一对类别,我们训练自己的分类器,它返回一个对象属于其中一个类别的概率。将会有 C N 2 = N ( N − 1 ) 2 C_N^2 = \frac{N(N-1)}{2} CN2=2N(N−1) ~ N 2 N^2 N2 个这样的分类器。作为这种算法的结果,我们将返回对某个对象进行分类的总体概率最大的类。
在“全部对抗”方法中,我们必须在样本的一小部分上训练~ N 2 N^2 N2 个分类器。
在 Fisher 鸢尾花的具体例子中,我们再次需要 3 个分类器来分别对类对
[
0
−
1
]
[0-1]
[0−1]、
[
1
−
2
]
[1-2]
[1−2]、
[
0
−
2
]
[0-2]
[0−2]
# 对 0-1
# 在这里我们丢弃所有 2 类的对象
x_train_01 = X_train[y_train!=2]
y_train_01 = y_train[y_train!=2]
# 对 0-2
# 在这里我们丢弃所有 1 类的对象
x_train_02 = X_train[y_train!=1]
y_train_02 = y_train[y_train!=1]
# 对 1-2
# 在这里我们丢弃所有 0 类的对象
x_train_12 = X_train[y_train!=0]
y_train_12 = y_train[y_train!=0]
# 我们训练 3 个模型
LR_01 = LogisticRegression().fit(x_train_01, y_train_01)
LR_02 = LogisticRegression().fit(x_train_02, y_train_02)
LR_12 = LogisticRegression().fit(x_train_12, y_train_12)
让我们引入以下符号:p_ij 是对象属于类 i 的第 j 个概率。
p_01, p_11 = LR_01.predict_proba([x])[0]
p_01
输出:np.float64(0.0019602440131075705)
p_11
输出:np.float64(0.9980397559868924)
p_02, p_21 = LR_02.predict_proba([x])[0]
p_02
输出:np.float64(0.009483745071467542)
p_21
输出:np.float64(0.9905162549285325)
p_12, p_22 = LR_12.predict_proba([x])[0]
p_12
输出:np.float64(0.11430672335057235)
p_22
输出:np.float64(0.8856932766494277)
那么该对象属于第 0 类的总程度为:
p_01 + p_02
输出:
np.float64(0.011443989084575112)
第 1 类:
p_11 + p_12
输出:np.float64(1.1123464793374649)
第 2 类:
p_21 + p_22
输出:np.float64(1.87620953157796)
让我们对测试样本中的所有对象执行相同的操作。
P0 = LR_01.predict_proba(X_test)
p_01, p_11 = P0[:, 0], P0[:, 1]
P1 = LR_02.predict_proba(X_test)
p_02, p_21 = P1[:, 0], P1[:, 1]
P2 = LR_12.predict_proba(X_test)
p_12, p_22 = P2[:, 0], P2[:, 1]
Prob_0 = p_01 + p_02
Prob_1 = p_11 + p_12
Prob_2 = p_21 + p_22
Preds = np.argmax(np.stack([Prob_0, Prob_1, Prob_2]),axis=0)
Preds
输出:
array([2, 1, 0, 2, 0, 1, 1, 0, 1, 1, 0, 0, 1, 2, 1, 1, 0, 0, 1, 2, 2, 0,
0, 1, 1, 2, 1, 1, 2, 0, 2, 1, 0, 2, 1, 1, 0, 2, 0, 1, 2, 1, 1, 1,
1])
accuracy_score(Preds, y_test)
输出:0.9777777777777777
多类分类质量评估
很明显,我们现在无法获得 p r e c i s i o n precision precision 和 r e c a l l recall recall,因为我们得到的不是 2x2 误差矩阵,而是一个维度为 N × N N \times N N×N 的矩阵,其中 N N N 是类别的数量。
然而,如果我们考虑将 N N N 个类与其他类分离的问题,正如我们已经发现的那样,每个这样的问题都将是一个二分类问题,并且对于每个问题,我们都可以计算其自己的 p r e c i s i o n precision precision 和 r e c a l l recall recall。
那么我们又有两条路径。
微平均
对于 N N N 个任务中的每一个,我们计算 p r e c i s i o n precision precision 和 r e c a l l recall recall,取平均值,并根据平均值计算最终指标 (roc_auc 或 pr_auc)。
让我们看一个例子:
假设我们有下表,其中列出了 3 个类别的 p r e c i s i o n precision precision 和 r e c a l l recall recall。
import pandas as pd
table = pd.DataFrame({'precision': [0.4, 0.6, 0.8], 'recall': [0.5, 0.7, 0.6]})
table
输出:
微平均建议采用以下行为策略来计算平均 f1 测量值(以及任何其他指标):
mean_prec, mean_rec = table.precision.mean(), table.recall.mean()
mean_f1 = 2*(mean_prec*mean_rec)/(mean_prec + mean_rec)
mean_f1
输出:np.float64(0.6)
宏观平均
对于每个任务,我们计算 p r e c i s i o n precision precision、 r e c a l l recall recall和最终指标。然后我们将对最终指标的值进行平均。
尽管这两种方法有相似之处,但差异可能很大。
table['f1'] = 2*(table.precision*table.recall)/(table.precision + table.recall)
table
输出:
我们将按如下方式计算最终值 f 1 f1 f1:
table.f1.mean()
输出:np.float64(0.5921041921041922)
示例
import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
# 加载数据
X, y = load_iris(return_X_y=True)
# 分成训练和测试样本
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.33, random_state=42
)
# 特征标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 训练逻辑回归模型
lr_model = LogisticRegression(random_state=42)
lr_model.fit(X_train_scaled, y_train)
输出:
y_score = lr_model.predict_proba(X_test_scaled)
rac_1 = roc_auc_score(y_test, y_score, multi_class='ovo', average='macro')
print(f'Roc Auc to LR, macro-avereging = {rac_1}')
输出:Roc Auc to LR, macro-avereging = 1.0
关于测量回归质量的一些补充
在之前的讲座中我们已经讨论过一些衡量回归质量的方法。具体来说,我们讨论了确定构建的回归模型质量的相当自然的方法——MSE、MAE、RMSE。今天,我们将在此列表中添加一些内容,并讨论评估回归质量的最流行指标之一 - R 2 R^2 R2
判定系数,或所谓的 r 平方。
需要这个指标来估计回归模型解释的预测随机变量的方差比例。
假设我们有一些函数,我们需要学习预测它的值。我们称之为 z ( x ) z(x) z(x)。假设有一个回归模型,可以一定精度地预测 z ( x ) z(x) z(x) 的值。我们将回归模型称为 f ( x ) f(x) f(x)。当然,也存在一些预测误差。我们将其称为 ϵ ( x ) ϵ(x) ϵ(x)。让我们省略参数并写出以下等式:
z = f + ϵ z=f+ϵ z=f+ϵ
最后一个条目可以被视为随机变量 f f f 和 ϵ ϵ ϵ 的条目。我们假设平均误差为零。错误本身通常被认为是从正态分布中选择出来的。
就随机变量而言, f f f 和 ϵ ϵ ϵ 是独立的假设是相当合理的。
在这种假设的情况下,根据概率论的规则,我们可以写出方差的相应等式:
D z = D f + D ϵ Dz=Df+Dϵ Dz=Df+Dϵ
我们对以下值感兴趣:
R 2 = D f D z = D z − D ϵ D z = 1 − D ϵ D z R^2 = \frac{Df}{Dz} = \frac{Dz - Dϵ}{Dz} = 1 - \frac{Dϵ}{Dz} R2=DzDf=DzDz−Dϵ=1−DzDϵ
回顾随机变量方差的定义,以及数理统计学中方差样本估计的实用方法,很容易写出以下表达式:
- 可以使用以下公式计算模型误差方差的估计值
D ϵ = 1 N ∑ i = 1 N ( f ( x i ) − y i ) 2 Dϵ = \frac{1}{N}∑\limits_{i=1}^N(f(x_i) - y_i)^2 Dϵ=N1i=1∑N(f(xi)−yi)2
- 可以使用以下公式获得随机变量 y 方差的估计值
D z = 1 N ∑ i = 1 N ( y i − y i ˉ ) 2 Dz = \frac{1}{N}∑\limits_{i=1}^N(y_i - \bar{y_i})^2 Dz=N1i=1∑N(yi−yiˉ)2 其中 y i ˉ \bar{y_i} yiˉ 是所有 y i y_i yi 的平均值
因此,我们得到 R 2 R^2 R2 的最终符号:
R 2 = 1 − ∑ i = 1 N ( f ( x i ) − y i ) 2 ∑ i = 1 N ( y i − y i ˉ ) 2 R^2 = 1 - \frac{∑\limits_{i=1}^N(f(x_i) - y_i)^2}{∑\limits_{i=1}^N(y_i - \bar{y_i})^2} R2=1−i=1∑N(yi−yiˉ)2i=1∑N(f(xi)−yi)2
注意:被减去的分数的值可以有不同的解释。请注意,这个分数的分子(假设分子和分母都除以数字 N N N,在这种情况下已经减少)包含 M S E MSE MSE。分母是我们正在预测的因随机变量的方差估计值。也就是说,从本质上讲,我们得到了一个标准化的均方根误差。然后我们从 1 中减去这个值。
R 2 R^2 R2 是需要最大化的值。
其中:
为了描述样本的分散性,使用样本方差(或简称方差)的公式:
D [ x ] = 1 N − 1 ∑ i = 1 N ( x i − x ‾ ) 2 D[x]=\frac{1}{N-1}\sum_{i=1}^{N}(x_i-\overline{x})^2 D[x]=N−11i=1∑N(xi−x)2
它显示样本值与平均值的差异有多大。
这个值越大,原始样本的数值越多样化。
为了说明,我们可以使用一个有目标的例子,射手试图击中靶心(我们将目标的中心视为数学期望 - 平均值)。
因此,散布度越小,箭偏离目标中心的距离就越小,射手的命中率就越高:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import r2_score
# 生成两个具有不同解释方差的数据集
np.random.seed(42)
n = 100
y_target1 = np.random.randn(n) # + np.random.randn(n)
y_predicted1 = y_target1 + np.random.randn(n)/5
y_target2 = np.random.randn(n)
y_predicted2 = y_target2 + np.random.randn(n)/2
# 计算两个数据集的 R^2
r2_1 = r2_score(y_target1, y_predicted1)
r2_2 = r2_score(y_target2, y_predicted2)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# 第一个子图
ax1.scatter(y_target1, y_predicted1, alpha=0.5)
ax1.plot([y_target1.min(), y_target1.max()], [y_target1.min(), y_target1.max()], 'r--', lw=2)
ax1.set_title(f'Dataset 1\nR²: {r2_1:.2f}')
ax1.set_xlabel('y_target')
ax1.set_ylabel('y_predicted')
# 第二个子图
ax2.scatter(y_target2, y_predicted2, alpha=0.5)
ax2.plot([y_target2.min(), y_target2.max()], [y_target2.min(), y_target2.max()], 'g--', lw=2)
ax2.set_title(f'Dataset 2\nR²: {r2_2:.2f}')
ax2.set_xlabel('y_target')
ax2.set_ylabel('y_predicted')
ax1.set_xlim(-3, 3)
ax1.set_ylim(-3, 3)
# 设置第二个子图的坐标轴
ax2.set_xlim(-3, 3)
ax2.set_ylim(-3, 3)
plt.tight_layout()
plt.show()
输出: