DDPM 深度剖析:变分推断视角下的去噪扩散概率模型

4848 字
24 分钟
DDPM 深度剖析:变分推断视角下的去噪扩散概率模型

1. DDPM 的诞生:为什么是 2020 年?#

1.1 在 DDPM 之前:两段独立的历史#

扩散概率模型并非凭空出现——它由两条独立发展的理论脉络交汇而成。

历史脉络 A:变分推断

2002 VAE (Kingma & Welling)
- 变分下界 (ELBO)
- 重参数化技巧
- 重构 + KL 正则化
2019 Sohl-Dickstein et al.
- 扩散概率模型 (Deep Unsupervised Learning using Nonequilibrium Thermodynamics)
- 马尔可夫前向 + 逆向
- 但太慢,生成需 ~1000 步

历史脉络 B:Score Matching

2011 Score Matching (Hyvarinen)
- 学 ∇_x log p(x) — 不需要归一化常数
2019 Song et al. — Noise Conditional Score Network (NCSN)
- 用 Score Matching 学噪声尺度下的对数密度梯度
- 朗之万采样生成
2021 Song et al. — Score SDE
- 把 NCSN 统一到 SDE 框架

2020 年,Ho et al. 把两条脉络合二为一

DDPM = Sohl-Dickstein 扩散的工程化 + Score Matching 的数学优雅 + 简单的 MSE 训练目标

1.2 一句话概括 DDPM#

DDPM 是一个分两步走的生成模型:① 预设一个将数据逐步破坏成纯噪声的前向过程(马尔可夫链);② 训练一个神经网络去学习逆向过程——从噪声一步步恢复出原始数据。用变分推断证明,训练等价于简单的 MSE(预测噪声或 x0x_0)。

1.3 DDPM 论文信息#

论文: "Denoising Diffusion Probabilistic Models" (DDPM)
作者: Jonathan Ho, Ajay Jain, Pieter Abbeel
单位: Google Brain, UC Berkeley
发表于: NeurIPS 2020
引用: > 15,000 次 (截至 2024)
开源: https://github.com/hojonathanho/diffusion

2. 前向过程:预设的加噪马尔可夫链#

2.1 马尔可夫链定义#

设原始数据为 x0q(x0)x_0 \sim q(x_0)(真实数据分布)。前向过程定义为一个 TT 步马尔可夫链,每一步添加少量高斯噪声:

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

其中 {βt}t=1T\{\beta_t\}_{t=1}^T噪声调度(noise schedule),满足 0<β1<β2<<βT<10 < \beta_1 < \beta_2 < \cdots < \beta_T < 1βt\beta_ttt 递增。

def forward_step(xt_minus_1, beta_t):
"""前向过程的单步: x_{t-1} → x_t"""
mean = torch.sqrt(1 - beta_t) * xt_minus_1
std = torch.sqrt(beta_t)
noise = torch.randn_like(xt_minus_1)
return mean + std * noise

2.2 关键性质:闭式分布#

不用逐步递推! 通过重参数化技巧,任意时刻 tt 的分布可以直接写出:

q(xtx0)=N(xt;αˉtx0,  (1αˉt)I)q(x_t | x_0) = \mathcal{N}\left(x_t; \sqrt{\bar\alpha_t} x_0, \; (1 - \bar\alpha_t) I\right)

其中:

αt=1βt,αˉt=s=1tαs=s=1t(1βs)\alpha_t = 1 - \beta_t, \quad \bar\alpha_t = \prod_{s=1}^t \alpha_s = \prod_{s=1}^t (1 - \beta_s)

推导

ϵ1,ϵ2,,ϵtN(0,I)\epsilon_1, \epsilon_2, \ldots, \epsilon_t \sim \mathcal{N}(0, I) 独立,则:

xt=αtxt1+1αtϵ1=αtαt1xt2+1αtϵ1+αt(1αt1)ϵ2==αˉtx0+1αˉtϵ\begin{aligned} x_t &= \sqrt{\alpha_t} x_{t-1} + \sqrt{1-\alpha_t} \epsilon_1 \\ &= \sqrt{\alpha_t \alpha_{t-1}} x_{t-2} + \sqrt{1-\alpha_t} \epsilon_1 + \sqrt{\alpha_t(1-\alpha_{t-1})} \epsilon_2 \\ &= \cdots \\ &= \sqrt{\bar\alpha_t} x_0 + \sqrt{1 - \bar\alpha_t} \epsilon \end{aligned}

其中 ϵN(0,I)\epsilon \sim \mathcal{N}(0, I) 是标准高斯噪声。

def closed_form_forward(x0, t, alphas_cumprod):
"""
闭式前向分布: q(x_t | x_0)
参数:
x0: (batch_size, ...) 原始数据
t: (batch_size,) 时间步 (整数或浮点)
alphas_cumprod: (T,) cumprod 数组
返回: (batch_size, ...) 在 t 时刻加噪后的数据
"""
idx = t.long() # 安全取整数索引
sqrt_alpha_bar = torch.sqrt(alphas_cumprod[idx])
sqrt_one_minus = torch.sqrt(1 - alphas_cumprod[idx])
noise = torch.randn_like(x0)
B, C, H, W = x0.shape
reshape_shape = (B, 1, 1, 1)
return sqrt_alpha_bar.view(*reshape_shape) * x0 \
+ sqrt_one_minus.view(*reshape_shape) * noise

