扩散模型图像生成


文档摘要

扩散模型图像生成 本节摘要:扩散模型学着去噪。训练它从一张噪声图里去掉一点点噪声,把这个去噪过程反向重复一千次,你就有了一个图像生成器。本节从零推导前向加噪过程 与闭合形式 ,实现 DDPM 的「预测噪声」训练目标与从纯噪声走回图像的采样器,搭一个带时间条件的小 U-Net(小到能在 CPU 上训),并讲清 DDPM 与 DDIM 采样的区别(后者快约 20 倍)。读完本节,你就掌握了下一节 Stable Diffusion 把它接成生产系统(VAE + 文本编码器 + 无分类器引导)所需的全部基础。 对应原课程:Phase 4 · Lesson 10 · (原英文 )。 学习目标 阅读完本节,你应当能够: 推导前向加噪过程 ,并解释为什么闭合形式 对任意 t 都成立。

扩散模型图像生成

本节摘要:扩散模型学着去噪。训练它从一张噪声图里去掉一点点噪声,把这个去噪过程反向重复一千次,你就有了一个图像生成器。本节从零推导前向加噪过程 x_0 → x_1 → … → x_T 与闭合形式 q(x_t | x_0),实现 DDPM 的「预测噪声」训练目标与从纯噪声走回图像的采样器,搭一个带时间条件的小 U-Net(小到能在 CPU 上训),并讲清 DDPM 与 DDIM 采样的区别(后者快约 20 倍)。读完本节,你就掌握了下一节 Stable Diffusion 把它接成生产系统(VAE + 文本编码器 + 无分类器引导)所需的全部基础。

对应原课程:Phase 4 · Lesson 10 · image-generation-diffusion(原英文 phases/04-computer-vision/10-image-generation-diffusion/docs/en.md)。

学习目标

阅读完本节,你应当能够:

  1. 推导前向加噪过程 x_0 → x_1 → … → x_T,并解释为什么闭合形式 q(x_t | x_0) 对任意 t 都成立。
  2. 实现 DDPM 风格训练目标(回归每步添加的噪声)与一个从纯噪声走回图像的采样器
  3. 搭一个时间条件的 U-Net(小到能在 CPU 上训),为任意时间步预测噪声。
  4. 解释 DDPM 与 DDIM 采样的区别及各自适用场景(第 23 节深入讲流匹配与整流流)。

一、问题与直觉

GAN 一次性生成:噪声进、图像出、一次前向。它快但难训。扩散模型迭代地生成:从纯噪声开始,以小步去噪,图像慢慢浮现。它慢但易训。过去五年,后一个性质主导了一切:任何小团队都能训扩散模型拿到合理样本;GAN 训练是一门你要多年失败运行才能学会的手艺。

除了训练稳定性,扩散的迭代结构解锁了现代图像生成做的一切:文本条件、内绘、图像编辑、超分辨率、可控风格。采样循环的每一步都是注入新约束的位置。这个钩子正是 Stable Diffusion、Imagen、DALL-E 3、Midjourney,以及你将用的每一个可控图像模型,都是基于扩散的原因。

本节搭最小 DDPM:前向加噪、反向去噪、训练循环。下一节(Stable Diffusion)把它接成生产系统,加 VAE、文本编码器、无分类器引导。

前向过程

取一张图像 x_0,加一丁点高斯噪声得 x_1,再加一丁点得 x_2,如此 T 步,直到 x_T 几乎与纯高斯噪声不可区分。

q(x_t | x_{t-1}) = N(x_t; sqrt(1 - beta_t) * x_{t-1}, beta_t * I)

beta_t 是一个小方差调度,典型在 T=1000 步上线性从 0.0001 到 0.02。每步略微收缩信号并注入新噪声。

闭合形式跳跃

一次加一点噪声是马尔可夫链,但数学折叠了:你可以一步直接从 x_0 采样 x_t

