logo Mr.Robota 具身智能学习手册 EN
第十七章:具身世界模型

DDPM 详解与代码实现

本教程从 VAE 的局限出发,解释 DDPM 为什么要把“一次生成”改成“多步去噪”,再从前向扩散、反向生成、ELBO、真实后验、噪声预测目标一路推导到最终训练和采样公式。


0. 学习路线

DDPM 的主线可以压缩成五步:

  1. VAE 通过潜变量 zz 一步生成图像,但压缩和重建假设容易带来细节损失。
  2. DDPM 不急着一步生成完整图像,而是先定义一个固定的加噪过程,把真实图像逐步变成高斯噪声。
  3. 生成时从高斯噪声开始,训练神经网络学习每一步如何去掉一点噪声。
  4. 严格推导来自最大似然和 ELBO,但最终可以简化成一个很直观的监督学习任务:预测加到图像里的噪声。
  5. 实现时的核心不是复杂 loss,而是正确处理噪声日程、时间步、U-Net 输入输出、采样公式和数值范围。

DDPM 先把图像逐步加噪到接近纯噪声,再训练模型反过来逐步去噪;它把高难度的“一步生成图像”拆成很多个简单的“预测并去掉噪声”问题。

DDPM 最常用的训练目标是:

Lsimple(θ)=Et,x0,ϵ[∥ϵ−ϵθ(xt,t)∥2] \mathcal{L}_{\mathrm{simple}}(\theta) {=} \mathbb{E}_{t,x_0,\epsilon} \left[ \left\| \epsilon-\epsilon_\theta(x_t,t) \right\|^2 \right]

其中:

xt=αˉtx0+1−αˉtϵ,ϵ∼N(0,I) x_t {=} \sqrt{\bar{\alpha}_t}x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon, \quad \epsilon\sim\mathcal{N}(0,I)

也就是说,训练时我们知道真实噪声 ϵ\epsilon,网络只需要根据带噪图像 xtx_t 和时间步 tt 把它预测出来。


1. 为什么需要 DDPM

1.1 VAE 存在的几个问题

VAE 很重要,但它也有几个常见问题。

第一,VAE 通常把图像压缩到一个低维潜变量:

x→z→x^ x\rightarrow z\rightarrow \hat{x}

如果 zz 的容量不足,细节会丢失,生成图像容易偏模糊。

第二,VAE 的重建项常对应像素级似然。以高斯似然为例,最大化 log⁡pθ(x∣z)\log p_\theta(x|z) 常等价于最小化均方误差。均方误差会鼓励模型输出“平均答案”,而平均图像往往看起来不锐利。

第三,VAE 一步从 zz 生成 xx:

z→x z\rightarrow x

这要求 Decoder 在一次前向传播中完成从抽象语义到全部像素细节的生成。对复杂图像来说,这个映射压力很大。

所以 VAE 的核心矛盾可以理解为:

它想用一个规整、可采样的潜空间生成图像,但这个压缩和一步解码过程可能损失高频细节。

1.2 DDPM 怎么换一个思路

DDPM 不再把生成看成:

z→x z\rightarrow x

而是看成:

xT→xT−1→⋯→x1→x0 x_T\rightarrow x_{T-1}\rightarrow\cdots\rightarrow x_1\rightarrow x_0

其中:

  • x0x_0 是真实干净图像;
  • x1,…,xTx_1,\dots,x_T 是逐步加噪后的图像;
  • xTx_T 接近标准高斯噪声。

生成时先采样:

xT∼N(0,I) x_T\sim\mathcal{N}(0,I)

然后每一步只做一件小事:

根据当前带噪图像,预测噪声并去掉一点噪声。

DDPM 的好处是:

  1. 训练目标稳定,不需要 GAN 那样的对抗训练。
  2. 每一步任务简单,只需要学习局部去噪。
  3. 图像空间中逐步生成,不需要把所有细节压进一个很小的潜变量。
  4. 可以使用 U-Net 保留多尺度空间细节。
  5. 数学上可以从 ELBO 推导到简单的噪声预测 MSE。

2. DDPM 的基本设定

DDPM 有两个过程:

  1. 前向扩散过程:固定规则,不需要学习,把真实图像逐步加噪。
  2. 反向生成过程:需要学习,从噪声逐步去噪生成图像。

2.1 前向扩散过程

前向过程从真实图像 x0x_0 出发:

x0→x1→x2→⋯→xT x_0\rightarrow x_1\rightarrow x_2\rightarrow\cdots\rightarrow x_T

DDPM 把它定义为 Markov chain:

q(x1:T∣x0)=∏t=1Tq(xt∣xt−1) q(x_{1:T}|x_0) {=} \prod_{t=1}^{T} q(x_t|x_{t-1})

Markov chain 的意思是:如果已经知道 xt−1x_{t-1},那么生成 xtx_t 时不需要再直接看更早的状态。

每一步加噪定义为:

q(xt∣xt−1)=N(xt;1−βtxt−1,βtI) q(x_t|x_{t-1}) {=} \mathcal{N} \left( x_t; \sqrt{1-\beta_t}x_{t-1}, \beta_t I \right)

这里:

  • βt\beta_t 是第 tt 步的噪声强度;
  • βtI\beta_t I 表示每个维度加入方差为 βt\beta_t 的独立高斯噪声;
  • 1−βtxt−1\sqrt{1-\beta_t}x_{t-1} 表示保留一部分上一时刻的图像信号。

通常定义:

αt=1−βt \alpha_t=1-\beta_t

于是:

q(xt∣xt−1)=N(xt;αtxt−1,(1−αt)I) \boxed{ q(x_t|x_{t-1}) {=} \mathcal{N} \left( x_t; \sqrt{\alpha_t}x_{t-1}, (1-\alpha_t)I \right) }

2.2 从分布写成采样公式

如果:

x∼N(μ,σ2I) x\sim\mathcal{N}(\mu,\sigma^2I)

就可以写成:

x=μ+σϵ,ϵ∼N(0,I) x=\mu+\sigma\epsilon, \quad \epsilon\sim\mathcal{N}(0,I)

把这个规则用到 DDPM 的一步前向分布:

xt∣xt−1∼N(αtxt−1,(1−αt)I) x_t|x_{t-1} \sim \mathcal{N} \left( \sqrt{\alpha_t}x_{t-1}, (1-\alpha_t)I \right)

得到:

xt=αtxt−1+1−αtϵt \boxed{ x_t {=} \sqrt{\alpha_t}x_{t-1} + \sqrt{1-\alpha_t}\epsilon_t }

其中:

ϵt∼N(0,I) \epsilon_t\sim\mathcal{N}(0,I)

这就是一步加噪公式。

2.3 为什么系数用平方根

很多初学者会问:为什么是 1−βt\sqrt{1-\beta_t},而不是 1−βt1-\beta_t?

假设 xt−1x_{t-1} 的方差大约是 II,噪声 ϵt\epsilon_t 的方差也是 II。如果:

xt=1−βtxt−1+βtϵt x_t {=} \sqrt{1-\beta_t}x_{t-1} + \sqrt{\beta_t}\epsilon_t

那么方差大约是:

Var(xt)=(1−βt)I+βtI=I \mathrm{Var}(x_t) {=} (1-\beta_t)I+\beta_t I {=} I

也就是说,每一步加噪后,整体尺度不会突然变大或变小。

如果改成:

xt=(1−βt)xt−1+βtϵt x_t=(1-\beta_t)x_{t-1}+\sqrt{\beta_t}\epsilon_t

那么方差会变成:

(1−βt)2I+βtI (1-\beta_t)^2I+\beta_t I

它不再正好等于 II。所以 DDPM 使用平方根系数,是为了让信号和噪声的方差配比稳定。


3. 从一步加噪到任意时刻加噪