2.3 噪声调度的直观理解#

αˉt\sqrt{\bar\alpha_t}1αˉt\sqrt{1-\bar\alpha_t} 随时间步变化:

  • t=0t = 0αˉt1.0,1αˉt0\sqrt{\bar\alpha_t} \approx 1.0, \sqrt{1-\bar\alpha_t} \approx 0,对应原始数据
  • t=500t = 500:两者约各占一半,噪声和数据混合
  • t=Tt = Tαˉt0,1αˉt1.0\sqrt{\bar\alpha_t} \approx 0, \sqrt{1-\bar\alpha_t} \approx 1.0,接近纯噪声
时刻信号占比噪声占比含义
t=0t = 0极低原始数据
t=500t = 500中等中等噪声和数据各占一半
t=Tt = T极低接近纯噪声

核心洞察:当 t=Tt = T 时,αˉT0\bar\alpha_T \approx 0,所以 xTN(0,I)x_T \approx \mathcal{N}(0, I)——前向过程把任意数据分布变成了标准高斯噪声。

2.4 常见噪声调度#

class NoiseSchedule:
"""三种常见噪声调度。"""
@staticmethod
def linear(T, beta_start=1e-4, beta_end=0.02):
"""线性调度 (DDPM 原始论文使用)。"""
betas = torch.linspace(beta_start, beta_end, T)
return betas
@staticmethod
def cosine(T, s=0.008):
"""余弦调度 (改进版, 噪声衰减更平滑)。"""
steps = torch.arange(T + 1)
# \bar\alpha_t = cos²( (t/T + s)/(1+s) * π/2 )
alphas_cumprod = torch.cos((steps / T + s) / (1 + s) * (torch.pi / 2)) ** 2
alphas_cumprod = alphas_cumprod / alphas_cumprod[0] # 归一化: \bar\alpha_0 = 1
betas = 1 - alphas_cumprod[1:] / alphas_cumprod[:-1]
return betas
@staticmethod
def sigmoid(T, beta_start=1e-4, beta_end=0.02):
"""S 形调度 (介于线性和余弦之间)。"""
betas = torch.linspace(-6, 6, T)
betas = torch.sigmoid(betas) * (beta_end - beta_start) + beta_start
return betas
ᾱ_t 曲线对比:
1.0 ─────────────────────────────────────────
╲ Linear (更快变噪声)
╲ Cosine (更平滑)
──────────────────────── 0.0 t

3. 逆向过程:学习的去噪分布#

3.1 逆向分布的必要性#

如果能精确知道 q(xt1xt)q(x_{t-1} | x_t),就可以从纯噪声 xTN(0,I)x_T \sim \mathcal{N}(0, I) 精确恢复数据 x0x_0

q(xt1xt)q(x_{t-1} | x_t) 不可直接计算(需要对所有 x0x_0 积分),所以用神经网络 pθ(xt1xt)p_\theta(x_{t-1} | x_t)近似

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

3.2 逆向过程的参数化#

均值参数化(最直觉的写法):

pθ(xt1xt)=N(xt1;μθ(xt,t),β~tI)p_\theta(x_{t-1} | x_t) = \mathcal{N}\left(x_{t-1}; \mu_\theta(x_t, t), \tilde\beta_t I\right)

但更常用的是直接预测噪声或 x0x_0,而不是直接预测均值——因为这让目标函数成为简单的 MSE。

3.3 均值与噪声的关系#

由前向过程的闭式公式,可以反解出均值:

μt(xt,x0)=1αt(xt1αtϵ)\mu_t(x_t, x_0) = \frac{1}{\sqrt{\alpha_t}} \left( x_t - \sqrt{1 - \alpha_t} \epsilon \right)

其中 ϵ\epsilon 是注入的噪声。由 xt=αˉtx0+1αˉtϵx_t = \sqrt{\bar\alpha_t} x_0 + \sqrt{1-\bar\alpha_t} \epsilon,得:

x0=xt1αˉtϵαˉtx_0 = \frac{x_t - \sqrt{1-\bar\alpha_t} \epsilon}{\sqrt{\bar\alpha_t}}

代入均值表达式:

μt(xt,x0)=1αt(xtβtxtαˉtx01αˉt)\mu_t(x_t, x_0) = \frac{1}{\sqrt{\alpha_t}} \left( x_t - \beta_t \cdot \frac{x_t - \sqrt{\bar\alpha_t} x_0}{\sqrt{1-\bar\alpha_t}} \right)

或者,直接用噪声 ϵ\epsilonx0x_0 来表达(这是神经网络实际预测的):

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

网络 ϵθ(xt,t)\epsilon_\theta(x_t, t) 预测注入的噪声 ϵ\epsilon,从而反推均值。

4. 变分推断与 ELBO#

4.1 生成模型的对数似然目标#

