5  从加噪到去噪:扩散的数学骨架

本章地图

你在这里:换了一条流水线 —— 下篇的扩散模型,从噪声到数据。上篇的注意力在这里会以”交叉注意力”的形式再出现一次(第 8 章)。 本章解决:前向加噪为什么有闭式解;反向过程为什么必须是一个分布;训练目标(ELBO)是从哪一步推出来的。 前置:第 1、2 章。下篇不需要第 3、4 章,但”训练和推理是两套流程”这个视角会一直用。 读完能做:从零写一个能跑的扩散模型(67 行);并判断一个采样实现为什么会塌。

上篇四章处理的是一个已经训好的模型:它怎么算、怎么推理、里面能读出什么。 从这一章起换一个问题——这些东西是怎么被训出来的。

扩散模型的训练目标看起来和 Transformer 毫无关系:它要模型学会从噪声里恢复信号。 但下面你会看到,两者的骨架是同一件事的两面:

两边的核心困难也一样:训练目标写得出来,不等于实现是对的。 第 3 章里,“缓存写错了”在生成文本上看不出来; 这一章里,“反向过程写错了”在训练 loss 上看不出来——一位小数都不差。

全景:一个扩散模型长什么样

先说这一节为什么存在。

第 1 章在拆注意力之前,先给了一张 Transformer 的完整数据流图(图 1-1)—— 因为不看全图就直接读细节,会不知道那个零件装在哪。 下篇同样需要这样一张图。

而且扩散比 Transformer 更需要:它有一条 Transformer 没有的分叉—— 训练走的路和采样走的路不是同一条。 这个分叉不是实现细节,是理解整章的关键:后面那个”训练 loss 完全看不出采样 bug”的案例, 根就在这里。

图 5.1  一个扩散模型的完整数据流。左列是训练(前向),右列是采样(反向),两侧共用同一个去噪网络。红框是本章的范围。

读这张图的正确方式:先看它有两个列,再看箭头往哪走。

第 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 章):

  1. 文本 → 向量:用一个文本编码器(CLIP / T5)把提示词编码成一串向量(第 8 章)
  2. 注入:通过交叉注意力把文本向量喂进 \varepsilon_\theta—— 这时 Q 来自图像特征,K、V 来自文本(第 8 章)
  3. 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 而不是图像。 这是下篇四章共同的选择,理由有三条:

  1. 学生自备 CPU 机器。2D 上训一个 5 万参数的 MLP 只要几秒; CIFAR-10 上训一个能看的扩散模型要几小时到几天。
  2. 概率密度可以直接画出来。图像上你只能看”采样结果好不好看”; 2D 上你能眼睁睁看着整个分布怎么从双月变成高斯、又怎么变回来。
  3. 每一步都能手算核对。本章 §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 一位小数都没变。

先记住这个事实。这一章剩下的部分要回答两个问题:

  1. 为什么少了那一行会塌? —— 需要先把前向过程、反向过程、 后验分布的数学全部写清楚(§5.4–§5.6)。
  2. 为什么训练 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
图 5.2  正确采样与丢掉噪声的采样的均方半径轨迹

正确那条:从 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)
是否逐点相同 是

两个模型完全一样,逐位相同。

重要为什么训练看不见这个 bug

因为训练走的是前向过程,采样走的是反向过程,它们没有一行共同的代码。

训练循环里的三行:

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

图 5.3  ᾱ_t、√ᾱ_t、√(1−ᾱ_t) 随 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}

警告符号约定:本章和 DDPM 论文差一个负号

上面 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
图 5.4  ELBO 中每一项 L_(t−1) 的大小随 t 的分布

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-\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 实测:两种调度的形状

图 5.5  线性与余弦调度的 log-SNR 对比
指标 线性 余弦
\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 不能跨调度比

上表里线性的训练 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 跨度——而模型在每一步能修正的量是有限的。

检查你的调度是否合格,有两个便宜的指标:

  1. \bar\alpha_T 是否足够接近 0:决定了 L_T 是否接近 0, 也就是”采样起点和训练终点的分布是否匹配”(§5.6)。 实测线性是 4.04\times10^{-5},合格。
  2. 每步 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 倍, 而且是步数少的那个更好。

为什么? 至少有两个原因是可测的:

  1. 训练预算被摊薄了。5000 个训练步分给 1000 个噪声等级, 每个等级平均只有 5 次更新;分给 25 个等级则有 200 次。 这个实验的设定对 T=1000 不利,这是必须说明的。
  2. 步数多 = 误差累积的机会多。每一步的 \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 是已知的、固定的。

重要这一条是”扩散为什么能替代 GAN”的核心答案

它的损失函数是一个回归损失,不是博弈。

回归损失的性质:有全局最优、梯度无偏、损失单调可监控。 本章从头到尾画的所有 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,信号已被破坏殆尽)占多少步。

观察引导。

  1. 把 \beta 终点从 0.020 拖到 0.008:红色区间消失了。 这时 \bar\alpha_{1000} 还有多少?(看左侧第一张图的读数。)
  2. 切到余弦调度,比较曲线形状:它在哪一段更陡?
  3. 最难的一个问题:红色区间越大,模型的任务是越难还是越容易? (提示:回到 §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 降了一半,采样质量差了 3.5 倍

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 大了几千倍),实测说”看不出来”。

为什么? 两个原因,本章都没能完全分离:

  1. 采样起点的不匹配是一次性的扰动,而 1000 步的反向过程有足够的 自我修正能力把它吸收掉(这正是 §5.5 讲的”每步注入噪声”的另一个好处)。
  2. 这个玩具模型太宽容: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 送进去? 两个理由:

  1. 尺度。t \in [1,1000],直接送进去数值范围比 x_t(约 \pm3)大两个数量级, 第一层权重的梯度会被主导。
  2. 周期性。正弦编码让”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 x

add_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。

重要这 67 行说明了什么

它说明扩散模型的”骨架”很小。

真正让它能生成图像的不是更多的数学,而是更大的网络和更多的数据。 本章从头到尾推的那些公式(前向闭式解、后验、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 = mean

t=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。

重要这个 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 玩具的结论去指导大模型训练
  • 对自己:写这张表的过程会逼你发现哪些结论其实没有支撑

本章真的有两条被这张表逼出来的:

  1. T=25 比 T=1000 好——写进分界表时才发现, 这个结论几乎完全由”训练预算固定”这个设定决定
  2. \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 章会用到它。

读这些实现的建议:

  1. 先找调度器(scheduler / sigmas),对照本章的 (2)(3)(4) 三式。 如果它的 \bar\alpha 定义和本章一致,后面的就好读了。
  2. 再找训练循环里那三行(抽 t、造 x_t、回归)。 这一步在任何一个实现里都是 3–5 行。
  3. 最后找采样循环。重点看它对 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 上收益会缩水甚至消失。

读任何一篇扩散模型的实证论文时,问三个问题:

  1. 它测的是什么分辨率、什么规模?
  2. 它的对照有没有把训练预算对齐?(§5.9 那个实验就栽在这里)
  3. 它报的是 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 步,代价是什么,以及那个”扔掉权重反而更好”的谜。