扩散模型理论基础

扩散概率模型(Diffusion Probabilistic Models)是一类基于马尔可夫链的生成模型,通过逐步加噪和去噪实现数据生成。本文档从第一性原理推导其数学基础。1

核心思想

扩散模型包含两个过程:

  1. 前向过程(Forward/Diffusion):逐步向数据添加高斯噪声,直到接近纯噪声
  2. 反向过程(Reverse/Denoising):学习逆转前向过程,从噪声逐步恢复数据
┌─────────────────────────────────────────────────────────────────┐
│                      扩散模型工作流程                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  数据 x₀ ──加噪──▶ x₁ ──加噪──▶ x₂ ──...──▶ xT ──▶ 纯噪声     │
│   ↓                                          (前向过程 q)       │
│   │                                                             │
│   │                                                             │
│   │                     ┌─────────────────────┐               │
│   │                     │    学习反向过程      │               │
│   │                     │  pθ(x_{t-1}|x_t)   │               │
│   │                     └─────────────────────┘               │
│   │                                                             │
│   ▼                                                             │
│  数据 x₀ ←──去噪── x₁ ←──去噪── x₂ ←──...←── xT              │
│                              (反向过程 pθ)                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

前向过程

高斯噪声调度

前向过程 是预先定义的超参马尔可夫链:

其中 是噪声调度(noise schedule),通常满足:

封闭形式的噪声注入

利用重参数化技巧,可以直接计算任意时间步 的噪声状态:

其中:

关键性质:任意时刻的分布 可以直接计算,无需迭代!

import torch
import numpy as np
 
def make_beta_schedule(schedule='linear', n_timestep=1000, linear_start=1e-4, linear_end=2e-2):
    """
    生成噪声调度表
    
    参数:
        schedule: 调度类型 ('linear', 'cosine', 'quadratic')
        n_timestep: 总时间步数 T
        linear_start/end: 线性调度的起止值
    """
    if schedule == 'linear':
        betas = np.linspace(linear_start, linear_end, n_timestep)
    elif schedule == 'cosine':
        # 余弦调度,DDPM论文推荐
        steps = n_timestep + 1
        x = np.linspace(0, n_timestep, steps)
        alphas_cumprod = np.cos(((x / n_timestep) + 0.008) / 1.008 * np.pi * 0.5) ** 2
        alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
        betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1])
        betas = np.clip(betas, 0, 0.999)
    elif schedule == 'quadratic':
        betas = np.linspace(linear_start ** 0.5, linear_end ** 0.5, n_timestep) ** 2
    else:
        raise NotImplementedError(schedule)
    
    return betas
 
# 示例
betas = make_beta_schedule('cosine', n_timestep=1000)
alphas = 1 - betas
alphas_cumprod = np.cumprod(alphas)
sqrt_alphas_cumprod = np.sqrt(alphas_cumprod)
sqrt_one_minus_alphas_cumprod = np.sqrt(1 - alphas_cumprod)

常用噪声调度对比

调度方式公式特点
Linear简单,DDPM原始方案
Cosine训练更稳定,推荐方案
Quadratic中间段变化更慢
Sigmoid两端平滑过渡

反向过程

学习目标

反向过程 是参数化的马尔可夫链:

其中 是先验分布。

每个去噪转移定义为:

变分下界(ELBO)

通过变分推断,训练目标转化为最大化证据下界(ELBO):

三项的物理意义

  1. 重建项 :从 重建
  2. 先验匹配项,当 时自动满足
  3. 去噪匹配项:学习 的近似

简化的损失函数

经过推导(详见下方数学推导),损失函数可以简化为:

即:预测注入的噪声

三种参数化方式

1. ε-预测(Noise Prediction)

直接预测注入的噪声

class DiffusionModel_eps(torch.nn.Module):
    def __init__(self, denoiser, T=1000):
        super().__init__()
        self.denoiser = denoiser  # UNet等去噪网络
        self.T = T
    
    def training_losses(self, x0):
        """计算训练损失"""
        batch_size = x0.shape[0]
        t = torch.randint(0, self.T, (batch_size,), device=x0.device)
        
        # 采样噪声
        eps = torch.randn_like(x0)
        
        # 计算带噪图像
        sqrt_alphas_cumprod_t = self.sqrt_alphas_cumprod[t].view(-1, 1, 1, 1)
        sqrt_one_minus_alphas_cumprod_t = self.sqrt_one_minus_alphas_cumprod[t].view(-1, 1, 1, 1)
        
        xt = sqrt_alphas_cumprod_t * x0 + sqrt_one_minus_alphas_cumprod_t * eps
        
        # 预测噪声
        eps_theta = self.denoiser(xt, t)
        
        # MSE损失
        loss = F.mse_loss(eps_theta, eps, reduction='mean')
        return loss

2. x₀-预测(Direct Prediction)

直接预测原始图像

对应的均值:

3. v-预测(Velocity Prediction)

预测速度向量

这是 Stable Diffusion 2.0+ 使用的方案,实验表明效果更好。