我们想最大化生成数据的对数似然 logpθ(x0)\log p_\theta(x_0)。但 logpθ(x0)\log p_\theta(x_0) 直接计算困难(需要对所有隐变量积分),所以用变分推断引入隐变量 x1:Tx_{1:T}

logpθ(x0)=logpθ(x0:T)dx1:T\log p_\theta(x_0) = \log \int p_\theta(x_{0:T}) \, dx_{1:T}

4.2 ELBO 的引入#

对任意分布 q(x1:Tx0)q(x_{1:T} | x_0)(建议分布,即前向过程),用 Jensen 不等式得到变分下界 (ELBO)

logpθ(x0)Eq[logpθ(x0:T)q(x1:Tx0)]=L\log p_\theta(x_0) \geq \mathbb{E}_{q} \left[ \log \frac{p_\theta(x_{0:T})}{q(x_{1:T}|x_0)} \right] = \mathcal{L}

展开 pθ(x0:T)=p(xT)t=1Tpθ(xt1xt)p_\theta(x_{0:T}) = p(x_T) \prod_{t=1}^T p_\theta(x_{t-1}|x_t)q(x1:Tx0)=t=1Tq(xtxt1)q(x_{1:T}|x_0) = \prod_{t=1}^T q(x_t|x_{t-1}),得到 ELBO 的分项形式:

L=logp(xT)LT+t=1TEq[DKL(q(xt1xt,x0)pθ(xt1xt))]Lt1+Eq[logpθ(x0x1)]L0\mathcal{L} = \underbrace{-\log p(x_T)}_{-\mathcal{L}_T} + \sum_{t=1}^T \underbrace{\mathbb{E}_q \left[ D_{\mathrm{KL}}(q(x_{t-1}|x_t, x_0) \| p_\theta(x_{t-1}|x_t)) \right]}_{-\mathcal{L}_{t-1}} + \underbrace{\mathbb{E}_q \left[ \log p_\theta(x_0 | x_1) \right]}_{-\mathcal{L}_0}

4.3 ELBO 三项的物理含义#

ELBO = -[ L_T + L_{T-1} + ... + L_1 + L_0 ]
↓ ↓ ↓ ↓
终点 中间步 中间步 重构项
KL项 KL项 KL项 重构损失

ELBO 包含三项,每项对应不同的训练阶段:

名称含义训练时是否需要
LT\mathcal{L}_T终点先验 KLq(xTx0)q(x_T \mid x_0)p(xT)p(x_T) 的 KL,TT 足够大时自动满足
Lt1\mathcal{L}_{t-1}去噪匹配项q(xt1xt,x0)q(x_{t-1} \mid x_t, x_0)(可解析)与 pθ(xt1xt)p_\theta(x_{t-1} \mid x_t) 的 KL核心
L0\mathcal{L}_0重构项t=1t = 1 时从 x1x_1 重构 x0x_0✅(可与 L1\mathcal{L}_1 合并)

4.4 关键:q(xt1xt,x0)q(x_{t-1} | x_t, x_0) 有闭式解#

定理:给定 x0x_0xtx_t,逆向条件分布 q(xt1xt,x0)q(x_{t-1} | x_t, x_0) 也是高斯分布,可以直接写出均值和方差:

q(xt1xt,x0)=N(xt1;μ~t(xt,x0),β~tI)q(x_{t-1} | x_t, x_0) = \mathcal{N}\left(x_{t-1}; \tilde\mu_t(x_t, x_0), \tilde\beta_t I\right)

其中:

μ~t(xt,x0)=αˉt1βt1αˉtx0+αt(1αˉt1)1αˉtxt\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αˉt11αˉtβt\tilde\beta_t = \frac{1 - \bar\alpha_{t-1}}{1 - \bar\alpha_t} \cdot \beta_t
def reverse_posterior_mean(x0, xt, t, alphas_cumprod, betas):
"""
计算 q(x_{t-1} | x_t, x_0) 的均值 (闭式)。
这是在 ELBO 中,用于计算 L_{t-1} KL 项的 target mean。
"""
alpha_bar_t = alphas_cumprod[t]
alpha_bar_tm1 = alphas_cumprod[t - 1]
alpha_t = 1 - betas[t]
# 从 xt 解出注入的噪声
sqrt_alpha_bar_t = torch.sqrt(alpha_bar_t)
sqrt_one_minus = torch.sqrt(1 - alpha_bar_t)
eps = (xt - sqrt_alpha_bar_t * x0) / sqrt_one_minus
# 计算均值 (用噪声表示)
sqrt_alpha_bar_tm1 = torch.sqrt(alpha_bar_tm1)
beta_t = betas[t]
# 方法 1: 用 x0 和 xt 表示
mean = (sqrt_alpha_bar_tm1 * beta_t / (1 - alpha_bar_t)) * x0 \
+ (torch.sqrt(alpha_t) * (1 - alpha_bar_tm1) / (1 - alpha_bar_t)) * xt
# 方法 2: 用噪声 ε 表示 (更常用, 神经网络预测 ε)
mean_eps = (xt - torch.sqrt(1 - alpha_bar_t) * beta_t / (1 - alpha_bar_t).sqrt() * eps) \
/ alpha_t.sqrt()
return mean_eps