定义 alpha_t = 1 - beta_t 定义 alpha_bar_t = prod_{s=1..t} alpha_s 则: q(x_t | x_0) = N(x_t; sqrt(alpha_bar_t) * x_0, (1 - alpha_bar_t) * I) 等价地: x_t = sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * epsilon 其中 epsilon ~ N(0, I)

这一个方程就是扩散实用的全部原因。训练时你随机挑一个 t,直接从 x_0 采样 x_t,一步训练——无需模拟整条马尔可夫链。

反向过程

前向过程是固定的。反向过程 p(x_{t-1} | x_t) 才是神经网络学的。扩散模型不直接预测 x_{t-1};它预测第 t 步添加的噪声 epsilon,数学再由它推出 x_{t-1}

训练损失

每个训练步:

  1. 采样一张真实图像 x_0
  2. 从 [1, T] 均匀采样一个时间步 t
  3. 采样噪声 epsilon ~ N(0, I)
  4. 计算 x_t = sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * epsilon
  5. 用网络预测 epsilon_theta(x_t, t)
  6. 最小化 || epsilon - epsilon_theta(x_t, t) ||^2

就这样。神经网络学会预测任意时间步的噪声。损失是 MSE。没有对抗博弈、没有崩溃、没有振荡。

采样器(DDPM)

生成时:从 x_T ~ N(0, I) 出发,一步一步反向走。

for t = T, T-1, ..., 1: eps = model(x_t, t) x_{t-1} = (1 / sqrt(alpha_t)) * (x_t - (beta_t / sqrt(1 - alpha_bar_t)) * eps) + sqrt(beta_t) * z 其中 t > 1 时 z ~ N(0, I),否则为 0 return x_0

关键是:尽管一般情况下反向条件式没有闭合形式,但对这个特定的高斯前向过程,它是有的。那些看起来丑陋的系数,就是贝叶斯规则给你的。

为什么是 1000 步

前向噪声调度选成每步刚好加足够噪声,让反向步近乎高斯。步太少,反向步远离高斯,网络建模不好;步太多,采样变贵而增益递减。T=1000、线性调度是 DDPM 默认。

DDIM:快 20 倍的采样

训练不变,采样变。DDIM(Song 等,2020)定义了一个确定性的反向过程,无需重训就能跳过时间步。用 DDIM 50 步采样能达到接近 1000 步 DDPM 的质量。每个生产系统都用 DDIM 或更快的变体(DPM-Solver、Euler ancestral)。

时间条件

网络 epsilon_theta(x_t, t) 需要知道自己在去哪个时间步的噪。现代扩散模型用正弦时间嵌入(与 transformer 里的位置编码同思路)把 t 注入,在 U-Net 每一级加到特征图上。

t_embedding = sinusoidal(t) feature_map += MLP(t_embedding)

没有时间条件,网络就得从图像本身猜噪声水平,能行但样本效率低得多。

二、从零实现

步骤 1:噪声调度

import torch def linear_beta_schedule(T=1000, beta_start=1e-4, beta_end=2e-2): return torch.linspace(beta_start, beta_end, T) def precompute_schedule(betas): alphas = 1.0 - betas alphas_cumprod = torch.cumprod(alphas, dim=0) return { "betas": betas, "alphas": alphas, "alphas_cumprod": alphas_cumprod, "sqrt_alphas_cumprod": torch.sqrt(alphas_cumprod), "sqrt_one_minus_alphas_cumprod": torch.sqrt(1.0 - alphas_cumprod), "sqrt_recip_alphas": torch.sqrt(1.0 / alphas), } schedule = precompute_schedule(linear_beta_schedule(T=1000))

预计算一次,训练和采样时按下标取。

步骤 2:前向扩散(q_sample)

def q_sample(x0, t, noise, schedule): sqrt_a = schedule["sqrt_alphas_cumprod"][t].view(-1, 1, 1, 1) sqrt_one_minus_a = schedule["sqrt_one_minus_alphas_cumprod"][t].view(-1, 1, 1, 1) return sqrt_a * x0 + sqrt_one_minus_a * noise

