跳至内容
4/30第 4 章,共 30 章

分类、交叉熵,以及如何不被自己骗倒

用第 2 章的损失与第 3 章的下降构建 logistic 分类器,并看清为何 98% 准确率也可能什么都没找到。

本页内容

一个模型对传送带上下来的每个零件都回答 这个零件没问题,它有 98.15% 的时间是对的。它也毫无价值:测试集里的 74 个缺陷件,它一个都没抓到。

这两句话描述的是同一个模型。它们之间的距离,就是本章要讲的内容。

前半部分构建分类器。它几乎不需要新东西:第 2 章给出了从“数据如何产生”的假设推导出损失函数的配方,第 3 章给出了沿着这个配方交给你的任意损失往下走的机器。把两者用到一个是/否问题上,logistic 回归就自然出现了,外加一个新概念 —— logit —— 它会在第 17 章再次收费。

后半部分更难。从课程的这一点往后,一切都由某个人测出来的数字来评判;如果你分不清真正的改进和测量伪影,那么后面的每一章都只是装饰。所以:混淆矩阵、精确率和召回率、三份数据切分、泄漏,以及那个几乎没人诚实回答的问题 —— 我到底需要多少测试样本?

这里的算术跑在 20,000 行数据上,所以全程都向量化 —— NumPy 从第 2 章起就在干活,从这里开始也不值得再特别说明。

还是第 1 章的那家工厂,但问题更难。现在不再问 接收还是拒收,而是问 这个零件是否有缺陷 —— 缺陷很少见,这让本章的测量部分很难,也让建模部分显得具有欺骗性的简单。

belt.pyPYTHON
import numpy as np

rng = np.random.default_rng(4)
N = 20_000
width  = rng.normal(22.0, 0.9, N)      # millimetres
weight = rng.normal(57.0, 3.0, N)      # grams

z_true = -5.90 + 1.90 * (width - 22.0) + 0.42 * (weight - 57.0)
y = (rng.random(N) < 1 / (1 + np.exp(-z_true))).astype(float)

perm = rng.permutation(N)
train, val, test = perm[:12_000], perm[12_000:16_000], perm[16_000:]
TEXT
N = 20000  defects = 337  base rate = 0.0169
defects per split = 203 60 74

三份切分,不是两份。原因值得单独讲一节,下面就会讲;现在先在第一份上训练,在第二份上调参,不要看第三份。

特征经过标准化 —— 减去均值,再除以标准差 —— 而且只使用训练集统计量。原因就是第 1 章用感知机的收敛界展示过的:未居中的数据会让几何形状变得不友好。你被允许从哪些行里计算这个均值,会在本章后面变成一个真实问题。

感知机返回一个符号。符号无法区分 拒收拒收,但只是差一点,而这正是工厂决定哪些零件应该优先由人工复检时需要的差异。

所以照着第 2 章的配方来。写下你声称标签如何产生,取似然,取对数,取负数,你就得到了一个损失。对于是/否结果,这个声称是一个 Bernoulli 分布:零件有一个概率 pp 是缺陷件,并且

P(yp)=py(1p)1yP(y \mid p) = p^{\,y}\,(1-p)^{\,1-y}

这只是把“如果 y=1y = 1 就是 pp,如果 y=0y = 0 就是 1p1-p”写得紧凑一些。对它取对数再取负,一个样本的损失就是

L=[ylogp+(1y)log(1p)]L = -\big[\,y \log p + (1 - y)\log(1 - p)\,\big]

这就是二元交叉熵。它不是因为方便才被选中;它是一次硬币投掷唯一可能分布的负对数似然。没有别的选择。

还缺的是 pp 从哪里来。模型计算一个加权和 s=wx+bs = \mathbf{w}\cdot\mathbf{x} + b,这是一个实数,范围覆盖整条实数轴,而概率必须落在 (0,1)(0,1) 中。负责在两者之间转换的函数是 logistic sigmoid

