概率 入门

Transformer 真正跑在上面的那部分概率,全部搭在同一张图上:一排高度加起来等于一的柱子。分布、期望与方差、贝叶斯、似然、采样、熵与交叉熵 —— 页面上每一个数字都由旁边那张图算出来,所以你可以把它拖到任何状态,它依然成立。

01

概率是一个份额

一个数,介于 0 和 1 之间。这一页剩下的全部内容,都是围绕它做的算术。

有两种东西都叫概率。一种是频率:把实验重复十万次,然后数。另一种是对只发生一次的事情的信念程度。它们共用一个名字,因为它们服从同样的三条规则;而频率这一种是可以画出来的。那就画:一百个方块,一次实验一个,被填满的那些就是事情真的发生了的那几次:

p = 0.35 —— 100 格中的 35 格

注意,滑块越过最后一格之后就无处可去了。概率只能待在 [0, 1] 里,因为它是某个整体的份额,而份额不可能超过整体:时,右边的读数不再给分数而是写着必然;时,它写着绝无。

频率这个读法附带一句警告。抛一枚公平硬币,正面的实时比例会先游荡一阵,才慢慢落到那个概率上 —— 而且比大多数人以为的慢得多。把抛掷次数往右推,看游荡怎么收窄:

抛了 1 次之后

抛第一次之后比例是 1.000:硬币出了正面,目前为止的全部证据都在说它永远会出正面。要把这个比例拉到 0.570 需要,拉到 0.501 需要 10,000 次。那条带子是一个标准误0.5/√n —— 数据翻四倍,误差才减半。

第三条规则是后面每一节都要靠的。把每个结果当作同一根长条上的一块份额 ——雨、阴、晴正好把它铺满 —— 挪动分界线只会从邻居那里拿走宽度,不会把长条变长:

雨 0.35 · 阴 0.36

看读数的右端:晴从来不是被选出来的,它是被算出来的,1 − 0.35 − 0.36。这就是加起来等于 1 在实践中的含义 —— 其中一个数是不自由的。一份 0.35、0.36、0.40 的预报不是乐观,是账算不平。

几乎没有什么东西一来就加起来等于 1,所以我们除以总和 —— 这一招一直有效,直到失效。把第三个分数拖到零以下:

分数 = 3.0

在 c = 3 时,分数 3、4、3 归一化成 0.30、0.40、0.30,总和是 10。在 时,总和是 6,第三个份额是 −0.17。照样返回三个数,什么都没有报错,而这个形状毫无意义。归一化并不检查任何东西 —— 如果分数全是零,它会一声不吭地返回 NaN。

02

分布是一个形状

每个结果一个数,都不为负,加起来正好是一。

把 §01 那根长条按结果的数量切开,再把每一块立起来当作高度,你就得到了机器学习里每一张图的原型。这里的虚线是公平骰子给每一面的高度 —— 把灌铅量拉上去,看六点怎么从另外五面身上把多出来的概率质量拿走:

p(灌铅面)= 0.17

一边拖一边看读数里的总和:它一动不动。在时,六点是 1.00,另外五面正好是 0 —— 它仍然是一个合法的分布,只是里面已经没有随机性了。这个退化的角落后面还要用到:温度为零、argmax、以及 one-hot 标签,全都是这同一个形状。

模型并不直接吐出概率。它吐出的是 logits —— 每个 token 一个、没有上下界的实数分数 —— softmax 把它们取指数再除以总和。拖动第二个 token 的 logit,看下面那一行跟着走:

z = 1.0

因为 exp 是单调的,上下两行的顺序完全一致,所以 softmax 从不改变 argmax。它改变的是间距。你手里的那个 token 在上下两行同时动; logit 差 1 变成概率之比 e ≈ 2.72,于是第一个 token 和最后一个之间 5 的差距就成了 148 倍 —— 0.577 对 0.0039。

所有真实实现都会先减去最大的那个 logit。它在分子和分母里同时消掉,结果一模一样;换来的好处是:在 float64 里 exp(800) 是Infinity,而 exp(0) 是 1。在 50,257 个 token 的词表上,整趟只是每个 token 一次指数、一次除法,跟算出这些 logits 的那次矩阵乘法比根本不值一提。不过,先把 logits 除以一个温度就是真的改变了 —— 把它往零的方向拉,看最高的那个 token怎么吞掉整行:

T = 1.00

一旦 T 降到 ,最高的 token 精确到三位就是 1.000 —— 那就是贪心解码,也是 API 里 temperature=0 的含义。在 时它跌到 0.278,朝着 1/5 去。温度改变的不是模型相信什么,而是这份信念有多少能活着进入采样。