4.5 ELBO 的简化#

在 DDPM 论文中,Lt1\mathcal{L}_{t-1} 简化为:

Lt1Et,x0,ϵ[βt22σt2(1αt)(1αˉt)ϵϵθ(αˉtx0+1αˉtϵ,t)2]\mathcal{L}_{t-1} \approx \mathbb{E}_{t, x_0, \epsilon} \left[ \frac{\beta_t^2}{2 \sigma_t^2 (1-\alpha_t)(1-\bar\alpha_t)} \left\| \epsilon - \epsilon_\theta(\sqrt{\bar\alpha_t}x_0 + \sqrt{1-\bar\alpha_t}\epsilon, t) \right\|^2 \right]

论文发现,去掉复杂的系数后,简化为:

Lt1simple=Et,x0,ϵϵϵθ(xt,t)2\mathcal{L}_{t-1}^{\text{simple}} = \mathbb{E}_{t, x_0, \epsilon} \left\| \epsilon - \epsilon_\theta(x_t, t) \right\|^2

这意味着:训练 DDPM 等价于训练一个 MSE 损失——预测注入的噪声 ϵ\epsilon

5. 三种参数化方式#

5.1 噪声预测 ϵ\epsilon-prediction#

网络输出:注入的噪声 ϵ\epsilon 目标ϵθ(xt,t)ϵ\epsilon_\theta(x_t, t) \approx \epsilon

Lsimple=Et,x0,ϵ[ϵϵθ(xt,t)2]\mathcal{L}_{\text{simple}} = \mathbb{E}_{t, x_0, \epsilon} \left[ \| \epsilon - \epsilon_\theta(x_t, t) \|^2 \right]
def loss_eps_prediction(model, x0, t, alphas_cumprod):
"""DDPM 标准噪声预测损失。"""
eps = torch.randn_like(x0)
xt = torch.sqrt(alphas_cumprod[t]) * x0 + torch.sqrt(1 - alphas_cumprod[t]) * eps
pred_eps = model(xt, t)
return F.mse_loss(pred_eps, eps)

5.2 x0x_0 预测 (x-prediction)#

网络输出:原始数据 x0x_0 目标x^0=xθ(xt,t)x0\hat{x}_0 = x_\theta(x_t, t) \approx x_0

x0=(xt1αˉtϵ)/αˉtx_0 = (x_t - \sqrt{1-\bar\alpha_t} \epsilon) / \sqrt{\bar\alpha_t}

x0=xt1αˉtϵθ(xt,t)αˉtx_0 = \frac{x_t - \sqrt{1 - \bar\alpha_t} \epsilon_\theta(x_t, t)}{\sqrt{\bar\alpha_t}}
def loss_x0_prediction(model, x0, t, alphas_cumprod):
"""x0 预测损失。"""
eps = torch.randn_like(x0)
xt = torch.sqrt(alphas_cumprod[t]) * x0 + torch.sqrt(1 - alphas_cumprod[t]) * eps
pred_x0 = model(xt, t)
return F.mse_loss(pred_x0, x0)

5.3 速度预测 v-prediction#

网络输出:速度向量 v=αtϵαˉtx0v = \alpha_t \epsilon - \sqrt{\bar\alpha_t} x_0 目标vθ(xt,t)vv_\theta(x_t, t) \approx v

xt=αˉtx0+1αˉtϵx_t = \sqrt{\bar\alpha_t} x_0 + \sqrt{1-\bar\alpha_t} \epsilon,对 tt 求导得:

v=dxtdt=dαˉtdtx0+d1αˉtdtϵv = \frac{dx_t}{dt} = -\frac{d\sqrt{\bar\alpha_t}}{dt} x_0 + \frac{d\sqrt{1-\bar\alpha_t}}{dt} \epsilon

在实际中,DDPM++ 论文使用:

v=αˉtϵ1αˉtx0v = \sqrt{\bar\alpha_t} \epsilon - \sqrt{1-\bar\alpha_t} x_0
def loss_v_prediction(model, x0, t, alphas_cumprod):
"""速度预测损失 (DDPM++ / ADM 采用)。"""
eps = torch.randn_like(x0)
sqrt_ab = torch.sqrt(alphas_cumprod[t])
sqrt_1m = torch.sqrt(1 - alphas_cumprod[t])
xt = sqrt_ab * x0 + sqrt_1m * eps
# 真实速度
v = sqrt_ab * eps - sqrt_1m * x0
pred_v = model(xt, t)
return F.mse_loss(pred_v, v)

5.4 三种参数化对比#

维度噪声预测 (DDPM)x0 预测速度预测 (DDPM++)
网络输出ϵ\epsilonx0x_0vv
损失函数MSEMSEMSE
低噪声时 (t0t \to 0)最好
高噪声时 (tTt \to T)
训练稳定性
典型应用DDPM, SD 1/2/xlADM, DiT, MMDiT

6. 训练与采样完整流程#

6.1 训练算法#

