衡量一个模型 · 人工智能基础
你的模型准确率 95%。它好用吗?
课程目录为什么需要它
一个分数就可能掩盖一个没用的模型。如果你关心的情况(比如欺诈或者疾病)只占很少一部分,一个永远回答「不是」的模型看起来非常出色,却一个也抓不到。选对衡量标准,并在没见过的数据上检查它,你才能分清真正有用的模型和只是看起来不错的模型。
直觉理解
假设 100 笔交易里有 5 笔是欺诈。一个永远预测「不是欺诈」的模型对了 95 次:准确率 95%,抓到的欺诈:0 笔。
把结果分进四个格子,这就是混淆矩阵(1 = 欺诈,即「正类」):
| 预测为 1 | 预测为 0 | |
|---|---|---|
| 实际为 1 | 真正例 (TP) | 假负例 (FN) |
| 实际为 0 | 假正例 (FP) | 真负例 (TN) |
然后有两个问题很重要:
- 精确率 = TP / (TP + FP):我们标记出来的里面,有多少是真的?
- 召回率 = TP / (TP + FN):真的那些里面,我们抓到了多少?
它们互相拉扯。把所有东西都标记出来,召回率是完美的,精确率却很糟。F1 = 2 × P × R / (P + R) 把两者合成一个数,只有两个都高时它才高。
最后是过拟合:模型可能把训练数据连同噪声一起背下来。它的信号是差距很大,比如训练集上 99%,测试集上 60%。
定义
准确率
The share of all predictions that were right. Misleading when one class is rare.所有预测中正确的比例。当某一类很少见时会误导人。
精确率
Of the cases predicted positive, the share that really were: TP / (TP + FP).被预测为正类的里面,真正是正类的比例:TP / (TP + FP)。
召回率
Of the real positive cases, the share the model found: TP / (TP + FN).真正的正类里面,被模型找出来的比例:TP / (TP + FN)。
过拟合
Learning the training data so closely, noise included, that the model does worse on new data.把训练数据(连同噪声)学得太死,导致模型在新数据上表现更差。
示例
准确率陷阱
动手试试永远回答「不是」能拿 95%,却什么也抓不到。
actual = [0] * 95 + [1] * 5
always_no = [0] * 100
correct = sum(1 for a, p in zip(actual, always_no) if a == p)
print("accuracy:", correct / len(actual))
caught = sum(1 for a, p in zip(actual, always_no) if a == 1 and p == 1)
print("fraud caught:", caught, "of", actual.count(1))accuracy: 0.95 fraud caught: 0 of 5
- L195 笔正常交易,5 笔欺诈(编的数据)。
- L4100 个里对了 95 个。
accuracy: 0.95 - L6召回率是 0:一笔欺诈都没找到。
fraud caught: 0 of 5
混淆矩阵
动手试试数出四个格子,再算精确率和召回率。
actual = [1, 1, 1, 0, 0, 0, 0, 1, 0, 0]
predicted = [1, 0, 1, 1, 0, 0, 0, 1, 0, 1]
pairs = list(zip(actual, predicted))
tp = pairs.count((1, 1))
fp = pairs.count((0, 1))
fn = pairs.count((1, 0))
tn = pairs.count((0, 0))
print("TP", tp, "FP", fp, "FN", fn, "TN", tn)
print("precision", tp / (tp + fp))
print("recall", tp / (tp + fn))TP 3 FP 2 FN 1 TN 4 precision 0.6 recall 0.75
- L3每一对是 (实际, 预测)。
- L5(0, 1):我们说是,其实不是。
- L9我们标记的 5 个里有 3 个是真的。
precision 0.6 - L104 个真的里面我们抓到了 3 个。
recall 0.75
一个靠死记的模型
动手试试在训练数据上完美,在新数据上跟抛硬币差不多。
train = {(1, 1): "red", (2, 1): "red", (8, 8): "blue", (9, 8): "blue"}
test = {(1, 2): "red", (8, 9): "blue"}
def memoriser(point):
return train.get(point, "red")
def accuracy(model, data):
return sum(model(p) == label for p, label in data.items()) / len(data)
print("train", accuracy(memoriser, train))
print("test", accuracy(memoriser, test))train 1.0 test 0.5
- L4查表找这个点;如果是新点,就猜红色。
- L10每个训练点都在表里。
train 1.0 - L11(8, 9) 显然是蓝色,可它不在表里。
test 0.5
常见错误
只报告准确率
当某一类很少见时,即使是没用的模型,准确率也很高。要报告你关心的那一类的精确率和召回率。
除以零
一个从不预测正类的模型,TP + FP = 0。事先规定这时精确率是多少(通常是 0),而不是让程序崩溃。
在测试集上调参
如果你试了 50 种设置,留下测试分数最高的那个,测试集就变成了训练数据。调参要用单独的验证集。
小测验
1. 这段代码会输出什么?
actual = [1, 0, 1, 1]
predicted = [1, 1, 0, 1]
tp = sum(1 for a, p in zip(actual, predicted) if a == 1 and p == 1)
fp = sum(1 for a, p in zip(actual, predicted) if a == 0 and p == 1)
print(round(tp / (tp + fp), 2))查看答案
答案:A
TP = 2,FP = 1:精确率 2 / 3 ≈ 0.67。
2. 垃圾邮件过滤器必须几乎从不把正常邮件放进垃圾箱。哪个数最重要?
查看答案
答案:A
精确率问的是:我们判为垃圾邮件的里面,有多少真的是?一个假正例就是一封丢掉的正常邮件。
3. 训练集准确率 99%,测试集准确率 60%。这是怎么回事?
查看答案
答案:A
模型学会的是训练数据本身,而不是规律。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出数格子
准确写出它打印的内容。
- 改需求修正召回率
recall用错了格子。把它改成 TP / (TP + FN)。如果根本没有真正的正类(TP + FN 为 0),返回0.0。 - 从零写完整的报告
写一个
report(actual, predicted)(两个由 0 和 1 组成的列表),返回一个包含"precision"、"recall"和"f1"的字典,每个都四舍五入到 3 位小数。- 分母为 0 的指标都是
0.0 - F1 = 2 × P × R / (P + R),用的是没有四舍五入的 P 和 R
report([1, 1, 1, 0, 0, 0, 0, 1, 0, 0], [1, 0, 1, 1, 0, 0, 0, 1, 0, 1]) # {'precision': 0.6, 'recall': 0.75, 'f1': 0.667} - 分母为 0 的指标都是
本课术语
| 术语 | 中文 | 英文定义 |
|---|---|---|
| Accuracy | 准确率 | The share of predictions that were right. |
| Confusion matrix | 混淆矩阵 | Counts of TP, FP, FN and TN. |
| Precision | 精确率 | TP / (TP + FP): how many flagged cases were real. |
| Recall | 召回率 | TP / (TP + FN): how many real cases were found. |
| Overfitting | 过拟合 | Doing well on training data and badly on new data. |