σ(s)=11+es\sigma(s) = \frac{1}{1 + e^{-s}}
TEXT
logit -4.0  ->  p = 0.0180        loss when y=1 and p=0.9  : 0.1054
logit -1.0  ->  p = 0.2689        loss when y=1 and p=0.5  : 0.6931
logit  0.0  ->  p = 0.5000        loss when y=1 and p=0.01 : 4.6052
logit  4.0  ->  p = 0.9820

把右栏读成价格表。以 90% 置信度答对,成本是 0.105。拒绝表态,成本是 0.693 —— 也就是 log2\log 2,耸肩的价格。自信地答错,成本是 4.6,高出四十四倍;而且随着模型对错误越来越确定,价格会无限上升。交叉熵不只是数错误:它会向傲慢收费。

第 3 章说过:要训练任何东西,就求损失对每个参数的导数。对一个样本来做。令 s=wx+bs = \mathbf{w}\cdot\mathbf{x} + bp=σ(s)p = \sigma(s)

Ls=py,Lw=(py)x,Lb=py\frac{\partial L}{\partial s} = p - y, \qquad \frac{\partial L}{\partial \mathbf{w}} = (p - y)\,\mathbf{x}, \qquad \frac{\partial L}{\partial b} = p - y
查看详情

让混乱相消的两行。 sigmoid 有一个异常好用的导数,σ(s)=σ(s)(1σ(s))=p(1p)\sigma'(s) = \sigma(s)\,(1 - \sigma(s)) = p(1-p)。而损失求导得到

Lp=yp+1y1p=pyp(1p)\frac{\partial L}{\partial p} = -\frac{y}{p} + \frac{1-y}{1-p} = \frac{p - y}{p\,(1-p)}

按链式法则把两者相乘,p(1p)p(1-p) 在分子和分母各出现一次。它们完全相消,留下的是 pyp - y。这种相消不是巧合 —— 只要损失是某个分布的负对数似然,且输出函数正是该分布自然使用的函数,就会发生这种事。这一配对有个名字 —— 广义线性模型 —— 而整洁的 gradient 就是它的指纹。1

所以更新就是预测减真值,再乘以输入。没有别的。下面是完整训练器,也就是第 3 章的 descent,只改了一行:

logistic.pyPYTHON
def sigmoid(z):
    return np.where(z >= 0, 1.0 / (1.0 + np.exp(-z)),
                    np.exp(np.minimum(z, 0)) / (1.0 + np.exp(np.minimum(z, 0))))


def fit_logistic(X, y, lr=0.5, epochs=4000):
    w, b = np.zeros(X.shape[1]), 0.0
    for _ in range(epochs):
        p = sigmoid(X @ w + b)
        g = p - y                        
        w -= lr * (X.T @ g) / len(y)     
        b -= lr * g.sum() / len(y)       
    return w, b

sigmoid 里的 np.where 不是装饰。直接计算 1/(1+es)1/(1+e^{-s}) 会在很大的负 ss 上溢出;这个分支会选择代数上等价、但能让指数保持为负的形式。这是第 2 章的浮点数盒子第一次讨债,两节之后它还会收一笔更大的。

为什么不用平方误差,以及为什么答案关乎 gradient

链接到此部分:为什么不用平方误差,以及为什么答案关乎 gradient

偏好交叉熵而不是平方误差的标准解释,是上面的似然论证:平方误差来自高斯噪声假设,而标签不是高斯的,所以不要用。这个解释是对的,但说服不了任何人,因为你完全可以在 sigmoid 上写 L=(py)2L = (p - y)^2,它也会训练。

真正有说服力的论证在 gradient。把平方误差放在 sigmoid 上,链式法则给出

Ls=2(py)p(1p)\frac{\partial L}{\partial s} = 2\,(p - y)\,p\,(1-p)

额外的这个 p(1p)p(1-p),就是前面相消掉的那个。现在它没有相消,并且只要模型很自信,它就会趋近于零 —— 包括模型自信地错了的时候。对一个真实标签为 1 的样本,在几个分数上评估两者:

分数 sspp交叉熵 L/s\partial L/\partial s平方误差 L/s\partial L/\partial s比值
8-80.0003350.999665-0.9996650.000670-0.0006701,491
4-40.0179860.982014-0.9820140.034690-0.03469028.3
2-20.1192030.880797-0.8807970.184956-0.1849564.8
000.5000000.500000-0.5000000.250000-0.2500002.0
+2+20.8807970.119203-0.1192030.025031-0.0250314.8

s=8s = -8 处,模型已经错到不能再错,而平方误差给出的 gradient 比交叉熵小 1,491 倍。错误越严重,模型从中学到的越少。相反,交叉熵的 gradient 会在 1-1 处饱和:最大程度地错误产生最大程度的信号,但不会更大。

跑一场比赛。两千个平衡样本,相同的初始权重,而且这些权重被选成自信地错误(w=[6,6]\mathbf{w} = [-6, -6]),相同的学习率,只有损失不同。两次运行都用交叉熵评分,所以列之间可比。

epoch交叉熵损失准确率平方误差损失准确率
15.48650.23005.94990.2290
101.55250.24605.90420.2290
500.46420.77805.69130.2320
1000.46390.77705.39550.2410
2000.46390.77704.63110.2745
5000.46390.77700.52910.7660
1,0000.46390.77700.46400.7765

交叉熵到 epoch 50 就完成了。平方误差在 epoch 100 仍然只有 24% 准确率 —— 而且从 epoch 10 的 23% 起几乎没动 —— 比猜还差,因为它一开始就自信地错了,而本该拯救它的 gradient 被乘上了 0.0007。它大约到 epoch 500 才逃出来,并最终落到同一个位置。所以诚实的总结是:sigmoid 上的平方误差并非不正确;它是恰好在速度最重要的地方很慢。在一个双参数模型上,你损失 450 个 epoch。在一个百层网络里,总会有某个地方的某个单元自信地错着,于是你损失的是整次训练。

三个量,在第 8 章讲困惑度时需要正确使用,在第 11 章讲让 fine-tuned policy 靠近其 reference 的惩罚时也需要。它们比名声中听起来容易。2

Entropy 是:如果你为一个分布使用可能的最佳编码,那么传递从该分布抽出的一次结果,平均需要花多少比特:

H(p)=ipilog2piH(p) = -\sum_i p_i \log_2 p_i

交叉熵 是:当数据实际来自 pp,但你使用为 qq 构建的编码时,要花多少:

H(p,q)=ipilog2qiH(p, q) = -\sum_i p_i \log_2 q_i

KL 散度 是多出来的部分 —— 因为相信 qq 而真相是 pp 所造成的浪费,单位是比特:

DKL(pq)=H(p,q)H(p)D_{\mathrm{KL}}(p \parallel q) = H(p,q) - H(p)

在传送带上检查这三个量:

TEXT
test defect rate                                = 0.0185
entropy of that coin                            = 0.1329 bits
cross-entropy of the constant predictor on test = 0.1330 bits
KL(test coin || fair coin)                      = 0.8671 bits
H + KL                                          = 1.0000 bits
cross-entropy of the p=0.5 predictor on test    = 1.0000 bits

这里能看到两件事。第一,一个只报告训练集基准率 1.69% 的模型,取得了 0.1330 bit 的交叉熵,几乎正好等于测试标签的 entropy —— 也必然如此,因为它有正确的分布,却没有关于个体的其他信息。Entropy 是“对个体一无所知”能买到的下限。第二,一个耸耸肩说 0.5 的模型,正好支付 1 bit;两者之间的差距 0.8671 bit,正是 KL 散度。H+DKL=H(p,q)H + D_{\mathrm{KL}} = H(p,q) 不是一个要背的恒等式;它是一张你能看着逐项累加起来的账单。

