ROC曲线
statistic
本文字数:2.4k 字 | 阅读时长 ≈ 10 min

ROC曲线

statistic
本文字数:2.4k 字 | 阅读时长 ≈ 10 min

1. 什么是 ROC 曲线

ROC 曲线(Receiver Operating Characteristic Curve)用于评估二分类模型区分正负样本的能力。
以“违规图片识别”为例,模型通常不会直接输出“违规/不违规”,而是输出一个分数,分数越高表示越可能违规。业务再设置一个分类阈值:分数高于阈值就判为“违规”,否则判为“正常”。
随着阈值不断变化,模型的召回能力和误报程度也会变化。ROC 曲线就是把这些不同阈值下的表现画在同一张图上,用来回答一个核心问题:

当我们不断调整分类阈值时,模型为了获得更高召回率,需要付出多少误报率?

因此,ROC 曲线越靠近左上角越好:越靠左表示误报越少,越靠上表示召回越高。

下面是一个roc曲线的例子

对应 Python 代码

import matplotlib.pyplot as plt
import numpy as np
from sklearn.metrics import auc, roc_curve

rng = np.random.default_rng(42)
y_true = rng.integers(0, 2, 1000)
y_score = np.clip(rng.normal(0.3 + 0.4 * y_true, 0.2), 0, 1)

fpr, tpr, thresholds = roc_curve(y_true, y_score)
roc_auc = auc(fpr, tpr)

target_threshold = 0.5
finite_mask = np.isfinite(thresholds)
valid_thresholds = thresholds[finite_mask]
valid_fpr = fpr[finite_mask]
valid_tpr = tpr[finite_mask]
point = np.argmin(np.abs(valid_thresholds - target_threshold))

plt.figure(figsize=(6, 5))
plt.plot(fpr, tpr, label=f"ROC (AUC = {roc_auc:.3f})")
plt.plot([0, 1], [0, 1], "--", label="Random")
plt.scatter(valid_fpr[point], valid_tpr[point], s=70, label=f"Threshold ≈ {target_threshold}")
plt.annotate(
    f"(FPR={valid_fpr[point]:.2f}, TPR={valid_tpr[point]:.2f})",
    (valid_fpr[point], valid_tpr[point]),
    xytext=(10, -15),
    textcoords="offset points"
)

plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.title("ROC Curve")
plt.legend()
plt.grid(alpha=0.3)
plt.savefig("roc_curve_basic.png", dpi=160, bbox_inches="tight")
plt.show()

2. 一个完整例子:违规图片识别

下面通过一个完整的例子来加深理解

假设现在有 10 张图片,其中 5 张确实需要拦截,5 张可以正常放出。模型给每张图片打了一个分数,分数越高表示越可能违规。

图片 模型分数 真实标签
A 0.95 需要拦截
B 0.90 可以放出
C 0.85 需要拦截
D 0.80 需要拦截
E 0.70 可以放出
F 0.60 需要拦截
G 0.55 可以放出
H 0.40 可以放出
I 0.30 需要拦截
J 0.10 可以放出

如果把阈值设为 0.80,那么 A、B、C、D 会被判为“违规”,其余样本判为“正常”。
此时四种结果分别是:

于是:$TPR=\frac{TP}{TP+FN}=\frac{3}{5}=0.6$。$FPR=\frac{FP}{FP+TN}=\frac{1}{5}=0.2$
这说明,在阈值为 0.80 时,模型召回了 60% 的违规图片,同时误伤了 20% 的正常图片。这个阈值在 ROC 平面上就对应一个点:(FPR,TPR)=(0.2,0.6)
样本分数与阈值示意图

对应 Python 代码

import matplotlib.pyplot as plt
import numpy as np

samples = [
    ("A", 0.95, 1),
    ("B", 0.90, 0),
    ("C", 0.85, 1),
    ("D", 0.80, 1),
    ("E", 0.70, 0),
    ("F", 0.60, 1),
    ("G", 0.55, 0),
    ("H", 0.40, 0),
    ("I", 0.30, 1),
    ("J", 0.10, 0),
]

samples = sorted(samples, key=lambda x: x[1], reverse=True)
names = [x[0] for x in samples]
scores = np.array([x[1] for x in samples])
labels = np.array([x[2] for x in samples])  # 1=需要拦截, 0=可以放出
x = np.arange(len(samples))
threshold = 0.80

plt.figure(figsize=(8, 4.5))
plt.scatter(x[labels == 1], scores[labels == 1], s=70, label="Positive: need block")
plt.scatter(x[labels == 0], scores[labels == 0], s=70, marker="x", label="Negative: can pass")
plt.axhline(threshold, linestyle="--", label=f"Threshold = {threshold}")

for i, name in enumerate(names):
    plt.text(x[i], scores[i] + 0.03, name, ha="center", fontsize=9)

