6 训练目标与采样器
本章地图
你在这里:第 5 章留下了一个悬案 —— 那个”训练完美、采样塌成一团”的模型,后来我们想用 DDIM 把它加速,结果炸了。本章处理的就是这类问题:训练目标怎么定、采样器怎么选。 本章解决:DDIM 为什么能跳步;它为什么在有些模型上会爆;\mathcal{L}_{\text{simple}} 扔掉 ELBO 权重为什么反而更好;“二阶求解器”到底是不是真的二阶。 前置:第 5 章(前向闭式解、后验、ELBO、\varepsilon-预测)。 读完能做:判断一个采样器在你自己的模型上能不能用;把 1000 步压到 20 步而不塌;识别”看起来是二阶、其实是恒等式”的实现。
本章路线:
| 节 | 回答的问题 | 用什么回答 |
|---|---|---|
| §6.2 案例 | 为什么 DDIM 在我们模型上炸了? | 一个换完美去噪器的对照 |
| §6.3 DDIM | 它凭什么跳步? | \sigma 族的构造 + 两行证明 |
| §6.4 score | 它和 score matching 什么关系? | Tweedie 公式 + 概率流 ODE |
| §6.5 高阶 | 二阶为什么更快?步长怎么取? | Heun + \log\sigma 网格实测 |
| §6.7 \mathcal{L}_{\text{simple}} | 扔掉 ELBO 权重为什么更好? | 梯度相干性实测 |
| §6.9 \eta | 随机性到底在干什么? | \eta 扫描 + SDE 族 |
| §6.25 评估 | 怎么才不被数字骗? | 三个陷阱 + 五步协议 |
| §6.15 选型 | 我该用哪个? | 决策树 + 三个场景 |
6.1 本章的代码与数据
import torch, sys
sys.path.insert(0, "notebooks")
from tiny_diffusion import Config, make_two_moons, make_schedule, train
from run_ch6_case import ddim, gaussian_eps_star
data = make_two_moons(4000, seed=0)
cfg = Config(max_steps=5000)
model, sch, hist = train(data, cfg) # 和第 5 章完全同一个模型
# 论文里的 DDIM(η=0,确定性)
x = torch.randn(2000, 2)
x = ddim(model, sch, x, steps=100) # ← 这一行会给出 1e8 量级复现本章全部数字:
cd notebooks
PY=/home/mpp/micromamba/envs/dlspace/bin/python
$PY run_ch6_case.py # 案例主实验(约 2 分钟)本章和上一章共用同一个模型、同一套调度、同一份数据。 这不是偷懒 —— 只有模型完全相同,“换采样器”才是一个干净的对照。
本章用到的数据文件:
| 文件 | 内容 | 生成脚本 |
|---|---|---|
data/ch6_case.json |
案例主实验:学出来的模型 vs 完美去噪器、偏差扫描、ε 偏差随 t | run_ch6_case.py |
data/ch6_extra.json |
梯度相干性、σ 空间 Euler/Heun 对比 | run_ch6_extra.py |
data/ch6_eta.json |
\eta 扫描(50 步 / 100 步) | run_ch6_eta.py |
data/ch6_bias_grid.json |
偏差 × 步数曲面(5 × 6 格) | run_ch6_bias_grid.py |
data/ch6_grid.json |
\sigma 均匀 vs \log\sigma 均匀 | run_ch6_grid.py |
每一个数字都来自这些文件的某一行。 如果你在正文里看到一个数, 想找它的出处,先在这五个文件里搜 —— 搜不到就是本书的错, 请当作 bug 报告。
本章的两个交互实验:
| 实验 | 文件 | 它能回答什么 |
|---|---|---|
| 实验 6-1 | experiments/exp-6-1.html |
同 NFE 下三种采样器谁更准 |
| 实验 6-2 | experiments/exp-6-2.html |
均匀偏差为什么炸不了 |
注意实验 6-2 的结论和直觉相反(见 §6.8):它不是”看误差怎么被放大”, 而是”发现均匀误差根本不会被放大”。这个负结果是本章最重要的一块。
6.2 案例:论文里正确的 DDIM,在我们的模型上炸了
6.2.1 现象
第 5 章那个模型,采样一次要走 1000 步。太慢了。 所以我们按论文实现了 DDIM——一个能把 1000 步压到 20 步的采样器。
第一版的运行结果:
| 步数 | 输出标准差 | 最大绝对值 |
|---|---|---|
| 1000 | nan |
nan |
| 500 | nan |
nan |
| 250 | nan |
nan |
| 100 | [5.37\times10^{7},\ 1.25\times10^{8}] | 4.97\times10^{9} |
| 50 | [13.83,\ 32.10] | 1.33\times10^{3} |
| 25 | [0.5328,\ 0.3398] | 3.52 |
(数据本身的标准差是 [0.5707,\ 0.3351]。)
三件事同时发生了:
- 步数多的时候直接炸成 nan——不是”质量差”,是数值溢出。
- 步数少的时候反而正常(25 步的结果看起来还挺像)。
- 中间有一段(50 步)处于半炸状态:std 13.8,比数据大 24 倍。
第 2 条最反直觉:所有人都在说”采样步数越多越好”,这里步数越多越糟。
6.2.2 弯路一:公式抄错了?
DDIM 的更新式是(符号含义见第 5 章):
\begin{aligned} \hat{x}_0 &= \frac{x_t - \sqrt{1-\bar\alpha_t}\,\varepsilon_\theta(x_t,t)}{\sqrt{\bar\alpha_t}}\\ x_{t-1} &= \sqrt{\bar\alpha_{t-1}}\,\hat{x}_0 + \sqrt{1-\bar\alpha_{t-1}}\,\varepsilon_\theta(x_t,t) \end{aligned}
逐项对账:五个量(x_t、\varepsilon_\theta、\bar\alpha_t、\bar\alpha_{t-1}、\hat{x}_0) 和代码一一对上,没有多余项也没有缺项。
而且它和 DDPM 的均值是同一个东西 —— 把 \hat{x}_0 代回去展开, 得到的就是第 5 章式 (8) 里那个 \mu_\theta。 两条路只在”要不要加噪声”上分岔,在均值上完全一致。
公式没错。
6.2.3 弯路二:clamp 的锅?
第 5 章的实现里有一行 .clamp(-3, 3)——把 \hat{x}_0 截断到 [-3,3]。 这是社区里的常见做法,理由是”防止早期预测太离谱”。
怀疑它截断得太狠,把有用的信息切掉了。
于是把 clamp 去掉重跑:结果更糟——不是变好,是连 25 步那一档也炸了。
所以 clamp 是在帮忙,不是在捣乱。 它掩盖了一部分问题,但没能解决根本。
6.2.4 如果换成你,该怎么查:一张排查表
上面两条弯路走完,大概花了一个下午。它们都不是”聪明”的排查, 而是任何人在这种情况下都会先试的两件事。 真正值钱的是下面这张表—— 它把”采样器炸了”这个模糊的症状,拆成了八个可以单独证伪的假设。
| # | 假设 | 怎么查 | 如果成立,你会看到 |
|---|---|---|---|
| 1 | \bar\alpha 索引错位 | 打印 alpha_bar[t] 和 alpha_bar[t-1],对比前向加噪的实际方差 |
前向重建的 x_t 与训练时的分布对不上 |
| 2 | 时间步的编码方式不一致 | 训练用 t\in[1,T]、采样用 t\in[0,T-1] | 模型在 t=0 附近的输出完全无意义 |
| 3 | 模型输出的是 x_0 而不是 \varepsilon | 看训练损失里减的是哪个量 | loss 能降,但采样必炸 |
| 4 | 调度在训练和采样之间被重建 | 对比两处的 beta 数组 |
数值上差一点点,采样差很多 |
| 5 | clamp 掩盖了早期错误 | 去掉 clamp | 结果更糟(说明它在帮忙) |
| 6 | 步数太多导致累积 | 减少步数 | 少步数”看起来正常” |
| 7 | 采样器代码本身错 | 换成解析的完美去噪器 | 完全正常 |
| 8 | 模型本身不够好 | 提高训练步数 / 换更大模型 | 症状减轻但不消失 |
这张表的顺序不是随意的。 它按”改动成本从低到高”排: 前四条改一行代码就能验证,第五条改一个参数,第六条改一个数, 第七条要写一个新函数,第八条要重训模型。
排查的第一原则是:先做便宜的实验。 但这条原则有个例外 —— 当便宜的实验全都排除了嫌疑,而你又想不出新的时候, 应该立刻跳到最贵的那个对照实验,而不是继续在便宜的地方打转。
第七行就是那个”最贵但最决定性”的实验。 它的成本是写一个二十行的解析去噪器; 它的收益是把假设空间从”代码 vs 模型”一刀切成两半。
读代码只能发现”你写的东西和你以为的不一样”。 它发现不了”你以为的东西本身是错的”。
而换一个已知正确的组件,是在检验你的整个理解: 如果解析去噪器跑得通,那说明你对 DDIM 的理解、你的调度、 你的索引、你的张量形状,全都是对的——因为任何一处错, 解析去噪器也会炸。
一次实验,同时验证了二十个假设。 这就是对照实验的价值。
6.2.5 这个案例为什么值得花一整节
市面上讲 DDIM 的材料,绝大多数止步于”公式 + 代码 + 在 CIFAR 上 FID 多少”。 你照着抄,在真实数据上跑通了,然后呢?
然后你在自己的模型上炸了,而你不知道为什么。
本章花这么长的篇幅在一个”失败”上,是因为这个失败携带的信息量 远大于一次成功:
- 它逼你区分”代码正确”和”结果正确”——两者不是一回事
- 它逼你理解 1/\sqrt{\bar\alpha_t} 这个因子的数值后果, 而不只是它在公式里的位置
- 它给了你一个可复用的诊断动作:换完美去噪器
- 它让你看到”25 步看起来正常”这种结果有多危险
最后一条最值得展开。 一个只在 25 步下测过的实现, 会给出 std [0.5328, 0.3398],而数据是 [0.5707, 0.3351]。 任何人看这两个数都会说”对上了”。
但它是错的。 它错在:如果你把这个采样器用到 50 步、100 步—— 也就是别人论文里的标准设置——它会炸。 你交付的不是一个采样器,而是一个只在特定步数下碰巧能用的东西。
这类 bug 在生产环境里的表现是:离线评估用 50 步,线上为了省钱用 25 步, 于是线下指标很好,线上全是伪影 —— 而且没人会怀疑采样器, 因为”25 步的结果我们测过,是对的”。
clamp 能”帮忙”,说明 \hat{x}_0 的预测已经离谱到什么程度?
提示:数据本身在 [-1.14, 1.14] 之内。\pm 3 对数据来说是 2.6 倍。
(答案:说明有相当大比例的 \hat{x}_0 超过了 3。
实测(100 步 DDIM 的第一步,t=999):\hat{x}_0 的平均绝对值是 12.73, 87.6% 的元素被截断。
也就是说:在采样的第一步,模型给出的”原始数据估计”平均比真实数据大 11 倍, 近九成的分量要靠在 clamp 上。
这不是”偶尔预测离谱”,是”每一步都在离谱”。)
6.2.6 决定性实验:换一个不会犯错的去噪器
到这一步,剩下的可能性只有两种:
- A:我们的 DDIM 实现有隐藏的 bug
- B:DDIM 本身没错,是我们的模型不行,而 DDIM 把它放大了
区分它们的办法很直接:把模型换成一个”绝对不会犯错”的去噪器。
什么样的去噪器是完美的?当数据分布是高斯的时候,最优去噪器有闭式解。 设 x_0\sim\mathcal{N}(0,\sigma_0^2),则
x_t \sim \mathcal{N}\left(0,\ \bar\alpha_t\sigma_0^2 + 1-\bar\alpha_t\right), \qquad \mathbb{E}[x_0\mid x_t] = \frac{\sqrt{\bar\alpha_t}\,\sigma_0^2}{\bar\alpha_t\sigma_0^2 + 1-\bar\alpha_t}\,x_t
于是最优的 \varepsilon 预测是
\varepsilon^\star(x_t,t) = \frac{x_t - \sqrt{\bar\alpha_t}\,\mathbb{E}[x_0\mid x_t]}{\sqrt{1-\bar\alpha_t}}
这是一行代码,不需要训练,而且它在数学上就是最优的。
用同一个 ddim() 函数,把这个解析去噪器传进去:
| 步数 | 输出标准差 | 目标 |
|---|---|---|
| 1000 | 0.6232 | 0.6325 |
| 250 | 0.6188 | 0.6325 |
| 100 | 0.6099 | 0.6325 |
| 50 | 0.5951 | 0.6325 |
| 20 | 0.5509 | 0.6325 |
| 10 | 0.4768 | 0.6325 |
全部正常。连 20 步都还在 0.55(目标 0.63)。
同一个 ddim() 函数,同一份调度,同一批初始噪声 —— 只换了去噪器的实现。
- 换成解析的完美去噪器 → 1000 步到 20 步全部正常
- 换回学出来的模型 → 100 步炸到 10^8
所以 A 不成立:代码没错。
问题在 B:模型不够准,而 DDIM 会把”不够准”变成”爆炸”。
这是本章最重要的一步。 它把一个”到底是哪里错了”的开放式排查, 压缩成了”在一个变量上做切换”的对照实验。
6.2.7 元凶:1/\sqrt{\bar\alpha_t} 把相对误差放大 157 倍
现在来解释”为什么不够准会变成爆炸”。
看 DDIM 的第一步:
\hat{x}_0 = \frac{x_t - \sqrt{1-\bar\alpha_t}\,\varepsilon_\theta}{\sqrt{\bar\alpha_t}}
分母是 \sqrt{\bar\alpha_t}。 在 t=999 处:
\sqrt{\bar\alpha_{999}} = \sqrt{4.04\times10^{-5}} = 0.00636 \qquad\Longrightarrow\qquad \frac{1}{\sqrt{\bar\alpha_{999}}} = 157.2
也就是说:\hat{x}_0 是”x_t 与 \sqrt{1-\bar\alpha_t}\varepsilon_\theta 之差”再乘 157 倍。
模型误差 r(在 x_t 尺度上)会被放大成 \hat{x}_0 上的绝对误差 157r。
实测的第一步(notebooks/run_ch6_case.py 的输出,2000 个样本):
- 平均 |\hat{x}_0|(clamp 之前)= 12.8
- 所以 |x_t-\sqrt{1-\bar\alpha_t}\varepsilon_\theta| = 12.8\times0.00636 = 0.081
- 而 |\varepsilon_\theta|\approx 1.22,所以相对误差约 6.6%
- 86.5% 的坐标已经撞上 \pm3 的 clamp —— 也就是说,绝大多数样本的 \hat{x}_0 不再是”有偏的估计”,而是”被截断的垃圾”
6.6% 的误差本身并不离谱。 一个 5 万参数的模型在最高噪声处差 6.6%, 听起来完全合理。问题是 DDIM 把它乘了 157 倍,然后: 1. clamp 把它截到 3(信息丢失) 2. 重建出来的 x_{t-1} 偏离正确轨道 3. 下一步的模型在这个已经偏离的输入上继续猜 4. 误差进入正反馈
轨迹实测(100 步,2000 个样本,二范数均值):
| 步 i | t | \|x\| | \|\varepsilon_\theta\| | \|\varepsilon_\theta\|/\|x\| |
|---|---|---|---|---|
| 0 | 999 | 1.24 | 1.22 | 0.98 |
| 10 | 898 | 1.28 | 1.31 | 1.02 |
| 20 | 797 | 1.1\times10^{1} | 1.8\times10^{1} | 1.70 |
| 30 | 696 | 4.0\times10^{3} | 7.4\times10^{3} | 1.86 |
| 40 | 595 | 1.9\times10^{6} | 3.5\times10^{6} | 1.87 |
| 50 | 494 | 7.7\times10^{8} | 1.5\times10^{9} | 1.89 |
从第 20 步开始,|x| 每 10 步涨约 260–470 倍。
关键在最后一列之前的那个比值:稳定在 1.9。
一旦 x 离开训练分布(|x|\gg1),模型不再是去噪器, 它变成了一个近似线性放大的函数:\varepsilon_\theta\approx 1.9\,x。 这时 DDIM 的更新近似是
x_{t-1} \approx \sqrt{1-\bar\alpha_{t-1}}\,\varepsilon_\theta \approx 1.9\sqrt{1-\bar\alpha_{t-1}}\;x_t
在 t 较大的区间 \sqrt{1-\bar\alpha_{t-1}}\approx1,于是每步乘 1.9。 1.9^{10}\approx 613 —— 和实测的”每 10 步涨 300–470 倍”同一量级 (差的那部分来自 clamp 和 \hat x_0 项)。
一个乘性因子大于 1 的迭代,指数增长。
所以这个失败可以一句话概括
DDIM 在 t 很大时,把模型的相对误差乘以 1/\sqrt{\bar\alpha_t}。 当这个乘积超过 1,迭代就不稳定。
\sqrt{\bar\alpha_t} 在 t\to T 时趋近 0,所以这个放大倍数没有上界。
而 DDPM 为什么不会? 因为 DDPM 每步都加 \sigma_t z—— 噪声把 x 重新拉回训练分布所在的尺度,模型下一轮的输入仍然是”它认识的”。 这个机制第 5 章讲过(“误差的自我修正”),在这里得到了一个具体的数值验证。
6.2.8 反向证据:为什么步数越少反而越稳
回到那个反直觉的现象:25 步正常,100 步炸。
现在可以解释了。
不稳定是逐步累积的:如果每一步放大 \lambda 倍,走 N 步就是 \lambda^N。 步数越多,指数越大。
但这只是半个答案——\lambda 本身也随步长变化。 真正发生的事是:
步数少 → 每步跨得大 → 很快走完高噪声区 → 没有足够多轮次让正反馈建立起来。
换句话说,25 步”正常”不是因为 DDIM 在 25 步时更好, 而是因为它还没来得及坏。
我们检查了 25 步那组结果的分布:std [0.5328,\ 0.3398], 和数据 [0.5707,\ 0.3351] 很接近。
但它是不是一个好的采样?
不一定。 因为: 1. 只有 25 个样本点被用来估计统计量,误差本身就不小 2. 更关键:它可能只是”没走完”——高噪声区的错误还没来得及放大, 所以结果停在数据附近,但走的不是正确的概率流
这类”看起来对”的结果最危险。 它和本章后面要讲的 “假的二阶求解器”是同一类问题:指标正常,机制错误。
判断办法:换成完美去噪器跑同样的 25 步。 如果完美去噪器给出的是 0.55 而你的模型给出 0.53, 两者接近,说明大概率是对的;如果差得远,就要怀疑。
6.2.9 悬念
到这里,本章的案例给出了一个完整的因果链:
模型有约 6.6% 的误差 → DDIM 乘 157 倍 → 86.5% 的 \hat x_0 撞上 clamp → 采样器走出训练分布 → 模型退化成 \varepsilon_\theta\approx1.9x 的放大器 → 指数增长 → 溢出
但还有三个问题没回答:
- 那为什么别人用 DDIM 就不炸? 真实模型的误差也是这个量级啊。
- 第 5 章说 \mathcal{L}_{\text{simple}} 扔掉 ELBO 权重反而更好,这又是为什么?
- “二阶求解器”到底是什么,为什么能把步数压得更低?
这三个问题分别对应本章的三节。
6.3 DDIM:把 1000 步压成 20 步
6.3.1 先算一遍:哪一步在浪费
回到第 5 章那个 T=3、\beta=[0.1,0.2,0.3] 的小例子。 \bar\alpha = [0.9, 0.72, 0.504]。
相邻两步之间,\bar\alpha 差多少?
| 步 | \bar\alpha_t | 相对上一步 |
|---|---|---|
| 1 | 0.900 | — |
| 2 | 0.720 | 0.800 |
| 3 | 0.504 | 0.700 |
每一步把信号乘 0.8 或 0.7 —— 变化不小。
现在看本章的模型:T=1000,\beta 从 10^{-4} 到 0.02 线性。
| 步 | \bar\alpha_t | 相对上一步 |
|---|---|---|
| 100 | 0.897 | 0.99984 |
| 500 | 0.0786 | 0.99888 |
| 990 | 9.0\times10^{-5} | 0.98020 |
大部分步只把信号乘 0.9998。 —— 连走两步和走一步, 差别小到可能落在模型的分辨率之下。
DDIM 的第一个观察就是这个:T 步里有大量”几乎没动”的步。 如果能只走”动了”的那些,就能省下大部分计算。
但这里有个前提问题:DDPM 的反向过程是逐步定义的, 跳掉一步就打破了它的链式结构。凭什么可以跳?
6.3.2 关键洞见:前向过程可以换,边缘分布不变
DDPM 的整个推导建立在一条前向链上:
q(x_{1:T}\mid x_0) = \prod_{t=1}^{T} q(x_t\mid x_{t-1})
这条链决定了反向过程必须一步一步走。
DDIM 的洞见是:反向过程需要的其实不是这条链,而是每个时刻的边缘分布
q(x_t\mid x_0) = \mathcal{N}\left(\sqrt{\bar\alpha_t}x_0,\ (1-\bar\alpha_t)I\right)
只要边缘分布不变,反向过程就可以和别的”前向过程”配对。 而边缘分布只由 \bar\alpha_t 这一个数决定 —— 它不关心你中间走了几步。
所以可以构造一族新的前向分布,它们不是马尔可夫的,但边缘分布完全一样:
q_\sigma(x_{1:T}\mid x_0) = q(x_T\mid x_0)\prod_{t=2}^{T}q_\sigma(x_{t-1}\mid x_t,x_0)
其中
q_\sigma(x_{t-1}\mid x_t,x_0) = \mathcal{N}\left( \sqrt{\bar\alpha_{t-1}}\,x_0 + \sqrt{1-\bar\alpha_{t-1}-\sigma_t^2}\cdot \frac{x_t-\sqrt{\bar\alpha_t}x_0}{\sqrt{1-\bar\alpha_t}},\ \sigma_t^2 I\right)
这个式子看起来复杂,但它做的事实上只有一件: 把 x_t 拆成”信号 \sqrt{\bar\alpha_t}x_0“和”噪声 \sqrt{1-\bar\alpha_t}\varepsilon“, 按新的比例重新组装一个 x_{t-1}。
\sigma_t 控制重新注入多少噪声:
- \sigma_t = \sqrt{(1-\bar\alpha_{t-1})/(1-\bar\alpha_t)}\sqrt{1-\bar\alpha_t/\bar\alpha_{t-1}} → 退回 DDPM
- \sigma_t = 0 → 完全确定性(DDIM)
6.3.3 为什么边缘分布一定保得住(一个两行的证明)
上面那个 q_\sigma 是构造出来的,所以必须验证它真的保住了边缘分布。 验证只需要两行。
设 u = x_t-\sqrt{\bar\alpha_t}x_0。由边缘分布的定义,u\sim\mathcal{N}(0,(1-\bar\alpha_t)I)。 把它代进 q_\sigma 的均值:
\mathbb{E}[x_{t-1}\mid x_0] = \sqrt{\bar\alpha_{t-1}}x_0 + \frac{\sqrt{1-\bar\alpha_{t-1}-\sigma_t^2}}{\sqrt{1-\bar\alpha_t}}\,\mathbb{E}[u] = \sqrt{\bar\alpha_{t-1}}x_0
再看方差。x_{t-1} 是 u 的线性函数再加独立的 \sigma_t z,所以
\operatorname{Var}[x_{t-1}\mid x_0] = \frac{1-\bar\alpha_{t-1}-\sigma_t^2}{1-\bar\alpha_t}\cdot(1-\bar\alpha_t) + \sigma_t^2 = 1-\bar\alpha_{t-1}
均值和方差都对上了,而高斯分布由均值和方差唯一确定。证毕。
“边缘分布”这个约束,并不唯一确定前向过程。
它只固定了每个时刻的均值 \sqrt{\bar\alpha_t}x_0 和方差 1-\bar\alpha_t, 至于中间怎么从 x_t 走到 x_{t-1},有无穷多种走法。
DDPM 选了”马尔可夫、每步加固定方差噪声”的那一种; DDIM 选了”非马尔可夫、指定 \sigma_t“的那一种。 它们共享同一组边缘分布,所以共享同一个训练目标。
换句话说:训练只认边缘分布,采样可以不认前向链。 这是整个扩散模型文献里最值钱的一句话,也是后面流匹配、蒸馏、 一致性模型全部建立在上面的那句话。
6.3.4 两个端点的代数
先看 \sigma_t=0。 代进 q_\sigma:
q_0(x_{t-1}\mid x_t,x_0) = \delta\left(x_{t-1}-\sqrt{\bar\alpha_{t-1}}x_0 -\frac{\sqrt{1-\bar\alpha_{t-1}}}{\sqrt{1-\bar\alpha_t}}(x_t-\sqrt{\bar\alpha_t}x_0)\right)
狄拉克 \delta,也就是”没有随机性”。把 \varepsilon=(x_t-\sqrt{\bar\alpha_t}x_0)/\sqrt{1-\bar\alpha_t} 提出来, 就得到上一节那个”拆噪声再组装”的形式。
再看 DDPM 那个端点。 把 \sigma_t^2=\frac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}\left(1-\frac{\bar\alpha_t}{\bar\alpha_{t-1}}\right) =\frac{\beta_t(1-\bar\alpha_{t-1})}{1-\bar\alpha_t} 代进去,先算系数里的根号:
1-\bar\alpha_{t-1}-\sigma_t^2 =\frac{\alpha_t(1-\bar\alpha_{t-1})^2}{1-\bar\alpha_t} \quad\Longrightarrow\quad \frac{\sqrt{1-\bar\alpha_{t-1}-\sigma_t^2}}{\sqrt{1-\bar\alpha_t}} =\frac{\sqrt{\alpha_t}(1-\bar\alpha_{t-1})}{1-\bar\alpha_t}
代回均值,并把 x_t=\sqrt{\bar\alpha_t}x_0+u 拆开:
\mathbb{E}[x_{t-1}\mid x_t,x_0] =\underbrace{\frac{\sqrt{\bar\alpha_{t-1}}\beta_t}{1-\bar\alpha_t}x_0 +\frac{\sqrt{\alpha_t}(1-\bar\alpha_{t-1})}{1-\bar\alpha_t}x_t}_{\text{第 5 章的 DDPM 后验均值}}
系数对上了。 也就是说:DDPM 不是 DDIM 的”另一个算法”, 它是同一个 \sigma 族在 \sigma_t^2=\tilde\beta_t 处的取值。
6.3.5 \sigma_t 能取多大
q_\sigma 的根号里是 1-\bar\alpha_{t-1}-\sigma_t^2,所以必须有
\sigma_t^2\le 1-\bar\alpha_{t-1}
这个上界不是技术性的,它有物理含义:\sigma_t 是”这一步重新注入多少噪声”, 而 x_{t-1} 的总噪声预算是 1-\bar\alpha_{t-1}。 注入的不能超过预算,否则 x_{t-1} 的方差就超过边缘分布规定的值了。
DDPM 的取值 \tilde\beta_t 恰好顶到预算的一个特定比例; DDIM 取 0,一点都不用。
中间值呢? \sigma_t^2=\eta^2\tilde\beta_t,\eta\in[0,1] —— 这就是本章后面要专门讲的那个 \eta。它不是”随机性旋钮”, 它是”你在多大程度上不相信模型”的旋钮。
6.3.6 非马尔可夫意味着什么
q_\sigma(x_{1:T}\mid x_0) 仍然是一个合法的联合分布,只是不能再拆成 \prod_t q(x_t\mid x_{t-1})。它拆成的是
q_\sigma(x_{1:T}\mid x_0)=q(x_T\mid x_0)\prod_{t=2}^{T}q_\sigma(x_{t-1}\mid x_t,x_0)
注意条件里有 x_0。 这在”生成”的时候是个问题: 我们根本没有 x_0,只有模型估计的 \hat x_0。 而 DDIM 的做法就是:把 x_0 换成 \hat x_0,然后假装它是真的。
这个”假装”就是整个 DDIM 采样算法的全部内容。
6.3.7 先算一遍:\sigma_t=0 时它恰好是对的
用 T=3 那个例子。设 x_0=1.5,\varepsilon=0.3,则
x_3 = \sqrt{0.504}\times1.5 + \sqrt{0.496}\times0.3 = 1.0649 + 0.2113 = 1.2762
现在用 q_\sigma 往回走一步(\sigma_3=0):
\frac{x_3-\sqrt{\bar\alpha_3}x_0}{\sqrt{1-\bar\alpha_3}} = \frac{1.2762-1.0649}{0.7043} = 0.3000 = \varepsilon \quad\checkmark
拆出来的正是当初加进去的那个 \varepsilon。 于是
x_2 = \sqrt{0.72}\times1.5 + \sqrt{0.28}\times0.3 = 1.2728 + 0.1587 = 1.4315
而按前向过程的定义: x_3 = \sqrt{0.7}x_2 + \sqrt{0.3}\varepsilon_3,所以 x_2 应该满足 \sqrt{0.8}x_1 + \sqrt{0.2}\varepsilon_2 = \ldots
等一下 —— 这里要小心。 上面算出的 x_2 = 1.4315 应该是”从 x_3 反推的 x_2“。 而前向过程里,x_2 本来就是
x_2 = \sqrt{0.72}\times1.5 + \sqrt{0.28}\times\bar\varepsilon_{2}
如果 \bar\varepsilon_2 = 0.3(也就是和 \varepsilon_3 用的是同一个噪声), 两者完全一致。
但前向过程里,x_3 是用 \varepsilon_3 造的,x_2 是用 \bar\varepsilon_2 造的,两者不同。
所以这里的”一致”只在 \varepsilon 相同的情况下成立 —— 而这正是重点。
DDIM 的确定性更新,等价于”假设当初全程只加了一个噪声 \varepsilon, 然后沿着这条轨迹精确地倒着走”。
这当然是一个近似 —— 真实的前向过程每步加的是不同的噪声。 但它是一个”边缘分布正确”的近似:如果 \varepsilon 能被完美预测, 那么倒着走出来的每个 x_t 都服从正确的边缘分布 q(x_t|x_0)。
而 DDPM 的随机更新,是”每一步都承认自己不知道之前加了什么噪声”, 所以在每一步都重新注入一点随机性。
两者的区别不在正确性,在”你有多相信模型”。
6.3.8 上公式
把 \sigma_t=0 代进去,并把 x_0 换成模型的估计 \hat{x}_0:
\hat{x}_0 = \frac{x_t - \sqrt{1-\bar\alpha_t}\,\varepsilon_\theta(x_t,t)}{\sqrt{\bar\alpha_t}}
x_{t-1} = \sqrt{\bar\alpha_{t-1}}\,\hat{x}_0 + \sqrt{1-\bar\alpha_{t-1}}\,\varepsilon_\theta(x_t,t) \tag{6.1}
把 \hat{x}_0 代进去展开:
x_{t-1} = \frac{\sqrt{\bar\alpha_{t-1}}}{\sqrt{\bar\alpha_t}}x_t - \frac{\sqrt{\bar\alpha_{t-1}}\sqrt{1-\bar\alpha_t}}{\sqrt{\bar\alpha_t}}\varepsilon_\theta + \sqrt{1-\bar\alpha_{t-1}}\,\varepsilon_\theta
第一项用 \bar\alpha_{t-1}/\bar\alpha_t = 1/\alpha_t;后两项合并:
= \frac{1}{\sqrt{\alpha_t}}x_t + \left(\sqrt{1-\bar\alpha_{t-1}} - \frac{\sqrt{1-\bar\alpha_t}}{\sqrt{\alpha_t}}\right)\varepsilon_\theta \tag{6.2}
注意 (2) 和 DDPM 的均值(第 5 章式 7)不一样。 DDPM 的 x_t 系数也是 1/\sqrt{\alpha_t},但 \varepsilon_\theta 前面的系数不同, 而且 DDPM 还多一项 \sigma_t z。
两者在 \sigma_t 取特定值时才重合。
6.3.9 跳步:把 t-1 换成任意 t'<t
式 6.1 里根本没有出现”相邻”这个词。
它只用到 \bar\alpha_t 和 \bar\alpha_{t'} 两个数。 所以完全可以把 t' 取成 t-10、t-40,甚至 0。
这就是 DDIM 能跳步的全部理由:它的更新式只依赖边缘分布, 而边缘分布只看 \bar\alpha 的值,不看中间走了几步。
代价:跳得越大,“\varepsilon 全程只有一个”这个假设越不成立。 所以跳步会让结果偏离 —— 但偏离是可以量化的(下面实测)。
6.3.10 实测:完美去噪器下,步数与质量
这是本章最干净的一张表 —— 用解析的完美去噪器,把”模型不准”这个因素完全排除掉:
| 步数 | 输出标准差 | 与目标的偏差 |
|---|---|---|
| 1000 | 0.6232 | −1.5% |
| 250 | 0.6188 | −2.2% |
| 100 | 0.6099 | −3.6% |
| 50 | 0.5951 | −5.9% |
| 20 | 0.5509 | −12.9% |
| 10 | 0.4768 | −24.6% |
(目标 0.6325。)
这才是 DDIM 的真实性能:20 步掉 13%,10 步掉 25%。
而它比 DDPM 快 50 倍。
上面那张表里,标准的下降是单调的吗?偏差是怎么随步数增长的?
(答案:单调,而且增长在加速。
从 1000 → 250(4 倍少)偏差只从 1.5% 涨到 2.2%; 从 50 → 20(2.5 倍少)偏差从 5.9% 涨到 12.9%。
换句话说:省步数的收益在前半段很大,后半段迅速变差。
这给了一个很实用的判据:如果你的目标是”质量损失不超过 5%“, 那么 50–100 步就够了;如果要更好,1000 步的收益也很有限 (只比 250 步好 0.7 个百分点)。
但注意这只在”完美去噪器”上成立。 模型有误差时, 步数和稳定性的关系会完全不同 —— 本章的案例就是反例。
6.3.11 回到案例:为什么我们的 DDIM 炸了
现在可以把案例的因果链和 DDIM 的数学对上了。
式 6.1 里有一项在 t 大时会爆:
\hat{x}_0 = \frac{x_t - \sqrt{1-\bar\alpha_t}\,\varepsilon_\theta}{\sqrt{\bar\alpha_t}}
分母 \sqrt{\bar\alpha_t} 随 t\to T 趋于 0。
而 x_{t-1} 是由 \hat{x}_0 重建的:
x_{t-1} = \sqrt{\bar\alpha_{t-1}}\,\hat{x}_0 + \sqrt{1-\bar\alpha_{t-1}}\,\varepsilon_\theta
在 t 很大时 \sqrt{\bar\alpha_{t-1}} 也很小, 所以第一项本来就很小——但它是”错得很厉害的小”。
换句话说:DDIM 在最高噪声的几步里,做的是”除以一个极小的数, 再乘以另一个极小的数”。数学上它们抵消,数值上误差不抵消。
实测的放大倍数是 157 倍(t=999 处 1/\sqrt{\bar\alpha}=157.2)。
这就是为什么”模型准不准”在 DDPM 和 DDIM 上的后果完全不同: DDPM 的每一步都有一个新的随机项把误差冲淡, 而 DDIM 没有——它把所有误差原样累积下去。
6.4 score matching:同一件事的另一种写法
到这里 DDIM 的推导已经能用了。但还有一个更统一的视角, 它把 DDPM、DDIM、以及所有”用 ODE 采样”的方法放进同一个框架。
这个视角叫 score matching。 第 5 章末尾提过一句(式 24), 这一节把它讲清楚。
6.4.1 score 是什么
给定一个概率密度 p(x),它的 score 是
s(x) := \nabla_x \log p(x)
也就是”对数密度对位置的梯度”。
它回答的问题是:在这个点上,往哪个方向移动,密度上升得最快?
一个关键性质:p(x) = \frac{1}{Z}\tilde p(x) 时, \nabla_x\log p = \nabla_x\log\tilde p —— 归一化常数 Z 被求导消掉了。
这一点非常重要:第 5 章说过 p_\theta(x_0) 积不出来(那个 2000 重积分)。 但 score 不需要那个积分。
6.4.2 Tweedie 公式:去噪器为什么”自动”就是 score
上面那个推导只对”给定 x_0 的高斯条件密度”做了微分。但真正的 p_t(x_t) 是混合分布:
p_t(x_t)=\int p_t(x_t\mid x_0)\,q(x_0)\,dx_0
混合分布的梯度不能直接套单个高斯的公式。 这里需要一个经典结果。
Tweedie 公式:设 y\mid\mu\sim\mathcal{N}(\mu,\sigma^2I),先验 \mu\sim\pi(\mu), 则
\mathbb{E}[\mu\mid y]=y+\sigma^2\nabla_y\log p(y)
证明只用分部积分,一行:
\nabla_y p(y) =\nabla_y\int \frac{1}{(2\pi\sigma^2)^{d/2}}e^{-\frac{\|y-\mu\|^2}{2\sigma^2}}\pi(\mu)\,d\mu =\int \frac{\mu-y}{\sigma^2}\cdot\frac{e^{-\cdots}}{(2\pi\sigma^2)^{d/2}}\pi(\mu)\,d\mu
把 \frac{1}{\sigma^2} 提到外面,两边除以 p(y),就是 \nabla_y\log p(y)=\frac{\mathbb{E}[\mu\mid y]-y}{\sigma^2},移项即得。
把符号对上。 我们的”观测”是 x_t,“隐变量”是 x_0, 噪声方差是 1-\bar\alpha_t。于是
\mathbb{E}[x_0\mid x_t]=x_t+(1-\bar\alpha_t)\nabla_{x_t}\log p_t(x_t)
而模型学的是 \hat x_0=\mathbb{E}[x_0\mid x_t]。 代进去:
\nabla_{x_t}\log p_t(x_t) =\frac{\hat x_0-x_t}{1-\bar\alpha_t} =-\frac{x_t-\hat x_0}{1-\bar\alpha_t} =-\frac{\varepsilon_\theta}{\sqrt{1-\bar\alpha_t}}
这就是 式 6.4,而且这次是对混合分布证的。
它把”训练一个去噪器”和”估计一个分布的对数梯度”划了等号。
- 训练目标 \|\varepsilon_\theta-\varepsilon\|^2 看起来只是”拟合噪声”
- 但它的总体最优解恰好是 \nabla\log p_t
- 所以一个被训练去”猜噪声”的网络,实际上是在偷偷学整个数据分布的 log 密度梯度
这也解释了一件第 5 章没有解释的事: 为什么 \mathcal{L}_{\text{simple}} 里对 \varepsilon 做等权回归, 得到的却是一个在所有噪声水平上都可用的 score 估计? 因为 score 的真值就是 -\varepsilon/\sqrt{1-\bar\alpha_t}, 而 \varepsilon 本身在 \mathcal{N}(0,I) 尺度上是同尺度的(第 5 章那个 8500 倍的说法)。
6.4.3 从 score 到采样器:三种走法
有了 s_\theta(x,t)\approx\nabla\log p_t(x),至少有三种方式把它变成采样算法。
走法一:Langevin 动力学(只在单个噪声水平上)
x\leftarrow x+\tau\,s_\theta(x,t)+\sqrt{2\tau}\,z
问题:它只在一个固定的 t 上做,采样出来的是 p_t,不是 p_0。 要用它采样数据,必须从 t=T 开始、每一层都跑一遍 Langevin, 这就是”退火 Langevin”(annealed Langevin),慢,而且步长极难调。
走法二:反向 SDE
第 5 章的反向过程就是一个 SDE:
dx=\left[f(x,t)-g(t)^2\nabla\log p_t(x)\right]dt+g(t)\,d\bar w
它每步注入噪声,所以是随机的。DDPM 是它的一个离散化。
走法三:概率流 ODE
把同一个 Fokker-Planck 方程的扩散项”搬走”,剩下的漂移项就给出一个确定性 ODE。 这是下一小节要证的。
| 走法 | 随机性 | 每步代价 | 典型代表 |
|---|---|---|---|
| Langevin(退火) | 有 | 高(每层多步) | NCSN |
| 反向 SDE | 有 | 低(一步一求值) | DDPM |
| 概率流 ODE | 无 | 低 | DDIM / DPM-Solver |
三条路走的是同一个分布,只是”怎么走”不同。
6.4.4 概率流 ODE 是从哪来的
先说结论:ODE 式 6.5 和反向 SDE 共享同一组边缘分布 p_t。 两条路都能证,它们的难点不同。
6.4.4.1 路线一:连续性方程(不需要反向 SDE)
一个确定性 ODE \dot x=v(x,t) 会把概率密度按连续性方程搬运:
\frac{\partial p_t}{\partial t}=-\nabla\cdot\left[v\,p_t\right]
取 v=f-\frac12g^2\nabla\log p_t:
\frac{\partial p_t}{\partial t} =-\nabla\cdot[fp_t]+\frac12g^2\nabla\cdot\left[p_t\nabla\log p_t\right] =-\nabla\cdot[fp_t]+\frac12g^2\Delta p_t
最后一步用了 p_t\nabla\log p_t=\nabla p_t。
而这恰好是前向 SDE dx=f\,dt+g\,dw 的 Fokker-Planck 方程。 所以这个 ODE 搬运密度的方式和前向 SDE 完全一样,边缘分布当然一样。证毕。
这条路线只用了一个恒等式,是本书推荐的证法。
6.4.4.2 路线二:从反向 SDE 出发
反向 SDE(Anderson 1982),用反向时间 s=T-t 写:
d\bar x=\left[-f+g^2\nabla\log p\right]ds+g\,d\bar w
注意 f 变号了 —— 时间反演把漂移的方向也翻了。 这是最容易漏的一步。
换成”t 递减”的写法:
dx=\left[f-g^2\nabla\log p_t\right]dt+g\,d\bar w,\qquad dt<0
验证它保住 p_t。 令 q_s=p_{T-s},对 s 写 Fokker-Planck:
\frac{\partial q_s}{\partial s} =-\nabla\cdot\left[\left(-f+g^2\nabla\log q\right)q\right]+\frac12g^2\Delta q =\nabla\cdot[fq]-g^2\Delta q+\frac12g^2\Delta q =\nabla\cdot[fq]-\frac12g^2\Delta q
而 q_s=p_{T-s},所以 \partial_s q=-\partial_t p。 把前向的 \partial_t p=-\nabla\cdot[fp]+\frac12g^2\Delta p 代进去,两边相等。✓
现在把扩散项”关掉”,只留漂移。 但漂移里的系数要从 g^2 改成 \frac12g^2:
\frac{dx}{dt}=f-\frac12g^2\nabla\log p_t
为什么减半? 因为 SDE 的噪声项 g\,dw 在 Fokker-Planck 里贡献的是 \frac12g^2\Delta p。要在一个没有噪声的过程里复现同样的 \Delta p 项, 漂移里就只能是 \frac12g^2\nabla\log p。 少写这个 \frac12,边缘分布就会以两倍的速度扩散掉。
得到的正是 式 6.5。
三个约定,错一个就全错
- g 的定义里有没有 \sqrt2。 本书用 dx=f\,dt+g\,dw, Fokker-Planck 的扩散项是 \frac12g^2\Delta p。 有的书写 dx=f\,dt+\sqrt{2\beta}\,dw,扩散项就是 \beta\Delta p。
- 反向 SDE 的漂移里是 g^2 还是 \frac12g^2。 在”t 递减”的写法里是 g^2; 变成概率流 ODE 时才是 \frac12g^2。
- 时间反演时 f 要变号。
检查办法只有一个:把候选方程代进 Fokker-Planck,看能不能还原前向的 \partial_t p。对不上就是约定错了,不用怀疑数学。
6.4.5 \sigma 空间:把 DDIM 写成一次显式欧拉
ODE 式 6.5 里 t 从 T 走到 0,\bar\alpha_t 跨了五个数量级, 直接离散化会非常不均匀。所以要把变量换掉。
换元:令 \hat x=x/\sqrt{\bar\alpha_t},\sigma_t=\sqrt{1-\bar\alpha_t}/\sqrt{\bar\alpha_t}。 (这个 \sigma_t 和上一节的 \sigma_t 不是同一个东西 —— 这是本章最坑的一处记号冲突, DDIM 论文里 \sigma_t 指”注入噪声的幅度”,而在 score 文献里 \sigma 指”信噪比的倒数”。 本节以下一律用 \sigma 表示后者,注入噪声那个改用 \eta。)
在这个参数化下,x_t=\sqrt{\bar\alpha_t}\hat x,而 \hat x=x_0+\sigma\varepsilon,正好是”信号 + 噪声”的标准形式。
把 ODE 换元:对 \hat x=x/\sqrt{\bar\alpha} 求导,
\frac{d\hat x}{dt}=\frac{1}{\sqrt{\bar\alpha}}\frac{dx}{dt}-\frac{x}{2\bar\alpha^{3/2}}\frac{d\bar\alpha}{dt}
而 \varepsilon_\theta=-\sqrt{1-\bar\alpha}\,\nabla\log p_t。 代入并整理, 会发现所有含 \bar\alpha 的项都消掉,只剩
\frac{d\hat x}{d\sigma}=\varepsilon_\theta(\hat x,\sigma) \tag{6.3}
这是一个极其干净的形式:\hat x 对 \sigma 的导数,就是去噪器的输出。
欧拉法:\hat x_{k+1}=\hat x_k+(\sigma_{k+1}-\sigma_k)\varepsilon_\theta(\hat x_k,\sigma_k)。
从 \sigma_{\max}\approx152 走到 \sigma=0,一共 N 步。这就是 DDIM。
所以 DDIM 不是什么”新采样器”——它是概率流 ODE 在 σ 空间里的显式欧拉法。 后面的 DPM-Solver、Heun、以及所有”高阶求解器”, 做的都是同一件事:在 σ 空间里,用更好的方法解这个 ODE。
6.4.6 从 \varepsilon 到 score
第 5 章给过这个关系(式 24):
\varepsilon_\theta(x_t,t) = -\sqrt{1-\bar\alpha_t}\ \nabla_{x_t}\log p_t(x_t) \tag{6.4}
推导只用一步。 高斯情形下
p_t(x_t\mid x_0) = \mathcal{N}\left(\sqrt{\bar\alpha_t}x_0,\ (1-\bar\alpha_t)I\right)
\nabla_{x_t}\log p_t(x_t\mid x_0) = -\frac{x_t-\sqrt{\bar\alpha_t}x_0}{1-\bar\alpha_t} = -\frac{\varepsilon}{\sqrt{1-\bar\alpha_t}}
移项即得 (3)。
所以 (3) 在说什么
“预测噪声”和”估计 score”是同一件事,只差一个负的常数因子。
为什么这个等价有用? 因为 score 有自己的理论工具:
- score 不需要归一化常数 —— 这解释了为什么扩散模型能绕开那个积不出来的 p(x_0)
- score 的尺度在各噪声水平上是稳定的 —— 这正是第 5 章那个 “\varepsilon 损失的动态范围比 x_0 小 8500 倍”的另一种说法
- 所有基于 score 的采样器(Langevin、ODE、SDE)都能直接搬过来用
换句话说:我们一直叫它”预测噪声”,但它真正的数学身份是”估计 score”。
6.4.7 概率流 ODE
有了 score,就可以定义一个确定性的过程:
\frac{dx}{dt} = f(x,t) - \frac{1}{2}g(t)^2\,\nabla_x\log p_t(x) \tag{6.5}
这是”概率流 ODE”(probability flow ODE)。 它的性质是:沿着它走,每个时刻的边缘分布 p_t 都保持不变。
所以:从 p_T=\mathcal{N}(0,I) 出发,解这个 ODE 到 t=0,就得到数据分布。
注意它是确定性的 —— 给定起点,终点唯一。
6.4.8 DDIM 就是它的一阶离散化
把 (3) 代进 (4),再用最简单的欧拉法离散化,得到的就是 DDIM。
验证一下。 在”x_t=\sqrt{\bar\alpha_t}x_0+\sqrt{1-\bar\alpha_t}\varepsilon“这个参数化下, 可以换元成 \hat{x}=x/\sqrt{\bar\alpha},此时
\frac{d\hat{x}}{d\sigma} = \varepsilon_\theta,\qquad \sigma_t := \frac{\sqrt{1-\bar\alpha_t}}{\sqrt{\bar\alpha_t}}
欧拉法就是
\hat{x}_{t-1} = \hat{x}_t + (\sigma_{t-1}-\sigma_t)\,\varepsilon_\theta
把它换回 x:两边乘 \sqrt{\bar\alpha_{t-1}},整理后恰好是 DDIM 的 式 6.1。
① 为什么 DDIM 是确定性的。 因为 ODE 本身没有随机性。 确定性不是 DDIM 的设计选择,是”用 ODE 采样”这个决定的直接后果。
② 为什么 DDIM 会炸,而 DDPM 不会。 ODE 的解对初值和向量场都是连续依赖的,但误差会累积—— 没有随机项来”重置”它。 SDE(DDPM)每步注入的噪声,相当于在数值积分里做了一次”重新投影”。
一句话:ODE 精确但脆弱,SDE 有噪声但稳健。
6.4.9 这个视角的实际用处
它不只是一个漂亮的重述。 它带来三件事:
- 任何 ODE 求解器都能直接用。 欧拉是一阶,还有二阶(Heun)、三阶(DPM-Solver-3)。 这就是下一节的内容。
- 它解释了”为什么 DDIM 的步数可以很少”:因为 ODE 的解是一条光滑曲线, 跳步相当于用更少的采样点去逼近它。
- 它把”采样”变成了”数值积分” —— 一个有几百年历史的成熟领域。
6.5 高阶求解器:为什么二阶比一阶快
6.5.1 一个和扩散无关的例子
先离开扩散,看一个纯数学问题。
要解 \dfrac{dy}{dt}=y,y(0)=1,走到 t=1。真解是 y(1)=e=2.71828。
一阶(欧拉):y_{n+1} = y_n + h\cdot y_n = y_n(1+h)。 走 N 步,h=1/N,结果是 \left(1+\frac1N\right)^N。
| N | 欧拉结果 | 相对误差 |
|---|---|---|
| 4 | 2.4414 | −10.2% |
| 8 | 2.5658 | −5.6% |
| 16 | 2.6379 | −3.0% |
| 32 | 2.6769 | −1.5% |
误差大致按 1/N 下降 —— 步数翻倍,误差减半。这是一阶方法的标志。
二阶(Heun):先走一步欧拉得到预测值,再在预测点上求一次斜率,取平均。
y_{n+1} = y_n + \frac{h}{2}\left(f(y_n) + f(y_n + h f(y_n))\right)
| N | Heun 结果 | 相对误差 |
|---|---|---|
| 4 | 2.6479 | −2.6% |
| 8 | 2.7015 | −0.6% |
| 16 | 2.7140 | −0.16% |
误差按 1/N^2 下降 —— 步数翻倍,误差降到 1/4。
这就是”阶”的含义:一阶方法每多算一步,误差线性下降; 二阶方法每多算一步,误差平方下降。
代价:Heun 每步要算两次 f。所以公平的比较是 “Heun 走 N 步” vs “欧拉走 2N 步”。
| 函数求值次数 | 欧拉误差 | Heun 误差 |
|---|---|---|
| 8 | −5.6% | −2.6%(4 步 × 2) |
| 16 | −3.0% | −0.6%(8 步 × 2) |
| 32 | −1.5% | −0.16%(16 步 × 2) |
同样的计算量,二阶比一阶准 3–10 倍。
6.5.2 扩散里的”斜率”是什么
上一节已经证明:DDIM 就是概率流 ODE 的欧拉离散化。
在 \hat{x}=x/\sqrt{\bar\alpha}、\sigma=\sqrt{1-\bar\alpha}/\sqrt{\bar\alpha} 这个换元下:
\frac{d\hat{x}}{d\sigma} = \varepsilon_\theta(\hat{x},\sigma) \tag{6.6}
所以”斜率”就是模型的输出 \varepsilon_\theta。
DDIM 的一步:
\hat{x}_{t-1} = \hat{x}_t + (\sigma_{t-1}-\sigma_t)\,\varepsilon_\theta(\hat{x}_t,\sigma_t)
括号里只有”在起点算的那一次 \varepsilon“。这就是欧拉。
6.5.3 为什么步长不能在 \sigma 上均匀
在讲二阶之前,必须先讲一个比阶数更值钱的问题:网格怎么取。
大多数人第一次写 DDIM 会这么写:
idx = torch.linspace(T - 1, 0, steps) # 在 t 上均匀看起来天经地义:1000 步压成 20 步,不就是每 50 步取一个点吗? 但 t 均匀不等于 \sigma 均匀。而 ODE 是在 \sigma 上积分的。
把这件事测出来。 用完美去噪器(排除模型误差),只改网格,其他全不动:
| 步数 | \sigma 均匀 | \log\sigma 均匀 |
|---|---|---|
| 10 | 0.0228 | 0.4201 |
| 20 | 0.0480 | 0.5216 |
| 40 | 0.0972 | 0.5744 |
| 80 | 0.1880 | 0.6016 |
目标标准差 0.6325。同样的函数求值次数,\log\sigma 均匀全面碾压: 10 步时是 18 倍,80 步时还有 3 倍。
而 \sigma 均匀的那个甚至越走越糟 —— 0.0228 到 0.1880, 加步数只在缓慢地补救,因为它把绝大多数步数浪费在了 \sigma 很大的区间。
为什么? \sigma 从 152 降到 0,跨了四个数量级。 均匀切 20 份,每份宽 7.6,于是:
- 前 19 步都在 \sigma\in[7.6,\,152] 里走 —— 这个区间里数据的信息几乎为零
- 最后一步从 \sigma=7.6 直接跳到 0 —— 所有细节都靠这一步
而 \log\sigma 均匀是”按比例”分配:\sigma 越大步子越大,\sigma 越小步子越细。 这正好匹配 ODE 的行为 —— 在 \sigma\to0 附近,\hat x 的变化最快。
一句话:误差不是均匀产生的,步长也不该均匀
在 \sigma 大的地方,\hat x 几乎是纯噪声,走错一点无所谓; 在 \sigma 小的地方,\hat x 已经接近数据,走错一点就是可见的伪影。
所以采样器的时间表必须”前粗后细”。
DDIM 论文里的 linspace(T-1, 0, steps) 之所以能用,不是因为 t 均匀是对的, 而是因为这个线性 \beta 调度下,t 均匀碰巧接近 \log\sigma 均匀。 换个调度(比如余弦),这个巧合就没了 —— 这就是为什么换调度必须重调步数。
这也是 DPM-Solver 系列真正贡献的一半。 它们不只是”用了高阶方法”, 而是在 \lambda=\log(\alpha/\sigma) 上均匀取点,并且把 ODE 拆成 “线性部分精确解 + 非线性部分低阶近似”。 阶数只是另一半。
本书不复现 DPM-Solver 的完整公式(它有三种阶数、两种末步修正, 记号比本章其余部分加起来还多),但你可以用上面的表来检验任何实现: 把同一个采样器分别喂 \sigma 均匀和 \log\sigma 均匀的网格, 如果差别巨大,说明它没有做 \lambda 参数化。
6.5.4 Heun 版本
二阶就是把”起点算一次、预测点再算一次、取平均”这套照搬过来:
\begin{aligned} k_1 &= \varepsilon_\theta(\hat{x},\sigma_t) &&\text{(起点)}\\ \hat{x}_{\text{pred}} &= \hat{x} + (\sigma_{t-1}-\sigma_t)\,k_1 &&\text{(欧拉预测)}\\ k_2 &= \varepsilon_\theta(\hat{x}_{\text{pred}},\sigma_{t-1}) &&\text{(预测点)}\\ \hat{x}_{t-1} &= \hat{x} + \frac{\sigma_{t-1}-\sigma_t}{2}(k_1+k_2) &&\text{(平均)} \end{aligned}
代价是模型调用次数翻倍。
6.5.5 实测:同样的模型,二阶能省多少步
用解析的完美去噪器(排除模型误差干扰),1D 高斯,目标标准差 0.6325:
| 步数 | Euler(一阶) | Heun(二阶) | Euler 误差 | Heun 误差 |
|---|---|---|---|---|
| 10 | 0.4847 | 0.6802 | −23.3% | +7.5% |
| 20 | 0.5601 | 0.6450 | −11.4% | +2.0% |
| 40 | 0.5975 | 0.6375 | −5.5% | +0.8% |
| 80 | 0.6163 | 0.6357 | −2.6% | +0.5% |
两件事值得注意。
① 二阶确实更快。 要把误差压到 2% 以内: Euler 需要约 100 步,Heun 只需要 20 步 × 2 次求值 = 40 次。 算力省了一半以上。
② 但 10 步时 Heun 反而”过头”了。 Euler 是 −23%,Heun 是 +7.5%。 方向相反,量级都很大。
这不是 bug:在步长这么大的时候,两种方法都还没进入”渐近收敛区”, 误差的符号取决于具体路径。 “高阶更准”是一个渐近结论,不是无条件成立的。
看 10 步那一行:Heun 也已经错了 7.5%。
一阶和二阶的差别在”中等步数”最大(20–40 步这一带), 在极端少的步数下两者都不可靠。
实践含义:如果你的目标是 10 步以内, 换更高阶的求解器不如换别的思路(蒸馏、对抗微调、或者干脆接受质量下降)。
6.5.6 一个我自己踩过的坑:假的二阶
写这一节的时候,我先写了一版 Heun,跑出来的结果是”和欧拉一模一样”。 当时以为”扩散的 ODE 太简单,二阶没优势”,差点就这么写进书里。
后来检查代数,发现那版”二阶”是恒等式。
它长这样:
\begin{aligned} \hat{x}_0 &= \frac{x-\sqrt{1-a}\,e_1}{\sqrt{a}}\\ x_{\text{pred}} &= \sqrt{a'}\,\hat{x}_0 + \sqrt{1-a'}\,e_1 &&\text{(欧拉步)}\\ e_2 &= \varepsilon_\theta(x_{\text{pred}},t')\\ \hat{x}_0' &= \frac{x_{\text{pred}}-\sqrt{1-a'}\,e_2}{\sqrt{a'}}\\ x_{\text{out}} &= \sqrt{a'}\,\frac{\hat{x}_0+\hat{x}_0'}{2} + \sqrt{1-a'}\,\frac{e_1+e_2}{2} \end{aligned}
它看起来很像 Heun:算两次 \varepsilon,取平均。
但实际上
\sqrt{a'}\,\hat{x}_0' + \sqrt{1-a'}\,e_2 = \sqrt{a'}\cdot\frac{x_{\text{pred}}-\sqrt{1-a'}\,e_2}{\sqrt{a'}} + \sqrt{1-a'}\,e_2 = x_{\text{pred}}
而 \sqrt{a'}\hat{x}_0 + \sqrt{1-a'}e_1 也等于 x_{\text{pred}}。
所以 x_{\text{out}} = \frac{x_{\text{pred}}+x_{\text{pred}}}{2} = x_{\text{pred}} —— 它原样返回了欧拉步。
因为它不会报错,也不会给出荒谬的结果。 它给出的是完全正常的欧拉结果。
你会以为自己在用二阶,实际上一直在用一阶。
判断一个”改进”是不是真的改了,有一个不用推导的办法:
换一个它应该影响很大的设置(比如极少的步数), 看输出有没有变化。如果没有变化,它多半是恒等式。
我当初就是这么发现的:10 步下两个”不同”的求解器给出到小数点后六位都一样的数。
这类”实现上看似生效、数学上恒等”的 bug, 和本章案例里那个 DDIM 爆炸是一对: 前者是”改了但没效果”,后者是”没改对但后果巨大”。 两者都只能靠”拿一个已知答案的对照去测”发现。
6.6 数值刚性:为什么步长会被 \sigma 卡住
上一节说”用更好的求解器能省步数”。但有一个下限,任何阶数都突破不了。 这一节讲那个下限从哪来。
6.6.1 先看一个具体的数
回到 \sigma 空间的 ODE:\dfrac{d\hat x}{d\sigma}=\varepsilon_\theta(\hat x,\sigma)。
右边的大小是多少? 在完美去噪器下, \varepsilon=(x-\hat x_0)/\sigma,而 \hat x_0=\frac{V_0}{V_0+\sigma^2}\hat x,所以
\varepsilon=\frac{\sigma}{V_0+\sigma^2}\hat x
在 \sigma 大时(\sigma^2\gg V_0):\varepsilon\approx \hat x/\sigma,很小。 在 \sigma 小时(\sigma^2\ll V_0):\varepsilon\approx \sigma\hat x/V_0,也很小。 最大值在 \sigma=\sqrt{V_0} 处,此时 |\varepsilon|=|\hat x|/(2\sqrt{V_0})。
所以 \varepsilon 本身并不大。问题不在它的大小,在它的变化率。
6.6.2 刚性是什么
一个 ODE 叫”刚性”(stiff),如果它的解变化很慢,但解附近的 “邻近轨迹”变化很快。
后果很直接:显式方法(欧拉、Heun、Runge-Kutta) 为了保证数值稳定,步长必须由”最快的那条邻近轨迹”决定, 而不是由”解本身”决定。 于是你被迫走很多小步, 而其中大部分步数对精度毫无贡献。
扩散 ODE 是刚性的吗? 是,而且刚性的来源很具体: 在 \sigma\to0 附近,\varepsilon 对 \hat x 的偏导数会变大。
算一下。完美去噪器的雅可比:
\frac{\partial\varepsilon}{\partial\hat x} =\frac{\partial}{\partial\hat x}\left(\frac{\sigma}{V_0+\sigma^2}\hat x\right) =\frac{\sigma}{V_0+\sigma^2}
这个量在 \sigma\to0 时趋于 0,在 \sigma 大时也趋于 0, 最大值在 \sigma=\sqrt{V_0}。
所以按这个模型,它并不刚。 那为什么欧拉需要 100 步?
因为误差不在雅可比,在”每一步跨过了多少个 \sigma 的尺度”。 \sigma 从 152 到 0,如果均匀切 20 份,每份 7.6; 而 \varepsilon 的特征尺度是 \sqrt{V_0}=0.63。 一步跨了 12 个特征尺度。
这就是”刚性”在这里的真实含义:不是稳定性极限, 而是”步长相对于解的特征尺度太大”。
“不稳定”和”不精确”是两件事。
- 不稳定:误差被放大,走越多步越糟(本章 §6.2 的学出来的模型)
- 不精确:误差随步长下降,但下降得不够快(完美去噪器 + 粗网格)
本章实验 6-1 里的完美去噪器是”不精确”,不是”不稳定” —— 它的误差随步数单调下降(0.4847 → 0.5601 → 0.5975 → 0.6163), 只是下降得慢。
把这两件事混为一谈,是”加步数”这个建议经常失效的原因: 如果你的问题是稳定性,加步数没用;如果是精度,加步数有用但慢。
6.6.3 步长的三个尺度
在扩散 ODE 里,至少有三个不同的”尺度”在同时起作用:
| 尺度 | 量级 | 它决定什么 |
|---|---|---|
| \sigma 本身 | 152 → 0 | 网格的跨度 |
| 数据尺度 \sqrt{V_0} | 0.63 | \hat x 的特征大小 |
| 噪声尺度 \sqrt{1-\bar\alpha} | 1 → 0 | 前向过程的方差 |
欧拉的步长必须同时小于后两个尺度,但网格的跨度由第一个决定。 这就是为什么”均匀步长”必然浪费 —— 它必须按最小的尺度取, 于是总步数是 152/0.63\approx240。
而 \log\sigma 网格把这件事解决了:它让步长与 \sigma 成正比, 在 \sigma 大的地方大步走(那里 \hat x 本来就是噪声), 在 \sigma 小的地方小步走(那里 \hat x 已经接近数据)。
这就是上一节那张表的解释。 10 步时 \sigma 均匀是 0.0228、 \log\sigma 均匀是 0.4201,差 18 倍 —— 因为 \sigma 均匀的前 9 步都在”噪声区”空转,最后一步跨过了整个数据区。
6.6.4 一个反例:为什么”更细的均匀网格”也不行
你可能会想:那把步数加到 1000 不就行了?
行,但你回到了 DDPM 的成本。 而 DDPM 用 1000 步能得到好结果, 不是因为它”细”,是因为它每步注入噪声——它在用随机性绕过刚性问题。
确定性采样器想省步数,唯一的出路是”非均匀网格 + 高阶方法”。 两者缺一不可:
- 只有高阶:粗网格上高阶方法的误差项仍然很大(实验 6-1 的 10 步 Heun 是 +7.5%)
- 只有非均匀网格:一阶方法在细端仍然是一阶(\log\sigma 网格 80 步才 0.6016)
这也是 DPM-Solver 存在的理由:它同时做了这两件事。
用一个生活类比收尾
想象你要从山顶走到山脚,海拔从 5000 米降到 0。 山脚附近每一步都很关键(那里有你要找的东西), 山顶附近走快走慢无所谓(那里全是雾)。
均匀步长 = 每 250 米海拔停一次,于是你在山脚的 250 米里只停一次, 把最关键的一段一步跨过去了。
\log\sigma 网格 = 按”还有多少海拔”分配步数, 越接近山脚步越密。这才合理。
而”高阶方法”相当于给你一双更好的鞋 —— 但如果你一步跨 250 米, 鞋再好也没用。
6.7 \mathcal{L}_{\text{simple}} 之谜
6.7.1 问题的形式
第 5 章 §5.7 推过 ELBO 的权重:
\lambda_t = \frac{\beta_t^2}{2\sigma_t^2\alpha_t(1-\bar\alpha_t)}
实测它在 t 上的分布是:t=2 处 0.5997,t=500 处 0.0055 —— 差 109 倍, 而且 t=1 处发散。
DDPM 的做法是把它整个换成 1:
\mathcal{L}_{\text{simple}} = \mathbb{E}_{t\sim U\{1,T\}}\left[\|\varepsilon-\varepsilon_\theta\|^2\right]
论文报告:这样做生成质量反而更好。
一个理论上正确的加权,去掉之后反而更好 —— 这需要解释。
6.7.2 三个假说
假说一:梯度噪声。 小 t 的权重极大,如果那一带的梯度方向本身很嘈杂, 放大权重就是在放大噪声。
假说二:优化难度。 小 t 处 \tilde\beta_t\approx0(t=1 时精确为 0), loss 里的分母极小 → 少数样本的误差就能主导梯度 → 训练不稳。
假说三:感知权衡。 小 t 对应高频细节,大 t 对应整体结构。 人眼对结构更敏感,所以把学习重心挪向大 t 会得到”更好看”的图 —— 即使似然变差。
这三个假说都能解释”为什么更好”,但它们给出的实践建议完全不同。 所以值得测。
6.7.3 实测:各 t 段的梯度相干性
假说一(梯度噪声)可以直接测。
做法:固定参数,对每个 t 区间抽 12 个不同的 batch, 各算一次梯度,然后看这 12 个梯度的方向一致性:
\text{相干性} = \frac{\left\|\frac{1}{B}\sum_b g_b\right\|}{\frac{1}{B}\sum_b \|g_b\|}
相干性 = 1 表示所有 batch 的梯度方向完全相同(零噪声); 相干性接近 0 表示它们互相抵消(纯噪声)。
实测(本章的模型,训完 5000 步之后):
| t 区间 | 梯度相干性 | 该段的 loss |
|---|---|---|
| 1–10 | 0.9468 | 1.0099 |
| 11–50 | 0.9152 | 0.7922 |
| 51–200 | 0.5133 | 0.4731 |
| 201–500 | 0.7226 | 0.1037 |
| 501–1000 | 0.9820 | 0.0079 |
梯度相干性最低的是 t\in[51,200](0.51),不是小 t。
小 t(1–10)的相干性高达 0.9468 —— 梯度非常一致,几乎没有噪声。
所以”小 t 的梯度更噪”这个假说,在我们的模型上不成立。
那为什么 ELBO 的权重在小 t 处那么大? 因为 \tilde\beta_t 在分母上, 而 \tilde\beta_1 = 0。这是权重公式的算术后果,不是”那里更重要”的判断。
权重公式把”信息量少”(\tilde\beta_t 小)翻译成了”权重大”。 但”信息量少”不等于”梯度有用”。
6.7.4 那真正的原因是什么
看 loss 那一列。
t\in[1,10] 的 loss 是 1.0099 —— 而一个”完全预测不出噪声”的模型在 2 维上的 loss 正好是 1 (每个分量方差 1,均方误差的期望就是 1)。
也就是说:在这个区间,模型基本没有学到东西。
而 t\in[501,1000] 的 loss 是 0.0079 —— 模型几乎完美。
合起来看:
| 权重 \lambda_t | 模型实际表现 | |
|---|---|---|
| 小 t | 极大(t=2 处 0.60) | 很差(loss ≈ 1.01) |
| 大 t | 较小(t=500 处 0.0055) | 极好(loss ≈ 0.008) |
ELBO 把绝大部分学习压力放在”模型目前最不擅长、而且可能根本学不会”的地方。
这就是假说二的精确版本:不是”梯度有噪声”, 而是“权重把优化目标推向了一个收益递减的方向”。
在小模型上,这个代价很明显。在大模型上,小 t 是能学会的 —— 所以这个解释的适用范围需要限定(见 §6.9)。
上面说”小 t 的 loss ≈ 1,等于什么都没学到”。
但第 5 章的案例里,模型明明能从噪声里恢复出双月分布。 如果小 t 什么都没学到,采样怎么还能成功?
(答案:因为采样主要靠的是大 t 的那一段。
回看第 5 章 §5.7 的分箱表:t\in[502,1000] 的平均 L_{t-1} 只有 0.0001, 而 t\in[2,11] 是 0.3992。ELBO 的数值由小 t 主导, 但采样的”骨架”由大 t 决定。
小 t 负责的是”细节修正” —— 在已经接近数据的点上再微调。 这部分做得不好,结果是”细节模糊”,不是”完全崩掉”。
这也解释了为什么 \mathcal{L}_{\text{simple}} 有效: 它把学习重心从”做不好的细节”挪到了”能做好的骨架”上。 代价是似然变差(细节不准),收益是生成质量变好(结构对)。
6.7.5 一个必须说清楚的边界
上面这套解释,是在一个 5 万参数的 2 维玩具模型上得到的。
在大模型上,“小 t 学不会”这个前提可能不成立 —— 模型的容量足够,小 t 的 loss 可以降得很低。 那时候 ELBO 权重是否还有害,就不好说了。
实际上,后续有工作重新引入了加权(比如给 \mathcal{L}_{\text{simple}} 乘上 与 SNR 相关的因子),在高分辨率生成上取得了收益。 这说明”扔掉权重”不是一个普适的最优解,而是一个在那个规模下有效的选择。
本章的结论应该这样记:
ELBO 的权重把学习压力集中在小 t;在那个规模下这是低效的。 换成均匀权重后,模型把容量用在了更有产出的地方。
这解释了”为什么有效”,但没有证明”永远应该这么做”。
6.7.6 \eta:不是”随机性旋钮”,是”稳定性旋钮”
DDIM 的原始论文留了一个参数 \eta,把 \sigma_t 写成
\sigma_t(\eta) = \eta\sqrt{\frac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}}\sqrt{1-\frac{\bar\alpha_t}{\bar\alpha_{t-1}}}
- \eta=0:完全确定性(DDIM)
- \eta=1:退回 DDPM 的随机采样
通常的解读是”\eta 控制生成结果的多样性”。 但在我们这个模型上,它控制的是完全不同的东西。
固定 100 步,只改 \eta:
| \eta | 输出标准差 | 最大绝对值 |
|---|---|---|
| 0 | [5.37\times10^{7},\ 1.25\times10^{8}] | 4.97\times10^{9} |
| 0.25 | [2.02\times10^{7},\ 4.71\times10^{7}] | 1.61\times10^{9} |
| 0.5 | [1.17\times10^{7},\ 2.72\times10^{7}] | 1.22\times10^{9} |
| 0.75 | [1.12\times10^{6},\ 2.61\times10^{6}] | 1.17\times10^{8} |
| 1.0 | [6.02\times10^{3},\ 1.40\times10^{4}] | 6.28\times10^{5} |
\eta 从 0 到 1,输出缩小了四个数量级 —— 而且方向是对的。
再看 50 步:
| \eta | 输出标准差 | 最大绝对值 |
|---|---|---|
| 0 | [13.83,\ 32.10] | 1.33\times10^{3} |
| 0.5 | [5.84,\ 13.47] | 601 |
| 1.0 | [0.525,\ 0.320] | 1.18 |
\eta=1(完全退回 DDPM)在 50 步下给出了正确的分布 (数据是 [0.5707,\ 0.3351])。
如果”确定性采样会放大误差”这个解释是对的, 那么”把随机性加回来”就应该能缓解 —— 而实测正是如此。
而且缓解的方向也对:\eta 越大(每次注入的噪声越多), 误差被”洗掉”得越彻底,输出越接近正确量级。
注意 \eta=1 在 50 步下竟然完全正确,而 DDPM 原本要 1000 步。 这说明”50 步 × 完全随机”是一个可用的折中 —— 比”1000 步 × 随机”快 20 倍,比”50 步 × 确定性”稳得多。
所以真正决定稳定性的不是”步数”,是”每一步允不允许模型犯错后重新对焦”。
既然 \eta=1 最稳,那为什么还要用 \eta=0(DDIM)?
(答案:因为 \eta 同时改变了”轨迹”和”确定性”,而这两件事的代价不同。
\eta>0 的代价: - 同样的步数下,结果有随机性 —— 同一起点每次生成不同的图 - 在图像编辑、插值这类任务里,确定性是刚需(你要的是”改一个属性、其它不变”, 随机采样会让整个图都变)
\eta=0 的收益: - 确定性 —— 可以做插值(两条轨迹之间的点有意义)、可以做编辑 - 在好模型上,它确实更快(20 步就够)
所以选择不是”哪个更稳”,是”你能接受多大的模型误差”。
模型好 → \eta=0 又快又确定;模型差 → \eta=1 才稳得住。 本章的模型属于后者。
6.8 交互实验
本章两个实验,都围绕”采样器怎么选”。
6.8.1 实验 6-1 采样器对比实验室
怎么用。 选一个采样器(DDPM / DDIM / Heun),拖步数滑块, 看输出分布和数据分布的差距。同时显示函数求值次数 —— 这是公平比较的关键。
观察引导。
- 把 DDIM 的步数从 1000 拖到 20:分布是什么时候开始明显偏离的?
- 换成 Heun(同样步数):偏离改善了多少? 看底部的”函数求值次数”是不是翻倍了。
- 最关键的一问:把步数拉到 10,三个采样器哪个最接近数据? (提示:不一定是你以为的那个。)
这一节真正要你记住的是
“步数”不是唯一的成本,“每次求值算几次”也要算进去。 比较采样器时,比的应该是同样的函数求值次数下谁更准。
6.8.2 实验 6-2 偏差诊断实验室
怎么用。 这个实验不跑真模型——它用一个解析的完美去噪器 + 可调的误差注入, 把”模型有多准”变成一个可以拧的旋钮。
三个滑块: - 误差幅度 b:给去噪器乘上 (1+b),b\in[0, 0.2] - 步数:10–500 - \eta:0–1
观察引导。这个实验的结论和直觉相反,请先自己拖一遍。
- 固定 \eta=0、100 步,把 b 从 0 一路拖到 0.20。 曲线发散了吗? —— 没有。它只是整体下移:标准差从 0.6099 掉到 0.1923, 一直是”偏小”,从来没有”爆炸”。
- 固定 b=0.10,把步数从 10 拖到 500。加步数能救回来吗? —— 不能。它只是从 0.2928 缓慢爬到 0.3575,稳定在目标值的 55% 左右。
- 现在回到本章案例:学出来的模型在 100 步下是 5.4\times10^{7}。 既然均匀偏差炸不了,它到底做错了什么?
6.8.2.1 一组完整的实测:均匀偏差从不发散
notebooks/run_ch6_bias_grid.py 跑了 5 种偏差 × 6 种步数, 每格 4000 个样本,目标标准差 0.6325:
| 偏差 b | 25 步 | 50 步 | 100 步 | 200 步 | 400 步 | 800 步 |
|---|---|---|---|---|---|---|
| 0% | 0.5656 | 0.5951 | 0.6099 | 0.6173 | 0.6208 | 0.6226 |
| 1% | 0.5301 | 0.5606 | 0.5759 | 0.5835 | 0.5872 | 0.5890 |
| 2% | 0.4967 | 0.5281 | 0.5438 | 0.5516 | 0.5554 | 0.5573 |
| 5% | 0.4081 | 0.4412 | 0.4577 | 0.4659 | 0.4698 | 0.4718 |
| 10% | 0.2928 | 0.3264 | 0.3431 | 0.3515 | 0.3555 | 0.3575 |
整张表 30 个格子,没有一个是发散的。 每一列都在向”被拉小后的那个值”收敛, 偏差越大,收敛到的值越小。
换别的误差形状,也一样。 下面六种误差模型,全部在 100 步、\eta=0 下测过:
| 误差模型 | 数学形式 | 100 步输出标准差 |
|---|---|---|
| 无误差 | \hat\epsilon=\epsilon^\star | 0.6099 |
| 乘性偏差 10% | (1.1)\,\epsilon^\star | 0.3439 |
| 加性噪声 0.1 | \epsilon^\star+0.1\,\xi | 0.6128 |
| x_0 常数偏移 0.1 | 先算 \hat x_0,再 +0.1 | 0.6112 |
| t 剖面误差 | \epsilon^\star\cdot r(t),r 用实测曲线 | 0.9693 |
| 线性反馈 c=0.1 | \epsilon^\star+0.1\,x | 0.3370 |
| 超线性外推 c=0.1 | \epsilon^\star+0.1\,x\|x\| | 0.3140 |
没有一种发散。有些让输出偏小,有些让输出偏大(t 剖面那条到 0.9693), 但没有一种能变成 10^{7}。
一个负结果,比一个编出来的正结果有用
我原本以为”模型误差被 DDIM 逐步放大”就是爆炸的原因,还准备画一条 漂亮的指数曲线来解释它。但实测不支持这个说法——上面六种误差注入, 没有一种能复现 10^{7}。
所以本章必须把结论收窄到数据允许的范围:
确定性采样对均匀的模型误差并不敏感,它最多把输出整体推偏。
学出来的模型在 100 步下炸到 5.4\times10^{7},这件事不能用 “模型不够准”来解释。它需要模型具有某种特定的错误结构。
那这种结构是什么?本章能给的最强线索是 §6.2 测到的轨迹: 第 0 步时 |\hat x_0| 的均值是 12.73,87.6% 的样本撞上了 clamp; 到第 20 步,|x| 已经是 21.6。也就是说,采样器一旦走出模型训练时见过的范围, 模型的输出就不再是”稍微偏一点”,而是完全失控——这正是”外推”而不是”偏差”。
要把它坐实,需要一个能复现爆炸的误差模型。本书没有做到。 这是一个诚实的缺口,也是留给读者的一个可做的实验: 你能不能构造一种误差结构,让 DDIM 在 100 步下复现 10^{7}?
这一节真正要你记住的是
“模型准不准”不是一个标量。
均匀偏差只把输出整体拉小,而且步数越多越收敛到”被拉小后的那个值”—— 它是个系统性偏移,不是不稳定性。
判断你的模型是”偏”还是”不稳”,看两个量: 偏差随步数是收敛还是发散; 以及采样轨迹上 clamp 命中率——命中率高,说明采样器已经在模型的知识边界之外。
6.9 中间地带:\eta 到底在 ODE 和 SDE 之间做了什么
本章把 \eta=0 叫”确定性”、\eta=1 叫”DDPM”,但中间那一段一直没讲清楚。 这一节补上。
6.9.1 每一个 \eta 都对应一个 SDE
回忆 \sigma 族(§6.3):
q_\sigma(x_{t-1}\mid x_t,x_0)=\mathcal{N}\left(\sqrt{\bar\alpha_{t-1}}x_0 +\sqrt{1-\bar\alpha_{t-1}-\sigma_t^2}\,\varepsilon,\ \sigma_t^2I\right)
取 \sigma_t^2=\eta^2\tilde\beta_t,就得到一族采样过程。
在连续时间的极限下,这一族对应一族 SDE:
dx=\left[f(x,t)-\frac{1+\eta^2}{2}g(t)^2\nabla\log p_t(x)\right]dt+\eta\,g(t)\,d\bar w
检查两个端点:
- \eta=0:dx=\left[f-\frac12g^2\nabla\log p\right]dt —— 正是概率流 ODE
- \eta=1:dx=\left[f-g^2\nabla\log p\right]dt+g\,d\bar w —— 正是反向 SDE
而中间值给出的,是一族”部分随机”的过程,它们的边缘分布完全相同。
它说明”确定性 vs 随机”不是一个二元选择,是一个连续谱。
而谱上的每一点,都给出同一个分布。
这在数学上很漂亮,在工程上很实用: 你可以根据”模型有多准”来选 \eta(本章的决策树), 而不用担心”选了 \eta=0.5 会不会采样到错的分布”——不会。
唯一的代价是:\eta>0 时,同样的步数下有效信息更少 (一部分步数用来”重新加噪”),所以质量会略降,但稳定性会升。
这就是本章那个 \eta 扫描表的全部内容。
6.9.2 一个容易混淆的地方
“\eta 连续变化”不等于”结果连续变化”。
本章的 50 步扫描:\eta=0 时 max 是 1.33\times10^3, \eta=0.5 时是 13.47,\eta=1 时是 1.18。 从 0 到 0.5 降了两个数量级,从 0.5 到 1 只降了一个。
为什么? 因为 \eta 的作用不是线性的——它控制的是 “每步注入的噪声能否把 x 拉回训练分布”。 一旦这个机制开始生效(\eta 超过某个阈值),再加就只是边际收益。
这就是”临界 \eta“的存在。 本章没有精确测出它, 但 50 步的数据显示它落在 (0,\,0.5] 之间。
6.9.3 实践上怎么选 \eta
给一个粗糙但可用的规则:
| 模型相对误差 | 建议 \eta | 理由 |
|---|---|---|
| < 3% | 0 | 确定性收益最大,误差不足以引发不稳定 |
| 3–8% | 0.3–0.5 | 开始需要噪声吸收误差 |
| 8–15% | 0.7–1.0 | 稳定性优先 |
| > 15% | 1.0 + 修模型 | 采样器只能兜底 |
这个表是从本章的实测里外推的,不是定理。 它的用法是”给一个起点”,不是”给一个答案”。
验证方法:在 25 / 50 / 100 三个步数下各扫一遍 \eta。 如果你看到”\eta=0 在某个步数下突然炸”,就把 \eta 提到它上面一档。
6.10 一次完整的排查记录
前面讲的是”应该怎么查”。这一节是”实际怎么查的”。
我把它写出来,是因为教科书里的推导永远是对的, 而真实排查里有一半时间花在”走错路又走回来”。
6.10.1 第一步:先确认它是真的炸了
第一反应不是查代码,是确认”炸”这件事本身。
我把 100 步的输出打印出来:std [5.37\times10^{7},\,1.25\times10^{8}], 最大绝对值 4.97\times10^{9}。
数据的尺度是多少? 双月分布,标准差 [0.5707,\,0.3351]。
所以输出比数据大了八个数量级。 这不是”质量差”,是”数值上完全错了”。
这里有一个小的决定:要不要先怀疑”是不是我打印错了”? 我检查了:describe() 里用的是 x.std(0),和训练时的统计方式一致。 排除了。
6.10.2 第二步:降低步数,看它是不是”一直坏”
如果 1000 步、500 步、250 步全是 nan,而 100 步是 5e7、50 步是 13.8、25 步是 0.53 ——这是一条非常干净的单调曲线。
它告诉我两件事:
- 不是随机故障(否则不会有单调性)
- 不稳定是逐步累积的(步数越多越糟)
“单调”这个性质,比任何单个数字都有信息量。 它直接把”某一步的偶发溢出”排除掉了。
6.10.3 第三步:怀疑公式(走错的第一条路)
我做的第一件事,是把 DDIM 的更新式和论文逐项对照。
五个量、两次代入、一次展开。对完发现没错。
然后我做了一件后来觉得很有用的事:把 \hat x_0 代回去展开, 看它和 DDPM 的均值是不是同一个。
是同一个。 两条路只在”加不加噪声”上分岔。
这一步的收获不是”公式对了”,是”我知道公式和 DDPM 的关系了”。 这个理解在后面的推导里一直在用。
6.10.4 第四步:怀疑 clamp(走错的第二条路)
clamp 是最可疑的——因为它是一个”人为的、没有理论依据的”操作。
我把 clamp 去掉重跑。
结果更糟:连 25 步那一档也炸了。
这个结果当时很反直觉:一个”防止爆炸”的操作,去掉之后更糟—— 说明它不是在制造问题,是在掩盖问题。
这条弯路的价值:它把 clamp 从”嫌疑犯”变成了”证人”。 后面测 clamp 命中率(86.5%)时,我已经知道它在帮忙了。
6.10.5 第五步:停下来,问一个不同的问题
前四步都在问”哪里写错了”。第五步换了问题:
“如果我的代码是对的,那什么情况下它会给出这个结果?”
这个问题把我从”读代码”推到了”设计实验”。
如果代码对、模型对,DDIM 应该给出 0.63 左右。 如果代码对、模型错,它会给出什么? —— 这正是要测的。
6.10.6 第六步:换完美去噪器(决定性的一步)
我写了一个十行的解析去噪器:在高斯数据下, \mathbb{E}[x_0\mid x_t] 有闭式解。
同一个 ddim() 函数,只换 eps_fn。
结果:1000 步到 20 步全部正常。
这一刻,问题的范围从”整个系统”缩到了”模型”。
这是整个排查里唯一一次”一次性排除二十个假设”的时刻。 前面五步加起来,排除的假设不超过五个。
6.10.7 第七步:解释它(这里开始出现错误)
知道”是模型”之后,我写了一个解释:
模型有 8% 的误差 → DDIM 乘 157 倍 → clamp 截断 → 正反馈 → 指数增长
这个解释听起来很顺。我把它写进了草稿,并且准备就这样交稿。
然后我决定验证它。
6.10.8 第八步:验证失败(本章最重要的一步)
验证方法:如果”误差大小”是原因,那给完美去噪器注入误差,应该能复现爆炸。
我试了六种:乘性、加性、x_0 偏移、t 剖面、线性反馈、超线性外推。
没有一种发散。
最刺眼的是第一张表:偏差从 0% 加到 10%,输出从 0.6099 掉到 0.3431—— 它只是变小,从来没有变大。
然后我意识到一件事:正文里那句”误差被放大 157 倍”没错, 但”放大”不等于”发散”。 1/\sqrt{\bar\alpha_t} 放大的是单步的估计误差, 而 DDIM 的更新是收缩的——它会把放大后的误差重新压回去。
真正发散的,是当模型本身变成了一个增益大于 1 的放大器时。 这需要 \partial\varepsilon_\theta/\partial x>1,而不是”误差大”。
6.10.9 第九步:回到轨迹,找到了 1.9
我去重新测了轨迹,并且加了两个量:\|\varepsilon_\theta\| 和 \|x\| 的比值, 以及 clamp 命中率。
比值稳定在 1.9。
这就是那个”增益大于 1”。 而它只在 |x| 很大时出现—— 模型在分布外退化成了 \varepsilon\approx1.9x。
这条因果链和第七步那条不一样:
- 第七步:误差大 → 放大 → 爆炸(错的,被实验否定)
- 第九步:走出分布 → 模型变成放大器 → 每步乘 1.9 → 爆炸(有数据支持)
区别在于:前者把”误差”当成标量,后者把”误差”当成函数。
6.10.10 第十步:把错误留在书里
最后一步不是技术性的,是写作上的决定。
我本可以把第七步那段删掉,直接写第九步。 但那样你会以为”想清楚因果链”是一件一次成功的事。
它不是。 它是”提出解释 → 设计实验 → 被否定 → 修正解释”的循环。 而本章的案例,正好把这个循环完整地走了一遍。
所以我把失败的那一版留在了 §6.8 的 callout 里, 并且明确写了”本书没有做到”。
第五步:“如果我的代码是对的,那什么情况下它会给出这个结果?”
前面四步都在”找错误”,第五步在”构造一个能产生这个现象的世界”。
这两种思维方式的分界线,是”会不会做对照实验”。
找错误:读代码、猜、改一行、再跑 —— 收敛慢,而且永远不知道什么时候该停。 构造世界:设计一个”如果假设成立,就应该看到 X”的实验 —— 一次实验能排除一整类假设。
本章的案例之所以值得写,不是因为它难,是因为它把这个转折点暴露得很清楚。
6.11 这条路走到头了吗
本章讲了六种”让采样更快”的办法。这一节问一个更大的问题: 这条路的极限在哪?
6.11.1 一个数值分析的答案
任何 ODE 求解器的误差,都可以拆成两部分:
\text{总误差}\;\le\;\underbrace{C_1 h^{p}}_{\text{截断误差}}\;+\;\underbrace{C_2\,\|f\|\,e^{L T}}_{\text{稳定性}}
其中 h 是步长、p 是阶数、L 是 f 的 Lipschitz 常数。
对扩散 ODE:
- p 可以由高阶方法提高(欧拉 1 → Heun 2 → 三阶)
- L 由模型决定,采样器改不了
- T 是积分的”时间跨度”,在 \sigma 空间里是 \log\sigma_{\max}-\log\sigma_{\min}
注意第三项:T 是对数跨度,\log(152/10^{-4})\approx14.2。 而 L 随模型误差增大而增大。
所以:
\text{所需步数}\;N\;\gtrsim\;L\cdot T\cdot\left(\frac{C}{\text{容差}}\right)^{1/p}
模型越差(L 越大),需要的步数越多。 这就是”模型误差和采样步数绑定”的数学表达。
它解释了本章的两个观察:
为什么步数少时高阶方法救不了你 —— p 变大只改善 (C/\text{容差})^{1/p} 这一项, 但 L\cdot T 是乘在前面的。 如果 L 很大,再高的阶数也是杯水车薪。
为什么”\log\sigma 网格”有效 —— 它把步长按 \sigma 分配,让有效 h 在 f 变化快的地方变小, 相当于局部地降低了 L\cdot h。
而”蒸馏”做的是另一件事:它把 T 直接压成 0—— 不是”用更少的步解 ODE”,是”不解 ODE”。
6.11.2 三条出路
如果”步数 ∝ L\cdot T“,那么加速只有三条路:
① 提高阶数 p —— 本章 §6.5、§6.16 讲的所有方法。 收益是 p 次根号级别的,边际递减。
② 降低 L —— 也就是”让模型更平滑”。 蒸馏、一致性模型属于这一类(学生网络被训练成”从任意点直接到终点”, 它的有效路径短,所以 L\cdot T 小)。
③ 降低 T —— 改路径。 流匹配、整流流把弯曲的路径拉直,等价于把有效跨度缩短。
本章讲的所有方法,都能放进这三条里。
这也解释了为什么”更好的采样器”的收益会饱和: 它只能在①上做文章,而①的上限由 L 决定。
6.11.3 一个悲观的推论
如果模型误差是固定的,那么采样步数有一个下界。
本章的实测给了这个下界的量级: 完美去噪器(L\approx0)下,一阶需要约 100 步到 2%; 二阶需要 20 步。而学出来的模型,100 步还在炸。
这不是”采样器不够好”,是”模型不够好”。
所以本章最后一句话是:
当一个领域开始反复发表”又快了一点”的采样器时, 往往说明真正的瓶颈已经不在采样器上了。
这句话不是本章的原创,它是很多领域的共同经验。 但在扩散模型上,它有一个特别清楚的数值表达:L\cdot T 那一项。
假设你有一个模型,DDIM 50 步能用、100 步炸。
现在给你两个选项:
- A:把采样器换成二阶,50 步 → 25 步,质量不变
- B:多花一周把模型训好,误差从 8% 降到 3%
哪个更划算?
用本章的框架分析:
- A 的收益是常数倍(2×),而且它不改变 100 步会炸这件事
- B 的收益是改变 L,而 L 进入的是 N\gtrsim L\cdot T 这个下界
如果 B 能让 50 步变成”稳定可用”,那它同时解决了两个问题 (速度和稳定性)。
但如果 B 要多花一个月,而 A 只要一天——A 更划算。
这道题没有标准答案。它要你建立的判断是: “加速”有两种,一种降低常数,一种降低下界。先问清楚你买的是哪一种。
6.12 为什么用二维数据
这是最常被问的一个问题,值得单独回答。
问法通常是:“二维双月太玩具了,结论能迁移到图像上吗?”
答案是:能迁移的那部分,恰好是本章想讲的那部分;不能迁移的那部分,本章没讲。
6.12.1 二维提供了什么
三样东西,在图像上都拿不到:
① 精确的分布。 双月分布的密度有闭式表达。 你可以算出它的真实标准差、真实分位数、真实 KL 散度。 在图像上没有这个 —— 你只有 FID,而 FID 是一个代理指标。
② 精确的最优去噪器。 高斯数据下 \mathbb{E}[x_0\mid x_t] 是解析的。 这就是”完美去噪器”对照实验的全部基础。 在图像上,你永远不知道”完美去噪器”长什么样。
③ 完全的可视化。 两维可以画出来。你能看见分布塌成一个点、 或者炸成一条线。 在图像上,你只能看”像不像”。
这三样东西,共同支撑了本章最核心的一个实验: 同一个采样器,换掉去噪器,七个数量级的差别。 这个实验在图像上做不出来,因为你没有一个”已知正确”的去噪器。
6.12.2 二维不能提供什么
① 真实的模型误差结构。 本章最大的未解问题(§6.34 问题二),正是被这一点卡住的。 一个 5 万参数的模型在二维上的失败方式,不一定代表一个 8 亿参数的模型在图像上的失败方式。
② 高维的几何。 图像的”分布外”和二维的”分布外”不是一回事。 高维空间里,所有的点都远离彼此,模型的外推行为和低维完全不同。
③ 计算上的真实性。 二维上 1000 步是几秒钟的事。 图像上,你不会为了调一个采样器跑 1000 步。
6.12.3 所以本章的结论应该怎么用
按”可迁移性”分三档:
| 结论 | 可迁移性 | 理由 |
|---|---|---|
| DDIM = σ 空间欧拉 | 完全 | 纯数学,与维度无关 |
| \log\sigma 网格优于 σ 均匀 | 高 | 数值性质,不依赖数据 |
| 二阶在 20 步左右最划算 | 中 | 依赖模型的误差水平 |
| 均匀偏差不会发散 | 中 | 依赖误差的定义方式 |
| \varepsilon\approx1.9x 的放大器机制 | 低 | 这是那个特定模型的行为 |
| 25 步正常、100 步炸 | 低 | 特定模型的特定失败 |
前两行你可以直接用。后两行当成”一个可能的模式”,不要当成定律。
问自己:这个结论里,有没有出现”模型”这个词?
- 没有 → 它是数学或数值性质,可以迁移
- 有,但只是”模型要够准” → 半可迁移,取决于你的模型有多准
- 有,而且是”这个模型的具体行为” → 不可迁移,只能当参考
本章的 §6.3–§6.6 属于第一类,§6.7、§6.9 属于第二类,§6.2 属于第三类。
这也是为什么本章花那么多篇幅在数学上 —— 因为只有那部分,是你在自己的模型上可以无条件相信的。
6.13 它在哪里失效
这一节把本章所有的失败模式写成”可复现实验”。
每一条都按同一个格式给:材料 → 步骤 → 预期 → 实测 → 判据 → 修法。 你的环境里跑不出”实测”那一列的数不重要——重要的是症状对得上, 因为判据是根据症状设计的,不是根据具体数值。
6.13.1 失败一:确定性采样 + 不完美的模型
这是本章的案例。
材料:第 5 章那个 5 万参数的双月模型;一个解析的完美去噪器 (gaussian_eps_star,十行代码)。
步骤: 1. 用学出来的模型跑 DDIM,\eta=0,步数取 \{1000,500,250,100,50,25\} 2. 不改任何一行采样代码,把 eps_fn 换成解析去噪器,重跑同一组步数 3. 记录输出标准差与最大绝对值
预期:如果代码有 bug,两个版本都会炸;如果模型有问题,只有第一个炸。
实测:
| 步数 | 学出来的模型(\eta=0) | 完美去噪器(\eta=0) |
|---|---|---|
| 1000 | nan |
0.6232 |
| 500 | nan |
— |
| 250 | nan |
0.6188 |
| 100 | [5.37\times10^{7},\,1.25\times10^{8}] | 0.6099 |
| 50 | [13.83,\,32.10] | 0.5951 |
| 25 | [0.5328,\,0.3398] | 0.5656 |
判据:两列的差别达到七个数量级。代码没错,是模型。
但注意,这张表不能读成”模型越不准越容易炸”。 实验 6-2 已经证明:均匀的误差只会让输出塌缩,不会让它爆炸。 这里的问题不是”准不准”这个标量,是模型输出的结构 (分布外时 \varepsilon_\theta\approx1.9x,见 §6.2)。
修法:先做这个对照实验,再决定是改代码还是改模型。 在没有做这个对照之前,所有的排查都是在猜。
6.13.2 失败二:把”平均两次”当成二阶
这是我自己踩过的坑,而且它比看上去更隐蔽—— 因为代码跑得通,结果也不炸,只是和一阶逐位相同。
材料:任何一阶 DDIM 实现。
步骤:写出这个”二阶”:
x0_a = solve_x0(x, eps(theta(x, t)), abar[t])
x0_b = solve_x0(x, eps(theta(x, t-1)), abar[t-1])
x0 = (x0_a + x0_b) / 2
x_next = reconstruct(x0, eps(theta(x, t)), abar[t-1])预期:10 步时应该比一阶准。
实测:和纯欧拉逐位相同(差值恰好为 0,不是”很小”)。
为什么? 因为
\sqrt{\bar\alpha'}\,\hat x_0'+\sqrt{1-\bar\alpha'}\,\varepsilon_2=x_{\text{pred}} \sqrt{\bar\alpha'}\,\hat x_0+\sqrt{1-\bar\alpha'}\,\varepsilon_1=x_{\text{pred}}
两个式子右边是同一个 x_{\text{pred}},所以”平均 \hat x_0“再重建, 在代数上恒等于不平均。这不是精度问题,是恒等式。
判据:改一个应该影响很大的设置(10 步 vs 100 步),看输出有没有变。 如果逐位相同,就是没生效。
更强的判据:把”二阶”的两次求值换成同一个 t。 如果结果还是不变,说明第二次求值根本没进入更新式。
修法:二阶要平均的是斜率(\varepsilon),不是位置(\hat x_0)。 正确形式见 §6.5 的 Heun 代码。
6.13.3 失败三:以为”步数越少越安全”
材料:本章的学出来的模型 + 25 步 DDIM。
步骤:跑 25 步,得到 std [0.5328,\,0.3398]; 数据本身是 [0.5707,\,0.3351]。
预期:看起来几乎一样。
实测:它就是错的。 同一份代码在 50 步下 std 是 [13.83,\,32.10], 在 100 步下是 5.4\times10^{7}。
为什么 25 步”对”? 因为不稳定是逐步累积的。 25 步的采样器还没走够步数让正反馈建立起来,它停在了一条错误轨迹的早期。 输出的统计量碰巧接近,不代表轨迹是对的。
判据:同一份代码,至少测两个相差 4 倍以上的步数档位。 如果结论相反,你测到的不是”采样器”,是”这个步数下的巧合”。
修法:把”多档位一致性”作为采样器的验收标准之一。 只在一个步数下好看的采样器,不能交付。
6.13.4 失败四:把 \eta=0 当成”更稳定的选择”
很多教程说”\eta=0 是确定性的,结果可复现,所以更稳定”。 “可复现”和”稳定”是两件事。
材料:本章的学出来的模型;50 步与 100 步各一组。
步骤:固定步数,只扫 \eta\in\{0,\,0.25,\,0.5,\,0.75,\,1.0\}。
实测(100 步):
| \eta | 输出标准差 | 最大绝对值 |
|---|---|---|
| 0 | — | 5.37\times10^{7} |
| 0.25 | — | 2.02\times10^{7} |
| 0.5 | — | 1.17\times10^{7} |
| 0.75 | — | 1.12\times10^{6} |
| 1.0 | — | 6021 |
实测(50 步):
| \eta | 输出标准差 | 最大绝对值 |
|---|---|---|
| 0 | [13.83,\,32.10] | 1.33\times10^{3} |
| 0.5 | [5.84,\,13.47] | — |
| 1.0 | [0.525,\,0.320] | 1.18 |
判据:\eta=0 与 \eta=1 的最大绝对值差三个数量级。 确定性没有带来稳定,它取消了唯一的误差吸收机制。
修法:把 \eta 当作”稳定性旋钮”而不是”随机性旋钮”。 需要可复现时,用固定种子 + \eta=0,但必须同时接受”模型误差零容忍”。 做不到,就放弃可复现。
6.13.5 失败五:用 clamp 掩盖问题
材料:100 步 DDIM,学术模型,clamp=3.0。
步骤:在每步记录三个量: |x0_hat| 的均值、clamp 的命中率、以及去掉 clamp 之后的结果。
实测: - 第 0 步(t=999):平均 |\hat x_0|=12.8,命中率 86.5% - 第 10 步(t=898):命中率 57.8% - 第 20 步(t=797):命中率 13.2% - 去掉 clamp:连 25 步那一档也炸了
判据:命中率 > 50% 就是红灯。 它说明 \hat x_0 的估计已经不可用——不是 clamp 截得太狠,是上游已经错了。
修法:clamp 保留(它确实在帮忙),但把命中率作为监控指标。 命中率持续高,说明模型在 t 大的区间不准,该去查训练,不是查采样器。
6.13.6 失败六:训练与采样的调度不一致
这个坑和采样器无关,但经常被误诊成采样器问题。
材料:任何训练/采样分两个模块写的代码库。
步骤: 1. 在训练脚本里打印 alpha_bar[500] 和 beta[:3] 2. 在采样脚本里打印同样两个量 3. 逐位比较
预期:完全相同。
常见的不一致: - 训练用 torch.linspace(1e-4, 0.02, 1000),采样用 torch.linspace(1e-4, 0.02, 999) - 训练在 CPU 上算(float64),采样在 GPU 上算(float32) - 采样脚本里 make_schedule(cfg) 被调用了两次,中间 cfg 被改过
实测:本书没有对这一条做定量测量——它一旦发生就是灾难性的, 没有”程度”可言。这里给的是排查动作,不是一个数。
判据:两个模块打印的 \bar\alpha_{500} 必须逐位相等。 不相等,后面所有比较都没有意义。
修法:把 schedule 序列化存盘,训练和采样都从同一个文件读。 不要在两边各自构造。
失败六。 因为它最便宜(打印两个数),而且它的后果最严重 (所有后续实验都作废)。
然后是失败二(逐位相同 → 二阶没生效), 再是失败一(换完美去噪器), 最后才是失败三、四、五——它们需要多档位、多参数的扫描。
这个顺序和”排查成本”一致,和”问题严重程度”无关。 先排除”便宜的致命错误”,再处理”昂贵的细微差异”。
6.14 上手实现:一个能用的采样器模块
6.14.1 为什么单独写一个模块
第 5 章的 sample() 只有 13 行,它只做一件事:跑完 DDPM。 本章要比较 DDPM、DDIM、Heun 三种采样器,还要能调 \eta、能算函数求值次数。 把这三种写成一个统一的接口,比较才是公平的。
下面这份代码在 notebooks/samplers.py 里,本章所有的实测都用它。
6.14.2 接口设计
三种采样器共享同一个签名:
def sample(model, sch, x, *, steps, eta=0.0, order=1) -> tuple[Tensor, int]返回值里带上”函数求值次数” —— 这是公平比较的关键。 Heun 走 20 步要调 40 次模型,DDIM 走 20 步只调 20 次。 不比这个数,“二阶更快”就是一句空话。
6.14.3 第一部分:统一的 sigma 参数化
def sigma_of(abar):
"""σ = √(1-ᾱ)/√ᾱ,把"信号+噪声"的混合写成 x̂ = x₀ + σ·ε。"""
return ((1 - abar).clamp_min(1e-12) / abar.clamp_min(1e-12)).sqrt()这个换元是三种采样器能统一的原因。
在原始参数化下,DDPM 更新 x_t,DDIM 更新 \hat{x}_0,Heun 要更新”斜率”—— 三个不同的对象。换到 \hat{x}=x/\sqrt{\bar\alpha} 之后,三者都变成 “沿着 \sigma 积分同一个 ODE”,只是离散化的方式不同。
验证一下这个换元:x_t=\sqrt{\bar\alpha_t}x_0+\sqrt{1-\bar\alpha_t}\varepsilon, 两边除以 \sqrt{\bar\alpha_t}:
\hat{x}_t = x_0 + \frac{\sqrt{1-\bar\alpha_t}}{\sqrt{\bar\alpha_t}}\varepsilon = x_0 + \sigma_t\varepsilon \quad\checkmark
它的好处:\hat{x} 的”信号部分”恒为 x_0,不随 t 变。 所以 \hat{x} 随 \sigma 的变化率就是 \varepsilon —— 这正是我们要积分的那个 ODE。
6.14.4 第二部分:欧拉(= DDIM)
def euler(model, sch, x, steps, eta=0.0):
idx = torch.linspace(len(sch["abar"]) - 1, 0, steps).round().long()
a0 = sch["abar"][idx[0]]
xhat = x / a0.sqrt() # 换元到 x̂ 空间
nfe = 0
for i in range(len(idx) - 1):
a, ap = sch["abar"][idx[i]], sch["abar"][idx[i + 1]]
s, sp = sigma_of(a), sigma_of(ap)
eps = model(xhat * a.sqrt(), torch.full_like(idx[:x.shape[0]], idx[i]))
nfe += 1
step = sp - s
if eta > 0:
# η>0 时注入噪声:这是"退回 DDPM"的那一项
sig = eta * (sp ** 2 - s ** 2).clamp_min(0).sqrt()
xhat = xhat + step * eps + sig * torch.randn_like(xhat)
else:
xhat = xhat + step * eps
return xhat, nfe三处值得停一下。
① sp - s 是负的。 因为我们在倒着走 \sigma(从大往小)。 所以 \hat{x} 是在”减”,不是”加”。写成 + step 是对的,因为 step 本身是负数。
② 换元之后调用模型时要把 x 换回去。 model(xhat * a.sqrt(), ...) —— 模型是在原始 x_t 上训练的,不是在 \hat{x} 上。
③ \eta>0 那一项的形式。 \sigma_t^2-\sigma_{t-1}^2 是”两个噪声水平之间的方差差”, 开方就是需要补的噪声幅度。\eta=1 时它正好退回 DDPM。
6.14.5 第三部分:Heun
def heun(model, sch, x, steps):
idx = torch.linspace(len(sch["abar"]) - 1, 0, steps).round().long()
xhat = x / sch["abar"][idx[0]].sqrt()
nfe = 0
for i in range(len(idx) - 1):
a, ap = sch["abar"][idx[i]], sch["abar"][idx[i + 1]]
s, sp = sigma_of(a), sigma_of(ap)
k1 = model(xhat * a.sqrt(), torch.full_like(idx[:x.shape[0]], idx[i]))
nfe += 1
pred = xhat + (sp - s) * k1 # 欧拉预测
k2 = model(pred * ap.sqrt(), torch.full_like(idx[:x.shape[0]], idx[i + 1]))
nfe += 1
xhat = xhat + (sp - s) * (k1 + k2) / 2 # 平均
return xhat, nfe和上一版”假的二阶”的区别在哪?
关键在于 pred 是直接在 \hat{x} 空间里做欧拉步,而不是”先解出 \hat{x}_0 再重建”。
那个假版本的错误在于:它把 \hat{x}_0(一个”数据估计”)和 \hat{x}_0'(另一个时刻的数据估计) 平均,而这两个量物理上不可比。 正确做法是平均”斜率”(\varepsilon),不是平均”位置”(\hat{x}_0)。
6.14.6 十二个坑:从”能跑”到”能用”
上面的代码是能跑的最小版本。 但把它放进真实项目, 你会遇到下面这些。每一条都附了”症状”,方便你反查。
坑 1:把 alpha_bar 的下标当成时间步。
症状:训练正常,采样在 t 很小时输出无意义。
原因:第 5 章用的 \bar\alpha_t=\prod_{s=1}^{t}(1-\beta_s) 是从 1 开始的, 而 PyTorch 的张量是 0-indexed。abar[t] 和 abar[t-1] 差一个 \beta, 在 t=999 处无所谓,在 t=1 处就是天壤之别。
修法:全章统一用”t 从 1 数、数组下标 t-1“,并在函数入口断言。
坑 2:训练用 \varepsilon,采样时忘了模型输出的是 \varepsilon。
症状:loss 收敛得很好,采样出来是灰噪。
原因:代码里写成了 x0 = (x - (1-a).sqrt()*eps)/a.sqrt() 但是把 eps 换成了 model(x,t) 的输出——如果模型被训练成预测 x_0,这一步就错了。
修法:在模型类里加一个字段 prediction_type,采样器读它决定怎么解释输出。
坑 3:\sigma 的两种定义混用。
症状:\eta>0 时输出方差明显不对,\eta=0 时却正常。
原因:本章用的 \sigma_t=\sqrt{1-\bar\alpha_t}/\sqrt{\bar\alpha_t} 是”信噪比倒数”, 而 DDIM 论文里的 \sigma_t 是”注入噪声的幅度”。 两者都叫 \sigma,公式长得也像。
修法:给它们不同的名字。本书统一叫 sigma(SNR 那个)和 eta_scale(注入那个)。
坑 4:torch.linspace 在 t 上取点,却当成 \sigma 网格用。
症状:步数越少越糊,加到 100 步突然好转。
原因:本章 §6.5 那张表:\sigma 均匀 10 步是 0.0228,\log\sigma 均匀是 0.4201。 差 18 倍。
修法:在 \log\sigma 上取点,或者至少验证你的调度下 “t 均匀”是否接近”\log\sigma 均匀”。
坑 5:clamp 的位置放错。
症状:clamp 了但没效果,或者 clamp 之后反而更糟。
原因:本章的 clamp 是作用在 \hat x_0 上的(截断”数据估计”), 不是作用在 x_t 上的。加错位置会直接破坏前向过程的方差。
修法:clamp 只在”从 x_t 解出 \hat x_0“之后、重建 x_{t-1} 之前。
坑 6:\eta 的公式少开一个根号。
症状:\eta=1 时输出比 DDPM 更模糊,方差偏小。
原因:注入的噪声幅度是 \sigma_{\text{inj}}=\eta\sqrt{\sigma_t^2-\sigma_{t-1}^2}, 是方差差的开方,不是方差差。 而且重建时 \varepsilon 前面的系数要用 \sqrt{\sigma_{t-1}^2-\sigma_{\text{inj}}^2},不是 \sigma_{t-1}。
修法:\eta=1 时必须逐位复现 DDPM 的采样结果(同样的种子)。 这是最好用的单元测试。
坑 7:把”步数”和”NFE”混为一谈。
症状:和别的方法比较时结论相反。
原因:Heun 每步 2 次,三阶每步 3 次,某些方法末步还要多一次。
修法:在采样函数里返回一个 nfe 计数器,所有对比表用它排序。
坑 8:忘了固定随机种子,却以为是确定性问题。
症状:\eta=0 时同一个输入仍然每次不同。
原因:dropout 没关、数据加载器顺序没固定、或者 torch.randn 在别处被调用。
修法:model.eval() + torch.no_grad() + 显式 torch.manual_seed()。
坑 9:模型在 train() 模式下被采样调用。
症状:单张图正常,批量时结果和单张不一致。
原因:BatchNorm / Dropout 在训练模式下的行为不同。
修法:采样函数入口 model.eval(),退出时恢复。
坑 10:潜空间模型忘了缩放因子。
症状:潜空间采样结果解码后颜色发灰或过饱和。
原因:Stable Diffusion 的 VAE 有一个 scaling_factor(通常 0.18215), 训练和推理必须用同一个。 忘了它,\hat x_0 的尺度就是错的。
修法:把缩放因子存进模型元数据,采样器从模型读,不要硬编码。
坑 11:调度在训练和采样之间被”重建”。
症状:训练 loss 正常,采样分布明显偏移。
原因:make_schedule(cfg) 被调用了两次,而 cfg 在两次调用之间被改过 (或者 beta 的计算用了不同的 device/dtype)。
修法:把 schedule 作为参数传进采样器,不要在采样器内部重建。
坑 12:用最终结果判断采样器好坏,不看轨迹。
症状:知道”炸了”,但不知道从哪一步开始。
原因:只存了 x_final。
修法:在循环里记 (step, t, |x|.mean(), |eps|.mean(), clamp_hit_rate)。 本章能找到”第 20 步开始失控”,全靠这一行。
坑 3(\sigma 的两种定义)、坑 6(\eta 的根号)、坑 12(轨迹记录)。
前两个是因为记号冲突——它们是”读论文时不会有问题、 写代码时一定会遇到”的那类错误。唯一的解法是给每个量起一个不会混淆的名字。
第三个不是 bug,是工作习惯。它决定了你能否从”炸了”走到”为什么炸”。
欧拉法在位置空间里更新,\hat{x} \leftarrow \hat{x} + h\,k。
二阶要修正的是 k,不是 \hat{x}。
把两个”位置估计”平均起来,在数学上没有任何依据 —— 而在某些参数化下,它恰好等于不修正。
6.14.7 第四部分:一个公平的基准
比较采样器时最容易犯的错,是只比步数、不比求值次数。
def benchmark(model, sch, data, configs):
rows = []
for name, fn, steps in configs:
torch.manual_seed(0)
x = torch.randn(2000, 2, device=DEV)
out, nfe = fn(model, sch, x, steps)
rows.append({
"sampler": name, "steps": steps, "nfe": nfe,
"std": out.std(0).tolist(),
"mmd": mmd_rbf(out.cpu(), data).item(),
})
return rows注意 torch.manual_seed(0) 在循环里 —— 每个配置从同一批初始噪声出发。 这是唯一变量原则在采样器比较里的落实。
6.14.8 实测:三种采样器,按求值次数排在一条线上
| 采样器 | 步数 | 求值次数 | 输出 std | MMD |
|---|---|---|---|---|
| DDPM | 1000 | 1000 | [0.5598,\ 0.3437] | 0.01146 |
| DDIM | 25 | 25 | [0.5328,\ 0.3398] | 待测 |
| DDIM | 100 | 100 | 发散 | — |
| Heun | 20 | 40 | 待测 | 待测 |
(Heun 那一行是本章要补的实测。)
读这张表的正确方式:把”求值次数”那一列当作横轴。 DDIM 25 步只花 25 次求值就接近了 DDPM 1000 次的结果 —— 这就是它值得用的理由。
而它的代价在本章已经反复出现:确定性让它对模型误差零容忍。
6.15 采样器怎么选:一棵决策树
这一节把全章的内容压成一个可以照着走的选择流程。
6.15.1 先问三个问题
问题一:你的模型有多准?
判据:在验证集上,\varepsilon_\theta 的相对误差大约是多少? (可以这样量:\dfrac{\|\varepsilon-\varepsilon_\theta\|^2}{\|\varepsilon\|^2}。)
- 误差 < 5%:确定性采样可以用
- 误差 5–15%:建议 \eta\geq0.5
- 误差 > 15%:先把模型训好,任何采样器都救不了
本章的模型约 6.6%(t=999 处,实测),落在”建议加噪声”那一档, 而它的实测表现也确实是”\eta=0 炸、\eta=1 好”。
问题二:你需要确定性吗?
- 需要(编辑、插值、可复现的演示)→ \eta=0,但要接受对模型误差的零容忍
- 不需要(只是生成样本)→ \eta 可以取 0.5–1,稳得多
问题三:你的算力预算允许多少次函数求值?
- < 30 次:只能用一阶方法,接受质量损失(实测 20 步掉 13%)
- 30–100 次:二阶方法开始划算(20 步 × 2 = 40 次,误差 2%)
- > 100 次:一阶和二阶差别不大,选简单的
6.15.2 一棵决策树
模型相对误差 ε_rel
├─ > 15% → 回去训模型。采样器换什么都没用。
├─ 5–15% → η 取 0.5–1.0
│ ├─ 求值预算 < 50 → 一阶 + 大 η(本章实测:50 步 η=1 正确)
│ └─ 求值预算 ≥ 50 → 一阶 + 中等 η
└─ < 5% → η 可以取 0
├─ 需要确定性 → DDIM / 一阶
├─ 求值预算 < 50 → 二阶(Heun)
└─ 求值预算 ≥ 50 → 一阶够用
这棵树不是从论文里抄的,是从本章的实测里长出来的。 每一条分支背后都有一个具体的数字。
6.15.3 三个反直觉的结论
① “步数越多越好”是错的。
本章的案例里,25 步正常、100 步爆炸。 步数只在”模型够准”的前提下才是越多越好。
② “确定性更稳定”是错的。
确定性 = 可复现;不等于稳定。 恰恰相反:确定性采样没有吸收误差的机制。
③ “二阶一定比一阶好”是错的。
在 10 步这个量级,Heun 反而”过头”(+7.5%),Euler 是 −23%。 高阶的优势在中等步数,不在极端少的步数。
6.15.4 三个真实场景:把决策树走一遍
抽象的分支不好用。下面三个场景,每个都给出具体的选择和理由。
6.15.4.1 场景 A:图生图编辑,要求可复现
约束:用户上传一张图,改一句话,要生成结果。 同一个输入必须给出同一个输出(否则用户会以为”每次都不一样是 bug”)。 预算:30 次函数求值以内。模型相对误差约 5%。
走树:误差 5% 落在边界上 → 先试 \eta=0; 需要确定性 → \eta=0;预算 30 → 一阶或二阶都可以,但二阶要 15 步。
推荐:\eta=0,20 步,一阶(20 NFE)。
理由:5% 是本章实测的”\eta=0 还能用”的上界附近。 如果上线后发现伪影,第一个动作不是换采样器,是把 \eta 提到 0.3 —— 代价是可复现性,但换来稳定性。
注意:图生图还有另一个约束——初始噪声来自原图,不是随机的。 这意味着 \eta>0 会破坏”保留原图结构”这件事, 所以这类应用里 \eta 的可用范围比纯文生图窄得多。
6.15.4.2 场景 B:批量生成训练数据,不需要可复现
约束:一晚生成 10 万张,质量优先,多样性优先。预算 50 NFE。模型误差约 3%。
走树:误差 3% → \eta 可以取 0;不需要确定性; 预算 50 → 一阶够用。
推荐:\eta=0,50 步一阶(50 NFE)。
理由:在这个预算下,二阶的收益已经很小 (实测 40 步 Heun 是 0.6375,80 步 Euler 是 0.6163,同成本下 Heun 略好, 但差距小于模型误差本身)。
更重要的是多样性:\eta=0 是确定性的,同一个起点永远给同一个样本。 批量生成时必须换随机种子,否则你会得到一个”看起来很多、实际很少”的数据集。 这是确定性采样在数据生产里的隐藏陷阱。
6.15.4.3 场景 C:手机端实时生成
约束:8 次函数求值以内。模型是小模型,误差可能到 15%。
走树:误差 15% → 落在”先训模型”的边缘; 但端侧模型没法更大,所以只能接受。
推荐:不要用 DDIM。用蒸馏过的一步/四步模型 (本章 §6.16 讲的第三类方法)。
理由:在 8 NFE 的预算下,本章的所有测量都表明 一阶方法会损失 20% 以上的精度,二阶方法还没进入渐近区。 这不是采样器能解决的问题,是”必须换模型”的问题。
很多人会拿着 DDIM 去调 8 步,然后抱怨”质量不行”。
但 8 步 DDIM 的质量上限,不是由采样器决定的,是由 ODE 的刚度决定的 (上一节那张 \log\sigma 网格的表:10 步已经只剩目标的 66%)。
在这个预算下,正确的动作是”换模型”,不是”调采样器”。
判别方法:如果你的预算低于 15 NFE,而模型不是专门为少步训练的, 先去做蒸馏,别在采样器上花时间。
6.15.5 一张”症状 → 处方”表
最后给一张反向查表:从你观察到的现象,倒推该改什么。
| 症状 | 最可能的原因 | 先试什么 | 不要做什么 |
|---|---|---|---|
| 输出全是灰噪 | 步数太少 + 模型误差大 | 提 \eta 到 1 | 加步数 |
| 输出模糊、缺细节 | 网格太粗(\sigma 均匀) | 换 \log\sigma 网格 | 换更高阶方法 |
| 小步数正常、大步数炸 | 模型在分布外放大 | 加 \eta / 换完美去噪器验证 | 继续加步数 |
| 换调度后质量崩 | 网格与新调度不匹配 | 重算 \sigma 网格 | 重训模型 |
| 每次生成都不一样(\eta=0) | 有未固定的随机源 | 检查 dropout / 数据加载顺序 | 换采样器 |
| 多样性差、样本雷同 | 确定性采样 + 固定种子 | 换种子 / 提 \eta | 调温度 |
注意第一行和第三行的区别。 “全是灰噪”通常是精度不足(走得太粗), “小步数正常大步数炸”是稳定性问题(误差被放大)。 两者的处方完全相反 —— 一个要加噪声,一个要减步数。
这一节真正要你记住的是
采样器的选择不是”哪个方法更新”,是”三个约束下的最优解”: 模型精度、确定性需求、算力预算。
脱离这三个约束谈”最好的采样器”,和脱离剂量谈毒性一样没有意义。
6.16 同一族里的其他成员:PLMS、DPM-Solver++、UniPC
本章的正文只实现了三个采样器:DDPM(\eta=1)、DDIM(\eta=0,一阶欧拉)、 Heun(二阶)。但你在实际代码库里会看到一堆别的名字。 这一节把它们的位置标出来,不写公式,只讲”它们各自解决了哪个具体问题”。
6.16.1 PLMS:不要多算,用历史
Heun 的问题是每步算两次。 对于大模型,这意味着显存和时间的双重压力。
PLMS(Pseudo Linear Multistep,2022)的想法是:既然已经算过了,就别再算。
它的做法借用经典数值分析里的线性多步法: 用前几步已经算出的 \varepsilon 值,线性组合出这一步的斜率。
\hat x_{k+1}=\hat x_k+(\sigma_{k+1}-\sigma_k)\sum_{j=0}^{p-1}c_j\,\varepsilon^{(k-j)}
关键是:这些 \varepsilon^{(k-j)} 都是之前已经算过的,不需要新的模型调用。
于是:
| 方法 | 每步 NFE | 阶数 | 额外内存 |
|---|---|---|---|
| DDIM | 1 | 1 | 无 |
| Heun | 2 | 2 | 无 |
| PLMS | 1 | 2 | 存 p 个历史 \varepsilon |
同样的 NFE,二阶。 听起来是免费午餐——代价是”启动开销”: 多步法需要前 p 步的历史,而第一步没有历史,只能用欧拉起步。 所以在总步数很少时(< 10),PLMS 的二阶优势被启动阶段吃掉了。
还有第二个代价:它假设步长是”准均匀”的。 如果网格变化剧烈,多步法的系数要重算,否则会不稳定。
因为它需要改的东西太多,而收益可以在别处看到。
多步法的系数表(Adams-Bashforth 的 c_j)依赖步数 p 和网格, 写成通用代码需要一张系数表和一个”历史缓冲区”。 它的核心思想——“复用历史求值”——一句话就能说完, 而实现细节对理解本章的主线(阶数、网格、稳定性)没有新增信息。
如果你要上手,顺序建议是:先把本章的欧拉和 Heun 写对、 用完美去噪器测过,再加 PLMS。反过来会很难 debug —— 因为多步法的错误表现和网格错误非常像。
6.16.2 DPM-Solver++:换一个预测目标
这个改进针对的是一个具体的失败模式:高噪声区的数值不稳定。
回忆本章的实测:在 t 很大时,\varepsilon_\theta 的误差会被 1/\sqrt{\bar\alpha_t} 放大(t=999 处是 157 倍)。而 DPM-Solver++ 的观察是: 在高噪声区,直接预测 x_0 比预测 \varepsilon 更稳定。
为什么? 因为
\hat x_0=\frac{x_t-\sqrt{1-\bar\alpha_t}\varepsilon_\theta}{\sqrt{\bar\alpha_t}}
当 \sqrt{\bar\alpha_t} 很小时,这个式子是”两个大数相减再除以一个小数”。 \varepsilon_\theta 的一点点误差,经过 1/\sqrt{\bar\alpha_t} 放大, 再和 x_t/\sqrt{\bar\alpha_t} 这个大项相减 —— 灾难性抵消。
如果模型直接输出 \hat x_0,就没有这个除法。 而 \varepsilon 可以由 \hat x_0 反算:
\varepsilon=\frac{x_t-\sqrt{\bar\alpha_t}\hat x_0}{\sqrt{1-\bar\alpha_t}}
分母是 \sqrt{1-\bar\alpha_t}\approx1,不会放大误差。
所以 DPM-Solver++ 的内核是: 把求解器的更新式改写成以 \hat x_0 为变量的形式, 在 \sigma 大的区间用 \hat x_0,在 \sigma 小的区间用 \varepsilon。
这是一个纯粹的数值技巧,不改变 ODE,只改变离散化的写法。 它的收益随步数减少而增大 —— 因为步数越少,每步跨的 \sigma 越大, 抵消问题越严重。
一句话总结 DPM-Solver++
同一个方程,换一个未知量来离散,数值稳定性就不一样。
这和”解线性方程组时选主元”是同一类思想: 数学上等价,数值上差很多。
6.16.3 UniPC:把预测-校正统一起来
UniPC(Unified Predictor-Corrector,2023)做的事情是”归类”。
它指出:PLMS、DPM-Solver、DPM-Solver++ 这些方法, 都可以写进同一个”预测-校正”框架:
- 预测:用历史值外推一个 \hat x^{(0)}
- 校正:用(可选的)新求值修正它
不同的方法,区别只在”用什么公式外推”和”校不校正”。
它的实际价值不是”更快”,而是”让超参数变少”: 在 UniPC 的框架下,阶数、是否校正、末步怎么处理, 变成了几个正交的选项,而不是每个方法一套独立的调参经验。
6.16.4 蒸馏:不修采样器,改模型
上面所有方法都在”更好地解 ODE”。蒸馏换了一个问题: “能不能不让他解?”
核心想法:训练一个学生网络,直接学”从任意 (\hat x,\sigma) 一步到 \hat x_0“。
- 一致性模型(Consistency Models, 2023): 训练 f_\theta(\hat x,\sigma)\approx\hat x_0, 约束是”同一条 ODE 轨迹上的任意两点,映射到同一个 x_0”
- LCM(Latent Consistency Model, 2023): 把上面这件事搬到潜空间,用少量步数(2–4)生成
- SDXL Turbo / 各类加速 LoRA: 用对抗损失或蒸馏损失把步数压到 1–4
代价很明确:
| 训练成本 | 采样成本 | 质量 | 多样性 | |
|---|---|---|---|---|
| 原始模型 + DDIM | 无 | 20–50 NFE | 基准 | 基准 |
| 原始模型 + DPM-Solver++ | 无 | 10–25 NFE | 接近基准 | 接近基准 |
| 蒸馏模型 | 要重训 | 1–4 NFE | 略低 | 明显下降 |
“多样性下降”是蒸馏的结构性问题,不是调参能解决的。 学生网络把一整条轨迹压成一个点,它必然丢掉轨迹上的随机性。
如果你的应用需要”同一个 prompt 生成很多不同的结果”,蒸馏会伤到你。
判别方法:用同一个 prompt、不同的种子生成 20 张, 看它们的差异是不是比你预期的少。 如果少,那就是多样性塌缩。
这时候的正确选择是:用原始模型 + 更好的采样器(DPM-Solver++ 或 UniPC), 接受 15–25 NFE 的成本,而不是用 4 步蒸馏换一个”看起来能用”的结果。
6.16.5 一张归位表
把这一节的名字放回本章的主线:
| 方法 | 它改的是 | 相对本章的哪个概念 |
|---|---|---|
| DDPM | \eta=1 | \sigma 族的右端点 |
| DDIM | \eta=0,一阶 | 欧拉 |
| Heun | 二阶 | 多一次求值换阶数 |
| PLMS | 多步,复用历史 | 同 NFE 下的二阶 |
| DPM-Solver | \lambda 网格 + 高阶 | 换自变量 |
| DPM-Solver++ | 改预测目标 | 数值稳定性 |
| UniPC | 统一框架 | 归类 |
| 蒸馏 / LCM | 改模型 | 跳出”解 ODE” |
注意最后一行。 前七个都在”怎么解这个 ODE”里, 只有最后一行跳出了这个问题。 这就是本章反复强调的那条线:采样器的收益有上限,模型的收益没有。
6.17 噪声调度 × 采样器:两个必须一起调的东西
这一节讲一个经常被忽略的事实:采样器的表现,一半由调度决定。
你在论文里看到一个采样器在某个模型上 20 步出好结果, 换到自己的模型上就不行——很可能不是采样器的问题,是调度变了。
6.17.1 调度到底改了什么
调度就是 \bar\alpha_t 这条曲线。 它决定了三件事:
- \sigma 的范围:\sigma_{\max}=\sqrt{1-\bar\alpha_T}/\sqrt{\bar\alpha_T}
- \sigma 在 t 上的分布:t 均匀时,\sigma 怎么变
- “有效步数”:真正在改变 \hat x 的那些步有多少
第三件事最容易被忽略。
线性 \beta 调度(本章用的那个)在 t 大时 \sigma 变化极快, 在 t 小时变化极慢。后果是:t 均匀取点时,大部分步数落在”\sigma 变化慢”的区间, 而那些步对结果的贡献很小。
余弦调度(cosine schedule)的设计目标就是让 \sigma 的变化更均匀。 所以它和 \log\sigma 网格更匹配,和 t 均匀网格反而不匹配。
6.17.2 一个可以直接测的判据
如果你怀疑”调度和网格不匹配”,做这个实验:
- 固定采样器(比如 DDIM)
- 固定步数(比如 20)
- 只改调度(线性 / 余弦 / 平方根)
- 看输出标准差
如果三个调度的结果差很多,说明网格没有适配调度。
本章没有跑这组完整实验(它需要重新训练,因为模型是在线性调度下训的), 但你可以用一个不需要训练的办法:用完美去噪器, 把 \bar\alpha_t 换成别的调度,跑同一组步数。 这时候模型误差为零,你看到的差异纯粹是”调度 × 网格”的交互。
因为模型是在特定调度下训练的。
\varepsilon_\theta(x_t,t) 的输入是 x_t,而 x_t 的分布由 \bar\alpha_t 决定。 换调度 = 换输入分布 = 模型在训练时没见过这些输入。
这就是”训练/采样不一致”(train-test mismatch)的一种。 它的症状和本章案例一模一样:采样炸,但训练 loss 正常。
所以”换个调度试试”不是一个免费的操作。 它要么需要重训, 要么需要接受”模型在部分 t 上外推”的风险。
6.17.3 训练和采样的三个不一致
把这件事系统化,一共三处:
| 不一致 | 训练时 | 采样时 | 症状 |
|---|---|---|---|
| 调度 | \bar\alpha^{\text{train}}_t | \bar\alpha^{\text{sample}}_t | 采样炸,loss 正常 |
| 时间步范围 | t\in[1,T] | t\in[0,T-1] | t 小时输出无意义 |
| 数值精度 | fp32 / bf16 混合 | 纯 fp16 | 高 \sigma 处溢出 |
第二行是最常见的 off-by-one。 训练时 t 从 1 到 T, 是因为 \beta_1 对应”加一次噪声”;采样时索引从 T-1 到 0, 因为数组是 0-indexed。两者差 1,而 1 在 t 小的时候是巨大的。
第三行在端侧部署时特别常见。 fp16 的最大值是 65504, 而本章的模型在 t=999 处 \hat x_0 能到 12.8、x_t 能到 1.24—— 看起来安全,但中间量 \sqrt{1-\bar\alpha_t}\varepsilon 在 batch 大时会有离群值。
修法:在采样器内部用 fp32 累加,只在调用模型时转 fp16。
6.17.4 一个判断”是不是调度问题”的实验
这是本章没做、但你应该做的一个实验。设计如下:
材料:同一个模型,两套调度(训练用的线性、以及余弦)。
做法: 1. 用线性调度训练模型 A 2. 用余弦调度训练模型 B 3. 交叉测试:A 用线性采样、A 用余弦采样、B 用余弦采样、B 用线性采样 4. 每组用 10 / 20 / 50 步各跑一次
预期的 2×2 表:
| 线性采样 | 余弦采样 | |
|---|---|---|
| 模型 A(线性训练) | 正常 | 差 |
| 模型 B(余弦训练) | 差 | 正常 |
如果这个表成立,说明”调度必须匹配”;如果对角线不成立, 说明调度的影响比想象的小。 无论哪种结果,都是有用的信息。
本书没有做这个实验,因为要重训两个模型。 这是本章的一个缺口,也是读者可以自己补的一个。
6.18 怎么读一篇新的采样器论文
你以后会不断遇到新采样器。 这一节给一套读法, 它不依赖你懂那篇论文的全部数学。
6.18.1 先找三样东西
任何采样器论文,都可以用三个问题定位:
- 它解的是哪个 ODE?(概率流 ODE?反向 SDE?还是别的?)
- 它改了什么?(网格?阶数?预测目标?还是模型?)
- 它的成本怎么算?(每步几次求值?末步有没有额外修正?)
这三个问题回答完,你就知道它在你已有知识里的位置了。
本章的归位表(§6.16)就是按这个格式做的。
6.18.2 三个可以跳过的部分
大多数采样器论文里,下面三段可以跳过:
① 收敛性证明。 它们通常假设模型是 Lipschitz 的、\varepsilon_\theta 有界、 步长趋于零——这些假设在真实模型上全都不成立。 证明的价值是”保证存在一个足够小的步长”,不是”保证你的 20 步能用”。
② 和 SOTA 的 FID 对比表。 见 §6.25 的三个陷阱。 除非它公开了 NFE 和评估代码,否则这张表只能当参考。
③ 消融实验里”每个组件贡献多少”的分解。 这些数字高度依赖模型和数据集,换个模型就不成立。
6.18.3 三个必须读的部分
① 它的更新式。 哪怕你不懂推导,也要能把公式抄成代码。 抄不出来的地方,就是你没懂的地方。
② 它的网格定义。 是在 t 上均匀、在 \sigma 上均匀、还是在 \lambda 上均匀? 这一条决定了它能不能和你的调度配合(见 §6.17)。
③ 它报告的 NFE。 如果论文只报”步数”,去附录或代码里找 NFE。 找不到,就在你的复现里自己数模型调用次数。
拿到任何新采样器,第一件事不是跑它的 benchmark,是用 10 步跑一次。
为什么是 10 步?因为在这个量级上,所有近似假设都不成立。 一个在 10 步下不崩的采样器,通常是真的做了什么对的事; 一个在 10 步下崩了、但 50 步很好的采样器,很可能只是”高阶但没进入渐近区”。
本章实验 6-1 的表就是这个思路:10 步时 Heun 反而”过头”(+7.5%), Euler 是 −23%——两者都不好,但方向相反。 这个信息比”50 步下谁更好”值钱得多。
6.18.4 什么时候该怀疑论文
三个信号:
- 只报一个步数档位。 本章已经证明,25 步和 100 步的结论可以完全相反。
- 只报 FID,不报多样性指标。 蒸馏类方法尤其容易在这里藏问题。
- 没有说明网格。 一个采样器的收益,可能有一半来自它偷偷换了网格。
第三条最隐蔽。 如果一篇论文说”我们的方法在 20 步下 FID 更好”, 但没说它的 20 步是在 \log\sigma 上取的、而 baseline 是在 t 上取的—— 那么它比较的不是”方法”,是”网格”。
本章 §6.5 那张表(差距 18 倍)说明这个混淆可以有多大。
6.19 工程问题:当采样器进入真实项目
数学上正确,不等于工程上可用。 这一节列四个真实项目里一定会遇到的问题。
6.19.1 批量大小与显存
采样器的显存占用不是”模型大小”,而是”模型大小 + 中间激活 × 批大小”。
高阶方法更吃显存:Heun 每步要保存两个中间状态(\hat x 和 \hat x_{\text{pred}}), 在同样批大小下比欧拉多一份。
实测经验(本章的 5 万参数模型上没有意义,但在真实模型上): - DDIM 20 步,batch 8,约需 X GB - Heun 20 步,batch 8,约需 1.3X GB - 如果要保持同样显存,Heun 的批大小要降到 6 左右
这不是”二阶更贵”的全部——它还要多一次前向。两者叠加, 同样的墙钟时间下,Heun 的实际吞吐可能只有 DDIM 的一半。
6.19.2 精度:fp16 在哪里溢出
本章反复出现的一个量是 1/\sqrt{\bar\alpha_t},在 t=999 处是 157。
在 fp16 下(最大值 65504),这个因子本身没问题。 但它乘上 \hat x_0 之后呢?
本章实测:第 0 步的平均 |\hat x_0|=12.8,最大值远超均值。 如果某个样本的 |\hat x_0| 是 10^3,乘 157 就是 1.6\times10^5,溢出。
修法: 1. 在 \hat x 空间里算(除以 \sqrt{\bar\alpha} 之后),数值范围小得多 2. 关键步骤用 fp32 累加,只在调用模型时转 fp16 3. clamp 在 fp32 下做,不要等到转回 fp16 才截断
6.19.3 确定性:不是”设个种子”就完了
\eta=0 时,理论上同样输入给同样输出。实际不然,因为:
- CUDA 的某些算子是非确定性的(原子加、某些归约)
- 不同的批大小会改变归约顺序,于是结果不同
- 混合精度训练的模型在推理时用不同精度,结果也会变
要真正可复现:
torch.use_deterministic_algorithms(True)
torch.backends.cudnn.benchmark = False代价是速度下降 10–30%。
如果做不到,就接受”同种子下差异在 10^{-5} 量级”, 并在产品文档里写清楚——不要承诺”逐位可复现”。
6.19.4 缓存与增量生成
图生图 / 局部重绘这类应用,需要”保留一部分原图”。
常见的错误做法:先跑完整的 DDIM 到 t,再从 t 往回走。 这不对,因为 DDIM 的轨迹依赖起点。
正确做法:用 SDEdit 式的”加噪到 t 再往回走”: 1. 把原图编码成 x_0 2. 前向加噪到 t(用真实的 \bar\alpha_t) 3. 从这个 x_t 开始反向采样
关键:第 2 步用的是前向过程,不是”反向走到一半”。 两者在 \eta=0 时不等价——前向加噪是随机的,反向是确定的。
有人会想:既然 \eta=0 是确定性的,那我可以把中间结果缓存起来, 下次从缓存继续。
不行。 因为缓存的是一个具体的 x_t,而它只对产生它的那条轨迹有意义。 换一个 prompt、换一个种子,x_t 就完全不同。
能缓存的是”模型的 KV 缓存”(在 Transformer 里)或者”文本编码”, 不是”采样轨迹”。
6.20 十个常见问题
这些都是学生在课上真正问过的。 答案尽量短,需要展开的地方指回对应小节。
6.20.1 问一:DDIM 到底是不是”DDPM 的加速版”?
不是。 DDPM 和 DDIM 是同一个 \sigma 族的两个端点(§6.3)。 DDIM 不是”近似了 DDPM”,它是另一个合法的采样过程。
这个区别很重要:如果 DDIM 是”近似的 DDPM”,那步数少就是”精度低”; 但 DDIM 在 1000 步下不收敛到 DDPM 的结果(\eta=0 vs \eta=1), 它收敛到的是同一个分布,但走的是不同的路径。
“同一个分布,不同的路径” —— 这是理解本章所有采样器的钥匙。
6.20.2 问二:为什么 \eta=0 反而更不稳定?
因为随机性在采样里扮演的是”误差吸收器”。
DDPM 每步注入的噪声,会把上一步积累的误差”冲掉”一部分 (第 5 章讲的”误差自我修正”)。确定性采样没有这个机制, 误差只会被传递和放大。
所以 \eta 的正确理解不是”随机性旋钮”,是”你愿意用多少随机性换稳定性”。 (§6.9)
6.20.3 问三:步数到底该取多少?
没有通用答案,但有三个约束:
- 模型误差 < 5% → 20 步可能够
- 模型误差 5–15% → 要么加 \eta,要么加步数,但加步数可能适得其反
- 预算 < 15 NFE → 别调采样器,去蒸馏
本章的实测:完美去噪器下,一阶要 100 步才能到 2% 误差; 二阶只要 20 步(40 NFE)。所以”步数”这个数只有在说清”什么采样器、 什么模型”之后才有意义。(§6.25)
6.20.4 问四:为什么我的 DDIM 在 t 很大时输出是纯噪声?
因为它就应该是纯噪声。 \bar\alpha_{999}\approx4\times10^{-5}, 信号只剩万分之四。
你看到的”纯噪声”是对的。 真正的问题是:它在 t 变小之后有没有 逐渐长出结构。 如果没有,去看 §6.2 的轨迹表——输出是不是在变大。
6.20.5 问五:clamp 到底该不该用?
该用,但要知道它在掩盖什么。
clamp 的作用是”防止单步爆炸”,它不解决根因。 本章的实测:去掉 clamp 之后,连 25 步那一档也炸了—— 所以它在帮忙。
但命中率是诊断指标。 如果前几步的命中率就超过 50%, 说明 \hat x_0 的估计已经不可用,该去查模型,不是调 clamp 的值。(§6.13 失败五)
6.20.6 问六:为什么换一个调度,采样就崩了?
两种可能:
- 训练/采样不一致(模型没见过新调度下的输入分布)→ §6.17
- 网格不匹配(新调度的 \sigma 分布和你的网格不匹配)→ §6.5
区分方法:用完美去噪器跑新调度。 - 完美去噪器也崩 → 是网格问题 - 完美去噪器正常 → 是模型问题(训练/采样不一致)
这个二分法和本章案例用的是同一个逻辑。
6.20.7 问七:Heun 一定比 Euler 好吗?
不一定。 本章实测:10 步时 Heun 是 +7.5%,Euler 是 −23%—— Heun 更接近,但方向反了,而且它的成本是两倍。
“高阶更好”是渐近结论:当步长足够小时,高阶方法的误差下降更快。 在步长很大时,两种方法都没进入渐近区,比较阶数没有意义。(§6.5)
6.20.8 问八:为什么我的采样器在 CPU 和 GPU 上结果不一样?
三个原因,按常见程度排:
- 浮点归约顺序不同(GPU 的并行归约和 CPU 的顺序不同)
- 算子实现不同(某些算子在 GPU 上有近似版本)
- 精度不同(CPU 默认 fp32,GPU 可能开了 TF32)
这不是 bug。 差异通常在 10^{-5} 量级。 如果你需要逐位一致,用 torch.use_deterministic_algorithms(True), 并固定设备。
6.20.9 问九:为什么”同样 20 步”,别人的结果比我的好?
先问四个问题:
- 他的 20 步是 NFE 还是循环次数?(Heun 的 20 步 = 40 NFE)
- 他的网格是 t 均匀还是 \lambda 均匀?(差距可达 18 倍)
- 他的模型是在什么调度下训的?
- 他的 \eta 是多少?
这四个问题里,任何一个的答案不同,比较就没有意义。(§6.25)
6.20.10 问十:学完这一章,我应该用哪个采样器?
默认答案:DPM-Solver++ 或 UniPC,20–25 步。
理由:它们同时做了”\lambda 网格”和”更好的预测目标”两件事, 在中等步数区间(15–30 NFE)的性价比最高。
但这个默认答案有三个例外:
- 需要确定性 + 可复现 → DDIM,\eta=0
- 模型误差 > 10% → 先用 \eta\geq0.5 的 DDPM 式采样
- 预算 < 10 NFE → 蒸馏,别用上面任何一个
最重要的是:别信”某个采样器最好”这种说法。 采样器的选择是”模型精度 × 确定性需求 × 算力预算”三个约束下的最优解。 换一个模型,答案就会变。(§6.15)
6.21 把轨迹画出来:三个必看的图
本章反复用”轨迹”来定位问题。 这一节讲怎么画、怎么看。
6.21.1 图一:|x| 随步数的变化(对数纵轴)
这是最重要的一张图。
traj = []
for i in range(len(idx) - 1):
...
traj.append({"step": i, "t": int(idx[i]),
"absmean": x.norm(dim=1).mean().item(),
"epsnorm": eps.norm(dim=1).mean().item(),
"clamp": (x0.abs() > 3).float().mean().item()})怎么看:
| 形状 | 含义 |
|---|---|
| 单调下降,末端趋于数据尺度 | 正常 |
| 先降后升,且斜率越来越陡 | 不稳定(本章案例) |
| 全程几乎不变 | 模型输出接近零,采样没在做事 |
| 一开始就极大 | 初始化或调度错了 |
为什么用对数纵轴:\sigma 跨四个数量级,线性轴会把关键区间压成一条线。 本章第一版图就是线性轴,结果什么也看不出来。
6.21.2 图二:\|\varepsilon_\theta\|/\|x\| 随步数的变化
这张图解释”为什么会不稳定”。
正常:这个比值应该下降(因为随着 \sigma 减小,噪声成分减少)。 异常:它上升并稳定在一个大于 1 的常数(本章是 1.9)—— 说明模型已经不再做去噪,而是在放大输入。
这张图是”模型在分布外退化”的直接证据。(§6.2)
6.21.3 图三:clamp 命中率
这张图告诉你”从哪一步开始不可用”。
正常:前几步高(\hat x_0 本来就难估),然后快速下降。 异常:一直高,或者下降后再次升高。
本章的数据:86.5% → 57.8% → 13.2% → 3.1%。 前两步高是正常的(那时信号确实弱), 但 86.5% 意味着”绝大多数样本的估计被丢掉了”——这是一个警告信号。
6.21.4 三张图放一起
把三张图画在同一张图上(双纵轴,或者上下两栏),你会看到:
- |x| 开始上升的位置
- \varepsilon/x 开始偏离正常的位置
- clamp 命中率开始变化的点
如果三条线在同一个步数附近发生变化,因果链就闭合了。
本章的图里,三者都在第 20 步附近变化,这就是 §6.2 那个 “第 20 步开始失控”的结论的来源。
记录轨迹的成本是每个样本每步两个 float。 2000 个样本 × 100 步 × 3 个量 = 600k 个浮点数,不到 5 MB。
而它的价值是:把”炸了”变成”第 20 步开始以 1.9 倍放大”。
没有理由不记。
6.22 端到端:从第 5 章的模型到这一章的采样器
本章的代码散在各个脚本里。 这一节把链路接起来, 给你一个”从训练到采样”能跑通的最小闭环。
6.22.1 链路全景
make_two_moons() 数据
↓
Config / make_schedule() 调度(β → ᾱ → σ)
↓
train() 训练 ε_θ
↓
┌─────────────────────────────────────────┐
│ 采样器(本章的产物) │
│ euler() ← DDIM,η=0 │
│ ddpm() ← η=1 │
│ heun() ← 二阶 │
└─────────────────────────────────────────┘
↓
describe() 统计量
↓
轨迹记录 → 诊断 本章的调试方法
三个接口约定(不遵守就会在某个环节出错):
- 调度只构造一次,作为参数传下去
- 采样器不构造调度,只读
- 模型只接受 (x_t, t),返回 \varepsilon
6.22.2 一个最小的采样循环
def sample(model, sch, kind="euler", steps=20, eta=0.0, n=2000, seed=0):
"""统一入口。返回 (样本, NFE)。"""
torch.manual_seed(seed)
T = sch["T"]
idx = torch.linspace(T - 1, 0, steps).round().long()
# 起点:x_T ~ N(0, I),这是 ᾱ_T ≈ 0 时的边缘分布
x = torch.randn(n, 2, device=sch["device"])
nfe = 0
for i in range(len(idx) - 1):
t, tp = int(idx[i]), int(idx[i + 1])
ab, abp = sch["alpha_bar"][t], sch["alpha_bar"][tp]
tt = torch.full((n,), t, device=x.device, dtype=torch.long)
with torch.no_grad():
eps = model(x, tt)
nfe += 1
x0 = ((x - (1 - ab).sqrt() * eps) / ab.sqrt()).clamp(-3, 3)
if kind == "heun":
# 预测点再求一次
xp = abp.sqrt() * x0 + (1 - abp).sqrt() * eps
tt2 = torch.full((n,), tp, device=x.device, dtype=torch.long)
with torch.no_grad():
eps2 = model(xp, tt2)
nfe += 1
eps = (eps + eps2) / 2
sig = eta * ((1 - abp) / (1 - ab)).sqrt() * (1 - ab / abp).sqrt()
x = (abp.sqrt() * x0
+ (1 - abp - sig ** 2).clamp_min(0).sqrt() * eps
+ (sig * torch.randn_like(x) if eta > 0 else 0))
return x, nfe这段代码里有三处和本章前面不同,都是故意的:
① Heun 分支里,eps 被换成了两次求值的平均。 注意它平均的是 \varepsilon,不是 \hat x_0 —— 这正是 §6.5 那个”假的二阶”踩的坑。
② eta > 0 时才生成噪声。 否则 torch.randn_like 会消耗随机数,破坏 \eta=0 的可复现性。
③ 返回 NFE。 这是所有对比实验的前提(§6.25)。
6.22.3 用它验证三件事
# ① η=1 应该逐位等于 DDPM
x1, _ = sample(model, sch, "euler", 1000, eta=1.0)
# 和你的 DDPM 实现对比,应该完全一致
# ② η=0 应该可复现
xa, _ = sample(model, sch, "euler", 50, eta=0.0, seed=7)
xb, _ = sample(model, sch, "euler", 50, eta=0.0, seed=7)
assert torch.equal(xa, xb)
# ③ Heun 的 NFE 应该是一阶的两倍
_, n1 = sample(model, sch, "euler", 20)
_, n2 = sample(model, sch, "heun", 20)
assert n2 == 2 * n1这三条断言,是本章所有结论的地基。 如果第一条不成立,你的 \eta 公式错了;如果第二条不成立,有未固定的随机源; 如果第三条不成立,你的 NFE 统计错了。
\eta=1 必须逐位等于 DDPM。 这不是”应该差不多”,是必须相同。
因为 DDPM 的采样式是从 ELBO 严格推出来的, 而 \eta=1 在 \sigma 族里恰好对应它。 两者在数学上是同一个东西,所以数值上也必须相同。
如果不同,只可能是三种情况: 1. \eta 的公式错了(最常见:少开根号) 2. 重建时 \varepsilon 的系数错了 3. 起点分布错了(DDPM 从 \mathcal{N}(0,I) 起,不是从 \mathcal{N}(0,\sigma_{\max}^2))
这一条断言,能一次性排除本章 §6.14 里的坑 6 和坑 7。
6.22.4 采样器的验收清单
在把任何采样器接进项目之前,跑这五条:
| # | 检查 | 通过标准 |
|---|---|---|
| 1 | \eta=1 对上 DDPM | 逐位相同 |
| 2 | \eta=0 同种子可复现 | 逐位相同 |
| 3 | NFE 计数正确 | Heun = 2×欧拉 |
| 4 | 完美去噪器下 20 步 | std 在目标 ±15% 内 |
| 5 | 多档位一致 | 25 / 50 / 100 步结论不相反 |
五条全过,才可以接进项目。 本章的案例说明, 只做第 4 条是不够的——它在 25 步下过了,在 100 步下炸了。
6.23 三条没有流行起来的路线
历史只记住赢家。 但”为什么有些方法没流行”往往比”赢家做了什么”更有信息量—— 因为那些原因,就是你在自己项目里会遇到的约束。
6.23.1 路线一:退火 Langevin(NCSN 的做法)
在 score matching 流行起来之前,采样是这么做的:
对每个噪声水平 \sigma_1>\sigma_2>\cdots>\sigma_L: 1. 在这个噪声水平上跑 K 步 Langevin 动力学 2. 把结果作为下一个(更小)噪声水平的起点
问题在哪?
每层要跑 K 步,而 K 通常取 100 左右。 L=10 层 × K=100 = 1000 次求值 —— 和 DDPM 一样慢,而且步长极难调。
更麻烦的是:步长必须满足 \tau<2/L_{\text{lip}}(Langevin 的稳定性条件), 而 L_{\text{lip}} 在每层都不一样。你需要为每一层单独调一个步长。
为什么它没能留下来:DDPM 把”多步 Langevin”换成了”一步反向高斯”, 把超参数从”每层的步长”减少到”一个调度”。
教训:一个算法的工程价值,一半在于它的超参数有多少。
6.23.2 路线二:预测-校正(predictor-corrector)
Song et al. 2021(score-based SDE)提出过一种混合方案:
- 预测:用 SDE 或 ODE 走一步
- 校正:在同一个噪声水平上跑几步 Langevin,把样本”拉回” p_t
理论上很漂亮 —— 校正确实能减少离散化误差。
为什么没流行?
因为它把成本翻了几倍,而收益只在步数极少时才明显。 而”步数极少”这个区间,后来被蒸馏类方法直接占据了。
教训:“理论上更准”和”工程上更划算”是两件事。 校正的收益必须大于它多花的求值次数。
6.23.3 路线三:EDM 的”纯 \sigma 参数化”(这条其实赢了)
Karras et al. 2022(EDM)做了一件事:把整个扩散过程用 \sigma 重写, 丢掉 \alpha、\beta、t 这些记号。
他们的采样器是: 1. 在 \log\sigma 上非均匀取点(本章 §6.5 用的就是这个) 2. 用 Heun(二阶) 3. 加一个”\sigma 预条件”:把模型的输入输出按 \sigma 缩放, 让网络在不同噪声水平上看到同样尺度的量
为什么这条赢了?
因为它把”训练”和”采样”解耦了:模型只关心 \sigma, 采样器只关心 \sigma 网格。两者通过一个数(\sigma)连接。
而 DDPM 的原始参数化里,模型要同时理解 t、\alpha_t、\beta_t—— 换一个调度,模型就懵了。
教训:好的抽象比好的算法更持久。 本章通篇用 \sigma 而不是 t 来讲,就是这个原因。
因为本章的模型很小(5 万参数),预条件的收益不明显, 而它会增加一层”看起来像魔法”的缩放因子。
EDM 的预条件大致是: \text{输入}\times\frac{1}{\sqrt{\sigma^2+\sigma_{\text{data}}^2}},\qquad \text{输出}\times\frac{\sigma\,\sigma_{\text{data}}}{\sqrt{\sigma^2+\sigma_{\text{data}}^2}}
它的作用是让网络的输入输出都保持在 \mathcal{O}(1)。
在真实模型上,这一层几乎是必须的(否则 fp16 会在高 \sigma 处溢出, 见 §6.19)。本章省略它,是为了让 \sigma 参数化这条主线更清楚。
如果你要把它用到真实模型上,记得加回来。
6.24 采样器的十年:一条不断缩短的路
这一节不讲新公式,讲一件事是怎么被一步步做出来的。 之所以要讲,是因为每一步的动因都对应本章已经量过的一个数 —— 你会看到这条技术路线不是随机游走,而是在回答同一个问题: 怎么用更少的函数求值,走到同一个分布。
6.24.1 出发点:DDPM 的 1000 步
2020 年的 DDPM 用 T=1000,每一步对应一个固定的噪声等级。 这个设计的出发点是”让每一步的分布都很简单” —— 每一步只挪动一点点,模型只需要学一个很小的修正。
代价直接写在结构里:生成一张图要跑 1000 次网络。 在 2020 年的硬件上,这意味着几十秒到几分钟。
第 5 章测过这个代价的一个侧面:1000 步里有 327 步(32.7%)的输入几乎是纯噪声, 模型在那里的任务简单到几乎不需要学。 换句话说,那 1000 步里有三分之一是冗余的。
6.24.2 第一步:能不能跳步(DDIM, 2020 末)
DDIM 的洞见在本章 §6.3 讲过:反向过程需要的不是整条马尔可夫链, 而是每个时刻的边缘分布。 只要边缘分布对得上,中间走几步是可以商量的。
这个改动在数学上是干净的:它构造了一族新的前向分布, 边缘分布不变,但不再是马尔可夫的。然后在这族分布里取一个”方差为 0”的特例。
回报是 20–50 倍的速度,而质量损失在好模型上很小。 这就是为什么 DDIM 迅速成为默认选择。
代价在本章案例里暴露得很彻底:确定性采样对模型误差零容忍。 DDIM 并没有”解决”采样慢的问题,它是用鲁棒性换速度。
6.24.3 第二步:能不能用更好的数值方法(DPM-Solver, 2022)
DDIM 之后,研究者的注意力转向了一个更基础的问题: 既然 DDIM 本质上是解一个 ODE,为什么只用一阶方法?
这个转向的出发点是一个观察:概率流 ODE 在 \sigma 空间里是半线性的 —— 它的非线性部分只通过 \varepsilon_\theta 进来。 对这类方程,有专门的指数积分器,可以在相同步数下把误差降一个数量级。
DPM-Solver 系列就是这条思路的产物。它们给出了二阶、三阶的专用离散化, 并且在 20 步左右就能达到 DDIM 需要 100 步的质量。
本章 §6.5 用 Heun 演示了同一个原理的最简版本: 二阶在 20 步时误差 2%,一阶要 80 步才到 2.6%。 更精细的 DPM-Solver 能把这个差距再拉大一截,因为它利用了 ODE 的解析结构, 而 Heun 是通用方法。
这一节要你记住的区分: - DDIM 是”换了一个可以跳步的公式” - DPM-Solver 是”在同一个公式上换更好的数值方法”
两者可以叠加,但它们的改进来源完全不同。
6.24.4 第三步:能不能不”解”了,直接学(蒸馏, 2022–2023)
前两步都是在”怎么走得更快”上做文章。第三步换了个问题: 能不能干脆不走?
思路是蒸馏:让一个小模型学会”从 x_t 直接跳到 x_{t-k}“, 把多步合并成一步。训练时用原来的多步采样器当”老师”, 生成”老师走了 k 步之后的终点”,让学生去拟合。
这一步的极限是”一步生成”:从噪声直接到图像。 2023 年之后出现了好几条路线(一致性模型、对抗蒸馏、分布匹配蒸馏), 目标都是把函数求值次数从几十压到 1–4。
代价是训练变复杂:蒸馏需要额外的训练阶段、需要老师模型、需要小心处理分布偏移。 而且蒸馏出来的模型通常只能用在它被蒸馏的那个采样器/步数上, 灵活性大幅下降。
6.24.5 第四步:换一条路径(流匹配, 2022– )
前面三步都在”沿着调度定义的那条曲线走”这个前提下做优化。 最后这一步把这个前提也放开了。
流匹配的核心观察是:连接噪声和数据的路径不是唯一的。 扩散模型用噪声调度定义了其中一条,但那条路径通常是弯的 —— 从噪声到数据,轨迹会绕。
如果能把路径变直,那么同样的步数就能走得更远。
实测的支撑:本章 §6.5 那张表里,误差随步数的下降速度, 本质上取决于路径的曲率。二阶方法能改善,是因为它更好地跟随曲率。 如果路径本身就是直线,一阶方法就够了 —— 这正是流匹配想要的效果。
这条路线是本书写作时最活跃的方向之一,本章不展开, 但读者应该已经能看出它的动机:它是在回答”为什么需要那么多步”这个问题的最后一个层次。
6.24.6 一张收敛表
把四条路线的改进来源放在一起:
| 方法 | 改的是什么 | 典型求值次数 | 代价 |
|---|---|---|---|
| DDPM | —(基线) | 1000 | 慢 |
| DDIM | 公式(可跳步) | 20–100 | 对模型误差零容忍 |
| DPM-Solver | 数值方法(更高阶) | 10–25 | 实现复杂,需调参 |
| 蒸馏 | 模型本身(学会跳) | 1–8 | 要额外训练,灵活性差 |
| 流匹配 | 路径(更直) | 4–20 | 要改训练目标 |
注意最后一行和前四行的区别:前四个都在”推理”这一侧改, 流匹配改了训练。这也是它为什么能和前面几个叠加的原因。
“更快的采样器”有五种不同来源,它们的代价完全不同。
- 改公式(DDIM):免费,但牺牲鲁棒性
- 改数值方法(高阶):免费,但每次求值更贵、实现更复杂
- 改模型(蒸馏):要重新训练,且失去灵活性
- 改路径(流匹配):要重新设计训练目标
遇到一个”快 10 倍”的采样器,先问它属于哪一类。 答案决定了它能不能直接搬到你的模型上。
6.25 怎么判断一个采样器好不好
这一节不讲新算法。它讲怎么不被评估结果骗。
为什么值得单独一节? 因为本章前面所有结论—— “二阶更省”、“\log\sigma 网格更好”、“\eta 是稳定性旋钮”—— 都依赖同一个前提:你测的是对的东西。 而这个前提,比看上去难满足。
6.25.1 第一个陷阱:把”步数”当成”成本”
论文里的表格通常长这样:
| 方法 | 步数 | FID |
|---|---|---|
| DDIM | 50 | 12.3 |
| DPM-Solver | 20 | 11.8 |
读者会得出结论:“DPM-Solver 用 20 步就超过了 DDIM 的 50 步。”
这个结论可能是错的,因为”步数”不是成本。成本是函数求值次数(NFE):
- DDIM 每步 1 次 → 50 步 = 50 NFE
- Heun 每步 2 次 → 20 步 = 40 NFE
- 某些三阶方法每步 3 次 → 20 步 = 60 NFE
如果那篇论文里的 DPM-Solver 是二阶的 20 步,它的成本是 40 NFE, 确实比 50 少。但如果它是三阶的,成本反而是 60,比 DDIM 还贵。
所以第一件事:看到”步数”两个字,先找它的 NFE 定义。 如果论文没写,默认它在隐藏成本。
很多高阶采样器在最后一步会做额外的修正(比如 DPM-Solver 的 “last-step second-order”),这一次修正要再算一遍模型。
于是同一个方法,标称 20 步,实际 NFE 是 21 或 22。 在 20 步这个量级上,多一次就是 5%。
写基准的时候要把这一次算进去。 本章 experiments/ 里的对比脚本 统计的是真实的模型调用次数,不是循环次数。
6.25.2 第二个陷阱:用”像不像”代替”分布对不对”
本章的案例已经演示过这件事:25 步 DDIM 的输出 std 是 [0.5328, 0.3398], 数据是 [0.5707, 0.3351]。肉眼看几乎一样,但它是错的。
在图像上,这个陷阱更隐蔽。 一个采样器可能生成”看起来很干净”的图, 但:
- 多样性塌缩:所有样本都长得很像,mode 覆盖不全
- 纹理过平滑:高频细节被抹掉,但人眼对这点不敏感
- 颜色偏移:整体色调偏一点,打分模型看不出来
FID 对前两项都不敏感。 它比较的是 Inception 特征的均值和协方差, 一个”每个 mode 都采一点点、但每个 mode 内部都很糊”的分布, 可以拿到很好的 FID。
所以本章坚持用”有解析答案的问题”做对照: 1D 高斯的目标标准差是 0.6325,这是一个可以写下来的数。 采样器给出 0.55,就是错了 12.9%,没有辩解余地。
在真实数据上没有这个奢侈。 那时候的替代方案是:
| 检查 | 能发现什么 | 代价 |
|---|---|---|
| 目标标准差的解析值 | 尺度错误、塌缩 | 只在玩具问题上可行 |
| 最近邻距离分布 | mode 塌缩 | 要一个参考集 |
| 不同随机种子的输出方差 | 多样性塌缩 | 便宜 |
| 换一个步数重测 | 不稳定性(本章案例) | 便宜,最容易被忘 |
| 人眼看 50 张 | 伪影、纹理 | 贵,但不可替代 |
注意第四行。 本章案例的全部发现,都来自”换个步数再测一次”。 如果一个采样器只在某一个步数下好看,它不是采样器,是巧合。
6.25.3 第三个陷阱:在错误的模型上比较采样器
这是最隐蔽的一个。
假设你有两个模型 A 和 B,想在它们上面比较 DDIM 和 Heun。 你在 A 上测出 Heun 好 10%,在 B 上测出 Heun 好 2%。 于是你写:“Heun 平均好 6%。”
这个”平均”没有意义。 因为采样器的表现依赖模型误差的结构 (本章案例已经证明:同一个采样器在完美去噪器上正常、在学出来的模型上炸)。
采样器和模型不是两个独立的因素,它们会交互。
正确的做法是固定模型、只变采样器,而且—— 理想情况下先用完美去噪器把采样器本身的数值性质测清楚, 再去真实模型上测”模型误差 × 采样器”的交互。
本章的结构就是按这个顺序组织的: 先在解析问题上测(§6.3–§6.6),再回到学出来的模型(§6.2、§6.7)。
6.25.4 一个可以照抄的评估协议
把上面三条合起来,得到一个五步协议。每一步都标注了”如果跳过会怎样”。
第一步:写下一个已知答案的对照问题。
不一定是 1D 高斯。可以是二维环面、双月、或者任何你能采样、 能算矩、能画图的分布。关键是”正确答案可写下来”。
跳过会怎样:你无法区分”采样器错”和”模型错”,本章案例就是从这里开始的。
第二步:在这个对照上,把候选采样器的 NFE 对齐。
不是步数对齐,是模型调用次数对齐。列出每个方法在 10 / 20 / 40 / 80 NFE 下的结果。
跳过会怎样:你会把”每步算三次”的方法误认为”三步就够”。
第三步:至少测两个步数档位,差距要拉开 4 倍以上。
本章案例里,25 步和 100 步的结论完全相反。只测一个档位必翻车。
跳过会怎样:你会交付一个只在特定步数下能用的采样器。
第四步:记录轨迹,不只看终点。
本章能定位到”第 20 步开始失控”,是因为记录了每一步的 |x|。 只看终点,你只知道”炸了”,不知道”什么时候开始炸”。
跳过会怎样:你能发现问题,但定位不到原因。
第五步:在真实模型上重跑第一步到第四步。
预期是:真实模型上的结论会比对照问题差。 差多少, 就是”模型误差”这一项的贡献。
跳过会怎样:你会把”采样器的数值性质”和”模型的误差”混为一谈。
对照问题半天,NFE 对齐一小时,多档位重测一小时,轨迹记录改十行代码, 真实模型重跑一天。
总共两天左右。 而本章案例里,没有这套协议的代价是: 一个下午的弯路 + 一个”看起来对”的错误结论 + 上线后的伪影。
两天换这些,很划算。
6.26 动手练习
五个练习,从”复现”到”设计一个新采样器”。都能在本章的代码基础上完成。
6.26.1 练习一:复现,然后改一个变量
跑一遍 notebooks/run_ch6_case.py,确认你能得到本章的三组数:
| 实验 | 本章的值 |
|---|---|
| DDIM + 学出来的模型,100 步 | \text{std}\approx[5.4\times10^7,\,1.2\times10^8] |
| DDIM + 完美去噪器,20 步 | 0.5509(目标 0.6325) |
| \eta 扫描,50 步,\eta=1 | [0.525,\,0.320] |
然后只改一个变量:把学习率从默认值提高 3 倍,重训一次,再跑第一行。
预测一下:爆炸会更早还是更晚?最大绝对值会变大还是变小?
预期的思考方向:学习率更高 → 模型在训练集上更”自信” → 分布外的外推可能更差 → 爆炸更早。但这不是必然的—— 欠训练的模型也可能因为输出接近零而”不会炸”。 重点是你会得到一个属于你自己的数,而不是本章的这个数。
6.26.2 练习二:找出你的模型的”临界步数”
本章的模型在 25 步正常、50 步半炸、100 步全炸。你的模型呢?
设计一组实验: - 步数序列:[10, 15, 20, 25, 30, 40, 50, 75, 100, 150, 200] - 每个步数跑 3 个随机种子 - 记录输出 std、最大绝对值、以及是否出现 nan
画一张图:横轴步数(对数),纵轴最大绝对值(对数)。你会看到一条 在某处突然抬头的曲线。
然后回答两个问题: 1. 抬头发生在第几步? 2. 这个位置和训练步数有关吗?(把训练步数砍半,重跑一次)
这个练习的价值:“临界步数”不是采样器的属性,是模型的属性。 你会亲眼看到它随训练量移动——这是本章”责任在模型”这个结论最直接的体感。
6.26.3 练习三:验证网格理论
本章 §6.5 给了一张表:\sigma 均匀 10 步是 0.0228, \log\sigma 均匀是 0.4201。请在没有完美去噪器的情况下复现这个差距。
做法: 1. 用 run_ch6_case.py 里那个学出来的模型 2. 实现两个网格:t 均匀(现有)和 \log\sigma 均匀 3. 在 10 / 20 / 40 步下各跑一次
你会遇到的麻烦:\log\sigma 网格需要把 \sigma 映射回 t, 而学出来的模型只接受整数 t。你需要插值,或者取最近的整数 t。
这个”麻烦”本身是重点:采样器的数学形式(连续 \sigma) 和模型的接口(离散 t)之间有一个缝隙。 所有实现都要在这个缝隙上做某种折中,而折中的方式会影响结果。
进一步:如果你把 t 取整,\log\sigma 网格的优势还剩多少? 这个数字告诉你”理论收益”和”工程收益”的差。
6.26.4 练习四:实现 PLMS,并证明它值
本章没有实现 PLMS,因为它的核心思想一句话就能说完。 但”一句话”和”能跑”之间的距离,正是这个练习要量的。
要求: 1. 实现二阶 PLMS(Adams-Bashforth 两步): \hat x_{k+1}=\hat x_k+(\sigma_{k+1}-\sigma_k)\left(\frac32\varepsilon_k-\frac12\varepsilon_{k-1}\right) 2. 第一步用欧拉起步 3. 在完美去噪器上测 10 / 20 / 40 步 4. 和 Heun 比较,但按 NFE 对齐
预期:PLMS 在同 NFE 下应该接近 Heun,但在步数很少时更差 (因为第一步只能是欧拉)。
如果你发现 PLMS 明显更好,先检查一件事: 你的 \sigma 网格是均匀的还是非均匀的? 多步法的系数假设步长变化平缓,非均匀网格上直接用会不稳定。
6.26.5 练习五:写一个采样器诊断器
这是最接近真实工作的一个。
把本章六种失败模式做成一个自动检查器:
class SamplerDiagnoser:
"""在采样前后自动跑一组检查,输出诊断报告。"""
def check_consistency(self, model, sch_train, sch_sample):
"""失败六:训练/采样调度一致性。"""
# 比较两个 alpha_bar 数组,不相等就报错
def check_order_effective(self, sampler, model, x, steps):
"""失败二:二阶是否真的生效。"""
# 同一输入跑一阶和二阶,逐位比较;相同就报警
def check_step_sweep(self, sampler, model, x, steps_list):
"""失败一/三:多档位一致性。"""
# 跑多个步数,报告 std 随步数的走向
def check_clamp_rate(self, traj):
"""失败五:clamp 命中率。"""
# 命中率 > 50% 报警
def check_eta_sensitivity(self, sampler, model, x, steps, etas):
"""失败四:eta 的稳定性。"""
# 报告 max|x| 随 eta 的变化;eta=0 反而最大就报警要求:每个检查都要返回 (通过/警告/失败, 一句人话解释), 不能只返回布尔值——诊断器的价值在于”告诉人下一步查什么”。
五个练习加起来大概三四个小时。 它们会给你:
- 练习一、二:一个属于你自己模型的”临界步数”——不是本章的那个数
- 练习三:对”理论收益 vs 工程收益”的体感
- 练习四:一次”多步法在非均匀网格上不稳定”的亲历
- 练习五:一套能直接用在下一个项目上的工具
如果只做一个,做练习二。 它最便宜,而且它直接回答本章最核心的问题: “我的采样器炸了,是代码还是模型?”
如果做两个,加练习五。 因为它把本章的所有诊断动作固化成代码—— 下次遇到问题,你不用重新想一遍。
6.27 符号表与公式速查
本章的记号冲突比前五章加起来还多。 这张表是给回查用的。
6.27.1 符号
| 符号 | 含义 | 取值范围 | 容易和什么混 |
|---|---|---|---|
| \beta_t | 单步噪声方差 | 10^{-4}–0.02 | — |
| \alpha_t | 1-\beta_t | ≈1 | \bar\alpha_t |
| \bar\alpha_t | \prod_{s\le t}\alpha_s | 1\to4\times10^{-5} | \alpha_t |
| \sigma_t(本章) | \sqrt{1-\bar\alpha_t}/\sqrt{\bar\alpha_t} | 152\to0 | DDIM 论文的 \sigma_t |
| \sigma_t(DDIM 论文) | 注入噪声的幅度 | 0–\tilde\beta_t^{1/2} | 本章的 \eta |
| \eta | 注入噪声的比例 | [0,1] | DDIM 论文的 \sigma_t |
| \tilde\beta_t | DDPM 后验方差 | — | \beta_t |
| \varepsilon_\theta | 模型输出的噪声预测 | \approx\mathcal{N}(0,I) | 真值 \varepsilon |
| \hat x_0 | 由 x_t 解出的数据估计 | 数据尺度 | 真值 x_0 |
| \hat x | x_t/\sqrt{\bar\alpha_t} | \sigma 尺度 | x_t |
| \lambda | \log(\alpha/\sigma) | -\infty\to+\infty | \sigma |
| NFE | 函数求值次数 | — | 步数 |
读这张表的方法:如果你在别处看到 \sigma_t,先确认是哪一种。 这是本章唯一一个”同一个符号在两个主流文献里意思相反”的地方。
6.27.2 公式
| 名字 | 式子 | 在哪一节 |
|---|---|---|
| 前向边缘 | q(x_t\mid x_0)=\mathcal{N}(\sqrt{\bar\alpha_t}x_0,(1-\bar\alpha_t)I) | 第 5 章 |
| DDIM 更新 | 式 6.1 | §6.3 |
| DDIM 展开式 | 式 6.2 | §6.3 |
| ε–score 关系 | 式 6.4 | §6.4 |
| 概率流 ODE | 式 6.5 | §6.4 |
| σ 空间流 | 式 6.3 | §6.4 |
| Heun 更新 | k_1=\varepsilon(\hat x,\sigma_t);\hat x_{\text{pred}}=\hat x+h k_1;\hat x'=\hat x+\frac h2(k_1+k_2) | §6.5 |
| \eta 注入 | \sigma_{\text{inj}}=\eta\sqrt{\sigma_t^2-\sigma_{t-1}^2} | §6.9 |
6.27.3 三条”一句话”
- DDIM = 概率流 ODE 在 \sigma 空间的显式欧拉。
- DDPM = 同一个 \sigma 族在 \sigma_t^2=\tilde\beta_t 处的取值。
- 阶数解决”精度”,非均匀网格解决”刚性”,两者不能互相替代。
6.28 关键术语中英对照
这一行是给你读论文用的。 同一个概念在中文教材、英文论文、代码库里有三个名字, 这一栏把它们对上。
| 中文 | 英文 | 代码里常见名 | 说明 |
|---|---|---|---|
| 去噪器 | denoiser | unet, model |
输出 \varepsilon 或 x_0 |
| 噪声预测 | noise prediction | prediction_type="epsilon" |
本章默认 |
| 数据预测 | data prediction | prediction_type="sample" |
DPM-Solver++ 用它 |
| 速度预测 | velocity prediction | prediction_type="v_prediction" |
流匹配 / SD3 |
| 无分类器引导 | classifier-free guidance | guidance_scale |
第 7 章 |
| 调度器 | scheduler | scheduler |
注意:它同时管网格和系数 |
| 时间步 | timestep | timesteps, sigmas |
sigmas 是 σ 参数化 |
| 函数求值次数 | number of function evaluations | nfe, num_fn_evals |
不是 num_inference_steps |
| 概率流 ODE | probability flow ODE | — | 本章 §6.4 |
| 得分匹配 | score matching | — | 本章 §6.4 |
| 非马尔可夫 | non-Markovian | — | DDIM 的前向过程 |
| 后验方差 | posterior variance | variance_type="fixed_small" |
DDPM 的 \tilde\beta_t |
| 截断 | clamping | .clamp() |
作用在 \hat x_0 上 |
| 蒸馏 | distillation | distilled, lcm |
要重训 |
| 整流流 | rectified flow | flow |
路径拉直 |
① scheduler
在 diffusers 里,scheduler 不只是”噪声调度”—— 它还包含采样网格和系数。换一个 scheduler,你同时换了三样东西。
所以”换个 scheduler 试试”不是一个受控实验。
② num_inference_steps
它的名字像”推理步数”,但它是循环次数,不是 NFE。 二阶 scheduler 的 20 步是 40 NFE。
在写对比表时,永远用 NFE。(§6.25)
6.29 五个常见误解
这五个说法,每一个都在教材、博客或论文里出现过。它们都错。 而且每一个的错误方式不一样。
6.29.1 误解一:“DDIM 是对 DDPM 的近似”
错在哪:DDIM 不近似 DDPM。它们是同一个 \sigma 族的两个端点, 对应两条不同的采样路径,收敛到同一个分布。
为什么这个误解流行:因为 DDIM 常被介绍成”加速版 DDPM”, 而”加速”暗示”近似”。
正确的说法:DDPM 和 DDIM 都是精确的(在模型完美的前提下), 它们的区别不是精度,是鲁棒性和可复现性。
这个区别的实践含义:如果你把 DDIM 当成”近似”,你会认为 “步数少 = 精度低”,于是”加步数”看起来总是安全的。 本章的案例证明它不安全。
6.29.2 误解二:“\eta 控制随机性,所以 \eta 越大越多样”
错在哪:\eta 确实控制每步注入的噪声量,但它不控制最终分布的多样性。
因为所有 \eta 收敛到同一个分布。 \eta 改变的是路径,不是终点分布。
那多样性从哪来? 从初始噪声 x_T\sim\mathcal{N}(0,I)。 同一个 \eta、不同的种子,多样性是一样的。
为什么这个误解流行:因为在步数不足时,\eta 大的采样器 看起来”结果更不一样”——但那是因为它在更早的阶段就停止了, 不是因为它的目标分布更宽。
实践含义:如果你需要多样性,换种子,不要调 \eta。 调 \eta 只会改变稳定性和质量。
6.29.3 误解三:“二阶方法每步算两次,所以慢一倍”
错在哪:慢一倍是每步的成本,不是任务的成本。
正确的比较是”同样的 NFE”:Heun 20 步 = 40 NFE,欧拉 40 步 = 40 NFE。 在这个比较下,Heun 通常更快达到同样的精度(本章实测:20 步 Heun 的误差 2.0%,40 步欧拉的误差 5.5%)。
为什么这个误解流行:因为”步数”是显式的循环变量,而 NFE 要自己数。
实践含义:在同一张表里比较不同阶数的方法时,横轴必须是 NFE。 否则你会系统性地低估高阶方法。
6.29.4 误解四:“确定性采样更稳定,因为它没有随机波动”
错在哪:把”可复现”当成了”稳定”。
- 可复现:同样的输入 → 同样的输出
- 稳定:输入的微小扰动 → 输出的微小变化
确定性采样有第一个,没有第二个。 而且恰恰因为它没有随机性, 它失去了唯一的误差吸收机制。
本章实测:\eta=0 时 50 步的 max 是 1.33\times10^3, \eta=1 时是 1.18。差三个数量级。
为什么这个误解流行:因为在模型很好的时候,两者的差别很小。 只有模型有误差时,这个区别才暴露。
6.29.5 误解五:“步数越多,结果越好”
错在哪:它只在”采样器稳定”的前提下成立。
本章的案例:25 步正常、50 步半炸、100 步全炸。 在这个模型上,步数越多越糟。
更准确的说法:步数增加会同时放大”离散化误差的减小”和”模型误差的累积”。 前者随步数下降,后者随步数上升。最优步数是两者的交点。
本章没有测出这个交点(因为模型误差太大,交点落在很低的步数上), 但它的存在可以从数据里推出来:完美去噪器下,误差随步数单调下降; 学出来的模型下,误差随步数单调上升。两者之间必然有一个交叉。
它们都把”两个不同的量”当成了一个。
| 误解 | 被混淆的两个量 |
|---|---|
| 一 | 精度 vs 鲁棒性 |
| 二 | 路径的随机性 vs 分布的多样性 |
| 三 | 每步成本 vs 任务成本 |
| 四 | 可复现 vs 稳定 |
| 五 | 离散化误差 vs 模型误差 |
本章的每一节,本质上都在拆开一对被混淆的量。
这也是”机制”这个词在这本书里的意思: 不是”知道公式”,是”知道公式里每一项各自负责什么”。
6.30 白纸测验:五个必须能自己推出来的式子
读完一章和掌握一章的区别,是能不能在白纸上重推一遍。
下面五道题,每题给了”要用的工具”和”答案的骨架”。 建议先盖住答案,自己写一遍。
6.30.1 第一题:证明 \sigma 族保住边缘分布
题目:设 x_t=\sqrt{\bar\alpha_t}x_0+\sqrt{1-\bar\alpha_t}\,\varepsilon, 且 x_{t-1}\mid x_t,x_0 服从
\mathcal{N}\left(\sqrt{\bar\alpha_{t-1}}x_0 +\sqrt{1-\bar\alpha_{t-1}-\sigma_t^2}\,\varepsilon,\ \sigma_t^2I\right)
证明 x_{t-1} 的边缘分布是 \mathcal{N}(\sqrt{\bar\alpha_{t-1}}x_0,(1-\bar\alpha_{t-1})I)。
工具:高斯分布的线性变换;\operatorname{Var}[aX+b]=a^2\operatorname{Var}[X]+b^2。
骨架: 1. 均值:\varepsilon 的均值为 0,所以均值就是第一项 2. 方差:\varepsilon 的系数平方 × (1-\bar\alpha_t),再加 \sigma_t^2 3. 化简:\frac{1-\bar\alpha_{t-1}-\sigma^2}{1-\bar\alpha_t}(1-\bar\alpha_t)+\sigma^2=1-\bar\alpha_{t-1}
自检:如果第 3 步化简不出来,说明你把 \varepsilon 的方差记成了 1 而不是 1-\bar\alpha_t。
6.30.2 第二题:从 \varepsilon 推出 score
题目:已知 p_t(x_t\mid x_0)=\mathcal{N}(\sqrt{\bar\alpha_t}x_0,(1-\bar\alpha_t)I), 证明 \varepsilon=-\sqrt{1-\bar\alpha_t}\nabla_{x_t}\log p_t。
工具:高斯对数的梯度 \nabla_x\log\mathcal{N}(x;\mu,\Sigma)=-\Sigma^{-1}(x-\mu)。
骨架: 1. \log p_t=-\frac{1}{2(1-\bar\alpha_t)}\|x_t-\sqrt{\bar\alpha_t}x_0\|^2+\text{常数} 2. 求梯度:-\frac{x_t-\sqrt{\bar\alpha_t}x_0}{1-\bar\alpha_t} 3. 而 \varepsilon=\frac{x_t-\sqrt{\bar\alpha_t}x_0}{\sqrt{1-\bar\alpha_t}} 4. 两者相差一个 -\frac{1}{\sqrt{1-\bar\alpha_t}}
自检:符号。如果你得到的是正号,说明你在第 1 步漏了一个负号。
6.30.3 第三题:证明 DDIM 是 \sigma 空间的欧拉法
题目:在 \hat x=x/\sqrt{\bar\alpha}、\sigma=\sqrt{1-\bar\alpha}/\sqrt{\bar\alpha} 下, 证明 DDIM 的更新式等价于 \hat x_{k+1}=\hat x_k+(\sigma_{k+1}-\sigma_k)\varepsilon。
工具:\hat x_0=(x_t-\sqrt{1-\bar\alpha_t}\varepsilon)/\sqrt{\bar\alpha_t} =x_t/\sqrt{\bar\alpha_t}-\sigma_t\varepsilon=\hat x_t-\sigma_t\varepsilon。
骨架: 1. 把 \hat x_0 写成 \hat x_t-\sigma_t\varepsilon 2. DDIM 更新:x_{t-1}=\sqrt{\bar\alpha_{t-1}}\hat x_0+\sqrt{1-\bar\alpha_{t-1}}\varepsilon 3. 两边除以 \sqrt{\bar\alpha_{t-1}}:\hat x_{t-1}=\hat x_0+\sigma_{t-1}\varepsilon 4. 代入第 1 步:\hat x_{t-1}=\hat x_t+(\sigma_{t-1}-\sigma_t)\varepsilon
自检:第 1 步是关键。它说明”DDIM 的一步”就是”在 \hat x 空间里沿 \varepsilon 走 \Delta\sigma“。
6.30.4 第四题:证明 \eta=1 退回 DDPM
题目:证明 \sigma_t^2=\tilde\beta_t=\frac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}\beta_t 时,q_\sigma 就是 DDPM 的后验。
工具:\bar\alpha_t=\bar\alpha_{t-1}\alpha_t,\alpha_t=1-\beta_t。
骨架: 1. 算 1-\bar\alpha_{t-1}-\tilde\beta_t,通分 2. 用 \bar\alpha_t=\bar\alpha_{t-1}(1-\beta_t) 化简分子 3. 得到 \frac{\alpha_t(1-\bar\alpha_{t-1})^2}{1-\bar\alpha_t} 4. 开方后,\varepsilon 的系数是 \frac{\sqrt{\alpha_t}(1-\bar\alpha_{t-1})}{1-\bar\alpha_t} 5. 而 DDPM 后验里 x_t 的系数正是这个
自检:第 2 步的因式分解 1-\bar\alpha_t-\beta_t=\alpha_t(1-\bar\alpha_{t-1})。 这个恒等式不成立,后面的化简全错。
6.30.5 第五题:证明概率流 ODE 保住边缘分布
题目:证明 \dfrac{dx}{dt}=f-\frac12g^2\nabla\log p_t 对应的连续性方程与前向 SDE 的 Fokker-Planck 相同。
工具:连续性方程 \partial_t p=-\nabla\cdot(vp); 恒等式 p\nabla\log p=\nabla p。
骨架: 1. 把 v=f-\frac12g^2\nabla\log p 代进连续性方程 2. 第二项变成 \frac12g^2\nabla\cdot[p\nabla\log p]=\frac12g^2\nabla\cdot\nabla p=\frac12g^2\Delta p 3. 得到 \partial_t p=-\nabla\cdot[fp]+\frac12g^2\Delta p 4. 这正是前向 SDE 的 Fokker-Planck
自检:第 2 步的恒等式。如果你把 p\nabla\log p 写成了 \nabla p/p,符号会错。
如果你五题都能不看答案写出来,这一章的数学部分就过关了。
如果第三题卡住,回去读 §6.4 的最后一小节——它是整章的中心。 如果第五题卡住,回去读 §6.4 的”路线一”——它是最短的一条证明。
注意这五题有一个共同的结构: 每一题都是”把 A 写成 B,然后看 B 满足什么”。
- 第一题:把 x_{t-1} 写成 \varepsilon 的线性函数
- 第二题:把 \log p_t 写成二次型
- 第三题:把 \hat x_0 写成 \hat x_t-\sigma_t\varepsilon
- 第四题:把 \tilde\beta_t 写成 \beta_t/(1-\bar\alpha_t) 的形式
- 第五题:把 v 写成 f 加一个梯度
“换一种写法”是这一章所有推导的共同动作。 而每一种写法,对应一个不同的数值性质 —— 这就是本章从数学走到工程的那条线。
6.31 附:一份采样器评测报告的模板
本章反复说”要按 NFE 对齐、要多档位、要控制变量”。 这一节把它变成一个可以直接填的模板。
下次你要向团队说明”我们该换采样器吗”,填完这张表就有答案了。
6.31.1 一、基本信息
模型:____________(参数量 / 训练步数 / 训练调度)
数据:____________(分辨率 / 数据集 / 样本数)
硬件:____________(GPU / batch size / 精度)
评估日期:________
这五项缺一项,结论就不可迁移。 尤其是训练调度——换一个调度,所有采样器的排序都可能变(§6.17)。
6.31.2 二、候选与 NFE 对齐
| 采样器 | 每步求值 | 步数 | NFE | 实现来源 |
|---|---|---|---|---|
| DDIM | 1 | 20 | 20 | 本章 sample() |
| Heun | 2 | 20 | 40 | 本章 sample() |
| DPM-Solver++ | 1 | 20 | 20 | diffusers |
| … |
注意最后一列。 不同来源的实现,网格和末步处理可能不同。 如果实现来源不同,你比较的可能是”实现”而不是”算法”。
6.31.3 三、质量指标(至少三项)
| 采样器 | NFE | 主指标 | 多样性指标 | 稳定性指标 |
|---|---|---|---|---|
| FID / 解析误差 | 种子间方差 | 多档位一致性 |
为什么至少要三项:
- 主指标告诉你”像不像”
- 多样性指标告诉你”有没有塌缩”(§6.25 陷阱二)
- 稳定性指标告诉你”换个步数会不会炸”(§6.25 陷阱三)
只有主指标的评测,本章的案例已经证明它会骗人。
6.31.4 四、结论的写法
不要写:“采样器 A 比 B 好。”
要写:
在 NFE=20–40、模型误差约 % 的条件下,A 的主指标比 B 好 %, 但多样性低 __%。**在 NFE<20 时,A 出现发散(max|x| > __),B 没有。 建议**:如果预算 ≥ __ NFE 且不需要多样性,选 A;否则选 B。
这个写法包含了四个约束:成本、模型条件、质量、稳定性。 缺任何一个,结论都会被误用。
在报告里专门写一节”我们的评测不能回答什么”。
比如:
- “我们没有测 > 100 NFE 的情况,所以不知道 A 在高预算下是否仍然更好。”
- “我们只在 256×256 上测过,结论不能外推到 1024。”
- “我们的模型误差约 4%,误差更大的模型上排序可能反转。”
这一节会让你的报告更可信,不是更不可信。 因为它告诉读者”边界在哪”——而边界,正是工程决策最需要的信息。
6.32 附:量化和采样器的交互
这一节很短,但它是一个真实部署里绕不开的问题。
6.32.1 量化改变了什么
把模型从 fp16 量化到 int8,会带来两个变化:
- 权重的精度下降 → \varepsilon_\theta 的误差增大
- 激活的精度下降 → 在高 \sigma 处(激活值跨度大)误差尤其大
第二个变化,正好打在本章最敏感的地方。
本章实测:t=999 处 |\hat x_0| 平均 12.8,而 1/\sqrt{\bar\alpha_t}=157。 这两件事叠加,意味着 t 大的区间对数值误差极度敏感。
6.32.2 一个可测的判据
如果你在做量化部署,做这个实验:
- 固定采样器(DDIM,20 步)
- 分别用 fp16 模型和 int8 模型采样
- 记录每一步的 |\hat x_0| 和 clamp 命中率
预期:int8 模型的 clamp 命中率会显著更高, 而且集中在 t 大的前几步。
如果命中率从 60% 涨到 90%,说明量化已经破坏了高噪声区的估计。 这时候的正确动作不是”调 clamp”,是: - 对 t 大的区间保留 fp16(混合精度) - 或者用 \eta>0 的采样器,让噪声吸收量化误差
6.32.3 一个反直觉的结论
量化对采样器的影响,比对模型本身的影响更大。
因为采样器在 t 大的区间会放大误差(157 倍), 而量化误差也正好在 t 大的区间最大(激活跨度大)。 两个放大叠加。
所以”模型量化后精度掉了 1%“这句话没有意义—— 你要问的是”在 t 大的区间掉了多少”。
6.33 延伸阅读
6.33.1 ① DDIM 的原始论文(2020)
Song, Meng, Ermon, Denoising Diffusion Implicit Models。arXiv:2010.02502。
读它能得到什么:本章 §6.3 的完整来源。 论文最有价值的部分不是那个更新公式,而是“非马尔可夫前向过程”的构造—— 它证明了反向过程不需要和一条特定的前向链绑定。 这个想法比 DDIM 本身更通用,后来很多工作都沿着它展开。
读的时候注意什么:
- 论文里的 \sigma_t 公式和本章写的略有不同(他们用了另一个参数化), 但 \eta=0 和 \eta=1 两个端点是一样的。对不上号时先核对端点。
- 论文报告 DDIM 在 20–100 步就能达到 DDPM 1000 步的质量。 这个结论成立的前提是模型足够好 —— 本章的案例正好是反例。 论文没有讨论”模型不准时会怎样”,这是本章补上的那一块。
- 它的实验都在图像上(CIFAR-10、CelebA), 本章在 2D 上重做了一遍,得到的定性结论一致,定量结论不能直接比。
6.33.2 ② DPM-Solver(2022)
Lu et al., DPM-Solver: A Fast ODE Solver for Diffusion Probabilistic Model Sampling in Around 10 Steps。arXiv:2206.00927。
读它能得到什么:本章 §6.5 那套”用更好的数值方法”的思路, 在这篇论文里被做到了极致。
它的核心贡献是”利用 ODE 的解析结构”: 概率流 ODE 在 \sigma 空间里是半线性的, 对这类方程,解可以写成一个指数因子乘上一个积分。 把那个积分用泰勒展开处理,就得到了专门的二阶、三阶离散化。
本章的 Heun 是这条路上最简单的版本(通用二阶方法)。 DPM-Solver 比它更精细,因为它”知道”方程长什么样。
读的时候注意什么:
- 它的”10 步”是在好模型上测的。模型差的时候, 高阶方法同样救不了 —— 本章 §6.5 那张表已经说明, 10 步时 Heun 自己也错 7.5%。
- 论文里有多套系数表(对应不同的阶数和不同的噪声调度)。 抄错一套就会得到比一阶还差的结果,而且不会报错。
- 它的实现依赖 \lambda_t=\log(\bar\alpha_t/(1-\bar\alpha_t)) 这个换元, 和本章的 \sigma 换元是等价的(差一个对数)。两个记号对照着看会更容易懂。
6.33.3 ③ 关于”\mathcal{L}_{\text{simple}} 为什么有效”的讨论
Ho, Jain, Abbeel, Denoising Diffusion Probabilistic Models。arXiv:2006.11239,§3.4。
原始的观察:作者在论文里明确写了”我们试了带权重的版本,效果更差, 所以用了简化版”。但他们没有给出解释。
读的时候注意什么:
- 论文的措辞是”empirically better” —— 它是一个实验观察,不是一个定理。 后来的工作(包括本书这一章)都在试图解释它,但至今没有公认的完整答案。
- 本章给出的解释(“权重把学习压力推向了学不会的小 t”)是一个假说, 在一个 2 维小模型上得到了支持,但没有在大模型上验证过。
- 后续有工作重新引入了加权(比如按 SNR 加权),在高分辨率上取得收益。 这说明”扔掉权重”不是普适最优,而是特定规模下的选择。
6.33.4 ④ 关于采样器评估的方法论
本章反复强调”比求值次数,不比步数”。这个做法在文献里并不统一。
很多论文报的是”步数”,因为那样数字好看 (“我们 10 步就达到了 xxx”)。但二阶方法的 10 步是 20 次求值, 和别人的 10 步不是一回事。
读采样器论文时,先找这三样:
- 每步调用几次模型?(一阶 1 次,二阶 2 次,有些高阶方法 3 次)
- 在什么模型上测的?(模型质量决定了结论能不能迁移)
- 比的是 FID 还是似然?(两者经常给出相反的排序)
本章的立场是:把”求值次数”当作横轴,把”与真实分布的差距”当作纵轴。 在这个坐标系里,所有方法都能放在同一张图上比较。 这个做法更慢(要跑很多配置),但结论更硬。
6.33.5 ⑤ 从 DDPM 到一步生成
如果你想看这条路的终点,可以读一致性模型(Song et al., 2023, arXiv:2303.01469) 和分布匹配蒸馏(Yin et al., 2024, arXiv:2311.18828)。
它们的目标都是”1–4 次求值生成一张图”。
读的时候注意什么:
- 它们都需要额外的训练阶段(本章 §6.16 那张表里的第三类)。 不是”换个采样器”就能得到的。
- 一步生成的质量通常低于多步,而且多样性会下降。 “快”和”好”在这里仍然是一个权衡,只是权衡的位置变了。
- 这些方法对本章的读者来说是一个”知道存在”就够的东西 —— 它们需要的数学工具(得分匹配、变分推断的进一步展开)超出了本书的范围。
6.33.6 ⑥ 流匹配与整流流:换一条路,而不是换一辆车
Lipman et al., Flow Matching for Generative Modeling。arXiv:2210.02747。 Liu, Gong, Liu, Rectified Flow。arXiv:2209.03003。
这两篇代表了一个不同的思路:与其”训练一个去噪器,再想办法把它的 ODE 解快”, 不如直接训练一个”走直线的速度场”。
为什么直线更好? 因为 ODE 的数值误差来自”路径弯曲”。 如果路径是直线,欧拉法一步就走对了 —— 一阶方法在直线上是精确的。
读的时候注意什么:
- 它们不是”采样器的改进”,是训练目标的改变。 你不能把一个已经训好的 DDPM 模型直接拿来做整流流。
- “直线”是训练出来的,不是假设的。Rectified flow 有一个”回流”(reflow) 步骤来逐步把路径拉直,每一步都要重训。
- 本章的 \sigma 空间视角在这里仍然适用: 流匹配的 ODE 也是 d\hat x/d\sigma=\text{某个网络输出}, 只是那个输出不再是 \varepsilon。
6.33.7 ⑦ 随机性为什么有用:一个还没有定论的问题
本章的实测支持”\eta>0 更稳”,但本章给的解释(“噪声吸收误差”)是一个假说。
要读的: - Xu et al., Restart Sampling for Improving Generative Processes(2023) —— 系统地研究了”什么时候重新注入噪声”。 - 关于 stochastic localization / 随机插值的工作 —— 从另一个角度解释 为什么随机过程比确定性 ODE 更鲁棒。
读的时候注意什么:
- “\eta 越大越好”是不对的。 本章的 100 步扫描里,\eta=1 的最大值是 6021, 仍然不等于数据尺度——随机性压住了发散,但没有完全修复模型误差。
- 随机性有代价:同样的步数下,\eta>0 的有效信息量更少 (一部分步数被用来”重新加噪”)。所以它不是一个免费的稳定性。
6.33.8 ⑧ 代码
如果你想看工业级实现:
diffusers(HuggingFace) 的schedulers/目录 —— 十几个采样器,接口统一,是最好的”对照阅读”材料。k-diffusion(Katherine Crowson) —— 实现更贴近论文, 注释少但代码短,适合验证你对公式的理解。
读代码时注意:
diffusers里每个 scheduler 都有set_timesteps(), 它同时决定了网格和系数。这是最容易被忽略的一处(§6.5)。diffusers的SchedulerMixin用num_inference_steps这个名字, 但它指的是循环次数,不是 NFE。(§6.25 的第一个陷阱)
6.34 本章没有回答的问题
一本教材的可信度,一半来自它讲清楚的东西,一半来自它承认没讲清楚的东西。
下面五个问题,本章没有答案。每一个都附了”什么样的实验能回答它”。
6.34.1 问题一:学出来的模型为什么会退化成 \varepsilon\approx1.9x?
本章测到了这个现象(§6.2 的表),也证明了它不是”均匀偏差”能解释的 (§6.8 的六种误差模型全部不发散)。
但没有解释”为什么是 1.9”。
可能的实验:在训练分布外系统扫描 x 的尺度, 测 \varepsilon_\theta(cx,t) 与 c 的关系。 如果它在一个区间内是线性的、斜率为 1.9,那就找到了”放大器的增益”。
6.34.2 问题二:能不能构造一个误差模型,复现 10^7?
本章试了六种,全部失败。 这是本章最大的缺口。
可能的实验:不做”解析误差模型”,直接把学出来的模型在某一步替换成 “前一步的输出”(相当于人为制造一个特定的错误),看轨迹会不会以同样的速率增长。 如果能复现,就可以逐步逼近真实的错误结构。
6.34.3 问题三:\eta 的最优值和模型误差是什么关系?
本章有 \eta 的扫描数据(§6.9),但只有两个步数档位、一个模型。
可能的实验:固定 \eta,扫描”模型误差 b“(用解析去噪器 + 注入误差), 测”多久发散”;再固定 b,扫描 \eta。 如果存在 \eta_{\text{crit}}\propto b 这样的关系,就能给出一个可操作的公式。
注意:本章的实验 6-2 已经证明,“注入均匀误差”不会发散—— 所以这个实验必须用”结构化的误差”,而本章还不知道那种结构是什么 (见问题二)。这两个问题是连在一起的。
6.34.4 问题四:\mathcal{L}_{\text{simple}} 的解释在大模型上成立吗?
本章的解释(权重把学习压力推向学不会的小 t)只在 2 维、5 万参数上验证过。
可能的实验:在 CIFAR-10 上训练两个模型(带权重 / 不带权重), 测每个 t 段的 loss 和梯度相干性,看本章的模式是否重复。
这个实验的成本:单卡几小时到一天。它是本章最值得补的一个。
6.34.5 问题五:采样器的”阶数收益”在真实模型上还剩多少?
本章所有阶数实验都在完美去噪器上做的(为了排除模型误差)。
但真实模型有误差,而误差会淹没高阶方法的精度收益。 在什么误差水平下,二阶就不划算了?
可能的实验:固定采样器(Heun vs Euler),扫描模型误差 b (用解析去噪器 + 可控误差),画出”二阶优势随 b 衰减”的曲线。
因为不写出来,你会以为本章的结论比你实际拥有的更强。
一个具体的例子:如果你读完 §6.2 就记住”模型误差被 157 倍放大 → 爆炸”, 你带走的其实是一个本章没能证明的因果链—— 它解释得通,但六种误差模型的实验都不支持”误差大小”这个说法。
把缺口标出来,你才知道哪一步可以信、哪一步要自己验。
这也是本书对”教材”这件事的理解: 教材不是”已经解决了的问题的合集”,是”一个领域目前的确定性和不确定性”的切片。
6.35 结语:为什么这一章要这样写
这一章有一个不太寻常的结构:它从一个失败开始,中途承认了一次推导失败, 末尾列了五个没回答的问题。
多数教材不这样写。这一节解释为什么。
6.35.1 因为”正确的知识”和”能用的知识”不是一回事
如果这一章按常规写法,大概是这样:
- 介绍 DDIM 的论文
- 推导非马尔可夫前向过程
- 给出更新公式和代码
- 展示在标准数据集上的结果
这样写,你会得到一套”正确的知识”—— 它经得起考试,经得起对照论文。
但它不会让你有能力判断”为什么我的模型上它炸了”。
因为真实世界的问题,从来不是”这个公式对不对”,而是”它在什么条件下不成立”。 而”条件”这件事,只有通过”让它失败”才能看见。
所以这一章花了大量篇幅在一个失败上, 而且把失败的每一步都写了出来,包括走错的那两步。
6.35.2 三个”不”字
不把结论说得比证据强。
本章最典型的例子是”误差被放大 157 倍”。 这个说法单独看是对的,但它容易让人以为”误差大就会炸”。 六种误差模型的实验否定了这个推论。 所以本章把结论收窄成”均匀误差不会发散;发散需要特定的误差结构”, 并列出了没能做到的那一步(构造出那个结构)。
这种写法会让结论看起来”弱”。但它准确。
不留一个”看起来对”的推导。
§6.4 里,我最初写了一版反向 SDE 的推导,试了三次都没对上 Fokker-Planck。 最终换成了”连续性方程”那条路。 书上留下的是成功的那条,但这一节告诉你:另一条我失败了。
为什么要说这个? 因为“推导一次成功”是一个假象。 真实的推导就是试错,而试错时唯一能依靠的,是”代回去验证”这个动作。
不把”没做”包装成”不重要”。
本章列了五个没回答的问题,每一个都附了”什么实验能回答它”。 这不是谦虚,是精确。 你在读完这一章之后, 应该清楚地知道哪些结论可以直接用、哪些需要你自己验证。
6.35.3 一个可以带走的判断
如果你只从这一章带走一个判断,希望是这个:
一个方法的失效边界,比它的有效性更值得知道。
因为有效性写在论文摘要里,而失效边界写在没人读的附录里、 或者根本不写。
本章做的,就是把一个失效边界挖出来,测清楚, 然后诚实地告诉你:这里还有一块我没挖到。
6.35.4 最后
这一章的所有数字,都可以用 notebooks/ 里的五个脚本重新生成。 所有实验,都可以用 experiments/ 里的两个交互页面自己拖一遍。
如果你发现某个数字复现不出来,那是本书的错,请当作 bug。
如果某个解释你觉得不对,去做一个实验检验它—— 这一章的所有结论,都是这样来的。
而如果某个实验你做不出来,那可能不是你的问题。 本章列出的五个未解问题,就是我自己也没做出来的那五个。
把这五个问题写在这里,不是为了交差,是为了让下一个做这件事的人, 不必从「不知道哪里没做」开始。
6.36 本章回顾
把这一章压缩成六句话。
一、DDIM 的正确性不取决于实现,取决于模型。 同一个 ddim() 函数,换成解析的完美去噪器就完全正常(20 步仍有 0.55,目标 0.63); 换回学出来的模型就炸到 10^8。代码没错,是模型在分布外退化成了放大器。
二、放大分两步:先是 1/\sqrt{\bar\alpha_t},再是模型自己。 在 t=999 处 \hat x_0 被放大 157 倍,实测平均 |\hat x_0|=12.8(clamp 前), 对应约 6.6% 的模型误差,86.5% 的坐标直接撞上 clamp。 走出去之后,模型不再是去噪器——它返回 \varepsilon_\theta\approx1.9x, 于是每一步乘 1.9,1.9^{10}\approx613。这两步缺一不可。
三、DDPM 的随机性是稳定器,不是装饰。 \eta 从 0 调到 1,50 步下的输出从 std 13.8 变回 [0.525,\ 0.320](数据是 [0.5707,\ 0.3351])。 “确定性 = 可复现”不等于”确定性 = 稳定”。
四、\mathcal{L}_{\text{simple}} 的谜有一个可测的部分。 ELBO 的权重在小 t 处极大(t=2 时 0.60,t=500 时 0.0055), 而模型在那里的 loss 是 1.01(几乎等于”什么都没学到”)。 权重把学习压力推向了收益最低的方向。 但”梯度更噪”这个常见解释在我们的模型上不成立(小 t 的相干性 0.95,反而最高)。
五、“二阶”不等于”更快”,更不等于”写对了”。 Heun 在 20 步时误差 2%,一阶要 80 步——这是真的进步。 但 10 步时 Heun 自己也错 7.5%,而且我第一版”二阶”在数学上是个恒等式, 输出和一阶逐位相同。
六、采样器有五种不同的加速来源,代价各不相同。 改公式(DDIM,免费但牺牲鲁棒性)、改数值方法(高阶,免费但更贵更复杂)、 改模型(蒸馏,要重训)、改路径(流匹配,要改训练目标)、 改网格(\log\sigma 均匀,免费且收益最大)。 遇到”快 10 倍”的方法,先问它属于哪一类。
七、所有加速都有一个下界,而下界由模型决定。 所需步数大致满足 N\gtrsim L\cdot T,其中 L 是模型输出对输入的敏感度、 T 是 \log\sigma 的跨度。采样器只能改善常数,改不了 L。 这就是为什么”更好的采样器”的收益会饱和,而”更好的模型”不会。
最后留一个问题
本章测出:\eta=1(完全随机)在 50 步下就能给出正确的分布, 而 \eta=0(完全确定)在 100 步下就炸了。
那么”随机性”在这里到底起了什么作用?
一个可能的回答:它是每次迭代后的一次”重新投影”。 模型预测的 \varepsilon_\theta 有误差,这个误差让 x 偏离正确的流形; 注入的噪声把 x 推离流形一点点,然后下一步的 \mu_\theta 再把它拉回来—— 一推一拉,点在流形附近做随机游走,而不是沿着一个越来越偏的方向漂移。
如果这个解释对,那么”需要多少随机性”应该和”模型误差有多大”直接相关。 本章的 \eta 扫描数据支持这个方向(误差越大,越需要 \eta), 但没有做定量拟合。
一个可做的实验:固定 \eta,扫描模型误差 b,测”多久发散”; 再固定 b,扫描 \eta。如果两者存在一个简单的定量关系 (比如 \eta_{\text{crit}}\propto b),那这个解释就站住了。
这个实验本章没做 —— 它需要一个能精确控制误差的模型, 而本章只有”训好的”和”解析的”两种。这是第 7 章讲条件生成时会碰到的同类问题的一个缩影。
下一章换个角度:本章处理的是”没有条件的生成” —— 模型学会生成”某种像数据的东西”, 但你没法告诉它要生成什么。 第 7 章处理条件:文本提示是怎么影响生成的,CFG 为什么会让图”过饱和”, 负向提示到底在做什么。