01 - DDIM加速采样¶
⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。
学习时间: 3小时 重要性: ⭐⭐⭐⭐⭐ 扩散模型实用化的关键技术
🎯 学习目标¶
完成本章后,你将能够: - 理解DDIM的核心思想和数学原理 - 掌握非马尔可夫前向过程的推导 - 实现DDIM采样算法 - 实现步数跳过加速策略
1. 为什么需要加速采样¶
1.1 DDPM的采样瓶颈¶
问题:DDPM需要1000步才能生成一张图像 - 每步都需要神经网络前向传播 - 生成一张图像需要数秒甚至数分钟 - 难以实时应用
计算成本:
1.2 加速采样的方向¶
- 减少采样步数:DDIM、DPM-Solver
- 模型蒸馏:Progressive Distillation
- 潜空间扩散:LDM、Stable Diffusion
- 并行采样:ParaNormFlow
DDIM的核心思想: - 将扩散过程视为非马尔可夫过程 - 允许使用更少的步数采样 - 保持与DDPM相同的训练目标
2. DDIM的数学原理¶
📝 符号约定:本章中 \(\bar{\alpha}_t = \prod_{s=1}^{t} \alpha_s\) 为累积噪声调度,\(\alpha_t = 1 - \beta_t\) 为单步保留率。DDIM 公式中的 \(\alpha\) 均指 \(\bar{\alpha}_t\)(累积量)。
2.1 非马尔可夫前向过程¶
DDPM的前向过程是马尔可夫的: $\(q(x_t | x_{t-1}) = \mathcal{N}(\sqrt{1-\beta_t}x_{t-1}, \beta_t I)\)$
DDIM定义了一族非马尔可夫的前向过程:
其中 \(\sigma_t\) 是方差参数,控制随机性。
2.2 边缘分布保持不变¶
关键性质:对于任意 \(\sigma_t\),边缘分布 \(q(x_t | x_0)\) 与DDPM相同!
这意味着: - 训练目标不变:仍然使用DDPM的训练目标 - 只需要修改采样过程
2.3 反向过程¶
给定 \(x_t\) 和 \(x_0\),采样 \(x_{t-1}\):
其中 \(\epsilon_t \sim \mathcal{N}(0, I)\)。
2.4 从噪声预测推导¶
从 \(x_t = \sqrt{\alpha_t}x_0 + \sqrt{1-\alpha_t}\epsilon\),得到:
代入反向过程:
3. DDIM的特殊情况¶
3.1 确定性采样(\(\sigma_t = 0\))¶
当 \(\sigma_t = 0\) 时,采样变为确定性:
简化:
这就是DDIM(Denoising Diffusion Implicit Models)!
3.2 与DDPM的关系¶
| 方法 | \(\sigma_t\) | 特性 |
|---|---|---|
| DDPM | \(\sqrt{\frac{1-\alpha_{t-1}}{1-\alpha_t}} \sqrt{\beta_t}\) | 随机采样 |
| DDIM | \(0\) | 确定性采样 |
| 一般形式 | 任意 | 可调随机性 |
3.3 步数跳过¶
DDIM允许使用任意子集的时间步进行采样:
# 原始1000步
timesteps = list(range(1000))
# DDIM 50步(均匀子采样)
timesteps = list(range(0, 1000, 20)) # [0, 20, 40, ..., 980]
# DDIM 10步(更稀疏)
timesteps = list(range(0, 1000, 100)) # [0, 100, 200, ..., 900]
关键:由于边缘分布不变,跳步采样仍然有效!
4. DDIM实现¶
import torch
import torch.nn as nn
import numpy as np
from typing import Optional, List
class DDIMSampler:
"""
DDIM采样器
"""
def __init__(
self,
diffusion, # GaussianDiffusion对象
ddim_steps: int = 50,
ddim_eta: float = 0.0, # 0=确定性, 1=随机性
):
self.diffusion = diffusion
self.ddim_steps = ddim_steps
self.ddim_eta = ddim_eta
# 选择时间步子集
self.timestep_map = self._get_timestep_map()
def _get_timestep_map(self) -> List[int]:
"""
将DDPM的1000步映射到DDIM的ddim_steps步
"""
c = self.diffusion.timesteps // self.ddim_steps
timesteps = list(range(0, self.diffusion.timesteps, c))
return timesteps[:self.ddim_steps]
def ddim_sample(
self,
model: nn.Module,
x_t: torch.Tensor,
t: int,
t_prev: int,
clip_denoised: bool = True,
) -> torch.Tensor:
"""
单步DDIM采样
参数:
model: 噪声预测模型
x_t: 当前状态
t: 当前时间步
t_prev: 上一个时间步
clip_denoised: 是否裁剪预测值
返回:
x_{t_prev}: 上一个时间步的状态
"""
# 预测噪声
t_tensor = torch.full((x_t.shape[0],), t, device=x_t.device, dtype=torch.long)
eps = model(x_t, t_tensor)
# 计算alpha值
alpha_t = self.diffusion.alphas_cumprod[t]
alpha_t_prev = self.diffusion.alphas_cumprod[t_prev] if t_prev >= 0 else torch.tensor(1.0)
# 预测x_0
x_0_pred = (x_t - torch.sqrt(1 - alpha_t) * eps) / torch.sqrt(alpha_t)
if clip_denoised:
x_0_pred = torch.clamp(x_0_pred, -1.0, 1.0)
# 计算方差
sigma_t = self.ddim_eta * torch.sqrt(
(1 - alpha_t_prev) / (1 - alpha_t) * (1 - alpha_t_prev / alpha_t)
)
# 计算噪声系数
noise_coeff = torch.sqrt(1 - alpha_t_prev - sigma_t**2)
# DDIM采样公式
x_prev = (
torch.sqrt(alpha_t_prev) * x_0_pred +
noise_coeff * eps
)
# 添加随机噪声(如果eta > 0)
if self.ddim_eta > 0:
noise = torch.randn_like(x_t)
x_prev = x_prev + sigma_t * noise
return x_prev
def sample(
self,
model: nn.Module,
shape: tuple,
device: torch.device,
noise: Optional[torch.Tensor] = None,
progress: bool = True,
) -> torch.Tensor:
"""
完整的DDIM采样
参数:
model: 噪声预测模型
shape: 输出形状 (B, C, H, W)
device: 计算设备
noise: 可选的初始噪声
progress: 是否显示进度条
返回:
生成的样本
"""
model.eval() # eval()评估模式
# 初始化
if noise is not None:
x = noise
else:
x = torch.randn(shape, device=device)
# 时间步列表(反向)
timesteps = self.timestep_map[::-1]
if progress:
from tqdm import tqdm
timesteps = tqdm(timesteps, desc="DDIM Sampling")
# 采样循环
for i, t in enumerate(timesteps): # enumerate同时获取索引和元素
t_prev = timesteps[i + 1] if i + 1 < len(timesteps) else -1
x = self.ddim_sample(model, x, t, t_prev)
return x
# 使用示例
if __name__ == "__main__":
from diffusion import GaussianDiffusion
# 创建扩散过程
diffusion = GaussianDiffusion(timesteps=1000)
# 创建DDIM采样器
ddim_sampler = DDIMSampler(
diffusion=diffusion,
ddim_steps=50, # 只用50步
ddim_eta=0.0, # 确定性采样
)
print(f"DDPM时间步: {diffusion.timesteps}")
print(f"DDIM时间步: {ddim_sampler.ddim_steps}")
print(f"加速比: {diffusion.timesteps / ddim_sampler.ddim_steps:.1f}x")
5. 步数选择策略¶
5.1 均匀采样¶
def uniform_timestep_schedule(total_steps, num_ddim_steps):
"""均匀选择时间步"""
c = total_steps // num_ddim_steps
return list(range(0, total_steps, c))[:num_ddim_steps]
# 示例
print(uniform_timestep_schedule(1000, 50))
# [0, 20, 40, 60, ..., 980]
5.2 非均匀采样¶
在噪声较大的早期使用更多步,在后期使用更少步:
def quadratic_timestep_schedule(total_steps, num_ddim_steps):
"""二次采样,早期更密集"""
timesteps = []
for i in range(num_ddim_steps):
t = int((i / num_ddim_steps) ** 2 * total_steps)
timesteps.append(t)
return timesteps
print(quadratic_timestep_schedule(1000, 50))
# 早期步数更密集
5.3 步数与质量权衡¶
| 步数 | 质量 | 速度 | 适用场景 |
|---|---|---|---|
| 1000 | 最高 | 最慢 | 高质量生成 |
| 100 | 高 | 中等 | 一般应用 |
| 50 | 良好 | 快 | 实时应用 |
| 20 | 可接受 | 很快 | 快速预览 |
| 10 | 较低 | 极快 | 调试测试 |
6. 性能对比¶
def benchmark_sampling(model, diffusion, shape, device):
"""对比DDPM和DDIM的采样速度"""
import time
# DDPM采样
start = time.time()
x_ddpm = diffusion.p_sample_loop(model, shape, device, progress=False)
ddpm_time = time.time() - start
# DDIM采样(50步)
ddim_sampler = DDIMSampler(diffusion, ddim_steps=50)
start = time.time()
x_ddim = ddim_sampler.sample(model, shape, device, progress=False)
ddim_time = time.time() - start
print(f"DDPM (1000步): {ddpm_time:.2f}s")
print(f"DDIM (50步): {ddim_time:.2f}s")
print(f"加速比: {ddpm_time / ddim_time:.1f}x")
return ddpm_time, ddim_time
7. 本章总结¶
核心概念¶
- DDIM的核心思想
- 非马尔可夫前向过程
- 边缘分布保持不变
-
训练目标不变,只改采样
-
确定性采样
- \(\sigma_t = 0\) 时无随机性
- 同样的输入产生同样的输出
-
适合可重复实验
-
步数跳过
- 可以使用任意子集的时间步
- 50步可以达到接近1000步的质量
- 10-20倍加速
关键公式¶
| 概念 | 公式 |
|---|---|
| DDIM采样 | \(x_{t-1} = \sqrt{\alpha_{t-1}}x_0^{pred} + \sqrt{1-\alpha_{t-1}-\sigma_t^2} \cdot \epsilon_\theta\) |
| 确定性 | \(\sigma_t = 0\) |
| 随机性 | \(\sigma_t > 0\) |
实现要点¶
# 核心DDIM采样步骤
x_0_pred = (x_t - sqrt(1-alpha_t) * eps) / sqrt(alpha_t)
x_{t-1} = sqrt(alpha_{t-1}) * x_0_pred + sqrt(1-alpha_{t-1}) * eps
📝 自测问题¶
基础问题¶
- DDIM原理
- DDIM与DDPM的主要区别是什么?
- 为什么DDIM可以使用更少的步数?
-
非马尔可夫过程的优势是什么?
-
数学推导
- 推导DDIM的采样公式
- 解释为什么边缘分布保持不变
-
\(\sigma_t\) 参数的作用是什么?
-
实现细节
- 如何选择DDIM的步数?
- 均匀采样和非均匀采样有什么区别?
- 确定性采样和随机性采样各有什么优缺点?
编程练习¶
- 实现完整的DDIM采样器
- 比较不同步数下的生成质量
- 实现非均匀时间步选择策略
- 可视化DDIM和DDPM的采样轨迹
思考题¶
- DDIM为什么能保持与DDPM相同的训练目标?
- 步数减少到多少时质量会明显下降?
- 如何进一步优化采样速度?
🔗 下一步¶
理解了DDIM加速采样后,我们将学习条件生成与引导,掌握如何控制生成内容。
→ 下一步:02-条件生成与引导.md