AI 工程地基 16|采样:从一枚均匀骰子,到 GPT 的五万张词牌

「AI 工程地基」系列第十七篇。把 GitHub 上 58k star 的开源课 AI Engineering from Scratch(MIT 协议)一课一课啃完、核完、讲完。今天第 16 课:采样方法,Build 型,120 分钟,前置是概率与贝叶斯那两课。

目录
  1. AI 工程地基 16|采样:从一枚均匀骰子,到 GPT 的五万张词牌
  2. 五万个分数,挑一个
  3. 一个均匀数,够产生任何分布的一个样本
  4. 提议,然后掷骰子决定收不收
  5. 不用要样本,只要期望
  6. 掷一万次骰子,把圆周率逼出来
  7. 沿着分布走路的算法
  8. 大模型的三个旋钮
  9. 把随机性搬出计算图
  10. 把空间切块,强制均匀
  11. 扩散模型:每一步都是采样
  12. 收摊之前,留一道题

AI 工程地基 16|采样:从一枚均匀骰子,到 GPT 的五万张词牌

「AI 工程地基」系列第十七篇。把 GitHub 上 58k star 的开源课 AI Engineering from Scratch(MIT 协议)一课一课啃完、核完、讲完。今天第 16 课:采样方法,Build 型,120 分钟,前置是概率与贝叶斯那两课。

五万个分数,挑一个

语言模型读完你的提示词,吐出五万个数。词表里每个 token 一个分数。现在要挑一个往下写。

永远挑最高的?每次回答一模一样,像复读机。完全随机挑?输出是胡话。答案在两个极端之间,而中间那片地带,由采样决定。

这一课把生成式 AI 的骰子从头造一遍。从最普通的一枚均匀骰子出发,一路造到大模型每天在用的那几颗。

一个均匀数,够产生任何分布的一个样本

先从最不起眼的东西开始:[0, 1) 上的均匀随机数。它的方差是 1/12,均值是 1/2,土得掉渣。

但它有个惊人的性质:一个均匀数,恰好够你产生任何目标分布的一个样本。不多不少。前提是找对变换。

最常用的变换叫逆 CDF。累积分布函数 F(x) 回答「落在 x 以下的概率」,它把数值变成概率;反过来用它,把概率变回数值。拿一个均匀数 u,代入 F 的反函数,出来的就是目标分布的样本。

原理三行就能验完:P(F⁻¹(U) ≤ x) = P(U ≤ F(x)) = F(x)。左边是变换后的取值分布,右边恰好是目标分布的定义。完事。

拿指数分布练手。它的 CDF 是 F(x) = 1 − e^(−λx),令它等于 u,解出 x = −ln(u)/λ。就这一行代码,我跑了一万次,样本均值 0.4995,理论值 1/λ = 0.5。贴合得没有任何脾气。

麻烦在正态分布身上:它的 CDF 没有闭式反函数。所以才有 Box-Muller 那类专门技巧。逆 CDF 好用,但不是万能钥匙。

提议,然后掷骰子决定收不收

逆不动 CDF,但能算目标分布的密度(哪怕差个常数倍),就用拒绝采样:找一个能采样的提议分布 q,找一个包络常数 M,保证目标密度始终被 M·q 罩住。每轮从 q 里提一个候选,再掷一个均匀数 u,u 小于目标与包络之比就收下,否则扔掉重来。

用课后的 Beta(2, 5) 练习算笔账。这个分布的最高点在 x=0.2,密度值 2.4576,拿 Uniform(0,1) 当提议,M 就是 2.4576。接受率 = 1/M = 0.4069。我实际跑了一遍:十二万次提议,接受率 0.4059。理论与实跑只差千分之一。

这里有个容易漏看的前提。1/M 这个数,站在「目标密度已经归一化」的地面上说话。而这门课开头自己说了,目标可以差个常数倍——那种情况下接受率是这个常数和 M 的比,不是 1/M。练习里是归一化的,正文的话才成立。差两个字,学生自己复算时会对不上。

还有个更狠的:维度。拒绝采样的接受率随维度指数下跌,因为高维空间里提议的体积几乎全被扔掉。一二维好用,十维往上基本没戏。

不用要样本,只要期望

有时你根本不需要目标分布的样本,只需要它下面的一个期望。重要性采样说:用别的分布的样本,配上权重就行。权重就是两个密度之比 p(x)/q(x)。

这就是 PPO 的心脏。策略更新时,轨迹是旧策略采的,要优化的却是新策略。每个样本乘上「新策略给这个动作的概率 ÷ 旧策略给的概率」,就能用旧数据估新策略的期望。PPO 对这个比值做裁剪,防止新策略飘太远。