再连回训练:当标签是某个已知的单一类别时,“真实”分布是 one-hot,它的 entropy 为零,因此交叉熵等于 KL 散度。最小化交叉熵与把模型分布拉向真相,是同一件事。

不止两个答案:softmax,以及不花代价的平移

链接到此部分:不止两个答案:softmax,以及不花代价的平移

缺陷不是一种东西。在注塑中,一个零件可能是 short shot(材料不足)、flash(材料过多,被挤出模具),或 burn。四种结果,所以有四个 logits,它们必须变成四个加起来为一的概率。这就是 softmax

softmax(z)i=ezijezj\operatorname{softmax}(\mathbf{z})_i = \frac{e^{z_i}}{\sum_j e^{z_j}}

它有一个看起来像意外、其实是整个实现核心的性质:

softmax(z+c)=softmax(z)\operatorname{softmax}(\mathbf{z} + c) = \operatorname{softmax}(\mathbf{z})

对任意常数 cc 都成立,因为 ezi+c=ecezie^{z_i + c} = e^{c} e^{z_i}ece^c 在上下相消。只有 logits 之间的差异才有意义。绝对水平不是信息。

幸好如此,因为绝对水平正是会让计算机坏掉的东西:

TEXT
logits            = [800. 801. 799.]
naive softmax     = [nan nan nan]
shifted by -max   = [0.2447 0.6652 0.09  ]
same softmax after adding 1000 to every logit: True

e800e^{800} 会让 64 位浮点数溢出,和变成无穷大,而无穷大除以无穷大是 nan —— 不是错误,不是崩溃,只是三个概率原本所在的位置变成了一个静默的洞。减去最大 logit 在数学上不改变任何东西,但在数值上改变一切,因为最大的指数会正好变成 e0=1e^0 = 1。这是第 2 章的 logsumexp 技巧穿上工装,所有严肃实现都会这么做:

softmax.pyPYTHON
def softmax(Z):
    Z = Z - Z.max(axis=1, keepdims=True)   
    E = np.exp(Z)
    return E / E.sum(axis=1, keepdims=True)


def fit_softmax(X, Y, lr=1.0, epochs=6000):
    W, b = np.zeros((X.shape[1], Y.shape[1])), np.zeros(Y.shape[1])
    for _ in range(epochs):
        G = (softmax(X @ W + b) - Y) / len(X)   
        W -= lr * (X.T @ G)
        b -= lr * G.sum(0)
    return W, b

gradient 又一次是预测减真值,只不过现在 YY 是 one-hot。二元情形原来一直只是一个特例。

在 3,000 个零件上训练、1,000 个零件上测试,每个零件有三项测量(宽度、重量、熔体温度),它达到 94.00% 准确率。下面是这个数字藏起来的东西:

真实 ↓ / 预测 →okshort shotflashburn召回率
ok8505900.984
short shot2221000.488
flash2003010.588
burn300390.929
精确率0.9500.8080.7690.975

模型找到的 short shot 不到一半。准确率看不见这一点,因为 86% 的零件是好的,把这些做对就足以撑起平均值。Macro F1 —— 各类别 F1 分数的平均值,它对稀有类别和常见类别赋予同等权重 —— 是 0.7983,而 micro F1 是 0.9400,按定义它与准确率相同。只要有人报告一个 F1 数字,就问是哪一种。

建模部分到这里结束。剩下的章节都在讲数字。

取训练好的二元模型,再把每个 logit 乘以一个常数,做出两个变体:0.35 表示犹豫版,4 表示过度自信版。乘以正数不会改变任何符号,所以三个模型对全部 4,000 个测试零件预测出完全相同的标签。准确率无法区分它们。交叉熵却毫不费力:

模型准确率交叉熵答对时的平均损失答错时的平均损失最大单项损失
犹豫版(logits × 0.35)0.98300.15490.13691.19902.80
按训练结果0.98300.05640.01472.46897.82
过度自信版(logits × 4)0.98300.15630.00099.142727.63

