多类接收者操作特征 (ROC)#
本示例介绍了使用接收者操作特征 (ROC) 指标来评估多类分类器质量。
ROC 曲线通常在 Y 轴上显示真阳性率 (TPR),在 X 轴上显示假阳性率 (FPR)。这意味着图的左上角是“理想”点 - FPR 为零,TPR 为一。这不太现实,但这确实意味着曲线下面积 (AUC) 越大通常越好。ROC 曲线的“陡峭程度”也很重要,因为理想情况下应该最大化 TPR 并最小化 FPR。
ROC 曲线通常用于二元分类,其中 TPR 和 FPR 可以明确定义。在多类分类的情况下,只有在将输出二值化后才能获得 TPR 或 FPR 的概念。这可以通过两种不同的方式完成
一对多方案将每个类别与所有其他类别(假定为一个)进行比较;
一对一方案比较每个类别的唯一成对组合。
在本示例中,我们探讨了这两种方案,并演示了微平均和宏平均的概念,作为总结多类 ROC 曲线信息的不同方法。
注意
请参阅 使用交叉验证的接收者操作特征 (ROC),了解本示例的扩展,该示例估计了 ROC 曲线的方差及其相应的 AUC。
加载和准备数据#
我们导入了 鸢尾花植物数据集,其中包含 3 个类别,每个类别对应一种鸢尾花植物。一个类别与另外两个类别线性可分;后两个类别彼此之间**不可**线性可分。
在这里,我们将输出二值化并添加噪声特征以使问题更难。
import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
iris = load_iris()
target_names = iris.target_names
X, y = iris.data, iris.target
y = iris.target_names[y]
random_state = np.random.RandomState(0)
n_samples, n_features = X.shape
n_classes = len(np.unique(y))
X = np.concatenate([X, random_state.randn(n_samples, 200 * n_features)], axis=1)
(
X_train,
X_test,
y_train,
y_test,
) = train_test_split(X, y, test_size=0.5, stratify=y, random_state=0)
我们训练了一个 LogisticRegression
模型,该模型可以自然地处理多类问题,这得益于使用多项式公式。
from sklearn.linear_model import LogisticRegression
classifier = LogisticRegression()
y_score = classifier.fit(X_train, y_train).predict_proba(X_test)
一对多多类 ROC#
一对多 (OvR) 多类策略,也称为一对所有,包括为每个 n_classes
计算 ROC 曲线。在每个步骤中,给定类别被视为正类别,其余类别被视为负类别。
注意
不要将用于**评估**多类分类器的 OvR 策略与用于**训练**多类分类器的 OvR 策略混淆,后者通过拟合一组二元分类器来完成(例如,通过 OneVsRestClassifier
元估计器)。OvR ROC 评估可用于仔细检查任何类型的分类模型,无论它们是如何训练的(请参阅 多类和多输出算法)。
在本节中,我们使用 LabelBinarizer
以一对多的方式通过独热编码对目标进行二值化。这意味着形状为 (n_samples
,) 的目标被映射到形状为 (n_samples
, n_classes
) 的目标。
from sklearn.preprocessing import LabelBinarizer
label_binarizer = LabelBinarizer().fit(y_train)
y_onehot_test = label_binarizer.transform(y_test)
y_onehot_test.shape # (n_samples, n_classes)
(75, 3)
我们也可以轻松地检查特定类别的编码
label_binarizer.transform(["virginica"])
array([[0, 0, 1]])
显示特定类别的 ROC 曲线#
在下图中,我们显示了将鸢尾花视为“维吉尼亚”(class_id=2
)或“非维吉尼亚”(其余)时的 ROC 曲线。
class_of_interest = "virginica"
class_id = np.flatnonzero(label_binarizer.classes_ == class_of_interest)[0]
class_id
2
import matplotlib.pyplot as plt
from sklearn.metrics import RocCurveDisplay
display = RocCurveDisplay.from_predictions(
y_onehot_test[:, class_id],
y_score[:, class_id],
name=f"{class_of_interest} vs the rest",
color="darkorange",
plot_chance_level=True,
)
_ = display.ax_.set(
xlabel="False Positive Rate",
ylabel="True Positive Rate",
title="One-vs-Rest ROC curves:\nVirginica vs (Setosa & Versicolor)",
)
使用微平均 OvR 的 ROC 曲线#
微平均聚合了所有类别的贡献(使用 numpy.ravel
)以计算平均指标,如下所示
\(TPR=\frac{\sum_{c}TP_c}{\sum_{c}(TP_c + FN_c)}\) ;
\(FPR=\frac{\sum_{c}FP_c}{\sum_{c}(FP_c + TN_c)}\) .
我们可以简要演示 numpy.ravel
的效果
print(f"y_score:\n{y_score[0:2,:]}")
print()
print(f"y_score.ravel():\n{y_score[0:2,:].ravel()}")
y_score:
[[0.38 0.05 0.57]
[0.07 0.28 0.65]]
y_score.ravel():
[0.38 0.05 0.57 0.07 0.28 0.65]
在类别高度不平衡的多类分类设置中,微平均优于宏平均。在这种情况下,可以替代地使用加权宏平均,这里没有演示。
display = RocCurveDisplay.from_predictions(
y_onehot_test.ravel(),
y_score.ravel(),
name="micro-average OvR",
color="darkorange",
plot_chance_level=True,
)
_ = display.ax_.set(
xlabel="False Positive Rate",
ylabel="True Positive Rate",
title="Micro-averaged One-vs-Rest\nReceiver Operating Characteristic",
)
在主要兴趣不是图而是 ROC-AUC 分数本身的情况下,我们可以使用 roc_auc_score
复制图中显示的值。
from sklearn.metrics import roc_auc_score
micro_roc_auc_ovr = roc_auc_score(
y_test,
y_score,
multi_class="ovr",
average="micro",
)
print(f"Micro-averaged One-vs-Rest ROC AUC score:\n{micro_roc_auc_ovr:.2f}")
Micro-averaged One-vs-Rest ROC AUC score:
0.77
这等效于使用 roc_curve
计算 ROC 曲线,然后使用 auc
为展平的真实类别和预测类别计算曲线下面积。
from sklearn.metrics import auc, roc_curve
# store the fpr, tpr, and roc_auc for all averaging strategies
fpr, tpr, roc_auc = dict(), dict(), dict()
# Compute micro-average ROC curve and ROC area
fpr["micro"], tpr["micro"], _ = roc_curve(y_onehot_test.ravel(), y_score.ravel())
roc_auc["micro"] = auc(fpr["micro"], tpr["micro"])
print(f"Micro-averaged One-vs-Rest ROC AUC score:\n{roc_auc['micro']:.2f}")
Micro-averaged One-vs-Rest ROC AUC score:
0.77
注意
默认情况下,ROC 曲线的计算通过使用线性插值和 McClish 校正 [分析 ROC 曲线的一部分 Med Decis Making. 1989 Jul-Sep; 9(3):190-5.] 在最大假阳性率处添加一个点。
使用 OvR 宏平均的 ROC 曲线#
获得宏平均需要独立计算每个类别的指标,然后对它们进行平均,因此先验地平等对待所有类别。我们首先聚合每个类别的真/假阳性率
for i in range(n_classes):
fpr[i], tpr[i], _ = roc_curve(y_onehot_test[:, i], y_score[:, i])
roc_auc[i] = auc(fpr[i], tpr[i])
fpr_grid = np.linspace(0.0, 1.0, 1000)
# Interpolate all ROC curves at these points
mean_tpr = np.zeros_like(fpr_grid)
for i in range(n_classes):
mean_tpr += np.interp(fpr_grid, fpr[i], tpr[i]) # linear interpolation
# Average it and compute AUC
mean_tpr /= n_classes
fpr["macro"] = fpr_grid
tpr["macro"] = mean_tpr
roc_auc["macro"] = auc(fpr["macro"], tpr["macro"])
print(f"Macro-averaged One-vs-Rest ROC AUC score:\n{roc_auc['macro']:.2f}")
Macro-averaged One-vs-Rest ROC AUC score:
0.78
此计算等效于简单地调用
macro_roc_auc_ovr = roc_auc_score(
y_test,
y_score,
multi_class="ovr",
average="macro",
)
print(f"Macro-averaged One-vs-Rest ROC AUC score:\n{macro_roc_auc_ovr:.2f}")
Macro-averaged One-vs-Rest ROC AUC score:
0.78
将所有 OvR ROC 曲线一起绘制#
from itertools import cycle
fig, ax = plt.subplots(figsize=(6, 6))
plt.plot(
fpr["micro"],
tpr["micro"],
label=f"micro-average ROC curve (AUC = {roc_auc['micro']:.2f})",
color="deeppink",
linestyle=":",
linewidth=4,
)
plt.plot(
fpr["macro"],
tpr["macro"],
label=f"macro-average ROC curve (AUC = {roc_auc['macro']:.2f})",
color="navy",
linestyle=":",
linewidth=4,
)
colors = cycle(["aqua", "darkorange", "cornflowerblue"])
for class_id, color in zip(range(n_classes), colors):
RocCurveDisplay.from_predictions(
y_onehot_test[:, class_id],
y_score[:, class_id],
name=f"ROC curve for {target_names[class_id]}",
color=color,
ax=ax,
plot_chance_level=(class_id == 2),
)
_ = ax.set(
xlabel="False Positive Rate",
ylabel="True Positive Rate",
title="Extension of Receiver Operating Characteristic\nto One-vs-Rest multiclass",
)
一对一多类 ROC#
一对一 (OvO) 多类策略包括为每个类别对拟合一个分类器。由于它需要训练 n_classes
* (n_classes
- 1) / 2 个分类器,因此该方法通常比一对多方法慢,因为它的复杂度为 O(n_classes
^2)。
在本节中,我们演示了使用 OvO 方案针对 鸢尾花植物数据集 中的 3 种可能组合的宏平均 AUC:“山鸢尾”与“变色鸢尾”,“变色鸢尾”与“维吉尼亚”以及“维吉尼亚”与“山鸢尾”。请注意,微平均对于 OvO 方案没有定义。
使用 OvO 宏平均的 ROC 曲线#
在 OvO 方案中,第一步是识别所有可能的唯一对组合。分数的计算是通过将给定对中的一个元素视为正类,另一个元素视为负类来完成的,然后通过反转角色并取两个分数的平均值来重新计算分数。
from itertools import combinations
pair_list = list(combinations(np.unique(y), 2))
print(pair_list)
[('setosa', 'versicolor'), ('setosa', 'virginica'), ('versicolor', 'virginica')]
pair_scores = []
mean_tpr = dict()
for ix, (label_a, label_b) in enumerate(pair_list):
a_mask = y_test == label_a
b_mask = y_test == label_b
ab_mask = np.logical_or(a_mask, b_mask)
a_true = a_mask[ab_mask]
b_true = b_mask[ab_mask]
idx_a = np.flatnonzero(label_binarizer.classes_ == label_a)[0]
idx_b = np.flatnonzero(label_binarizer.classes_ == label_b)[0]
fpr_a, tpr_a, _ = roc_curve(a_true, y_score[ab_mask, idx_a])
fpr_b, tpr_b, _ = roc_curve(b_true, y_score[ab_mask, idx_b])
mean_tpr[ix] = np.zeros_like(fpr_grid)
mean_tpr[ix] += np.interp(fpr_grid, fpr_a, tpr_a)
mean_tpr[ix] += np.interp(fpr_grid, fpr_b, tpr_b)
mean_tpr[ix] /= 2
mean_score = auc(fpr_grid, mean_tpr[ix])
pair_scores.append(mean_score)
fig, ax = plt.subplots(figsize=(6, 6))
plt.plot(
fpr_grid,
mean_tpr[ix],
label=f"Mean {label_a} vs {label_b} (AUC = {mean_score :.2f})",
linestyle=":",
linewidth=4,
)
RocCurveDisplay.from_predictions(
a_true,
y_score[ab_mask, idx_a],
ax=ax,
name=f"{label_a} as positive class",
)
RocCurveDisplay.from_predictions(
b_true,
y_score[ab_mask, idx_b],
ax=ax,
name=f"{label_b} as positive class",
plot_chance_level=True,
)
ax.set(
xlabel="False Positive Rate",
ylabel="True Positive Rate",
title=f"{target_names[idx_a]} vs {label_b} ROC curves",
)
print(f"Macro-averaged One-vs-One ROC AUC score:\n{np.average(pair_scores):.2f}")
Macro-averaged One-vs-One ROC AUC score:
0.78
也可以断言,我们“手动”计算的宏平均值等效于 average="macro"
的实现 roc_auc_score
函数。
macro_roc_auc_ovo = roc_auc_score(
y_test,
y_score,
multi_class="ovo",
average="macro",
)
print(f"Macro-averaged One-vs-One ROC AUC score:\n{macro_roc_auc_ovo:.2f}")
Macro-averaged One-vs-One ROC AUC score:
0.78
将所有 OvO ROC 曲线绘制在一起#
ovo_tpr = np.zeros_like(fpr_grid)
fig, ax = plt.subplots(figsize=(6, 6))
for ix, (label_a, label_b) in enumerate(pair_list):
ovo_tpr += mean_tpr[ix]
ax.plot(
fpr_grid,
mean_tpr[ix],
label=f"Mean {label_a} vs {label_b} (AUC = {pair_scores[ix]:.2f})",
)
ovo_tpr /= sum(1 for pair in enumerate(pair_list))
ax.plot(
fpr_grid,
ovo_tpr,
label=f"One-vs-One macro-average (AUC = {macro_roc_auc_ovo:.2f})",
linestyle=":",
linewidth=4,
)
ax.plot([0, 1], [0, 1], "k--", label="Chance level (AUC = 0.5)")
_ = ax.set(
xlabel="False Positive Rate",
ylabel="True Positive Rate",
title="Extension of Receiver Operating Characteristic\nto One-vs-One multiclass",
aspect="equal",
xlim=(-0.01, 1.01),
ylim=(-0.01, 1.01),
)
我们确认“versicolor”和“virginica”类别不能被线性分类器很好地识别。请注意,“virginica”与其他类别之间的 ROC-AUC 分数(0.77)介于“versicolor”与“virginica”(0.64)和“setosa”与“virginica”(0.90)之间的 OvO ROC-AUC 分数之间。实际上,OvO 策略提供了有关一对类别之间混淆的额外信息,但当类别数量很大时,会以计算成本为代价。
如果用户主要对正确识别特定类别或类别子集感兴趣,则建议使用 OvO 策略,而评估分类器的全局性能仍然可以通过给定的平均策略进行总结。
由于计数被合并,因此微平均 OvR ROC 受更频繁的类别支配。宏平均的替代方案更好地反映了较不频繁类别的统计数据,因此当所有类别的性能被认为同等重要时,它更合适。
脚本的总运行时间:(0 分钟 0.795 秒)
相关示例