代价藏在方差里。q 和 p 差得越远,少数样本会攥着巨大的权重主导一切。所以选提议分布是个手艺。

掷一万次骰子,把圆周率逼出来

蒙特卡洛估计是采样最朴素的用法:往单位正方形里随机撒点,落进四分之一圆内的比例乘 4,就是 π。我撒了十万次,得到 3.1502。不算惊艳,但有意思的是误差怎么缩。

课后练习让你算 sin 在 0 到 π 上的积分,样本数一千、一万、十万各来一遍。真值是 2。我把每档重复了两百次,量估计量的标准差:一万样本是 0.0101,十万样本是 0.0032。比值 3.12,理论上应该正好是 √10 = 3.16。误差确实按 1/√N 缩——每把样本乘一百,误差只缩十倍。蒙特卡洛收敛得慢,但稳。

课里说这个误差率「与维度无关」。这句话要读细:是收敛速度与维度无关,误差本身未必。藏起来的常数是 Var(f)——被积函数的方差,它可以随维度悄悄爆掉。速度无关,代价有户口。

沿着分布走路的算法

前面所有方法都在跟一个硬约束打架:你得能直接采。多峰、高维、只差常数倍的诡异目标,全都不好直接采。

Metropolis-Hastings 换了个思路:不采了,走路。从任意一点出发,每步向旁边试探性地挪一点,挪还是不挪由一个比值决定——新位置的密度比上旧位置的密度,高于 1 就挪,低于 1 就掷骰子按概率挪。走足够多步,走过的脚印就是目标分布的样本。

保证这事成立的是细致平衡:从 x 走到 x' 的概率,和从 x' 走回 x 的概率,恰好相等。像一条可逆的流水账,账面不动,人换了一茬又一茬。

道理干净,跑起来才知道脾气。我让它采一个双峰分布——两个高斯各占一半。五万步之后,落在右峰的样本占 46.6%,理论 50%。为什么偏?因为每步都是小碎步,从一个峰跳到另一个峰,得靠提议恰好跨过中间的低密度谷地。这正是课里「提议尺度」那段话的现场版:步子太小,挪得慢;步子太大,全被拒绝,原地钉死。

课里还给了个数字:高维高斯目标下的最优接受率约 0.234。我实测了:二十维、提议尺度 0.5 时接受率 0.278,一百维、尺度 0.25 时 0.212。维度越高,最优值确实滑向 0.234。这个数字有出处——1994 年 Roberts、Gelman、Gilks 三人证的,但它站在几个前提上:目标得是各维独立的高斯、提议是随机游走、维度趋于无穷。换成别的目标,最优值不是这个数。

Gibbs 采样是这条路的老亲戚:多变量分布,一次只更新一个变量,其余钉住,从它的条件分布里采。接受率永远 100%,因为条件分布采出来天然满足细致平衡。代价是变量之间相关性一强,就只能贴着坐标轴一格一格挪,对角方向的大步迈不出去。

大模型的三个旋钮

现在到正题。语言模型输出的是 logits——softmax 之前的原始分数。

温度在最前面:先给所有 logit 除以 T,再 softmax。热力学名字,小学除法。拿两个 token 举例:logit 是 2 和 1。T=1 时概率比 2.72(就是 e);T=0.5 时除完变 4 和 2,概率比 7.39(e²);T=2 时变 1 和 0.5,概率比 1.65(e 的平方根)。除以小于 1 的温度=放大差距=更自信;除以大于 1 的温度=抹平差距=更发散。五万词的概率重新洗牌,就靠这一个除数。

课里的实践表从 T=0(贪心、事实问答)一路列到 T>1.5(基本没用)。但注意:T=0 这一行,页面上配套的代码跑不了——代码里就一句 z / temperature,T 传 0 当场除零报错。有意思的是,同一个仓库里 code/sampling.py 的完整版本其实有守卫:温度小于等于 0 就直接返回最大 logit 的下标。课程页面上贴的代码块是旧版,守卫没跟上。想试 T=0,要么自己加一行判断,要么直接用仓库版。

Top-k 从另一头动手:不管温度,先把词表按概率排个序,只留前 k 个,剩下的全部出局,再在 k 个里归一化采样。k=1 就是贪心,k=50 是 2018 年 Fan 等人那篇层级故事生成论文带火的设置。

它的毛病是 k 死板。模型特别自信时(某个词 95% 概率),留 40 个候选纯属浪费——后面 39 个都是凑数的。模型特别犹豫时(概率摊在一千个词上),k=40 又会把合理选项切掉。一刀切,两头挨骂。