def ddpm_train_step(model, x0, alphas_cumprod, betas):
"""
DDPM 训练步 (Algorithm 1, Ho et al. 2020)。
"""
B = x0.shape[0]
# 1) 采样时间步 t ~ Uniform{1, ..., T}
t = torch.randint(0, len(alphas_cumprod), (B,), device=x0.device)
# 2) 采样噪声 ε ~ N(0, I)
eps = torch.randn_like(x0)
# 3) 前向加噪 (闭式)
sqrt_ab = alphas_cumprod[t] ** 0.5
sqrt_1m = (1 - alphas_cumprod[t]) ** 0.5
xt = sqrt_ab.view(-1, *([1] * (x0.dim() - 1))) * x0 \
+ sqrt_1m.view(-1, *([1] * (x0.dim() - 1))) * eps
# 4) 计算损失 (噪声预测)
eps_theta = model(xt, t)
loss = F.mse_loss(eps_theta, eps)
return loss
def train_ddpm(model, dataloader, T, beta_schedule_fn):
"""完整 DDPM 训练循环。"""
betas = beta_schedule_fn(T)
alphas = 1 - betas
alphas_cumprod = torch.cumprod(alphas, dim=0)
for epoch in range(num_epochs):
for batch in dataloader:
x0 = batch.to(device)
loss = ddpm_train_step(model, x0, alphas_cumprod, betas)
optimizer.zero_grad()
loss.backward()
optimizer.step()

6.2 采样算法(DDPM 原版)#

@torch.no_grad()
def ddpm_sample(model, alphas_cumprod, betas, shape, T):
"""
DDPM 采样 (Algorithm 2, Ho et al. 2020)。
从 x_T ~ N(0, I) 开始,T 步逆向去噪。
"""
# 1) 从纯噪声开始
xt = torch.randn(*shape, device=device)
# 2) 逐步逆向去噪 (t = T, T-1, ..., 1)
for t in reversed(range(T)):
t_batch = torch.full((shape[0],), t, device=device)
# 预测噪声
eps_theta = model(xt, t_batch)
# 估计 x_0
sqrt_ab = alphas_cumprod[t] ** 0.5
sqrt_1m = (1 - alphas_cumprod[t]) ** 0.5
x0_pred = (xt - sqrt_1m * eps_theta) / sqrt_ab
# 计算均值
beta_t = betas[t]
alpha_t = 1 - beta_t
mean = (xt - (beta_t / sqrt_1m) * eps_theta) / alpha_t ** 0.5
# 如果不是最后一步,加随机噪声
if t > 0:
std = (1 - alphas_cumprod[t - 1] / alphas_cumprod[t]) ** 0.5 * beta_t ** 0.5
xt = mean + std * torch.randn_like(xt)
else:
xt = mean
return xt

注意:原版 DDPM 需要 1000 步才能生成高质量图像——这太慢了,所以后来有了 DDIM 等加速方法。

6.3 DDIM 加速采样#

DDIM(Denoising Diffusion Implicit Models, Song et al., 2021)放弃了马尔可夫假设,用非马尔可夫逆向过程实现确定性且更快的采样:

@torch.no_grad()
def ddim_sample(model, alphas_cumprod, betas, shape,
num_steps=50, eta=0.0):
"""
DDIM 采样 (Song et al., 2021)。
num_steps << T (比如 T=1000, num_steps=50)
eta = 0: 完全确定性 (DDIM)
eta = 1: 随机马尔可夫 (等效 DDPM)
"""
T = len(alphas_cumprod) - 1
# 选择子序列
step_list = torch.linspace(0, T - 1, num_steps, dtype=torch.long).tolist()
seq = step_list[::-1] # 倒序: [T-1, ..., 0]
# 从纯噪声开始
xt = torch.randn(*shape, device=device)
for i, t in enumerate(seq):
t_batch = torch.full((shape[0],), t, device=device)
# 预测噪声
eps_theta = model(xt, t_batch)
# 估计 x_0
sqrt_ab = alphas_cumprod[t] ** 0.5
sqrt_1m = (1 - alphas_cumprod[t]) ** 0.5
x0_pred = (xt - sqrt_1m * eps_theta) / sqrt_ab
# 下一个时间步
t_next = seq[i + 1] if i + 1 < len(seq) else 0
sqrt_ab_next = alphas_cumprod[t_next] ** 0.5
sqrt_1m_next = (1 - alphas_cumprod[t_next]) ** 0.5
# DDIM 确定性方向
pred_eps = eps_theta
xt_next = sqrt_ab_next * x0_pred + sqrt_1m_next * pred_eps
# 如果要随机性 (eta > 0)
if eta > 0:
std = eta * ((1 - alphas_cumprod[t_next]) / (1 - alphas_cumprod[t]) * betas[t]) ** 0.5
xt_next = xt_next + std * torch.randn_like(xt)
xt = xt_next
return xt

DDIM 的关键洞察tt1t \to t-1 的转移不需要是随机的——只要 xt1x_{t-1} 能从 xtx_t 推出 x0x_0,就是合法的逆向过程。

6.4 DDPM vs DDIM vs Flow Matching 对比#