训练 DDPM 时,如果每次都从 x0x_0 一步步加噪到 xtx_t,会很慢。幸运的是,前向过程有闭式公式,可以直接从 x0x_0 得到任意时间步的 xtx_t。

最终公式是:

q(xt∣x0)=N(xt;αˉtx0,(1−αˉt)I) \boxed{ q(x_t|x_0) {=} \mathcal{N} \left( x_t; \sqrt{\bar{\alpha}_t}x_0, (1-\bar{\alpha}_t)I \right) }

其中:

αˉt=∏s=1tαs \bar{\alpha}_t {=} \prod_{s=1}^{t}\alpha_s

对应采样公式是:

xt=αˉtx0+1−αˉtϵ \boxed{ x_t {=} \sqrt{\bar{\alpha}_t}x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon }

其中:

ϵ∼N(0,I) \epsilon\sim\mathcal{N}(0,I)

这个公式非常重要。它说明训练时可以随机选择一个时间步 tt,然后一次性构造对应的带噪图像 xtx_t。

3.1 两步展开看懂闭式公式

一步加噪是:

xt=αtxt−1+1−αtϵt x_t {=} \sqrt{\alpha_t}x_{t-1} + \sqrt{1-\alpha_t}\epsilon_t

再写出上一时刻:

xt−1=αt−1xt−2+1−αt−1ϵt−1 x_{t-1} {=} \sqrt{\alpha_{t-1}}x_{t-2} + \sqrt{1-\alpha_{t-1}}\epsilon_{t-1}

代入第一式:

xt=αt(αt−1xt−2+1−αt−1ϵt−1)+1−αtϵt x_t {=} \sqrt{\alpha_t} \left( \sqrt{\alpha_{t-1}}x_{t-2} + \sqrt{1-\alpha_{t-1}}\epsilon_{t-1} \right) + \sqrt{1-\alpha_t}\epsilon_t

展开:

xt=αtαt−1xt−2+αt(1−αt−1)ϵt−1+1−αtϵt x_t {=} \sqrt{\alpha_t\alpha_{t-1}}x_{t-2} + \sqrt{\alpha_t(1-\alpha_{t-1})}\epsilon_{t-1} + \sqrt{1-\alpha_t}\epsilon_t

后两项都是独立高斯噪声的线性组合,所以仍然是高斯噪声。它的方差是:

αt(1−αt−1)+(1−αt)=1−αtαt−1 \alpha_t(1-\alpha_{t-1})+(1-\alpha_t) {=} 1-\alpha_t\alpha_{t-1}

因此可以合并成一个新的标准高斯噪声 ϵ\epsilon:

xt=αtαt−1xt−2+1−αtαt−1ϵ x_t {=} \sqrt{\alpha_t\alpha_{t-1}}x_{t-2} + \sqrt{1-\alpha_t\alpha_{t-1}}\epsilon

继续递推到 x0x_0,就得到:

xt=αtαt−1⋯α1x0+1−αtαt−1⋯α1ϵ x_t {=} \sqrt{\alpha_t\alpha_{t-1}\cdots\alpha_1}x_0 + \sqrt{1-\alpha_t\alpha_{t-1}\cdots\alpha_1}\epsilon

定义:

αˉt=αtαt−1⋯α1 \bar{\alpha}_t {=} \alpha_t\alpha_{t-1}\cdots\alpha_1

就得到任意时刻公式:

xt=αˉtx0+1−αˉtϵ x_t {=} \sqrt{\bar{\alpha}_t}x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon

3.2 为什么 xTx_T 会接近标准高斯噪声

因为:

αˉt=∏s=1tαs \bar{\alpha}_t {=} \prod_{s=1}^{t}\alpha_s

而:

αs=1−βs<1 \alpha_s=1-\beta_s<1

当 tt 足够大时:

αˉt→0 \bar{\alpha}_t\rightarrow 0

代入:

xt=αˉtx0+1−αˉtϵ x_t {=} \sqrt{\bar{\alpha}_t}x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon

第一项会消失:

αˉtx0→0 \sqrt{\bar{\alpha}_t}x_0\rightarrow 0

第二项会变成:

1−αˉtϵ→ϵ \sqrt{1-\bar{\alpha}_t}\epsilon\rightarrow\epsilon

所以:

xT≈ϵ∼N(0,I) x_T\approx\epsilon\sim\mathcal{N}(0,I)

这解释了为什么生成时可以从纯高斯噪声开始。


4. 反向扩散过程

前向过程是人为固定的,不需要学习。真正需要学习的是反向过程:

xT→xT−1→⋯→x0 x_T\rightarrow x_{T-1}\rightarrow\cdots\rightarrow x_0

模型联合分布写成:

pθ(x0:T)=p(xT)∏t=1Tpθ(xt−1∣xt) p_\theta(x_{0:T}) {=} p(x_T) \prod_{t=1}^{T} p_\theta(x_{t-1}|x_t)

其中:

p(xT)=N(0,I) p(x_T)=\mathcal{N}(0,I)

反向一步通常也设为高斯分布:

pθ(xt−1∣xt)=N(xt−1;μθ(xt,t),σt2I) \boxed{ p_\theta(x_{t-1}|x_t) {=} \mathcal{N} \left( x_{t-1}; \mu_\theta(x_t,t), \sigma_t^2I \right) }

这里:

  • μθ(xt,t)\mu_\theta(x_t,t) 是神经网络预测出的反向均值;
  • σt2I\sigma_t^2I 是反向方差,很多基础实现中先固定不学;
  • 网络输入是带噪图像 xtx_t 和时间步 tt;
  • 网络输出可以设计成预测均值、预测原图 x0x_0 或预测噪声 ϵ\epsilon。

直觉上,反向一步在做:

给定当前噪声图 xtx_t,猜测上一步稍微干净一点的图像 xt−1x_{t-1} 应该在哪里。


5. 训练目标:从最大似然到 ELBO

DDPM 作为生成模型,本来希望最大化真实图像的似然:

log⁡pθ(x0) \log p_\theta(x_0)

但:

pθ(x0)=∫pθ(x0:T)dx1:T p_\theta(x_0) {=} \int p_\theta(x_{0:T})dx_{1:T}

这个积分需要把所有中间变量 x1,…,xTx_1,\dots,x_T 都积分掉,直接计算很困难。

因此 DDPM 使用变分下界。训练时最小化负 ELBO:

L=Eq(x1:T∣x0)[−log⁡pθ(x0:T)q(x1:T∣x0)] \mathcal{L} {=} \mathbb{E}_{q(x_{1:T}|x_0)} \left[ {-} \log \frac{ p_\theta(x_{0:T}) }{ q(x_{1:T}|x_0) } \right]

其中:

pθ(x0:T)=p(xT)∏t=1Tpθ(xt−1∣xt) p_\theta(x_{0:T}) {=} p(x_T) \prod_{t=1}^{T} p_\theta(x_{t-1}|x_t)
q(x1:T∣x0)=∏t=1Tq(xt∣xt−1) q(x_{1:T}|x_0) {=} \prod_{t=1}^{T} q(x_t|x_{t-1})

详细代数推导放在附录 A。主文只保留结论:

L=Eq[DKL(q(xT∣x0)∥p(xT))+∑t>1DKL(q(xt−1∣xt,x0)∥pθ(xt−1∣xt))−log⁡pθ(x0∣x1)] \boxed{ \mathcal{L} {=} \mathbb{E}_q \left[ D_{\mathrm{KL}} \left( q(x_T|x_0)\|p(x_T) \right) + \sum_{t>1} D_{\mathrm{KL}} \left( q(x_{t-1}|x_t,x_0) \| p_\theta(x_{t-1}|x_t) \right) {-} \log p_\theta(x_0|x_1) \right] }

它有三类项。

第一项:

DKL(q(xT∣x0)∥p(xT)) D_{\mathrm{KL}} \left( q(x_T|x_0)\|p(x_T) \right)

