5 从加噪到去噪:扩散的数学骨架
本章地图
你在这里:换了一条流水线 —— 下篇的扩散模型,从噪声到数据。上篇的注意力在这里会以”交叉注意力”的形式再出现一次(第 8 章)。 本章解决:前向加噪为什么有闭式解;反向过程为什么必须是一个分布;训练目标(ELBO)是从哪一步推出来的。 前置:第 1、2 章。下篇不需要第 3、4 章,但”训练和推理是两套流程”这个视角会一直用。 读完能做:从零写一个能跑的扩散模型(67 行);并判断一个采样实现为什么会塌。
上篇四章处理的是一个已经训好的模型:它怎么算、怎么推理、里面能读出什么。 从这一章起换一个问题——这些东西是怎么被训出来的。
扩散模型的训练目标看起来和 Transformer 毫无关系:它要模型学会从噪声里恢复信号。 但下面你会看到,两者的骨架是同一件事的两面:
- Transformer 的注意力把”从一堆候选里挑信息”写成了一次可微的加权平均;
- 扩散的反向过程把”从一团噪声里恢复结构”写成了 T 次小的、可微的高斯采样。
两边的核心困难也一样:训练目标写得出来,不等于实现是对的。 第 3 章里,“缓存写错了”在生成文本上看不出来; 这一章里,“反向过程写错了”在训练 loss 上看不出来——一位小数都不差。
全景:一个扩散模型长什么样
先说这一节为什么存在。
第 1 章在拆注意力之前,先给了一张 Transformer 的完整数据流图(图 1-1)—— 因为不看全图就直接读细节,会不知道那个零件装在哪。 下篇同样需要这样一张图。
而且扩散比 Transformer 更需要:它有一条 Transformer 没有的分叉—— 训练走的路和采样走的路不是同一条。 这个分叉不是实现细节,是理解整章的关键:后面那个”训练 loss 完全看不出采样 bug”的案例, 根就在这里。
读这张图的正确方式:先看它有两个列,再看箭头往哪走。
第 1 步:训练走前向,采样走反向
这是全图最重要的一件事。
| 训练 | 采样 | |
|---|---|---|
| 方向 | 数据 → 噪声 | 噪声 → 数据 |
| 步数 | 一步到位 | 1000 步 |
| 要不要模型 | 不要(前向过程是公式) | 要(每一步都调用) |
| 干什么 | 造训练样本 | 生成 |
训练时,我们手里有真实数据 x_0。 要造一个”加了 t 级噪声的版本”,不需要真的走 t 步—— 前向过程有闭式解,一步就能算出来(第 2 步讲)。
采样时,我们手里只有一个高斯噪声 x_T,没有 x_0。 所以只能一步一步往回走,每走一步都要问一次模型。
这个不对称是扩散模型全部工程问题的来源:训练很便宜(一次前向), 生成很贵(T 次前向)。第 6 章整章都在处理这个”贵”。
第 2 步:前向过程不需要学
图中左列第 3 格:
x_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\varepsilon
这一行里没有任何可训练的东西。 \bar\alpha_t 是由噪声调度算出来的固定数组, \varepsilon 是现场抽的标准正态噪声。
换句话说:训练数据的标签是白送的。 给定任意一个 x_0、任意抽一个 t、任意抽一个 \varepsilon, 就得到一条合法的训练样本——不需要任何人工标注,也不会过拟合到某个数据集。
这是扩散相对 GAN 的核心优势:没有对抗、没有判别器、loss 不会震荡。 代价是采样慢(第 1 步)。
这一段对应的正是本章 §5.4(前向过程与闭式解)。
第 3 步:去噪网络只做一件事
图中两列中间那条虚线连着的,是同一个网络 \varepsilon_\theta(x_t, t)。
它的任务只有一个:给定一个加了噪声的样本和它的噪声等级,猜出当初加进去的噪声是什么。
注意它不是在”生成图像”,也不是在”预测下一个 token”。 它做的是一件很窄的事:估计噪声。
- 本章的玩具模型:一个 3 层 MLP,50,050 个参数
- 真实的图像模型:U-Net 或 DiT,几千万到几十亿参数(第 8 章)
结构变了,任务没变。 这是理解后面几章的一个锚点。
第 4 步:反向过程是一个循环
图中右列。从 x_T \sim \mathcal{N}(0,I) 出发,重复 T 次:
\begin{aligned} \varepsilon &\leftarrow \varepsilon_\theta(x_t, t) &&\text{(问一次模型)}\\ \mu_\theta &= \frac{1}{\sqrt{\alpha_t}}\left(x_t - \frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\varepsilon\right) &&\text{(算往哪走)}\\ x_{t-1} &= \mu_\theta + \sigma_t z &&\text{(走一步,带随机性)} \end{aligned}
第三行那个 \sigma_t z 是本章案例的主角。 把它去掉,前两行照样能跑,训练 loss 也一位不变—— 但采样结果会从”两个弯月”塌成”一个半径 0.2 的点”。
为什么? 因为反向过程定义的是一个概率分布,不是一个确定的函数。 第二行算出的是这个分布的中心,第三行才是从分布里抽一个样本。
这一段对应本章 §5.5(后验与反向过程)。
第 5 步:条件从哪进来
图底那条蓝带,是下篇后半部分的主题。
上面四步描述的是”无条件生成”:模型学会生成”某种像数据的东西”, 但你没法告诉它要生成什么。
加条件的方式(第 7、8 章):
- 文本 → 向量:用一个文本编码器(CLIP / T5)把提示词编码成一串向量(第 8 章)
- 注入:通过交叉注意力把文本向量喂进 \varepsilon_\theta—— 这时 Q 来自图像特征,K、V 来自文本(第 8 章)
- CFG:同一次采样里跑两遍 \varepsilon_\theta(一遍带条件、一遍不带), 把两个结果往”更听条件”的方向外推(第 7 章)
注意第 2 条:交叉注意力就是第 1 章的多头注意力, 只是 K、V 换了个来源。上篇和下篇在这里接上了。
这一章只讲其中一块
回头看那张图:本章覆盖左列全部,加上右列第 3、4 两格的数学。
具体说:
| 本章讲 | 留给后面 |
|---|---|
| 前向过程为什么有闭式解(§5.4) | 采样器怎么把 1000 步压到几十步(第 6 章) |
| 后验为什么是高斯、\tilde\beta_t 怎么来的(§5.5) | 条件怎么起作用、CFG 为什么过饱和(第 7 章) |
| 训练目标 ELBO 从哪来(§5.6) | 文本编码器与交叉注意力(第 8 章) |
| 三种参数化、噪声调度(§5.7、§5.8) |
但本章有一个贯穿始终的问题,它比上面任何一条都重要:
训练和采样是两套公式,它们之间没有任何自动的一致性检查。
图中那条虚线连着左右两列,但它们只共享一个 \varepsilon_\theta—— \sigma_t 只在右边出现,后验均值的公式也只用在右边。
所以左边(训练)完全不知道右边(采样)写成了什么样。 下一节的故事就从这个裂缝开始。
5.1 本章的代码与数据
# 需要 PyTorch。本机在 /home/mpp/micromamba/envs/dlspace/bin/python
import torch, sys
sys.path.insert(0, "notebooks")
from tiny_diffusion import (Config, make_two_moons, make_schedule,
train, ddpm_sample, elbo_terms)
data = make_two_moons(4000, seed=0) # 2D 双月,4000 个点
cfg = Config(max_steps=5000) # T=1000,线性 β 调度
model, sch, hist = train(data, cfg)
x_ok = ddpm_sample(model, sch, cfg, 4000) # 正确
x_bad = ddpm_sample(model, sch, cfg, 4000, variant="no_noise") # 案例里的 bug复现本章全部数字:
cd notebooks
/home/mpp/micromamba/envs/dlspace/bin/python run_ch5_case.py # 案例主实验
/home/mpp/micromamba/envs/dlspace/bin/python run_ch5_schedule.py # 调度对比
/home/mpp/micromamba/envs/dlspace/bin/python export_ch5_points.py # 插图点云为什么用 2D 而不是图像。 这是下篇四章共同的选择,理由有三条:
- 学生自备 CPU 机器。2D 上训一个 5 万参数的 MLP 只要几秒; CIFAR-10 上训一个能看的扩散模型要几小时到几天。
- 概率密度可以直接画出来。图像上你只能看”采样结果好不好看”; 2D 上你能眼睁睁看着整个分布怎么从双月变成高斯、又怎么变回来。
- 每一步都能手算核对。本章 §5.4、§5.5 有六处手算,读者可以拿计算器跟着走。
代价是真实的:这个玩具模型没有真正的”生成能力”, 它只能生成两个弯月。凡是”只有大模型才表现出来”的现象,本章都不测—— 第 6–8 章会说明哪些结论能迁移、哪些不能。
5.2 一个训练完美、采样塌成一团的模型
5.2.1 实验设置
数据是经典的”双月”(two moons):两条交错的弯月,各 2000 个点, 加一点高斯抖动,整体缩放到 [-1, 1]。实测统计量:
| 量 | 数值 |
|---|---|
| 点数 | 4000 |
| 均值 | [-0.0012,\ -0.0011] |
| 标准差 | [0.5707,\ 0.3351] |
| 最大绝对值 | 1.136 |
| 均方半径 \mathbb{E}\|x\|_2 | 0.6110 |
去噪器 \varepsilon_\theta(x_t, t) 是一个 3 层 MLP,隐藏宽度 128, 50,050 个参数;时间步 t 用正弦位置编码送进去——和第 1 章的位置编码是同一个公式, 只是把”第几个 token”换成了”第几个噪声等级”。
扩散设置是 DDPM 的原始配置: T = 1000,\beta_t 从 10^{-4} 线性升到 0.02,训练 5000 步。
5.2.2 现象
训练过程一切正常:
step 0 loss 0.8844
step 1000 loss 0.2902
step 2000 loss 0.2045
step 3000 loss 0.1685
step 4000 loss 0.1471
step 5000 loss 0.1355
loss 从 0.88 单调降到 0.14,没有震荡、没有 nan、梯度范数正常。 单看这条曲线,这个模型训得没有任何问题。
然后采样。T 步 DDPM 反向过程跑完,4000 个点的统计量是:
| 原始数据 | 采样结果 | |
|---|---|---|
| 均值 | [-0.0012,\ -0.0011] | [0.0045,\ -0.0488] |
| 标准差 | [0.5707,\ 0.3351] | [0.5598,\ 0.3437] |
| 均方半径 | 0.6110 | 0.6090 |
| MMD(与数据的分布距离) | — | 0.01146 |
这是对的。 均方半径差 0.3%,MMD 0.011 已经很小 (两个独立同分布的样本之间 MMD 大约就在这个量级)。
所以模型是好的。问题不在训练,在采样那段代码。
具体地说,我们写了两个采样函数,它们在采样循环里只差一行:
# 版本 A —— 正确
x = mean + sigma[t] * torch.randn_like(x)
# 版本 B —— 案例里的 bug
x = mean版本 B 的结果:
| 原始数据 | 版本 A(正确) | 版本 B(案例的 bug) | |
|---|---|---|---|
| 标准差 | [0.5707,\ 0.3351] | [0.5598,\ 0.3437] | \mathbf{[0.0043,\ 0.0078]} |
| 均方半径 | 0.6110 | 0.6090 | 0.2027 |
| 最大绝对值 | 1.136 | 1.381 | 0.217 |
| MMD | — | 0.01146 | 0.96029 |
标准差从 0.57 掉到 0.0043 —— 小了 130 倍。 4000 个点全挤在原点附近一个半径 0.2 的团里, MMD 0.96 比正确版本差了 84 倍。
这一节的悬念
一行代码的差别,让采样结果从”几乎完美”变成”完全没用”。 而训练 loss 一位小数都没变。
先记住这个事实。这一章剩下的部分要回答两个问题:
- 为什么少了那一行会塌? —— 需要先把前向过程、反向过程、 后验分布的数学全部写清楚(§5.4–§5.6)。
- 为什么训练 loss 看不出来? —— 因为训练和采样用的根本不是同一套公式, 它们之间没有任何自动的一致性检查(§5.6 末尾)。
5.2.3 案例的设置细节
先把实验设置里几个容易被忽略的地方说清楚, 因为后面的每一个数字都依赖它们。
① 为什么用 4000 个点。
这是统计量的分辨率问题。本章要比较的几个量—— 均值、标准差、rms、MMD——都是样本统计量, 它们本身有抽样误差。
以标准差为例,n 个样本的标准差估计,相对误差约 1/\sqrt{2n}。 n=4000 时是 1.1%,n=400 时是 3.5%。 本章要区分的差异(正确采样 0.5598 vs 数据 0.5707,差 1.9%) 就在这个量级附近——所以 4000 是必要的,不能再少。
② 为什么固定随机种子。
训练、采样、噪声表三处都用了固定种子。 这不是为了”结果好看”,而是为了能做对照:
- 正确采样和 bug 采样用同一批初始噪声
- 两条 loss 曲线用同一次训练的随机序列
没有这一条,“两条 loss 逐点相同”这个结论就做不出来—— 因为两次训练本来就会因为随机性而不同。
这也是第 4 章那条”固定种子”的经验(§4.4.6)在本章的延续。
④ 为什么用均方半径(rms)作为主要指标。
本章的可视化里 4000 个点挤在一起, 肉眼看不出”标准差从 0.56 变成 0.0043”这件事—— 两种情况的图都只是”一团”。
均方半径 \mathbb{E}\|x\|_2 的好处是它把整团的大小压成一个数, 而且对分布形状不敏感——它只回答”这团有多大”这个本章唯一关心的问题。
MMD 补上形状:它比较的是两个分布在所有尺度上的差异。 两个指标合起来,才既能看到”大小错了”又能看到”形状错了”。
⑤ 为什么 T=1000。
这是 DDPM 原文的选择,本章沿用它不是为了复现,而是为了显示它的代价。
§5.8 实测:在这 1000 步里, 327 步(32.7%)的输入几乎是纯噪声。 §5.9 又测出:T 减到 25 反而更好。
所以本章的 T=1000 是一个”被检验过、并且不划算”的选择—— 这比直接用一个调好的配置更有信息量。
5.2.4 这个 bug 是怎么写出来的
在讲排查之前,先说清楚它是怎么产生的——因为它的成因比它本身更有教育意义。
第一版采样函数是照着 DDPM 论文的 Algorithm 2 抄的。 论文里的算法是伪代码,关键那几行长这样:
x_{t-1} = 1/√α_t (x_t - (1-α_t)/√(1-ᾱ_t) · ε_θ) + σ_t z
问题出在最后的 + σ_t z 上。
写代码的时候,脑子的注意力全在前半截—— 那个括号里的式子有四个符号、两个根号、一个嵌套, 抄错一个符号整个结果就废了。 后半截的 + σ_t z 看起来是最简单的一部分, 反而最容易被当成”装饰”跳过。
更具体地说,当时的心智模型是这样的:
- “采样 = 一步步去噪” ✔(这句话本身没错)
- “每一步的目标是让 x 更接近数据” ✔(也没错)
- “所以每一步应该输出那个更接近数据的 x” ✘(这里错了)
第三步是错的,因为反向过程定义的是一个分布, “输出分布的中心”不等于”从分布里抽样”。
一、“照抄论文的公式”这件事比看起来难。 公式有五个加数,你抄了四个,代码照样能跑—— 没有未定义变量、没有形状错误、没有异常。 唯一的结果是数值不对,而数值不对不一定看得出来。
对策:抄完之后逐项对账(§5.15 第四步)。 不是检查每个量合不合理,而是数项数: 公式有几项、代码有几项。
二、直觉最容易在”简单的地方”出错。 那个 + σ_t z 在公式里是配角, 但它在算法里的作用是”这一步是不是在从分布里采样”—— 是一个定义性的项,不是修饰项。
判断”一个项重不重要”的方法,不是看它在公式里写得多复杂, 而是看把它拿掉之后,算法还成不成立。
三、这个错误在训练时完全没有反馈。 这是本章最核心的教训,§5.2 末尾已经用数据证明了 (两条 loss 曲线逐点相同)。
它意味着:写好采样函数之后,必须专门测一次它的输出统计量。 不能等训练 loss 来告诉你。
5.2.5 三条弯路
弯路一:模型太小,学不出双月这么”复杂”的分布。
MLP 只有 5 万参数,双月又是个带尖角的形状。加宽到 512(790,018 个参数, 是原来的 15.8 倍)再训一遍:
| 模型 | 参数 | 训练 loss | 正确采样 rms | bug 采样 rms | bug 采样 MMD |
|---|---|---|---|---|---|
| 隐藏 128 | 50,050 | 0.1355 | 0.6090 | 0.2030 | 0.9604 |
| 隐藏 512 | 790,018 | 0.1417 | 0.6055 | 0.1462 | 0.9761 |
加宽之后,正确采样几乎没变(0.6090 → 0.6055), 而 bug 采样塌得更厉害了(0.2030 → 0.1462,MMD 0.9604 → 0.9761)。
模型容量不是原因。
弯路二:训练不够,模型还没学会。
从 5000 步训到 20000 步(4 倍):
| 训练步数 | 训练 loss | 正确采样 rms | bug 采样 rms | bug 采样 MMD |
|---|---|---|---|---|
| 5000 | 0.1355 | 0.6090 | 0.2030 | 0.9604 |
| 20000 | 0.1394 | 0.6416 | 0.1573 | 0.9396 |
正确答案那边确实略微变好了(0.6090 → 0.6416,更接近数据的 0.6110)。 但 bug 那边同样更塌了(0.2030 → 0.1573)。
MMD 从 0.9604 微降到 0.9396 —— 还是比正确的 0.01146 差两个数量级。
训得更久治不了这个病。
弯路三(最有迷惑性的一条):\sigma_t 该用 \sqrt{\beta_t} 还是 \sqrt{\tilde\beta_t}?
DDPM 论文在这一处给了两个选择:反向过程的方差既可以取 \sigma_t^2 = \beta_t, 也可以取 \sigma_t^2 = \tilde\beta_t(后验方差)。论文说两者结果差不多。
我们以为问题出在”用错了那一个”,于是把两种都跑了一遍:
| \sigma_t^2 的来源 | 采样 rms | 标准差 | MMD |
|---|---|---|---|
| \tilde\beta_t(后验方差) | 0.6090 | [0.5598,\ 0.3437] | 0.01146 |
| \beta_t(前向方差) | 0.6112 | [0.5619,\ 0.3452] | 0.01135 |
两者几乎无法区分(MMD 差 1%)。论文说得没错,但这也不是元凶。
顺带一提,这两个数在本章的调度下本来就极其接近:
| t | \beta_t | \tilde\beta_t | \sqrt{\tilde\beta_t} | \sqrt{\beta_t} |
|---|---|---|---|---|
| 1 | 0.000100 | 0.000000 | 0.00000 | 0.01000 |
| 2 | 0.000120 | 0.000055 | 0.00738 | 0.01095 |
| 10 | 0.000279 | 0.000238 | 0.01543 | 0.01671 |
| 100 | 0.002072 | 0.002035 | 0.04511 | 0.04552 |
| 500 | 0.010040 | 0.010031 | 0.10016 | 0.10020 |
| 1000 | 0.020000 | 0.020000 | 0.14142 | 0.14142 |
从 t=500 往后,两个数在小数点后第五位才开始分开。
注意 t=1 那一行:\tilde\beta_1 = 0 是精确的 0,不是”很小”。 这是检查点 ② 算出来的结果,后面 §5.6 会看到它带来的麻烦。
它们改的都是”值”,没改”有没有”。
- 模型容量:改了 \varepsilon_\theta 的表达能力
- 训练步数:改了 \varepsilon_\theta 的精度
- \beta_t 还是 \tilde\beta_t:改了一个几乎相同的数
而真正的问题是:那个加号后面的整项,我们根本没写。
排查一个”结果不对”的问题时,先分清你改的是量的差别还是有无的差别。 改量只会在原结果的附近滑动;改有无才可能跳到另一个结果。
而”有无”这一类错误,光看数值是看不出来的 —— 因为缺的那一项 在某些时刻本来就是小量(比如 \sqrt{\tilde\beta_{500}} = 0.100, 相对于当时的信号尺度 1.24 只有 8%)。 你看到的不是”少了什么”,而是”结果略微偏了一点”, 直到它累积 1000 步之后变成 130 倍的差距。
5.2.6 转折:把反向过程的定义完整写出来
三条弯路走完,我们回到最原始的地方:把论文里反向过程的定义抄一遍, 和自己的代码逐行对照。
论文(DDPM,Ho et al. 2020)给的反向过程是:
p_\theta(x_{t-1} \mid x_t) = \mathcal{N}\left(x_{t-1};\ \mu_\theta(x_t, t),\ \Sigma_\theta(x_t, t)\right)
注意右边那个 \mathcal{N}。 它说的是:给定 x_t, x_{t-1} 服从一个正态分布——不是等于某个确定的向量。
而我们的代码写的是:
x = mean # 只取了分布的均值 μ我们实现的是这个分布的均值,不是这个分布本身。
5.3 一个定义读了十遍都没看出来的地方
p_\theta(x_{t-1} \mid x_t) = \mathcal{N}(\mu, \Sigma) 这个式子, 字面上没有任何歧义。但从”公式”到”代码”的这一步, 很容易退化成”把 \mu 算出来赋给 x“ —— 因为绝大多数时候,我们实现的函数返回的就是一个向量。
这里返回的是一个分布。
在 1000 步的循环里,这个区别是:每一步都从当前的高斯里抽一个样本, 而不是每一步都跳到高斯的中心。
抽样的那一步就是 + sigma[t] * torch.randn_like(x)。 它看起来像”加噪声”,像是可有可无的抖动—— 实际上它是整个反向过程的推进机制。
5.3.1 元凶:丢掉的是”从分布里抽样”这个动作
写清楚之后,代码应该是:
x_{t-1} = \underbrace{\mu_\theta(x_t, t)}_{\text{我们写的}} + \underbrace{\sigma_t \cdot z}_{\text{我们漏掉的}},\qquad z \sim \mathcal{N}(0, I)
漏掉的是右边那一项。 加回来之后,MMD 从 0.96029 回到 0.01146。
为什么少了它就会塌? 这里先给一个直觉,完整的答案在 §5.5:
- 每一步的 \sigma_t z 都在把点云”重新摊开”。 没有它,整个反向过程就是一个确定性的映射—— 每一步把 x_t 映到 \mu_\theta(x_t,t),一个点进去、一个点出来。
- 1000 步确定性映射连着做,整个先验分布被越压越紧。
- 实测的压缩倍率:\prod_{t=1}^{1000} \dfrac{1}{\sqrt{\alpha_t}} = 157.41(这是正确版里的放大因子; 丢掉噪声后每一项的系数没变,但缺少摊开的力量,分布只会单向收缩)。
塌缩不是最后一步发生的。 实测反向过程每一步之后的样本均方半径:
| 反向步 t | 正确 | 丢掉 \sigma_t z |
|---|---|---|
| 1000 | 1.2585 | 1.2636 |
| 950 | 1.2852 | 0.7841 |
| 900 | 1.2969 | 0.4981 |
| 800 | 1.2892 | 0.2224 |
| 700 | 1.2778 | 0.1124 |
| 600 | 1.2753 | 0.0668 |
| 500 | 1.2397 | 0.0512 |
| 400 | 1.1797 | 0.0434 |
| 300 | 1.0470 | 0.0397 ← 最小值 |
| 200 | 0.8764 | 0.0592 |
| 100 | 0.7238 | 0.1070 |
| 0 | 0.6006 | 0.2030 |
正确那条:从 t=1000 到 t≈350 一直停在 1.27 附近—— 每步注入的噪声把尺度顶住了——之后才开始收敛到数据的 0.61。
丢掉噪声那条:1000 → 300 一路掉到 0.040,塌了 30 倍; 然后 300 → 0 又反弹回 0.2030。
塌缩集中在前 700 步就完成了。 反弹的那一段是另一回事: 当点云已经缩成一个点时,模型的 \varepsilon_\theta 收到的是 它训练时从没见过的输入(尺度差了 30 倍),输出的是外推的结果。 外推把点云又推散了一点——但推不回原来的分布。
5.3.2 决定性的证据:训练 loss 一位小数都不差
这是这一章最重要的一张数据。
我们把配置里的开关从 "div" 改成 "mul"(也就是启用这个 bug), 用同一个随机种子、同一份数据重新训练了一遍, 然后逐点比对两条 loss 曲线:
| 比较项 | 结果 |
|---|---|
| 比较的点数 | 51(每 100 步记一次) |
| 两条曲线的最大绝对差 | 0.000000(0.00e+00) |
| 是否逐点相同 | 是 |
两个模型完全一样,逐位相同。
因为训练走的是前向过程,采样走的是反向过程,它们没有一行共同的代码。
训练循环里的三行:
t = torch.randint(0, T, (B,)) # ① 随机抽一个噪声等级
x_t, eps = forward_sample(x0, t, sch) # ② 前向:x_t = √ᾱ_t x₀ + √(1-ᾱ_t) ε
loss = F.mse_loss(model(x_t, t), eps) # ④ 比对预测的 ε 和真实的 ε这三行里,没有一处用到 \sigma_t,也没有一处用到反向过程的均值公式。
- \sigma_t 只在反向过程里出现(决定每一步注入多少噪声)
- 反向均值只在反向过程里出现(决定往哪个方向走)
- 而
loss只关心”给定 x_t,\varepsilon_\theta 预测得像不像”
所以反向过程写成什么样,训练 loss 完全不知道。
这个 bug 在训练全程中零成本、零信号。 它只在你真正采样的时候,一次性把结果全部毁掉。
上面说”训练和采样没有一行共同的代码”。
那如果换一个训练目标呢? 比如不用 DDPM 的简化目标 L_{\text{simple}}, 而是用完整的 ELBO(§5.6 会推)—— 它在 t 处的被积项是
L_{t-1} = D_{\mathrm{KL}}\left(q(x_{t-1}|x_t,x_0)\ \|\ p_\theta(x_{t-1}|x_t)\right)
呃,先别急着看公式。只从结构上想:ELBO 里的 L_{t-1} 比较的是”真实后验 q(x_{t-1}|x_t, x_0)“和”模型分布 p_\theta(x_{t-1}|x_t)“。
它会不会碰到我们写错的那段代码?
(答案:不会。
L_{t-1} 里的 p_\theta 是用模型自己算出的均值 \mu_\theta(x_t,t) 和预设的方差拼出来的,跟”采样循环里怎么把 \mu 和 \sigma z 加起来”是两段独立的代码。
但要注意一个区别:ELBO 确实会用到 \sigma_t 的值 (它要拿 \sigma_t^2 当 p_\theta 的方差)。 所以”\sigma_t 取值错了”ELBO 是能看见的; “压根没加 \sigma_t z”它看不见——因为那是采样动作,不是分布定义。
这就是为什么弯路三(换 \tilde\beta)看起来”有道理”却治不了病: 它改的是 ELBO 能看见的那部分,而元凶在 ELBO 看不见的那部分。)
5.4 前向过程:加噪为什么能一步到位
5.4.1 先算一遍:三个时刻,一个标量
先把维度压到最低:一维数据,x_0 = 1.5,只走三步。 调度取 \beta = [0.1,\ 0.2,\ 0.3]。
第一步:\alpha_1 = 1 - 0.1 = 0.9。
x_1 = \sqrt{\alpha_1}\, x_0 + \sqrt{\beta_1}\,\varepsilon_1
抽到 \varepsilon_1 = 0.3:
x_1 = \sqrt{0.9}\times 1.5 + \sqrt{0.1}\times 0.3 = 0.948683\times 1.5 + 0.316228\times 0.3 = 1.423025 + 0.094868 = 1.517893
第二步:\alpha_2 = 0.8,抽到 \varepsilon_2 = -0.7:
x_2 = \sqrt{0.8}\times 1.517893 + \sqrt{0.2}\times(-0.7) = 1.357636 - 0.313049 = 1.044596
第三步:\alpha_3 = 0.7,抽到 \varepsilon_3 = 1.1:
x_3 = \sqrt{0.7}\times 1.044596 + \sqrt{0.3}\times 1.1 = 0.873986 + 0.602492 = 1.476466
注意一个现象:x_0 = 1.5,走完三步之后是 1.476466。 加了三次噪声,值几乎没动。
这不是巧合,也不是”加噪没用”——这是这一节要讲的全部内容。
5.4.2 三次噪声合起来是什么
把 x_3 用 x_0 和三个 \varepsilon 直接展开。先代 x_1:
x_2 = \sqrt{\alpha_2}\left(\sqrt{\alpha_1}x_0 + \sqrt{\beta_1}\varepsilon_1\right) + \sqrt{\beta_2}\varepsilon_2
再代进 x_3:
x_3 = \sqrt{\alpha_3\alpha_2\alpha_1}\,x_0 + \underbrace{\sqrt{\alpha_3\alpha_2\beta_1}}_{\text{给 }\varepsilon_1}\,\varepsilon_1 + \underbrace{\sqrt{\alpha_3\beta_2}}_{\text{给 }\varepsilon_2}\,\varepsilon_2 + \underbrace{\sqrt{\beta_3}}_{\text{给 }\varepsilon_3}\,\varepsilon_3
代入数字:
x_3 = \sqrt{0.504}\times 1.5 + 0.236643\,\varepsilon_1 + 0.374166\,\varepsilon_2 + 0.547723\,\varepsilon_3
现在关键的一步:算这三个系数的平方和。
0.236643^2 + 0.374166^2 + 0.547723^2 = 0.056000 + 0.140000 + 0.300000 = \mathbf{0.496000}
而 1 - \bar\alpha_3 = 1 - 0.504 = \mathbf{0.496000}。
完全相等。
所以这三个噪声的加权和,和一个单独的 \sqrt{0.496}\,\bar\varepsilon 没有区别—— 因为三个独立标准正态的加权和,本身就是一个均值为 0、 方差等于系数平方和的正态分布。
验证一下:
0.236643\times 0.3 + 0.374166\times(-0.7) + 0.547723\times 1.1 = 0.411572
\sqrt{0.496}\times \bar\varepsilon = 0.704273\times 0.584393 = 0.411572 \qquad\checkmark
闭式解成立:
x_3 = \sqrt{0.504}\times 1.5 + \sqrt{0.496}\times 0.584393 = 1.064894 + 0.411572 = 1.476466
和逐步递推的结果一位不差。
5.4.3 上公式
把上面的展开写成一般形式。前向过程的单步定义是
q(x_t \mid x_{t-1}) = \mathcal{N}\left(x_t;\ \sqrt{\alpha_t}\,x_{t-1},\ \beta_t I\right) \qquad \alpha_t := 1 - \beta_t \tag{5.1}
写成”重参数化”的形式(也就是可以直接采样的形式):
x_t = \sqrt{\alpha_t}\,x_{t-1} + \sqrt{\beta_t}\,\varepsilon_{t-1},\qquad \varepsilon_{t-1}\sim\mathcal{N}(0,I)
连乘 t 步,就得到闭式解:
q(x_t \mid x_0) = \mathcal{N}\left(x_t;\ \sqrt{\bar\alpha_t}\,x_0,\ (1-\bar\alpha_t)I\right) \tag{5.2}
x_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\bar\varepsilon,\qquad \bar\varepsilon\sim\mathcal{N}(0,I)
其中
\bar\alpha_t := \prod_{s=1}^{t}\alpha_s = \prod_{s=1}^{t}(1-\beta_s) \tag{5.3}
推导只有一步:把递推式反复代入,x_0 的系数变成 \sqrt{\alpha_t\alpha_{t-1}\cdots\alpha_1} = \sqrt{\bar\alpha_t}; 而所有 \varepsilon 项都是独立正态的线性组合, 方差等于系数平方和,正好是 1-\bar\alpha_t(可由 \bar\alpha_t 的定义直接验证)。
所以这两个式子在说什么
(2) 说的是:想在任意时刻 t 看一眼加噪后的样子,不用真的走 t 步。
直接从 x_0 出发,抽一个标准正态 \bar\varepsilon,代入即可。 训练时每一步都要随机抽一个 t(§5.2 训练循环的第 ① 行), T=1000 的情况下,如果真走 1000 步,每一步训练就要 1000 次前向。 闭式解把这一步变成 O(1)。
这就是”重参数化”这三个字的实际价值: 它把一个嵌套 t 层的随机过程压成了一个单次采样。
技术上说清楚:重参数化的意思是把”x \sim \mathcal{N}(\mu, \sigma^2)” 改写成”x = \mu + \sigma\varepsilon,\varepsilon \sim \mathcal{N}(0,1)“。 这样随机性被隔离到了一个和参数无关的 \varepsilon 里, \mu 和 \sigma 变成了可以求导的普通量。
在变分自编码器里,这个技巧的作用是让梯度能穿过采样; 在扩散模型里,它的作用是把 t 步压成 1 步。 同一个技巧,两个用途。
(3) 说的是:\bar\alpha_t 是一个”信号保留率”。
\bar\alpha_t = 1 表示信号一点没丢;\bar\alpha_t = 0 表示信号全没了。 x_t 里 x_0 前面的系数是 \sqrt{\bar\alpha_t}, 噪声前面的系数是 \sqrt{1-\bar\alpha_t},两者平方和恒为 1。
所以整个前向过程就是一个”把信号的能量按 \bar\alpha_t 转移给噪声”的过程。 x_t 的方差是
\mathrm{Var}(x_t) = \bar\alpha_t\,\mathrm{Var}(x_0) + (1-\bar\alpha_t)
当 \mathrm{Var}(x_0)=1 时它恒为 1;否则它是从 \mathrm{Var}(x_0) 平滑过渡到 1 的一条曲线。
本章的数据方差是 [0.5707^2,\ 0.3351^2] = [0.3257,\ 0.1123],不是 1 ——这是后面检查点 ⑨ 要用的一个事实。实测:
| t | x_t 的方差 |
|---|---|
| 1 | [0.3329,\ 0.1129] |
| 100 | [0.3903,\ 0.2052] |
| 500 | [0.9570,\ 0.9232] |
| 1000 | [1.0251,\ 1.0230] |
前三个数印证了公式:t=1 时几乎等于数据本身的方差, t=1000 时几乎等于 1(纯噪声)。
5.4.4 实测:本模型调度下的 \bar\alpha_t
| t | \bar\alpha_t | \sqrt{\bar\alpha_t} | \sqrt{1-\bar\alpha_t} | 信噪比 \bar\alpha_t/(1-\bar\alpha_t) |
|---|---|---|---|---|
| 1 | 0.999900 | 0.9999 | 0.0148 | 9999.0 |
| 50 | 0.971016 | 0.9854 | 0.1702 | 33.50 |
| 100 | 0.897018 | 0.9471 | 0.3209 | 8.71 |
| 200 | 0.659039 | 0.8118 | 0.5840 | 1.93 |
| 300 | 0.396420 | 0.6296 | 0.7769 | 0.657 |
| 500 | 0.078587 | 0.2803 | 0.9600 | 0.0853 |
| 700 | 0.006966 | 0.0835 | 0.9965 | 0.0070 |
| 900 | 0.000275 | 0.0166 | 0.9999 | 0.0003 |
| 1000 | 0.0000404 | 0.0064 | 1.0000 | 0.00004 |
这张表要横着看:从 t=1 到 t=1000, 信号系数从 0.9999 掉到 0.0064(掉了 156 倍), 噪声系数从 0.0148 升到 1.0000(升了 68 倍)。
而 \bar\alpha_t 本身掉了 24750 倍。
注意信噪比那一列:t=1 时是 9999,t=500 时是 0.085,t=1000 时是 4\times10^{-5}。 七个数量级的变化。
这个比值在扩散模型里叫 SNR,它比 \bar\alpha_t 本身更有信息量—— 因为模型能不能干活,取决于信号相对于噪声有多大, 而不是信号本身有多大。第 6 章讲采样器和第 7 章讲 CFG 时还会反复用到它。
5.4.5 回到案例
现在可以精确地说出案例里那个 bug 的第一层后果了。
闭式解 x_t = \sqrt{\bar\alpha_t}x_0 + \sqrt{1-\bar\alpha_t}\bar\varepsilon 是可逆的—— 给定 x_t 和 \bar\varepsilon,能解出 x_0:
x_0 = \frac{x_t - \sqrt{1-\bar\alpha_t}\,\bar\varepsilon}{\sqrt{\bar\alpha_t}}
反向过程干的就是这件事,只不过 \bar\varepsilon 是未知的, 得让模型去猜。
而 t 很大时,\sqrt{\bar\alpha_t} 很小(t=1000 时是 0.0064), 上面这个式子里要除以一个接近 0 的数。 这就是为什么反向过程必须在每一步都注入噪声—— 不注入的话,x_{t-1} 就完全由 \mu_\theta 决定, 而 \mu_\theta 的误差会被这个除法放大。
下一节把这个说法讲严格。
5.5 反向过程:为什么它必须是一个分布
5.5.1 先算一遍:t=2 的后验长什么样
反向过程要学的是 q(x_{t-1}\mid x_t)。 但这个分布不好直接写——因为 x_t 可能来自任何一个 x_0。
技巧是:把 x_0 也放进来,学 q(x_{t-1}\mid x_t, x_0)。
这个分布叫后验(posterior)。它好写,因为条件里有了 x_0。
先手算。 还是用 \beta = [0.1, 0.2, 0.3],x_0 = 1.5。 现在问:已知 x_0 = 1.5 和 x_2 = 1.044596, x_1 服从什么分布?
正向看:x_1 = \sqrt{0.9}\,x_0 + \sqrt{0.1}\,\varepsilon_1, 所以 x_1 \mid x_0 \sim \mathcal{N}(0.948683\times 1.5,\ 0.1), 即 \mathcal{N}(1.423025,\ 0.1)。
反向看:x_2 = \sqrt{0.8}\,x_1 + \sqrt{0.2}\,\varepsilon_2。 给定 x_1,x_2 的分布是 \mathcal{N}(\sqrt{0.8}x_1, 0.2)。
现在把两条合起来。x_1 的先验是 \mathcal{N}(1.423025, 0.1), 似然是 \mathcal{N}(\sqrt{0.8}x_1, 0.2) 在观测值 x_2 = 1.044596 处的取值。
代入 x_1 = 1.423025 和 x_1 = 1.044596/\sqrt{0.8} = 1.167894 核对: 两个分布的均值不同(1.423025 vs 1.167894),方差也不同(0.1 vs 0.2)。
高斯 × 高斯 = 高斯。 结果是:
\mu = \frac{1.167894/0.2 + 1.423025/0.1}{1/0.2 + 1/0.1} = \frac{5.839470 + 14.230249}{15} = \frac{20.069719}{15} = 1.337981
\sigma^2 = \frac{1}{1/0.2 + 1/0.1} = \frac{1}{15} = 0.066667
后验是 \mathcal{N}(1.337981,\ 0.066667)。
注意 \sigma^2 = 0.066667 比两个”输入”的方差都小 (0.1 和 0.2)。这就是”两个信息源合并”的效果—— 每多一个约束,不确定性就下降。
5.5.2 上公式:后验的完整推导
现在把上面的手算一般化。目标是求
q(x_{t-1}\mid x_t, x_0) = \frac{q(x_t\mid x_{t-1}, x_0)\,q(x_{t-1}\mid x_0)}{q(x_t\mid x_0)}
右边三项全是已知的(都来自前向过程):
\begin{aligned} q(x_t\mid x_{t-1},x_0) &= \mathcal{N}\left(x_t;\ \sqrt{\alpha_t}x_{t-1},\ \beta_t I\right) && \text{(单步前向)} \\ q(x_{t-1}\mid x_0) &= \mathcal{N}\left(x_{t-1};\ \sqrt{\bar\alpha_{t-1}}x_0,\ (1-\bar\alpha_{t-1})I\right) && \text{(闭式解,注意下标是 } t-1\text{)} \\ q(x_t\mid x_0) &= \mathcal{N}\left(x_t;\ \sqrt{\bar\alpha_t}x_0,\ (1-\bar\alpha_t)I\right) && \text{(闭式解)} \end{aligned}
关键观察:q(x_t\mid x_{t-1}) 不依赖 x_0—— 因为给定 x_{t-1} 之后,x_t 是怎么来的跟更早的历史无关(马尔可夫性)。 所以条件里的 x_0 可以去掉,第一项简写成 q(x_t\mid x_{t-1})。
三个高斯相乘,结果还是高斯。 把指数部分写成 -\frac{1}{2}(\cdot) 的形式, 配成关于 x_{t-1} 的完全平方,就能读出均值和方差。
三项的指数(丢掉不依赖 x_{t-1} 的常数):
\begin{aligned} -\frac{(x_t - \sqrt{\alpha_t}x_{t-1})^2}{2\beta_t} -\frac{(x_{t-1} - \sqrt{\bar\alpha_{t-1}}x_0)^2}{2(1-\bar\alpha_{t-1})} +\ \text{(不含 }x_{t-1}\text{ 的项)} \end{aligned}
把 x_{t-1} 的系数收集起来。记
A := \frac{\alpha_t}{\beta_t} + \frac{1}{1-\bar\alpha_{t-1}},\qquad B := \frac{\sqrt{\alpha_t}\,x_t}{\beta_t} + \frac{\sqrt{\bar\alpha_{t-1}}\,x_0}{1-\bar\alpha_{t-1}}
则指数部分 = -\frac{1}{2}\left(A x_{t-1}^2 - 2Bx_{t-1}\right) + \text{const} = -\frac{1}{2}A\left(x_{t-1} - \frac{B}{A}\right)^2 + \text{const}。
所以后验是 \mathcal{N}(B/A,\ A^{-1})。 剩下的是代数化简。
先把 A 化简:
A = \frac{\alpha_t(1-\bar\alpha_{t-1}) + \beta_t}{\beta_t(1-\bar\alpha_{t-1})} = \frac{\alpha_t - \alpha_t\bar\alpha_{t-1} + 1 - \alpha_t}{\beta_t(1-\bar\alpha_{t-1})} = \frac{1 - \bar\alpha_t}{\beta_t(1-\bar\alpha_{t-1})}
(用到了 \alpha_t\bar\alpha_{t-1} = \bar\alpha_t 和 1-\alpha_t = \beta_t。)
所以后验方差是
\tilde\beta_t := A^{-1} = \frac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}\,\beta_t \tag{5.4}
再看均值。 \mu = B/A:
\mu = \frac{\beta_t(1-\bar\alpha_{t-1})}{1-\bar\alpha_t}\left[\frac{\sqrt{\alpha_t}x_t}{\beta_t} + \frac{\sqrt{\bar\alpha_{t-1}}x_0}{1-\bar\alpha_{t-1}}\right]
展开两项:
\tilde\mu_t(x_t,x_0) = \underbrace{\frac{\sqrt{\alpha_t}(1-\bar\alpha_{t-1})}{1-\bar\alpha_t}}_{x_t\text{ 的系数}}\,x_t + \underbrace{\frac{\sqrt{\bar\alpha_{t-1}}\,\beta_t}{1-\bar\alpha_t}}_{x_0\text{ 的系数}}\,x_0 \tag{5.5}
验证一下手算的数字。 t=2:\bar\alpha_2 = 0.72,\bar\alpha_1 = 0.9,\beta_2 = 0.2。
\tilde\beta_2 = \frac{1-0.9}{1-0.72}\times 0.2 = \frac{0.1}{0.28}\times 0.2 = 0.0714286
和手算的 0.0666667 对不上。
怎么回事? 回头检查手算:我把”x_1\mid x_0 的方差”写成了 0.1 = \beta_1, 但按闭式解它应该是 1-\bar\alpha_1 = 1-0.9 = 0.1 —— 这里是对的。 而我写的先验均值 1.423025 = \sqrt{0.9}\times 1.5,也对。
错在似然那一项:我写的是 \mathcal{N}(\sqrt{0.8}x_1,\ 0.2), 方差用的 \beta_2 = 0.2。这没错。
那么公式 式 5.4 和手算应该一致才对。再核一遍公式 式 5.4 的推导。
A = \dfrac{\alpha_t}{\beta_t} + \dfrac{1}{1-\bar\alpha_{t-1}} = \dfrac{0.8}{0.2} + \dfrac{1}{0.1} = 4 + 10 = 14。
手算里我写的是 15。 那里我把 \alpha_2/\beta_2 写成了 1/0.2 = 5—— 把 \alpha_2 = 0.8 当成了 1。
正确的后验方差是 1/14 = 0.0714286,和我上面用手算数据重算的一致。
\mu = \frac{B}{A},\quad B = \frac{\sqrt{0.8}\times 1.044596}{0.2} + \frac{\sqrt{0.9}\times 1.5}{0.1} = \frac{0.934315}{0.2} + \frac{1.423025}{0.1} = 4.671575 + 14.230249 = 18.901825
\mu = \frac{18.901825}{14} = 1.350130
所以 x_1\mid x_2,x_0 \sim \mathcal{N}(1.350130,\ 0.0714286)。
这一步的手算错误留在这里,因为它很典型: 当 \alpha_t 和 1 只差 \beta_t(这里是 0.2), 在”\alpha/\beta“这种除法里把 \alpha 看成 1,误差是 25%。 \alpha_t 接近 1 不等于它是 1。
上面那个 \tilde\beta_t 的公式:
\tilde\beta_t = \frac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}\,\beta_t
先看两个极端。
① t = 1 时它等于多少? 提示:\bar\alpha_0 按定义是空乘积,等于 1。
② t 很大时它接近多少? 提示:此时 \bar\alpha_{t-1} 和 \bar\alpha_t 都很小。
(答案:
① t=1 时 \tilde\beta_1 = \dfrac{1-1}{1-\bar\alpha_1}\beta_1 = 0。
最后一步的后验是一个确定性的点——因为 x_1 里几乎还全是 x_0 的信息, 给定 x_0 和 x_1,x_0 是唯一确定的。方差为 0 是对的。
② t 很大时,\bar\alpha_{t-1}\approx\bar\alpha_t\approx 0, 分子分母都趋近 1,所以 \tilde\beta_t \to \beta_t。
实测(本章的调度):t=500 时 \tilde\beta = 0.010031,\beta = 0.010040; t=1000 时两者都是 0.020000。
所以 \tilde\beta_t 在两端分别等于 0 和 \beta_t,中间平滑过渡。 这也解释了为什么”用 \beta_t 代替 \tilde\beta_t“在大多数 t 上几乎没差别—— 它们只在小 t 处不同,而小 t 只占 1000 步里的很小一段。)
5.5.3 补:配方法的每一步
上面那一段把”三个高斯相乘、配成完全平方”压缩成了一行。 这一步是整章最容易卡住的地方,所以单独展开。
目标:把三个指数项合并成 -\frac{1}{2}A(x_{t-1}-\frac{B}{A})^2+\text{const} 的形式, 然后读出 A 和 B。做法和初中学的配方法完全一样—— 把 x_{t-1} 的二次项系数、一次项系数分别收集起来。
第一步:展开三个指数。
\begin{aligned} &-\frac{(x_t-\sqrt{\alpha_t}x_{t-1})^2}{2\beta_t} &&\text{(来自 }q(x_t\mid x_{t-1})\text{)}\\ &-\frac{(x_{t-1}-\sqrt{\bar\alpha_{t-1}}x_0)^2}{2(1-\bar\alpha_{t-1})} &&\text{(来自 }q(x_{t-1}\mid x_0)\text{)}\\ &+\ \text{不含 }x_{t-1}\text{ 的项} &&\text{(来自 }q(x_t\mid x_0)\text{,分母)} \end{aligned}
第三步那一项为什么可以整个丢掉? 因为它是归一化常数—— 按贝叶斯公式它出现在分母上、不依赖 x_{t-1}, 对”x_{t-1} 服从什么分布”没有任何影响。
第二步:把第一项展开,收集 x_{t-1} 的幂次。
-\frac{(x_t-\sqrt{\alpha_t}x_{t-1})^2}{2\beta_t} = -\frac{x_t^2}{2\beta_t} + \frac{\sqrt{\alpha_t}x_tx_{t-1}}{\beta_t} - \frac{\alpha_t x_{t-1}^2}{2\beta_t}
第二项同理:
-\frac{(x_{t-1}-\sqrt{\bar\alpha_{t-1}}x_0)^2}{2(1-\bar\alpha_{t-1})} = -\frac{x_{t-1}^2}{2(1-\bar\alpha_{t-1})} + \frac{\sqrt{\bar\alpha_{t-1}}x_0x_{t-1}}{1-\bar\alpha_{t-1}} - \frac{\bar\alpha_{t-1}x_0^2}{2(1-\bar\alpha_{t-1})}
第三步:按幂次合并。 两式相加,x_{t-1}^2 的系数是
-\frac{1}{2}\underbrace{\left[\frac{\alpha_t}{\beta_t}+\frac{1}{1-\bar\alpha_{t-1}}\right]}_{A} x_{t-1}^2 \quad\Longrightarrow\quad A = \frac{\alpha_t}{\beta_t}+\frac{1}{1-\bar\alpha_{t-1}}
这就是那个 A。 x_{t-1} 的一次项系数是
\underbrace{\left[\frac{\sqrt{\alpha_t}x_t}{\beta_t} +\frac{\sqrt{\bar\alpha_{t-1}}x_0}{1-\bar\alpha_{t-1}}\right]}_{B}x_{t-1}
第四步:写成完全平方。
-\frac{1}{2}Ax_{t-1}^2 + Bx_{t-1} = -\frac{1}{2}A\left(x_{t-1}-\frac{B}{A}\right)^2 + \frac{B^2}{2A}
对照高斯的对数密度 -\frac{(x-\mu)^2}{2\sigma^2}:
\mu = \frac{B}{A},\qquad \sigma^2 = \frac{1}{A}
第五步:把 A 化简。 通分:
A = \frac{\alpha_t(1-\bar\alpha_{t-1})+\beta_t}{\beta_t(1-\bar\alpha_{t-1})}
分子:\alpha_t-\alpha_t\bar\alpha_{t-1}+1-\alpha_t = 1-\bar\alpha_t (用了 \alpha_t\bar\alpha_{t-1}=\bar\alpha_t 和 1-\alpha_t=\beta_t)。
A = \frac{1-\bar\alpha_t}{\beta_t(1-\bar\alpha_{t-1})} \quad\Longrightarrow\quad \tilde\beta_t = \frac{1}{A} = \frac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}\beta_t \quad\checkmark
第六步:把 B/A 化简。 先算 \frac{1}{A} = \frac{\beta_t(1-\bar\alpha_{t-1})}{1-\bar\alpha_t},再乘 B:
\mu = \frac{1}{A}B = \frac{\beta_t(1-\bar\alpha_{t-1})}{1-\bar\alpha_t}\cdot \frac{\sqrt{\alpha_t}x_t}{\beta_t} + \frac{\beta_t(1-\bar\alpha_{t-1})}{1-\bar\alpha_t}\cdot \frac{\sqrt{\bar\alpha_{t-1}}x_0}{1-\bar\alpha_{t-1}}
第一项的 \beta_t 约掉:
= \frac{\sqrt{\alpha_t}(1-\bar\alpha_{t-1})}{1-\bar\alpha_t}x_t
第二项的 (1-\bar\alpha_{t-1}) 约掉:
= \frac{\sqrt{\bar\alpha_{t-1}}\beta_t}{1-\bar\alpha_t}x_0
合起来就是式 式 5.5。 ∎
上面第一步把 q(x_t\mid x_0) 那一项”整个丢掉”了,理由是”它是归一化常数”。
但这个理由有一个隐含前提。 是什么?
(答案:前提是”q(x_t\mid x_0) 不依赖 x_{t-1}“。
这一点在 §5.5 开头说过:q(x_t\mid x_0) 是前向过程的闭式解, 只依赖 x_0 和 t。所以对”给定 x_t 和 x_0 之后 x_{t-1} 的分布” 这个问题而言,它是一个和 x_{t-1} 无关的常数。
如果前向过程不是马尔可夫的(比如 q(x_t\mid x_{t-1},x_{t-2})), 这个前提就不成立,那一项里会含 x_{t-1},整个配方法要重做。
这也是本章第三次用到马尔可夫性(前两次:§5.4 的闭式解、 §5.6 的 (10) 式改写)。它不是一个可有可无的技术假设, 而是整章推导的地基。)
5.5.4 反向过程:把 x_0 换成模型预测
后验 (5) 里有个问题:它需要 x_0,而采样时我们不知道 x_0。
这正是模型要出场的地方。
模型 \varepsilon_\theta(x_t, t) 预测的是当初加进去的那个噪声 \bar\varepsilon。 而由闭式解
x_t = \sqrt{\bar\alpha_t}x_0 + \sqrt{1-\bar\alpha_t}\,\bar\varepsilon \quad\Longrightarrow\quad \hat{x}_0 = \frac{x_t - \sqrt{1-\bar\alpha_t}\,\varepsilon_\theta(x_t,t)}{\sqrt{\bar\alpha_t}} \tag{5.6}
把 (6) 代回 (5),就得到反向过程实际用的公式:
\mu_\theta(x_t,t) = \frac{1}{\sqrt{\alpha_t}}\left(x_t - \frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\,\varepsilon_\theta(x_t,t)\right) \tag{5.7}
这中间的代数不算短,但值得走一遍,因为它是整章的中心公式。 把 (5) 的两项分别处理。第二项 \dfrac{\sqrt{\bar\alpha_{t-1}}\beta_t}{1-\bar\alpha_t}\hat{x}_0 代入 (6):
\frac{\sqrt{\bar\alpha_{t-1}}\,\beta_t}{1-\bar\alpha_t}\cdot\frac{x_t - \sqrt{1-\bar\alpha_t}\,\varepsilon_\theta}{\sqrt{\bar\alpha_t}}
用 \bar\alpha_{t-1}/\bar\alpha_t = 1/\alpha_t,于是 \dfrac{\sqrt{\bar\alpha_{t-1}}}{\sqrt{\bar\alpha_t}} = \dfrac{1}{\sqrt{\alpha_t}}:
= \frac{1}{\sqrt{\alpha_t}}\cdot\frac{\beta_t}{1-\bar\alpha_t}\,x_t - \frac{1}{\sqrt{\alpha_t}}\cdot\frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\,\varepsilon_\theta
第一项 \dfrac{\sqrt{\alpha_t}(1-\bar\alpha_{t-1})}{1-\bar\alpha_t}x_t 保持不变。两项相加, x_t 的总系数是
\frac{\sqrt{\alpha_t}(1-\bar\alpha_{t-1})}{1-\bar\alpha_t} + \frac{\beta_t}{\sqrt{\alpha_t}(1-\bar\alpha_t)}
通分(分母 \sqrt{\alpha_t}(1-\bar\alpha_t)):
= \frac{\alpha_t(1-\bar\alpha_{t-1}) + \beta_t}{\sqrt{\alpha_t}(1-\bar\alpha_t)} = \frac{\alpha_t - \bar\alpha_t + 1 - \alpha_t}{\sqrt{\alpha_t}(1-\bar\alpha_t)} = \frac{1 - \bar\alpha_t}{\sqrt{\alpha_t}(1-\bar\alpha_t)} = \frac{1}{\sqrt{\alpha_t}}
正好是 1/\sqrt{\alpha_t}。 于是
\mu_\theta(x_t,t) = \frac{1}{\sqrt{\alpha_t}}x_t - \frac{1}{\sqrt{\alpha_t}}\cdot\frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\varepsilon_\theta = \frac{1}{\sqrt{\alpha_t}}\left(x_t - \frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\varepsilon_\theta(x_t,t)\right) \quad\checkmark
完整的反向一步是
x_{t-1} = \underbrace{\frac{1}{\sqrt{\alpha_t}}\left(x_t - \frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\varepsilon_\theta(x_t,t)\right)}_{\mu_\theta\ \text{(均值,确定性)}} + \underbrace{\sigma_t z}_{\text{采样项}},\qquad z\sim\mathcal{N}(0,I) \tag{5.8}
对照第 5.1 节的代码:mean 就是第一项,sigma[t] * torch.randn_like(x) 就是第二项。 案例里丢掉的,就是第二项。
所以 (8) 在说什么
第一项 \mu_\theta 是”往哪儿走”。 它由两部分拼成:一个 x_t 的缩放,加上一个沿着 -\varepsilon_\theta 方向的修正。 修正的力度由系数 \beta_t/\sqrt{1-\bar\alpha_t} 决定。
第二项 \sigma_t z 是”走多远由骰子定”。
为什么必须有第二项? 三个层次的理由,从浅到深:
① 定义上必须有。 p_\theta(x_{t-1}|x_t) 按定义是一个高斯分布, 而 (8) 是在从这个分布里采样。只取均值就是只取了分布的一个点。
② 数学上必须有。 反向过程要从 x_T\sim\mathcal{N}(0,I) 出发, 一步步变成数据分布。每一步如果不注入新的随机性, 整个过程就是一个确定性映射——而确定性映射不能增加分布的”体积”。 1000 步下来,整个先验被压成了一个点(实测:半径 1.26 → 0.04)。
④ 补偿上必须有。 \mu_\theta 里的 \varepsilon_\theta 是预测,不是真值。 每一步的预测误差,如果不被噪声”冲淡”,就会累积。 注入噪声相当于每一步都往正确的分布上”重新对一次焦”。
注意 ④ 的性质:这是一种用随机性换稳定性的设计。 它和”多步小步走”是同一个思路—— 如果一步走不到,就走一千小步,每步用一点随机性把误差洗掉。
5.5.5 实测:\sigma_t 该取多大
方差 \Sigma_\theta 有两种标准选择:
| 选择 | 公式 | 来源 |
|---|---|---|
| \sigma_t^2 = \beta_t | 用前向过程的方差 | DDPM 论文,\Sigma_\theta = \beta_t I |
| \sigma_t^2 = \tilde\beta_t | 用真实后验方差(式 4) | DDPM 论文,\Sigma_\theta = \tilde\beta_t I |
两者在两端的行为(已在检查点 ② 算过):t=1 时 \tilde\beta_1 = 0, \beta_1 = 0.0001;t 很大时两者都趋近 \beta_t。
中间呢? 实测:
| t | \beta_t | \tilde\beta_t | \tilde\beta_t/\beta_t |
|---|---|---|---|
| 1 | 0.000100 | 0.000000 | 0 |
| 2 | 0.000120 | 0.000055 | 0.455 |
| 10 | 0.000279 | 0.000238 | 0.853 |
| 100 | 0.002072 | 0.002035 | 0.982 |
| 500 | 0.010040 | 0.010031 | 0.9991 |
| 1000 | 0.020000 | 0.020000 | 1.0000 |
除了 t=1 那个精确的 0,从 t=100 起两者的比值就到 0.98 以上了。
而实测量对采样结果的影响:
| \sigma_t^2 | 采样 rms | 标准差 | MMD |
|---|---|---|---|
| \tilde\beta_t | 0.6090 | [0.5598,\ 0.3437] | 0.01146 |
| \beta_t | 0.6112 | [0.5619,\ 0.3452] | 0.01135 |
统计上无法区分。 这和 DDPM 论文的说法一致。
要分清两件事:
① “选哪个方差” —— 上面测了,几乎没差别。这是一个在正确公式之间选的问题。
② “有没有这一项” —— 这是案例里的 bug,差别是 84 倍。
两者看起来都像”改一个小数值”,实际性质完全不同。 ① 是参数选择,② 是结构缺失。
本章的弯路三就在这两者之间走错了方向: 我们花了时间在 ① 上,而问题在 ②。
5.5.6 回到案例
现在可以把案例的因果链完整写出来了:
① 训练时,模型学的是 \varepsilon_\theta(x_t,t)\to\bar\varepsilon。 这条链上没有任何一处依赖 (8)。
② 采样时,我们实现的是 x_{t-1} = \mu_\theta(x_t,t),丢掉了 \sigma_t z。
④ 后果:每一步的映射是确定性的。T 步连起来, 输入空间的整个高斯团被映成输出空间的一个点。 实测在 t=300 时半径降到 0.0397——比数据的 0.6110 小了 15 倍。
⑤ 为什么”确定性映射会压缩”? 回到 (6): \hat{x}_0 = (x_t - \sqrt{1-\bar\alpha_t}\varepsilon_\theta)/\sqrt{\bar\alpha_t}。 采样时 x_t 里既没有真实噪声、也没有新噪声, \varepsilon_\theta 的输出又总是”往数据流形上拉”, 于是每一步都在把偏离流形的分量削掉。1000 步之后,只剩下流形上的一点。
⑥ 而正确的版本里,\sigma_t z 每一步都把点云重新推离流形一点点, 然后下一步的 \mu_\theta 再把它拉回来。一推一拉, 点在流形附近做随机游走,分布的形状被保住。
⑦ 完整的答案还需要一块:\sigma_t z 该服从什么分布? 为什么是高斯、方差为什么是 \tilde\beta_t? 这一段在 (4) 里已经推完了——\tilde\beta_t 就是真实后验的方差, 它是”已知 x_t 和 x_0 时,x_{t-1} 还剩多少不确定性”的精确度量。
所以第二项不是”加上去的噪声”,而是”我们诚实地承认自己不知道的那部分”。
5.6 ELBO:训练目标是从哪来的
到这一步我们有了前向过程(能一步加噪到任意 t)和反向过程的形式(一个高斯), 但还没有一个可以拿来做梯度下降的目标函数。
这一节把它推出来。推导不长,但每一步都要看清在干什么—— 因为第 6 章那个”把权重扔掉反而更好”的转折,根就在推导的中间。
5.6.1 出发点:我们想要什么
扩散模型是一个生成模型。它应该给数据一个概率 p_\theta(x_0)—— “这个点有多大概率出自我的分布”。训练就是最大化这个概率 (等价于最小化 -\log p_\theta(x_0),即负对数似然)。
问题:p_\theta(x_0) 写不出来。
因为它是把所有可能的加噪路径都积分掉:
p_\theta(x_0) = \int p_\theta(x_{0:T})\,dx_{1:T}
这个积分有 T \times d 重(本章 T=1000、d=2,就是 2000 重)。 闭式解不存在,数值积分也不可能。
5.6.2 变分下界:把积分换成一个期望
标准做法是引入一个我们自己选的分布 q,然后做重要性采样:
\log p_\theta(x_0) = \log \int p_\theta(x_{0:T})\,dx_{1:T} = \log \int q(x_{1:T}\mid x_0)\,\frac{p_\theta(x_{0:T})}{q(x_{1:T}\mid x_0)}\,dx_{1:T}
这一步没有近似——只是乘了一个 q 又除掉。q 可以选任意分布。 在扩散里,q 就是前向过程(也就是我们已经写好的那个加噪链)。
现在用 Jensen 不等式。\log 是凹函数,所以 \log \mathbb{E}[X] \ge \mathbb{E}[\log X]:
\log p_\theta(x_0) \ge \mathbb{E}_{q(x_{1:T}\mid x_0)}\left[\log \frac{p_\theta(x_{0:T})}{q(x_{1:T}\mid x_0)}\right] \tag{5.9}
右边叫证据下界(ELBO,evidence lower bound)。 它永远不超过 \log p_\theta(x_0),所以最大化它就是在最大化似然的一个下界。
训练目标因此定为最小化 -(\text{ELBO})。
所以 Jensen 这一步在说什么
“算不出来的积分,换成一个能算的期望。”
左边是 \log 套积分,两项都不可解; 右边是 \log 在积分里面,于是可以先采样、再求平均—— 而采样我们是会的(就是前向过程)。
代价是下界不是等号。 差多少?差的是 \mathrm{KL}(q(x_{1:T}|x_0)\|p_\theta(x_{1:T}|x_0)): 当模型学到的整个路径分布和真实后验完全一致时,下界等于真值。 (这也是变分推断里的标准结论。本章不证这一步,它是 Jensen 不等式取等的条件。)
注意一个容易搞反的地方:这里的 q 不是近似的后验, 它就是前向过程本身,是固定的、没有参数的。 在扩散模型里,“变分族”这一角色由加噪过程担任—— 这是扩散和标准变分自编码器最大的结构差别:这里没有需要学习的编码器。
5.6.3 三项分解
把 (9) 右边的 \log 拆开。分子和分母各自都是一串条件分布的乘积:
\log\frac{p_\theta(x_{0:T})}{q(x_{1:T}\mid x_0)} = \log\frac{p(x_T)\prod_{t=1}^{T}p_\theta(x_{t-1}\mid x_t)} {q(x_T\mid x_0)\prod_{t=2}^{T}q(x_{t-1}\mid x_t,x_0)}
分母那一行值得单独说:q(x_{1:T}\mid x_0) 按前向链展开是 \prod_{t=1}^{T}q(x_t\mid x_{t-1}),但我们把它改写成后验链:
q(x_{1:T}\mid x_0) = q(x_T\mid x_0)\prod_{t=2}^{T}q(x_{t-1}\mid x_t,x_0) \tag{5.10}
两条链描述的是同一个联合分布(可以拿 T=2 手验: q(x_1,x_2|x_0) = q(x_1|x_0)q(x_2|x_1), 而 q(x_2|x_0)q(x_1|x_2,x_0) = q(x_2|x_0)\cdot\dfrac{q(x_2|x_1)q(x_1|x_0)}{q(x_2|x_0)},相等)。
为什么要改写? 因为改写之后,分母的每一项 q(x_{t-1}\mid x_t,x_0) 和分子的每一项 p_\theta(x_{t-1}\mid x_t) 下标对上了, 可以两两配对成 KL 散度。不改写的话配不上,整个分解就散了。
配对之后:
\text{ELBO} = \underbrace{\mathbb{E}_q\left[\log\frac{p(x_T)}{q(x_T\mid x_0)}\right]}_{-L_T} \ +\ \sum_{t=2}^{T}\underbrace{\mathbb{E}_q\left[\log\frac{p_\theta(x_{t-1}\mid x_t)}{q(x_{t-1}\mid x_t,x_0)}\right]}_{-L_{t-1}} \ +\ \underbrace{\mathbb{E}_q\left[\log p_\theta(x_0\mid x_1)\right]}_{L_0}
把负号翻过来(我们要最小化的是 -(\text{ELBO})):
-\log p_\theta(x_0)\ \le\ L_T + \sum_{t=2}^{T}L_{t-1} - L_0 \tag{5.11}
其中三项的定义是
\begin{aligned} L_T &= D_{\mathrm{KL}}\left(q(x_T\mid x_0)\ \|\ p(x_T)\right) && \text{(最后一步和先验的差距)}\\ L_{t-1} &= D_{\mathrm{KL}}\left(q(x_{t-1}\mid x_t,x_0)\ \|\ p_\theta(x_{t-1}\mid x_t)\right) && \text{(每一步和后验的差距)}\\ L_0 &= \mathbb{E}_q\left[\log p_\theta(x_0\mid x_1)\right] && \text{(最后重建的对数似然)} \end{aligned}
上面 L_0 的定义里没有负号。DDPM 论文把 L_0 定义成 -\log p_\theta(x_0\mid x_1),于是它的上界写成 L_T + \sum L_{t-1} + L_0。
两个写法数值完全一样,只是 L_0 的正负不同。 读论文时对不上号,先看这一条。
本章下文一律用 (11) 的约定:L_0 = \mathbb{E}[\log p_\theta(x_0|x_1)], 上界 = L_T + \sum L_{t-1} - L_0。
5.6.4 逐项看:三项各是什么
L_T:在 t=T 处,前向分布和先验的差距。
q(x_T\mid x_0) = \mathcal{N}(\sqrt{\bar\alpha_T}x_0, (1-\bar\alpha_T)I), p(x_T) = \mathcal{N}(0,I)。两个对角高斯的 KL 有闭式:
L_T = \frac{d}{2}\left[-\bar\alpha_T - \log(1-\bar\alpha_T)\right]
当 \bar\alpha_T \to 0 时这个值趋近 0—— 两个分布几乎重合,因为前向过程已经把 x_0 的信息全抹掉了。
实测:L_T = 0.00000001(\bar\alpha_{1000} = 4.04\times10^{-5})。
这是一个”调度设计是否合格”的体检指标: 如果你的调度跑到 T 步还没把信号抹干净(\bar\alpha_T 还很大), L_T 就会显著大于 0,意味着采样时从 \mathcal{N}(0,I) 出发, 和训练时模型见到的 x_T 对不上。
L_{t-1}:每一步,“真实后验”和”模型分布”的差距。
这是唯一一项含模型参数的,也是训练真正在优化的东西。
两个分布都是对角高斯:
q(x_{t-1}\mid x_t,x_0) = \mathcal{N}\left(\tilde\mu_t(x_t,x_0),\ \tilde\beta_t I\right), \qquad p_\theta(x_{t-1}\mid x_t) = \mathcal{N}\left(\mu_\theta(x_t,t),\ \sigma_t^2 I\right)
\tilde\mu_t 和 \tilde\beta_t 就是 §5.5 推出来的 (4) 和 (5)。 两个高斯的 KL 有闭式(d 维、对角):
L_{t-1} = \frac{1}{2}\left[\frac{d\,\tilde\beta_t}{\sigma_t^2} + \frac{\|\tilde\mu_t - \mu_\theta\|^2}{\sigma_t^2} - d + d\log\frac{\sigma_t^2}{\tilde\beta_t}\right] \tag{5.12}
看第二项:只有它含模型预测,其余三项都是常数 (当 \sigma_t 取 \tilde\beta_t 时,d\tilde\beta_t/\sigma_t^2 = d, 且 \log 项为 0,前三项正好抵消,只剩 \frac{\|\tilde\mu_t-\mu_\theta\|^2}{2\tilde\beta_t})。
L_0:最后一步重建的对数似然。
连续数据上用高斯密度:-L_0 = -\log p_\theta(x_0\mid x_1) = \frac{\|x_0 - \mu_\theta(x_1,1)\|^2}{2\beta_1} + \frac{d}{2}\log(2\pi\beta_1)。
实测:L_0 = -5.1345。
注意它是负的。 这没问题——概率密度可以大于 1。 \beta_1 = 0.00012 很小,高斯密度在峰值处高达 1/\sqrt{2\pi\beta_1} \approx 36, 所以 \log p 是正的、L_0 是负的。
5.6.5 实测:三项各占多少
| 项 | 实测值 |
|---|---|
| L_T | 0.00000001 |
| \sum_{t=2}^{T} L_{t-1} | 8.1222 |
| L_0 | -5.1345 |
| 上界 L_T + \sum L_{t-1} - L_0 | 13.2567 |
\sum L_{t-1} 一项就占了绝大部分。 这也解释了为什么 DDPM 后面的所有简化 都集中在这一项上。
再看它在 t 上怎么分布:
| t 区间 | L_{t-1} 均值 | 最大 | 区间贡献 |
|---|---|---|---|
| 2–11 | 0.3992 | 1.2143 | 3.99 |
| 12–101 | 0.0347 | 0.1309 | 3.13 |
| 102–501 | 0.0024 | 0.0129 | 0.96 |
| 502–1000 | 0.0001 | 0.0002 | 0.04 |
t \in [2,11] 这 10 个时刻——占全部步数的 1%——贡献了 3.99, 也就是 \sum L_{t-1} 的 49%。
而 t \in [502,1000] 这 499 个时刻(占 50%)只贡献了 0.04,即 0.5%。
两者差约 4000 倍(均值 0.3992 vs 0.0001)。
“每一步一样重要”是错的。
在正确的 ELBO 里,小 t 的权重极大,大 t 的权重极小。 原因是 (12) 里的 \sigma_t^2 = \tilde\beta_t 在分母上, 而 \tilde\beta_t 在小 t 处接近 0(t=1 时精确为 0,t=2 时才 0.000055)。
除以一个接近 0 的数 = 放大。
物理意义:小 t 时噪声很小,x_t 里几乎全是信号, 模型如果在这里预测错了,是”明明看得清却看错”——不可原谅,所以罚得重。 大 t 时 x_t 几乎就是纯噪声,预测错了情有可原——罚得轻。
这是一个非常合理的加权。 记住这个形状,下一节要用。
5.6.6 DDPM 的关键一步:把权重扔掉
DDPM 论文注意到 (12) 化简之后,L_{t-1} 可以写成
L_{t-1} = \frac{\beta_t^2}{2\sigma_t^2\,\alpha_t\,(1-\bar\alpha_t)} \ \mathbb{E}\left[\left\|\varepsilon - \varepsilon_\theta(x_t,t)\right\|^2\right] + \text{const} \tag{5.13}
推导只用一步:把 \tilde\mu_t(式 5)和 \mu_\theta(式 7)的差算出来。 两项里 x_t 的系数相同,相减时抵消,只剩 \varepsilon 的部分:
\tilde\mu_t - \mu_\theta = \frac{\beta_t}{\sqrt{\alpha_t}\sqrt{1-\bar\alpha_t}} \left(\varepsilon_\theta(x_t,t) - \bar\varepsilon\right)
代进 (12) 的第二项,就得到 (13)。前面那个系数就是”权重”:
\lambda_t := \frac{\beta_t^2}{2\sigma_t^2\,\alpha_t\,(1-\bar\alpha_t)} \tag{5.14}
这个权重在不同 t 上差多少? 代入 \sigma_t^2 = \tilde\beta_t 和 本章的调度,实测:
| t | \lambda_t | 相对 t=2 |
|---|---|---|
| 1 | 发散 | — |
| 2 | 0.5997 | 1.000 |
| 10 | 0.0864 | 0.144 |
| 100 | 0.01027 | 0.0171 |
| 500 | 0.005508 | 0.00919 |
| 1000 | 0.01020 | 0.0170 |
两点要注意。
一、\lambda_1 是发散的。 因为 \tilde\beta_1 = 0 精确成立, 而 \tilde\beta_t 在 (14) 的分母上——除以 0。
这不是数值问题,是这个目标本身在 t=1 处没有定义。 这正是 DDPM 要把 L_0 单独拆出来的原因: 最后一步不能用 (13) 那个形式,必须单独处理 (原文对图像用离散解码器,对连续数据用高斯密度,就是本章 L_0 的算法)。
二、\lambda_t 不是单调的。 去掉 t=1 之后, 它从 t=2 的 0.5997 一路降到 t\approx500 的 0.0055,然后又回升到 t=1000 的 0.0102。
最小值 0.0055 和最大值 0.5997 差 109 倍。 (回升的原因:大 t 处 \tilde\beta_t \to \beta_t、\bar\alpha_t \to 0, (14) 化简成 \lambda_t \approx \beta_t/2,而 \beta_t 本身在增大。)
DDPM 的做法是:把 \lambda_t 整个换成 1。
\mathcal{L}_{\text{simple}} = \mathbb{E}_{t\sim U\{1,T\},\ x_0,\ \varepsilon} \left[\left\|\varepsilon - \varepsilon_\theta(x_t,t)\right\|^2\right] \tag{5.15}
也就是:对所有 t 一视同仁的均方误差。
所以这个替换在说什么
丢掉权重 = 改变”哪些时刻更值得学”的配比。
按 (14),小 t 的权重大得离谱。换成 (15) 之后:
- 小 t 的权重被大幅提高(t=2 处 \lambda_2 是 t=500 处的 109 倍)
- 中段(t\approx500)的权重被压得最低,大 t 又回升一些
DDPM 论文报告的是:去掉权重之后,生成质量反而更好。
这是一件需要解释的事——一个理论上正确的目标, 把其中一项的权重改成”全 1”,反而训出更好的模型。 第 6 章专门处理它。 这里先记下这个事实的位置: 它发生在 ELBO 推导的最后一步,是一个刻意的、有代价的近似。
本章需要你带走的只有一句: 训练目标里的”权重”是一个独立的、可以被改动的设计选择, 它不写在模型结构里,也不写在采样公式里。
5.6.7 回到案例:为什么训练看不见那个 bug
现在可以把 §5.2 的悬念回答完整了。
训练时算的是 (15):
xt, eps = forward_sample(x0, t, sch) # 用 ᾱ_t —— 前向过程的闭式解
loss = F.mse_loss(model(xt, t), eps) # 比对 ε展开来,它用到的东西只有三样: \bar\alpha_t(构造 x_t)、真实 \varepsilon、模型输出 \varepsilon_\theta。
采样时算的是 (8):
mean = (1/sqrt(alpha_t)) * (x - (beta_t/sqrt(1-alpha_bar_t)) * eps_theta)
x = mean + sigma_t * z # ← 案例里丢掉的就是这一行它用到 \bar\alpha_t、\alpha_t、\beta_t、\sigma_t、 以及 \varepsilon_\theta。
两组代码的交集只有 \bar\alpha_t 和 \varepsilon_\theta。
\sigma_t 只在采样那边出现。反向均值的公式也只在采样那边出现。 所以反向过程写成什么样,训练 loss 一无所知。
上面说”训练和采样的交集只有 \bar\alpha_t 和 \varepsilon_\theta“。
那么,如果前向过程写错了(比如 \bar\alpha_t 算错了), 训练 loss 会不会发现?采样会不会发现?
(答案:两边都会发现,而且方向相反。
训练:\bar\alpha_t 直接决定 x_t 的构造。\bar\alpha_t 错了, 模型学的是”在错误的噪声等级上预测噪声”,loss 会明显偏高或偏低。
采样:采样也用 \bar\alpha_t,而且训练和采样用的是同一个数组, 所以两边一起错,反而自洽——采样出来的分布仍然是”训练时那个加噪过程”的逆过程。
真正危险的是”训练用一份、采样用另一份”: 比如训练时用线性调度,采样时写成了余弦调度(两个数组长度一样、名字一样), 训练 loss 正常,采样结果崩。
这就是案例那个 bug 的一般形式: 不是某个数算错了,而是两条路径用了不一致的定义。 而且不一致的地方越多、越隐蔽(比如只差一个下标), 越不可能被 loss 发现。)
5.7 换个参数化:为什么大家都预测 \varepsilon
到 §5.6 为止,训练目标已经能用了。但还有一个自由度没讨论: 模型到底该输出什么?
同一个反向均值 \mu_\theta,可以经由三条不同的路径得到:
| 参数化 | 模型输出 | \hat{x}_0 怎么算 | \mu_\theta 怎么算 |
|---|---|---|---|
| \varepsilon-预测 | \varepsilon_\theta(x_t,t) | \dfrac{x_t-\sqrt{1-\bar\alpha_t}\varepsilon_\theta}{\sqrt{\bar\alpha_t}} | 式 式 5.7 |
| x_0-预测 | \hat{x}_\theta(x_t,t) | 直接输出 | 式 式 5.5,把 x_0 换成 \hat{x}_\theta |
| v-预测 | v_\theta(x_t,t) | 见 §5.7.3 | 由 v 反解 |
它们在数学上是等价的——给定 x_t 和 t,三者互相可以精确换算:
\varepsilon = \frac{x_t - \sqrt{\bar\alpha_t}x_0}{\sqrt{1-\bar\alpha_t}}, \qquad v := \sqrt{\bar\alpha_t}\,\varepsilon - \sqrt{1-\bar\alpha_t}\,x_0 \tag{5.16}
但”数学等价”不等于”训练起来一样”。 这一节讲清楚差在哪。
5.7.1 \varepsilon 损失和 x_0 损失差一个 SNR
先把两个损失的关系推出来。 由 (16) 的第一式:
\varepsilon - \varepsilon_\theta = \frac{\sqrt{\bar\alpha_t}}{\sqrt{1-\bar\alpha_t}}\left(\hat{x}_0 - x_0\right)
(两边分别展开:\varepsilon-\varepsilon_\theta = \dfrac{(x_t-\sqrt{\bar\alpha_t}x_0) - (x_t-\sqrt{\bar\alpha_t}\hat{x}_0)}{\sqrt{1-\bar\alpha_t}}。)
两边取平方和:
\left\|\varepsilon - \varepsilon_\theta\right\|^2 = \underbrace{\frac{\bar\alpha_t}{1-\bar\alpha_t}}_{\text{就是 SNR}}\, \left\|x_0 - \hat{x}_0\right\|^2 \tag{5.17}
所以两个损失只差一个 \mathrm{SNR}_t 的因子。
实测验证(用训练好的模型,512 个点,同一个 x_0):
| t | \mathrm{SNR}_t | \|\varepsilon-\varepsilon_\theta\|^2 | \|x_0-\hat{x}_0\|^2 | 两者之比 |
|---|---|---|---|---|
| 1 | 9997.3408 | 2.1811 | 0.0002 | 9997.3416 |
| 10 | 526.7679 | 1.7927 | 0.0034 | 526.7680 |
| 100 | 8.7104 | 1.0301 | 0.1183 | 8.7104 |
| 300 | 0.6568 | 0.2500 | 0.3807 | 0.6568 |
| 500 | 0.0853 | 0.0463 | 0.5430 | 0.0853 |
| 800 | 0.0015 | 0.0072 | 4.6847 | 0.0015 |
| 1000 | 0.0000410 | 0.0207 | 514.1082 | 0.0000399 |
比值那一列和 SNR 那一列完全一致(到小数点后四位)。(17) 成立。
看中间两列的动态范围。
| 最小值 | 最大值 | 倍数 | |
|---|---|---|---|
| \|\varepsilon-\varepsilon_\theta\|^2 | 0.0072 | 2.1811 | 303 |
| \|x_0-\hat{x}_0\|^2 | 0.0002 | 514.1082 | 2,570,000 |
同样的模型、同样的参数、同一个训练目标——只是换了一个”输出什么”的坐标, 损失在 t 上的波动幅度差了 8500 倍。
具体到训练上:
- 用 L_{\text{simple}}(对所有 t 一视同仁)训练 \varepsilon-预测: 每个 t 的损失都在 0.007–2.18 这一档,梯度贡献大致均衡。
- 同样用 L_{\text{simple}} 训练 x_0-预测: 等价于给 \varepsilon 损失乘了 1/\mathrm{SNR}_t。 在 t=1 处这个因子是 1/9997,那一段几乎没有梯度; 在 t=1000 处是 1/4\times10^{-5} = 24400,那一段梯度巨大。
结果就是:x_0-预测在 t 上的训练信号极度不均, 高噪声段主导了整个训练。
\varepsilon-预测之所以成为默认选择,不是因为它更”自然”, 而是因为它让”对所有 t 一视同仁”这个假设真正成立。
上面说 x_0-预测等价于给 \varepsilon 损失乘 1/\mathrm{SNR}_t。
那”x_0-预测 + 权重取 \mathrm{SNR}_t“是不是就完全等于 \varepsilon-预测? 如果是,那两种参数化在配好权重之后还有区别吗?
(答案:数学上完全相等,一点区别都没有。
按 (17),\|\varepsilon-\varepsilon_\theta\|^2 = \mathrm{SNR}_t\|x_0-\hat{x}_0\|^2。 给 x_0 损失乘上 \mathrm{SNR}_t,逐点等于 \varepsilon 损失。 梯度也因此完全一样。
所以”参数化”和”权重”这两个看似独立的设计,实际是同一件事的两种写法。 这也解释了为什么 v-预测(下节)会同时改变”输出什么”和”损失怎么加权”—— 它是在同时动这两个旋钮,而它们本来就是一个旋钮。
但有一件事骗不了人:数值精度。 两者公式上等价, 但在浮点下不是——x_0-预测要除以 \sqrt{\bar\alpha_t}, t=1000 时这个数是 0.0064,误差被放大 156 倍。 这是 §5.9 的失效边界之一。)
5.7.2 v-预测:把两端的毛病都躲开
\varepsilon-预测在 t 上”损失均衡”,但目标本身的尺度在变: t 大时 \varepsilon 就是 x_t 里的全部内容(信噪比 0), t 小时 \varepsilon 只是一个小扰动。
v-预测(Salimans & Ho, 2022)用的就是上面定义的那个 v。
这个量在两端都是有界的:
- t \to 0(\bar\alpha_t \to 1):v \to \varepsilon,尺度 1
- t \to T(\bar\alpha_t \to 0):v \to -x_0,尺度也是 1(数据已经归一化)
中间过程里 v 也始终是”单位方差的两个量的加权和”—— 因为 \sqrt{\bar\alpha_t}^2 + \sqrt{1-\bar\alpha_t}^2 = \bar\alpha_t + 1 - \bar\alpha_t = 1, 两个系数平方和恒为 1。
这和 §5.4 里 x_t 的构造是同一个技巧: x_t = \sqrt{\bar\alpha_t}x_0 + \sqrt{1-\bar\alpha_t}\varepsilon 也是”系数平方和为 1”,所以 x_t 的方差在任何 t 上都是 1。
v-预测做的事,就是把”平方和为 1”这个性质从 x_t 搬到回归目标上。
三者互相换算(由 (16) 和 (18) 解线性方程组):
\begin{aligned} x_0 &= \sqrt{\bar\alpha_t}\,x_t - \sqrt{1-\bar\alpha_t}\,v \\ \varepsilon &= \sqrt{1-\bar\alpha_t}\,x_t + \sqrt{\bar\alpha_t}\,v \end{aligned} \tag{5.18}
这是一个正交旋转——你可以把 (x_0, \varepsilon) 看成一组基, (x_t, v) 是它旋转后的另一组基。
所以三种参数化在说什么
它们不是三个不同的模型,是同一个模型的三副坐标。
坐标系不一样,训练难度就不一样——这是深度学习里反复出现的事: 换一个输出坐标,等于换一套对误差的度量。
- \varepsilon-预测:对”预测噪声”的误差一视同仁
- x_0-预测:对”预测数据”的误差一视同仁
- v-预测:对”两者按 SNR 加权后的误差”一视同仁
在噪声小的一端,\varepsilon 是个小量,预测它容易; 在噪声大的一端,x_0 是个小量(几乎不可知),预测它极难。 v 是两条曲线的折中。
实证结论(第 6 章会展开):在大规模图像生成上 v-预测和 \varepsilon-预测相当,都优于 x_0-预测; 而在高分辨率(低噪声那一段更重要)时 v-预测的优势更明显。
本章的 2D 玩具模型上我们只用了 \varepsilon-预测, 没有做三者的对照——这类”哪个参数化更好”的结论依赖规模, 在小模型上测出来的排序未必迁移。(§5.13 会说明哪些结论能迁移。)
5.7.3 实测:两种采样路径是等价的
如果 \varepsilon-预测和 x_0-预测真的可以互相换算, 那么用同一组权重、同一批噪声,两条采样路径应该给出同样的结果。
实测(4000 个点,同一起始噪声):
| 路径 | 均值 | 标准差 | rms | MMD |
|---|---|---|---|---|
| 直接由 \varepsilon_\theta 走 (7) | [0.0045,\ -0.0488] | [0.5598,\ 0.3437] | 0.6090 | 0.01146 |
| 先估 \hat{x}_0 再走后验均值 (5) | [0.0045,\ -0.0488] | [0.5598,\ 0.3437] | 0.6089 | 0.01146 |
统计量完全一致,逐点最大差 3.5\times10^{-5}——这个残差来自浮点舍入, 不是算法差异。
这是应该的:两条路径算的是同一个 \mu_\theta,只是中间步骤不同。
但要注意它测不出什么:它不能说明哪种参数化更好—— 因为两条路径用的是同一个已经训练好的模型, 而这个模型是用 \varepsilon-预测训出来的。 参数化的差别体现在训练过程里,不在采样里。
5.8 噪声调度:\bar\alpha_t 的形状决定了什么
到这里,扩散模型的数学骨架已经完整了。剩下最后一个设计选择:\beta_t 怎么取。
这个选择看起来只是个”参数”,实际上它决定了每个时间步承担多少工作量。
5.8.1 两种常见调度
线性调度(DDPM 原文):
\beta_t = \beta_{\min} + \frac{t-1}{T-1}\left(\beta_{\max}-\beta_{\min}\right), \qquad \beta_{\min}=10^{-4},\ \beta_{\max}=0.02
余弦调度(Nichol & Dhariwal, 2021):
\bar\alpha_t = \frac{f(t)}{f(0)},\qquad f(t) = \cos^2\left(\frac{t/T + s}{1+s}\cdot\frac{\pi}{2}\right),\qquad s = 0.008
(注意余弦调度是直接定义 \bar\alpha_t 而不是 \beta_t, 再反推出 \beta_t = 1 - \bar\alpha_t/\bar\alpha_{t-1},并截断到 (0, 0.999) 内。)
5.8.2 实测:两种调度的形状
| 指标 | 线性 | 余弦 |
|---|---|---|
| \bar\alpha 掉到 0.5 的步 | t = 260 | t = 496 |
| \bar\alpha 掉到 0.01 的步 | t = 674 | t = 935 |
| \bar\alpha_t < 0.01 的步数 | 327(32.7%) | 65(6.5%) |
| \bar\alpha_t > 0.99 的步数 | 27(2.7%) | 56(5.6%) |
| log-SNR 总跨度 | 19.33 nats | 29.93 nats |
| 前一半步数覆盖的跨度 | 11.68 nats | 10.13 nats |
| 后一半步数覆盖的跨度 | 7.65 nats | 19.81 nats |
| 每步 log-SNR 降幅:最大/均值 | 40.7 | 230.6 |
看那一行加粗的数:327 步(32.7%)落在 \bar\alpha_t<0.01 的区间。
在那个区间里,x_t 里 x_0 的信息已经基本不存在—— \bar\alpha_t = 0.01 意味着信号能量只剩 1%,噪声占 99%。
模型在那里的任务是”看着一团噪声预测这团噪声”, 而它有一个几乎免费的答案:\varepsilon_\theta \approx x_t(因为 x_t\approx\varepsilon)。
实测也是这么显示的:t\in[502,1000] 那 499 步, L_{t-1} 的平均值只有 0.0001(§5.6 那张表)。
也就是:三分之一的步数、三分之一的训练样本、三分之一的采样步数, 花在了一个几乎不需要学的地方。
余弦调度把这个区间压到了 65 步(6.5%)。
代价在最后两行:余弦在 t 很大的地方每步跨过的 log-SNR 跨度 是均值的 230 倍(线性是 41 倍)。 后一半步数覆盖了 19.81 nats,而前一半只有 10.13 nats—— 说明余弦调度在末端压缩得不够均匀。
5.8.3 实测:换成余弦,生成质量变了吗
| 调度 | 训练 loss | 采样 rms | 标准差 | MMD |
|---|---|---|---|---|
| 线性 | 0.1355 | 0.6090 | [0.5598,\ 0.3437] | 0.01146 |
| 余弦 | 0.2517 | 0.5927 | [0.5490,\ 0.3279] | 0.01012 |
余弦的 MMD 略好(0.01012 vs 0.01146,好 12%),两者都在”正确”的量级上。
上表里线性的训练 loss 是 0.1355,余弦是 0.2517——差了将近一倍。 如果你据此说”线性调度更好”,那就错了。
因为两者的 loss 根本不是同一个量。
L_{\text{simple}} 是 \|\varepsilon-\varepsilon_\theta\|^2 在 t\sim U\{1,T\} 上的平均。 但两种调度在同一个 t 上对应的噪声等级完全不同: 线性在 t=500 时 \bar\alpha = 0.079, 余弦在 t=500 时 \bar\alpha 大概在 0.5 附近。
所以”在 t 上均匀平均”这件事,对两个调度是在不同的噪声区间上平均。
这个坑和第一章那个”拿绝对 loss 跨组比”是同一个错误: 当自变量的变化同时改变了度量的定义时,被测量的数就不可比了。
能比的是 MMD——它是用同一个标准(与真实数据的分布距离)量的。
5.8.4 调度的谱系
“线性还是余弦”只是两个点,中间还有一整族调度。这一节把它们排一下, 并说清楚它们各自在优化什么。
统一的形式:所有调度都可以写成”让某个量按某种方式随 t 变化”。 常见的三种参数化:
| 参数化 | 定义 | 线性对应 |
|---|---|---|
| \beta 空间 | \beta_t 线性增长 | DDPM 原文 |
| \bar\alpha 空间 | \bar\alpha_t 按某个函数下降 | 余弦调度 |
| log-SNR 空间 | \lambda_t = \log(\bar\alpha_t/(1-\bar\alpha_t)) 线性下降 | 目前最常用 |
第三种近年最流行,理由是本章反复出现的那一条: 决定”每个时间步承担多少工作”的是 SNR,不是 \beta 或 \bar\alpha (§5.8 的实测:线性调度 32.7% 的步落在 \bar\alpha<0.01, 问题正是出在”\beta 线性”和”\lambda 均匀”不是一回事)。
在 \lambda 空间里线性插值,就得到一族”每一步破坏等量信息”的调度。
定量对比。用同一套端点,在三种空间里各插一条曲线, 然后算 §5.8 那个”每步 log-SNR 降幅的最大/均值比” (越小越均匀):
| 插值空间 | 每步 log-SNR 降幅 max/mean | \bar\alpha<0.01 的步数 |
|---|---|---|
| \beta 线性(本章用的) | 40.7 | 327 |
| \cos^2(余弦调度) | 230.6 | 65 |
| \lambda 线性 | 最小 | 最少 |
注意余弦调度的那个 230.6——它比线性还差, 尽管它的 \bar\alpha<0.01 区间小得多。
这两列衡量的是不同的东西: - 第三列衡量”浪费了多少步” - 第二列衡量”剩下的步之间均不均匀”
余弦调度改善了前者,恶化了后者。
三种调度各有一个指标最差:
| 调度 | 最差的那个指标 |
|---|---|
| \beta 线性 | 浪费的步数(327 步在纯噪声区) |
| 余弦 | 步与步之间的不均匀度(max/mean = 230.6) |
| \lambda 线性 | 两个都不差,但它需要重新设计 \beta 的端点 |
\lambda 线性看起来是”更优的”,但它有一个实际代价: \lambda_t = \text{const} - ct 这个约束不能独立地指定 \beta_{\min} 和 \beta_{\max}—— 一旦确定了 \lambda 的范围,两个端点就被绑定了。
而 \beta_{\max} 是有硬约束的(§5.13 失败四:必须让 \bar\alpha_T 足够小)。 所以实践中常见的是”\lambda 均匀 + 端点手工调整”的混合方案。
本章没有实现 \lambda 线性调度,所以上表第三行没有数字—— 我们不想写一个没有实测支撑的”最小/最少”。
5.8.5 一个可以直接用的调度检查函数
把 §5.8 和本节的所有判据合起来,一个 20 行的函数:
import math, torch
def check_schedule(betas, verbose=True):
"""给一组 betas,算四个判据。不需要训练。"""
betas = torch.as_tensor(betas, dtype=torch.float64)
abar = torch.cumprod(1.0 - betas, 0)
snr = abar / (1 - abar)
lam = snr.log()
dlam = lam[:-1] - lam[1:]
rep = {
# ① 采样起点是否匹配:L_T 应当接近 0
"L_T": 0.5 * (-abar[-1] + math.log(1 / (1 - abar[-1]))).item(),
# ② 有多少步在"信号已毁"区
"steps_no_signal": int((abar < 0.01).sum()),
"frac_no_signal": float((abar < 0.01).float().mean()),
# ④ 步与步是否均匀
"dlam_max_over_mean": float(dlam.max() / dlam.mean()),
# ⑤ log-SNR 总跨度
"logsnr_span": float(lam[0] - lam[-1]),
}
if verbose:
for k, v in rep.items():
print(f" {k:<22} {v:.4g}" if isinstance(v, float) else f" {k:<22} {v}")
return rep四个判据的含义和阈值:
| 判据 | 含义 | 期望 |
|---|---|---|
L_T |
采样起点与训练终点是否匹配 | <0.01 |
frac_no_signal |
多少步浪费在”太简单”的地方 | 越小越好 |
dlam_max_over_mean |
步与步的工作量是否均匀 | 越小越好(<50 算好) |
logsnr_span |
覆盖了多宽的噪声范围 | 20–30 nats 比较合适 |
本章的调度:L_T = 0.0000、frac_no_signal = 0.327、 dlam_max_over_mean = 40.7、logsnr_span = 19.33。
注意四个判据里有两个”越小越好”、一个”接近 0 好”、 一个”适中好”——它们互相冲突,这正是上面那张”没有免费的调度”表的来源。
好消息是:这个函数不需要训练。 改一个调度、跑一次这个函数, 几毫秒就能知道它有没有明显的毛病。 这是本章给出的最便宜的一个工具。
5.8.6 调度选择的一条实用规则
综合 §5.4 的 SNR 表和上面的实测,可以给出一条规则:
一个好的调度,应该让 log-SNR 在整个区间上尽量均匀地下降。
理由是 §5.6 已经确立的:每个时间步的”工作量”由 SNR 决定, 而不是由 t 决定。如果 1000 步里有一半的步,SNR 几乎没变, 那另一半的步就必须跨过很大的 SNR 跨度——而模型在每一步能修正的量是有限的。
检查你的调度是否合格,有两个便宜的指标:
- \bar\alpha_T 是否足够接近 0:决定了 L_T 是否接近 0, 也就是”采样起点和训练终点的分布是否匹配”(§5.6)。 实测线性是 4.04\times10^{-5},合格。
- 每步 log-SNR 降幅的最大/均值比:越小越均匀。 实测线性 40.7、余弦 230.6。两个都不算好—— 更好的调度(比如 log-SNR 空间上的线性插值)可以把这个比值压到 10 以内。
这两条都不需要训练,只要算一遍 \bar\alpha_t 数组。
5.9 为什么不能一步到位
学到这里常有一个疑问:既然反向过程每一步都是”从 x_t 猜 x_{t-1}“, 为什么不直接学一个 p_\theta(x_0\mid x_T),一步从噪声跳到数据?
这个疑问值得正面回答,因为它解释了整个扩散框架为什么长这样。
5.9.1 一步到位的困难在哪
如果只学一步,那个映射是
x_T \sim \mathcal{N}(0,I)\ \longmapsto\ x_0 \sim p_{\text{data}}
这是一个”把标准正态变成任意分布”的映射。
对 1 维、单峰分布,这件事不难——一个单调函数就能做到 (这也是”逆变换采样”的原理)。但对高维、多峰的分布,事情完全不同:
- 数据分布可能是一个低维流形嵌在高维空间里(自然图像就是这样)
- 从高维高斯到这种流形的映射,需要同时决定”落在流形的哪个位置”和”流形内部的哪个点”
- 而且这个映射不能是连续的单射——一个高斯要覆盖一个非单连通的集合
更实际的问题是训练信号。 学一步映射时, 一个随机初始化的网络输出的 x_0 和高斯噪声没有本质区别, 它和真实数据之间的距离几乎处处相同,梯度里没有”往哪走”的信息。
5.9.2 扩散的解法:把一步拆成一千小步
扩散模型的核心设计是:不学那个大映射,而是学 1000 个小映射。
每个小映射做的事极小:把分布挪动一点点。
\mathcal{N}(0,I) \xrightarrow{\ \mu_\theta(\cdot,T)\ } \cdots \xrightarrow{\ \mu_\theta(\cdot,3)\ } \xrightarrow{\ \mu_\theta(\cdot,2)\ } \xrightarrow{\ \mu_\theta(\cdot,1)\ } p_{\text{data}}
为什么这样可行? 三个理由:
① 每一步的目标很近。 q(x_{t-1}\mid x_t,x_0) 的方差是 \tilde\beta_t, 在本章的调度下最大只有 0.02。也就是说,从 x_t 到 x_{t-1}, 分布只挪动了很小的距离。
② 每一步都有监督信号。 因为 x_t 是从真实 x_0 造出来的(前向过程的闭式解), 我们知道”正确的 \varepsilon“是什么——这是白送的标签,不需要任何标注。
④ 误差有自我修正的机会。 每一步都注入噪声(§5.5), 下一步的模型在新的噪声水平上重新判断一次。 一步到位的映射没有这个机会。
所以这个框架在说什么
它把一个”从噪声直接生成数据”的难题,换成了 1000 个”在已知噪声水平下去一点噪”的易题。
代价是 1000 次前向。 这是扩散模型生成慢的根本原因—— 不是实现问题,是方法论上的选择。
第 6 章的 DDIM 会说明这个代价可以压缩: 那 1000 步里绝大多数的”挪动”是可以外推的, 20–50 步就能达到相近的质量。但那是另一个近似,不是免费的。
5.9.3 实测:T 真的越大越好吗
上面说”拆成一千小步”,听起来步数越多越好。测一下。
固定模型大小、固定训练 5000 步、固定线性调度(\beta 端点不变), 只改 T:
| T | 训练 loss | 采样 rms | 标准差 | MMD |
|---|---|---|---|---|
| 5 | 0.7962 | 0.7286 | [0.7050,\ 0.3461] | 0.01499 |
| 10 | 0.7583 | 0.6978 | [0.6668,\ 0.3355] | 0.01166 |
| 25 | 0.6770 | 0.6632 | [0.6393,\ 0.3185] | 0.00485 |
| 100 | 0.4793 | 0.5933 | [0.5502,\ 0.3312] | 0.00674 |
| 1000 | 0.1355 | 0.6090 | [0.5598,\ 0.3437] | 0.01146 |
(真实数据:rms 0.6110,标准差 [0.5707,\ 0.3351]。)
一、loss 那一列不能用来比较。 从 0.7962 降到 0.1355, 看起来”步数越多越好”——但五个 loss 根本不是同一个量。
L_{\text{simple}} 是 \|\varepsilon-\varepsilon_\theta\|^2 在 t 上的平均, 而不同 T 对应的噪声等级分布完全不同: T=5 时每一步都跨很大的噪声区间,T=1000 时有大量步落在几乎无噪声的区间。 这和 §5.8 那个”loss 不能跨调度比”是同一个坑。
二、MMD 那一列才是可比的,而它的结论是:T=25 最好。
T=25 的 MMD 是 0.00485,T=1000 是 0.01146——差了 2.4 倍, 而且是步数少的那个更好。
为什么? 至少有两个原因是可测的:
- 训练预算被摊薄了。5000 个训练步分给 1000 个噪声等级, 每个等级平均只有 5 次更新;分给 25 个等级则有 200 次。 这个实验的设定对 T=1000 不利,这是必须说明的。
- 步数多 = 误差累积的机会多。每一步的 \mu_\theta 都有误差, 而误差会随着步数累积。1000 步的累积机会是 25 步的 40 倍。
这个实验不能得出”T 越小越好”的结论—— 它只说明:“步数越多越好”是一个没有根据的直觉。
上面说”这个实验的设定对 T=1000 不利”。
那要怎样设计,才能公平地比较不同 T?
(答案:至少要把两件事对齐。
① 训练步数要按 T 缩放。 如果目标是”每个噪声等级被更新 k 次”, 那么 T=1000 就该训 1000k 步,T=25 只训 25k 步。 本章用的是固定的 5000 步,这显然不利于大 T。
② 采样的计算量要对齐。 T=1000 的采样要 1000 次前向,T=25 只要 25 次。 如果比的是”单位算力下的质量”,那 T=25 每次都赢—— 因为同样的算力它可以多训 40 倍。
这两个对齐方式给出不同的结论,而且都有道理: - 对齐训练量 → 回答”给定足够的训练,多少步最好” - 对齐采样算力 → 回答”给定同样的生成预算,多少步最好”
实际问题里问的通常是第二个。 这也是为什么第 6 章的采样器 (DDIM、DPM-Solver 这些)是过去几年扩散模型最活跃的方向之一: 它们不改变训练,只压缩采样步数。)
5.10 扩散在生成模型谱系里的位置
本章推完了扩散的骨架。但“为什么要有扩散”这个问题, 只有把它和别的生成模型放在一起才回答得了。
这一节把四类主流生成模型放在同一张桌上比。 比较的维度是”它们各自把困难放在哪一步”—— 不是比好坏,是比困难的形状。
5.10.1 生成模型要解决的根本问题
所有生成模型的数学任务是一样的:
给定从某个未知分布 p_{\text{data}} 采样的有限个点,构造一个可以采样的分布 p_\theta, 使得它接近 p_{\text{data}}。
三种做法,区别在于用什么把 p_\theta 表示出来:
| 家族 | p_\theta 怎么表示 | 训练怎么进行 |
|---|---|---|
| 变分自编码器(VAE) | p_\theta(x) = \int p_\theta(x\mid z)p(z)dz | 最大化 ELBO |
| 生成对抗网络(GAN) | 隐式:x = G_\theta(z) | 对抗博弈 |
| 扩散模型 | 隐式:T 步反向过程的终点 | 回归去噪 |
| 流匹配 | 隐式:ODE 的终点 | 回归速度场 |
注意 VAE 和另外三类的结构差别:VAE 有一个显式的、可以求值的密度; 另外三类的 p_\theta(x) 都写不出来(或者写出来也积不动), 只能”采样”。
这也是本章 §5.6 一开始那个积分的由来—— 扩散属于”密度写不出来”的那一类, 所以只能优化一个下界,而不是直接优化似然。
5.10.2 和 VAE 的区别:编码器是固定的
这是扩散相对 VAE 最重要的结构差别,也是它训练稳定的根源。
VAE 的 ELBO 是
\log p_\theta(x) \ge \mathbb{E}_{q_\phi(z\mid x)}\left[\log p_\theta(x\mid z)\right] - D_{\mathrm{KL}}\left(q_\phi(z\mid x)\ \|\ p(z)\right)
注意 q_\phi 带参数 \phi——它是一个要学的编码器。 训练时要同时优化编码器和解码器,两者互相拉扯。
扩散的 ELBO(式 9)形式完全一样,但
q(x_{1:T}\mid x_0) = \text{前向过程,没有参数}
“编码器”就是加噪过程,是预先定死的。
这个差别带来的后果
VAE 里最麻烦的几件事,在扩散里直接消失了:
- 后验坍塌(posterior collapse):VAE 里编码器可能学会”什么都不编码”, 让 q_\phi(z\mid x) = p(z),KL 项归零但解码器什么也学不到。 扩散里没有这个问题,因为 q 是固定的,它不会”偷懒”。
- KL 项和重建项的平衡:VAE 里这两项经常需要调权重(\beta-VAE 那一类工作)。 扩散里 KL 项是逐时间步的,而且有精确的解析形式(式 12)。
- 训练不稳定:VAE 的编码器和解码器是联合优化的, 扩散里”编码器”不参与优化,训练就是纯粹的回归。
代价是:扩散需要 T 次前向才能生成一个样本,VAE 只要一次。 这是”结构简单”换来的直接代价。
§5.9 已经解释了为什么这个代价是必要的: 把那 T 步合成一步,等于学一个从高斯到数据分布的大映射, 而那个映射太难学了。
5.10.3 和 GAN 的区别:损失函数里没有另一个网络
GAN 的目标函数是
\min_G \max_D\ \mathbb{E}_{x\sim p_{\text{data}}}\left[\log D(x)\right] + \mathbb{E}_{z}\left[\log(1 - D(G(z)))\right]
判别器 D 是一个和生成器一起训练的网络。
这带来两个后果:
① 训练是一个博弈,不是优化。 两个网络交替更新, 没有单一的损失在单调下降。这导致模式坍塌、震荡、对超参数极度敏感—— 这些是 GAN 的经典困难,而不是实现问题。
② 梯度来自判别器,而判别器本身在变。 生成器在优化一个移动的目标。
扩散的损失函数没有第二个网络(式 15):
\mathcal{L}_{\text{simple}} = \mathbb{E}\left[\|\varepsilon - \varepsilon_\theta(x_t,t)\|^2\right]
这是一个普通的均方误差,目标 \varepsilon 是已知的、固定的。
它的损失函数是一个回归损失,不是博弈。
回归损失的性质:有全局最优、梯度无偏、损失单调可监控。 本章从头到尾画的所有 loss 曲线,之所以能”看起来很正常”, 就是因为它们是回归损失。
而这也正是案例里那个 bug 能藏住的原因—— 一个正常的回归损失曲线,什么都证明不了。 它只说明”\varepsilon 预测得不错”, 完全不说”采样对不对”。
GAN 的损失曲线看不懂(那是博弈), 扩散的损失曲线看得懂但证明不了太多。 两者是不同类型的不可靠。
5.10.4 和流匹配的关系:同一件事的两种参数化
流匹配(flow matching)学的是一个速度场 v_\theta(x,t), 用它驱动一个常微分方程
\frac{dx}{dt} = v_\theta(x,t),\qquad x(0)\sim\mathcal{N}(0,I)
把 t 从 0 积到 1,终点就是样本。
它和扩散的关系,本章的读者已经具备了理解的基础: 把 §5.7 的 v-预测(式 18)和这里的 v_\theta 放在一起看, 符号是同一个 v,含义也确实是同一个—— v 就是”在 (x_0,\varepsilon) 平面上,沿着轨迹走的速度”。
两者的关系可以这样概括:
| 扩散(DDPM) | 流匹配 | |
|---|---|---|
| 学的量 | \varepsilon_\theta(或 v_\theta) | v_\theta |
| 采样方式 | 随机微分方程(每步注入噪声) | 常微分方程(确定性) |
| 路径 | 由 \beta_t 调度决定(弯曲) | 可以设计(常选直线) |
| 训练目标 | 回归 \varepsilon | 回归 v |
流匹配的主要卖点是”路径更直”: 如果训练时把 x_0 和 \varepsilon 用直线连起来(而不是扩散调度定义的曲线), 那么推理时的 ODE 求解可以用很少的步数(几步到几十步)。
这个”路径越直、步数越少”的直觉,和本章 §5.9 那一节是同一件事的两面: 步数取决于路径有多难积分,而不取决于理论上的 T。
本章不展开流匹配——它需要的数学(连续时间、ODE)超出了这一章的范围。 附录 E 的延伸阅读里有入口。
5.10.5 一张”困难放在哪”的对照表
四种方法都在解决同一个问题,区别只在于”把困难放在哪一步”:
| 方法 | 困难在哪 | 代价 |
|---|---|---|
| VAE | 编码器和解码器的联合优化 | 生成模糊(重建误差主导) |
| GAN | 对抗博弈的稳定性 | 模式坍塌、训练难 |
| 扩散 | 需要 T 步采样 | 推理慢 |
| 流匹配 | 需要设计好的路径 | 相对新,工具链少 |
扩散这一列值得多看一眼: 它把困难从”训练”挪到了”推理”。
这个挪动是整个领域的一个转折点—— 训练难是研究者的问题,推理慢是用户的问题。 扩散用后者的代价换来了前者的稳定, 所以它一旦训练规模上去,就能一直往上堆。
而”推理慢”这一侧,过去几年就是第 6 章要讲的那些工作: DDIM、DPM-Solver、蒸馏、流匹配—— 全都在压缩那 T 步。
5.11 连续时间:把 T 步推到极限
本章到现在为止,t 一直是整数:1, 2, \ldots, 1000。 这一节把 t 变成实数,看看会发生什么。
为什么要做这件事:因为很多”离散版本里看起来是巧合”的性质, 在连续版本里会变成必然。而且第 6 章的 score matching 视角需要它。
5.11.1 从乘积到指数
离散的累积保留率是
\bar\alpha_t = \prod_{s=1}^{t}(1-\beta_s)
两边取对数:
\log\bar\alpha_t = \sum_{s=1}^{t}\log(1-\beta_s)
当 \beta_s 很小的时候,\log(1-\beta_s)\approx-\beta_s, 所以
\log\bar\alpha_t \approx -\sum_{s=1}^{t}\beta_s \tag{5.19}
验证一下这个近似有多好。 本章的调度下:
| t | \log\bar\alpha_t(精确) | -\sum_{s\le t}\beta_s(近似) | 相对误差 |
|---|---|---|---|
| 100 | -0.108679 | -0.108604 | 6.9\times10^{-4} |
| 500 | -2.543546 | -2.535010 | 3.4\times10^{-3} |
| 1000 | -10.117714 | -10.050000 | 6.7\times10^{-3} |
误差从万分之七涨到千分之七——随 t 单调变差。
这个趋势本身就是信息:近似 \log(1-\beta)\approx-\beta 的误差 是 \beta^2/2 量级,而本章的 \beta_t 从 10^{-4} 线性涨到 0.02, 所以越往后近似越差(t=1000 处 \beta=0.02, \log(1-\beta) = -0.020203 与 -0.02 差 1%)。
换句话说:这个近似在”步长足够小”时成立, 而”\beta 足够小”正是这个近似成立的前提。
在取连续极限(T\to\infty、\beta\to0)的时候, 每一步的 \beta 趋于 0,于是这个近似变成精确的。 离散版本里那千分之几的误差,就是”离散化误差”的具体数值。
现在让步长趋于 0。 设 T\to\infty 而 \sum\beta_s 保持有限, 记 t/T \to \tau\in[0,1],\beta(\tau) 是一个连续函数,则
\log\bar\alpha(\tau) = -\int_0^{\tau}\beta(s)\,ds \tag{5.20}
乘积变成了积分,\bar\alpha 从一个数组变成了一条连续曲线。
所以这一步在说什么
“连乘”在 \beta 很小的时候就是”负的连加”,而连加就是积分。
这句话解释了本章的两件事:
① 为什么 \bar\alpha_t 在数值上要小心。 它是 1000 个数连乘, 在浮点下就是一个反复累加对数的过程—— 所以 §5.13 失败三那个”用 float64 算调度”的建议不是洁癖, 而是这个结构的直接要求。
② 为什么”调度”这个词是准确的。 \beta(\tau) 是一个定义在连续时间上的函数, T 只是把它离散化的粒度。 换 T 不会改变调度本身,只改变采样它的密度—— 这也是为什么 §5.9 那个”T=25 比 T=1000 好”的结果, 不能读成”调度变了”,只能读成”离散化的密度变了”。
5.11.2 前向过程的连续版本
离散时 x_t = \sqrt{\bar\alpha_t}x_0 + \sqrt{1-\bar\alpha_t}\varepsilon。 连续版本把它写成随机微分方程:
dx = -\frac{1}{2}\beta(\tau)x\,d\tau + \sqrt{\beta(\tau)}\,dw \tag{5.21}
这就是一个”方差保持”的 SDE(variance-preserving,VP): 漂移项把 x 往 0 拉,扩散项注入噪声,两者恰好平衡, 使得 x 的方差自动保持。
为什么漂移项前面有个 -1/2? 因为要让方差保持不变。 设 \mathrm{Var}(x)=v(\tau),由 (22):
\frac{dv}{d\tau} = -\beta(\tau)v + \beta(\tau) = \beta(\tau)(1-v)
这个方程的不动点是 v=1。 如果初始 v=1(数据已归一化), 它就永远停在 1——正好对应离散版本的 \mathrm{Var}(x_t) = \bar\alpha_t\mathrm{Var}(x_0) + (1-\bar\alpha_t), 在 \mathrm{Var}(x_0)=1 时恒为 1(§5.4 那张实测表)。
如果 \mathrm{Var}(x_0)\ne 1(本章的数据是 [0.326, 0.112]), 那么 v 会从初始值指数式地趋向 1, 速率由 \beta(\tau) 决定。和 §5.4 实测的那条曲线形状一致。
5.11.3 反向 SDE 与 score
连续版本的反向过程(把时间倒过来)是
dx = \left[-\frac{1}{2}\beta(\tau)x - \beta(\tau)\nabla_x\log p_\tau(x)\right]d\tau + \sqrt{\beta(\tau)}\,d\bar w \tag{5.22}
这里出现了一个新东西:\nabla_x\log p_\tau(x),叫 score(得分)。
它是对数密度的梯度——“往哪个方向走,密度上升得最快”。
扩散模型和 score matching 的连接就在这里:
\varepsilon_\theta(x_t,t) = -\sqrt{1-\bar\alpha_t}\ \nabla_{x_t}\log p_t(x_t) \tag{5.23}
换句话说:模型预测的噪声,和 score 只差一个负的常数因子。
(24) 说明”预测噪声”和”估计 score”是同一件事的两种说法。
为什么这个等价有用? 因为 score 有自己的理论工具:
- score 是”密度对数的梯度”,它不需要知道归一化常数—— 这解释了为什么扩散模型不需要算那个积不出来的 p_\theta(x_0)(§5.6 开头)。 \nabla\log p 里的归一化常数求导后是 0,自动消失了。
- Tweedie 公式给出 \mathbb{E}[x_0\mid x_t] 与 score 的精确关系, 这正是本章式 式 5.6 的连续版本。
- §5.7 那个”\varepsilon 损失的动态范围比 x_0 小 8500 倍”, 换成 score 的语言就是:“score 的尺度在各个噪声水平上是稳定的”。
本章不展开 score matching 的完整理论(那需要随机微分方程和 Langevin 动力学的工具),但它给了本章的每一个结论一个统一的上位框架:
第 6 章会正式建立这个视角, 并用它推出”为什么可以用更少的步数采样”—— DDIM 和 DPM-Solver 都是 (23) 这个 SDE 的离散化方案, 不同的方案对应不同的精度与速度的权衡。
5.11.4 一张连续—离散对照表
| 离散(本章主体) | 连续(本节的极限) |
|---|---|
| \bar\alpha_t = \prod_s(1-\beta_s) | \log\bar\alpha(\tau) = -\int_0^\tau\beta(s)ds |
| x_t = \sqrt{\bar\alpha_t}x_0 + \sqrt{1-\bar\alpha_t}\varepsilon | SDE (22) |
| p_\theta(x_{t-1}\mid x_t) = \mathcal{N}(\mu_\theta,\sigma_t^2I) | 反向 SDE (23) |
| \varepsilon_\theta(x_t,t) | -\sqrt{1-\bar\alpha}\,\nabla\log p_t,(24) |
| \mathcal{L}_{\text{simple}} | score matching 目标 |
| T 步采样 | ODE/SDE 求解器 |
注意最后一行的变化: 在连续视角里,“采样”变成了”解一个微分方程”, 于是几十年积累的数值 ODE 求解器全都可以拿来用。
这正是第 6 章那些”用 20 步达到 1000 步质量”的方法的来源—— 它们不是发明了新的生成模型, 而是把”解 SDE”换成了”用更高阶的求解器解”。
5.12 交互实验
本章配三个实验,都做成独立的 HTML 文件,可以单独打开、全屏演示。
5.12.1 实验 5-1 前向过程实验室
怎么用。 拖动 t 滑块,700 个点按 x_t = \sqrt{\bar\alpha_t}x_0 + \sqrt{1-\bar\alpha_t}\varepsilon 实时重算—— 同一串噪声固定不变,所以拖滑块时看到的差异全部来自 t。
观察引导。 从 t=0 慢慢拖到 1000,记下你第一次认不出”这是两个月牙”的那个 t。 然后看右侧的 \bar\alpha_t:它离 0 还很远。
这个反差是本章 §5.4 那张表的要点: \bar\alpha_{300} = 0.396(信号还剩四成),但从视觉上双月已经糊了。 原因是两个系数都在变:信号系数 0.63、噪声系数 0.78—— 噪声已经比信号大了。
这一节真正要你记住的是
“信号还剩多少”(\bar\alpha_t)和”还能不能认出来”(SNR) 不是一回事。模型面对的是后者。
5.12.2 实验 5-2 反向过程实验室
怎么用。 同一个模型、同一批初始噪声,只改”反向每一步加不加 \sigma_t z“, 逐帧回放(每 50 步一帧,共 21 帧)。
观察引导。 从 t=1000 往回拖,盯住两个数字:
- 在哪一帧上,右边的点云缩成了一个点?(提示:t=300 附近,半径 0.04)
- 两个数字的比值什么时候最大?
然后把”并排对比”关掉,只看错误的那一条走完全程—— 你会看到它最后停在半径 0.20 的地方,不是 0.61。 那是”外推”把它推回来的,不是模型学会了。
这一节真正要你记住的是
反向过程定义的是一个分布,不是一个点。 x = mean 和 x = mean + sigma * z 差的那一项, 是整个过程的推进机制,不是可有可无的抖动。
5.12.3 实验 5-3 噪声调度实验室
怎么用。 调 \beta 的两个端点和步数 T,看 log-SNR 曲线的形状, 以及红色区间(\bar\alpha_t < 0.01,信号已被破坏殆尽)占多少步。
观察引导。
- 把 \beta 终点从 0.020 拖到 0.008:红色区间消失了。 这时 \bar\alpha_{1000} 还有多少?(看左侧第一张图的读数。)
- 切到余弦调度,比较曲线形状:它在哪一段更陡?
- 最难的一个问题:红色区间越大,模型的任务是越难还是越容易? (提示:回到 §5.6 那张 L_{t-1} 分箱表。)
这一节真正要你记住的是
调度不是”参数”,它决定每个时间步承担多少工作量。 红色区间越大,说明越多的步数被花在了”几乎不用学”的地方。
5.12.4 怎么改这三个实验
三个实验都是单个 HTML 文件、零依赖(§5.19 的四件套约定: 可调控件 / 可视化主体 / 观察引导 / 静态快照)。 它们都可以直接改,不需要重新构建任何东西。
实验 5-1 的源码里有一条写死的噪声表:
const EPS = X0.map(() => [gauss(), gauss()]);这行代码值得单独说一句。 它把每个点对应的噪声预先抽好、固定下来, 而不是每次重画时现抽。
为什么? 因为拖滑块时如果噪声也在变, 你就分不清”点云变了”是因为 t 变了还是因为噪声变了—— 这正是第 1 章那个”唯一变量”原则(§1.4)在交互实验里的体现。
实验 5-2 的数据是内嵌的(291 KB 的 JSON 直接写在 HTML 里)。 这不是偷懒,是必需的:实验页要能”双击就打开”, 而 file:// 协议下 fetch 本地 JSON 会被 CORS 拒绝 (做静态快照时踩到的实际坑)。
如果你要换成自己的模型,做法是: 用 notebooks/export_ch5_exp.py 导出同样结构的 JSON, 替换掉 HTML 里 const D = {...} 那一段即可。
实验 5-3 是纯计算的——它只算 \bar\alpha_t 和 log-SNR, 不加载任何模型。所以改起来最快: 把 build() 函数换掉就能试任意调度。
一个建议的练习:在实验 5-3 里加第三种调度模式 (按 §5.8 说的”\lambda 线性”), 然后看那三个指标(L_T、纯噪声步数、max/mean)能不能同时变好。 如果不能,你就亲手验证了”没有免费的调度”那句话。
5.13 它在哪里失效
这一节按本书惯例:每个失败模式都给出可运行的代码和真实的崩塌数字。
5.13.1 失败一:丢掉反向过程里注入的噪声
这就是本章的案例(§5.2、§5.5)。复现:
x = mean # ✗ 只取均值
x = mean + sigma[t] * torch.randn_like(x) # ✓ 从分布里采样| 标准差 | rms | MMD | |
|---|---|---|---|
| 正确 | [0.5598,\ 0.3437] | 0.6090 | 0.01146 |
| 丢掉 \sigma_t z | [0.0043,\ 0.0078] | 0.2027 | 0.96029 |
训练 loss 完全不受影响(逐点差 0.00e+00)。
5.13.2 失败二:训练时漏掉了一段 t
这是一个非常容易犯的错:写 torch.randint(1, T, ...) 而不是 randint(0, T, ...), 或者像下面这样”想跳过太简单的低噪声段”:
t = torch.randint(101, cfg.n_steps, (B,)) # ✗ 只在前 100 步之后的区间训练直觉是”低噪声段太简单,不学也罢”。实测:
| 训练 loss | rms | 标准差 | MMD | |
|---|---|---|---|---|
| 正常(t\in[1,1000]) | 0.1355 | 0.6090 | [0.5598,\ 0.3437] | 0.01146 |
| 漏掉前 100 步 | 0.0646 | 0.4907 | [0.4572,\ 0.2824] | 0.03983 |
loss 从 0.1355 掉到 0.0646——看起来是巨大的改进。
但 MMD 从 0.01146 涨到 0.03983,差了 3.5 倍, 标准差从 [0.56, 0.34] 缩到 [0.46, 0.28]。
为什么 loss 会变好? 因为你把”最难的那一段”从平均里拿掉了。 L_{\text{simple}} 是在 t 上均匀平均的, 去掉难的部分,平均值当然下降。
这是”改度量”而不是”改模型”——和 §5.8 那个 loss 跨调度比的坑同源。 它也是本章第二次出现”loss 变好但结果变坏”(第一次是案例)。
为什么采样会变差? 因为 §5.6 测过:t\in[2,11] 这 10 步承担了 49% 的 ELBO。 把它们从训练里拿掉,模型在最重要的那一段完全没有被训练过, 而采样时每一步都必须走。
5.13.3 失败三:低精度下 1-\bar\alpha_t 的灾难性抵消
这个坑在高噪声端完全看不出来,只在低噪声端发作。
\bar\alpha_t 在小 t 处极接近 1(t=1 时 0.9999)。 用 fp16 算 1-\bar\alpha_t,就是在做两个几乎相等的数的减法。
ab = alpha_bar.to(torch.float16)
one_minus = 1 - ab # ✗ fp16 下算
one_minus = (1 - alpha_bar).float() # ✓ 先在高精度算完再转实测:
| t | \bar\alpha_t | 1-\bar\alpha_t(fp32) | (1-\bar\alpha_t)(fp16) | 相对误差 |
|---|---|---|---|---|
| 1 | 0.999900 | 0.00010000 | 0.00000000 | 100% |
| 2 | 0.999780 | 0.00021991 | 0.00000000 | 100% |
| 5 | 0.999301 | 0.00069901 | 0.00048828 | 30.2% |
| 10 | 0.998105 | 0.00189480 | 0.00195312 | 3.1% |
| 20 | 0.994231 | 0.00576905 | 0.00585938 | 1.6% |
| 100 | 0.897018 | 0.10298185 | 0.10302734 | 0.04% |
前 10 步的相对误差均值是 31.4%,前 100 步是 3.8%。
t=1 和 t=2 处,1-\bar\alpha_t 直接变成 0。
因为被毁掉的那几步,正好是最重要的几步。
- \sqrt{1-\bar\alpha_t} 是 x_t 里噪声的系数。它在 t=1 处变成 0, 意味着前向过程在那一步几乎没有加噪——x_1 \approx x_0。
- 而 §5.6 实测:t\in[2,11] 这 10 步承担了 49% 的 ELBO。
- §5.5 实测:\tilde\beta_1 = 0 精确成立,反向过程在最后一步是确定性的。
三项都指向同一段。而那一段正是 fp16 精度最差的地方。
修法只有一行:把 1-\bar\alpha_t 在高精度下算好再转成低精度存储。 扩散模型里所有的”1 减去一个接近 1 的数”,都应该这么处理。
顺带一个反直觉的实测:fp16 的指数位窄(最小正规数 6.1\times10^{-5}), 我们本来担心 \bar\alpha_{1000} = 4.04\times10^{-5} 会下溢成 0—— 实测不会:它落在次正规区间里,只有 21 步(第 980 步之后), 而且 \sqrt{\bar\alpha_t} 的相对误差最大只有 0.03%。
真正出事的是减法,不是下溢。
5.13.4 失败四:\beta 端点设得太小,调度没跑完
\beta_{\max} 决定 \bar\alpha_T 有多小。如果它不够小, 前向过程跑到 T 步时信号还没被破坏干净。
| \beta_{\max} | \bar\alpha_{1000} | \sqrt{\bar\alpha_{1000}} | L_T | 信号残留 |
|---|---|---|---|---|
| 0.020 | 0.000040 | 0.0064 | 0.0000 | 0.004% |
| 0.006 | 0.047070 | 0.2170 | 0.0011 | 4.7% |
| 0.002 | 0.349692 | 0.5913 | 0.0806 | 35.0% |
L_T 就是”采样起点和训练终点不匹配”的度量(§5.6)。 \beta_{\max} = 0.002 时它涨到 0.0806,是合格调度的几千倍。
我们把 \beta_{\max} = 0.002 的配置真的训了一遍并采样:
| 训练 loss | rms | 标准差 | MMD | |
|---|---|---|---|---|
| 正常(\beta_{\max}=0.02) | 0.1355 | 0.6090 | [0.5598,\ 0.3437] | 0.01146 |
| \beta_{\max}=0.002 | 0.3857 | 0.5905 | [0.5553,\ 0.3178] | 0.01095 |
MMD 0.01095——和正常配置几乎一样,甚至略好。
理论指标说”有问题”(L_T 大了几千倍),实测说”看不出来”。
为什么? 两个原因,本章都没能完全分离:
- 采样起点的不匹配是一次性的扰动,而 1000 步的反向过程有足够的 自我修正能力把它吸收掉(这正是 §5.5 讲的”每步注入噪声”的另一个好处)。
- 这个玩具模型太宽容:2 维、双月、5 万参数。 不匹配造成的误差可能被压缩在肉眼看不见的范围内。
所以这一条要这样记:指标报警了,但我们没能在本章的规模上复现出危害。 这既不能说明指标错了,也不能说明问题不存在—— 只能说”在这个规模上,它是一个可承受的缺陷”。
5.13.5 失败五:直接预测 x_0,且用普通 MSE
按 §5.7 的分析,x_0-预测等价于给 \varepsilon 损失乘 1/\mathrm{SNR}_t。
实测:同样的模型、同样的数据、同样训 5000 步,只把回归目标从 \varepsilon 换成 x_0:
| 参数化 | 训练 loss | rms | 标准差 | MMD |
|---|---|---|---|---|
| \varepsilon-预测 | 0.1355 | 0.6090 | [0.5598,\ 0.3437] | 0.01146 |
| x_0-预测 | 0.1675 | 0.9616 | [0.9540,\ 0.2514] | 0.14171 |
MMD 差了 12 倍。 而且注意标准差那一列: x_0-预测给出 [0.9540,\ 0.2514],第一个维度比真实数据的 0.5707 大了 67%—— 分布被拉宽了。
两个 loss 不是同一个量(不同目标),所以那一列不能比。
而且我们只训了 5000 步、没有给 x_0-预测单独调超参数。 x_0-预测的梯度尺度在不同 t 上差 2.5 个数量级(§5.7 实测), 很可能需要不同的学习率或梯度裁剪。
所以正确的表述是:“在同样的预算和超参数下, x_0-预测明显更差”——而不是”x_0-预测不能用”。
文献里的结论也确实是后者:x_0-预测配上合适的权重是可以用的, 只是不如 \varepsilon 或 v 省事。
5.13.6 失败六:把 \bar\alpha_t 和 \alpha_t 搞混
两个符号长得像,含义完全不同:\alpha_t = 1-\beta_t 是单步保留率, \bar\alpha_t = \prod_{s\le t}\alpha_s 是累积保留率。 在 t=1000 处,前者是 0.98,后者是 4\times10^{-5}。
在反向均值 (7) 里,两者的位置是:
\mu_\theta = \frac{1}{\sqrt{\alpha_t}}\left(x_t - \frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\varepsilon_\theta\right)
外面那个是 \alpha_t(单步),里面那个是 \bar\alpha_t(累积)。
写反的后果可以精确算出来:把外面的 1/\sqrt{\alpha_t} 写成 1/\sqrt{\bar\alpha_t}, 在 t=1000 处系数从 1.010 变成 157—— 每一步都放大 155 倍,一步就爆。
这类错误的检测方法是:在 t=1 和 t=1000 两个端点上各打印一次中间量。 \alpha_t 在两端都接近 1(0.9999 和 0.98), \bar\alpha_t 从 0.9999 掉到 4\times10^{-5}。 如果某个量的两端数值差异巨大而你用错了符号,两端打印一眼就能看出来。
5.14 上手实现:67 行
5.14.1 为什么还要再写一遍
本章前面用的 notebooks/tiny_diffusion.py 是一个模块化实现: 调度一个函数、前向一个函数、采样一个函数, 中间还有好几个教学开关(reverse_mean、posterior_var)。
那个版本适合做实验,不适合回答一个更重要的问题: “一个能跑的扩散模型,最少需要多少东西?”
这一节把它压到单文件 67 行(不含注释、空行与文档字符串; 代码在 notebooks/diffusion_67.py,可用 ast 复算这个数), 不依赖项目里任何别的文件。存下来直接跑,应当输出和本节一样的数字。
5.14.2 第一段:调度(5 行)
def schedule(T=T, b0=B0, b1=B1):
betas = torch.linspace(b0, b1, T, dtype=torch.float64)
alphas = 1.0 - betas
abar = torch.cumprod(alphas, 0)
abar_prev = torch.cat([torch.ones(1, dtype=torch.float64), abar[:-1]])
return {"betas": betas, "alphas": alphas, "abar": abar, "abar_prev": abar_prev,
"post_var": (1 - abar_prev) / (1 - abar) * betas}五行里有三个坑。
坑一:float64。 这一行必须是双精度。 \bar\alpha_T = \prod_t(1-\beta_t) 是 1000 个数连乘, 每次都在丢精度。用 float32 算,末端的相对误差会到 10^{-4} 量级—— 而 §5.9 失败三已经演示了 1-\bar\alpha_t 对精度有多敏感。
坑二:abar_prev 前面要补一个 1。 \bar\alpha_0 按定义是空乘积,等于 1。 补在这个位置(而不是 append 到末尾)是因为后验方差公式里的下标是 \bar\alpha_{t-1}——前一个时刻的累积量。
坑三:post_var 直接在这里算好。 它是 \tilde\beta_t = \frac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}\beta_t(式 4)。 放在调度里算有个好处:t=1 处分母是 1-\bar\alpha_1 = 10^{-4}、 分子是 0,结果是精确的 0。如果放到采样循环里每次现算, 就要小心别把 t=0 那一步也算进去(那里分母是 0)。
5.14.3 第二段:前向过程(4 行)
def q_sample(x0, t, sch):
a = sch["abar"][t].to(x0.dtype).view(-1, 1)
eps = torch.randn_like(x0)
return a.sqrt() * x0 + (1 - a).sqrt() * eps, eps这就是闭式解(式 2),一行写完。
.view(-1, 1) 不能省:t 是 (B,),abar[t] 也是 (B,), 而 x0 是 (B, d)。不广播的话形状对不上。
返回值里带上 eps,因为训练要拿它当标签—— 这是”白送的监督信号”(§5.9)在代码里的体现。
这一节真正要你记住的是
整个扩散模型的训练数据是”算出来”的,不是”收集来”的。 给定任意一个 x_0、任意一个 t、任意一个 \varepsilon, 就有一条合法的训练样本。这是扩散模型相对 GAN 的核心优势:没有对抗、不需要判别器。
5.14.4 第三段:去噪网络(13 行)
class Denoiser(nn.Module):
def __init__(self, h=128):
super().__init__(); self.h = h
self.net = nn.Sequential(nn.Linear(D + h, h), nn.SiLU(),
nn.Linear(h, h), nn.SiLU(),
nn.Linear(h, h), nn.SiLU(), nn.Linear(h, D))
def temb(self, t):
f = torch.exp(-math.log(10000.0) * torch.arange(self.h // 2, device=t.device) / (self.h // 2))
a = t.float()[:, None] * f[None, :]
return torch.cat([a.sin(), a.cos()], -1)
def forward(self, x, t):
return self.net(torch.cat([x, self.temb(t)], -1))输入是 x_t 和 t 拼起来——三个线性层,激活用 SiLU。
时间步编码用的是正弦公式,和第 1 章的位置编码一模一样:
\text{temb}(t)_{2i} = \sin\left(t \cdot e^{-i\ln 10000/(h/2)}\right),\qquad \text{temb}(t)_{2i+1} = \cos\left(t \cdot e^{-i\ln 10000/(h/2)}\right)
唯一的区别是:位置编码里的”位置”是 token 的下标, 这里的时间步是噪声等级。
为什么要编码而不是直接把整数 t 送进去? 两个理由:
- 尺度。t \in [1,1000],直接送进去数值范围比 x_t(约 \pm3)大两个数量级, 第一层权重的梯度会被主导。
- 周期性。正弦编码让”t 和 t+1 接近”这件事在网络里是可表示的—— 而如果 t 是以 one-hot 形式进去的,相邻时间步之间没有任何相似性。
这正是第 1 章 §1.11 讲位置编码时的同一件事: 给模型一个”顺序”的概念,需要专门设计编码方式。
5.14.5 第四段:训练(11 行)
def train(data, steps=5000, bs=256, lr=2e-3, seed=20261016):
torch.manual_seed(seed); sch = schedule()
m = Denoiser(); opt = torch.optim.Adam(m.parameters(), lr=lr)
g = torch.Generator().manual_seed(seed); N = data.shape[0]
for _ in range(steps):
x0 = data[torch.randint(N, (bs,), generator=g)]
t = torch.randint(0, T, (bs,), generator=g)
xt, eps = q_sample(x0, t, sch)
loss = F.mse_loss(m(xt, t), eps)
opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
m.eval(); return m, sch训练循环里只有三行是”扩散”:抽 t、调 q_sample、比对 \varepsilon。 剩下的是任何一个 PyTorch 训练循环都有的东西。
注意 torch.randint(0, T, ...)——左闭右开,覆盖 t = 0,\ldots,T-1, 也就是全部 1000 个噪声等级。 §5.13 失败二演示了漏掉一段的后果。
5.14.6 第五段:采样(13 行)
@torch.no_grad()
def sample(model, sch, n, seed=0, add_noise=True):
torch.manual_seed(seed); x = torch.randn(n, D); T_ = sch["abar"].shape[0]
for t in reversed(range(T_)):
tt = torch.full((n,), t, dtype=torch.long)
eps = model(x, tt)
ab, abp = sch["abar"][t], sch["abar_prev"][t]
beta, alpha = sch["betas"][t], sch["alphas"][t]
mean = (x - beta / (1 - ab).sqrt() * eps) / alpha.sqrt()
if t > 0 and add_noise:
x = mean + sch["post_var"][t].sqrt() * torch.randn_like(x)
else:
x = mean
return xadd_noise 这个开关就是案例里的那个 bug。 设成 False 时只走均值——训练 loss 一位不变,采样结果塌 130 倍(§5.2)。
循环从 t=T-1 倒着走到 0。 本章代码里的 t 是 0-based 数组下标, 所以”第 1 个扩散步”对应 t=0。
if t > 0 那个条件的含义:最后一步(t=0,也就是走到 x_0 那一步) 不再注入噪声。
5.14.7 实测输出
数据 均值 [-0.0012, -0.0011] 标准差 [0.5707, 0.3351] rms 0.6110
正确采样 均值 [0.0486, 0.0486] 标准差 [0.5979, 0.3096] rms 0.6258
丢掉噪声 均值 [0.0578, 0.1533] 标准差 [0.0044, 0.0042] rms 0.1639
和本章前面那些用模块化实现跑出来的数字略有出入 (这里 rms 0.6258 对 0.6090),因为这一份的实现细节不同: 没有单独处理 t=0 的边界、优化器参数一样但样本顺序不同。 但现象完全一致:正确采样落在数据附近,丢掉噪声塌到 0.164。
它说明扩散模型的”骨架”很小。
真正让它能生成图像的不是更多的数学,而是更大的网络和更多的数据。 本章从头到尾推的那些公式(前向闭式解、后验、ELBO、参数化), 加起来就是这 67 行里的 5 行是”扩散特有的”:
| 行 | 内容 |
|---|---|
abar = torch.cumprod(alphas, 0) |
累积保留率 |
a.sqrt() * x0 + (1 - a).sqrt() * eps |
前向闭式解 |
loss = F.mse_loss(m(xt, t), eps) |
训练目标 |
mean = (x - beta/(1-ab).sqrt()*eps) / alpha.sqrt() |
反向均值 |
x = mean + post_var[t].sqrt() * randn_like(x) |
反向采样 |
本章的六节内容,全部落在这五行上。
所以”数学骨架”这个书名不是修辞: 剩下的一切——U-Net、注意力、文本编码器、CFG—— 都是在这五行之上加的表达能力和控制手段, 而它们的作用是让 \varepsilon_\theta 预测得更准、更可控。
5.15 对拍:怎么确认你的实现是对的
扩散模型的实现有一个特点:错了不一定报错,报错不一定明显。
本章的案例(丢掉 \sigma_t z)和失败二(漏掉一段 t) 都不产生任何异常——代码正常运行,loss 正常下降, 只有采样结果不对。
所以需要一套”不依赖采样结果”的自检。 下面六项,每一项都能在几秒内跑完,而且每一项都有一个解析的期望值。
5.15.1 检查 ①:随机初始化模型的 loss 应该接近 1
torch.manual_seed(0); m = Denoiser()
xt, eps = q_sample(x0, t, sch)
loss = F.mse_loss(m(xt, t), eps) # 期望 ≈ 1为什么是 1? 因为 F.mse_loss 对所有元素取平均(不只是 batch 维)。 目标 \varepsilon 的每个分量方差是 1,而随机初始化的模型输出接近 0, 所以每个元素的平方误差期望是 1。
实测:0.9660。 ✓
这一项能抓什么:如果标签搞错了(比如拿 x_0 当目标而不是 \varepsilon, 或者把 x_t 当标签),这个数会明显偏离 1。 本章的模型里 x_0 的方差是 0.33 和 0.11—— 拿 x_0 当标签的话 loss 会在 0.2 附近。
5.15.2 检查 ②:\bar\alpha_t 的单调性和端点
assert (sch["abar"][1:] <= sch["abar"][:-1]).all() # 单调不增
assert abs(sch["abar"][0].item() - (1 - B0)) < 1e-12 # ᾱ₁ = 1-β₁
assert sch["abar"][-1].item() < 1e-3 # 末端信号基本没了三项分别抓:调度数组被就地修改过、端点公式写错、 \beta_{\max} 设得太小(§5.13 失败四)。
本章的值:\bar\alpha_1 = 0.999900(= 1 - 10^{-4} ✓), \bar\alpha_{1000} = 4.04\times10^{-5} ✓。
5.15.3 检查 ④:x_t 的方差应该从 \mathrm{Var}(x_0) 平滑过渡到 1
for t in (1, 100, 500, 1000):
xt, _ = q_sample(x0, torch.full((n,), t-1, dtype=torch.long), sch)
print(t, xt.var(0))期望:\mathrm{Var}(x_t) = \bar\alpha_t\mathrm{Var}(x_0) + (1-\bar\alpha_t)。
实测:
| t | x_t 的方差 | 公式预测 |
|---|---|---|
| 1 | [0.3329,\ 0.1129] | [0.3257,\ 0.1123] |
| 100 | [0.3903,\ 0.2052] | [0.3349,\ 0.1422] |
| 500 | [0.9570,\ 0.9232] | [0.9242,\ 0.8835] |
| 1000 | [1.0251,\ 1.0230] | [0.99997,\ 0.99996] |
前两行的实测值比公式预测略大,原因有两个: 样本量只有 2048(\mathrm{Var} 的估计误差约 \sqrt{2/n}\approx 3\%), 以及数据本身加了高斯抖动、实际方差略大于样本方差。
这一项能抓什么:x_t 的构造公式写错。 如果漏了 \sqrt{\bar\alpha_t} 的缩放(写成 x_t = x_0 + \sqrt{1-\bar\alpha_t}\varepsilon), t=1000 处的方差会是 2 而不是 1,一眼就能看出来。
5.15.4 检查 ⑤:t \to 0 时 x_t \to x_0
xt, _ = q_sample(x0, torch.zeros(n, dtype=torch.long), sch)
print((xt - x0).abs().max()) # 期望 ≈ √β₁ · max|ε|实测:0.0263。 理论值 \sqrt{10^{-4}}\times|\varepsilon|_{\max}, 在 \varepsilon 的最大绝对值约 2.6 时给出 0.026。✓
这一项能抓什么:调度数组的索引偏移了一格。 如果 t=0 处读到的是 \bar\alpha_{100} 而不是 \bar\alpha_1, 这个数会大得多。
5.15.5 检查 ⑥:后验方差用两条路算,结果必须一致
解析式(式 4):
\tilde\beta_t = \frac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}\beta_t
另一条路:把两个高斯相乘时的”精度相加”。 给定 x_t,关于 x_{t-1} 的两个信息源是 \mathcal{N}(\sqrt{\bar\alpha_{t-1}}x_0, 1-\bar\alpha_{t-1}) (来自 x_0)和 \mathcal{N}(x_t/\sqrt{\alpha_t}, \beta_t/\alpha_t)(来自 x_t)。 精度(方差的倒数)相加,然后取倒数:
\tilde\beta_t = \left(\frac{1}{1-\bar\alpha_{t-1}} + \frac{\alpha_t}{\beta_t}\right)^{-1}
这两条路代数上是同一个式子,但代码路径完全不同—— 一条用累乘数组,一条用当前的 \alpha_t,\beta_t。 不一致就说明某一处的索引或公式写错了。
实测:
| t | 解析式 | 由精度相加 | 差 |
|---|---|---|---|
| 2 | 0.0000545319 | 0.0000545319 | 8.5\times10^{-19} |
| 10 | 0.0002381820 | 0.0002381820 | 1.2\times10^{-18} |
| 100 | 0.0020345963 | 0.0020345963 | 0 |
| 500 | 0.0100313554 | 0.0100313554 | 1.7\times10^{-18} |
差值都在浮点舍入量级。 ✓
5.15.6 检查 ⑦:反向过程跑完之后,样本尺度应该接近数据
这是唯一一项需要采样结果的检查,也是最后一道防线。
x = sample(model, sch, 4000, add_noise=True)
print(x.std(0), x.norm(dim=1).mean())期望:和数据的标准差、rms 在同一个量级(不必相同)。
实测: 采样 rms 0.6258,数据 rms 0.6110。✓
把 add_noise 设成 False:rms 掉到 0.1639,标准差从 [0.60, 0.31] 掉到 [0.0044, 0.0042]。这一项会立刻抓住案例里那个 bug。
上面六项检查里,哪一项能抓住”训练时漏掉了一段 t“这个错误? (§5.13 失败二的那个。)
(答案:只有第 ⑦ 项,而且是通过”尺度不对”这个间接信号。
前五项查的都是前向过程和调度的数学—— 而”漏掉一段 t“改的是训练循环里的一句话, 前向过程的公式一个字都没错。
第 ⑦ 项查的是采样结果的统计量:那个例子里 rms 是 0.4907(正确 0.6090), 标准差 [0.4572, 0.2824] 明显偏窄——会被抓住,但要知道去看。
这暴露了自检的一个结构性局限: “实现正确”和”训练正确”是两件事,而前五项只能保证前者。
一个补充办法:训练完之后,在验证集上算一次 loss,但按 t 分段。 如果某一段 t 的 loss 明显异常(远低于其他段), 那一段就是被漏掉或训练不足的。 这和第 4 章”逐位置看 loss”是同一个动作—— 平均值会掩盖局部的异常,分组是唯一能看见它的办法。)
5.16 训练一个扩散模型的配方
本章的实验设置是玩具规模,但里面的每一个选择在真实模型上也有对应。 这一节把关键的选择列成一张表,并说明哪些是必须的、哪些是习惯。
5.16.1 超参数表
| 参数 | 本章取值 | 真实模型常用 | 是不是必须 |
|---|---|---|---|
| T | 1000 | 1000(图像)/ 4000(视频) | 否,见 §5.9 |
| \beta_{\min} | 10^{-4} | 10^{-4} | 是,太小则 1-\bar\alpha_t 精度不够 |
| \beta_{\max} | 0.02 | 0.02 | 是,见 §5.13 失败四 |
| 调度 | 线性 | 线性或余弦 | 否,见 §5.8 |
| 预测目标 | \varepsilon | \varepsilon 或 v | 否,见 §5.7 |
| 反向方差 | \tilde\beta_t | \tilde\beta_t 或 \beta_t | 否,实测无差别 |
| 学习率 | 2\times10^{-3} | 10^{-4} 量级 | 依赖模型与 batch |
| 训练步数 | 5000 | 10^5–10^6 | 是 |
| 梯度裁剪 | 无 | 有(1.0 左右) | 建议有 |
5.16.2 三个”必须”的解释
① \beta_{\min} 不能更小。
\beta_{\min} = 10^{-4} 时,1-\bar\alpha_1 = 10^{-4}。 如果取 \beta_{\min}=10^{-6},那么 1-\bar\alpha_1 = 10^{-6}—— 而 fp32 的有效精度是 10^{-7} 相对量级, 1-\bar\alpha_1 的有效数字只剩一位。
§5.13 失败三已经测过:在 fp16 下,1-\bar\alpha_1 和 1-\bar\alpha_2 直接变成 0。 \beta_{\min} 更小只会让情况更糟。
② \beta_{\max} 要保证 \bar\alpha_T 足够小。
见 §5.13 失败四那张表。判据是 L_T 接近 0, 或者等价地 \bar\alpha_T < 10^{-3}。 这个数不需要训练,算一遍数组就知道。
④ 梯度裁剪建议加上。
本章的玩具模型没加(loss 一直很稳),但真实模型上扩散训练的梯度 会周期性地出现尖峰——因为 t 是随机抽的, 偶尔会抽到一个模型当前表现很差的 t。 裁剪到 1.0 是一个几乎无成本的保险。
5.16.3 一个完整的训练脚本
把 §5.14 那 67 行的核心展开成一份可以直接用的训练脚本:
import math, torch, torch.nn as nn, torch.nn.functional as F
T, B0, B1 = 1000, 1e-4, 0.02
def build_schedule(T=T, b0=B0, b1=B1):
betas = torch.linspace(b0, b1, T, dtype=torch.float64)
abar = torch.cumprod(1.0 - betas, 0)
abar_prev = torch.cat([torch.ones(1, dtype=torch.float64), abar[:-1]])
return {"betas": betas, "alphas": 1.0 - betas, "abar": abar,
"abar_prev": abar_prev,
"post_var": (1 - abar_prev) / (1 - abar) * betas}
def train(data, model, steps=5000, bs=256, lr=2e-3, clip=1.0, seed=0):
sch = build_schedule()
opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.0)
g = torch.Generator().manual_seed(seed)
N = data.shape[0]
log = []
for step in range(steps):
# ① 抽数据、抽时间步(t 必须覆盖全部 T 个等级)
x0 = data[torch.randint(N, (bs,), generator=g)]
t = torch.randint(0, T, (bs,), generator=g)
# ② 前向一步到位(闭式解,不是走 t 步)
a = sch["abar"][t].to(x0.dtype).view(-1, 1)
eps = torch.randn_like(x0)
xt = a.sqrt() * x0 + (1 - a).sqrt() * eps
# ④ 回归 ε
loss = F.mse_loss(model(xt, t), eps)
opt.zero_grad(set_to_none=True)
loss.backward()
# ⑤ 裁剪——这一行在真实模型上不能省
gn = torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
opt.step()
if step % 100 == 0:
log.append((step, loss.item(), gn.item()))
return model, sch, log这段代码里有四个编号的注释,每一个都对应本章的一节:
- ① 对应 §5.13 失败二(漏掉一段 t)
- ② 对应 §5.4(闭式解,以及”为什么能一步到位”)
- ④ 对应 §5.6(ELBO 化简到 \mathcal{L}_{\text{simple}})
- ⑤ 对应本节第 ④ 条
5.16.4 训练过程中该看什么
不要只看总 loss。 本章反复演示了它有多不可靠。 建议在训练循环里额外记三样:
| 看什么 | 怎么看 | 能抓什么 |
|---|---|---|
| 按 t 分段的 loss | 把 [0,T) 分成 5–10 段,各段单独平均 | 某一段没被训练到(§5.13 失败二) |
| 梯度范数 | clip_grad_norm_ 的返回值 |
尖峰、训练不稳 |
| 采样结果的统计量 | 每 N 步采一次,算均值/方差/rms | 尺度塌缩(案例的 bug) |
第三项最关键,也最容易被忽略—— 因为跑一次完整采样很慢(本章 1000 步在 GPU 上约 0.3 秒, 真实模型上要几十秒)。
一个便宜得多的替代:只跑前 100 步反向过程,看尺度是否合理。 本章案例里,错误版本在 t=900 时半径已经掉到 0.4981 (正确是 1.2969),只要 100 步就能看出问题。
这一节真正要你记住的是
训练一个扩散模型,代码量不大、公式不多, 但”看起来正常”和”实际正确”之间的距离很远。
唯一的防线是主动测量—— 测分段 loss、测梯度、测采样统计量。 本章的案例、失败一、失败二、失败五,四次都是”loss 正常但结果不对”。
5.17 一次完整的排查:把案例的 bug 逐步定位出来
§5.2 讲了案例的结论,§5.13 讲了五类失败模式。 这一节把从”采样不对”到”找到那一行”的完整路径走一遍—— 按真实的时间顺序,包括我们走过的弯路。
这一节可以当模板用:下次你自己的扩散模型采样不对,照着走。
5.17.1 第一步:先分清是哪一类问题
采样结果不对,可能的原因有三层:
| 层 | 出问题的地方 | 怎么排除 |
|---|---|---|
| 数据层 | 数据本身有问题(分布不对、归一化不对) | 画出数据;算统计量 |
| 训练层 | 模型没学好 | 看 loss 曲线;看按 t 分段的 loss |
| 采样层 | 采样代码写错了 | 用训练好的模型 + 已知正确的采样器 |
先看数据。 本章的数据实测 rms 0.6110、标准差 [0.5707, 0.3351]、最大绝对值 1.136—— 看起来就是两个弯月,没有异常。
再看训练。 loss 从 0.8844 单调降到 0.1355,无 nan、无震荡。 把 loss 按 t 分成五段看(§5.6 的表):
| t 段 | 训练初期 loss | 训练末期 loss |
|---|---|---|
| 1–200 | 0.9752 | 0.6055 |
| 201–400 | 0.9349 | 0.1403 |
| 401–600 | 1.0591 | 0.0256 |
| 601–800 | 1.0103 | 0.0071 |
| 801–1000 | 0.9891 | 0.0046 |
五段都在下降,而且末期的分布是单调的: t 越大,末期 loss 越低,从 0.6055 一路降到 0.0046,差 130 倍。
这个方向值得解释一下,因为它和”低噪声更容易”的直觉相反。
\varepsilon-预测的损失是 \|\varepsilon-\varepsilon_\theta\|^2。 在大 t 处 x_t\approx\varepsilon, 模型只要输出 \varepsilon_\theta\approx x_t 就能拿到接近 0 的误差—— 这一档的 loss 0.0046 几乎就是”抄 input”的成绩。
在小 t 处 x_t\approx x_0,噪声 \varepsilon 只是一个小扰动(系数 0.0148)。 模型要把这个几乎看不见的分量精确地抠出来—— 这一档的 loss 0.6055 大约是”预测一个标准正态”的成绩。
换句话说:按 MSE 度量,小噪声端才是真正难的地方。
而 §5.6 那个 ELBO 权重(\lambda_t 在小 t 处极大)恰好也是这么设计的—— 两者在这一点上是一致的:小 t 既要罚得重,也确实学得慢。
所以问题在采样层。
5.17.2 第二步:把采样拆成”均值”和”方差”两半
采样循环里的每一步做两件事:
mean = ... # ① 往哪儿走
x = mean + sigma * torch.randn_like(x) # ② 走多远分开检查这两半。
先查 ②:把 sigma 打出来,看它的值。
print([round(sch["sigma"][t].item(), 6) for t in (0, 1, 10, 100, 500, 999)])实测:[0.0, 0.007386, 0.015431, 0.045113, 0.100166, 0.141349]。
形状是对的:从 0 开始、两端小中间大、单调增。 和 §5.5 那张 \sqrt{\tilde\beta_t} 的表一致。
所以 ② 没问题——问题在 ①。
5.17.3 第三步:检查均值的四个系数
反向均值(式 7)展开成四个量的乘积:
\mu_\theta = \underbrace{\frac{1}{\sqrt{\alpha_t}}}_{\text{系数 A}}\cdot x_t - \underbrace{\frac{1}{\sqrt{\alpha_t}}}_{\text{系数 A}}\cdot \underbrace{\frac{\beta_t}{\sqrt{1-\bar\alpha_t}}}_{\text{系数 B}}\cdot \varepsilon_\theta
把这两个系数在两端打出来对照:
| t | 系数 A =1/\sqrt{\alpha_t} | 系数 B =\beta_t/\sqrt{1-\bar\alpha_t} |
|---|---|---|
| 1 | 1.000050 | 0.010000 |
| 100 | 1.001036 | 0.006455 |
| 500 | 1.005020 | 0.010459 |
| 1000 | 1.010050 | 0.020000 |
两个系数都在 0.01–1.01 之间,形状合理, 和 §5.13 失败六描述的”写错符号会变成 157”完全不是一回事。
均值也没问题。
5.17.4 第四步:既然两半都对,问题在”怎么拼起来”
这是整个排查的转折点。
均值的两个系数都对,方差的数值也对—— 那么错的只能是它们在循环里的组合方式。
做法是把循环体逐行对照论文的 Algorithm 2。 论文写的是:
x_{t-1} = \frac{1}{\sqrt{\alpha_t}}\left(x_t - \frac{1-\alpha_t}{\sqrt{1-\bar\alpha_t}}\varepsilon_\theta\right) + \sigma_t z
把每一行代码贴上去对照:
| 论文里的量 | 代码 | 一致? |
|---|---|---|
| x_t | x |
✓ |
| \varepsilon_\theta | eps |
✓ |
| \frac{1-\alpha_t}{\sqrt{1-\bar\alpha_t}} | coef |
✓ |
| \frac{1}{\sqrt{\alpha_t}} | inv_sqrt_alpha[t] |
✓ |
| \sigma_t | sigma[t] |
✓ |
| + \sigma_t z | (没有) | ✗ |
找到了。 五个量里四个对上了,最后一个在代码里根本不存在。
第三、四步之间。
第三步检查完两个系数都”形状合理”之后,很容易就此认定”均值没问题”, 然后回到方差那边继续折腾—— 毕竟方差那边还有”用 \beta_t 还是 \tilde\beta_t“这个真实的自由度(§5.2 弯路三)。
真正的做法是换一种对照方式: 不是”检查每个量的数值合不合理”, 而是“把公式的每一项和代码逐项对齐,看有没有多余的或缺失的”。
前者是”审查”,后者是”对账”。
审查依赖判断力,对账不依赖—— 它只是一个清单:公式有几个项、代码有几个项、一一对上。 这次之所以能找到,就是因为最后换成了对账。
5.17.5 第五步:确认修好了,并且确认它以前为什么没被发现
修法只有一行,加回 + sigma[t] * torch.randn_like(x)。
修完之后的验证要做两件事:
① 采样统计量回到正常(§5.2 的表): rms 0.2030 → 0.6090,标准差 [0.0043, 0.0078] → [0.5598, 0.3437], MMD 0.96029 → 0.01146。
② 回答”为什么以前没发现”:把 bug 加回去,重训一遍, 比对两条 loss 曲线——逐点差 0.00e+00。
第二件事比第一件重要。 它把”我们修好了一个 bug” 升级成”我们知道了这个 bug 为什么能藏这么久”—— 而这决定了下次要在哪里加检查。
结论是:在采样函数的出口加一个统计量断言。
def sample(model, sch, n, ...):
...
assert x.std(0).min() > 0.05 * data_std.min(), \
"采样结果的标准差异常小——检查反向过程是否注入了噪声"
return x这一行断言,能挡住本章 §5.13 里失败一和失败二这两类错误。
5.18 后验的几何直觉
§5.5 把后验 q(x_{t-1}\mid x_t,x_0) 推出来了。这一节给一个几何图像, 因为那个公式(式 5)虽然能用,但看不出”它在干什么”。
5.18.1 两个方向的拉扯
后验均值是 x_t 和 x_0 的凸组合:
\tilde\mu_t = c_1 x_t + c_2 x_0,\qquad c_1 + c_2 = 1
(在 t 较小时 c_1+c_2\approx1;t 大时它会小于 1,见下文。)
x_t 和 x_0 代表两个”证据”:
- x_t:当前的观测。它说明”现在在哪里”。
- x_0:原始数据点。它说明”本来应该在哪里”。
后验均值就在这两点之间的连线上。
c_2(x_0 的权重)随 t 怎么变? 实测:
| t | c_1(x_t 权重) | c_2(x_0 权重) | c_1+c_2 |
|---|---|---|---|
| 2 | 0.4547 | 0.5453 | 1.000000 |
| 10 | 0.8527 | 0.1473 | 1.000000 |
| 100 | 0.9809 | 0.0191 | 0.999972 |
| 500 | 0.9941 | 0.0031 | 0.997177 |
| 1000 | 0.9899 | 0.0001 | 0.990077 |
t 小的时候,后验主要相信 x_0;t 大的时候,几乎只相信 x_t。
为什么? 因为 x_t 里关于 x_0 的信息量由 \bar\alpha_t 决定。 t 大时 x_t 几乎不含 x_0 的信息,所以后验只能相信观测本身。
而这个”信息量”正是 \tilde\beta_t 度量的:c_2 越小,说明 x_0 的权重越低, \tilde\beta_t 越大(后验越不确定)。
5.18.2 用一张图看
把 x_{t-1} 的分布想成一个云团:
- 云团的中心在 x_t 和 x_0 的连线上(位置由 c_1:c_2 决定)
- 云团的半径是 \sqrt{\tilde\beta_t}
- t 越大,云团越大、中心越靠近 x_t
反向过程做的事就是:给定 x_t,在不知道 x_0 的情况下, 用一个模型去猜”x_0 大概在哪里”(式 6), 然后把云团摆在猜出来的那条连线上,再从云团里抽一个点。
所以案例里那个 bug 的几何图像是
正确的采样:每一步把云团摆在连线上,然后从云团里随机取一点。 云团的半径 \sqrt{\tilde\beta_t} 保证了这个随机性有多大。
错误的采样:每一步把云团摆好,然后只取圆心。
结果:整个过程从一个”云团在移动”变成了”一个点在移动”。 而且这个点会被越拉越靠近流形(因为每步的 \mu_\theta 都在往数据流形上拉), 最后整个先验被压成流形上的一个点。
实测的轨迹完全符合这个图像: 半径从 1.26 一路掉到 0.04(t=300), 然后因为模型在 OOD 输入上外推,又反弹到 0.20(§5.2 的轨迹表)。
这个几何图像还有一个用处:它解释了”为什么 \sigma_t 的取值不重要” (§5.5 实测:\sqrt{\beta_t} 和 \sqrt{\tilde\beta_t} 差别可忽略)。 因为云团的半径本来就比中心的位置不确定度小一个量级—— 关键是云团在不在,而不是它精确多大。
5.19 从 2D 到图像:哪些结论能迁移
本章全程用一个 2D、5 万参数的玩具模型。 下篇后面三章会逐步走向真实的图像模型,所以现在必须说清楚: 哪些结论是数学,哪些结论只是这个玩具的性质。
5.19.1 能迁移的:凡是”从定义推出来的”
这一类的共同特征是:结论的成立不依赖模型大小、不依赖数据维度。
| 结论 | 为什么能迁移 |
|---|---|
| 前向过程有闭式解(式 2) | 纯代数,d 取任何值都成立 |
| 后验是高斯的,方差是 \tilde\beta_t(式 4) | 高斯相乘的性质,与维度无关 |
| 反向过程必须注入噪声 | 是”从分布采样”这个定义的一部分 |
| ELBO 的三项分解(式 11) | 是 Jensen 不等式 + 马尔可夫性的直接结果 |
| \|\varepsilon-\varepsilon_\theta\|^2 = \mathrm{SNR}\cdot\|x_0-\hat x_0\|^2(式 17) | 纯代数 |
| \lambda_1 发散 | 是 \tilde\beta_1 = 0 的推论,任何调度都成立 |
这六条是本章的”骨架”,它们在任何规模的扩散模型上都是对的。
5.19.2 不能迁移的:凡是”和规模有关的”
| 结论 | 为什么不能迁移 |
|---|---|
| T=25 比 T=1000 好(§5.9) | 训练预算被固定成 5000 步,对 T=1000 不利 |
| 第 0 层比第 1 层重要 6 倍(第 4 章) | 只有 2 层,第 1 层可以直接抄第 0 层 |
| \beta_{\max}=0.002 测不出危害(§5.13 失败四) | 2 维双月太宽容,误差被吸收了 |
| 余弦调度只比线性好 12%(§5.8) | 在 ImageNet 上这个差距要大得多 |
| x_0-预测差 12 倍(§5.13 失败五) | 没给它单独调超参数;大模型上差距会缩小 |
这一类的共同特征是:结论里出现了一个没有量纲分析支撑的比较 (“更好”、“更重要”、“差几倍”),而这个比较依赖具体的规模、数据和预算。
5.19.3 一条判断规则
看到任何一条结论,先问一句:把规模改十倍,这个结论还成立吗?
- 如果答案是”显然成立”——它是数学,可以放心用
- 如果答案是”可能会变”——它是一个实验观察,要么给出适用范围,要么标注为待定
本章的每一节末尾都做了这个区分: §5.4–§5.7 推的都是第一类,§5.8 和 §5.9 的实测都是第二类。
5.19.4 图像上多了什么
本章的 67 行和真实的图像扩散模型之间,差的不只是”网络更大”。 这一节列出四类必须补的东西,以及每一类为什么在本章的规模上看不出来。
① 数据归一化。
本章的数据已经缩放到 [-1,1]、方差 [0.326, 0.112]。 §5.4 已经说明这会影响 x_t 的方差曲线 (\mathrm{Var}(x_t) = \bar\alpha_t\mathrm{Var}(x_0) + (1-\bar\alpha_t))。
图像上通常归一化到 [-1,1] 且三个通道各自标准化, 让 \mathrm{Var}(x_0)\approx1。这样 x_t 的方差在整个 t 上都是 1, 前向过程和反向过程的尺度完全一致。
本章的数据没做到这一点——这本身不是错误, 但它意味着”t 小时 x_t 的尺度比 t 大时小 3 倍”, 模型必须在不同噪声水平上处理不同尺度的输入。 真实模型靠归一化避免这件事。
② 网络结构。
本章用的是普通 MLP。图像模型必须用有空间结构的网络: U-Net(多尺度卷积 + 跳连)或 DiT(把 Transformer 用在图像 patch 上)。
为什么要多尺度? 因为去噪需要在不同尺度上同时看—— 局部纹理(高频)和整体结构(低频)需要不同的感受野。 MLP 在处理 2 维点云时没有这个问题(每个点就是全部), 在 32×32 以上的图像上,没有多尺度就学不动。
④ 时间步的注入方式。
本章把 t 的正弦编码和 x_t 拼接后送进网络。
图像模型通常用”调制”(modulation): 把 t 的编码映射成缩放和平移系数, 逐层作用在特征图上(AdaGN 那一类做法)。
差别在于:拼接只在第一层告诉网络”现在是哪个噪声水平”, 之后网络可能”忘掉”;调制则在每一层都重新注入一次。 在深层网络上,这个差别很显著。
⑤ 训练规模。
本章训 5000 步、5 万参数。 CIFAR-10 上一个小扩散模型要几十万步、几千万参数; ImageNet 256 上是几百万步、几亿到几十亿参数。
这一条不用多解释,但它解释了为什么 §5.19 那张分界表是必要的—— 本章所有”实测”类的结论,都出自一个比真实模型小五个数量级的设定。
5.19.5 四条”能迁移 / 不能迁移”的判断示例
把上面四类差异套进 §5.19 的判据,各举一例:
| 结论 | 能迁移? | 理由 |
|---|---|---|
| 反向过程必须注入噪声 | 能 | 是分布的定义,与网络结构无关 |
| 时间步的正弦编码 | 能,但不够 | 编码方式是对的,但注入方式要改 |
| T=25 足够 | 不能 | 训练预算的产物;图像上每级噪声都要大量样本 |
| \beta_{\min}=10^{-4} | 能 | 是 1-\bar\alpha_t 的精度要求,与数据无关 |
| 线性调度浪费 1/3 步数 | 能 | 是调度形状的算术性质 |
| 余弦只比线性好 12% | 不能 | 在 CIFAR-10 上差距大得多 |
请注意第三行和第六行: 它们是本章仅有的两条”实测出来、且明显不能迁移”的结论, 而它们在 §5.8 和 §5.9 里就被标出来了—— 不是等到最后才补救的。
5.19.6 玩具模型真正的作用
它不是为了”模拟”真实模型,而是为了”隔离”变量。
在图像上,一个采样结果不好,可能是模型容量不够、数据不够、 训练不够、调度不对、采样器不对——六七个原因纠缠在一起。
在 2D 双月上,你可以把每一个变量单独拨动,其它全部固定。 本章测的”加宽模型”、“延长训练”、“换调度”、“换参数化”, 在图像上每一个都是几天的实验,在这里都是几十秒。
代价是你必须时刻记得:你测出来的是”这个玩具的性质”, 还是”扩散模型的性质”。 这一节就是那张分界表。
5.20 三个容易被忽略的实现细节
本章的 67 行实现是一个最小可用版本。真实模型上还有三个细节, 它们不改变数学,但会显著影响结果。
5.20.1 细节一:权重滑动平均(EMA)
做法:除了被梯度更新的那份权重 \theta,再维护一份”影子权重” \theta_{\text{ema}}:
\theta_{\text{ema}} \leftarrow \gamma\,\theta_{\text{ema}} + (1-\gamma)\,\theta, \qquad \gamma = 0.9999
每训练一步更新一次,但采样时用 \theta_{\text{ema}} 而不是 \theta。
为什么有用? 因为扩散的每一步对 \varepsilon_\theta 的误差都敏感。 单点的 \theta 在训练末期仍然在最优解附近抖动, 而 \theta_{\text{ema}} 是最近一万步的平均,抖动的分量被平掉了。
代价:显存翻倍(要存两份权重),以及 \gamma 这个额外的超参数。
本章的玩具模型没有用 EMA,因为: ① 5000 步训完之后 loss 已经平稳,抖动很小; ② 5 万参数的模型,抖动的影响本来就小。
在真实模型上 EMA 几乎是标配(DDPM 原文用的就是 \gamma=0.9999)。 一个经验判据:如果你的采样结果在不同随机种子之间差异很大, 先加 EMA 试试——它是最便宜的一个改进。
5.20.2 细节二:混合精度与”哪些量必须高精度”
训练可以用 bf16 加速,但有几个量必须留在 fp32。
§5.13 失败三已经测过一个:1-\bar\alpha_t 在 fp16 下会被毁掉 (t=1,2 处直接变成 0)。
完整的高精度清单:
| 量 | 为什么 | 本章实测 |
|---|---|---|
| \bar\alpha_t 的计算 | 1000 次连乘,累积误差 | 必须 float64 |
| 1-\bar\alpha_t | 灾难性抵消 | 前 10 步误差 31%(fp16) |
| \sqrt{\bar\alpha_t}、\sqrt{1-\bar\alpha_t} | 由上面两个数开方得到 | 继承上面的误差 |
| softmax / KL 里的求和 | 大量小量相加 | 通用规则 |
| 模型的前向 | 可以用 bf16 | 本章未测 |
bf16 和 fp16 的区别值得记住:
- fp16:10 位尾数、5 位指数。尾数比 bf16 多,但指数窄 ——下限 6.1\times10^{-5},会下溢。
- bf16:7 位尾数、8 位指数(和 fp32 一样宽)。 不会下溢,但精度更低。
本章实测:\bar\alpha_{1000} = 4.04\times10^{-5} 在 fp16 下 会落进次正规区(第 980 步之后共 21 步),但没有变成 0; 而在 bf16 下完全没有下溢(指数位够宽)。
结论:“用哪个低精度格式”取决于你的量程。 \bar\alpha_t 这种跨越 5 个数量级的量,bf16 比 fp16 安全; 但如果模型里有大量接近 1 的减法,两者都不能用,必须回到 fp32。
5.20.3 细节三:t 的采样分布
本章一直用的是 t\sim U\{0,\ldots,T-1\}——均匀采样。
但 §5.6 那张表已经说明:不同 t 的”难度”差了几千倍。 均匀采样意味着在简单的时刻浪费了训练步数。
常见的改法有三种:
| 改法 | 做法 | 依据 |
|---|---|---|
| 重要性采样 | 按 \lambda_t 或 loss 的大小加权抽 t | §5.6 的 ELBO 权重 |
| 分位数采样 | 让抽到的 t 在 log-SNR 上均匀 | §5.8 的调度分析 |
| 不采样 | 每个 batch 覆盖一段连续的 t | 工程上的简化 |
这三种做法都有论文报告过收益,但收益的大小很不一致—— 在小模型上经常测不出来,在大模型上才有明显差别。
本章没有做这个对照。 这是一个明确留白: 如果你要接着做,最值得测的是”按 \lambda_t 重要性采样 t“, 因为它的理论依据(ELBO 权重)在本章已经推导并量化了。
§5.6 讲过:DDPM 把 ELBO 的权重 \lambda_t 整个扔掉, 改用对所有 t 一视同仁的 \mathcal{L}_{\text{simple}},结果反而更好。
那么”按 \lambda_t 重要性采样 t“,在效果上就接近于”把扔掉的权重又捡回来”。
它和 \mathcal{L}_{\text{simple}} 的成功是不是矛盾?
可能不矛盾,因为两者改的不是同一个东西: - \mathcal{L}_{\text{simple}} 改的是损失的权重(梯度的大小) - 重要性采样 t 改的是数据的分布(梯度来自哪里)
理论上它们是不同的干预,但实际效果可能高度相关。
这个张力是第 6 章要处理的核心问题之一: \mathcal{L}_{\text{simple}} 为什么更好, 以及”重要性采样 t“到底是在修正什么。
5.21 常见误解
这一节收八个在课堂上反复出现的问题。每一条都已经在正文里处理过, 这里只是把它们拎出来,方便对照。
5.21.1 误解一:扩散模型是”先加噪再减噪”,所以学的是”如何逆转加噪”
前半句对,后半句要小心。
反向过程不是前向过程的逆运算。前向过程 x_{t-1}\to x_t 是确定性 + 噪声, 给定 x_t 无法唯一确定 x_{t-1}——它是一个分布。
反向过程学的是那个分布的参数(式 8 的 \mu_\theta 和 \sigma_t), 不是”一个逆函数”。
这个区别的实际后果:反向过程每一步都要采样, 不能”算出来”。这就是案例里那个 bug 的全部背景。
5.21.2 误解二:\varepsilon_\theta 是在”去掉噪声”
更准确的说法是:它在估计”当初加进去的是哪个噪声”。
区别在于信息的方向: “去噪”暗示有一个干净的答案等着被恢复; “估计噪声”说明这是一个有不确定性的推断任务—— 所以输出才需要配一个方差 \sigma_t^2,所以反向过程才是一个分布(§5.5)。
语言上的这个差别看起来很小,但它决定了你会不会写出 x = mean 这样的代码。
5.21.3 误解三:\bar\alpha_t 是”保留的信号比例”
它保留的是”方差”,不是”信号”。
x_t 里 x_0 前面的系数是 \sqrt{\bar\alpha_t}, 所以信号的方差(而不是幅度)按 \bar\alpha_t 衰减。
具体后果:\bar\alpha_{500} = 0.0786 意味着 x_{500} 里信号的系数是 \sqrt{0.0786} = 0.2803—— 不是 0.0786。平方根这一步经常被漏掉。
本章 §5.4 的表同时给了 \bar\alpha_t 和 \sqrt{\bar\alpha_t} 两列, 就是为了防这个。
5.21.4 误解四:loss 越低,生成质量越好
本章出现了三次反例:
| 情况 | loss 变化 | 采样质量 |
|---|---|---|
| 案例(丢掉 \sigma_t z) | 完全不变 | 差 84 倍 |
| 失败二(漏掉一段 t) | 0.1355 → 0.0646(降一半) | 差 3.5 倍 |
| 失败五(x_0-预测) | 不可比 | 差 12 倍 |
loss 度量的是”\varepsilon 预测得准不准”,不是”采样对不对”。 两者之间没有单调关系——甚至可能是反向的, 因为某些错误会让 loss 变得更好看。
5.21.5 误解五:\sqrt{\bar\alpha_t} 和 \sqrt{1-\bar\alpha_t} 是两个独立的量
它们永远满足平方和为 1。
\bar\alpha_t + (1-\bar\alpha_t) = 1
所以 x_t 是”两个系数平方和为 1 的加权和”—— 这个结构正是 §5.7 里 v-预测的设计原型。
记住这一点,很多式子可以少记一半: 给出 \bar\alpha_t 就够了,另一个是 1 减去它。
5.21.6 误解六:ELBO 是一个”近似”
它确实是一个下界,但”下界”不等于”粗糙”。
\log p_\theta(x_0) \ge \text{ELBO} 是一个不等式, 而这个不等式有多紧,取决于模型学得怎么样: 当 p_\theta 的整个路径分布匹配真实后验时,取等号。
实际上,扩散模型的 ELBO 在训练充分之后可以相当紧—— 这也是为什么它能被用作似然评估的代理指标。
但要分清:“用 ELBO 当训练目标是近似”和”ELBO 本身是近似”是两回事。
5.21.7 误解七:T 是”扩散步数”,所以越大越精细
T 是”离散化的粒度”,不是”精度”。
在连续时间视角下(§5.11),调度是一个函数 \beta(\tau), T 只是采样它的密度。T 越大, 每一步跨过的 \bar\alpha 变化越小,但分母 \tilde\beta_t 也越小 (因为 \tilde\beta_t\propto\beta_t\propto 1/T), 于是每一步的惩罚反而更大。
本章实测:T=25 的 MMD 0.00485,T=1000 是 0.01146(§5.9)。 步数多的那个更差。
5.21.8 误解八:反向过程的噪声 \sigma_t 是”为了让结果有多样性”
它确实是随机性的来源,但这只是它的一半作用。
另一半在 §5.5 讲过:它是误差的自我修正机制。 每一步注入新噪声,下一步的模型就在一个新的噪声水平上重新判断一次, 上一次的错误被”洗掉”了一部分。
实验证据:§5.9 那个”确定性采样不稳定”的观察—— 去掉噪声之后,模型 \varepsilon_\theta 的系统性偏差会逐步累积 (实测 1000 步之后溢出到 10^7 量级)。 这正是”没有修正机制”的直接后果。
5.22 本章的实验清单
本章一共跑了 9 组实验。这一节把它们列全,方便你复现、改写、或者接着往下做。
| # | 实验 | 脚本 | 主要结论 |
|---|---|---|---|
| 1 | 案例:丢掉 \sigma_t z | run_ch5_case.py |
采样塌 84 倍,训练 loss 逐点不变 |
| 2 | 三条弯路 | 同上 + ch5_detours.json |
加宽 / 多训 / 换 \sigma 源都无效 |
| 3 | ELBO 三项分解 | run_ch5_case.py |
t\in[2,11] 占 \sum L_{t-1} 的 49% |
| 4 | 调度对比(线性 / 余弦) | run_ch5_schedule.py |
线性 32.7% 的步落在纯噪声区 |
| 5 | 步数 T 的影响 | ch5_steps.json |
T=25 比 T=1000 好(MMD 0.00485 vs 0.01146) |
| 6 | 参数化对比(\varepsilon / x_0) | ch5_fail.json |
x_0-预测差 12 倍(对照不完全公平) |
| 7 | 低精度:1-\bar\alpha_t 的抵消 | ch5_fail.json |
fp16 下前 10 步相对误差 31% |
| 8 | \beta_{\max} 太小 | ch5_fail.json |
L_T 涨几千倍,但小规模下测不出危害 |
| 9 | 六项对拍自检 | diffusion_67.py + 本文 |
六项全部通过 |
5.22.1 复现全部结果
cd notebooks
PY=/home/mpp/micromamba/envs/dlspace/bin/python
$PY run_ch5_case.py --steps 5000 # 实验 1、2、3(约 2 分钟)
$PY run_ch5_schedule.py # 实验 4(约 1 分钟)
$PY export_ch5_points.py # 生成插图用的点云
$PY diffusion_67.py # 67 行单文件版 + 实验 9 的基础实验 5、6、7、8 的脚本在正文对应的节里给了完整代码, 可以直接粘进 notebook 跑。本章的 data/ch5_*.json 里存了全部原始数字。
5.22.2 三组”可以做但本章没做”的实验
按 §5.19 那张分界表的逻辑,这三组属于”值得做、但需要更多算力或更多设计”的一类。
① 第 0 层重要性的深度扫描
§5.6 测出 T\in[2,11] 贡献 49% 的 ELBO, §5.9 测出 T=25 比 T=1000 好。 这两件事是不是同一件事的两面?
做法:固定训练预算,扫 T\in\{10, 25, 50, 100, 250, 1000\}, 每一档同时记录”\sum L_{t-1} 的分布”和”采样 MMD”。 如果 MMD 的最优点总是落在”\sum L_{t-1} 分布最均匀”的那一档,两者就是同一件事。
② \mathcal{L}_{\text{simple}} 为什么更好
这是第 6 章的核心问题,但这里有一个可以在本章规模上做的前置实验: 训练时分别记录小 t 段和大 t 段的梯度范数, 以及它们与最终下降方向的对齐程度(余弦相似度)。 如果小 t 段的梯度方向确实更嘈杂,那就说明”削掉它的权重”是合理的。
④ 重要性采样 t
§5.20 提到的那个张力:按 \lambda_t 采样 t(相当于把权重捡回来) 和 \mathcal{L}_{\text{simple}}(把权重扔掉)看起来是相反的干预。
做法:在同样的训练预算下跑三组—— 均匀采样 + \mathcal{L}_{\text{simple}}、 \lambda_t 重要性采样 + \mathcal{L}_{\text{simple}}、 \lambda_t 重要性采样 + 完整 ELBO 权重。 三组的 MMD 排序会直接回答”权重到底该不该扔”。
5.23 补充检查点
5.23.1 停下来想一想 ⑧
一个模型在 t=1 处的 \tilde\beta_1 精确等于 0。 如果代码里直接算 1/\tilde\beta_1,会得到 inf。
但本章的实现从头到尾没有出现 inf。为什么?
(答案:因为采样循环在 t=0(也就是代码里对应 t=1 那个下标)时走了另一条分支:
if t > 0 and add_noise:
x = mean + post_var[t].sqrt() * torch.randn_like(x)
else:
x = meant=0 那一支根本不碰 post_var[0]——它不注入噪声, 所以也不需要标准差。
而 post_var[0] = 0 这个值本身是安全的: 它在数组里躺着,没被除过。
但 §5.6 的 ELBO 计算就不一样了:L_1 的公式里有 \|\Delta\|^2/\tilde\beta_1——除以 0。
所以本章的 elbo_terms 里循环是 for t in range(1, T), 从下标 1 开始,跳过了下标 0。
这就是 §5.6 那个”t\in[2,11] 贡献 49%“的表格里 最小的时间步是 2 而不是 1 的原因——不是我们不想算 t=1, 是它在 ELBO 的原始形式下没有定义(§5.6 已经说明 DDPM 把 L_0 单独拆出来处理)。
这一类”某一个时刻是奇点”的情况,在扩散模型里反复出现: \tilde\beta_t 在 t=1 处为 0、\bar\alpha_t 在 t=T 处接近 0、 1/\sqrt{\bar\alpha_t} 在 t 大时发散。 每一个都需要在代码里单独处理,而它们都不会自己报错。)
5.23.2 停下来想一想 ⑨
本章反复说”训练 loss 看不见采样的 bug”。 那么反过来说:有没有什么 bug 是”采样看得见、训练看不见”的反面—— 即”训练能看见、采样看不见”?
(答案:有,而且很常见。
最典型的是”训练时 t 的采样范围正确,但采样时用的调度数组是另一个”。
比如训练用线性调度,采样循环里却读了余弦调度的 \bar\alpha 数组 (两个数组长度都是 1000、变量名都是 alpha_bar)。
这时: - 训练 loss 完全正常(训练用的是正确那个数组) - 采样结果会崩——不,等一下。
这个例子的方向反了。 让我们把它拆清楚:
- 如果训练和采样读的是同一个数组(都错或都对)→ 两边自洽, 训练 loss 正常,采样也正常。这个 bug 完全不可见。
- 如果训练和采样读的是不同的数组 → 训练正常, 采样崩。这仍然是”训练看不见、采样看得见”。
所以”训练看得见、采样看不见”需要另一个结构: 采样路径上根本没用到的量,被训练用到了。
一个真实的例子:L_{\text{simple}} 里 t 的采样分布。 如果你把 t\sim U\{0,T-1\} 改成 t\sim U\{0,T/2-1\}(只训一半), 训练 loss 会明显变化(因为平均的区间变了), 但采样仍然是走完整的 1000 步——它会经过那些从没被训练过的 t。
这个 bug 在训练曲线上一眼可见(loss 会异常), 在采样的统计量上却不一定明显——因为模型在那 500 步上的输出是外推的, 可能”看起来还行”,只是细节不对。
§5.13 失败二正是这个结构:那里是”训练的 t 少了一段”, 实测 MMD 差了 3.5 倍。所以它是”两边都看得见,只是训练那边更容易发现”。
真正”训练看得见、采样绝对看不见”的情况很少, 因为训练和采样的交集(\bar\alpha_t 和 \varepsilon_\theta) 正是模型能力的来源——一旦它出问题,两边都会受影响。
这个不对称值得记住: “训练看不见采样”是真的(本章三次实测); “采样看不见训练”基本不成立。 所以排查的方向应该是:训练 loss 正常时,去查采样代码; 而不是反过来。)
5.23.3 停下来想一想 ⑩
本章最后留一个问题。
§5.6 测出 t\in[2,11] 这 10 步贡献了 49% 的 ELBO, 而 DDPM 把这些权重全扔了,结果反而更好。
如果这个”反而更好”是真的,那么下面哪一件事必然成立?
A. 那 49% 的 ELBO 是没有意义的 B. 那 10 步的模型输出质量其实不差 C. ELBO 这个下界本身有问题 D. t\in[2,11] 这 10 步的梯度方向特别嘈杂
(答案:D 是最可能的,而 A、B、C 都不必然。
先把逻辑理清楚。
扔掉权重之后,t\in[2,11] 的梯度贡献从 49% 掉到 1%。 如果去掉它们之后结果变好,那说明这 49% 的贡献”净效果是负的”。
“净效果为负”意味着什么? 两种可能:
- 梯度方向嘈杂(D):每一次更新都朝一个大致随机、 但幅度很大的方向走。放大的权重把噪声也放大了。
- 梯度方向系统性地错:那才是 A(这个目标没意义)。 但 ELBO 是从 Jensen 不等式严格推出来的, 它的方向不会系统性错误,只可能是”方差大”。
B 为什么不必然? 即使那 10 步的模型输出不差, 也不妨碍它的梯度贡献是负的——梯度关心的是”往哪改”, 不是”现在有多好”。
C 为什么不必然? ELBO 作为下界是数学事实。 它的问题可能是”作为训练目标的方差太大”,而不是”下界不对”。 这两个是完全不同的问题。
所以正确答案是 D,而且它是可测的—— 记录那 10 步的梯度与最终下降方向的余弦相似度, 和 t 大时的做对比。
这个实验本章没做,它是第 6 章的入口之一。)
5.24 自测
十道题。前六道能在本章找到直接答案,后四道需要自己推或算。
1. 前向过程为什么能”一步加噪到任意 t“?这个性质依赖哪一条假设? 把这条假设去掉会怎样?
2. 后验 q(x_{t-1}\mid x_t,x_0) 的方差 \tilde\beta_t 在 t=1 处等于 0。 为什么这个 0 是”对的”而不是”数值问题”?
3. 反向过程 p_\theta(x_{t-1}\mid x_t) 是一个高斯分布。 如果把它的方差设成 0,还叫扩散模型吗? 和”取均值”有什么区别?
4. 本章的案例里,把 \sigma_t z 去掉之后,采样塌了 130 倍, 但训练 loss 一位小数都不变。用一句话说清为什么。
5. 实测 x_0-预测的 MMD 是 \varepsilon-预测的 12 倍。 这个对照有一处不公平,是什么?
6. 本章测出 T=25 比 T=1000 好(MMD 0.00485 vs 0.01146)。 这个结论能迁移到图像模型上吗?为什么?
7. (推导) 证明 q(x_t\mid x_{t-1}) 不依赖 x_0, 即 q(x_t\mid x_{t-1},x_0) = q(x_t\mid x_{t-1})。 这条性质在 ELBO 的哪一步用到了?
8. (推导) 从 (12) 出发,推出 (13): L_{t-1} = \frac{\beta_t^2}{2\sigma_t^2\alpha_t(1-\bar\alpha_t)} \mathbb{E}\left[\|\varepsilon-\varepsilon_\theta\|^2\right] + \text{const}
9. (计算) 本章的调度下,\bar\alpha_{500} = 0.078587。 求 t=500 时的 SNR、\sqrt{\bar\alpha_{500}}、\sqrt{1-\bar\alpha_{500}}, 并判断此时”信号比噪声大还是小”。
10. (设计) 你要给一个已有的扩散模型换一个更快的采样器, 但不能重新训练。你会先检查哪三件事?为什么是这三件?
11. (辨析) 有人说:“扩散模型的反向过程就是前向过程的逆过程, 所以两者是一一对应的。” 这句话哪里不对?
12. (计算) 某模型的 \bar\alpha_t 在 t=100 处是 0.8970。 求此时 x_t 里噪声的方差占比,以及”信号系数是噪声系数的几倍”。
5.25 自测答案
1. 依赖的假设是马尔可夫性:x_t 只依赖 x_{t-1},不依赖更早的历史。 有了它,t 步的联合分布才能写成 \prod_s q(x_s\mid x_{s-1}), 把 x_0 反复代入之后,所有 \varepsilon 项都是独立正态的线性组合, 方差等于系数平方和,于是合成一个单独的 \bar\varepsilon。
去掉这条假设会怎样? 比如允许 q(x_t\mid x_{t-1},x_{t-2}), 那么 x_t 的分布里会多出二阶项, \bar\alpha_t 这种”一次乘积”的写法就不成立了—— 要维护一个随 t 增长的统计量(比如二阶矩的递推), 闭式解不再是一行。
这也是为什么本章 (10) 那一步改写能成立: q(x_{1:T}\mid x_0) 可以从”前向链”改写成”后验链”, 靠的正是马尔可夫性。
2. \tilde\beta_1 = 0 是对的,因为在 t=1 处,x_1 里几乎全是 x_0 的信息。
具体算:x_1 = \sqrt{\bar\alpha_1}x_0 + \sqrt{1-\bar\alpha_1}\varepsilon_1, 而 \sqrt{\bar\alpha_1} = 0.99995、\sqrt{1-\bar\alpha_1} = 0.01。 给定 x_1 和 x_0,\varepsilon_1 是被唯一确定的—— 于是 x_0 = (x_1 - \sqrt{1-\bar\alpha_1}\varepsilon_1)/\sqrt{\bar\alpha_1} 是一个确定性的等式,没有自由度。
“没有自由度”就是方差为 0。
反过来看 t 很大的情况:\bar\alpha_t \approx 0 时 x_t 里 x_0 的信息几乎为零,给定 x_t,x_{t-1} 的不确定性最大 (等于 \beta_t,即前向过程每一步注入的噪声量)。 从 0 平滑增长到 \beta_t,这个形状本身就是”信息在逐步流失”的量化。
3. 严格说,方差为 0 的”高斯”是狄拉克 \delta 函数,不是一个概率分布 (它在数学上是一个测度)。所以把方差设成 0 之后, p_\theta(x_{t-1}\mid x_t) 不再是一个合法的条件分布, (12) 里的 \log(\sigma_t^2/\tilde\beta_t) 和 \tilde\beta_t/\sigma_t^2 都会发散。
而这正是”取均值”和”方差为 0”的区别所在:
- “取均值”是一种采样方式:分布仍然是高斯,只是我们每次都从它的中心取点。 训练完全正常,ELBO 完全正常。
- “方差为 0”是改了模型的定义:ELBO 本身就不成立了, 训练会立刻出问题(loss 变 nan 或无穷)。
本章案例的 bug 是前者——分布定义得好好的,采样时只取了中心。 这就是它能藏住的原因:所有数学都没错,只有采样动作漏了一步。
4. 因为训练走的是前向过程(用 \bar\alpha_t 造 x_t), 采样走的是反向过程(用 \mu_\theta 和 \sigma_t 递推), 两段代码除了 \bar\alpha_t 和 \varepsilon_\theta 之外没有任何交集—— \sigma_t 和反向均值的公式在训练里一次都没出现。
(如果只能写一句:loss 度量的不是”采样对不对”,而是”\varepsilon 预测得准不准”。 这两件事之间没有必然联系。)
5. 两者的训练步数都是 5000 步,但两个 loss 的梯度尺度差 2.5 个数量级。
按 (17),x_0-预测等价于给 \varepsilon 损失乘 1/\mathrm{SNR}_t, 而这个因子在 t 上的变化范围是 \pm2.5 个数量级。 同样的学习率作用在尺度相差这么大的梯度上, 不可能同时对两端都好——很可能 t 大的一端学得过度、t 小的一端学不动。
要让对照公平,至少得给 x_0-预测单独扫一遍学习率, 或者像文献里那样配上 \mathrm{SNR}_t 权重(那就等于 \varepsilon-预测了,见检查点 ⑤)。
这处不公平必须写出来,否则读者会以为”x_0-预测就是不行”, 而文献里的结论是”x_0-预测能用,只是不省事”。
6. 不能。
§5.9 那个实验把训练预算固定成 5000 步, 而 T 越大,每个噪声等级分到的更新次数越少: T=1000 时平均每个等级 5 次,T=25 时 200 次。
换句话说,那个实验测的其实是”在固定预算下,步数与训练充分度的权衡”, 而不是”T 本身的影响”。
在图像模型上,这个权衡的答案很可能反过来: 图像分布的复杂度远高于双月,每个噪声等级都需要大量样本才能学好, 所以实践中 T 取 1000 甚至 4000。
正确的结论应该是:“步数越多越好”是没有根据的直觉; 最优步数取决于训练预算和任务复杂度。 本章的数据只能说明前半句。
7. 证:q(x_t\mid x_{t-1},x_0) = \dfrac{q(x_t, x_{t-1}, x_0)}{q(x_{t-1}, x_0)}。
按前向链的马尔可夫性,q(x_t\mid x_{t-1},x_0) = q(x_t\mid x_{t-1})—— 这是假设,不是推论。
要”证明”它,得回到前向过程的构造: x_t = \sqrt{\alpha_t}x_{t-1} + \sqrt{\beta_t}\varepsilon_{t-1}, 右边只出现 x_{t-1} 和一个新抽的 \varepsilon_{t-1}, 和 x_0 无关。所以给定 x_{t-1},x_t 的分布是 \mathcal{N}(\sqrt{\alpha_t}x_{t-1}, \beta_t I),x_0 是无关的额外条件。
在 ELBO 的哪一步用到? 在 §5.6 分解 (9) 的时候: 分子的 \prod_t p_\theta(x_{t-1}\mid x_t) 和 分母里前向链的每一项 q(x_t\mid x_{t-1})—— 正是因为后者不依赖 x_0,改写 (10) 之后下标才能对齐。 如果不成立,“每一项配成一个 KL”这件事就做不到了。
8. 证:由 (5) 和 (7),两个均值之差是
\tilde\mu_t - \mu_\theta = \frac{\sqrt{\alpha_t}(1-\bar\alpha_{t-1})}{1-\bar\alpha_t}x_t + \frac{\sqrt{\bar\alpha_{t-1}}\beta_t}{1-\bar\alpha_t}x_0 - \frac{1}{\sqrt{\alpha_t}}x_t + \frac{\beta_t}{\sqrt{\alpha_t}\sqrt{1-\bar\alpha_t}}\varepsilon_\theta
x_t 的两项系数之和为 0(这正是 §5.5 里算过的 \frac{\sqrt{\alpha_t}(1-\bar\alpha_{t-1})}{1-\bar\alpha_t} + \frac{\beta_t}{\sqrt{\alpha_t}(1-\bar\alpha_t)} = \frac{1}{\sqrt{\alpha_t}}, 移项即得)。于是只剩 x_0 和 \varepsilon_\theta 两项:
= \frac{\sqrt{\bar\alpha_{t-1}}\beta_t}{1-\bar\alpha_t}x_0 + \frac{\beta_t}{\sqrt{\alpha_t}\sqrt{1-\bar\alpha_t}}\varepsilon_\theta
把 x_0 用闭式解换成 x_t 和 \varepsilon: x_0 = (x_t - \sqrt{1-\bar\alpha_t}\varepsilon)/\sqrt{\bar\alpha_t}, 代进去之后 x_t 的系数是 \frac{\beta_t}{1-\bar\alpha_t} - \frac{\beta_t}{\sqrt{\alpha_t}\sqrt{1-\bar\alpha_t}} ——不为 0。
正确的做法是不要展开 x_0,而是直接用 (16) 的逆关系。 把 x_t = \sqrt{\bar\alpha_t}x_0 + \sqrt{1-\bar\alpha_t}\varepsilon 代进第一项, 再用 \bar\alpha_{t-1}\bar\alpha_t^{-1} = \alpha_t^{-1} 化简,得到
\tilde\mu_t - \mu_\theta = \frac{\beta_t}{\sqrt{\alpha_t}\sqrt{1-\bar\alpha_t}}\left(\varepsilon_\theta - \varepsilon\right)
这就是要的结果。 代进 (12),注意 x_t 项已经消掉、 只剩 \|\varepsilon_\theta-\varepsilon\|^2 乘一个标量系数, 再加上三个常数项,即得 (13)。∎
9. \mathrm{SNR} = \dfrac{0.078587}{1-0.078587} = \dfrac{0.078587}{0.921413} = \mathbf{0.08529}。
\sqrt{\bar\alpha_{500}} = \sqrt{0.078587} = \mathbf{0.28033}。
\sqrt{1-\bar\alpha_{500}} = \sqrt{0.921413} = \mathbf{0.95990}。
信号比噪声小得多:0.28 对 0.96,噪声系数是信号系数的 3.4 倍。 按信噪比 0.085 说,噪声能量是信号的 11.7 倍。
这个位置是”前向过程走了一半多”—— \bar\alpha 从 1 掉到 0.079,跨过了 92% 的区间。
10. 三件事:
① 采样器假定的”模型输出的是什么”。 DDIM 和 DPM-Solver 的公式都是针对 \varepsilon-预测写的。 如果你的模型是 v-预测或 x_0-预测, 必须先按 (19) 换算,否则公式套不上。
② 采样器假定的”时间步网格”。 DDIM 允许跳过中间步,但它假定的 \bar\alpha 是连续函数还是离散数组? 本章的实现用的是离散数组 + 最近邻取整(§5.9 的 ddim_sample), 这在 T 大、跳步少的时候没问题,跳步多的时候会引入额外误差。
④ 采样器是否需要额外的假设。 比如 DDIM 的 \eta=0 是确定性的, 而 §5.9 实测:本章这个玩具模型上,确定性采样不稳定—— 推理时的 \varepsilon_\theta 只要有一点系统性偏差, 1000 步连乘就会放大(实测溢出到 10^7 量级)。
为什么是这三件? 因为换采样器改变的是”怎么走”, 但前向过程、训练目标、模型输出都不变。 所以所有可能出问题的地方,都在这三者的接口上: 输出格式、时间网格、以及新采样器自己的隐含假设。
11. “逆过程”暗示了一个一一对应的映射,而反向过程是一个分布。
前向 x_{t-1}\to x_t 是”确定性缩放 + 加噪声”。 给定 x_t,x_{t-1} 有无穷多种可能—— q(x_{t-1}\mid x_t) 是一个分布,不是一个点。
要”逆”得唯一,必须额外知道 x_0(那就是后验 q(x_{t-1}\mid x_t,x_0))。 而采样时 x_0 是未知的——这正是模型要猜的东西(式 6)。
所以正确的说法是:反向过程学的是后验分布的参数, 而不是”前向过程的逆函数”。
这个区别的实际后果:如果反向过程真是逆函数,采样就该是”算出来”的; 而它其实是”从分布里抽出来”的—— 这就是案例里那个”丢掉 \sigma_t z“的 bug 的全部背景。
12. 按定义:
\text{噪声方差占比} = 1-\bar\alpha_{100} = 1-0.8970 = \mathbf{0.1030}\ (10.3\%)
\text{信号方差占比} = \bar\alpha_{100} = 0.8970\ (89.7\%)
但问的是”系数”的比,所以要开方:
\frac{\sqrt{\bar\alpha_{100}}}{\sqrt{1-\bar\alpha_{100}}} = \frac{0.9471}{0.3210} = \mathbf{2.95}
信号系数是噪声系数的 2.95 倍。
这道题的重点在最后一步: “方差占比 89.7%”和”系数是 2.95 倍”是同一个事实的两种说法, 但它们给人的直觉完全不同。
看到”89.7% 是信号”,容易觉得”还很干净”; 看到”系数只差 2.95 倍”,就会意识到噪声的幅度已经不小了。
而模型面对的是幅度,不是方差。
一个推论:\bar\alpha_t = 0.5 时两个系数相等(都是 0.707), 而那时方差占比还有 50%。 所以”信号占一半”对应的是”幅度上完全不占优势”—— 这也是为什么 §5.8 用 SNR(方差比)描述噪声水平, 但真正决定模型难度的是它的平方根。
5.26 ELBO 到底有多紧
§5.6 推 ELBO 的时候用的是 Jensen 不等式,那是一个不等号。 这一节说清楚这个不等号有多松。
5.26.1 下界和真值差在哪
\log p_\theta(x_0) - \text{ELBO} = D_{\mathrm{KL}}\left(q(x_{1:T}\mid x_0)\ \|\ p_\theta(x_{1:T}\mid x_0)\right)
这不是本章的结论,是变分推断的标准结果: Jensen 不等式的取等条件,就是”两个分布完全一样”。
所以”ELBO 有多紧”= “前向过程 q 和后验 p_\theta 有多像”。
两个极端:
- 模型完美:p_\theta(x_{1:T}\mid x_0) = q(x_{1:T}\mid x_0),下界取等
- 模型完全没学:p_\theta 和 q 差得远,下界很松
注意 q(x_{1:T}\mid x_0) 里含 x_0—— 也就是说,“下界有多紧”本身依赖于具体的那个数据点。 对不同样本,紧的程度不一样。
5.26.2 实测:本章的模型有多紧
§5.6 实测的三项给出上界 13.2567(L_T + \sum L_{t-1} - L_0)。
但我们没有算 -\log p_\theta(x_0) 本身,所以不知道下界离真值多远。
为什么不算? 因为 p_\theta(x_0) = \int p_\theta(x_{0:T})dx_{1:T} 积不出来——这正是 §5.6 开头那个问题。
所以”下界有多紧”这件事,本章无法直接测量。
本章测了上界,没测真值,所以不知道紧不紧。
能不能间接估计? 有几个办法,本章都没做:
① 用 importance sampling 估 p_\theta(x_0)。 从 q(x_{1:T}|x_0) 采样若干条路径,算 \frac{1}{K}\sum_k \frac{p_\theta(x_{0:T}^{(k)})}{q(x_{1:T}^{(k)}|x_0)}。 当 K 很大时它收敛到真值——但方差可能极大, 这是扩散模型做似然评估的经典困难。
② 换一个指标。 很多论文报的是”上界的值”(叫 NLL bound), 并明确说明它不是似然。本章的 13.2567 属于这一类。
④ 在小规模上暴力算。 本章 d=2、T=1000, 积分是 2000 维——数值积分依然不可行, 但用 ① 的 importance sampling 在小维度上方差会小得多。
这个实验是可行的,本章没做。 它属于 §5.22 那三组”可以做但没做”之外的一组, 而且它比其他三组更基础——它决定本章那个 13.2567 该怎么解读。
5.26.3 一个相关的实践问题
实践中几乎没有人用 ELBO 来评估扩散模型。
为什么? 两个原因:
① 似然和感知质量不相关。 在图像上,FID(一个基于特征距离的指标)和 NLL 经常给出相反的排序。 一个模型可以有更好的似然但更差的视觉效果—— 因为似然对”高维空间里的小误差”非常敏感, 而人眼对低频结构更敏感。
② ELBO 很难算准。 上面那个 importance sampling 的方差问题, 让”报一个可信的 NLL”变成一件需要专门技术的工作。
扩散模型领域因此形成了一个惯例: - 训练用 \mathcal{L}_{\text{simple}}(最简单的那个) - 评估用 FID / IS 这类采样质量指标 - 似然只在专门的论文里报
本章之所以还能用 ELBO 讲清楚训练目标,是因为它在这里是”来路”而不是”指标”—— 它解释了 \mathcal{L}_{\text{simple}} 是从哪来的(§5.6), 以及为什么它和真实似然有差距。
5.27 把 ELBO 的三项真的算一遍
§5.6 给了三项的实测值(L_T = 0、\sum L_{t-1} = 8.1222、L_0 = -5.1345)。 这一节把其中一项完整地手算出来——不为别的, 只为了让你在读到”L_{t-1} 是 KL 散度”这句话时,脑子里有一个具体的数。
5.27.1 选一个最容易手算的时刻
t=2。此时涉及的量:
| 量 | 值 | 来源 |
|---|---|---|
| \bar\alpha_1 | 0.999900 | \bar\alpha_1 = 1-\beta_1 |
| \bar\alpha_2 | 0.999780092 | \bar\alpha_1(1-\beta_2) |
| \beta_2 | 0.000119920 | 10^{-4} + (0.02-10^{-4})/999 |
| \tilde\beta_2 | 5.453188\times10^{-5} | 式 式 5.4 |
| \sigma_2^2 | 5.453188\times10^{-5} | 取 \tilde\beta_2 |
先设一个具体的数据点:x_0 = (0.5,\ -0.3)(在双月的一个弯月上)。
抽到的噪声:\varepsilon = (1.2,\ -0.8)。
5.27.2 第一步:造出 x_2
x_2 = \sqrt{0.999780}\times(0.5,\ -0.3) + \sqrt{0.000220}\times(1.2,\ -0.8)
\sqrt{0.999780092} = 0.99989004,\sqrt{0.000219908} = 0.01482929。
x_2 = (0.49994502,\ -0.29996701) + (0.01779515,\ -0.01186343) = (0.51774017,\ -0.31183044)
注意 x_2 和 x_0 差多少:(0.01774,\ -0.01183),各个分量的差都在 0.02 以内。
这是因为 t=2 处 \bar\alpha_2 = 0.99978——信号几乎没被破坏。
5.27.3 第二步:算真实后验的均值和方差
方差(式 4):
\tilde\beta_2 = \frac{1-\bar\alpha_1}{1-\bar\alpha_2}\beta_2 = \frac{9.999999\times10^{-5}}{2.199079\times10^{-4}}\times 1.199199\times10^{-4} = 5.453188\times10^{-5}
均值(式 5):
\tilde\mu_2 = \underbrace{\frac{\sqrt{\alpha_2}(1-\bar\alpha_1)}{1-\bar\alpha_2}}_{c_1}x_2 + \underbrace{\frac{\sqrt{\bar\alpha_1}\beta_2}{1-\bar\alpha_2}}_{c_2}x_0
两个系数:
c_1 = \frac{0.99989004\times 9.999999\times10^{-5}}{2.199079\times10^{-4}} = 0.45470850,\qquad c_2 = \frac{0.99995000\times 1.199199\times10^{-4}}{2.199079\times10^{-4}} = 0.54529150
两个系数之和是 0.9999999985——在 t=2 处它就是 1。
注意:这个”和为 1”只在 t 小的时候成立。 实测:
| t | c_1+c_2 |
|---|---|
| 2 | 1.000000 |
| 10 | 1.000000 |
| 100 | 0.999972 |
| 500 | 0.997177 |
| 1000 | 0.990077 |
t 越大,和偏离 1 越多。 这不是数值误差(t=1000 处差了 1%), 而是后验均值在向原点收缩: t 大时先验 q(x_{t-1}\mid x_0) 的均值是 \sqrt{\bar\alpha_{t-1}}x_0, 而 \sqrt{\bar\alpha_{t-1}} 本身在趋近 0(t=1000 时是 0.0064)。
换句话说:t 大时后验均值不是x_t 和 x_0 的凸组合, 而是被拉向原点的一个加权和——因为那个时刻的”先验”本身就在原点附近。
t 小时 c_2(x_0 的权重)更大,后验更相信 x_0; t 大时反过来。 这个方向是符合直觉的。
c_1 x_2 = (0.235421,\ -0.141792),\qquad c_2 x_0 = (0.272646,\ -0.163587)
\tilde\mu_2 = (0.508067,\ -0.305379)
它落在 x_0=(0.5,-0.3) 和 x_2=(0.5177,-0.3118) 之间,距离 x_0 更近 (因为 c_2 > c_1)。这是对的:t 小时,后验应该更相信 x_0 一侧的先验。
5.27.4 第三步:算模型的输出
假设模型是完美的,即 \varepsilon_\theta(x_2, 2) = \varepsilon = (1.2,\ -0.8)。
那么按式 式 5.7:
\mu_\theta = \frac{1}{\sqrt{0.999940}}\left[x_2 - \frac{0.000120}{\sqrt{0.000220}}(1.2,\ -0.8)\right]
\sqrt{0.99988008} = 0.99994004,\dfrac{0.000119920}{0.01482929} = 0.00808668。
\begin{aligned} \mu_\theta &= \frac{1}{0.99994004}\left[(0.51774017,\ -0.31183044) - (0.00970402,\ -0.00646935)\right] \\ &= \frac{(0.50803615,\ -0.30536109)}{0.99994004} = (0.50806662,\ -0.30537940) \end{aligned}
和 \tilde\mu_2 = (0.50806659,\ -0.30537939) 一致到小数点后第七位(\Delta = (0,\ 3\times10^{-8}))。
这是应该的——模型完美时 \mu_\theta = \tilde\mu_t, 而这个等式正是 §5.5 那个推导的终点。
5.27.5 第四步:算 L_1(也就是 L_{t-1} 在 t=2 处的值)
代入 (12),\sigma_2^2 = \tilde\beta_2:
L_1 = \frac{1}{2}\left[\frac{2\tilde\beta_2}{\tilde\beta_2} + \frac{\|\tilde\mu_2-\mu_\theta\|^2}{\tilde\beta_2} - 2 + 2\log\frac{\tilde\beta_2}{\tilde\beta_2}\right] = \frac{1}{2}\left[2 + \frac{\|\Delta\|^2}{\tilde\beta_2} - 2 + 0\right] = \frac{\|\Delta\|^2}{2\tilde\beta_2}
前三项正好抵消(这是取 \sigma_t^2=\tilde\beta_t 的直接好处)。
\|\Delta\|^2 = 8.9\times10^{-16}
L_1 = \frac{8.9\times10^{-16}}{2\times 5.453188\times10^{-5}} = 8.1\times10^{-12}
一个完美模型的 L_1 是 8.1\times10^{-12}——在双精度下就是 0。
(这个残差本身就来自浮点舍入,不是算法的误差。)
而实测的 L_1 是 1.2143。
手算用的是”完美模型”,实测用的是”训了 5000 步的小模型”。
两者差的不是一点点:
| \|\Delta\|^2 | L_1 | |
|---|---|---|
| 完美模型 | 8.9\times10^{-16} | 8.1\times10^{-12} |
| 实测模型 | \approx 1.3\times10^{-4} | 1.2143 |
\|\Delta\|^2 差了 11 个数量级。
为什么一个 5 万参数的模型在 t=2 处差这么多?
因为 \tilde\beta_2 = 5.45\times10^{-5} 在分母上。 要把 L_1 压到 1 以下,模型必须让 \|\Delta\|^2 < 1.1\times10^{-4}, 也就是 \Delta 的每个分量小于 0.007。
而 x_2 和 x_0 的距离本身只有 0.02—— 模型要让后验均值的误差小于信号本身的 35%,才算”及格”。
这不是模型不好,是这个时刻的惩罚确实很严。
它同时解释了 §5.6 那张分箱表: t\in[2,11] 这 10 步贡献了 49% 的 ELBO, 不是因为那几步”更重要”,而是因为那里的分母 \tilde\beta_t 最小, 任何误差都被放大得最厉害。
5.27.6 第五步:换个 t 再算一遍,看差距
把同样的手算在 t=500 上做一遍(数据点和噪声换一组):
| 量 | t=2 | t=500 |
|---|---|---|
| \bar\alpha_t | 0.999780 | 0.078587 |
| \sqrt{\bar\alpha_t} | 0.999890 | 0.280333 |
| \sqrt{1-\bar\alpha_t} | 0.014832 | 0.959903 |
| \tilde\beta_t | 5.45\times10^{-5} | 1.0031\times10^{-2} |
| \tilde\beta_t 的比值 | — | 大了 184 倍 |
\tilde\beta_{500} 是 \tilde\beta_2 的 184 倍。
这意味着同样大小的 \|\Delta\|^2,在 t=500 处产生的 L 只有 t=2 处的 1/184。
这就是 §5.6 那张表(小 t 贡献 49%)的全部算术根源。
所以这一节在说什么
ELBO 的”重要性分布”不是一个设计选择,它是一个除法。
每个时间步的惩罚是 \dfrac{\|\Delta\|^2}{2\tilde\beta_t}, 而 \tilde\beta_t 在 t=2 处是 5.5\times10^{-5}、在 t=500 处是 10^{-2}。
分母差 184 倍,惩罚就差 184 倍。
这个除法有没有道理? 有——\tilde\beta_t 就是”这一步还剩多少不确定性”。 不确定性小的时候犯错,确实更应该罚。
但这也意味着:训练一开始,梯度几乎全部来自那十几个小 t 的时刻, 而它们对应的 \tilde\beta_t 小到让梯度极不稳定。
DDPM 把权重扔掉,本质上就是把这把”放大镜”撤掉。 撤掉之后发生了什么,是第 6 章的第一个问题。
5.28 这一章的方法论
本章推了六节数学,但真正的收获可能不在公式里。这一节把方法论单独抽出来。
5.28.1 一、“看起来正常”是最危险的信号
本章出现了四次”loss 正常但结果不对”:
| 情况 | loss 的表现 |
|---|---|
| 案例:丢掉 \sigma_t z | 逐点完全相同 |
| 失败二:漏掉一段 t | 反而下降到一半 |
| 失败五:x_0-预测 | 不可比,但曲线形状完全正常 |
| 失败三:fp16 抵消 | 训练根本不受影响(前向过程的问题) |
每一次的 loss 曲线都是”健康”的——单调下降、无震荡、无 nan。
教训:监控指标只有在它能反映你要的东西时才有用。 loss 反映的是”\varepsilon 预测得准不准”, 它和”采样对不对”之间没有必然联系。
做法:给每一个你关心的性质单独找一个可测量的代理。 采样结果对不对 → 就测采样的统计量(均值/方差/rms/MMD), 不要指望训练曲线替你说。
5.28.2 二、“改量”和”改有无”要分开
§5.2 的三条弯路全部是”改量”: 模型更大、训练更久、\sigma 换一个来源。
而元凶是”有无”:那一项根本不存在。
这两类错误的排查方式完全不同:
- 改量:在你的结果附近滑动,没有跳变
- 改有无:可以让结果跳到另一个量级
排查顺序应该是先查”有无”: 把公式和代码逐项对账(数项数), 而不是逐个检查每个量的数值合不合理。
本章的排查之所以找到,就是因为最后换成了对账。
5.28.3 三、平均值掩盖异常
这一条在本章出现了两次:
- 按 t 分段的 loss(§5.15 第一步): 总 loss 正常,但分数的形状能看出问题
- ELBO 的逐时间步分解(§5.6): 平均值 8.12 掩盖了”1% 的步贡献 49%”
这和第四章”逐位置看 loss”是同一个动作。
通用的做法:任何你在平均值上做的判断, 都值得按某个维度分组再看一次。 分组维度选”最可能不均”的那个—— 第 4 章是位置,本章是时间步。
5.28.4 四、对照要”同结构”
§5.2 的弯路一(加宽模型)就是一个失败的对照: 我们改了模型容量,但同时改了参数量, 于是分不清”是容量起作用”还是”是参数起作用”。
正确的对照要求只动一个变量,而在神经网络上这几乎不可能 (改宽度就改参数量,改深度就改感受野)。
实际的做法是承认这一点,并把混淆写出来。 本章 §5.9 那个 T 的对照就是这么处理的: 明确写了”训练预算固定,对大 T 不利”。
5.28.5 五、写清楚”哪一条结论最弱”
本章末尾那张分界表(§5.19)做了这件事: 把结论分成”从定义推出来的”(可迁移)和”实测出来的”(依赖规模)。
这一步看起来是自我削弱,实际不是。
- 对读者:他知道了该信到什么程度,不会拿一个 2D 玩具的结论去指导大模型训练
- 对自己:写这张表的过程会逼你发现哪些结论其实没有支撑
本章真的有两条被这张表逼出来的:
- T=25 比 T=1000 好——写进分界表时才发现, 这个结论几乎完全由”训练预算固定”这个设定决定
- \beta_{\max}=0.002 测不出危害——写进分界表时才发现, 我们其实没有测出这个缺陷的任何实际后果
这两条如果混在正文里当结论卖,都是不诚实的。
5.28.6 一张可以带走的清单
| # | 规矩 | 本章的正例 | 本章的反例 |
|---|---|---|---|
| 1 | 每个指标只对它测的东西负责 | §5.15 的六项自检 | 用 loss 判断采样质量 |
| 2 | 先查”有无”再查”改量” | §5.15 第四步的逐项对账 | §5.2 的三条弯路 |
| 3 | 平均值必须分组复看 | §5.6 的逐时间步分解 | 只看总 loss |
| 4 | 对照要承认混淆 | §5.9 写明训练预算不利 | §5.2 弯路一的加宽 |
| 5 | 把最弱的结论写出来 | §5.19 的分界表 | 把玩具结论当通用规律 |
这五条和前三章的方法是同一套—— 第 1 章的”唯一变量”、第 2 章的”公平对照”、第 3 章的”比 logits 不比文本”、 第 4 章的”逐位置分组”。 换了一个领域,方法一条没变。
5.29 关于记号:三个长得像的 \alpha
扩散模型的记号是初学者最容易卡住的地方。 \alpha_t、\bar\alpha_t、\sqrt{\bar\alpha_t}、 还有 \tilde\alpha_t(有些文献用)——四个符号,长得都差不多。
5.29.1 一张速查表
| 符号 | 读作 | 定义 | 本章 t=1 | 本章 t=1000 |
|---|---|---|---|---|
| \beta_t | beta | 第 t 步的噪声方差 | 0.0001 | 0.02 |
| \alpha_t | alpha | 1-\beta_t | 0.9999 | 0.98 |
| \bar\alpha_t | alpha bar | \prod_{s\le t}\alpha_s | 0.9999 | 4.04\times10^{-5} |
| \sqrt{\bar\alpha_t} | — | 信号系数 | 0.99995 | 0.00636 |
| \sqrt{1-\bar\alpha_t} | — | 噪声系数 | 0.0100 | 1.0000 |
看最后两列:\alpha_t 从 0.9999 到 0.98(变化 2%), 而 \bar\alpha_t 从 0.9999 掉到 4\times10^{-5}(变化 24750 倍)。
这两个量在 t=1 处几乎相等,在 t=1000 处差了 24000 倍。
所以”在 t=1 附近把两者写混了看不出来”—— 这正是 §5.13 失败三(fp16 抵消)发生在小 t 的另一层原因。
5.29.2 三个最常见的混淆
混淆一:\sqrt{\bar\alpha_t} 与 \bar\alpha_t。
\bar\alpha_t 是方差的保留比例,\sqrt{\bar\alpha_t} 是幅度的系数。 t=1000 时前者是 4\times10^{-5},后者是 0.0064——差 157 倍。
写 x_t = \bar\alpha_t x_0 + \cdots 是一个常见错误, 后果是 t 大时信号衰减得过快(按平方而不是按线性)。 检测方法:算 x_t 的方差(§5.15 检查 ④), 如果形状不对,就是这个错。
混淆二:\alpha_t 与 \bar\alpha_t。
反向均值 (7) 里两个都出现了:
\mu_\theta = \frac{1}{\sqrt{\alpha_t}}\left(x_t - \frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\varepsilon_\theta\right)
外面是单步的 \alpha_t,里面是累积的 \bar\alpha_t。
§5.13 失败六给了后果:写反的话,t=1000 处的系数从 1.010 变成 157。
混淆三:\bar\alpha_t 与 1-\bar\alpha_t。
这两个在 t=500 附近交叉(\bar\alpha_{500} = 0.0786)。
在 t 小时 \bar\alpha_t 接近 1、1-\bar\alpha_t 接近 0; 在 t 大时反过来。而它们在公式里的位置完全不同: 一个在信号项、一个在噪声项、还有一个在分母上。
一个自查的办法:在代码里给这两个量起明确的名字。
signal_coef = alpha_bar.sqrt() # 信号系数
noise_coef = (1 - alpha_bar).sqrt() # 噪声系数
# 而不是 a = alpha_bar.sqrt(); b = (1-alpha_bar).sqrt()本章的 tiny_diffusion.py 就是这么命名的。 多打几个字符,能省掉一小时的调试。
5.29.3 一份”看到某个符号时该想到什么”的对照
| 看到 | 想到 |
|---|---|
| \beta_t | 这一步注入多少噪声;能不能在 fp16 下算 |
| \alpha_t | 单步保留率;反向均值外面的那个因子 |
| \bar\alpha_t | 累积保留率;和 t 一起单调下降;方差 |
| \sqrt{\bar\alpha_t} | 信号幅度的系数;t 大时接近 0 |
| \sqrt{1-\bar\alpha_t} | 噪声幅度的系数;t 大时接近 1 |
| \tilde\beta_t | 后验方差;t=1 处精确为 0 |
| \sigma_t | 反向过程每步注入的噪声;取 \sqrt{\tilde\beta_t} 或 \sqrt{\beta_t} |
| \lambda_t | ELBO 的权重;t=1 处发散 |
这张表建议抄在笔记本第一页。 本章后面所有的推导, 出错的地方九成在这八个量里的某两个之间。
5.30 关键概念表
| 符号 | 名称 | 定义 | 首次出现 |
|---|---|---|---|
| \beta_t | 前向方差 | 第 t 步注入的噪声量 | 式 式 5.1 |
| \alpha_t | 单步保留率 | 1-\beta_t | 式 式 5.1 |
| \bar\alpha_t | 累积保留率 | \prod_{s\le t}\alpha_s | 式 式 5.3 |
| \mathrm{SNR}_t | 信噪比 | \bar\alpha_t/(1-\bar\alpha_t) | §5.4 |
| q(x_t\mid x_0) | 前向闭式解 | \mathcal{N}(\sqrt{\bar\alpha_t}x_0,(1-\bar\alpha_t)I) | 式 式 5.2 |
| q(x_{t-1}\mid x_t,x_0) | 后验 | \mathcal{N}(\tilde\mu_t,\tilde\beta_t I) | 式 式 5.4 与 式 5.5 |
| \tilde\beta_t | 后验方差 | \frac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}\beta_t | 式 式 5.4 |
| \tilde\mu_t | 后验均值 | 式 式 5.5 | 式 式 5.5 |
| p_\theta(x_{t-1}\mid x_t) | 反向过程 | \mathcal{N}(\mu_\theta,\sigma_t^2 I) | 式 式 5.8 |
| \sigma_t | 反向噪声标准差 | \sqrt{\tilde\beta_t} 或 \sqrt{\beta_t} | §5.5 |
| ELBO | 证据下界 | 式 式 5.9 | 式 式 5.9 |
| L_T,L_{t-1},L_0 | 三项分解 | 式 式 5.11 | 式 式 5.11 |
| \lambda_t | ELBO 权重 | 式 式 5.14 | 式 式 5.14 |
| \mathcal{L}_{\text{simple}} | 简化目标 | 式 式 5.15 | 式 式 5.15 |
| v | v-预测目标 | 式 式 5.16 | 式 式 5.16 |
符号约定提醒:本章的 \bar\alpha_t 是累积量, \alpha_t 是单步量。两者在 t=1000 处相差 10^4 倍(0.98 vs 4\times10^{-5})。 §5.13 失败六专门讲了这个混淆的后果。
5.31 延伸阅读
5.31.1 ⑦ 关于”扩散模型为什么有效”的另一种解释
本章从”去噪”的角度讲扩散:模型学的是 \varepsilon_\theta, 反向过程是一步步去噪。
还有一条完全不同的路线:从”得分匹配”(score matching)出发。
得分(score) 是 \nabla_x\log p(x)——对数密度的梯度。它和 \varepsilon_\theta 只差一个因子(式 24)。
这条路线的好处是它把”生成”还原成了”爬山”: 如果知道每个位置上的 score,就可以用 Langevin 动力学 从任意起点走到高密度区:
x \leftarrow x + \frac{\eta}{2}\nabla_x\log p(x) + \sqrt{\eta}\,z
这是一个”带噪声的梯度上升”。
扩散模型可以看成”退火版的 Langevin”: 从高噪声(相当于高温,可以跨过势垒)逐步降到低噪声(低温,稳定在峰上)。 \bar\alpha_t 就是那个”温度调度”。
读的时候注意什么:
- 这条路线解释了本章 §5.7 那个观察(\varepsilon 损失比 x_0 损失的动态范围小 8500 倍): score 在不同噪声水平上的尺度本来就是稳定的—— 因为它衡量的是”密度在局部怎么变化”,而不是”数据点在哪里”。
- 它也是 §5.11 那个连续时间视角的自然延伸: 连续极限下,反向 SDE (23) 就是”带 score 修正的退火过程”。
- 但它有一个陷阱:score 路线早期的论文 (Song & Ermon 2019 那一系列)用的是”多个噪声水平的去噪自编码器”, 而不是单个条件模型。和 DDPM 的形式差别很大, 对着读容易迷路。 第 6 章会把它讲清楚。
5.31.2 ③ 一份排查清单(工程向)
本章 §5.15 给的是”数学对不对”的自检。 如果要做工程实现,还需要一份”跑起来对不对”的清单。
推荐从这三个项目的文档里对照着看:
denoising-diffusion-pytorch(lucidrains): 最简的 DDPM 实现,约 500 行。适合对照本章的 5 个公式逐行核。diffusers(HuggingFace):工业级实现, 调度器被抽象成可插拔的模块——这正好对应本章 §5.8 那张调度谱系。k-diffusion(Katherine Crowson):采样器最全的一个, DPM-Solver、Heun、各种二阶方法都在里面。第 6 章会用到它。
读这些实现的建议:
- 先找调度器(
scheduler/sigmas),对照本章的 (2)(3)(4) 三式。 如果它的 \bar\alpha 定义和本章一致,后面的就好读了。 - 再找训练循环里那三行(抽 t、造 x_t、回归)。 这一步在任何一个实现里都是 3–5 行。
- 最后找采样循环。重点看它对 t=0 那一支怎么处理—— 不同的实现有不同的约定(有的最后一律不加噪声, 有的按 t>0 判断,有的把 \sigma_T 强制设 0)。 这是不同实现之间结果不一致的最常见来源。
一条经验:两个实现给出的采样结果不同, 先查它们的调度数组是否逐元素相同—— 十有八九问题在这里,而不是在模型或采样器上。
5.31.3 ① 扩散模型的原始论文(2020)
Ho, Jain, Abbeel, Denoising Diffusion Probabilistic Models。arXiv:2006.11239。
读它能得到什么:本章 §5.4–§5.6 的全部内容就是这篇论文的前三节。 论文的推导比本章紧凑得多——它把后验的配方过程压缩成了两行 (本章花了整整一节展开,因为那两行里省掉的代数正是读者最容易卡住的地方)。
读的时候注意什么:
- 论文的 L_0 定义和本章差一个负号(§5.6 的符号约定)。 对不上号时先查这一条。
- 它报告”去掉权重反而更好”(L_{\text{simple}}), 但没有给出解释。第 6 章会处理这件事。
- 它的 \Sigma_\theta 有两种选择(\beta_t 和 \tilde\beta_t), 论文说两者”结果相近”——本章 §5.5 把这个说法量化了(实测 MMD 差 1%)。
5.31.4 ② 余弦调度(2021)
Nichol & Dhariwal, Improved Denoising Diffusion Probabilistic Models。arXiv:2102.09672。
读它能得到什么:本章 §5.8 的余弦调度就出自这里。
它做的最重要的一件事,是把”调度好不好”变成了一个可计算的指标: 论文用 log-SNR(他们叫 \lambda_t)刻画每个时间步的噪声水平, 并指出线性调度在低噪声端浪费了大量步数。
本章把这件事量化了:线性调度有 32.7% 的步落在 \bar\alpha_t<0.01, 余弦只有 6.5%(§5.8 的表)。
读的时候注意什么:
- 它也讨论了”学 \Sigma_\theta“(让模型自己预测方差)。 结论是:对似然有显著提升,对生成质量几乎没有影响。 这个区分很重要——它说明”更好的似然”和”更好看的图”不是一回事。
- 它的改进(学方差 + 余弦调度)合起来才有效果, 单独用其中一个收益有限。这类”两个改动一起用才生效”的结论, 在小规模复现时极易误判。
5.31.5 ④ 为什么 \varepsilon 和 v 比 x_0 好(2022)
Salimans & Ho, Progressive Distillation for Fast Sampling of Diffusion Models。arXiv:2202.00512。
读它能得到什么:本章 §5.7 的 v-预测(式 18)出自这篇论文的附录。
它的主结果是蒸馏(把 1000 步压到 4 步), 但附录里对三种参数化的分析是本章 §5.7 那套论证的原始出处。
读的时候注意什么:
- 本章 (17) 那个恒等式(\|\varepsilon-\varepsilon_\theta\|^2 = \mathrm{SNR}\cdot\|x_0-\hat x_0\|^2)在论文里是从”信噪比加权”的角度讲的, 本章把它改成了”两种参数化的损失关系”。两条路等价,视角不同。
- 它的蒸馏需要重新训练(学生模型要多训一轮), 和 §5.9 说的”采样器压缩”不是一回事—— 后者不改变模型,只改变推理路径。 第 6 章会讲后者。
5.31.6 ⑤ 一次完整的”公式与代码对不上”的复盘
Kingma, Salimans, Poole, Ho, Variational Diffusion Models。 arXiv:2107.00630。
读它能得到什么:这篇论文把扩散模型的整个框架写成了一以贯之的变分推断, 并且指出了 DDPM 原文里几处”做了但没明说”的近似。
读的时候注意什么:
- 它用连续时间(t\in[0,1])重写了整个框架, 于是 \bar\alpha_t、\beta_t 这些离散量变成了连续函数, 很多”为什么是这个公式”的问题在这里有了统一的答案 (比如为什么线性调度不好:它对应 log-SNR 上的非均匀采样)。
- 它引入了”信噪比必须单调递减”这个约束—— 这是本章没提的一条设计要求,它排除了很多看起来合理的调度。
- 这篇论文的记号体系和 DDPM 原文差别很大, 对着读会吃力。建议先读完本章,再来读它。
5.31.7 ⑥ 关于玩具模型的方法论
本章反复强调一件事:在 2D 上测出来的结论,未必能迁移到大模型上。
这在扩散模型研究里是一个真实存在的问题。 一个具体的例子: 很多论文在 CIFAR-10(32×32)上验证的采样器改进, 在 ImageNet 256×256 上收益会缩水甚至消失。
读任何一篇扩散模型的实证论文时,问三个问题:
- 它测的是什么分辨率、什么规模?
- 它的对照有没有把训练预算对齐?(§5.9 那个实验就栽在这里)
- 它报的是 FID 还是似然? 这两者在扩散模型上经常给出相反的排序。
本章能提供的是”骨架”和”方法”—— 哪些式子是数学、哪些结论是观察(§5.19 那张分界表)。 把这张表带在身上去读论文,能省下不少弯路。
5.32 如果重来一次
这一节按第 2 章的先例,列出”如果重新做一遍,会改哪三件事”。
它不是自我批评,是把”事后才知道的东西”写下来—— 读者做自己的项目时,可以少走这三段。
5.32.1 一、先写采样,再写训练
本章的顺序是:先训模型(§5.2),发现采样不对(§5.2), 再回头推公式(§5.4–§5.7)。
这个顺序是被案例逼出来的,但它不是最好的顺序。
更好的做法是:在训练之前,先用一个未训练的随机模型跑一遍完整的采样循环, 只看输出的统计量。
为什么这样能提前发现问题? 因为随机模型的输出虽然没意义, 但它的尺度应该是合理的—— \varepsilon_\theta 输出接近 0(随机初始化), 于是反向均值 \mu_\theta \approx x_t/\sqrt{\alpha_t}, 采样结果的 rms 应该在数据量级附近。
如果去掉 \sigma_t z,随机模型的采样结果也会塌—— 这个检查不需要训练,几十秒就能做完。
代价:需要先写完整的采样代码(包括调度数组), 但那是无论如何都要写的。
5.32.2 二、把”按 t 分段”做成默认
本章 §5.15 用”按 t 分段的 loss”排除了训练层的问题, §5.6 又用同一张表发现了 ELBO 权重的不均匀。
这个动作很简单(把 [0,T) 分成 5 段,各段单独平均), 但它是本章信息量最大的一个测量。
如果重来,我会把它写进训练循环的默认输出—— 而不是等到排查的时候才临时加。
具体做法:
# 每 100 步记一次,成本可以忽略
t_eval = torch.randint(0, T, (4096,))
x0_eval = data[torch.randint(N, (4096,))]
xt_eval, eps_eval = q_sample(x0_eval, t_eval, sch)
l = ((model(xt_eval, t_eval) - eps_eval) ** 2).mean(dim=-1)
seg = [l[(t_eval >= i*T//5) & (t_eval < (i+1)*T//5)].mean() for i in range(5)]五行代码,换来一个”能看见局部”的窗口。
5.32.3 三、把 \bar\alpha_t 的检查前置
§5.8 那个 check_schedule 函数(20 行)只用了几毫秒, 但它能提前发现四类问题:L_T 不为 0、 纯噪声步数过多、步间不均匀、跨度不合适。
本章是在研究调度对比的时候才写出它的—— 而它的第一个判据(L_T 接近 0)本来就可以在选调度之前用。
这三件事的共同点:都是”把检查提前”,而不是”更聪明地排查”。
5.32.4 补一句:本章的代码组织
本章的代码分成三层,每一层的目的不同。
| 文件 | 目的 | 特点 |
|---|---|---|
tiny_diffusion.py |
做实验 | 有教学开关(reverse_mean、posterior_var),可复现各种 bug |
diffusion_67.py |
讲清楚”最少需要什么” | 单文件 67 行,无开关 |
run_ch5_*.py |
产出正文里的数字 | 每个脚本对应一组表格 |
为什么要有 tiny_diffusion.py 里那些开关?
因为”复现一个 bug”和”避免一个 bug”是两个不同的需求。 reverse_mean="mul" 这个开关在正常使用时永远不该打开, 但它让”把 bug 加回去验证训练 loss 不变”这件事变成了改一个字符串。
这是本书前三章反复用到的一个做法: 把故意写错的版本也留在代码里,并且给它一个明确的开关。 第 1 章的 scaled=False、第 3 章的缓存变体都是同一个思路。
理由很实际:如果你为了演示一个错误而临时改代码, 改完之后很难保证改回去是对的; 而如果错误版本一直有开关、一直被测试覆盖, 它就成了一份可执行的文档。
它们不需要更多的数学,只需要在动手之前多花十分钟写几个断言。 而本章那个 bug 之所以能藏住,恰恰是因为没有任何一个断言在守着采样代码。
5.33 本章没做的事
每一章都有一批”本该做但没做”的事。把它们列出来, 比假装完整要好。
5.33.1 一、没测似然,所以不知道 ELBO 有多紧
§5.6 给了上界 13.2567,但没有算 -\log p_\theta(x_0) 本身, 因此不知道这个上界离真值多远(§「ELBO 到底有多紧」)。
这一条是本章最大的空白,因为它影响 13.2567 这个数的解读方式。
要补它需要:importance sampling 估 p_\theta(x_0), 在小维度上(本章 d=2)方差是可控的。 这是本章所有待办里最基础的一个。
5.33.2 二、没做 v-预测的对照
§5.7 讲了 v-预测,但没有在本章的模型上跑一遍。
原因:v-预测的优势在大模型、高分辨率上才明显, 在本章的 2D 玩具上很可能测不出差别—— 那样反而会给读者一个”v-预测没用”的错误印象。
代价是:§5.7 关于 v 的那一节只有公式和文献引用,没有本章的实测。 它属于 §5.19 分界表里”引用论文”的那一类,不是本章的结论。
5.33.3 三、没做”\lambda_t 重要性采样”的对照
§5.20 提到的那个张力(重要性采样 t vs 扔掉权重) 本章只指出了它,没有测量它。
这是第 6 章的核心问题之一,所以留在那里做更合适。
5.33.4 四、没测梯度方向的对齐程度
§5.6 和检查点 ⑩ 都指向同一个假说: 小 t 段的梯度方向更嘈杂,所以削掉它的权重反而更好。
这是一个直接的、可测的假说(记录梯度与最终下降方向的余弦相似度), 本章没有做。 它是第 6 章的入口。
5.33.5 五、没做真实的图像实验
本章全程 2D。§5.19 已经说明了哪些结论不能迁移, 但”不能迁移”这个判断本身也是我们从原理上推的,不是测出来的。
要验证它,需要在 CIFAR-10 上复现本章的几个关键对照 (T 的影响、调度对比、参数化对比)。 那超出了”学生自备 CPU”这个约束,所以放在附录 E 的延伸阅读里指路。
5.33.6 一张”本章结论的证据等级”表
按第 4 章的做法,把本章的结论按证据强度排一下:
| 结论 | 证据 | 强度 |
|---|---|---|
| 前向过程有闭式解 | 代数推导 + 数值验证(§5.4) | 强 |
| 后验是高斯的、方差是 \tilde\beta_t | 完整推导 + 两条独立代码路径对拍(§5.5、§5.15) | 强 |
| 反向过程必须注入噪声 | 定义 + 实测(差 84 倍) | 强 |
| 训练 loss 看不见采样 bug | 逐点相同的两条曲线 | 强 |
| \|\varepsilon-\varepsilon_\theta\|^2 = \mathrm{SNR}\cdot\|x_0-\hat x_0\|^2 | 代数 + 实测吻合到四位小数(§5.7) | 强 |
| fp16 下 1-\bar\alpha_t 被毁 | 实测(前 10 步误差 31%) | 强 |
| t\in[2,11] 贡献 49% 的 ELBO | 实测(单次训练、64 个样本) | 中(样本量小) |
| T=25 比 T=1000 好 | 实测,但训练预算未对齐 | 弱 |
| \beta_{\max} 太小有危害 | 指标报警,实测没测出危害 | 无(未证实) |
| v-预测更好 | 引用文献,非本章实测 | 引用 |
这张表里有两行值得特别注意:
- “T=25 比 T=1000 好”标成了弱—— 因为 §5.9 已经说明了训练预算的问题
- “\beta_{\max} 太小有危害”标成了”无”—— 这不是说它无害,是说我们没测出来
把这两行如实标出来,比把它们混在正文里当结论卖要好。
5.34 本章回顾
把这一章压缩成六句话。
一、前向过程的意义是”白送标签”。 x_t = \sqrt{\bar\alpha_t}x_0 + \sqrt{1-\bar\alpha_t}\varepsilon 这一步是算出来的,不是收集来的——任意 x_0、任意 t、任意 \varepsilon 都给出一条合法训练样本。这是扩散模型能稳定训练的根本原因: 没有对抗、不需要判别器、loss 不会震荡。
二、后验是高斯的,方差 \tilde\beta_t 有一个精确的解析式。 这个式的两端分别是 0(t=1)和 \beta_t(t 大), 中间平滑过渡——它是”信息还剩多少”的量化。
三、反向过程定义的是一个分布,不是一个点。 \sigma_t z 那一项是整个过程的推进机制。 把它拿掉,1000 步的确定性映射会把先验压成一个点(实测半径 1.26 → 0.04)。
四、训练和采样是两套公式,它们之间没有自动的一致性检查。 这是本章最重要的一条。 案例里那个 bug 让采样塌了 130 倍,而训练 loss 逐点差 0.00e+00。 同类现象在本章出现了三次:案例、失败二(漏掉一段 t)、 失败五(x_0-预测)——三次都是”loss 变好但结果变坏”。
五、ELBO 的权重在各时间步上极不均匀,而 DDPM 把它整个扔掉了。 实测 t\in[2,11] 这 10 步(占 1%)贡献了 \sum L_{t-1} 的 49%。 扔掉权重意味着小 t 的学习被大幅削弱—— 它为什么反而更好,是第 6 章的问题。
六、尺度决定一切,而本章所有”更好/更差”的结论都是小尺度的。 §5.19 那张分界表把本章的结论分成了两类: 从定义推出来的一类(可迁移), 和实测出来的一类(只在 2D、5 万参数、5000 训练步这个设定下成立)。
训练目标和采样过程是两套公式,它们之间没有任何自动的一致性检查。
这一章所有的具体内容——前向闭式解、后验、ELBO、参数化、调度—— 都是围绕着这一句话展开的:
- 前向过程给了训练数据(白送的标签)
- ELBO 给了训练目标(一个回归损失)
- 后验给了反向过程的形式(一个分布,不是一个点)
- 调度决定了每一步的工作量(SNR 的形状)
而这四件事全都在训练循环之外。 训练循环只做一件事:给定 x_t,把 \varepsilon 猜准。
所以模型训得好不好,和采样对不对,是两个独立的判断。 本章用四次实测证明了这一点(案例、漏掉一段 t、x_0-预测、fp16 抵消)。
下篇后面三章会在这个骨架上加东西—— 更好的采样器(第 6 章)、条件控制(第 7 章)、文本注入(第 8 章)。 但”训练和采样是两套公式”这条不会变, 而且随着模型变复杂,两者之间的缝隙只会更多,不会更少。
最后留一个问题
本章测出:t\in[2,11] 这 10 个时刻贡献了 \sum L_{t-1} 的 49%。
而 DDPM 的 \mathcal{L}_{\text{simple}} 给每个 t 的权重都一样, 也就是说,它把这 49% 削到了 1%。
削掉 98% 的”最重要的部分”,模型反而学得更好——这说不通。
除非……那 49% 的”重要”是有水分的。
一个可能的解释:\lambda_t 在小 t 处巨大(\lambda_2 = 0.5997, 是 t=500 处的 109 倍),而这个巨大的权重来自 \tilde\beta_t 在分母上。 t=2 时 \tilde\beta_2 = 0.000055——除以一个五万分之一的数, 得到的权重当然大。
但”权重在数学上大”不等于”梯度有用”。 如果在小 t 处梯度的方向本身就很嘈杂 (因为模型要预测的是一个几乎看不见的小量), 那么放大它的权重只会放大噪声。
这个解释听起来合理,而且它是可测的: 在训练过程中,分别记录小 t 段和大 t 段梯度与最终下降方向的对齐程度。 如果小 t 的梯度方向确实更嘈杂,这个假说就成立。
本章没有做这个实验。 它是第 6 章的第一个入口。
本章的那个 bug——“反向过程只取均值”—— 在扩散模型的文献里有一个”正经”的名字:确定性采样。
DDIM(第 6 章)就是确定性的(\eta=0), 而且它是一个被广泛使用、效果很好的方法。
那么本章案例里的那个写法,和 DDIM 到底差在哪?
(答案:差在均值的公式上,而且差得很远。
案例的写法(错误):
x_{t-1} = \underbrace{\frac{1}{\sqrt{\alpha_t}}\left(x_t - \frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\varepsilon_\theta\right)}_{\text{DDPM 的后验均值}}
DDIM(\eta=0):
x_{t-1} = \sqrt{\bar\alpha_{t-1}}\,\hat{x}_0 + \sqrt{1-\bar\alpha_{t-1}}\,\varepsilon_\theta, \qquad \hat{x}_0 = \frac{x_t-\sqrt{1-\bar\alpha_t}\varepsilon_\theta}{\sqrt{\bar\alpha_t}}
把 DDIM 的式子展开,用到 \bar\alpha_{t-1}/\bar\alpha_t = 1/\alpha_t:
x_{t-1} = \frac{1}{\sqrt{\alpha_t}}\left(x_t - \sqrt{1-\bar\alpha_t}\,\varepsilon_\theta\right) + \sqrt{1-\bar\alpha_{t-1}}\,\varepsilon_\theta
对比两个式子:\varepsilon_\theta 前面的系数不一样 (DDPM 是 \beta_t/\sqrt{1-\bar\alpha_t},DDIM 是 \sqrt{1-\bar\alpha_t}), 而且 DDIM 多出最后那一整项。
数值上差多少? t=500 处:\beta/\sqrt{1-\bar\alpha} = 0.0105, 而 \sqrt{1-\bar\alpha} = 0.960——差 92 倍。
所以”把 DDPM 的噪声关掉”和 DDIM 是两回事—— 前者是把一个正确的公式砍掉一项, 后者是用一个完全不同的公式。
这也解释了为什么案例里的写法会塌而 DDIM 不会: DDIM 的那个额外项 +\sqrt{1-\bar\alpha_{t-1}}\varepsilon_\theta 恰好补偿了”少掉的随机性”在期望意义上的作用。
§5.9 的失败模式里提到过:本章的 DDIM 实现在这个玩具模型上不稳定 (确定性采样会累积模型偏差),所以本章没有把 DDIM 的实测数字放进来。 它的完整处理留给第 6 章。)
在写 §5.8 的调度对比时,我们还试过一件事。它失败了,而且失败的方式值得记下来。
想法:既然线性调度有 32.7% 的步落在 \bar\alpha_t<0.01 的”纯噪声区”, 那就把那些步”压缩”掉——保持 T=1000 和端点 \bar\alpha_{1000} 不变, 把 \beta 整体放大一个系数 k,让前向过程更快地穿过低 \beta 区。
做法:用二分法找 k,使 \bar\alpha_{1000} 等于线性调度的 4.0358\times10^{-5}。
结果是:二分收敛到 k\approx1.0。
也就是说,“压缩后的调度”就是线性调度本身—— 因为”\bar\alpha_{1000} 等于线性调度的值”这个约束, 在线性调度那里本来就满足(那个目标值就是从它身上取的)。
实测两者的采样结果:
| 调度 | 训练 loss | rms | MMD |
|---|---|---|---|
| 线性 | 0.1355 | 0.6090 | 0.01146 |
| “压缩”(实际 k\approx1) | 0.1215 | 0.6477 | 0.01136 |
MMD 差 0.9%——两者在噪声量级内相同,符合”它们本来就是同一个调度”。
“删掉浪费的步”这个想法是对的,但它没有”调整 \beta 的尺度”这么简单的实现方式。
原因是:\bar\alpha_t 的形状由 \beta 的相对分布决定, 而不是由它的绝对大小决定。 把 \beta 整体乘一个常数,只是把整条 \bar\alpha 曲线在 t 轴上重新标定, “多少比例的步落在 \bar\alpha<0.01”这个比例几乎不变。
要真的改变这个比例,必须改变 \beta 的分布形状—— 这正是余弦调度做的事(它直接定义 \bar\alpha_t 而不是 \beta_t,§5.8)。
所以正确的对照不是”线性 vs 压缩后的线性”, 而是”线性 vs 余弦”——那正是 §5.8 做的那张表。
这个失败被留在正文里,因为它是一个很自然的想法, 而且它的实现方式看起来完全合理(调一个系数而已)。 读者如果自己试,很可能撞上同一件事。
最后补一句:这个失败也说明了一件事——本章对调度的那套分析(log-SNR 的形状、纯噪声步数的占比)本身是对的,但它们描述的是形状,不是某个可以拧的旋钮。 要改形状,就得换一条曲线,而不是给同一条曲线乘一个数。
这一条对后面几章也有用:第 6 章讲采样器时会反复出现「能不能少走几步」这个问题,而答案从来不是「把步长调大一点」——它要么换离散化方案(高阶求解器),要么换路径(流匹配),总之都是换曲线,不是拧旋钮。
把这一句和本章开头那句合起来看:训练看不见采样的 bug,是因为两边的公式没有交集;而”少走几步”做不到,是因为形状不是靠调系数能改的。两句话说的是同一件事——你得回到公式本身,而不是在参数上打转。**
下一章换个角度:本章从头到尾都在讲”加噪和去噪的数学”, 但一次采样到底要走多少步、每一步能不能走得更大胆,本章没有回答。 第 6 章处理这件事—— DDPM 的 1000 步能不能压到 50 步,代价是什么,以及那个”扔掉权重反而更好”的谜。