引言
过去十年,图像生成经历了三次范式更替:GAN 生成快但训练不稳、易模式崩塌;VAE 理论优雅但输出模糊;直到 扩散模型(Diffusion Model) 出现,才第一次同时做到了高质量、训练稳定、覆盖多样。Stable Diffusion、Midjourney、DALL·E 背后的核心,都是扩散模型。
扩散模型的思想出奇地简单:先把图像一步步加噪直到变成纯噪声,再训练一个网络学会一步步去噪。生成时从纯噪声出发,反复去噪,就能「无中生有」地变出一张图。本文从 DDPM 的数学出发,讲到 U-Net/DiT 架构、潜在扩散的效率革命、CFG 条件控制、采样加速,最后给出 Stable Diffusion 的实战与 LoRA 微调完整流程。
前置:神经网络与反向传播基础见 https://plumephp.com/ml-neural-networks-basics/;图像任务与卷积基础见 https://plumephp.com/ml-cv-basics/;LoRA 低秩微调原理见 https://plumephp.com/ml-llm-finetuning-practice/;生成模型的部署见 https://plumephp.com/ml-model-deployment/。
目录
- 1. 生成模型的三条路线
- 2. DDPM:前向加噪与反向去噪
- 3. 噪声调度与训练目标
- 4. U-Net 与 DiT 架构
- 5. 潜在扩散与 VAE
- 6. 条件生成与 CFG
- 7. 采样加速
- 8. Stable Diffusion 实战与 LoRA 微调
- 9. 总结
- 延伸阅读
1. 生成模型的三条路线
1.1 三者的差异
| 模型 | 核心思想 | 优点 | 缺点 |
|---|---|---|---|
| GAN | 生成器与判别器博弈 | 采样快、图像锐利 | 训练不稳、模式崩塌 |
| VAE | 学隐变量 + 变分下界 | 理论清晰、隐空间连续 | 输出偏模糊 |
| Diffusion | 学去噪过程 | 质量高、训练稳、多样 | 采样慢 |
扩散模型的「慢」是它的原罪:原始 DDPM 生成一张图要 1000 步前向。但这个缺点可以用采样加速和后训练蒸馏大幅缓解,而质量和稳定性的优势很难被替代,所以它成了主流。
1.2 扩散模型的两个过程
前向过程(固定,无需学习):x0 → x1 → ... → xT (逐步加高斯噪声)
反向过程(需要学习) :xT → ... → x1 → x0 (逐步去噪)
前向过程把数据分布「打散」成标准高斯;反向过程学习如何「复原」。训练目标就是让网络预测每一步加进去的噪声。
一句话:扩散模型把「生成」转化为「去噪」——用固定规则加噪,用神经网络学去噪,从而绕开了 GAN 的对抗不稳定和 VAE 的模糊问题。
1.3 为什么它能工作
直觉上,把一个复杂分布逐步加噪,每一步的变换都足够小,因此每步的反向变换都可以用简单的高斯分布近似。把 1000 个「简单小步」串起来,就能从纯噪声走到一张真实的图。这是扩散模型「以时间换表达力」的核心。
2. DDPM:前向加噪与反向去噪
2.1 前向过程:一步到位的重参数化
前向过程定义为逐步加噪:x_t = √(1-β_t) x_{t-1} + √β_t ε。由于高斯分布的叠加性,可以从 x0 一步直接算到任意 x_t:
x_t = √(ᾱ_t) · x_0 + √(1 - ᾱ_t) · ε 其中 ε ~ N(0, I),ᾱ_t = ∏(1-β_i)
这个重参数化是 DDPM 能高效训练的关键:随机采一个 t,直接构造 x_t,无需模拟 T 步。
import torch
def q_sample(x0, t, alphas_cumprod):
"""前向扩散:从 x0 直接采样 x_t"""
sqrt_alpha_bar = alphas_cumprod[t].sqrt().view(-1, 1, 1, 1)
sqrt_one_minus = (1 - alphas_cumprod[t]).sqrt().view(-1, 1, 1, 1)
noise = torch.randn_like(x0)
return sqrt_alpha_bar * x0 + sqrt_one_minus * noise, noise
2.2 反向过程:网络学什么
网络不直接预测 x_{t-1},而是预测加进去的噪声 ε。给定 x_t 和 t,网络输出 ε_θ(x_t, t),再据此算出 x_{t-1} 的均值和方差。预测噪声比预测图像更稳定,因为噪声是标准高斯,尺度统一。
2.3 训练循环:极简的损失
DDPM 的训练损失出奇地简洁——就是预测噪声与真实噪声的 MSE:
import torch.nn.functional as F
def train_step(model, x0, alphas_cumprod, optimizer):
batch = x0.size(0)
t = torch.randint(0, 1000, (batch,), device=x0.device) # 随机时间步
x_t, noise = q_sample(x0, t, alphas_cumprod) # 加噪
noise_pred = model(x_t, t) # 预测噪声
loss = F.mse_loss(noise_pred, noise) # 简单 MSE
optimizer.zero_grad()
loss.backward()
optimizer.step()
return loss.item()
整个训练目标只有一行 MSE——这是扩散模型训练稳定的根本原因:没有对抗、没有 KL 散度,只有回归。
一句话:DDPM 用重参数化把「加噪 1000 步」变成「一步采样 x_t」,训练目标退化为「预测噪声的 MSE」,简单、稳定、可并行,这是它碾压 GAN 训练体验的原因。
3. 噪声调度与训练目标
3.1 β 调度:线性 vs 余弦
β_t 决定每步加多少噪声,是关键超参。线性调度(0.0001→0.02)是 DDPM 原始方案;余弦调度让噪声在中间步增长更平滑,低分辨率下效果更好。
| 调度 | 公式 | 特点 |
|---|---|---|
| Linear | β_t 线性递增 | 简单、原始方案 |
| Cosine | ᾱ_t = cos²(…) | 中间步更平滑、更常用 |
| Scaled Linear | 按分辨率缩放 | 高分辨率友好 |
| Sigmoid | sigmoid 曲线 | 两端平缓 |
import numpy as np
def cosine_schedule(timesteps=1000, s=0.008):
steps = timesteps + 1
x = np.linspace(0, timesteps, steps)
alphas_cumprod = np.cos(((x / timesteps) + s) / (1 + s) * np.pi * 0.5) ** 2
alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1])
return np.clip(betas, 0.0001, 0.9999)
3.2 预测什么:ε、x0 还是 v
网络的预测目标有三种等价但表现不同的选择:
| 目标 | 公式 | 特点 |
|---|---|---|
| ε-prediction | 预测噪声 | DDPM 原始,高噪声区稳 |
| x0-prediction | 预测干净图 | 低噪声区稳 |
| v-prediction | 预测速度 v=√ᾱ·ε−√(1−ᾱ)·x0 | 全区间均衡,SD 2.x 采用 |
v-prediction 在高低噪声区都数值稳定,是当前主流选择之一。
3.3 简化损失 vs 加权损失
DDPM 的简化损失对所有 t 一视同仁,但不同 t 的学习难度不同。改进方案是给损失加权(如 Min-SNR 加权),让训练聚焦在难学的中间步,收敛更快。
一句话:噪声调度决定「何时加多少噪」,预测目标决定「网络学什么」,两者共同决定生成质量;实践中余弦调度 + v-prediction + 损失加权是稳健的默认组合。
4. U-Net 与 DiT 架构
4.1 U-Net:扩散模型的经典骨干
U-Net 是编码器-解码器结构,带跳跃连接:编码器逐层下采样提取语义,解码器逐层上采样恢复分辨率,跳跃连接把同分辨率的浅层特征直接送到解码器,保住细节。时间步 t 通过时间嵌入(正弦位置编码 + MLP)注入每个残差块。
输入 x_t (H×W×C)
↓ 编码器(下采样,通道翻倍)
↓ 中间块(自注意力 + 残差)
↑ 解码器(上采样 + 跳跃连接)
输出 ε_pred (H×W×C)
时间嵌入 t → 注入每个残差块
4.2 DiT:用 Transformer 取代 U-Net
DiT(Diffusion Transformer)把 U-Net 换成纯 Transformer:图像切成 patch(同 ViT),加位置编码和时间嵌入,用堆叠的 Transformer 块处理。DiT 的扩展性更好——加大参数量、加大数据,FID 持续下降,而 U-Net 会饱和。Sora、SD3 都用 DiT 类架构。
import torch
import torch.nn as nn
class DiTBlock(nn.Module):
def __init__(self, dim, heads, mlp_ratio=4.0):
super().__init__()
self.norm1 = nn.LayerNorm(dim)
self.attn = nn.MultiheadAttention(dim, heads, batch_first=True)
self.norm2 = nn.LayerNorm(dim)
self.mlp = nn.Sequential(
nn.Linear(dim, int(dim * mlp_ratio)), nn.GELU(),
nn.Linear(int(dim * mlp_ratio), dim),
)
# adaLN:时间/条件通过调制 scale/shift 注入
self.ada = nn.Linear(dim, dim * 6)
def forward(self, x, cond):
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = \
self.ada(cond).chunk(6, dim=-1)
h = self.norm1(x) * (1 + scale_msa.unsqueeze(1)) + shift_msa.unsqueeze(1)
x = x + gate_msa.unsqueeze(1) * self.attn(h, h, h)[0]
h = self.norm2(x) * (1 + scale_mlp.unsqueeze(1)) + shift_mlp.unsqueeze(1)
x = x + gate_mlp.unsqueeze(1) * self.mlp(h)
return x
4.3 两者的取舍
| 维度 | U-Net | DiT |
|---|---|---|
| 归纳偏置 | 强(局部、多尺度) | 弱(需更多数据) |
| 扩展性 | 会饱和 | 持续提升 |
| 推理速度 | 较快 | 较慢(序列长) |
| 代表 | DDPM、SD1.5 | DiT、SD3、Sora |
一句话:U-Net 靠多尺度跳跃连接在中小规模下高效,DiT 靠 Transformer 的扩展性在大数据下持续变强;架构之争的答案取决于你有多少数据与算力。
5. 潜在扩散与 VAE
5.1 像素空间的昂贵
在 512×512×3 的像素空间直接做扩散,每一步都要处理 78 万维的向量,训练和采样都极慢。潜在扩散(Latent Diffusion) 的解法是:先用 VAE 把图像压缩到低维隐空间(如 64×64×4,压缩 48 倍),在隐空间里做扩散。
图像 512×512×3 → [VAE 编码器] → 隐表示 64×64×4
↓ 在隐空间做扩散(快 48 倍)
隐表示 64×64×4 → [VAE 解码器] → 图像 512×512×3
这就是 Stable Diffusion 名字里「Stable」之外的真正效率来源——Latent。
5.2 VAE 的两个损失
VAE 编码器输出的不是确定向量,而是均值和对数方差,采样时用重参数化技巧保证可导。损失 = 重建损失(像素 MSE/LPIPS)+ KL 正则(让隐空间接近高斯,便于扩散)。
import torch
import torch.nn as nn
import torch.nn.functional as F
class DiagonalGaussian(nn.Module):
"""把编码器输出拆成 mean 和 logvar,重参数化采样"""
def forward(self, z):
mean, logvar = torch.chunk(z, 2, dim=1)
logvar = torch.clamp(logvar, -30.0, 20.0)
std = torch.exp(0.5 * logvar)
if self.training:
return mean + std * torch.randn_like(mean)
return mean
5.3 自编码器的缩放因子
隐空间的数值尺度需要归一化到扩散模型友好的范围,Stable Diffusion 用一个 scale_factor(约 0.18215)缩放隐表示。加载 VAE 时要正确应用它,否则图像会全灰或过曝。
from diffusers import AutoencoderKL
vae = AutoencoderKL.from_pretrained("stabilityai/sd-vae-ft-mse")
scale_factor = vae.config.scaling_factor # 0.18215
一句话:潜在扩散把扩散过程从像素空间搬到 VAE 隐空间,计算量降一个数量级而质量几乎不损——这是 Stable Diffusion 能在消费级显卡上跑起来的根本原因。
6. 条件生成与 CFG
6.1 如何控制生成内容
无条件扩散只能随机生成。要「按提示词生成」,需要条件注入:把文本编码成向量,通过交叉注意力注入 U-Net/DiT 的每一层。文本编码器通常用 CLIP text encoder 或 T5。
文本 "a red cup" → [Text Encoder] → 文本嵌入
↓ 交叉注意力(Q 来自图像,K/V 来自文本)
图像特征 ─────────────────────────────┘
6.2 Classifier-Free Guidance
CFG 是控制「提示词强度」的核心技巧:同时算有条件预测和无条件预测,然后外推:
ε_guided = ε_uncond + w · (ε_cond − ε_uncond)
w 是 guidance scale。w=1 是普通条件生成;w>1 放大提示词影响、图像更贴合文本但可能过饱和、失真;w<1 更自由但可能跑题。
# 训练时随机把条件置空(10% 概率),让模型同时学会有条件/无条件
import random
if random.random() < 0.1:
text_emb = null_text_emb # 无条件
# 推理时 CFG
noise_uncond = model(x_t, t, null_text_emb)
noise_cond = model(x_t, t, text_emb)
noise = noise_uncond + guidance_scale * (noise_cond - noise_uncond)
6.3 其他条件控制
| 控制方式 | 手段 | 用途 |
|---|---|---|
| 文本 | 交叉注意力 | 文生图 |
| ControlNet | 额外网络 + 空间条件 | 边缘/深度/姿态控制 |
| IP-Adapter | 图像提示词 | 风格迁移 |
| LoRA | 低秩微调 | 概念/风格定制 |
| Inpainting | mask 条件 | 局部重绘 |
一句话:CFG 通过「有条件减无条件再外推」把文本控制力放大,guidance scale 是质量与贴合度的权衡旋钮;ControlNet、IP-Adapter、LoRA 则提供了文本之外的空间、风格、概念控制。
7. 采样加速
7.1 DDPM 慢在哪
原始 DDPM 要跑 1000 步,每步一次完整 U-Net 前向,生成一张图要几十秒。加速的两条路:减少步数(更好的采样器)和减少每步成本(蒸馏)。
7.2 主流采样器
| 采样器 | 步数 | 特点 |
|---|---|---|
| DDPM | 1000 | 原始、慢 |
| DDIM | 20-50 | 确定性、可跳步 |
| DPM-Solver | 10-20 | 高阶求解器、质量好 |
| Euler / Euler-a | 20-30 | 简单稳定 |
| LCM | 2-4 | 一致性蒸馏 |
from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16
).to("cuda")
# 换 DPM-Solver,20 步出图
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
image = pipe("a red cup on a wooden table", num_inference_steps=20,
guidance_scale=7.5).images[0]
7.3 蒸馏与一致性模型
LCM(Latent Consistency Model) 把扩散蒸馏成「一步预测」:训练学生网络直接从噪声映射到 x0 附近,2-4 步就能出图,适合实时应用。代价是细节和多样性略降。
| 加速手段 | 步数 | 相对质量 |
|---|---|---|
| DPM-Solver++ | 10-20 | 高 |
| LCM | 2-4 | 中高 |
| Turbo / Lightning | 1-4 | 中 |
| 量化 + 编译 | 同前 | 保持 |
一句话:采样加速先换采样器(DDIM/DPM-Solver 从 1000 步降到 20 步),再用蒸馏(LCM/Turbo)降到个位数步数;前者几乎无损,后者用少量质量换实时性。
8. Stable Diffusion 实战与 LoRA 微调
8.1 文生图完整脚本
import torch
from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16,
safety_checker=None,
).to("cuda")
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
pipe.enable_attention_slicing() # 省显存
prompt = "a red ceramic cup on a wooden table, soft window light, 50mm photo"
negative = "blurry, low quality, watermark"
image = pipe(
prompt=prompt,
negative_prompt=negative,
num_inference_steps=25,
guidance_scale=7.5,
generator=torch.Generator("cuda").manual_seed(42),
).images[0]
image.save("cup.png")
8.2 LoRA 微调:定制风格或概念
LoRA 在注意力层插入低秩矩阵,只训练极少参数(通常 < 1%),就能让模型学会新概念、新风格。用 diffusers + peft 训练:
accelerate launch train_dreambooth_lora.py \
--pretrained_model_name_or_path="runwayml/stable-diffusion-v1-5" \
--instance_data_dir="./my_concept" \
--instance_prompt="a photo of sks cup" \
--resolution=512 --train_batch_size=1 \
--gradient_accumulation_steps=4 \
--learning_rate=1e-4 --max_train_steps=800 \
--mixed_precision="fp16" --output_dir="./lora_cup"
推理时加载 LoRA:
pipe.load_lora_weights("./lora_cup")
pipe.fuse_lora(lora_scale=0.8) # 0.8 控制 LoRA 影响强度
image = pipe("a photo of sks cup on a beach", num_inference_steps=25).images[0]
8.3 微调的关键坑
| 现象 | 根因 | 处理 |
|---|---|---|
| 过拟合(只会画训练图) | 步数太多、lr 太大 | 降步数、加正则图、用 prior preservation |
| 概念学不会 | 提示词不唯一 | 用稀有 token(sks)当触发词 |
| 画风崩坏 | 训练图质量差 | 清洗数据、统一分辨率 |
| 显存 OOM | batch 太大 | 梯度累积 + 8bit Adam + 梯度检查点 |
| 生成全灰 | VAE 缩放因子错 | 检查 scaling_factor |
一句话:Stable Diffusion 实战三步走——选好采样器与 CFG、写好正负提示词、按需用 LoRA 定制;微调的核心是「少量步数 + 稀有触发词 + 防过拟合」。
9. 总结
9.1 技术栈全景
数学层:前向加噪 + 反向去噪(DDPM)
调度层:余弦 β + v-prediction
架构层:U-Net(中小规模)/ DiT(大规模)
效率层:VAE 潜在空间 + DPM-Solver 采样加速
控制层:CFG + ControlNet + LoRA
9.2 关键决策点
| 问题 | 选择 |
|---|---|
| 追求生成质量 | 大 DiT + 多步 DPM-Solver |
| 追求实时性 | LCM/Turbo 蒸馏 + 4bit 量化 |
| 需要精确空间控制 | ControlNet |
| 定制风格/概念 | LoRA 微调 |
| 显存不足 | 潜在扩散 + attention slicing |
| 提示词不听话 | 调高 guidance scale(但别过 12) |
9.3 一句话心法
扩散模型的全部魔法是「学会去噪」——训练目标简单到只有一行 MSE,工程复杂度几乎都花在架构(U-Net/DiT)、效率(潜在空间)和控制(CFG/LoRA)这三件事上。
延伸阅读
- https://plumephp.com/ml-neural-networks-basics/ — 反向传播与网络结构基础
- https://plumephp.com/ml-cv-basics/ — 卷积网络与图像特征提取
- https://plumephp.com/ml-llm-finetuning-practice/ — LoRA 低秩微调的原理与实现
- https://plumephp.com/ml-model-deployment/ — 生成模型的服务化与优化
- AI/ML 专题 — 生成模型与多模态深度文章
- Diffusers 官方文档
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。