表示最终噪声分布要接近标准高斯。因为 TT 足够大时 q(xT∣x0)≈N(0,I)q(x_T|x_0)\approx\mathcal{N}(0,I),这一项通常接近常数。

中间项:

DKL(q(xt−1∣xt,x0)∥pθ(xt−1∣xt)) D_{\mathrm{KL}} \left( q(x_{t-1}|x_t,x_0) \| p_\theta(x_{t-1}|x_t) \right)

是训练的核心。它要求模型学到的反向一步分布 pθ(xt−1∣xt)p_\theta(x_{t-1}|x_t) 接近真实后验 q(xt−1∣xt,x0)q(x_{t-1}|x_t,x_0)。

最后一项:

−log⁡pθ(x0∣x1) -\log p_\theta(x_0|x_1)

表示从 x1x_1 恢复 x0x_0 的重建误差。

所以 DDPM 的关键问题变成:

如何让神经网络学会逼近真实后验 q(xt−1∣xt,x0)q(x_{t-1}|x_t,x_0)?


6. 真实后验与均值匹配

真实后验是:

q(xt−1∣xt,x0) q(x_{t-1}|x_t,x_0)

它表示:如果已知原图 x0x_0 和当前带噪图 xtx_t,那么上一时刻 xt−1x_{t-1} 应该是什么分布。

根据贝叶斯公式:

q(xt−1∣xt,x0)=q(xt∣xt−1,x0)q(xt−1∣x0)q(xt∣x0) q(x_{t-1}|x_t,x_0) {=} \frac{ q(x_t|x_{t-1},x_0) q(x_{t-1}|x_0) }{ q(x_t|x_0) }

由于前向过程是马尔科夫链:

q(xt∣xt−1,x0)=q(xt∣xt−1) q(x_t|x_{t-1},x_0) {=} q(x_t|x_{t-1})

所以:

q(xt−1∣xt,x0)=q(xt∣xt−1)q(xt−1∣x0)q(xt∣x0) q(x_{t-1}|x_t,x_0) {=} \frac{ q(x_t|x_{t-1}) q(x_{t-1}|x_0) }{ q(x_t|x_0) }

右边三个分布都是高斯,所以结果仍然是高斯:

q(xt−1∣xt,x0)=N(xt−1;μ~t(xt,x0),β~tI) \boxed{ q(x_{t-1}|x_t,x_0) {=} \mathcal{N} \left( x_{t-1}; \tilde{\mu}_t(x_t,x_0), \tilde{\beta}_tI \right) }

其中:

μ~t(xt,x0)=αˉt−1βt1−αˉtx0+αt(1−αˉt−1)1−αˉtxt \boxed{ \tilde{\mu}_t(x_t,x_0) {=} \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 }

以及:

β~t=1−αˉt−11−αˉtβt \boxed{ \tilde{\beta}_t {=} \frac{ 1-\bar{\alpha}_{t-1} }{ 1-\bar{\alpha}_t } \beta_t }

详细配方推导放在附录 B。主文先理解它的含义:

  • 真实后验均值 μ~t(xt,x0)\tilde{\mu}_t(x_t,x_0) 是 x0x_0 和 xtx_t 的线性组合;
  • 它告诉我们从 xtx_t 往回走一步时,最合理的中心位置;
  • 训练模型时,可以让模型预测的均值 μθ(xt,t)\mu_\theta(x_t,t) 去接近这个真实均值。

模型反向分布是:

pθ(xt−1∣xt)=N(xt−1;μθ(xt,t),σt2I) p_\theta(x_{t-1}|x_t) {=} \mathcal{N} \left( x_{t-1}; \mu_\theta(x_t,t), \sigma_t^2I \right)

如果固定方差,那么两个高斯分布之间的 KL 与模型有关的部分就是均值 MSE:

Lt−1=Eq[12σt2∥μ~t(xt,x0)−μθ(xt,t)∥2]+C L_{t-1} {=} \mathbb{E}_q \left[ \frac{1}{2\sigma_t^2} \left\| \tilde{\mu}_t(x_t,x_0) {-} \mu_\theta(x_t,t) \right\|^2 \right] + C

其中 CC 是与模型参数 θ\theta 无关的常数。

这一步把复杂的概率匹配问题变成了:

让模型预测的反向均值接近真实后验均值。


7. 从预测均值到预测噪声

理论上,网络可以直接预测:

μθ(xt,t) \mu_\theta(x_t,t)

但 DDPM 实践中更常预测噪声:

ϵθ(xt,t) \epsilon_\theta(x_t,t)

这一步看起来突然,其实来自公式重写。

7.1 先从预测 x0x_0 开始

真实后验均值可以写成:

μ~t(xt,x0)=Atx0+Btxt \tilde{\mu}_t(x_t,x_0) {=} A_tx_0+B_tx_t

其中:

At=αˉt−1βt1−αˉt A_t {=} \frac{ \sqrt{\bar{\alpha}_{t-1}}\beta_t }{ 1-\bar{\alpha}_t }
Bt=αt(1−αˉt−1)1−αˉt B_t {=} \frac{ \sqrt{\alpha_t}(1-\bar{\alpha}_{t-1}) }{ 1-\bar{\alpha}_t }

生成时不知道真实 x0x_0,所以可以让网络预测原图:

x0,θ(xt,t) x_{0,\theta}(x_t,t)

再构造:

μθ(xt,t)=μ~t(xt,x0,θ(xt,t)) \mu_\theta(x_t,t) {=} \tilde{\mu}_t \left( x_t, x_{0,\theta}(x_t,t) \right)

这说明预测原图 x0x_0 和预测均值 μθ\mu_\theta 是可以互相转换的。

7.2 再从预测 x0x_0 改成预测噪声

前向加噪公式是:

xt=αˉtx0+1−αˉtϵ x_t {=} \sqrt{\bar{\alpha}_t}x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon

把它对 x0x_0 求解:

x0=xt−1−αˉtϵαˉt x_0 {=} \frac{ x_t-\sqrt{1-\bar{\alpha}_t}\epsilon }{ \sqrt{\bar{\alpha}_t} }

如果网络预测噪声:

ϵθ(xt,t) \epsilon_\theta(x_t,t)

就可以构造预测原图:

x0,θ(xt,t)=xt−1−αˉtϵθ(xt,t)αˉt \boxed{ x_{0,\theta}(x_t,t) {=} \frac{ x_t-\sqrt{1-\bar{\alpha}_t}\epsilon_\theta(x_t,t) }{ \sqrt{\bar{\alpha}_t} } }

再把 x0,θx_{0,\theta} 代入上一节的 μθ\mu_\theta,得到 DDPM 最常见的反向均值参数化:

μθ(xt,t)=1αt(xt−βt1−αˉtϵθ(xt,t)) \boxed{ \mu_\theta(x_t,t) {=} \frac{1}{\sqrt{\alpha_t}} \left( x_t {-} \frac{ \beta_t }{ \sqrt{1-\bar{\alpha}_t} } \epsilon_\theta(x_t,t) \right) }

这个公式的完整代数化简放在附录 C。

7.3 为什么更喜欢预测噪声

预测噪声有三个直观优势。

第一,监督信号天然可得。训练时我们自己采样:

ϵ∼N(0,I) \epsilon\sim\mathcal{N}(0,I)

然后构造:

xt=αˉtx0+1−αˉtϵ x_t {=} \sqrt{\bar{\alpha}_t}x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon

所以真实噪声 ϵ\epsilon 是已知标签。

第二,目标形式统一。无论图像内容多复杂,噪声始终来自标准高斯:

ϵ∼N(0,I) \epsilon\sim\mathcal{N}(0,I)

第三,采样公式直观。模型预测当前图像中的噪声,然后把它从 xtx_t 中减掉。


8. 最终训练目标和采样公式

8.1 从均值匹配到噪声预测 MSE

真实后验均值可以写成噪声形式:

μ~t(xt,x0)=1αt(xt−βt1−αˉtϵ) \tilde{\mu}_t(x_t,x_0) {=} \frac{1}{\sqrt{\alpha_t}} \left( x_t {-} \frac{ \beta_t }{ \sqrt{1-\bar{\alpha}_t} } \epsilon \right)

模型均值写成:

μθ(xt,t)=1αt(xt−βt1−αˉtϵθ(xt,t)) \mu_\theta(x_t,t) {=} \frac{1}{\sqrt{\alpha_t}} \left( x_t {-} \frac{ \beta_t }{ \sqrt{1-\bar{\alpha}_t} } \epsilon_\theta(x_t,t) \right)

两者相减:

μ~t(xt,x0)−μθ(xt,t)=βtαt1−αˉt(ϵθ(xt,t)−ϵ) \tilde{\mu}_t(x_t,x_0) {-} \mu_\theta(x_t,t) {=} \frac{ \beta_t }{ \sqrt{\alpha_t}\sqrt{1-\bar{\alpha}_t} } \left( \epsilon_\theta(x_t,t)-\epsilon \right)

因此:

∥μ~t(xt,x0)−μθ(xt,t)∥2=βt2αt(1−αˉt)∥ϵ−ϵθ(xt,t)∥2 \left\| \tilde{\mu}_t(x_t,x_0) {-} \mu_\theta(x_t,t) \right\|^2 {=} \frac{ \beta_t^2 }{ \alpha_t(1-\bar{\alpha}_t) } \left\| \epsilon-\epsilon_\theta(x_t,t) \right\|^2

代入 KL 对应的均值匹配项,会得到一个加权的噪声预测 MSE:

Lt−1=E[βt22σt2αt(1−αˉt)∥ϵ−ϵθ(xt,t)∥2]+C L_{t-1} {=} \mathbb{E} \left[ \frac{ \beta_t^2 }{ 2\sigma_t^2\alpha_t(1-\bar{\alpha}_t) } \left\| \epsilon-\epsilon_\theta(x_t,t) \right\|^2 \right] + C

DDPM 实践中常使用简化目标,去掉复杂权重:

Lsimple(θ)=Et,x0,ϵ[∥ϵ−ϵθ(xt,t)∥2] \boxed{ \mathcal{L}_{\mathrm{simple}}(\theta) {=} \mathbb{E}_{t,x_0,\epsilon} \left[ \left\| \epsilon-\epsilon_\theta(x_t,t) \right\|^2 \right] }

其中:

xt=αˉtx0+1−αˉtϵ \boxed{ x_t {=} \sqrt{\bar{\alpha}_t}x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon }

这就是 DDPM 最常见的训练目标。

8.2 训练流程

一次训练迭代可以理解为:

  1. 从数据集中采样真实图像:
x0∼qdata(x0) x_0\sim q_{\mathrm{data}}(x_0)
  1. 随机采样时间步:
t∼Uniform({1,…,T}) t\sim\mathrm{Uniform}(\{1,\dots,T\})
  1. 采样高斯噪声:
ϵ∼N(0,I) \epsilon\sim\mathcal{N}(0,I)
  1. 构造带噪图像:
xt=αˉtx0+1−αˉtϵ x_t {=} \sqrt{\bar{\alpha}_t}x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon
  1. 让网络预测噪声:
ϵθ(xt,t) \epsilon_\theta(x_t,t)
  1. 最小化噪声预测误差:
∥ϵ−ϵθ(xt,t)∥2 \left\| \epsilon-\epsilon_\theta(x_t,t) \right\|^2

8.3 采样流程

训练完成后,从纯噪声开始:

xT∼N(0,I) x_T\sim\mathcal{N}(0,I)

然后从 t=Tt=T 到 t=1t=1 逐步执行:

xt−1=μθ(xt,t)+σtz x_{t-1} {=} \mu_\theta(x_t,t) + \sigma_tz

其中:

z∼N(0,I) z\sim\mathcal{N}(0,I)

代入噪声预测形式的均值:

xt−1=1αt(xt−βt1−αˉtϵθ(xt,t))+σtz \boxed{ x_{t-1} {=} \frac{1}{\sqrt{\alpha_t}} \left( x_t {-} \frac{ \beta_t }{ \sqrt{1-\bar{\alpha}_t} } \epsilon_\theta(x_t,t) \right) + \sigma_tz }

通常在最后一步 t=1t=1 时不再加入随机噪声,即令:

z=0 z=0

这样可以避免最后输出又被额外扰动。


9. DDPM代码实现

本节结合 STL10 代码说明 DDPM 如何从公式落到 PyTorch 实现。完整代码位于:

  • code/train_stl10_ddpm.py
  • code/generate_stl10_ddpm.py

数据集采用 STL10,代码中使用 split="train",图像默认调整为 96×9696\times96。第一次运行时会自动下载数据集,需要提前安装好 torch 和 torchvision。

这份实现采用最基础的 DDPM 配置:线性 beta schedule、噪声预测目标、MSE loss、小型 U-Net 噪声预测器。它的目的不是一次写出最强扩散模型,而是先跑通“训练时加噪、模型预测噪声、采样时逐步去噪”这条闭环。

9.1 数据集和图像范围

STL10 是 RGB 图像,因此模型输入形状为:

x0∈R3×96×96 x_0\in\mathbb{R}^{3\times96\times96}

训练脚本中先把图像 resize 到指定大小,再把像素归一化到 [−1,1][-1,1]:

pythontransform = transforms.Compose(
    [
        transforms.Resize((args.image_size, args.image_size)),
        transforms.ToTensor(),
        transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
    ]
)
train_set = datasets.STL10(
    root=args.data_dir,
    split="train",
    download=True,
    transform=transform,
)

这里的 Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)) 会把原来的 [0,1][0,1] 像素变成 [−1,1][-1,1]:

xnorm=2x−1 x_{\text{norm}}=2x-1

这样做的原因是,DDPM 最后会把图像逐渐加到标准高斯噪声附近。图像范围接近 [−1,1][-1,1] 时,数据尺度和噪声尺度更匹配。生成保存图片时,再把结果映射回 [0,1][0,1]:

pythonutils.save_image((images + 1) / 2, out_path, nrow=int(num_samples**0.5))

9.2 时间嵌入

DDPM 的网络不能只看带噪图像 xtx_t,还必须知道当前时间步 tt。同一张图在 t=50t=50 和 t=900t=900 时噪声强度完全不同,如果不给时间信息,网络不知道应该去掉多少噪声。

代码中使用正弦时间嵌入,把整数时间步转成连续向量:

pythonclass SinusoidalTimeEmbedding(nn.Module):
    def __init__(self, dim: int) -> None:
        super().__init__()
        self.dim = dim

    def forward(self, t: torch.Tensor) -> torch.Tensor:
        half = self.dim // 2
        freq = torch.exp(
            -math.log(10000)
            * torch.arange(half, device=t.device, dtype=torch.float32)
            / max(half - 1, 1)
        )
        angles = t.float()[:, None] * freq[None, :]
        emb = torch.cat([angles.sin(), angles.cos()], dim=-1)
        return F.pad(emb, (0, self.dim % 2))

得到的时间向量再经过 MLP:

pythonself.time_mlp = nn.Sequential(
    SinusoidalTimeEmbedding(time_dim),
    nn.Linear(time_dim, time_dim),
    nn.SiLU(),
    nn.Linear(time_dim, time_dim),
)

这样每个时间步都有一个可供网络使用的条件向量。

9.3 残差块如何注入时间信息

U-Net 中的每个残差块都接收两个输入:图像特征 x 和时间特征 time_emb。时间特征先经过线性层投影到当前通道数,再加到卷积特征图上:

pythonclass ResBlock(nn.Module):
    def __init__(self, in_ch: int, out_ch: int, time_dim: int) -> None:
        super().__init__()
        self.conv1 = nn.Conv2d(in_ch, out_ch, 3, padding=1)
        self.norm1 = nn.GroupNorm(norm_groups(out_ch), out_ch)
        self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1)
        self.norm2 = nn.GroupNorm(norm_groups(out_ch), out_ch)
        self.time_proj = nn.Sequential(nn.SiLU(), nn.Linear(time_dim, out_ch))
        self.skip = nn.Conv2d(in_ch, out_ch, 1) if in_ch != out_ch else nn.Identity()

    def forward(self, x: torch.Tensor, time_emb: torch.Tensor) -> torch.Tensor:
        h = F.silu(self.norm1(self.conv1(x)))
        h = h + self.time_proj(time_emb)[:, :, None, None]
        h = F.silu(self.norm2(self.conv2(h)))
        return h + self.skip(x)

其中:

  • time_proj(time_emb) 的形状是 [B, C];
  • [:, :, None, None] 把它变成 [B, C, 1, 1];
  • 加到 h 上时会自动 broadcast 到整张特征图。

这一步对应公式中的条件网络:

ϵθ(xt,t) \epsilon_\theta(x_t,t)

也就是说,模型预测噪声时同时依赖图像状态 xtx_t 和时间步 tt。

9.4 U-Net 噪声预测器

训练脚本中的核心模型是 UNet96。它输入一张 96×9696\times96 的带噪 RGB 图像,输出同样大小的噪声预测:

xt∈R3×96×96 x_t\in\mathbb{R}^{3\times96\times96}
ϵθ(xt,t)∈R3×96×96 \epsilon_\theta(x_t,t)\in\mathbb{R}^{3\times96\times96}

模型先下采样提取语义特征,再上采样恢复空间分辨率:

pythonself.in_conv = nn.Conv2d(in_ch, base, 3, padding=1)
self.down1 = ResBlock(base, base, time_dim)
self.down2 = ResBlock(base, base * 2, time_dim)
self.down3 = ResBlock(base * 2, base * 4, time_dim)
self.down4 = ResBlock(base * 4, base * 8, time_dim)
self.mid = ResBlock(base * 8, base * 8, time_dim)

self.up3 = ResBlock(base * 8 + base * 4, base * 4, time_dim)
self.up2 = ResBlock(base * 4 + base * 2, base * 2, time_dim)
self.up1 = ResBlock(base * 2 + base, base, time_dim)
self.out = nn.Sequential(
    nn.GroupNorm(norm_groups(base), base),
    nn.SiLU(),
    nn.Conv2d(base, in_ch, 3, padding=1),
)

前向传播时,上采样阶段会把深层特征和下采样阶段保存的特征拼接起来:

pythonx1 = self.down1(self.in_conv(x), time_emb)
x2 = self.down2(self.pool(x1), time_emb)
x3 = self.down3(self.pool(x2), time_emb)
x4 = self.down4(self.pool(x3), time_emb)
h = self.mid(x4, time_emb)

h = F.interpolate(h, scale_factor=2, mode="nearest")
h = self.up3(torch.cat([h, x3], dim=1), time_emb)
h = F.interpolate(h, scale_factor=2, mode="nearest")
h = self.up2(torch.cat([h, x2], dim=1), time_emb)
h = F.interpolate(h, scale_factor=2, mode="nearest")
h = self.up1(torch.cat([h, x1], dim=1), time_emb)
return self.out(h)

这里的 skip connection 很重要。DDPM 要做的是逐像素噪声预测,既需要全局语义,也需要局部纹理位置。下采样路径负责扩大感受野,上采样路径负责恢复分辨率,skip connection 负责把浅层细节送回去。

9.5 Diffusion 工具类

Diffusion 类负责两件事:

  1. 预计算前向扩散和反向采样需要的系数;
  2. 提供 q_sample 和 sample 两个核心函数。

初始化时先定义线性 beta schedule:

pythonself.betas = torch.linspace(beta_start, beta_end, timesteps, device=self.device)
self.alphas = 1.0 - self.betas
self.alpha_bars = torch.cumprod(self.alphas, dim=0)
self.sqrt_alpha_bars = torch.sqrt(self.alpha_bars)
self.sqrt_one_minus_alpha_bars = torch.sqrt(1.0 - self.alpha_bars)

这对应前面推导过的:

αt=1−βt \alpha_t=1-\beta_t
αˉt=∏s=1tαs \bar{\alpha}_t=\prod_{s=1}^{t}\alpha_s

因为这些量只和时间步有关,所以应该在初始化时一次算好,而不是每个 batch 重新计算。

_extract 的作用是从长度为 timesteps 的系数表中取出当前 batch 对应的时间步系数:

pythondef _extract(self, values: torch.Tensor, t: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
    return values.gather(0, t).view(t.size(0), *((1,) * (x.ndim - 1)))

例如一个 batch 有 32 张图,每张图随机到不同的时间步,_extract 就能取出 32 个对应的 αˉt\sqrt{\bar{\alpha}_t},并 reshape 成 [B,1,1,1],方便和图像张量相乘。

9.6 训练时如何构造 xtx_t

训练 DDPM 不需要从 x0x_0 一步步加噪到 xtx_t,而是直接使用闭式公式:

xt=αˉtx0+1−αˉtϵ x_t {=} \sqrt{\bar{\alpha}_t}x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon

代码中的 q_sample 正是在实现这个公式:

pythondef q_sample(
    self,
    x0: torch.Tensor,
    t: torch.Tensor,
    noise: torch.Tensor | None = None,
) -> torch.Tensor:
    if noise is None:
        noise = torch.randn_like(x0)
    return (
        self._extract(self.sqrt_alpha_bars, t, x0) * x0
        + self._extract(self.sqrt_one_minus_alpha_bars, t, x0) * noise
    )

这里的 noise 就是训练监督信号 ϵ\epsilon。因为噪声是我们自己采样出来的,所以训练标签天然已知,不需要人工标注。

9.7 训练目标

一次训练迭代可以写成五步:

  1. 从数据集中取出真实图像 x0x_0;
  2. 随机采样时间步 tt;
  3. 随机采样噪声 ϵ\epsilon;
  4. 用闭式公式得到 xtx_t;
  5. 让模型根据 (xt,t)(x_t,t) 预测 ϵ\epsilon。

训练脚本中的核心代码是:

pythonfor x0, _ in train_loader:
    x0 = x0.to(device)
    t = torch.randint(0, args.timesteps, (x0.size(0),), device=device)
    noise = torch.randn_like(x0)
    xt = diffusion.q_sample(x0, t, noise)
    loss = F.mse_loss(model(xt, t), noise)

    optimizer.zero_grad(set_to_none=True)
    loss.backward()
    optimizer.step()

这正对应最终简化目标:

Lsimple(θ)=Et,x0,ϵ[∥ϵ−ϵθ(xt,t)∥2] \mathcal{L}_{\mathrm{simple}}(\theta) {=} \mathbb{E}_{t,x_0,\epsilon} \left[ \left\| \epsilon-\epsilon_\theta(x_t,t) \right\|^2 \right]

注意,模型不是直接预测干净图像 x0x_0,而是预测本次加进去的噪声 ϵ\epsilon。预测出噪声后,采样阶段就可以根据 DDPM 公式一步步构造反向均值。

9.8 训练脚本的输出

train_stl10_ddpm.py 会保存:

  • checkpoints/stl10_ddpm.pt:模型权重和必要配置;
  • outputs/stl10_sample_epoch_XXX.png:训练过程中从噪声采样得到的 STL10 风格图像。

checkpoint 中保存了:

python{
    "model_state": model.state_dict(),
    "base": args.base,
    "timesteps": args.timesteps,
    "image_size": args.image_size,
}

这些配置会被生成脚本复用。尤其是 image_size,因为 STL10 默认使用 96×9696\times96,生成阶段必须和训练阶段保持一致。

运行方式示例:

bashpython train_stl10_ddpm.py --epochs 200 --batch-size 32 --image-size 96

运行结果如下所示,可以看出,一开始训练的时候噪声比较大,当训练到200轮的时候,已经能够重建出一些轮廓,但是效果还是一般,教程为了让大家更好的理解,并没有添加很多的改进技巧,例如注意力机制,类别条件生成等,各位学习完课程之后可以进一步的学习~

9.9 采样生成过程

训练结束后,生成阶段从纯高斯噪声开始:

xT∼N(0,I) x_T\sim\mathcal{N}(0,I)

然后按照时间步反向循环:

xT→xT−1→⋯→x0 x_T\rightarrow x_{T-1}\rightarrow\cdots\rightarrow x_0

代码中的 sample 函数实现了这个过程:

python@torch.no_grad()
def sample(self, model: nn.Module, shape: tuple[int, int, int, int]) -> torch.Tensor:
    model.eval()
    x = torch.randn(shape, device=self.device)

    for step in reversed(range(self.timesteps)):
        t = torch.full((shape[0],), step, device=self.device, dtype=torch.long)
        pred_noise = model(x, t)
        beta_t = self._extract(self.betas, t, x)
        alpha_t = self._extract(self.alphas, t, x)
        alpha_bar_t = self._extract(self.alpha_bars, t, x)
        mean = (1.0 / torch.sqrt(alpha_t)) * (
            x - beta_t / torch.sqrt(1.0 - alpha_bar_t) * pred_noise
        )
        x = mean if step == 0 else mean + torch.sqrt(beta_t) * torch.randn_like(x)

    return x.clamp(-1, 1)

其中反向均值为:

μθ(xt,t)=1αt(xt−βt1−αˉtϵθ(xt,t)) \mu_\theta(x_t,t) {=} \frac{1}{\sqrt{\alpha_t}} \left( x_t {-} \frac{\beta_t}{\sqrt{1-\bar{\alpha}_t}} \epsilon_\theta(x_t,t) \right)

代码中的这一行:

pythonx = mean if step == 0 else mean + torch.sqrt(beta_t) * torch.randn_like(x)

表示最后一步不再额外加随机噪声。否则生成出的 x0x_0 会被最后一次噪声扰动。

9.10 测试生成脚本

generate_stl10_ddpm.py 只做生成,不再读取训练数据。它的流程是:

  1. 加载 checkpoints/stl10_ddpm.pt;
  2. 根据 checkpoint 里的 base 和 image_size 重建 UNet96;
  3. 加载模型权重;
  4. 从标准高斯噪声开始反向采样;
  5. 保存生成图像。

核心代码为:

pythoncheckpoint = torch.load(args.checkpoint, map_location=device)
image_size = int(checkpoint.get("image_size", 96))
model = UNet96(base=int(checkpoint["base"])).to(device)
model.load_state_dict(checkpoint["model_state"])

diffusion = Diffusion(timesteps=int(checkpoint["timesteps"]), device=device)
with torch.no_grad():
    images = diffusion.sample(model, (args.num_samples, 3, image_size, image_size))
    utils.save_image((images + 1) / 2, args.out, nrow=max(1, int(args.num_samples**0.5)))

运行方式示例:

bashpython generate_stl10_ddpm.py --checkpoint checkpoints/stl10_ddpm.pt --out outputs/generated_stl10.png --num-samples 1

默认只生成一张 96×9696\times96 图像,方便先检查模型是否真的能从噪声生成可见结构。如果想看多张图,可以调大 --num-samples。

运行结果如下所示:


10. 常见问题

10.1 DDPM 的前向过程是学出来的吗

不是。前向过程:

q(xt∣xt−1)=N(xt;αtxt−1,βtI) q(x_t|x_{t-1}) {=} \mathcal{N} \left( x_t; \sqrt{\alpha_t}x_{t-1}, \beta_tI \right)

是人为固定的加噪规则。需要学习的是反向过程:

pθ(xt−1∣xt) p_\theta(x_{t-1}|x_t)

10.2 为什么训练时不用一步步加噪

因为有闭式公式:

xt=αˉtx0+1−αˉtϵ x_t {=} \sqrt{\bar{\alpha}_t}x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon

它可以直接从 x0x_0 得到任意时间步的 xtx_t。

10.3 为什么目标是预测噪声,而不是预测图像

因为训练时噪声 ϵ\epsilon 是我们自己采样出来的,天然就是监督标签。预测噪声后,可以通过公式恢复 x0x_0 或构造反向均值:

μθ(xt,t)=1αt(xt−βt1−αˉtϵθ(xt,t)) \mu_\theta(x_t,t) {=} \frac{1}{\sqrt{\alpha_t}} \left( x_t {-} \frac{ \beta_t }{ \sqrt{1-\bar{\alpha}_t} } \epsilon_\theta(x_t,t) \right)

所以预测噪声并不弱,它等价于学习反向去噪步骤。

10.4 DDPM 和 VAE 最大区别是什么

VAE 是:

z∼N(0,I),x∼pθ(x∣z) z\sim\mathcal{N}(0,I), \quad x\sim p_\theta(x|z)

也就是从潜变量一步生成图像。

DDPM 是:

xT∼N(0,I) x_T\sim\mathcal{N}(0,I)
xT→xT−1→⋯→x0 x_T\rightarrow x_{T-1}\rightarrow\cdots\rightarrow x_0

也就是从噪声开始多步去噪生成图像。

10.5 DDPM 的缺点是什么

主要缺点是采样慢。因为它通常需要从 TT 到 11 逐步反推,基础版本可能需要几百到上千步。后续可以用 DDIM、加速采样器或蒸馏方法减少采样步数。


11. 总结

DDPM 的核心逻辑是:

  1. 固定前向加噪:
q(xt∣xt−1)=N(xt;αtxt−1,βtI) q(x_t|x_{t-1}) {=} \mathcal{N} \left( x_t; \sqrt{\alpha_t}x_{t-1}, \beta_tI \right)
  1. 推出任意时刻加噪:
xt=αˉtx0+1−αˉtϵ x_t {=} \sqrt{\bar{\alpha}_t}x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon
  1. 学习反向去噪:
pθ(xt−1∣xt)=N(xt−1;μθ(xt,t),σt2I) p_\theta(x_{t-1}|x_t) {=} \mathcal{N} \left( x_{t-1}; \mu_\theta(x_t,t), \sigma_t^2I \right)
  1. 用 ELBO 得到真实后验匹配:
DKL(q(xt−1∣xt,x0)∥pθ(xt−1∣xt)) D_{\mathrm{KL}} \left( q(x_{t-1}|x_t,x_0) \| p_\theta(x_{t-1}|x_t) \right)
  1. 把高斯 KL 转成均值 MSE,再转成噪声预测 MSE:
Lsimple(θ)=Et,x0,ϵ[∥ϵ−ϵθ(xt,t)∥2] \boxed{ \mathcal{L}_{\mathrm{simple}}(\theta) {=} \mathbb{E}_{t,x_0,\epsilon} \left[ \left\| \epsilon-\epsilon_\theta(x_t,t) \right\|^2 \right] }

与 VAE 相比,DDPM 不把全部图像信息压缩进一个低维潜变量,而是在图像空间中逐步去噪。它的训练目标来自概率推导,但最终落地成非常简单的监督学习问题。


12. 课后习题

  1. VAE 为什么容易生成模糊图像?
  2. DDPM 为什么不直接从潜变量一步生成图像?
  3. 前向过程中的 βt\beta_t 控制什么?
  4. 为什么一步加噪公式中要使用平方根系数?
  5. 如何从 x0x_0 直接采样任意时间步的 xtx_t?
  6. 为什么 xTx_T 会接近标准高斯噪声?
  7. DDPM 的反向过程为什么要建模为高斯分布?
  8. ELBO 分解中的中间 KL 项在约束什么?
  9. 为什么真实后验 q(xt−1∣xt,x0)q(x_{t-1}|x_t,x_0) 有解析解?
  10. 预测噪声 ϵ\epsilon 为什么等价于学习反向去噪?
  11. 采样时为什么最后一步通常不再加噪声?
  12. 如果没有时间嵌入,DDPM 网络会遇到什么问题?

附录 A:DDPM ELBO 分解详细推导

DDPM 最小化负 ELBO:

L=Eq(x1:T∣x0)[−log⁡pθ(x0:T)q(x1:T∣x0)] \mathcal{L} {=} \mathbb{E}_{q(x_{1:T}|x_0)} \left[ {-} \log \frac{ p_\theta(x_{0:T}) }{ q(x_{1:T}|x_0) } \right]

展开模型联合分布:

pθ(x0:T)=p(xT)∏t=1Tpθ(xt−1∣xt) p_\theta(x_{0:T}) {=} p(x_T) \prod_{t=1}^{T} p_\theta(x_{t-1}|x_t)

展开前向分布:

q(x1:T∣x0)=∏t=1Tq(xt∣xt−1) q(x_{1:T}|x_0) {=} \prod_{t=1}^{T} q(x_t|x_{t-1})

代入:

L=Eq[−log⁡p(xT)∏t=1Tpθ(xt−1∣xt)∏t=1Tq(xt∣xt−1)] \mathcal{L} {=} \mathbb{E}_q \left[ {-} \log \frac{ p(x_T) \prod_{t=1}^{T} p_\theta(x_{t-1}|x_t) }{ \prod_{t=1}^{T} q(x_t|x_{t-1}) } \right]

拆开对数:

L=Eq[−log⁡p(xT)−∑t=1Tlog⁡pθ(xt−1∣xt)+∑t=1Tlog⁡q(xt∣xt−1)] \mathcal{L} {=} \mathbb{E}_q \left[ {-} \log p(x_T) {-} \sum_{t=1}^{T} \log p_\theta(x_{t-1}|x_t) + \sum_{t=1}^{T} \log q(x_t|x_{t-1}) \right]

对 t>1t>1 使用:

q(xt−1∣xt,x0)=q(xt∣xt−1)q(xt−1∣x0)q(xt∣x0) q(x_{t-1}|x_t,x_0) {=} \frac{ q(x_t|x_{t-1})q(x_{t-1}|x_0) }{ q(x_t|x_0) }

所以:

q(xt∣xt−1)=q(xt−1∣xt,x0)q(xt∣x0)q(xt−1∣x0) q(x_t|x_{t-1}) {=} \frac{ q(x_{t-1}|x_t,x_0)q(x_t|x_0) }{ q(x_{t-1}|x_0) }

将这些项代入并整理 telescoping 项,可以得到:

L=Eq[−log⁡p(xT)q(xT∣x0)−∑t>1log⁡pθ(xt−1∣xt)q(xt−1∣xt,x0)−log⁡pθ(x0∣x1)] \mathcal{L} {=} \mathbb{E}_q \left[ {-} \log \frac{ p(x_T) }{ q(x_T|x_0) } {-} \sum_{t>1} \log \frac{ p_\theta(x_{t-1}|x_t) }{ q(x_{t-1}|x_t,x_0) } {-} \log p_\theta(x_0|x_1) \right]

写成 KL 形式:

L=Eq[DKL(q(xT∣x0)∥p(xT))+∑t>1DKL(q(xt−1∣xt,x0)∥pθ(xt−1∣xt))−log⁡pθ(x0∣x1)] \boxed{ \mathcal{L} {=} \mathbb{E}_q \left[ D_{\mathrm{KL}} \left( q(x_T|x_0)\|p(x_T) \right) + \sum_{t>1} D_{\mathrm{KL}} \left( q(x_{t-1}|x_t,x_0) \| p_\theta(x_{t-1}|x_t) \right) {-} \log p_\theta(x_0|x_1) \right] }

这个式子说明:训练 DDPM 的核心,就是让模型的反向一步分布匹配前向过程推出来的真实后验。


附录 B:真实后验 q(xt−1∣xt,x0)q(x_{t-1}|x_t,x_0) 的详细推导

令:

z=xt−1 z=x_{t-1}

我们要求:

q(z∣xt,x0) q(z|x_t,x_0)

根据贝叶斯公式:

q(z∣xt,x0)∝q(xt∣z)q(z∣x0) q(z|x_t,x_0) \propto q(x_t|z)q(z|x_0)

其中:

q(xt∣z)=N(xt;αtz,βtI) q(x_t|z) {=} \mathcal{N} \left( x_t; \sqrt{\alpha_t}z, \beta_tI \right)
q(z∣x0)=N(z;αˉt−1x0,(1−αˉt−1)I) q(z|x_0) {=} \mathcal{N} \left( z; \sqrt{\bar{\alpha}_{t-1}}x_0, (1-\bar{\alpha}_{t-1})I \right)

只看与 zz 有关的指数部分:

q(z∣xt,x0)∝exp⁡(−12βt∥xt−αtz∥2−12(1−αˉt−1)∥z−αˉt−1x0∥2) q(z|x_t,x_0) \propto \exp \left( {-} \frac{1}{2\beta_t} \left\| x_t-\sqrt{\alpha_t}z \right\|^2 {-} \frac{1}{2(1-\bar{\alpha}_{t-1})} \left\| z-\sqrt{\bar{\alpha}_{t-1}}x_0 \right\|^2 \right)

展开第一项:

∥xt−αtz∥2=∥xt∥2−2αtxt⊤z+αt∥z∥2 \left\| x_t-\sqrt{\alpha_t}z \right\|^2 {=} \left\|x_t\right\|^2 {-} 2\sqrt{\alpha_t}x_t^\top z + \alpha_t\left\|z\right\|^2

与 zz 有关的部分是:

−αt2βt∥z∥2+αtβtxt⊤z {-} \frac{\alpha_t}{2\beta_t} \left\|z\right\|^2 + \frac{\sqrt{\alpha_t}}{\beta_t} x_t^\top z

展开第二项:

∥z−αˉt−1x0∥2=∥z∥2−2αˉt−1x0⊤z+αˉt−1∥x0∥2 \left\| z-\sqrt{\bar{\alpha}_{t-1}}x_0 \right\|^2 {=} \left\|z\right\|^2 {-} 2\sqrt{\bar{\alpha}_{t-1}}x_0^\top z + \bar{\alpha}_{t-1} \left\|x_0\right\|^2

与 zz 有关的部分是:

−12(1−αˉt−1)∥z∥2+αˉt−11−αˉt−1x0⊤z {-} \frac{1}{2(1-\bar{\alpha}_{t-1})} \left\|z\right\|^2 + \frac{\sqrt{\bar{\alpha}_{t-1}}}{1-\bar{\alpha}_{t-1}} x_0^\top z

合并:

log⁡q(z∣xt,x0)=−12(αtβt+11−αˉt−1)∥z∥2+(αtβtxt+αˉt−11−αˉt−1x0)⊤z+C \log q(z|x_t,x_0) {=} {-} \frac{1}{2} \left( \frac{\alpha_t}{\beta_t} + \frac{1}{1-\bar{\alpha}_{t-1}} \right) \left\|z\right\|^2 + \left( \frac{\sqrt{\alpha_t}}{\beta_t}x_t + \frac{\sqrt{\bar{\alpha}_{t-1}}}{1-\bar{\alpha}_{t-1}}x_0 \right)^\top z + C

高斯分布的标准指数形式是:

log⁡N(z;μ,Σ)=−12z⊤Σ−1z+z⊤Σ−1μ+C \log\mathcal{N}(z;\mu,\Sigma) {=} {-} \frac{1}{2} z^\top\Sigma^{-1}z + z^\top\Sigma^{-1}\mu + C

对比二次项系数:

Σ−1=(αtβt+11−αˉt−1)I \Sigma^{-1} {=} \left( \frac{\alpha_t}{\beta_t} + \frac{1}{1-\bar{\alpha}_{t-1}} \right)I

所以:

Σ=(αtβt+11−αˉt−1)−1I \Sigma {=} \left( \frac{\alpha_t}{\beta_t} + \frac{1}{1-\bar{\alpha}_{t-1}} \right)^{-1}I

通分:

Σ=βt(1−αˉt−1)αt(1−αˉt−1)+βtI \Sigma {=} \frac{ \beta_t(1-\bar{\alpha}_{t-1}) }{ \alpha_t(1-\bar{\alpha}_{t-1})+\beta_t } I

因为:

αˉt=αtαˉt−1 \bar{\alpha}_t=\alpha_t\bar{\alpha}_{t-1}

且:

αt+βt=1 \alpha_t+\beta_t=1

所以:

αt(1−αˉt−1)+βt=1−αˉt \alpha_t(1-\bar{\alpha}_{t-1})+\beta_t {=} 1-\bar{\alpha}_t

因此:

Σ=βt(1−αˉt−1)1−αˉtI \Sigma {=} \frac{ \beta_t(1-\bar{\alpha}_{t-1}) }{ 1-\bar{\alpha}_t } I

即:

β~t=1−αˉt−11−αˉtβt \boxed{ \tilde{\beta}_t {=} \frac{ 1-\bar{\alpha}_{t-1} }{ 1-\bar{\alpha}_t } \beta_t }

再求均值。由一次项系数:

Σ−1μ=αtβtxt+αˉt−11−αˉt−1x0 \Sigma^{-1}\mu {=} \frac{\sqrt{\alpha_t}}{\beta_t}x_t + \frac{\sqrt{\bar{\alpha}_{t-1}}}{1-\bar{\alpha}_{t-1}}x_0

所以:

μ=Σ(αtβtxt+αˉt−11−αˉt−1x0) \mu {=} \Sigma \left( \frac{\sqrt{\alpha_t}}{\beta_t}x_t + \frac{\sqrt{\bar{\alpha}_{t-1}}}{1-\bar{\alpha}_{t-1}}x_0 \right)

代入 Σ\Sigma:

μ=βt(1−αˉt−1)1−αˉt(αtβtxt+αˉt−11−αˉt−1x0) \mu {=} \frac{ \beta_t(1-\bar{\alpha}_{t-1}) }{ 1-\bar{\alpha}_t } \left( \frac{\sqrt{\alpha_t}}{\beta_t}x_t + \frac{\sqrt{\bar{\alpha}_{t-1}}}{1-\bar{\alpha}_{t-1}}x_0 \right)

得到:

μ=αt(1−αˉt−1)1−αˉtxt+αˉt−1βt1−αˉtx0 \mu {=} \frac{ \sqrt{\alpha_t}(1-\bar{\alpha}_{t-1}) }{ 1-\bar{\alpha}_t } x_t + \frac{ \sqrt{\bar{\alpha}_{t-1}}\beta_t }{ 1-\bar{\alpha}_t } x_0

所以:

μ~t(xt,x0)=αˉt−1βt1−αˉtx0+αt(1−αˉt−1)1−αˉtxt \boxed{ \tilde{\mu}_t(x_t,x_0) {=} \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 }

附录 C:从预测噪声推导反向均值公式

已知:

μθ(xt,t)=αˉt−1βt1−αˉtx0,θ(xt,t)+αt(1−αˉt−1)1−αˉtxt \mu_\theta(x_t,t) {=} \frac{ \sqrt{\bar{\alpha}_{t-1}}\beta_t }{ 1-\bar{\alpha}_t } x_{0,\theta}(x_t,t) + \frac{ \sqrt{\alpha_t}(1-\bar{\alpha}_{t-1}) }{ 1-\bar{\alpha}_t } x_t

又有:

x0,θ(xt,t)=xt−1−αˉtϵθ(xt,t)αˉt x_{0,\theta}(x_t,t) {=} \frac{ x_t-\sqrt{1-\bar{\alpha}_t}\epsilon_\theta(x_t,t) }{ \sqrt{\bar{\alpha}_t} }

代入:

μθ(xt,t)=αˉt−1βt1−αˉt⋅xt−1−αˉtϵθ(xt,t)αˉt+αt(1−αˉt−1)1−αˉtxt \mu_\theta(x_t,t) {=} \frac{ \sqrt{\bar{\alpha}_{t-1}}\beta_t }{ 1-\bar{\alpha}_t } \cdot \frac{ x_t-\sqrt{1-\bar{\alpha}_t}\epsilon_\theta(x_t,t) }{ \sqrt{\bar{\alpha}_t} } + \frac{ \sqrt{\alpha_t}(1-\bar{\alpha}_{t-1}) }{ 1-\bar{\alpha}_t } x_t

因为:

αˉt=αtαˉt−1 \bar{\alpha}_t=\alpha_t\bar{\alpha}_{t-1}

所以:

αˉt−1αˉt=1αt \frac{ \sqrt{\bar{\alpha}_{t-1}} }{ \sqrt{\bar{\alpha}_t} } {=} \frac{1}{\sqrt{\alpha_t}}

于是:

μθ(xt,t)=βtαt(1−αˉt)xt−βtαt1−αˉtϵθ(xt,t)+αt(1−αˉt−1)1−αˉtxt \mu_\theta(x_t,t) {=} \frac{ \beta_t }{ \sqrt{\alpha_t}(1-\bar{\alpha}_t) } x_t {-} \frac{ \beta_t }{ \sqrt{\alpha_t}\sqrt{1-\bar{\alpha}_t} } \epsilon_\theta(x_t,t) + \frac{ \sqrt{\alpha_t}(1-\bar{\alpha}_{t-1}) }{ 1-\bar{\alpha}_t } x_t

合并 xtx_t 系数:

βtαt(1−αˉt)+αt(1−αˉt−1)1−αˉt \frac{ \beta_t }{ \sqrt{\alpha_t}(1-\bar{\alpha}_t) } + \frac{ \sqrt{\alpha_t}(1-\bar{\alpha}_{t-1}) }{ 1-\bar{\alpha}_t }

通分:

βt+αt(1−αˉt−1)αt(1−αˉt) \frac{ \beta_t+\alpha_t(1-\bar{\alpha}_{t-1}) }{ \sqrt{\alpha_t}(1-\bar{\alpha}_t) }

因为:

βt+αt=1 \beta_t+\alpha_t=1

且:

αtαˉt−1=αˉt \alpha_t\bar{\alpha}_{t-1}=\bar{\alpha}_t

所以:

βt+αt(1−αˉt−1)=βt+αt−αtαˉt−1=1−αˉt \beta_t+\alpha_t(1-\bar{\alpha}_{t-1}) {=} \beta_t+\alpha_t-\alpha_t\bar{\alpha}_{t-1} {=} 1-\bar{\alpha}_t

因此 xtx_t 的系数是:

1αt \frac{1}{\sqrt{\alpha_t}}

最终:

μθ(xt,t)=1αtxt−βtαt1−αˉtϵθ(xt,t) \mu_\theta(x_t,t) {=} \frac{1}{\sqrt{\alpha_t}}x_t {-} \frac{ \beta_t }{ \sqrt{\alpha_t}\sqrt{1-\bar{\alpha}_t} } \epsilon_\theta(x_t,t)

提取 1αt\frac{1}{\sqrt{\alpha_t}}:

μθ(xt,t)=1αt(xt−βt1−αˉtϵθ(xt,t)) \boxed{ \mu_\theta(x_t,t) {=} \frac{1}{\sqrt{\alpha_t}} \left( x_t {-} \frac{ \beta_t }{ \sqrt{1-\bar{\alpha}_t} } \epsilon_\theta(x_t,t) \right) }