犹豫的模型在每个零件上都交一点小税,包括它答对的数千个零件。过度自信的模型答对时几乎免费,答错时灾难性昂贵 —— 测试集里有一个零件单独就让它付出 27.63 nats。两者从相反路径抵达几乎相同的总数,而经过训练、概率与数据校准的模型,比两者都低三倍。

这是说明损失指标差异最锋利的方式。损失是你优化的东西:它必须可微,并且能看到模型说出的全部内容,包括它有多确定。指标是你被评判的东西:它可以是阶跃函数、业务规则、漏检缺陷数。它们不是同一个对象,也不总是一致 —— 所以你要在开始前定义两者,绝不要因为损失恰好显示在屏幕上,就让它代替指标。

在任何模型之前,先问要求:最懒的答案能得多少分? 在这条传送带上,就是永远说没问题

TEXT
always-say-fine baseline: accuracy = 0.9815
confusion (tn, fp, fn, tp) = (3926, 0, 74, 0)

98.15%。现在看训练好的 logistic 模型,在默认阈值 0.5 下:

TEXT
logistic @0.5: accuracy=0.9830 precision=0.8000 recall=0.1081 F1=0.1905
confusion (tn, fp, fn, tp) = (3924, 2, 66, 8)

98.30%。它比 baseline 高了 0.15 个百分点,任何只停在准确率的报告都会称之为胜利。混淆矩阵说明实际发生了什么:

预测没问题预测有缺陷
实际没问题3,9242
实际有缺陷668

它在 74 个缺陷件里找到了 8 个,放过了 66 个。有三个数字对应这张表的三种读法:

  • 精确率 =TP/(TP+FP)=8/10=0.800= \mathrm{TP}/(\mathrm{TP}+\mathrm{FP}) = 8/10 = 0.800。它标记的零件中,有多少真的有缺陷。这是浪费检查的成本。
  • 召回率 =TP/(TP+FN)=8/74=0.108= \mathrm{TP}/(\mathrm{TP}+\mathrm{FN}) = 8/74 = 0.108。缺陷件中,它抓到了多少。这是把坏零件发给客户的成本。
  • F1 =2PR/(P+R)=0.190= 2PR/(P+R) = 0.190,两者的调和平均,会贴近较小的那个,因此拒绝被单独一个好看的数字奉承。

哪个重要取决于工厂,而不是数学:一次检查只花几秒,一个发出去的缺陷件却可能带来召回通知,所以这里召回率占主导,0.108 是失败。

但模型不是问题。阈值才是,而阈值不是模型的一部分 —— 它是在概率之后施加的业务决策。扫描它:

阈值TPFPFN准确率精确率召回率F1
0.50082660.98300.8000.1080.190
0.2002728470.98120.4910.3650.419
0.10042118320.96250.2630.5680.359
0.05054236200.93600.1860.7300.297
0.0206757070.85580.1050.9050.188
0.005711,36030.65930.0500.9590.094

向下读准确率这一列。它一路下降 —— 从 98.30% 到 65.93% —— 同时模型从抓到 8 个缺陷变成抓到 74 个中的 71 个。这个模型能做的每一件有用的事,都会让准确率变差。 一个优化标题数字的团队,会发布那个什么都找不到的版本。

查看详情

类别加权不会创造信号,它只会移动操作点。 面对类别不平衡,通常的第一反应是在损失中给稀有类别加权。对正例分别使用权重 1、10 和 60:

正例权重准确率精确率召回率F1AUC
10.98300.8000.1080.1900.9363
100.96050.2530.5810.3520.9361
600.82900.0910.9190.1660.9361

精确率和召回率移动了很远。AUC —— 模型把一个随机缺陷件排在一个随机好件之上的概率,它完全忽略阈值 —— 只移动了 0.0002,等于没动。重新加权只是把同一个模型沿着同一条权衡曲线滑动。这通常正是你想要的,但它从来不是新信息:如果排序本身很差,没有任何加权方案能救它。

