2. ROC 曲线基本概念:TPR 与 FPR 到底在说什么?
好,咱们进入正题。ROC 曲线这名字听起来挺学术,但说白了,它就是一张帮你判断「这个阈值设得值不值得」的二维地图。很多新手拿到测试数据集就急着调阈值,结果调了半天,假阳性一堆,真阳性也丢了——我当年就干过这种事。
想用好 ROC 曲线,你首先得搞清楚两个核心指标:真正例率(True Positive Rate,TPR)和假正例率(False Positive Rate,FPR)。这俩家伙就像天平的两端,你调阈值的时候,它们会相互拉扯。
2.1 从混淆矩阵说起
别急,我们先看一眼混淆矩阵。你手里有一组质控样本,模型对每个样本都会输出一个预测分数。你设一个阈值,分数高于阈值就算阳性(异常),低于就算阴性(正常)。然后你拿预测结果和真实标签一对比,就会得到四个格子:
- TP(真阳性):实际阳性,你预测为阳性
- FP(假阳性):实际阴性,你预测为阳性
- FN(假阴性):实际阳性,你预测为阴性
- TN(真阴性):实际阴性,你预测为阴性
你想想看,这四个数字直接决定了你的质控效果。我最开始做质控时只盯着准确率,结果忽略了假阳性,导致生产线上一堆误报,工程师天天抱怨。后来才悟到,你要看的不是绝对数字,而是比率。
2.2 TPR —— 你抓住了多少真正的异常?
TPR 的计算公式很简单:
TPR = TP / (TP + FN)
TP + FN 是所有真实阳性样本的总数。TPR 反映的是:在所有的真实异常中,你成功检测出了多少。换句话说,TPR 越高,漏检越少。
我在一个医学影像质控项目里遇到过这种情况:模型 TPR 达到 0.99,但依然有 1‰ 的漏检。甲方要求必须零漏检,怎么办?只能把阈值压得特别低,结果假阳性爆表。嗯,那时候我就开始认真研究 ROC 曲线了。
2.3 FPR —— 你冤枉了多少正常样本?
FPR 的公式长这样:
FPR = FP / (FP + TN)
FP + TN 是所有真实阴性样本的总数。FPR 体现的是:在所有的正常样本里,有多少被你错误地标记成了异常。FPR 越低,误报越少。
千万别小看 FPR。假设你的阴性样本有 100 万批,FPR 哪怕只有 0.01,也会产生 100 个假阳性。每个假阳性都要人工复核,成本够你喝一壶的。
2.4 我们为什么要把 TPR 和 FPR 放在一张图上?
因为单一阈值下,你只能看到一个 TPR 和 FPR 的组合点。但阈值从 0 滑到 1 时,你会得到一连串的点——把这些点连起来,就是 ROC 曲线。它的横轴是 FPR,纵轴是 TPR。
- 横轴(FPR):误报率,越往左越好
- 纵轴(TPR):抓取率,越往上越好
- 对角线:表示完全随机猜,毫无区分能力
一个优秀的分类器,ROC 曲线会疯狂向左上角凸起。如果曲线贴着对角线,那你的模型基本等于抛硬币。我以前用过一个特征堆叠的模型,画出来 ROC 曲线几乎贴着对角线,气得我当场删代码重构。
2.5 一个简单的计算例子
假设你有 5 个样本,真实标签和预测分数如下:
| 样本 | 真实标签 | 预测分数 |
|---|---|---|
| A | 阳性 | 0.92 |
| B | 阳性 | 0.76 |
| C | 阴性 | 0.63 |
| D | 阴性 | 0.38 |
| E | 阳性 | 0.25 |
设定阈值 0.7,得分 ≥ 0.7 预测为阳性。那么:
- TP:A 和 B → 2
- FP:C(阴性但分数≥0.7)→ 1
- FN:E(阳性但分数<0.7)→ 1
- TN:D → 1
计算:
TPR = 2 / (2+1) = 0.667
FPR = 1 / (1+1) = 0.5
把这个点标在坐标系上,就是 (0.5, 0.667)。换一个阈值,再算一个点,最终连线。
在 Python 里你可以直接写:
import numpy as np
y_true = [1, 1, 0, 0, 1]
y_score = [0.92, 0.76, 0.63, 0.38, 0.25]
thresholds = np.linspace(0, 1, 100)
tpr_list = []
fpr_list = []
for th in thresholds:
pred = [1 if s >= th else 0 for s in y_score]
tp = sum((p == 1) and (t == 1) for p, t in zip(pred, y_true))
fp = sum((p == 1) and (t == 0) for p, t in zip(pred, y_true))
fn = sum((p == 0) and (t == 1) for p, t in zip(pred, y_true))
tn = sum((p == 0) and (t == 0) for p, t in zip(pred, y_true))
tpr_list.append(tp / (tp + fn) if (tp+fn) > 0 else 0)
fpr_list.append(fp / (fp + tn) if (fp+tn) > 0 else 0)
嗯,这段代码虽然能跑,但效率不高。生产中我通常用 sklearn.metrics.roc_curve,不过为了理解原理,手动算一遍更有感觉。
2.6 知识体系图:从混淆矩阵到 ROC 曲线
下面这张图帮你把整个逻辑串起来,看一眼就能记住核心关系。
2.7 容易踩的坑
2.8 总结一下
TPR 和 FPR 是 ROC 曲线的左右腿。理解它们,你才能看懂曲线为什么往左上角弯,为什么 AUC 是 0.5 代表随机。
| 指标 | 公式 | 含义 |
|---|---|---|
| TPR | TP / (TP + FN) | 真正例率,越高漏检越少 |
| FPR | FP / (FP + TN) | 假正例率,越低误报越少 |
有了这俩概念,下一张图你就可以动手画 ROC 曲线了。不过话不多说,先消化今天的知识——拿你手头的数据跑一遍混淆矩阵,手动算几个 TPR 和 FPR,感受一下阈值变化带来的 trade-off。