连续型的结果还需要多一个概念,而这正是最容易绊倒人的那个。密度不是概率;它是每单位 x 上的概率,所以它完全可以大于 1。把钟形曲线收窄,再沿着它拖动那个窗口:

中心 = 0.00
面积 0.383

在 σ = 1 时,峰值读作 0.399,窗口里装着 0.383。在 σ = 0.2 时,峰值是 1.995 —— 任何概率都不允许取的值 —— 而同一个窗口装着 0.988,完全合法。面积才是概率;高度只是它的速率。密度大于 1 不是一个 bug 报告。

03

它落在哪里,又摊开多远

两个数替一整个分布说话:一个平衡点,和一段平方距离。

把所有结果排在一条线上,把每个结果的概率当作重物挂在它上方,整个分布就有了一个平衡点。把概率质量挪到某一面上,看支点沿着横梁跟着走:

E[X] = 3.50

因为每一面都同样可能,公平骰子平衡在 3.50 —— 一个它根本掷不出来的值。这是关于期望要先接受的第一件事:它是一个加权的地址,不是一个结果。把,支点会走到 6.00;同样的灌铅量放在一点上,它会被拖到 1.00。

描述摊开程度需要第二个数,而让它真正管用的是平方。每个结果都要付出它到均值的距离的平方,再按自己的概率加权。把概率质量往两端推,看这笔账怎么涨:

σ² = 2.92

注意,中间那几面即使是最可能的,贡献也几乎为零:离半个点位时,那个平方项是 0.25,而两端是 6.25。这次平方不是装饰。正是它让方差在互相独立的变量之间可以相加,而绝对距离做不到这一点。

平方的代价是单位。方差 4.15的单位是点数的平方,没有人能想象那是什么东西。开一次平方根,这个数就回到了它本该待的那条轴上:

σ = 1.71

在均匀设置下 σ 是 1.71,区间 μ ± σ 覆盖了0.67 的概率质量 —— 六面里的四面。把离散度, σ 涨到 2.38,而被覆盖的质量反而掉到 0.13。 σ 是一把尺子,不是一个容器。

还有一件,因为这是人们真的会犯的错。估计方差时,如果把平方偏差除以 n,答案会偏小。拖动样本量:

1.458

在 n = 2 时,÷n 这个估计量平均给出 1.458,而真值是 2.917 —— 正好一半,而且它从不吭声。在 时是真值的 90.0%,n = 20 时是 95.0%:偏差是 (n−1)/n,会缩小但永远合不拢。除以 n − 1在每一个 n 上都落在那条线上。

样本均值的方差是 σ²/n,标准误又是 §01 那个 1/√n —— 这就是 batch size 的道理。 GPT-3 的训练 batch 是 320 万 token: batch 放大一千倍,梯度只安静了三十二倍。

04

在别的事情已经发生的前提下

条件化就是重新归一化 —— 删掉被证据排除的那些人,再把剩下的重新缩放。

两个事件可以塞进同一个正方形。左右按是否患病切开,上下按检测结果切开,每一块的面积就是一个联合概率。这里的检测能抓到 99% 的患者;拖动那条竖切线来改变这种病有多常见:

患病率 = 10%
患病率 = 10%

在 10% 患病率下,「患病且阳性」那一块是 0.099,「健康却阳性」那一块是 0.090 —— 面积几乎一样,而这个检测在它要找的人身上有 99% 的正确率。健康那一列宽了九倍,所以哪怕它内部的错误率很小,切出来的那条也一样厚。

一个阳性结果会删掉整个阴性行,只留下一小块和一大条。剩下的还不是一个分布 —— 它加起来是 0.189,不是 1 —— 所以我们把它拉伸回去,直到它是。把下面那根长条的右端横着拖过整个舞台:

拉伸了 0.00%
拉伸了 0.00%

注意,你拖动的时候,长条内部的比例从来没变过。P(患病 | +) = P(患病 ∧ +) / P(+)把两部分同时除以同一个数,于是比例被钉死,变的只是「全体」这个词的含义。答案在拖动的两端都读作 0.524。

现在把这种病变罕见 —— 这正是筛查项目在做的事。把检测的两个方向都保持在 99%,从它开局的十分之一起,沿着对数轴把患病率往下拉:

患病率 = 10%

在的患病率下,答案是 0.090。一个 99% 准确的检测,一个阳性结果,而你依然有 91% 的可能是健康的 —— 因为每来一个真阳性,就会来 10.1 个假阳性。平衡点正好落在患病率 上,两个错误率在那里抵消;再往下就是基础率说了算,提高检测精度也改不了这一点,只是把交叉点挪一挪。