plt.xticks(x, names)
plt.ylim(0, 1.05)
plt.xlabel("Samples sorted by score")
plt.ylabel("Model score")
plt.title("Scores and Threshold")
plt.legend()
plt.grid(alpha=0.3)
plt.savefig("score_threshold_example.png", dpi=160, bbox_inches="tight")
plt.show()

3. 从一个点到一条曲线

单个阈值只能得到一个点,而 ROC 曲线是把所有可能阈值下的 (FPR, TPR) 都算出来,再连成一条线。还是上面的例子,当阈值很高时,模型只会拦截少量高分样本,因此误报通常较少,但也容易漏掉真正违规的内容;当阈值逐渐降低时,更多样本会被判为违规,召回率会上升,但误报率也往往会上升。也就是说:

ROC 曲线本质上就是在展示这种权衡关系。下面可以看一下不同阈值下 TPR/FPR 的变化

对应 Python 代码

import matplotlib.pyplot as plt
import numpy as np
from sklearn.metrics import roc_curve

y_true = np.array([1, 0, 1, 1, 0, 1, 0, 0, 1, 0])
y_score = np.array([0.95, 0.90, 0.85, 0.80, 0.70, 0.60, 0.55, 0.40, 0.30, 0.10])

fpr, tpr, thresholds = roc_curve(y_true, y_score)

mask = np.isfinite(thresholds)
thresholds = thresholds[mask]
fpr = fpr[mask]
tpr = tpr[mask]

plt.figure(figsize=(7, 4.5))
plt.plot(thresholds, tpr, marker="o", label="TPR")
plt.plot(thresholds, fpr, marker="o", label="FPR")
plt.gca().invert_xaxis()

plt.xlabel("Threshold (high → low)")
plt.ylabel("Rate")
plt.title("TPR / FPR under Different Thresholds")
plt.legend()
plt.grid(alpha=0.3)
plt.savefig("threshold_vs_tpr_fpr.png", dpi=160, bbox_inches="tight")
plt.show()

4. 什么是 AUC

AUC(Area Under the Curve)是 ROC 曲线下方的面积,用一个数来概括模型整体的排序能力。它的直观含义是:

随机抽取一个正样本和一个负样本,模型给正样本的分数高于负样本的概率。

这里的“随机抽取一个正样本和一个负样本”,可以理解成让它们进行一次分数比赛。这个不太好理解,依然的,我们举一个具体的例子,依然以违规图片为例。假设有 3 张违规图片和 3 张正常图片:

样本 真实情况 模型分数
A 违规 0.90
B 违规 0.85
C 违规 0.50
D 正常 0.80
E 正常 0.40
F 正常 0.20

任意拿一张违规图片和一张正常图片比较,一共有:$3\times3=9$种组合。

最终 9 次比较中,模型排对了 8 次:$AUC=\frac{8}{9}\approx0.89$。这意味着,随机拿一张真实违规图片和一张真实正常图片,模型大约有 89% 的概率给违规图片更高的风险分数。因此,当 AUC = 0.90 时,可以通俗地理解为:随机进行 100 次“正样本和负样本的分数比赛”,模型大约有 90 次能把正样本排在负样本前面。

注意:AUC = 0.90 并不代表 90% 的样本都预测正确,也不代表 90% 的违规图片都能被拦截。即

AUC 表达模型有没有能力让正样本整体排在负样本前面的能力

5. ROC 曲线的局限

ROC 曲线很适合看模型整体的区分能力,但在正负样本极不均衡时,它可能显得过于乐观。

例如,在热点发现里,100 万个候选词中可能只有 100 个真正会成为热点。哪怕 FPR 只有 1%,也仍然可能产生将近 1 万个误报,业务上完全无法处理。
因此,像热点趋势发现、风险拦截这类场景,通常不能只看 ROC/AUC,目前我也不太清楚这块怎么解决的,可能是根据业务来看的。

6. 工程上最容易出错的问题

1. 绘制 roc 曲线的问题

在绘制 roc 曲线时,标签用 0 还是 1 代表不拦截/拦截并不影响 ROC,一般来讲在分类中,分数大于阈值代表正类,即拦截,小于阈值为负类,即不拦截。

这时候如果分类模型的输出是正好相反,即分数大于阈值代表负类,小于阈值为正类。这时候只需要将TP,TN,FP,FN这些稍微修改一下即可。不耽误正常曲线的绘制

2. 实际业务口径误伤/误杀

FPR 和团队里口头说的“误伤率/误杀率”不一定是同一个口径。ROC 横轴使用的是:$FPR=\frac{FP}{FP+TN}$。它的分母是所有真实负样本。而有些业务里说的“误杀率”其实是:$\frac{FP}{TP+FP}=1-Precision$。它的分母是所有被模型判为正样本的样本。两者分母不同,不能混用。这个和团队统一口径即可。

Mar 13, 2026
ufw
Mar 13, 2026
ufw
Dec 14, 2025