class VelocityPrediction(torch.nn.Module):
    def predict_velocity(self, x0, eps, t):
        """计算velocity"""
        sqrt_alphas_cumprod_t = self.sqrt_alphas_cumprod[t].view(-1, 1, 1, 1)
        sqrt_one_minus_alphas_cumprod_t = self.sqrt_one_minus_alphas_cumprod[t].view(-1, 1, 1, 1)
        
        # v = sqrt(1-ᾱₜ)·x₀ - √(ᾱₜ)·ε
        return sqrt_one_minus_alphas_cumprod_t * x0 - sqrt_alphas_cumprod_t * eps
    
    def training_losses(self, x0):
        t = torch.randint(0, self.T, (x0.shape[0],))
        eps = torch.randn_like(x0)
        v = self.predict_velocity(x0, eps, t)
        
        xt = torch.sqrt(self.alphas_cumprod[t].view(-1, 1, 1, 1)) * x0 + \
             torch.sqrt(1 - self.alphas_cumprod[t].view(-1, 1, 1, 1)) * eps
        
        v_theta = self.denoiser(xt, t)
        return F.mse_loss(v_theta, v)

参数化对比

参数化输出优点缺点
ε-预测噪声简单,最常用中间步骤重建质量差
x₀-预测原始图像直观训练初期不稳定
v-预测速度向量收敛快,质量好需要调整网络结构

数学推导

后验分布

这是推导的核心,利用贝叶斯定理:

由于所有分布都是高斯分布,乘积仍然是高斯分布,其均值和方差可以直接计算:

其中:

损失函数的推导

的KL散度展开:

表示:

假设 也是 的函数:

代入KL散度:

由于常数项与 无关,最小化KL散度等价于最小化噪声预测的MSE!

采样过程

DDPM采样

@torch.no_grad()
def ddpm_sampling(model, xt, betas, T):
    """DDPM反向采样过程"""
    alphas = 1 - betas
    alphas_cumprod = torch.cumprod(alphas, dim=0)
    
    for t in reversed(range(T)):
        # 预测噪声
        eps_theta = model(xt, t)
        
        # 计算均值
        alpha_t = alphas[t]
        alpha_bar_t = alphas_cumprod[t]
        beta_t = betas[t]
        
        # 系数
        coef1 = beta_t / torch.sqrt(1 - alpha_bar_t)
        coef2 = torch.sqrt(alpha_t) * (1 - alphas_cumprod[t-1] if t > 0 else 1)
        coef3 = 1 - alphas_cumprod[t-1] if t > 0 else 1
        
        mean = (xt - coef1 * eps_theta) / torch.sqrt(alpha_t)
        
        # 添加噪声(最后一步不加噪声)
        if t > 0:
            noise = torch.randn_like(xt)
            xt = mean + torch.sqrt(beta_t) * noise
        else:
            xt = mean
    
    return xt

与Score Matching的关系

扩散模型的损失函数与Score Matching有深刻联系。

Score Matching的目标是学习对数密度的梯度(score function):

Song Yang等人的工作(SDE框架)证明了:

其中 是时间 对应的噪声水平。这意味着扩散模型的去噪器本质上学习的是不同噪声水平下的score函数。

详见 Score Matching与SDE

网络架构

UNet骨干网络

大多数扩散模型使用 U-Net 作为去噪网络:

class UNet(torch.nn.Module):
    def __init__(self, in_channels=3, out_channels=3, base_channels=128):
        super().__init__()
        # 编码器(下采样)
        self.enc1 = ResidualBlock(in_channels, base_channels)
        self.enc2 = ResidualBlock(base_channels, base_channels*2, downsample=True)
        self.enc3 = ResidualBlock(base_channels*2, base_channels*4, downsample=True)
        self.enc4 = ResidualBlock(base_channels*4, base_channels*4, downsample=True)
        
        # 瓶颈
        self.bottleneck = ResidualBlock(base_channels*4, base_channels*4)
        
        # 解码器(上采样)
        self.dec4 = ResidualBlock(base_channels*8, base_channels*4)
        self.dec3 = ResidualBlock(base_channels*6, base_channels*2)
        self.dec2 = ResidualBlock(base_channels*3, base_channels)
        self.dec1 = ResidualBlock(base_channels*2, out_channels)
        
        self.time_mlp = TimeEmbedding(base_channels)
        self.downsample = Downsample()
        self.upsample = Upsample()
    
    def forward(self, x, t):
        # 时间嵌入
        t_emb = self.time_mlp(t)
        
        # 编码路径
        e1 = self.enc1(x, t_emb)
        e2 = self.enc2(e1, t_emb)
        e3 = self.enc3(e2, t_emb)
        e4 = self.enc4(e3, t_emb)
        
        # 瓶颈
        b = self.bottleneck(e4, t_emb)
        
        # 解码路径(包含跳跃连接)
        d4 = self.dec4(torch.cat([b, e4], dim=1), t_emb)
        d3 = self.dec3(torch.cat([d4, e3], dim=1), t_emb)
        d2 = self.dec2(torch.cat([d3, e2], dim=1), t_emb)
        d1 = self.dec1(torch.cat([d2, e1], dim=1), t_emb)
        
        return d1

时间条件机制

扩散模型需要将时间步信息注入网络,常用方法:

  1. 正弦位置编码:将 映射到高频振荡基函数
  2. AdaIN (Adaptive Instance Normalization):将时间嵌入注入归一化层
  3. Adaptive Layer Norm

参考

Footnotes

  1. Ho et al., “Denoising Diffusion Probabilistic Models”, NeurIPS 2020. https://arxiv.org/abs/2006.11239