维度DDPM (马尔可夫)DDIM (非马尔可夫)Rectified Flow
逆向过程随机(加噪声)确定性(可选随机性)ODE(确定性)
采样步数1000(慢)20-50(快)10-50(快)
轨迹形状弧线取决于噪声调度直线(OT 最优)
训练目标MSE(ϵ\epsilon)同 DDPMMSE(vv)
轨迹可视化发散集中最短

7. 与 Score Matching 的理论联系#

7.1 Score Matching 的核心#

Score Matching 的目标是训练一个得分网络 sθ(x,t)s_\theta(x, t) 来估计对数密度梯度:

sθ(x,t)xlogpt(x)s_\theta(x, t) \approx \nabla_x \log p_t(x)

得分匹配损失(Hutchinson 估计器):

LSM=Expdata[tr(sθ(x)x)+12sθ(x)2]\mathcal{L}_{\text{SM}} = \mathbb{E}_{x \sim p_{\text{data}}} \left[ \operatorname{tr}\left( \frac{\partial s_\theta(x)}{\partial x} \right) + \frac{1}{2} \| s_\theta(x) \|^2 \right]

7.2 DDPM 与 Score Matching 的等价性#

核心联系:DDPM 预测的噪声 ϵθ(xt,t)\epsilon_\theta(x_t, t) 与得分函数成正比:

ϵθ(xt,t)=σtxtlogpt(xt)\epsilon_\theta(x_t, t) = -\sigma_t \nabla_{x_t} \log p_t(x_t)

其中 σt=1αˉt\sigma_t = \sqrt{1 - \bar\alpha_t} 是噪声标准差。

推导

  • pt(xt)=p(x0)q(xtx0)dx0p_t(x_t) = \int p(x_0) q(x_t | x_0) dx_0
  • q(xtx0)=N(αˉtx0,σt2I)q(x_t | x_0) = \mathcal{N}(\sqrt{\bar\alpha_t} x_0, \sigma_t^2 I)
  • xtlogq(xtx0)=xtαˉtx0σt2=ϵσt\nabla_{x_t} \log q(x_t | x_0) = -\frac{x_t - \sqrt{\bar\alpha_t} x_0}{\sigma_t^2} = -\frac{\epsilon}{\sigma_t}
  • 所以 xtlogpt(xt)=Eq(x0xt)[ϵσt]\nabla_{x_t} \log p_t(x_t) = \mathbb{E}_{q(x_0|x_t)}[-\frac{\epsilon}{\sigma_t}]
def score_from_eps(eps_theta, t, alphas_cumprod):
"""
从噪声预测网络得到得分函数 ∇_x log p_t(x)。
"""
sigma_t = torch.sqrt(1 - alphas_cumprod[t])
score = -eps_theta / sigma_t # s_θ(x_t, t) ≈ -ε / σ
return score
def eps_from_score(score, t, alphas_cumprod):
"""
从得分函数恢复噪声预测。
"""
sigma_t = torch.sqrt(1 - alphas_cumprod[t])
return -sigma_t * score

7.3 为什么 DDPM 比纯 Score Matching 更好训练?#

维度纯 Score MatchingDDPM
学习目标logp(x)\nabla \log p(x)(得分函数)ϵ\epsilon(噪声)
额外计算需要 Hessian 或 Hutchinson 估计不需要
采样方式朗之万动力学(慢,不精确)直接从 xTx_T 逆向(快)
训练稳定性差(得分函数在高维空间容易爆炸)好(MSE 损失有界)

DDPM 通过引入预设前向过程可闭式计算的 KL 目标,绕过了纯 Score Matching 的 Hessian 估计难题——这是它最重要的工程贡献。

8. 方差参数化:固定 vs 学习#

8.1 DDPM 的方差设计#

原版 DDPM 将逆向方差 βt\beta_t 固定为前向方差

σt2=βt\sigma_t^2 = \beta_t

论文发现,学习方差并不会带来显著提升,反而增加训练难度。

# DDPM 原版: 方差固定
def ddpm_variance(t, betas):
"""逆向过程的方差(固定为 β_t)。"""
return betas[t]
# DDPM++ (改进版): 学习方差
class LearnedVarianceUNet(nn.Module):
"""输出均值 + log 方差。"""
def forward(self, xt, t):
out = self.backbone(xt, t)
mean, log_var = out.chunk(2, dim=1)
log_var = torch.clamp(log_var, -10, 10)
return mean, log_var
def ddpmpp_sample(model, alphas_cumprod, betas, shape, T):
"""DDPM++ 采样 (学习方差版本)。"""
xt = torch.randn(*shape, device=device)
for t in reversed(range(T)):
t_batch = torch.full((shape[0],), t, device=device)
mean, log_var = model(xt, t_batch)
# 混合: 固定 + 学习
fixed_var = betas[t]
learned_var = log_var.exp()
var = (1 - alphas_cumprod[t - 1] / alphas_cumprod[t]) * betas[t]
std = var ** 0.5
xt = mean + std * torch.randn_like(xt)
return xt

9. Classifier Guidance(分类器引导)#

9.1 为什么需要引导?#

无条件 DDPM 生成质量不错,但无法控制类别。Classifier Guidance 用外部分类器注入类别信息:

p^ϕ(yxt)p(xty)γ\hat{p}_\phi(y | x_t) \propto p(x_t | y)^{\gamma}

对应的引导得分:

xtlogp(yxt)^=γxtlogpϕ(yxt)\widehat{\nabla_{x_t} \log p(y | x_t)} = \gamma \cdot \nabla_{x_t} \log p_\phi(y | x_t)

9.2 引导下的逆向分布#

ϵ^θ(xt,ty)=ϵθ(xt,t)1αˉtγxtlogpϕ(yxt)\hat{\epsilon}_\theta(x_t, t | y) = \epsilon_\theta(x_t, t) - \sqrt{1 - \bar\alpha_t} \cdot \gamma \cdot \nabla_{x_t} \log p_\phi(y | x_t)
def classifier_guidance(eps_theta, x_t, t, classifier, y, gamma=1.0):
"""
分类器引导 (Dhariwal & Nichol, 2021)。
在无类别噪声预测上叠加分类器梯度。
"""
# 分类器对 x_t 求梯度
x_t.requires_grad_(True)
logits = classifier(x_t, t)
log_probs = torch.log_softmax(logits, dim=-1)
target_log_prob = log_probs.gather(-1, y.unsqueeze(-1)).squeeze(-1)
grad = torch.autograd.grad(target_log_prob.sum(), x_t)[0]
# 引导噪声预测
sigma_t = ... # 噪声标准差
guided_eps = eps_theta - gamma * sigma_t * grad
return guided_eps

关键:引导强度 γ\gamma 控制类别保真度和多样性之间的 trade-off。γ=1\gamma = 1 是标准引导,γ\gamma 越大,生成越精确但越缺乏多样性。

9.3 CFG vs Classifier Guidance#

维度Classifier GuidanceClassifier-Free Guidance
是否需要单独训练分类器无需额外网络
训练方式分类器单独训,DDPM 单独训DDPM 联合训练条件 + 无条件
质量高(专用信号)高(隐式引导)
多样性低(强引导降低)可控(由 ww 调节)
典型使用ADM, Guided DiffusionSD 1/2/xl, SD3, FLUX

**CFG(无分类器引导)**是主流:

ϵ^θ(xt,tc)=(1+w)ϵθ(xt,t,c)wϵθ(xt,t,)\hat{\epsilon}_\theta(x_t, t | c) = (1 + w) \cdot \epsilon_\theta(x_t, t, c) - w \cdot \epsilon_\theta(x_t, t, \emptyset)
def cfg_eps(eps_cond, eps_uncond, w):
"""无分类器引导 (CFG)。"""
return (1 + w) * eps_cond - w * eps_uncond

10. DDPM 的完整 PyTorch 实现#

import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
class SinusoidalTimeEmbedding(nn.Module):
"""DDPM 标准时间步嵌入。"""
def __init__(self, dim):
super().__init__()
self.dim = dim
def forward(self, t):
device = t.device
half = self.dim // 2
embeddings = math.log(10000) / (half - 1)
embeddings = torch.exp(torch.arange(half, device=device) * -embeddings)
embeddings = t[:, None] * embeddings[None, :]
embeddings = torch.cat([embeddings.sin(), embeddings.cos()], dim=-1)
return embeddings
class ResidualBlock(nn.Module):
"""带 GroupNorm + SiLU 的残差块。"""
def __init__(self, in_ch, out_ch, time_emb_dim, dropout=0.1):
super().__init__()
self.norm1 = nn.GroupNorm(32, in_ch)
self.conv1 = nn.Conv2d(in_ch, out_ch, 3, padding=1)
self.time_mlp = nn.Sequential(
nn.SiLU(), nn.Linear(time_emb_dim, out_ch * 2)
)
self.norm2 = nn.GroupNorm(32, out_ch)
self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1)
self.dropout = nn.Dropout(dropout)
# 残差连接
self.shortcut = nn.Conv2d(in_ch, out_ch, 1) if in_ch != out_ch else nn.Identity()
def forward(self, x, t_emb):
h = self.norm1(x).swish()
h = self.conv1(h)
# 调制: AdaGN 风格
t = self.time_mlp(t_emb)
shift, scale = t.chunk(2, dim=-1)
h = h * (1 + scale[:, None, :, :]) + shift[:, None, :, :]
h = self.dropout(h)
h = self.norm2(h).swish()
h = self.conv2(h)
return h + self.shortcut(x)
class DDPMUNet(nn.Module):
"""简化版 DDPM U-Net。"""
def __init__(self, in_channels=3, base_channels=128, channel_mults=(1, 2, 4, 8)):
super().__init__()
self.time_embed_dim = base_channels * 4
self.time_mlp = nn.Sequential(
SinusoidalTimeEmbedding(base_channels),
nn.Linear(base_channels, self.time_embed_dim),
nn.SiLU(),
nn.Linear(self.time_embed_dim, self.time_embed_dim),
)
# Encoder
chs = [base_channels]
for mult in channel_mults:
chs.append(base_channels * mult)
self.encoder = nn.ModuleList()
for i in range(len(chs) - 1):
self.encoder.append(
ResidualBlock(chs[i], chs[i + 1], self.time_embed_dim)
)
# Bottleneck
self.bottleneck = ResidualBlock(chs[-1], chs[-1], self.time_embed_dim)
# Decoder
self.decoder = nn.ModuleList()
for i in reversed(range(len(chs) - 1)):
self.decoder.append(
ResidualBlock(chs[i + 1], chs[i], self.time_embed_dim)
)
# 输出层
self.final = nn.Sequential(
nn.GroupNorm(32, base_channels),
nn.SiLU(),
nn.Conv2d(base_channels, in_channels, 3, padding=1),
)
def forward(self, xt, t):
"""返回噪声预测 ε_θ(x_t, t)。"""
t_emb = self.time_mlp(t)
# Encoder
hs = []
for block in self.encoder:
xt = block(xt, t_emb)
hs.append(xt)
# Bottleneck
xt = self.bottleneck(xt, t_emb)
# Decoder with skip connections
for block in self.decoder:
xt = torch.cat([xt, hs.pop()], dim=1)
xt = block(xt, t_emb)
return self.final(xt)