三份切分,以及你马上要发现的泄漏

链接到此部分:三份切分,以及你马上要发现的泄漏

为什么是三份切分而不是两份?因为你一旦使用一组样本来选择任何东西 —— 阈值、学习率、六个模型里要发布哪个 —— 这组样本就已经被用于拟合,它的分数就不再无偏。3 在这条传送带上测量:在验证集上扫描阈值选出 0.196,然后模型在未触碰的测试集上得到 F1 = 0.4122。如果直接在测试集上扫描,那里能达到的最佳值是 0.4186 —— 这是任何人都无权报告的数字。

这里的差距很小,0.006,因为这只是一个超参数,在 4,000 个验证样本上扫描了一次。每多做一个决定、验证集每缩小一点,它都会增大。还要注意,单次运行里的方向并不保证:被选中的阈值在验证集上得 0.3902,在测试集上得 0.4122,所以这一次验证集反而低估了它。偏差是在许多决策上的系统现象,不一定在一次中可见。4

现在来练习。传送带日志带来了第三列,station_seconds:每个零件在检查站停留了多久。把它加入预处理只需要改一行。效果如下:

模型准确率精确率召回率F1交叉熵AUC
宽度 + 重量0.98300.8000.1080.1900.05640.9363
+ station_seconds0.99200.7920.7700.7810.02360.9970

召回率从 10.8% 到 77.0%。F1 提高了四倍多。还要注意准确率做了什么:98.30% → 99.20%,提升 0.9 个点,这种数字在总结幻灯片里很容易被四舍五入成“两边都差不多 99%”。准确率之前没能看出失败,现在也没能看出欺诈。

继续读之前:模型在作弊。找出它怎么作弊。

如何追查泄漏,按最快找到它的顺序。

  1. 比较训练和测试。 过拟合会表现为很大的差距。这里:诚实模型 0.9838 train / 0.9830 test;泄漏模型 0.9936 train / 0.9920 test。两个差距都低于 0.2 个点。泄漏看起来不像过拟合 —— 泄漏特征在测试时同样可用,所以模型能漂亮地泛化到一个并不存在的世界。

  2. 每次只用一个特征单独训练一个模型。 任何携带答案的东西都会自己跳出来:

    单独特征准确率召回率F1AUC
    width0.98150.0140.0260.8691
    weight0.98150.0000.0000.7914
    station_seconds0.98500.4050.5000.9960

    只有一列,单独使用时就能以 AUC 0.9960 排序缺陷件。卡尺和秤测出的两个量只能做到 0.87 和 0.79。这种不对称就是警报。

  3. 问每个数字是什么时候写下来的。 平均停留时间:通过的零件 2.23 秒,失败的零件 15.56 秒。 当然如此。零件停在检查站,是因为检查员把它从传送带上拿了下来 —— 这发生在之后,而且只因为,有人已经判定它有缺陷。这一列不是零件的测量值。它是判决的测量值。

the planted leakPYTHON
station = 1.8 + rng.exponential(0.35, N)                     # a part just passing through
audited = rng.random(N) < 0.006                              # random spot checks
station[audited] += rng.uniform(6.0, 26.0, audited.sum())
station[y == 1] = 9.0 + rng.exponential(7.0, (y == 1).sum())  

高亮的那一行就是泄漏:缺陷件的停留时间来自不同分布,因为人工把它从传送带上拿走了。这是应用机器学习中最常见的严重 bug,而且它有名字:target leakage —— 训练特征中包含了在必须做出预测的时刻并不可用的信息。5 它不会抛异常。它会产生更好的数字。项目里的每一种激励都会指向把它留下来。

防御方法是对每一列都问一个问题:在我需要这个预测的那一刻,这个值已经存在了吗? 在一条实时传送带上,station_seconds 要等到零件被检查之后才知道 —— 而这正是模型本该替代的事情。

假设你在 20 个样本上给模型打分,它答对 17 个。你报告 85%。

