首页/知识库/Diffusion 模型(一):原理与数学基础

Diffusion 模型(一):原理与数学基础

✍️ AI Master📅 创建 2026-04-12📖 20 min 阅读
💡

文章摘要

从加噪到去噪,理解扩散模型如何一步步生成高质量图像

1生成模型家族对比

在生成式 AI 领域,三大主流模型家族各有千秋。VAE 通过变分推断学习数据的隐空间表示,训练稳定但生成样本偏模糊;GAN 利用生成器与判别器的对抗博弈产生锐利图像,却面临模式崩溃和训练不稳定的难题;扩散模型(Diffusion)则以渐进式加噪与去噪为核心,兼具训练稳定性和生成质量。扩散模型之所以在 2021 年后迅速崛起,关键在于其目标函数简洁可微,避免了 GAN 的 min-max 对抗优化,同时采样质量超越了 VAE。三者本质上都在学习数据分布 p_data(x) 的近似,但路径截然不同:VAE 走隐变量概率推断之路,GAN 走对抗博弈之路,而 Diffusion 走非平衡热力学之路。

python
# 三大生成模型目标函数对比
import torch

# VAE: ELBO 最大化 (重构 + KL 散度)
def vae_loss(x_recon, x, mu, logvar):
    recon = torch.nn.functional.mse_loss(x_recon, x)
    kl = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
    return recon + kl

# GAN: 对抗损失
def gan_loss(real_pred, fake_pred):
    real_loss = torch.nn.functional.binary_cross_entropy_with_logits(
        real_pred, torch.ones_like(real_pred))
    fake_loss = torch.nn.functional.binary_cross_entropy_with_logits(
        fake_pred, torch.zeros_like(fake_pred))
    return real_loss, fake_loss
python
# 模型特性快速对比实验
import matplotlib.pyplot as plt

models = ["VAE", "GAN", "Diffusion"]
quality = [6.5, 9.0, 9.5]       # FID 得分(越低越好,此处反向展示)
stability = [9.0, 4.0, 8.5]     # 训练稳定性
diversity = [7.0, 5.0, 9.0]     # 样本多样性
speed = [9.0, 8.0, 3.0]        # 采样速度

x = range(len(models))
fig, ax = plt.subplots(figsize=(8, 5))
ax.bar(x, quality, width=0.2, label="生成质量")
ax.bar([i+0.2 for i in x], stability, width=0.2, label="训练稳定性")
ax.bar([i+0.4 for i in x], diversity, width=0.2, label="多样性")
ax.set_xticks([i+0.2 for i in x])
ax.set_xticklabels(models)
特性VAEGANDiffusion

训练目标

ELBO

对抗博弈

噪声预测 MSE

样本质量

中等

极高

训练稳定性

采样速度

模式崩溃

不会

不会

似然估计

下界

不可行

可行

选择模型时,追求极致生成质量选 Diffusion,追求实时推理速度选 VAE 或 GAN。

扩散模型采样慢是硬伤,后续章节会介绍加速方法。

2前向扩散过程

前向扩散过程(Forward Process)是整个扩散模型的基石。其核心思想非常简单:给定一张清晰图像 x_0,我们逐步向其中注入高斯噪声,经过 T 步后,图像变成纯高斯噪声 x_T。每一步加噪都遵循马尔可夫链,即 x_t 只依赖于 x_{t-1}。具体地,q(x_t | x_{t-1}) = N(x_t; sqrt(1 - beta_t) * x_{t-1}, beta_t * I),其中 beta_t 是预设的方差调度(variance schedule),通常从 10^-4 线性增长到 0.02。关键洞察在于,借助高斯分布的可加性,我们可以直接计算从 x_0 到任意 x_t 的闭式表达,无需逐步迭代。这一性质使得训练时可以直接从任意时间步 t 采样,极大地提升了训练效率。方差调度 beta_t 的选择至关重要,过小则噪声不足,过大则信息丢失过快。