这是整页里唯一一个因为「安静」而危险的失效。什么都不会报错,没有哪个数字看起来不对,而同一个检测的 P(+ | 患病) 和 P(患病 | +)分别是 0.99 和 0.09。独立是那个不需要做这些算术的特例 —— 它也有一张图。把两条切线错开:

偏移 = 0.00

偏移为零时,P(A | B) = P(A) = 0.50,正方形就是一张规规矩矩的网格。推到 时,P(A | B) 变成 0.80,而P(A) 一动没动 —— 边缘概率是被构造钉死的,所以两条切线之间那一级台阶就是相依性的全部。独立就是「那级台阶是零」这个断言,没有任何更松的意思。

05

哪个模型让这件事更可能

把问题倒过来问:不是某个参数预测什么,而是哪个参数让我们看到的这一切最可能发生。

假设我们抛了一枚硬币二十次,出了十三次正面。每一个可能的偏向值都会给这个结果分配一个概率,而把数据钉住、只把这些概率当作偏向的函数来看,那条曲线就是似然。沿着横轴拖动偏向:

θ = 0.50
θ = 0.50

看峰顶:它落在 0.65,正好是 13/20 —— 对一枚硬币来说,最大似然估计就是样本频率。在 θ = 0.50 处曲线已经掉到峰值的 0.40 ——「公平」没有被排除,只是被压过。把数据推到,峰顶就跳到 θ = 1.00:一个宣称反面永不出现的模型。

这条曲线有两点不方便:它不是关于偏向的分布;而且才二十次抛掷,它的取值就已经小得可怜。取一次对数:

θ = 0.50
log L = −13.86

因为 log 是单调的,峰顶一动没动:让 L 最大的也让 log L 最大。而现在取值读得出来了 —— 峰顶 −12.95,公平处 −13.86 —— 原始似然则打印成 2.4e−6 和 9.5e−7。

这并不是风格偏好。把互相独立的概率连乘起来,在序列变得有意思之前,乘积就已经跑出了 float64 的范围。取一个语言模型会满意的单 token 概率,把项数从 1 往外推:

1 项

在每个 token p = 0.02 时,乘到是 1.3e−170,到就正好是零 —— float64 最小的次正规数是 4.94e−324。对数之和读作 −747.2,还在往下走。什么都不会报错:乘积变成 0,log 0 变成 −∞,第一个 NaN 出现在下游的梯度里。

把这个对数的符号翻过来,就是每个语言模型训练用的损失:一条曲线,只在一处读取 ——模型给「真正来的下一个 token」的那个概率:

p = 0.500
p = 0.500

在 p = 1 时损失是 0,这是唯一免费的答案。在 0.5 时是 0.693 nat,0.2 时是 1.609,0.002 时是 6.215。损失没有上界,所以「自信而错」会被无限惩罚,而「不确定」很便宜 —— 这个不对称就是训练信号的全部。这也正是为什么:模型给某个 token 分配了正好 0,而那个 token 偏偏出现了,就会得到无穷大的损失和一次报废的训练。

06

从它里面抽一个出来

分布负责描述。采样器负责动手 —— 而它只需要一个随机数就够了。

把这五个概率首尾相接,铺在同一段长度为 1 的线上。向随机数生成器要一个落在 [0, 1) 里的均匀数 u,把它丢到这条带子上,落进哪一块,哪一块就是抽到的 token。拖动那支飞镖:

u = 0.320
u = 0.320

这就是逆变换采样,而且这就是全部算法。块的边界就是累积和 ——飞镖所在那一块被写在带子旁边,0.000 → 0.577 —— 所以在这些边界上做一次二分查找,就能把一个均匀数在 O(log V) 时间里变成一个 token。每一块被命中的频率正好等于它的宽度,而这是采样器唯一需要的性质。

采样同时也是我们测量那些大到没法求和的东西的办法:抽 n 个、数一数、除一除。答案的好坏只取决于 n。把样本数往外推:

10 个样本

由于两条轴都是对数轴,这条直线本身就是那个论断:样本数每爬两个数量级,区间才掉一个数量级。十个样本给 ±0.31,给 ±0.031,±0.01 则要 9,604 个。蒙特卡洛不在乎维数 —— 只在乎 1/√n。

放着不管,采样器迟早会伸手去够尾巴,而语言模型的尾巴基本上都是胡话,所以解码器会把它剪掉。把保留个数往下拉,看被丢掉的概率质量怎么冒出来:

k = 10

在 时保留的质量是 0.913,而一半词表已经没了;在 时是 0.448,而且没什么可抽的了 —— 又是贪心解码,只不过这次从另一边走到。核采样把控制反过来:先说出你想要的那份质量 0.9,再让 k 是多少就是多少 —— 这里是 5。

尾巴到底重不重要,取决于一个人们很少真去算的数。千分之一的 token 是罕见的;而一段生成很长。沿着对数轴把长度往外推:

1 个 token

一个 token 时概率是 0.001,跟宣传的一样。在 个时是 0.394,在 个时是 0.632:只要读数里那个乘积 qn 达到 1,1 − (1−q)ⁿ 就越过了 1 − e⁻¹。「每次抽样很罕见」和「每次生成很罕见」是两个不同的论断,而你的用户碰到的是后一个。

07

平均而言有多意外

熵是平均的意外度。交叉熵是「为错误的事情感到意外」要付的账。

得知一件必然的事发生了,什么信息也没有;得知一件几乎不可能的事发生了,信息量极大。−log p 就是这个想法,外加一套让独立的意外可以相加的算术。沿着横轴拖动概率:

p = 0.500
p = 0.500

在 p = 1 时意外度是 0,在 0.002 时是 6.215,而且随着 p 趋近于零无上限地往上爬。0.693 那条虚线是一个 bit —— 一枚公平硬币的意外度 —— 它同时也是单位换算:nat 乘以 1.4427 就是 bit。

熵就是把这份意外度按分布自己的权重平均一下。每个结果贡献 −p log p;把这六份贡献首尾相接,它们的总长度就是熵。把分布变集中,看这根条子怎么缩短:

H = 1.79

均匀是最大值。六个等可能的结果给出H = 1.79 nat,正好是log 6,条子顶到虚线那一格。把集中度, p(1) 爬到 0.831,而 H 掉到 0.73:一个几乎已经拿定主意的分布,传输起来几乎不要钱。

现在轮到模型。它并不知道 p —— 它提出的是q,而它要为源源不断从 p 里出来的结果支付 −log q。把模型从真实拖开,看条子末端怎么长出第二块:

H(p, q) = 1.67

偏移为零时两行完全重合,条子正好是H(p) = 1.67 —— 模型只付了不确定性本身的成本。把它推到 ,账单是 2.05,其中 0.38 是错配。这份多出来的开销就是 KL 散度:从不为负,只有 q 等于 p 时才为零 —— 这就是「最小化交叉熵」该做训练目标的原因。

nat 不好体会,所以业界改报 exp(H):「在多少个等可能的选项之间选择才这么难」。那个标记一开始就落在真实模型的损失上:

H = 2.86 nat

损失为 0 就是困惑度为 1:根本没得选。在 GPT-2 那 50,257 个 token 的词表上闭着眼睛猜是,也就是那条虚线。 15 亿参数的 GPT-2 在 WikiText-103 上零样本得分 17.5 —— 2.86 nat,或者说每个 token 4.13 bit —— 也就是说它把五万个选项收窄到了大约十八个。交叉熵和困惑度是同一次测量的两种单位。

08

全部内容,浓缩在一帧里

上面每一节,都会出现在语言模型跑的最后那四行里。

一次前向传播的结尾正是这样:一排 logits、对它们做 softmax、抽一次、算一个损失。这里真正的下一个 token 是 cat,采样器拿到的是固定的 u = 0.62。拖动那个 token 的 logit,看这四个阶段怎么一起动:

损失 2.05

在初始 logit 0.5 时,模型给真值 token 的概率是 0.129,而这一次抽样抽到了另一个 —— 损失读作 2.05 nat,困惑度 7.8。把 logit 提到,概率涨到 0.973,抽样落在 cat 上,损失塌缩到 0.03。把它压到 ,损失就是 6.41。

p = softmax(z / T)      # z -= z.max() first
i = searchsorted(cumsum(p), uniform())
loss = -log(p[true])    # nats, 0 at p = 1
ppl = exp(mean(loss))   # effective choices

训练在挪那个 logit,采样在读那一行。把这四行跑十二次,损失就只是加起来 —— 这正是 §05 里那个「对数之和」,只是这次给了它一个名字。一个位置一个位置地把它们计入:

12 个位置中的 1 个

十二个 token 之后,总和是 19.70 nat,平均 1.641, —— 而光是第 7 个位置,在 p = 0.02 上就花掉了其中的 3.91。整门学科就浓缩在这一根条子里:概率负责描述,对数让它可以相加,而平均值就是论文里报告的那个数。