一行闭合形式。t 是一批时间步,每图一个。

步骤 3:一个微型时间条件 U-Net

import torch.nn as nn import torch.nn.functional as F import math def timestep_embedding(t, dim=64): half = dim // 2 freqs = torch.exp(-math.log(10000) * torch.arange(half, device=t.device) / half) args = t[:, None].float() * freqs[None] emb = torch.cat([args.sin(), args.cos()], dim=-1) return emb class TinyUNet(nn.Module): def __init__(self, img_channels=3, base=32, t_dim=64): super().__init__() self.t_mlp = nn.Sequential( nn.Linear(t_dim, base * 4), nn.SiLU(), nn.Linear(base * 4, base * 4), ) self.t_dim = t_dim self.enc1 = nn.Conv2d(img_channels, base, 3, padding=1) self.enc2 = nn.Conv2d(base, base * 2, 4, stride=2, padding=1) self.mid = nn.Conv2d(base * 2, base * 2, 3, padding=1) self.dec1 = nn.ConvTranspose2d(base * 2, base, 4, stride=2, padding=1) self.dec2 = nn.Conv2d(base * 2, img_channels, 3, padding=1) self.time_proj = nn.Linear(base * 4, base * 2) def forward(self, x, t): t_emb = timestep_embedding(t, self.t_dim) t_emb = self.t_mlp(t_emb) t_proj = self.time_proj(t_emb)[:, :, None, None] h1 = F.silu(self.enc1(x)) h2 = F.silu(self.enc2(h1)) + t_proj h3 = F.silu(self.mid(h2)) d1 = F.silu(self.dec1(h3)) d2 = torch.cat([d1, h1], dim=1) return self.dec2(d2)

两层 U-Net,时间条件注入在瓶颈处。真实图像要加大深度和宽度。

步骤 4:训练循环

def train_step(model, x0, schedule, optimizer, device, T=1000): model.train() x0 = x0.to(device) bs = x0.size(0) t = torch.randint(0, T, (bs,), device=device) noise = torch.randn_like(x0) x_t = q_sample(x0, t, noise, schedule) pred = model(x_t, t) loss = F.mse_loss(pred, noise) optimizer.zero_grad() loss.backward() optimizer.step() return loss.item()

这就是整个训练循环。没有 GAN 博弈,没有专用损失,一次 MSE 调用。

步骤 5:采样器(DDPM)

@torch.no_grad() def sample(model, schedule, shape, T=1000, device="cpu"): model.eval() x = torch.randn(shape, device=device) betas = schedule["betas"].to(device) sqrt_one_minus_a = schedule["sqrt_one_minus_alphas_cumprod"].to(device) sqrt_recip_alphas = schedule["sqrt_recip_alphas"].to(device) for t in reversed(range(T)): t_batch = torch.full((shape[0],), t, dtype=torch.long, device=device) eps = model(x, t_batch) coef = betas[t] / sqrt_one_minus_a[t] mean = sqrt_recip_alphas[t] * (x - coef * eps) if t > 0: x = mean + torch.sqrt(betas[t]) * torch.randn_like(x) else: x = mean return x

1000 次前向产出一批样本。真实代码里你会换成 DDIM 50 步采样器。

步骤 6:DDIM 采样器(确定性,约快 20 倍)