python
# 前向扩散过程实现
import torch
import torch.nn as nn

def get_beta_schedule(T=1000, beta_start=1e-4, beta_end=0.02):
    """线性方差调度"""
    return torch.linspace(beta_start, beta_end, T)

def forward_diffusion(x_0, t, betas):
    """一步到位计算 x_t ~ q(x_t | x_0)"""
    alpha = 1.0 - betas
    alpha_bar = torch.cumprod(alpha, dim=0)
    alpha_bar_t = alpha_bar[t][:, None, None, None]
    
    mean = torch.sqrt(alpha_bar_t) * x_0
    noise = torch.randn_like(x_0)
    x_t = mean + torch.sqrt(1 - alpha_bar_t) * noise
    return x_t, noise
python
# 可视化不同时间步的加噪效果
import matplotlib.pyplot as plt
import numpy as np

T = 1000
betas = get_beta_schedule(T)

timesteps = [0, 50, 200, 500, 800, 999]
fig, axes = plt.subplots(1, 6, figsize=(15, 3))
for i, t in enumerate(timesteps):
    x_t, _ = forward_diffusion(img_tensor, torch.tensor([t]), betas)
    axes[i].imshow(x_t.squeeze().permute(1, 2, 0).numpy())
    axes[i].set_title(f"t={t}")
    axes[i].axis("off")
plt.suptitle("Forward Diffusion Process")
时间步 talpha_bar_t信号占比噪声占比图像可辨识度

0

1.000

100%

0%

完全清晰

100

0.905

90.5%

9.5%

轻微噪声

300

0.540

54.0%

46.0%

明显模糊

600

0.096

9.6%

90.4%

几乎不可辨

1000

0.000

~0%

~100%

纯高斯噪声

实际训练中 alpha_bar 可以预先计算并缓存,避免每一步重复计算 cumprod。

beta_t 调度不当会导致训练发散或信息过早丢失,建议从线性调度开始调参。

3反向去噪过程与 U-Net

反向去噪过程(Reverse Process)是扩散模型的生成核心。与前向过程固定不同,反向过程需要学习一个参数化的分布 p_theta(x_{t-1} | x_t),从纯噪声 x_T 逐步恢复出清晰图像 x_0。DDPM 选择用 U-Net 架构来预测每一步加入的噪声 epsilon_theta(x_t, t)。为什么选 U-Net?因为 U-Net 的编码器-解码器结构配合跳跃连接,能够同时捕获全局语义和局部细节,这对图像去噪至关重要。U-Net 还需要融入时间步信息,通常通过正弦位置编码或 MLP 将 t 编码为向量,注入到残差块中。近年来,Attention 机制被引入 U-Net,形成了更强大的架构,能够处理高分辨率图像和长距离依赖。训练好的网络在推理时从随机噪声开始,逐步预测并减去噪声,最终生成高质量图像。