Top-p(核采样)让候选集自己伸缩:不数个数,数概率质量。从高到低累加,加到占总概率 p(比如 0.9)就停,只留这一小撮。模型自信时可能只留两三个词,犹豫时可能留两百个。Holtzman 等人 2020 年那篇《神经文本退化的奇案》就是它。实践里温度 0.7 加 top-p 0.9 是最常见的通用搭配。

把随机性搬出计算图

采样有个先天残疾:不可导。VAE 的结构是「编码成分布 → 采样 → 解码」,中间那步采样本该把梯度拦腰斩断。

重参数化技巧把随机性和参数拆开。别直接 z ~ N(μ, σ²),改成先采 ε ~ N(0,1),再算 z = μ + σ·ε。同一个分布,但 z 现在是参数的普通函数,随机性被挪到了一个不带参数的变量里。

导数立刻能写:∂z/∂μ = 1,∂z/∂σ = ε。我用数值差分验证过,和解析式完全一致。就这么一下,VAE 的整套反向传播打通了。可以说没有这个技巧就没有能训练的 VAE。

高斯这么办,离散类别怎么办?Gumbel-Max 技巧接上:给每个类别的对数概率加一个 Gumbel 噪声,谁大选谁。听起来像野路子,但它数学上精确——加噪声取最大,落点频率严格等于原始概率。我拿 [0.6, 0.3, 0.1] 跑了五万次,落点 0.604、0.299、0.098。准得像变魔术。

但课程页面上的 Gumbel-Softmax 示例代码有个真问题。函数签名写着 gumbel_softmax(logits, temperature),参数名叫 logits;函数体里却对每个输入再做一次 math.log(p)。logits 和对数概率不是一回事——中间隔着一整个 softmax。log(softmax(z)) = z 减去一个常数,log(logits) 什么都不是。

后果我跑出来了。喂它这门课自己在「用起来」一节给的那组示例 logits [2, 1, 0.5, 0.1, −1],里面有负数,对 −1 取对数当场报错崩掉。换成全正的 [3, 1, 0.1] 让它跑五千次,落点频率是 0.73、0.24、0.03——而真实分布是 0.84、0.11、0.05。第二名被凭空抬高了一倍,越小的越被压。分布整体被扭成了一个更平的形状,温度白调。

同一个仓库的完整版代码没这个问题:它的参数名就叫 log_probs,直接加噪声,不做第二次对数。课程页面贴的代码块,和仓库里真正能跑的版本,不是同一版。用的时候认准仓库。想自己写对,也简单:Gumbel 噪声直接加在 logits 上取最大就行——softmax 的对数分母是个常数,不影响谁大谁小。

Gumbel-Softmax 是这个技巧的可微版:argmax 换成带温度 τ 的 softmax,τ 趋近 0 时逼近 one-hot,训练时前向可以用硬的选择、反向走软的梯度(直通估计器)。

把空间切块,强制均匀

标准蒙特卡洛靠巧合覆盖空间,可能扎堆也可能留洞。分层采样把 [0,1] 切成 N 条,每条里强制采一个。

对 sin 积分这种小实验,效果夸张:一百个样本,三百次重复,标准法的估计量标准差 0.091,分层法 0.0020——方差差了两千多倍。函数越平滑,分层越占便宜;分段常数的函数,分层采样直接精确。NeRF 沿光线采样时用的就是它。

扩散模型:每一步都是采样

最后把镜头拉远。扩散模型生成图像:前向过程一步步加高斯噪声直到纯噪声,反向过程学着一步步去噪。对照这一课:每个去噪步都在用重参数化(采一个噪声、做一次确定性变换),噪声调度扮演温度退火,训练目标是蒙特卡洛估计的证据下界,整条生成链是马尔可夫链。图像生成从头到尾就是迭代采样。

收摊之前,留一道题

课后第 4 题是个好题:用 Metropolis-Hastings 采二维分布 p(x,y) ∝ exp(−(x²y² + x² + y² − 8x − 8y)/2)。这个分布长得歪歪扭扭,直接采没戏。但固定 y 看 x 的条件分布,恰好是高斯(均值 4/(1+y²),方差 1/(1+y²))——意味着 Gibbs 采样能把它精确跑通。同一道题,两条路都通,还各有各的脾气。提议尺度扫一圈,链的轨迹会长出完全不同的形状。

词表五万张牌,骰子就这几颗。下一课见。

暂无表态

想参与讨论或点赞?登录后使用完整功能

讨论回复(0)

暂无回复,登录后可参与讨论
合作

智谱 GLM-5 已上线

在智谱开放平台 BigModel.cn 打造 AI 应用。新一代旗舰模型 GLM-5 在推理、代码、智能体综合能力达到开源模型 SOTA。

领取 2000万 Tokens