@torch.no_grad() def sample_ddim(model, schedule, shape, steps=50, T=1000, device="cpu", eta=0.0): model.eval() x = torch.randn(shape, device=device) alphas_cumprod = schedule["alphas_cumprod"].to(device) ts = torch.linspace(T - 1, 0, steps + 1).long() for i in range(steps): t = ts[i] t_prev = ts[i + 1] t_batch = torch.full((shape[0],), t, dtype=torch.long, device=device) eps = model(x, t_batch) a_t = alphas_cumprod[t] a_prev = alphas_cumprod[t_prev] if t_prev >= 0 else torch.tensor(1.0, device=device) x0_pred = (x - torch.sqrt(1 - a_t) * eps) / torch.sqrt(a_t) sigma = eta * torch.sqrt((1 - a_prev) / (1 - a_t) * (1 - a_t / a_prev)) dir_xt = torch.sqrt(1 - a_prev - sigma ** 2) * eps noise = sigma * torch.randn_like(x) if eta > 0 else 0 x = torch.sqrt(a_prev) * x0_pred + dir_xt + noise return x

eta=0 完全确定(同一噪声输入总产同一输出);eta=1 退化为 DDPM。

三、框架对比

生产工作用 diffusers:

from diffusers import DDPMScheduler, UNet2DModel unet = UNet2DModel(sample_size=32, in_channels=3, out_channels=3, layers_per_block=2) scheduler = DDPMScheduler(num_train_timesteps=1000)

这个库提供现成的调度器(DDPM、DDIM、DPM-Solver、Euler、Heun)、可配置的 U-Net、文生图与图生图的流水线、LoRA 微调助手。

研究用 k-diffusion(Katherine Crowson),有最忠实的参考实现和最好的采样变体。

四、可复用产物

本节产出两个可复用文件(位于原课程 outputs/):

  • prompt-diffusion-sampler-picker.md:一个提示词——按质量目标、延迟预算和条件类型,在 DDPM / DDIM / DPM-Solver / Euler 之间挑。
  • skill-noise-schedule-designer.md:一个技能——给定 T 和目标腐蚀程度,产出线性、余弦或 sigmoid 的 beta 调度,外加信噪比随时间的诊断图。

五、练习

  1. (简单) 可视化前向过程:取一张图,画出 t in [0, 100, 250, 500, 750, 1000] 处的 x_t。验证 x_1000 看起来像纯高斯噪声。
  2. (中等) 在合成圆形数据集上训 TinyUNet 20 个 epoch,采样 16 个圆。比较 DDPM(1000 步)与 DDIM(50 步)采样——同一噪声种子下它们产出相似的图吗?
  3. (困难) 实现余弦噪声调度(Nichol & Dhariwal, 2021):alpha_bar_t = cos^2((t/T + s) / (1 + s) * pi / 2)。用线性和余弦调度训同一模型,证明余弦在低步数下样本更好。

本节要点回顾

  1. 扩散 = 学去噪——前向加噪到纯高斯,反向逐步去噪出图;慢但易训。
  2. 闭合形式跳跃是关键——x_t = sqrt(ᾱ_t)·x_0 + sqrt(1-ᾱ_t)·ε,训练时一步采样 x_t,无需模拟整条链。
  3. 预测噪声而非图像——网络学 ε_θ(x_t, t),损失就是 MSE;无对抗、无崩溃、无振荡。
  4. 采样 1000 步——反向逐步走,每步从条件高斯采样;系数来自贝叶斯规则。
  5. DDIM 快 20 倍——确定性 ODE,无需重训就能 50 步接近 1000 步质量;eta=0 确定性,eta=1 退化 DDPM。
  6. 时间条件必须——正弦时间嵌入注入 U-Net,网络才知道噪声水平;否则只能从图本身猜。
  7. T=1000 线性调度是默认——太少步反向步远离高斯,太多步增益递减。
  8. 迭代结构是可控性的源泉——采样每一步都是注入约束(文本/内绘/超分)的位置,这正是所有可控图像模型基于扩散的原因。
  9. 生产用 diffusers——现成调度器、可配置 U-Net、文生图/图生图流水线、LoRA 微调。
  10. 研究用 k-diffusion——最忠实的参考实现和最佳采样变体。

下一节,我们把这套 DDPM 接成 Stable Diffusion——加 VAE(隐空间扩散)、CLIP 文本编码器、无分类器引导,让它能听懂文字。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U