python
# 简化版时间感知 U-Net
class TimeEmbedding(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.dim = dim
    
    def forward(self, t):
        half = self.dim // 2
        freqs = torch.exp(-torch.arange(half) * \
            (torch.log(torch.tensor(10000.0)) / half))
        args = t[:, None].float() * freqs[None]
        return torch.cat([torch.sin(args), torch.cos(args)], dim=-1)

class SimpleUNet(nn.Module):
    def __init__(self, in_ch=3, base_ch=64):
        super().__init__()
        self.time_embed = TimeEmbedding(base_ch * 4)
        self.down = nn.Conv2d(in_ch, base_ch, 3, padding=1)
        self.mid = nn.Conv2d(base_ch, base_ch, 3, padding=1)
        self.up = nn.Conv2d(base_ch * 2, in_ch, 3, padding=1)
python
# 反向采样过程(DDPM 原始采样)
@torch.no_grad()
def reverse_diffusion(model, shape, betas, device="cuda"):
    T = len(betas)
    alpha = 1.0 - betas
    alpha_bar = torch.cumprod(alpha, dim=0)
    
    x = torch.randn(shape, device=device)
    for t in reversed(range(T)):
        t_tensor = torch.full((shape[0],), t, device=device)
        predicted_noise = model(x, t_tensor)
        
        beta_t = betas[t]
        alpha_t = alpha[t]
        alpha_bar_t = alpha_bar[t]
        
        x = (1 / torch.sqrt(alpha_t)) * (x - \
            (beta_t / torch.sqrt(1 - alpha_bar_t)) * predicted_noise)
        if t > 0:
            x += torch.sqrt(beta_t) * torch.randn_like(x)
    return x
U-Net 组件功能输入维度输出维度关键技术

编码器

下采样提取特征

H x W x C

H/4 x W/4 x 4C

步长卷积

时间嵌入

编码时间步信息

标量 t

d_model 向量

正弦编码

注意力块

捕获长程依赖

H/8 x W/8 x 8C

同输入

Self-Attention

跳跃连接

融合高低层特征

拼接

拼接后卷积

Concat + Conv

解码器

上采样恢复分辨率

H/4 x W/4 x 4C

H x W x C

转置卷积

使用 GroupNorm 替代 BatchNorm 可以在小 batch size 下稳定训练扩散模型。

U-Net 输出必须与输入分辨率一致,注意 padding 和 stride 的匹配。

4DDPM 训练目标

DDPM 的训练目标可以优雅地简化为预测噪声的均方误差。从变分下界(ELBO)出发,经过一系列数学推导,KL 散度项最终等价于让神经网络预测前向过程中注入的真实噪声。具体地,训练损失 L = E_{t, x_0, epsilon}[||epsilon - epsilon_theta(x_t, t)||^2],其中 t 均匀采样自 [1, T],x_t 由前向过程得到,epsilon 是注入的标准高斯噪声。这种简化带来了两个巨大优势:一是目标函数无需近似,可以直接计算;二是无需像 GAN 那样维持判别器。训练时,每个 batch 中随机采样时间步 t,对同一批数据施加不同强度的噪声,相当于同时训练了 T 个不同噪声水平的去噪器。实际实现中,还可以对损失进行加权(如简单损失 vs 加权损失),但 Ho 等人的原始论文发现简单的未加权 MSE 效果最好。

python
# DDPM 训练循环
def train_step(model, x_0, optimizer, betas):
    batch_size = x_0.shape[0]
    T = len(betas)
    
    # 随机采样时间步
    t = torch.randint(0, T, (batch_size,), device=x_0.device)
    
    # 采样噪声
    noise = torch.randn_like(x_0)
    
    # 前向过程得到 x_t
    x_t, _ = forward_diffusion(x_0, t, betas)
    
    # 预测噪声
    predicted_noise = model(x_t, t)
    
    # 简单 MSE 损失
    loss = torch.nn.functional.mse_loss(noise, predicted_noise)
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    return loss.item()
python
# 完整训练流程
import torch.optim as optim
from tqdm import tqdm

def train_ddpm(model, dataloader, epochs=100, lr=2e-4):
    betas = get_beta_schedule(T=1000)
    optimizer = optim.AdamW(model.parameters(), lr=lr)
    
    for epoch in range(epochs):
        model.train()
        pbar = tqdm(dataloader, desc=f"Epoch {epoch+1}")
        total_loss = 0
        
        for batch in pbar:
            x_0 = batch["image"].cuda()
            loss = train_step(model, x_0, optimizer, betas)
            total_loss += loss
            pbar.set_postfix(loss=total_loss / (pbar.n + 1))
        
        print(f"Epoch {epoch+1} avg loss: {total_loss/len(dataloader):.6f}")
损失变体公式权重 w(t)适用场景效果

简单损失

||eps - eps_hat||^2

1

默认选择

稳定,效果最佳

加权损失

w(t)||eps - eps_hat||^2

1/(1-alpha_bar_t)

理论推导

实验效果较差

v-prediction

||v - v_hat||^2

1

大模型训练

更适合高步数

x_0-prediction

||x_0 - x_0_hat||^2

alpha_bar_t

特殊需求

重构更准确

使用 EMA(指数移动平均)权重进行推理可以显著提升生成质量,训练时保存 EMA 模型。

DDPM 需要 1000 步采样,训练时间较长,建议先用小 T 值调试代码。

5采样加速 DDIM

DDPM 原始采样需要 1000 步马尔可夫去噪,这在实践中太慢了。DDIM(Denoising Diffusion Implicit Models)的关键突破在于发现:扩散模型的反向过程可以是非马尔可夫的,只要边缘分布 q(x_t | x_0) 保持不变,整个训练目标就仍然有效。基于这一洞察,DDIM 推导出了确定性采样公式,可以直接跳过中间步骤,用 20-50 步就能达到与 DDPM 1000 步相当的质量。DDIM 采样器本质上是求解一个常微分方程(ODE),这带来了额外的优势:确定性采样意味着相同的初始噪声产生相同的图像,支持图像插值和隐空间编辑。此外,DDIM 的加速效果是数量级的,1000 步降到 50 步意味着推理速度提升 20 倍,这对实际应用至关重要。

python
# DDIM 采样器实现
@torch.no_grad()
def ddim_sample(model, shape, betas, steps=50, eta=0.0):
    T = len(betas)
    alpha = 1.0 - betas
    alpha_bar = torch.cumprod(alpha, dim=0)
    
    # 选择采样时间步(均匀间隔)
    skip = T // steps
    seq = range(0, T, skip)
    seq_next = [-1] + list(seq[:-1])
    
    x = torch.randn(shape, device="cuda")
    
    for i, j in zip(reversed(seq), reversed(seq_next)):
        t_tensor = torch.full((shape[0],), i, device="cuda")
        eps = model(x, t_tensor)
        
        x0_pred = (x - torch.sqrt(1 - alpha_bar[i]) * eps) / \
                   torch.sqrt(alpha_bar[i])
        
        if j >= 0:
            c1 = eta * torch.sqrt((1 - alpha_bar[i]) / (1 - alpha_bar[j]) * \
                   (1 - alpha_bar[j] / alpha_bar[i]))
            c2 = torch.sqrt((1 - alpha_bar[j]) - c1**2)
            x = torch.sqrt(alpha_bar[j]) * x0_pred + c2 * eps + \
                c1 * torch.randn_like(x)
    return x
python
# 对比 DDPM vs DDIM 采样速度与质量
import time

def benchmark_sampling(model, shape, betas, methods):
    results = []
    for name, steps in methods:
        start = time.time()
        if name == "DDPM":
            img = reverse_diffusion(model, shape, betas)
        else:
            img = ddim_sample(model, shape, betas, steps=steps)
        elapsed = time.time() - start
        results.append({"method": name, "steps": steps, 
                       "time": f"{elapsed:.2f}s"})
    return results

methods = [("DDPM", 1000), ("DDIM-100", 100), 
           ("DDIM-50", 50), ("DDIM-20", 20)]
benchmark = benchmark_sampling(model, (1, 3, 64, 64), betas, methods)
for r in benchmark:
    print(f"{r['method']:10s} | {r['steps']:4d} steps | {r['time']}")
采样器步数时间(s)FID确定性插值能力

DDPM

1000

~12.0

3.2

否(随机)

不支持

DDIM

100

~1.5

3.5

是(确定)

支持

DDIM

50

~0.8

3.8

是(确定)

支持

DDIM

20

~0.3

4.5

是(确定)

支持

DPM-Solver

20

~0.3

3.1

是(确定)

支持

eta=0 得到确定性 DDIM,eta=1 回到随机 DDPM,可以调节 eta 在质量和多样性间权衡。

DDIM 步数过少(<10)时质量会明显下降,需根据具体任务选择合适的步数。

6条件扩散模型

无条件扩散模型只能生成随机样本,而实际应用中我们往往需要控制生成内容,比如根据文本描述生成图像,或指定类别生成特定物体。条件扩散模型通过在去噪过程中注入条件信息来实现可控生成。主要有两种范式:Classifier Guidance 和 Classifier-Free Guidance。Classifier Guidance 在训练一个独立分类器的基础上,利用分类器的梯度引导采样方向,实现简单但需要额外训练分类器。Classifier-Free Guidance 则在训练时随机丢弃条件(以一定概率用空条件替代),使得同一个网络同时学习有条件和无条件去噪,采样时通过线性组合两者的预测实现引导。后者不需要额外分类器,且效果更好,已成为 Stable Diffusion 等主流模型的标准做法。引导强度通过 guidance_scale 参数控制,值越大条件约束越强,但过大可能导致过饱和或伪影。

python
# Classifier-Free Guidance 采样
@torch.no_grad()
def cfg_sample(model, shape, betas, cond, steps=50, guidance_scale=7.5):
    T = len(betas)
    alpha = 1.0 - betas
    alpha_bar = torch.cumprod(alpha, dim=0)
    skip = T // steps
    seq = range(0, T, skip)
    seq_next = [-1] + list(seq[:-1])
    
    x = torch.randn(shape, device="cuda")
    batch_size = shape[0]
    
    for i, j in zip(reversed(seq), reversed(seq_next)):
        t_tensor = torch.full((batch_size,), i, device="cuda")
        
        # 有条件和无条件同时预测
        # cond 重复两次:[cond, empty]
        double_x = torch.cat([x, x], dim=0)
        double_t = torch.cat([t_tensor, t_tensor], dim=0)
        double_cond = torch.cat([cond, torch.zeros_like(cond)], dim=0)
        
        eps_pred = model(double_x, double_t, double_cond)
        eps_cond = eps_pred[:batch_size]
        eps_uncond = eps_pred[batch_size:]
        
        # CFG 公式
        eps = eps_uncond + guidance_scale * (eps_cond - eps_uncond)
        
        # DDIM 更新
        x0_pred = (x - torch.sqrt(1 - alpha_bar[i]) * eps) / \
                   torch.sqrt(alpha_bar[i])
        if j >= 0:
            x = torch.sqrt(alpha_bar[j]) * x0_pred + \
                torch.sqrt(1 - alpha_bar[j]) * eps
    return x
python
# 训练时随机丢弃条件
class ConditionalUNet(nn.Module):
    def __init__(self, in_ch=3, base_ch=128, cond_dim=512, 
                 dropout_rate=0.1):
        super().__init__()
        self.unet = SimpleUNet(in_ch, base_ch)
        self.cond_proj = nn.Linear(cond_dim, base_ch * 4)
        self.dropout_rate = dropout_rate
    
    def forward(self, x, t, cond=None):
        time_emb = self.unet.time_embed(t)
        
        if cond is not None and self.training:
            # 随机丢弃条件(Classifier-Free 的关键)
            mask = torch.bernoulli(
                torch.full((x.shape[0], 1), 1 - self.dropout_rate))
            cond = cond * mask.to(cond.device)
        
        if cond is not None:
            cond_emb = self.cond_proj(cond)
            time_emb = time_emb + cond_emb
        
        return self.unet(x, time_emb)
特性Classifier GuidanceClassifier-Free Guidance

需要分类器

是(额外训练)

否(同一网络)

训练复杂度

高(两个模型)

中(一个模型)

引导质量

更好

实现难度

Stable Diffusion

未使用

核心机制

引导强度控制

分类器权重

guidance_scale

guidance_scale 在 7.5 左右通常效果最好,过高会导致过饱和,过低则条件控制不足。

训练时 dropout_rate 设为 0.1-0.2,推理时必须关闭 dropout 并使用 CFG。

7PyTorch 实战

本节从零搭建一个可在 CIFAR-10 上训练的简化扩散模型。我们将整合前面学到的所有核心组件:方差调度、前向加噪、时间感知 U-Net、DDPM 训练目标和 DDIM 采样器。虽然这个模型规模不大(约 1500 万参数),但它包含了现代扩散模型的完整管线。训练时建议使用 4-8 张 GPU,batch size 设为 128-256,学习率 2e-4 配合 AdamW 优化器。CIFAR-10 图像尺寸仅为 32x32,训练 100 个 epoch 大约需要 6-12 小时。训练完成后,用 DDIM 采样器生成图像,20 步即可获得质量不错的样本。这个实战项目的关键不在于达到 SOTA 结果,而在于完整走通从数据加载到图像生成的全流程,为后续学习 Stable Diffusion 等大规模模型打下坚实基础。

python
# 完整扩散模型训练脚本
import torch
import torch.nn as nn
import torchvision
from torchvision import transforms
from torch.utils.data import DataLoader

class DiffusionTrainer:
    def __init__(self, model, lr=2e-4, device="cuda"):
        self.model = model.to(device)
        self.betas = get_beta_schedule(T=1000)
        self.optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
        self.device = device
    
    def train_epoch(self, dataloader):
        self.model.train()
        total_loss = 0
        for batch, _ in dataloader:
            x_0 = batch.to(self.device)
            loss = self._step(x_0)
            total_loss += loss
        return total_loss / len(dataloader)
    
    def _step(self, x_0):
        t = torch.randint(0, 1000, (x_0.shape[0],), device=self.device)
        noise = torch.randn_like(x_0)
        alpha = 1.0 - self.betas
        alpha_bar = torch.cumprod(alpha, dim=0)
        alpha_bar_t = alpha_bar[t][:, None, None, None]
        x_t = torch.sqrt(alpha_bar_t) * x_0 + \
              torch.sqrt(1 - alpha_bar_t) * noise
        pred = self.model(x_t, t)
        loss = nn.functional.mse_loss(noise, pred)
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()
        return loss.item()
python
# 主入口:训练 + 生成
if __name__ == "__main__":
    # 数据
    transform = transforms.Compose([
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
    ])
    dataset = torchvision.datasets.CIFAR10("./data", train=True,
                                          download=True, transform=transform)
    loader = DataLoader(dataset, batch_size=128, shuffle=True)
    
    # 模型 + 训练器
    model = ConditionalUNet(in_ch=3, base_ch=128, cond_dim=10)
    trainer = DiffusionTrainer(model)
    
    # 训练 100 epoch
    for epoch in range(100):
        loss = trainer.train_epoch(loader)
        if (epoch + 1) % 10 == 0:
            print(f"Epoch {epoch+1}: loss={loss:.6f}")
            # 生成样本
            samples = ddim_sample(model, (16, 3, 32, 32), 
                                  trainer.betas, steps=50)
            torchvision.utils.save_image(
                (samples + 1) / 2, f"samples_epoch_{epoch+1}.png",
                nrow=4)
超参数推荐值调大影响调小影响

batch_size

128-256

更稳定但显存增加

训练不稳定

learning_rate

2e-4

可能发散

收敛慢

T (步数)

1000

加噪更平滑但训练慢

信息丢失过快

base_ch

128

容量更大但更慢

欠拟合

ema_rate

0.9999

更平滑但响应慢

质量略降

dropout_rate

0.1

条件控制减弱

容易过拟合

使用 torch.amp.autocast 混合精度训练可以节省约 50% 显存,加速 1.5-2 倍。

CIFAR-10 虽然小但足以验证管线正确性,不要跳过这一步直接上大模型。

继续你的 AI 学习之旅

浏览更多 AI 知识库文章,或者探索 GitHub 上的优质 AI 项目