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]。)

三件事同时发生了:

  1. 步数多的时候直接炸成 nan——不是”质量差”,是数值溢出。
  2. 步数少的时候反而正常(25 步的结果看起来还挺像)。
  3. 中间有一段(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 的放大器 → 指数增长 → 溢出

但还有三个问题没回答:

  1. 那为什么别人用 DDIM 就不炸? 真实模型的误差也是这个量级啊。
  2. 第 5 章说 \mathcal{L}_{\text{simple}} 扔掉 ELBO 权重反而更好,这又是为什么?
  3. “二阶求解器”到底是什么,为什么能把步数压得更低?

这三个问题分别对应本章的三节。


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。

警告

三个约定,错一个就全错

  1. g 的定义里有没有 \sqrt2。 本书用 dx=f\,dt+g\,dw, Fokker-Planck 的扩散项是 \frac12g^2\Delta p。 有的书写 dx=f\,dt+\sqrt{2\beta}\,dw,扩散项就是 \beta\Delta p。
  2. 反向 SDE 的漂移里是 g^2 还是 \frac12g^2。 在”t 递减”的写法里是 g^2; 变成概率流 ODE 时才是 \frac12g^2。
  3. 时间反演时 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 这个视角的实际用处

它不只是一个漂亮的重述。 它带来三件事:

  1. 任何 ODE 求解器都能直接用。 欧拉是一阶,还有二阶(Heun)、三阶(DPM-Solver-3)。 这就是下一节的内容。
  2. 它解释了”为什么 DDIM 的步数可以很少”:因为 ODE 的解是一条光滑曲线, 跳步相当于用更少的采样点去逼近它。
  3. 它把”采样”变成了”数值积分” —— 一个有几百年历史的成熟领域。

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),拖步数滑块, 看输出分布和数据分布的差距。同时显示函数求值次数 —— 这是公平比较的关键。

观察引导。

  1. 把 DDIM 的步数从 1000 拖到 20:分布是什么时候开始明显偏离的?
  2. 换成 Heun(同样步数):偏离改善了多少? 看底部的”函数求值次数”是不是翻倍了。
  3. 最关键的一问:把步数拉到 10,三个采样器哪个最接近数据? (提示:不一定是你以为的那个。)
提示

这一节真正要你记住的是

“步数”不是唯一的成本,“每次求值算几次”也要算进去。 比较采样器时,比的应该是同样的函数求值次数下谁更准。

6.8.2 实验 6-2 偏差诊断实验室

怎么用。 这个实验不跑真模型——它用一个解析的完美去噪器 + 可调的误差注入, 把”模型有多准”变成一个可以拧的旋钮。

三个滑块: - 误差幅度 b:给去噪器乘上 (1+b),b\in[0, 0.2] - 步数:10–500 - \eta:0–1

观察引导。这个实验的结论和直觉相反,请先自己拖一遍。

  1. 固定 \eta=0、100 步,把 b 从 0 一路拖到 0.20。 曲线发散了吗? —— 没有。它只是整体下移:标准差从 0.6099 掉到 0.1923, 一直是”偏小”,从来没有”爆炸”。
  2. 固定 b=0.10,把步数从 10 拖到 500。加步数能救回来吗? —— 不能。它只是从 0.2928 缓慢爬到 0.3575,稳定在目标值的 55% 左右。
  3. 现在回到本章案例:学出来的模型在 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 ——这是一条非常干净的单调曲线。

它告诉我两件事:

  1. 不是随机故障(否则不会有单调性)
  2. 不稳定是逐步累积的(步数越多越糟)

“单调”这个性质,比任何单个数字都有信息量。 它直接把”某一步的偶发溢出”排除掉了。

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 越大),需要的步数越多。 这就是”模型误差和采样步数绑定”的数学表达。

重要这个式子的实用含义

它解释了本章的两个观察:

  1. 为什么步数少时高阶方法救不了你 —— p 变大只改善 (C/\text{容差})^{1/p} 这一项, 但 L\cdot T 是乘在前面的。 如果 L 很大,再高的阶数也是杯水车薪。

  2. 为什么”\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% 以上的精度,二阶方法还没进入渐近区。 这不是采样器能解决的问题,是”必须换模型”的问题。

警告场景 C 是最常见的一类错误

很多人会拿着 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 的二阶优势被启动阶段吃掉了。

还有第二个代价:它假设步长是”准均匀”的。 如果网格变化剧烈,多步法的系数要重算,否则会不稳定。