TEXT
17 correct out of 20 -> accuracy 0.8500
  Wilson    95% CI : [0.6396, 0.9476]
  bootstrap 95% CI : [0.7000, 1.0000]
  P(a 65% model scores 17 or more out of 20) = 0.0444
  P(an 85% model scores 17 or more out of 20) = 0.6477

对 17/20 的诚实解读是:大概在 64% 到 95% 之间。一个真实水平为 65% 的模型,有 4.4% 的概率产生这个结果 —— 二十三次里大约一次 —— 如果你试了几个 prompts 然后报告最好的那个,你就是自己制造了这次运行。二十个里对十七个,无法区分一个 85% 的模型和一个 65% 的模型。

有两种给比例加区间的方法,两者都应该在你的工具箱里:

uncertainty.pyPYTHON
def wilson(k, n, z=1.959963985):
    """95% interval for k successes in n trials. Correct at small n; no simulation."""
    ph, d = k / n, 1 + z * z / n
    centre = (ph + z * z / (2 * n)) / d
    half = z * (ph * (1 - ph) / n + z * z / (4 * n * n)) ** 0.5 / d
    return centre - half, centre + half


def bootstrap_ci(correct, n_resamples=10_000, alpha=0.05, seed=0):
    """95% interval for the mean of any per-example score array. Works on F1 too."""
    rng = np.random.default_rng(seed)
    correct = np.asarray(correct, dtype=float)
    draws = correct[rng.integers(0, len(correct), size=(n_resamples, len(correct)))]
    lo, hi = np.quantile(draws.mean(axis=1), [alpha / 2, 1 - alpha / 2])
    return float(correct.mean()), float(lo), float(hi)

对普通成功率使用 Wilson6;它在任意 nn 下都表现良好,而且不需要随机性。注意上面当 n=20n = 20 时,bootstrap 的上端是 1.0000 —— 对 20 个点重采样,很容易抽到 20 个全对,因此它无法表示比自身粒度更窄的区间。在没有公式的地方使用 bootstrap7,而这包括大多数有趣的情况:F1、macro 平均、BLEU、pass@1、基于 rubric 的 judge 分数。在这条传送带上,调好阈值的模型 F1 为 0.4122,对应的 bootstrap 区间是 [0.3009, 0.5156] —— 这才应该出现在报告里,因为单独的点估计会邀请一种它无法支持的比较。

再做一次测量,因为它会改变你比较两个模型的方式。两个模型在同样的 500 个样本上打分:

TEXT
model A: 0.8580  95% CI [0.8260, 0.8880]
model B: 0.8120  95% CI [0.7780, 0.8460]
the two intervals overlap: True
paired difference A-B: 0.0460  95% CI [0.0260, 0.0680]
they disagree on 31 of 500 examples (A right 27, B right 4)

它们的区间重叠了,民间规则 —— 误差条重叠就表示没有显著差异 —— 会把这个比较判为没有结论。但这不对。两个模型跑在同一批样本上,所以正确的量是逐样本差异,它的区间是 [0.0260, 0.0680],稳稳高于零。它们只在 500 项中的 31 项上意见不一致,而 A 赢下了其中 27 项;共享的样本,无论容易还是困难,都会相互抵消,而不是增加噪声。成对比较模型,你可以用一小部分数据得出同样结论。

你现在有了一个输出校准概率的模型,一个从数据主张推导而来、不是为了方便而选择的损失,一个字面上就是预测减真值的 gradient,并且 —— 更重要的是 —— 有了判断这一切是否有效的机器。上面的十行 Wilson 区间会原封不动地复用:它承载第 15 章里的 prompt 变体、第 19 章里的检索表,以及第 29 章里的 golden set。没有公式时,就该拿出 bootstrap。

但模型仍然只有一层。它画的是一条线,而第 1 章已经用 XOR 的四行证明过,一条线不够。修复方法是堆叠:第一层弯曲空间,第二层在弯曲后的空间里画线。

