跳转至

01 - DDIM加速采样

⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。

学习时间: 3小时 重要性: ⭐⭐⭐⭐⭐ 扩散模型实用化的关键技术


🎯 学习目标

完成本章后,你将能够: - 理解DDIM的核心思想和数学原理 - 掌握非马尔可夫前向过程的推导 - 实现DDIM采样算法 - 实现步数跳过加速策略


1. 为什么需要加速采样

1.1 DDPM的采样瓶颈

问题:DDPM需要1000步才能生成一张图像 - 每步都需要神经网络前向传播 - 生成一张图像需要数秒甚至数分钟 - 难以实时应用

计算成本

Text Only
DDPM: T=1000步 × 网络前向时间
     = 1000 × 50ms = 50秒/张图像

1.2 加速采样的方向

  1. 减少采样步数:DDIM、DPM-Solver
  2. 模型蒸馏:Progressive Distillation
  3. 潜空间扩散:LDM、Stable Diffusion
  4. 并行采样: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定义了一族非马尔可夫的前向过程:

\[q_\sigma(x_{t-1} | x_t, x_0) = \mathcal{N}(\sqrt{\alpha_{t-1}}x_0 + \sqrt{1-\alpha_{t-1}-\sigma_t^2} \cdot \frac{x_t - \sqrt{\alpha_t}x_0}{\sqrt{1-\alpha_t}}, \sigma_t^2 I)\]

其中 \(\sigma_t\)方差参数,控制随机性。

2.2 边缘分布保持不变

关键性质:对于任意 \(\sigma_t\),边缘分布 \(q(x_t | x_0)\) 与DDPM相同!

\[q(x_t | x_0) = \mathcal{N}(\sqrt{\alpha_t}x_0, (1-\alpha_t)I)\]

这意味着: - 训练目标不变:仍然使用DDPM的训练目标 - 只需要修改采样过程

2.3 反向过程

给定 \(x_t\)\(x_0\),采样 \(x_{t-1}\)

\[x_{t-1} = \sqrt{\alpha_{t-1}}x_0 + \sqrt{1-\alpha_{t-1}-\sigma_t^2} \cdot \frac{x_t - \sqrt{\alpha_t}x_0}{\sqrt{1-\alpha_t}} + \sigma_t \epsilon_t\]

其中 \(\epsilon_t \sim \mathcal{N}(0, I)\)

2.4 从噪声预测推导

\(x_t = \sqrt{\alpha_t}x_0 + \sqrt{1-\alpha_t}\epsilon\),得到:

\[x_0 = \frac{x_t - \sqrt{1-\alpha_t}\epsilon}{\sqrt{\alpha_t}}\]

代入反向过程:

\[x_{t-1} = \sqrt{\alpha_{t-1}} \underbrace{\left(\frac{x_t - \sqrt{1-\alpha_t}\epsilon_\theta}{\sqrt{\alpha_t}}\right)}_{\text{预测的}x_0} + \sqrt{1-\alpha_{t-1}-\sigma_t^2} \cdot \epsilon_\theta + \sigma_t \epsilon\]

3. DDIM的特殊情况

3.1 确定性采样(\(\sigma_t = 0\)

\(\sigma_t = 0\) 时,采样变为确定性

\[x_{t-1} = \sqrt{\alpha_{t-1}} \left(\frac{x_t - \sqrt{1-\alpha_t}\epsilon_\theta}{\sqrt{\alpha_t}}\right) + \sqrt{1-\alpha_{t-1}} \cdot \epsilon_\theta\]

简化:

\[x_{t-1} = \frac{\sqrt{\alpha_{t-1}}}{\sqrt{\alpha_t}}x_t + \left(\sqrt{1-\alpha_{t-1}} - \frac{\sqrt{\alpha_{t-1}}\sqrt{1-\alpha_t}}{\sqrt{\alpha_t}}\right) \epsilon_\theta\]

这就是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允许使用任意子集的时间步进行采样:

Python
# 原始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实现

Python
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 均匀采样

Python
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 非均匀采样

在噪声较大的早期使用更多步,在后期使用更少步:

Python
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. 性能对比

Python
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. 本章总结

核心概念

  1. DDIM的核心思想
  2. 非马尔可夫前向过程
  3. 边缘分布保持不变
  4. 训练目标不变,只改采样

  5. 确定性采样

  6. \(\sigma_t = 0\) 时无随机性
  7. 同样的输入产生同样的输出
  8. 适合可重复实验

  9. 步数跳过

  10. 可以使用任意子集的时间步
  11. 50步可以达到接近1000步的质量
  12. 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\)

实现要点

Python
# 核心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

📝 自测问题

基础问题

  1. DDIM原理
  2. DDIM与DDPM的主要区别是什么?
  3. 为什么DDIM可以使用更少的步数?
  4. 非马尔可夫过程的优势是什么?

  5. 数学推导

  6. 推导DDIM的采样公式
  7. 解释为什么边缘分布保持不变
  8. \(\sigma_t\) 参数的作用是什么?

  9. 实现细节

  10. 如何选择DDIM的步数?
  11. 均匀采样和非均匀采样有什么区别?
  12. 确定性采样和随机性采样各有什么优缺点?

编程练习

  1. 实现完整的DDIM采样器
  2. 比较不同步数下的生成质量
  3. 实现非均匀时间步选择策略
  4. 可视化DDIM和DDPM的采样轨迹

思考题

  1. DDIM为什么能保持与DDPM相同的训练目标?
  2. 步数减少到多少时质量会明显下降?
  3. 如何进一步优化采样速度?

🔗 下一步

理解了DDIM加速采样后,我们将学习条件生成与引导,掌握如何控制生成内容。

→ 下一步:02-条件生成与引导.md