注记为什么本书没有实现 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++ 这些方法, 都可以写进同一个”预测-校正”框架:

  1. 预测:用历史值外推一个 \hat x^{(0)}
  2. 校正:用(可选的)新求值修正它

不同的方法,区别只在”用什么公式外推”和”校不校正”。

它的实际价值不是”更快”,而是”让超参数变少”: 在 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 这条曲线。 它决定了三件事:

  1. \sigma 的范围:\sigma_{\max}=\sqrt{1-\bar\alpha_T}/\sqrt{\bar\alpha_T}
  2. \sigma 在 t 上的分布:t 均匀时,\sigma 怎么变
  3. “有效步数”:真正在改变 \hat x 的那些步有多少

第三件事最容易被忽略。

线性 \beta 调度(本章用的那个)在 t 大时 \sigma 变化极快, 在 t 小时变化极慢。后果是:t 均匀取点时,大部分步数落在”\sigma 变化慢”的区间, 而那些步对结果的贡献很小。

余弦调度(cosine schedule)的设计目标就是让 \sigma 的变化更均匀。 所以它和 \log\sigma 网格更匹配,和 t 均匀网格反而不匹配。

6.17.2 一个可以直接测的判据

如果你怀疑”调度和网格不匹配”,做这个实验:

  1. 固定采样器(比如 DDIM)
  2. 固定步数(比如 20)
  3. 只改调度(线性 / 余弦 / 平方根)
  4. 看输出标准差

如果三个调度的结果差很多,说明网格没有适配调度。

本章没有跑这组完整实验(它需要重新训练,因为模型是在线性调度下训的), 但你可以用一个不需要训练的办法:用完美去噪器, 把 \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 先找三样东西

任何采样器论文,都可以用三个问题定位:

  1. 它解的是哪个 ODE?(概率流 ODE?反向 SDE?还是别的?)
  2. 它改了什么?(网格?阶数?预测目标?还是模型?)
  3. 它的成本怎么算?(每步几次求值?末步有没有额外修正?)

这三个问题回答完,你就知道它在你已有知识里的位置了。

本章的归位表(§6.16)就是按这个格式做的。

6.18.2 三个可以跳过的部分

大多数采样器论文里,下面三段可以跳过:

① 收敛性证明。 它们通常假设模型是 Lipschitz 的、\varepsilon_\theta 有界、 步长趋于零——这些假设在真实模型上全都不成立。 证明的价值是”保证存在一个足够小的步长”,不是”保证你的 20 步能用”。

② 和 SOTA 的 FID 对比表。 见 §6.25 的三个陷阱。 除非它公开了 NFE 和评估代码,否则这张表只能当参考。

③ 消融实验里”每个组件贡献多少”的分解。 这些数字高度依赖模型和数据集,换个模型就不成立。

6.18.3 三个必须读的部分

① 它的更新式。 哪怕你不懂推导,也要能把公式抄成代码。 抄不出来的地方,就是你没懂的地方。

② 它的网格定义。 是在 t 上均匀、在 \sigma 上均匀、还是在 \lambda 上均匀? 这一条决定了它能不能和你的调度配合(见 §6.17)。

③ 它报告的 NFE。 如果论文只报”步数”,去附录或代码里找 NFE。 找不到,就在你的复现里自己数模型调用次数。

注记一个实用的检验:用 10 步跑一次

拿到任何新采样器,第一件事不是跑它的 benchmark,是用 10 步跑一次。

为什么是 10 步?因为在这个量级上,所有近似假设都不成立。 一个在 10 步下不崩的采样器,通常是真的做了什么对的事; 一个在 10 步下崩了、但 50 步很好的采样器,很可能只是”高阶但没进入渐近区”。

本章实验 6-1 的表就是这个思路:10 步时 Heun 反而”过头”(+7.5%), Euler 是 −23%——两者都不好,但方向相反。 这个信息比”50 步下谁更好”值钱得多。

6.18.4 什么时候该怀疑论文

三个信号:

  1. 只报一个步数档位。 本章已经证明,25 步和 100 步的结论可以完全相反。
  2. 只报 FID,不报多样性指标。 蒸馏类方法尤其容易在这里藏问题。
  3. 没有说明网格。 一个采样器的收益,可能有一半来自它偷偷换了网格。

第三条最隐蔽。 如果一篇论文说”我们的方法在 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 问三:步数到底该取多少?

没有通用答案,但有三个约束:

  1. 模型误差 < 5% → 20 步可能够
  2. 模型误差 5–15% → 要么加 \eta,要么加步数,但加步数可能适得其反
  3. 预算 < 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 问六:为什么换一个调度,采样就崩了?