11. DDPM 的局限与后续发展#

11.1 DDPM 的三大局限#

局限描述影响
推理速度需要 1000 步,生成极慢实际应用受限
隐空间缺失直接在像素空间加噪,计算量大512×512 以上极慢
无条件生成文本条件需要额外引导机制CFG 引入计算开销

11.2 DDPM 之后的演进时间线#

2020 DDPM (Ho et al.) — MSE 训练, 1000 步推理
2021 DDIM (Song et al.) — 非马尔可夫, 20-50 步
2021 Classifier-free Guidance (Ho & Salimans)
2022 Latent Diffusion (Rombach et al.) — VAE 压缩到隐空间 ★
2022 Score SDE (Song et al.) — 统一 SDE 框架
2022 Consistency Models (Song et al.) — 蒸馏到 1 步
2022 DiT (Peebles & Xie) — Transformer 替换 UNet
2023 SDXL (Rombach et al.) — 级联扩散, 1024px
2024 Rectified Flow (Liu et al.) — 最优传输, 直线路径
2024 SD3 (Esser et al.) — MMDiT 双流架构 ★
2024 FLUX (Black Forest Labs) — RF + MMDiT, 12B

12. 总结#

12.1 核心公式速查#

名称公式
前向加噪xt=αˉtx0+1αˉtϵx_t = \sqrt{\bar\alpha_t} x_0 + \sqrt{1 - \bar\alpha_t} \epsilon
逆向均值μt=1αt(xtβt1αˉtϵθ)\mu_t = \frac{1}{\sqrt{\alpha_t}} \left(x_t - \frac{\beta_t}{\sqrt{1 - \bar\alpha_t}} \epsilon_\theta\right)
训练目标Et,x0,ϵ[ϵϵθ(xt,t)2]\mathbb{E}_{t, x_0, \epsilon}[\, \| \epsilon - \epsilon_\theta(x_t, t) \|^2\,]
DDIM 采样xt1=αˉt1x^0+1αˉt1ϵθx_{t-1} = \sqrt{\bar\alpha_{t-1}} \hat{x}_0 + \sqrt{1 - \bar\alpha_{t-1}} \epsilon_\theta
CFGϵ^=(1+w)ϵcwϵu\hat\epsilon = (1 + w) \epsilon_c - w \epsilon_u
Score 联系ϵθ=σtxtlogpt(xt)\epsilon_\theta = -\sigma_t \nabla_{x_t} \log p_t(x_t)

12.2 一句话总结#

DDPM 用”预设前向 + 变分推断 + 简单 MSE”的三角组合,绕过了纯 Score Matching 的计算难题,把扩散模型从理论可行的方法变成了实际可训练的生成模型——这是 2020 年最重要的生成建模突破之一,也是后续所有扩散改进(Latent Diffusion、DiT、MMDiT、Rectified Flow、FLUX)的起点。

12.3 推荐资源#

论文:
- DDPM (Ho et al., 2020): "Denoising Diffusion Probabilistic Models"
- DDIM (Song et al., 2021): "Denoising Diffusion Implicit Models"
- Score SDE (Song et al., 2021): "Score-Based Generative Modeling through SDEs"
- CFG (Ho & Salimans, 2022): "Classifier-Free Diffusion Guidance"
- Latent Diffusion (Rombach et al., 2022): "High-Resolution Image Synthesis with Latent Diffusion Models"
代码:
- hojonathanho/diffusion (官方实现)
- openai/guided-diffusion (ADM)
- CompVis/stable-diffusion (LDM)
- lucidrains/DDPM-pytorch
数学参考:
- Lilian Weng 博客: "What are Diffusion Models?" (最好的技术解读)
- Angus Gilmour 博客: "Deriving the DDPM Training Objective"

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

DDPM 深度剖析:变分推断视角下的去噪扩散概率模型
https://aiattnstudio.link/posts/ddpm/
作者
Federico
发布于
2026-07-16
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author

Federico

AI Research Lab

Hello, I'm Federico.

关于实验室 / About
公告

欢迎来到Federico的个人博客

分类
标签
站点统计
57文章
7分类
404标签