本章整洁的 gradient 到这里就用完了。上面所有东西之所以能工作,是因为对于输入和损失之间只有一层的模型,L/s=py\partial L/\partial s = p - y 可以手写出来,而且只要写一次。把第二层放到中间后,问题就变了形:对一个根本不直接接触输出的权重,损失的导数是什么 —— 这个权重的影响只通过另一层传来,甚至可能同时沿着多条路径传来?

这个导数存在。对任何大于玩具的东西,手工计算都没希望;而一次算一个参数,在另一个尺度上也没希望。需要的是一个过程:它能沿着 forward pass 刚刚走过的同一张图,做一次 backward pass,就得到网络中的每一个导数。

这就是第 5 章,也是本课程余下部分运行其上的引擎。


也值得与本章一起阅读:Bishop,Pattern Recognition and Machine Learning §1.2、§1.5、§1.6 和 §4.3,它按本章遵循的顺序覆盖概率、决策理论、信息论和线性分类;Murphy,Probabilistic Machine Learning: An Introduction,第 6 和第 10 章;Prince,Understanding Deep Learning §5.4–5.7;以及 Saito 和 Rehmsmeier,The Precision-Recall Plot Is More Informative than the ROC Plot When Evaluating Binary Classifiers on Imbalanced Datasets(PLOS ONE,2015)—— 当只有 1.7% 的零件有缺陷时,为什么上面引用的 AUC 不应该是你查看的唯一一个无阈值数字。

  1. Ma, T. 和 Ng, A. CS229 Lecture Notes,Stanford University,第 2 和第 3 章。让产生 pyp - y 的相消不再像运气的地方:选择与你的输出匹配的指数族分布,使用它的 canonical link,gradient 就永远是预测减真值。

  2. Olah, C. Visual Information Theory(2015),colah.github.io/posts/2015-09-Visual-Information。这是目前最清晰的讲解之一:把 entropy、交叉熵和 KL 散度解释为比特成本,而不是公式。

  3. Abu-Mostafa, Y. S., Magdon-Ismail, M. 和 Lin, H.-T. Learning From Data(AMLBook,2012),Caltech 课程第 13 和第 17 讲。第 13 讲是验证;第 17 讲讨论三条学习原则,并在其中命名了 data snooping。两者共同构成本章的纪律来源:每一次查看数据集都是一次拟合决策,无论你是否运行了优化器。

  4. James, G., Witten, D., Hastie, T. 和 Tibshirani, R. An Introduction to Statistical Learning,第 2 版(Springer,2021),第 2 和第 5 章,关于 bias–variance 分解和重采样。配套卷明确写出了选择陷阱:Hastie, Tibshirani 和 Friedman,The Elements of Statistical Learning,第 2 版,§7.10.2,The Wrong and Right Way to Do Cross-validation

  5. Kaufman, S., Rosset, S., Perlich, C. 和 Stitelman, O. Leakage in Data Mining: Formulation, Detection, and Avoidance. ACM Transactions on Knowledge Discovery from Data 6(4),2012。对上面演示的失败给出了形式化处理,并包含一些竞赛案例:获胜模型学到的是数据如何组装的伪影。

  6. Wilson, E. B. Probable Inference, the Law of Succession, and Statistical Inference. Journal of the American Statistical Association 22(158),第 209–212 页(1927)。上文 wilson() 使用的 score interval,至今仍是比例的正确默认选择。教科书区间 p^±zp^(1p^)/n\hat{p} \pm z\sqrt{\hat{p}(1-\hat{p})/n} 才是应该避开的那个:它在接近 0 和 1 时会给出荒唐结果,并且在小 nn 下严重 undercovers。

  7. Efron, B. Bootstrap Methods: Another Look at the Jackknife. The Annals of Statistics 7(1),第 1–26 页(1979)。这个思想让你能为任何可计算的统计量加上区间,包括那些没有抽样理论的统计量。

准备好让 LIA 替你选模型了吗?

所有 AI 模型都在一处——今天就免费开始。