两种可能:

  1. 训练/采样不一致(模型没见过新调度下的输入分布)→ §6.17
  2. 网格不匹配(新调度的 \sigma 分布和你的网格不匹配)→ §6.5

区分方法:用完美去噪器跑新调度。 - 完美去噪器也崩 → 是网格问题 - 完美去噪器正常 → 是模型问题(训练/采样不一致)

这个二分法和本章案例用的是同一个逻辑。

6.20.7 问七:Heun 一定比 Euler 好吗?

不一定。 本章实测:10 步时 Heun 是 +7.5%,Euler 是 −23%—— Heun 更接近,但方向反了,而且它的成本是两倍。

“高阶更好”是渐近结论:当步长足够小时,高阶方法的误差下降更快。 在步长很大时,两种方法都没进入渐近区,比较阶数没有意义。(§6.5)

6.20.8 问八:为什么我的采样器在 CPU 和 GPU 上结果不一样?

三个原因,按常见程度排:

  1. 浮点归约顺序不同(GPU 的并行归约和 CPU 的顺序不同)
  2. 算子实现不同(某些算子在 GPU 上有近似版本)
  3. 精度不同(CPU 默认 fp32,GPU 可能开了 TF32)

这不是 bug。 差异通常在 10^{-5} 量级。 如果你需要逐位一致,用 torch.use_deterministic_algorithms(True), 并固定设备。

6.20.9 问九:为什么”同样 20 步”,别人的结果比我的好?

先问四个问题:

  1. 他的 20 步是 NFE 还是循环次数?(Heun 的 20 步 = 40 NFE)
  2. 他的网格是 t 均匀还是 \lambda 均匀?(差距可达 18 倍)
  3. 他的模型是在什么调度下训的?
  4. 他的 \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 三张图放一起

把三张图画在同一张图上(双纵轴,或者上下两栏),你会看到:

  1. |x| 开始上升的位置
  2. \varepsilon/x 开始偏离正常的位置
  3. 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()                统计量
      ↓
轨迹记录 → 诊断            本章的调试方法

三个接口约定(不遵守就会在某个环节出错):

  1. 调度只构造一次,作为参数传下去
  2. 采样器不构造调度,只读
  3. 模型只接受 (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)提出过一种混合方案:

  1. 预测:用 SDE 或 ODE 走一步
  2. 校正:在同一个噪声水平上跑几步 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 来讲,就是这个原因。

注记为什么本章没有用 EDM 的预条件

因为本章的模型很小(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 三条”一句话”

  1. DDIM = 概率流 ODE 在 \sigma 空间的显式欧拉。
  2. DDPM = 同一个 \sigma 族在 \sigma_t^2=\tilde\beta_t 处的取值。
  3. 阶数解决”精度”,非均匀网格解决”刚性”,两者不能互相替代。

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,会带来两个变化:

  1. 权重的精度下降 → \varepsilon_\theta 的误差增大
  2. 激活的精度下降 → 在高 \sigma 处(激活值跨度大)误差尤其大

第二个变化,正好打在本章最敏感的地方。

本章实测:t=999 处 |\hat x_0| 平均 12.8,而 1/\sqrt{\bar\alpha_t}=157。 这两件事叠加,意味着 t 大的区间对数值误差极度敏感。

6.32.2 一个可测的判据

如果你在做量化部署,做这个实验:

  1. 固定采样器(DDIM,20 步)
  2. 分别用 fp16 模型和 int8 模型采样
  3. 记录每一步的 |\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. 每步调用几次模型?(一阶 1 次,二阶 2 次,有些高阶方法 3 次)
  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 因为”正确的知识”和”能用的知识”不是一回事

如果这一章按常规写法,大概是这样:

  1. 介绍 DDIM 的论文
  2. 推导非马尔可夫前向过程
  3. 给出更新公式和代码
  4. 展示在标准数据集上的结果

这样写,你会得到一套”正确的知识”—— 它经得起考试,经得起对照论文。

但它不会让你有能力判断”为什么我的模型上它炸了”。

因为真实世界的问题,从来不是”这个公式对不对”,而是”它在什么条件下不成立”。 而”条件”这件事,只有通过”让它失败”才能看见。

所以这一章花了大量篇幅在一个失败上, 而且把失败的每一步都写了出来,包括走错的那两步。

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 为什么会让图”过饱和”, 负向提示到底在做什么。