2026-02-07
第一章:从零训练扩散模型
本章基于 Hugging Face Diffusers 教程,学习如何从零开始训练一个简单的扩散模型(以蝴蝶图片为例)。
核心训练流程
扩散模型的训练可以概括为以下五个步骤:
- 从训练数据中加载图片
- 向图片添加不同程度的噪声
- 将带噪图片输入模型
- 评估模型的去噪效果
- 根据损失更新模型权重,重复以上步骤
数据预处理
使用 torchvision.transforms 进行数据增强:
preprocess = transforms.Compose([
transforms.Resize((image_size, image_size)),
transforms.RandomHorizontalFlip(), # 随机水平翻转,增加数据多样性
transforms.ToTensor(), # 转换为张量,值域 [0, 1]
transforms.Normalize([0.5], [0.5]), # 归一化到 [-1, 1]
])
要点:归一化到 -1, 1 是扩散模型的常见做法,因为噪声也是从标准正态分布采样的。
噪声调度器(Noise Scheduler)
DDPM 论文的核心思想是前向扩散过程——逐步向图像添加高斯噪声:
其中:
- 是第 t 步的带噪图像
- 是噪声调度参数,控制每一步添加多少噪声
- 是对原图的缩放系数
直接计算任意时刻的噪声图像:
其中 ,
from diffusers import DDPMScheduler
noise_scheduler = DDPMScheduler(num_train_timesteps=1000)
# 添加噪声
timesteps = torch.linspace(0, 999, 8).long().to(device)
noise = torch.randn_like(xb)
noisy_xb = noise_scheduler.add_noise(xb, noise, timesteps)
调度器选择:对于小尺寸图像(如 64x64),cosine 调度可能比默认的 linear 调度效果更好。
U-Net 模型架构
扩散模型通常使用 U-Net 架构,其特点是输入输出尺寸相同:
128x128 → [Downsample] → 64x64 → [Downsample] → 32x32 (中间层)
↓
128x128 ← [Upsample] ← 64x64 ← [Upsample] ← 32x32
关键设计:
- 下采样路径:多个 ResNet 块,每次将图像尺寸减半
- 上采样路径:对应的 ResNet 块,将图像尺寸翻倍
- 跳跃连接(Skip Connections):连接下采样和上采样的对应层,保留细节信息
from diffusers import UNet2DModel
model = UNet2DModel(
sample_size=image_size, # 目标图像分辨率
in_channels=3, # RGB 输入
out_channels=3, # RGB 输出
layers_per_block=2, # 每个 UNet 块中的 ResNet 层数
block_out_channels=(64, 128, 128, 256), # 各层通道数
down_block_types=("DownBlock2D", "DownBlock2D", ...),
up_block_types=("UpBlock2D", "UpBlock2D", ...),
)
内存优化提示:对于高分辨率图像,可以只在最低分辨率层使用注意力机制(Attention),以减少显存占用。
本章小结
| 组件 | 作用 |
|---|---|
| 数据预处理 | 归一化、增强,准备训练数据 |
| 噪声调度器 | 控制前向扩散过程,决定每步加多少噪声 |
| U-Net 模型 | 学习预测噪声,实现去噪 |
| 训练循环 | 加噪 → 预测 → 计算损失 → 更新权重 |
下一章将学习如何实现完整的训练循环和采样过程。
第二章:微调、引导与条件生成
本章学习如何利用预训练模型,以及如何控制生成过程。
微调(Fine-Tuning)
从零训练扩散模型既耗时又耗资源,尤其是高分辨率图像。更好的方案是从预训练模型开始:
- 预训练模型已学会去噪,提供了比随机初始化更好的起点
- 即使新数据与原始训练数据差异很大,微调仍然有效
- 例如:用 LSUN 卧室数据集训练的模型,只需 500 步就能微调到 WikiArt 艺术风格
最佳实践:新数据与原始数据越相似,微调效果越好(如:用人脸模型微调卡通人脸)。
引导(Guidance)
对于已训练好的无条件模型,如何控制生成内容?答案是引导:
在每一步生成过程中,用引导函数评估模型预测,并修改预测结果,使最终图像更符合期望。
引导函数可以是任意函数,这使其非常灵活:
| 引导类型 | 实现方式 |
|---|---|
| 颜色引导 | 计算生成图像与目标颜色的损失,引导模型生成特定色调 |
| CLIP 引导 | 使用 CLIP 模型计算图像与文本描述的相似度,实现文本引导生成 |
条件生成(Conditioning)
如果训练时就有额外信息(类别标签、文本描述等),可以直接将其作为条件输入模型:
训练时:模型学习 P(图像|条件)
推理时:指定条件 → 控制生成内容
三种传入条件信息的方式:
- 作为额外输入通道
- 将条件扩展为与输入图像相同的尺寸,拼接为额外通道
- 适用于:分割掩码、深度图、模糊图像(超分辨率)、类别标签
- 投影到内部层输出
- 将条件嵌入投影到与 UNet 内部层输出相同的维度,然后相加
- 时间步条件就是这样处理的(每个 ResNet 块的输出都加上时间步嵌入)
- 适用于:CLIP 图像嵌入等向量形式的条件
- 交叉注意力层(Cross-Attention)
- 在 UNet 中添加交叉注意力层,让模型"关注"条件序列
- 文本条件先通过 Transformer 映射为嵌入序列,再通过交叉注意力融入去噪路径
- Stable Diffusion 就是这样处理文本条件的(Unit 3 详解)
本章小结
| 技术 | 适用场景 | 核心思想 |
|---|---|---|
| 微调 | 已有预训练模型,想适配新数据 | 继承已学知识,快速适应新领域 |
| 引导 | 已有无条件模型,想控制生成 | 推理时用引导函数修改预测 |
| 条件生成 | 训练时有额外信息 | 让模型学习条件到图像的映射 |
下一章将深入学习 Stable Diffusion 的架构和文本条件生成机制。
附录:U-Net 架构详解
U-Net 是扩散模型的核心骨干网络,其设计使得模型能够在保持空间信息的同时进行多尺度特征提取。
整体架构图
输入: 带噪图像 x_t (B, C, H, W) + 时间步 t
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ U-Net │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Encoder │ │ Decoder │ │
│ │ (下采样) │ │ (上采样) │ │
│ │ │ ┌─────────┐ │ │ │
│ │ 64x64 ───┼────────►│ │◄─────────┼─── 64x64 │ │
│ │ │ │ Skip │ Middle │ Skip │ ▲ │ │
│ │ ▼ │ Conn │ Block │ Conn │ │ │ │
│ │ 32x32 ───┼────────►│ (4x4) │◄─────────┼─── 32x32 │ │
│ │ │ │ │ │ │ ▲ │ │
│ │ ▼ │ └─────────┘ │ │ │ │
│ │ 16x16 ───┼──────────────────────────────┼─── 16x16 │ │
│ │ │ │ │ ▲ │ │
│ │ ▼ │ │ │ │ │
│ │ 8x8 ───┼──────────────────────────────┼─── 8x8 │ │
│ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
│
▼
输出: 预测噪声 ε_θ (B, C, H, W)
输入输出详解
┌─────────────────────────────────────────────────────────────────┐
│ 输入 (Inputs) │
├─────────────────────────────────────────────────────────────────┤
│ 1. 带噪图像 x_t │
│ Shape: (batch_size, in_channels, height, width) │
│ 示例: (8, 3, 64, 64) - 8张 64x64 的 RGB 图像 │
│ │
│ 2. 时间步 t │
│ Shape: (batch_size,) │
│ 示例: tensor([100, 250, 500, ...]) - 每张图的噪声程度 │
│ │
│ 3. 条件信息 (可选) │
│ - 类别标签: (batch_size,) → 嵌入后 (batch_size, embed_dim) │
│ - 文本: (batch_size, seq_len, embed_dim) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 输出 (Output) │
├─────────────────────────────────────────────────────────────────┤
│ 预测的噪声 ε_θ(x_t, t) │
│ Shape: (batch_size, out_channels, height, width) │
│ 示例: (8, 3, 64, 64) - 与输入相同尺寸 │
│ │
│ 训练目标: MSE(ε_θ(x_t, t), ε) 即预测噪声与真实噪声的差距 │
└─────────────────────────────────────────────────────────────────┘
关键层及其作用
1. 时间步嵌入层 (Timestep Embedding)
# 将标量时间步转换为高维向量
t_emb = self.time_embed(t) # (B,) → (B, time_embed_dim)
原理:使用正弦位置编码(类似 Transformer)将时间步映射到高维空间:
作用:让模型知道当前处于去噪过程的哪个阶段,不同阶段需要不同的去噪策略。
2. ResNet 块 (ResBlock)
输入 x ──────────────────────────────┐
│ │ (残差连接)
▼ │
┌──────────────┐ │
│ GroupNorm │ │
│ SiLU 激活 │ │
│ Conv2d │ │
├──────────────┤ │
│ + t_emb │ ← 时间步嵌入注入 │
├──────────────┤ │
│ GroupNorm │ │
│ SiLU 激活 │ │
│ Dropout │ │
│ Conv2d │ │
└──────────────┘ │
│ │
└────────────── + ◄───────────────┘
│
▼
输出 x'
原理:
- 残差连接:缓解梯度消失,允许训练更深的网络
- GroupNorm:比 BatchNorm 更适合小 batch size
- SiLU (Swish):,平滑的非线性激活
- 时间步注入:将 t_emb 投影后加到特征图上,让每层都能感知时间步
3. 注意力层 (Attention)
┌─────────────────────────────────────────────────────────────┐
│ Self-Attention │
│ │
│ 输入特征 (B, C, H, W) → reshape → (B, H*W, C) │
│ │
│ Q = W_q(x) K = W_k(x) V = W_v(x) │
│ │
│ Attention(Q,K,V) = softmax(QK^T / √d_k) · V │
│ │
│ 输出 → reshape → (B, C, H, W) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Cross-Attention (条件注入) │
│ │
│ Q = W_q(x) ← 来自图像特征 │
│ K = W_k(context) ← 来自条件(如文本嵌入) │
│ V = W_v(context) ← 来自条件 │
│ │
│ 让图像特征"关注"文本中的相关部分 │
└─────────────────────────────────────────────────────────────┘
原理:
- Self-Attention:捕捉图像内部的长程依赖关系
- Cross-Attention:将条件信息(文本、类别)融入图像特征
- 通常只在低分辨率层使用(如 16x16, 8x8),减少计算量
4. 下采样块 (DownBlock)
class DownBlock:
def forward(self, x, t_emb):
# 1. 通过若干 ResNet 块
for resnet in self.resnets:
x = resnet(x, t_emb)
# 2. 可选的注意力层
if self.attentions:
for attn in self.attentions:
x = attn(x)
# 3. 下采样(步长为2的卷积或平均池化)
x = self.downsample(x) # (B,C,H,W) → (B,C',H/2,W/2)
return x
作用:提取多尺度特征,逐层降低分辨率、增加通道数。
5. 上采样块 (UpBlock)
class UpBlock:
def forward(self, x, skip_connection, t_emb):
# 1. 上采样
x = self.upsample(x) # (B,C,H,W) → (B,C,H*2,W*2)
# 2. 拼接跳跃连接
x = torch.cat([x, skip_connection], dim=1)
# 3. 通过 ResNet 块
for resnet in self.resnets:
x = resnet(x, t_emb)
return x
作用:逐步恢复分辨率,跳跃连接带回编码器的细节信息。
6. 跳跃连接 (Skip Connections)
Encoder Decoder
──────── ────────
64x64, 64ch ─────────────────────► concat → 64x64
│ ▲
32x32, 128ch ─────────────────────► concat → 32x32
│ ▲
16x16, 256ch ─────────────────────► concat → 16x16
│ ▲
└──────► Middle Block ────────────────┘
原理:
- 编码器丢失的空间细节通过跳跃连接传递给解码器
- 使用
concat(拼接)而非相加,保留更多信息 - 这是 U-Net 能生成清晰图像的关键设计
完整数据流
x_t (带噪图像) t (时间步) context (条件)
│ │ │
│ ┌──────┴──────┐ │
│ │ Sinusoidal │ │
│ │ Embedding │ │
│ └──────┬──────┘ │
│ │ │
│ t_emb (时间嵌入) │
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Down │──►│ Down │──►│ Middle │──►│ Up │──► │
│ │ Block 1 │ │ Block 2 │ │ Block │ │ Block 1 │ │
│ └────┬────┘ └────┬────┘ └─────────┘ └────▲────┘ │
│ │ │ │ │
│ │ └───────── skip ────────────┤ │
│ │ │ │
│ └──────────────── skip ───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
│
▼
预测噪声 ε_θ(x_t, t)
代码示例:简化的 UNet2DModel
class SimpleUNet(nn.Module):
def __init__(self, in_ch=3, out_ch=3, time_dim=256):
super().__init__()
# 时间步嵌入
self.time_mlp = nn.Sequential(
SinusoidalPosEmb(time_dim),
nn.Linear(time_dim, time_dim),
nn.GELU(),
nn.Linear(time_dim, time_dim),
)
# 编码器
self.down1 = DownBlock(in_ch, 64, time_dim)
self.down2 = DownBlock(64, 128, time_dim)
# 中间层
self.mid = MidBlock(128, time_dim)
# 解码器(通道数翻倍因为有 skip connection)
self.up1 = UpBlock(128 + 128, 64, time_dim)
self.up2 = UpBlock(64 + 64, out_ch, time_dim)
def forward(self, x, t):
# 时间嵌入
t_emb = self.time_mlp(t)
# 编码 (保存用于 skip connection)
d1 = self.down1(x, t_emb) # 64x64 → 32x32
d2 = self.down2(d1, t_emb) # 32x32 → 16x16
# 中间处理
mid = self.mid(d2, t_emb) # 16x16
# 解码 (使用 skip connection)
u1 = self.up1(mid, d2, t_emb) # 16x16 → 32x32
u2 = self.up2(u1, d1, t_emb) # 32x32 → 64x64
return u2 # 预测的噪声
为什么 U-Net 适合扩散模型?
- 输入输出同尺寸:扩散模型需要预测与输入相同尺寸的噪声
- 多尺度特征:下采样捕获全局语义,上采样恢复局部细节
- 跳跃连接:保留高频细节,生成清晰图像
- 灵活的条件注入:时间步、类别、文本都可以方便地融入网络
第三章:Stable Diffusion 深度解析
Stable Diffusion 是目前最流行的开源文生图模型,本章从架构、组件、术语等多个角度深入剖析。
核心术语表
| 术语 | 英文 | 定义 | 作用 |
|---|---|---|---|
| 潜在空间 | Latent Space | VAE 编码后的压缩表示空间 | 降低计算量,512x512→64x64 |
| 潜在扩散 | Latent Diffusion | 在潜在空间而非像素空间进行扩散 | SD 的核心创新 |
| 文本编码器 | Text Encoder | 将文本转为嵌入向量的模型 | 理解用户 prompt |
| 分词器 | Tokenizer | 将文本切分为 token 的工具 | 文本预处理 |
| CFG | Classifier-Free Guidance | 无分类器引导,增强 prompt 遵循度 | 控制生成质量 |
| 负面提示 | Negative Prompt | 描述不想生成的内容 | 避免不良特征 |
| 调度器 | Scheduler | 控制噪声添加/去除的策略 | 影响生成速度和质量 |
Stable Diffusion 整体架构
┌─────────────────────────────────────────────────────────────────────────┐
│ Stable Diffusion Pipeline │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Prompt │ │ Tokenizer │ │Text Encoder │ │
│ │ "a cat" │ ───► │ (CLIP) │ ───► │ (CLIP) │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ text_embeddings │
│ (1, 77, 768) │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────────────────────────┐ │
│ │Random Noise │ │ │ │
│ │ (Latents) │ ───► │ U-Net │ │
│ │(1,4,64,64) │ │ (预测噪声,条件:文本+时间步) │ │
│ └─────────────┘ └──────────────┬──────────────────┘ │
│ ▲ │ │
│ │ │ predicted_noise │
│ │ ▼ │
│ │ ┌─────────────────────────────┐ │
│ └──────────────│ Scheduler │ │
│ updated_latents │ (去噪一步,重复 N 次) │ │
│ └─────────────────────────────┘ │
│ │ │
│ │ final_latents │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ VAE Decoder │ │
│ │ (64x64 → 512x512 图像) │ │
│ └─────────────────────────────┘ │
│ │ │
│ ▼ │
│ 🖼️ 生成的图像 │
└─────────────────────────────────────────────────────────────────────────┘
组件详解
1. VAE(变分自编码器)
解决的问题:直接在 512x512 像素空间进行扩散计算量太大。
原理:将图像压缩到低维潜在空间,在那里进行扩散,最后再解码回像素空间。
┌─────────────────────────────────────────────────────────────────┐
│ VAE │
│ │
│ 输入图像 潜在表示 重建图像 │
│ (1,3,512,512) ──► (1,4,64,64) ──► (1,3,512,512) │
│ │
│ RGB 图像 Encoder Latent Decoder RGB 图像 │
│ 512x512 │ │ │ 512x512 │
│ │ 8x 压缩(空间) │ │
│ │ 1.33x 扩展(通道)│ │
└─────────────────────────────────────────────────────────────────┘
| 属性 | 输入 | 潜在表示 | 说明 |
|---|---|---|---|
| 形状 | (B, 3, 512, 512) | (B, 4, 64, 64) | 空间压缩 8 倍 |
| 数据量 | 786,432 | 16,384 | 压缩 48 倍 |
| 值域 | -1, 1 | 任意(需缩放) | 缩放因子 0.18215 |
代码示例:
# 编码:图像 → 潜在表示
with torch.no_grad():
latents = vae.encode(image).latent_dist.sample()
latents = latents * 0.18215 # 缩放因子
# 解码:潜在表示 → 图像
with torch.no_grad():
image = vae.decode(latents / 0.18215).sample
为什么需要缩放因子 0.18215?
- VAE 训练时潜在空间的方差不是标准化的
- 这个魔法数字使潜在空间的方差接近 1,与扩散过程的假设匹配
2. Tokenizer(分词器)
解决的问题:神经网络无法直接处理文本字符串。
原理:将文本切分为离散的 token(词或子词),映射为整数 ID。
输入: "A painting of a flooble"
│
▼
┌─────────────────────────────────────────┐
│ Tokenizer │
│ │
│ "A" → 320 │
│ "painting" → 3086 │
│ "of" → 539 │
│ "a" → 320 │
│ "floo" → 4062 ← 未知词被拆分 │
│ "ble" → 1059 │
│ │
│ 特殊 token: │
│ <|startoftext|> → 49406 │
│ <|endoftext|> → 49407 │
└─────────────────────────────────────────┘
│
▼
输出: [49406, 320, 3086, 539, 320, 4062, 1059, 49407]
| 属性 | 值 | 说明 |
|---|---|---|
| 词汇表大小 | ~49,408 | CLIP tokenizer |
| 最大长度 | 77 tokens | 超出截断,不足填充 |
| 未知词处理 | BPE 子词拆分 | 保证所有输入都能编码 |
3. Text Encoder(文本编码器)
解决的问题:token ID 只是离散符号,缺乏语义信息。
原理:使用预训练的 CLIP 文本 Transformer,将 token 序列转为富含语义的嵌入向量。
Token IDs Text Embeddings
(1, 77) (1, 77, 768/1024)
│ │
│ ┌──────────────────┐ │
└───►│ CLIP Text │──────┘
│ Transformer │
│ (12/24 layers) │
└──────────────────┘
每个 token 位置输出一个 768/1024 维向量
这些向量编码了词义 + 上下文关系
| 模型版本 | 嵌入维度 | Transformer 层数 |
|---|---|---|
| SD 1.x | 768 | 12 |
| SD 2.x | 1024 | 24 |
为什么用 CLIP?
- CLIP 在 4 亿图文对上训练,学会了图像-文本对齐
- 其文本编码器天然理解视觉相关的语义
- 迁移学习:直接复用预训练权重
4. U-Net(噪声预测网络)
解决的问题:给定带噪潜在表示和条件,预测其中的噪声。
Stable Diffusion U-Net 与普通 U-Net 的区别:
| 特性 | 普通 U-Net | SD U-Net |
|---|---|---|
| 输入空间 | 像素空间 | 潜在空间 |
| 条件输入 | 仅时间步 | 时间步 + 文本嵌入 |
| 注意力类型 | Self-Attention | Self + Cross-Attention |
| 输入通道 | 3 (RGB) | 4 (latent) |
输入输出:
# 输入
latents: (B, 4, 64, 64) # 带噪潜在表示
timestep: (B,) # 当前时间步
encoder_hidden_states: (B, 77, 768/1024) # 文本嵌入
# 输出
noise_pred: (B, 4, 64, 64) # 预测的噪声(与输入同形状)
Cross-Attention 的作用:
┌────────────────────────────────────────────────────────────────┐
│ Cross-Attention Layer │
│ │
│ 图像特征 (Query) 文本嵌入 (Key, Value) │
│ (B, H*W, C) (B, 77, 768) │
│ │ │ │
│ ▼ ▼ │
│ Q = Wq(x) K = Wk(text), V = Wv(text) │
│ │ │ │
│ └──────────► Attention ◄───────┘ │
│ │ │
│ ▼ │
│ 图像特征"关注"相关文本 │
│ 例如: 生成"猫"区域时关注"cat"词嵌入 │
└────────────────────────────────────────────────────────────────┘
5. Scheduler(调度器)
解决的问题:控制扩散过程中噪声的添加和去除策略。
常用调度器对比:
| 调度器 | 特点 | 推荐步数 | 速度 |
|---|---|---|---|
| PNDM | SD 默认,稳定 | 50 | 中 |
| DDIM | 确定性采样,可跳步 | 20-50 | 快 |
| LMS | 线性多步法 | 20-50 | 快 |
| Euler | 简单高效 | 20-30 | 快 |
| DPM++ | 最新高质量 | 15-25 | 很快 |
噪声调度曲线:
α̅ (信号保留率)
1.0 ┤████████████████████░░░░░░░░░░░░░░░░░░░░
│ ████░░░░░░░░░░░░░░░░
│ ████░░░░░░░░░░░░
│ ████░░░░░░░░
│ ████░░░░
0.0 ┤ ████
└────────────────────────────────────────►
t=0 (低噪声) t=1000 (高噪声)
█ = 原始信号比例
░ = 噪声比例
Classifier-Free Guidance (CFG)
解决的问题:如何让模型更好地遵循 prompt?
原理:同时预测有条件和无条件的噪声,然后放大两者的差异。
# 1. 准备两种输入
prompt_embeds = encode("a beautiful sunset") # 有条件
negative_embeds = encode("") # 无条件(或负面提示)
# 2. 模型预测两次
noise_pred_text = unet(latents, t, prompt_embeds) # 有条件预测
noise_pred_uncond = unet(latents, t, negative_embeds) # 无条件预测
# 3. CFG 公式
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# └── 基础噪声 ──┘ └─ 放大条件的影响 ─┘
guidance_scale 的影响:
| scale 值 | 效果 |
|---|---|
| 1.0 | 无引导,随机性高 |
| 7.0-8.0 | 推荐值,平衡质量和多样性 |
| 12.0+ | 过度饱和,伪影增多 |
| 20.0+ | 图像失真严重 |
guidance_scale: 1.0 7.5 15.0
│ │ │
▼ ▼ ▼
[模糊] [清晰自然] [过饱和]
多样性高 质量最佳 遵循度高但失真
完整采样流程
def sample(prompt, num_steps=50, guidance_scale=7.5):
# 1. 编码文本
text_emb = text_encoder(tokenizer(prompt))
uncond_emb = text_encoder(tokenizer(""))
text_embeddings = torch.cat([uncond_emb, text_emb])
# 2. 初始化随机噪声
latents = torch.randn((1, 4, 64, 64))
latents = latents * scheduler.init_noise_sigma
# 3. 设置时间步
scheduler.set_timesteps(num_steps)
# 4. 去噪循环
for t in scheduler.timesteps:
# 扩展 latents 用于 CFG(无条件 + 有条件)
latent_input = torch.cat([latents] * 2)
# UNet 预测噪声
noise_pred = unet(latent_input, t, text_embeddings)
# CFG
noise_uncond, noise_text = noise_pred.chunk(2)
noise_pred = noise_uncond + guidance_scale * (noise_text - noise_uncond)
# 调度器更新 latents
latents = scheduler.step(noise_pred, t, latents).prev_sample
# 5. VAE 解码
image = vae.decode(latents / 0.18215)
return image
扩展管线
Img2Img(图生图)
解决的问题:基于现有图像生成新图像。
原图 ──► VAE Encode ──► 添加噪声 ──► 部分去噪 ──► VAE Decode ──► 新图
│
strength 参数
控制添加多少噪声
(0=不变, 1=完全重绘)
strength 参数的影响:
strength=0.3: 保留大部分原图,微调风格strength=0.6: 保留结构,改变内容strength=0.9: 几乎完全重绘,只保留大致构图
Inpainting(图像修复)
解决的问题:只修改图像的指定区域。
┌────────────────────────────────────────────────────────┐
│ Inpainting 流程 │
│ │
│ 原图 掩码 (白=修改区) 结果 │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ 🐕 │ + │ ⬜⬛ │ = │ 🤖 │ │
│ │ │ │ ⬛⬛ │ │ │ │
│ └─────┘ └─────┘ └─────┘ │
│ │
│ Prompt: "A small robot sitting on a park bench" │
└────────────────────────────────────────────────────────┘
两种实现方式:
- Legacy 方法:每步将非掩码区域替换回原图潜在表示
- 专用模型:UNet 额外接收掩码和原图作为输入(效果更好)
Depth2Img(深度引导)
解决的问题:保持原图的 3D 结构,完全改变纹理和颜色。
原图 ──► 深度估计模型 ──► 深度图 ──┐
├──► 微调的 UNet ──► 新图
Prompt ──► 文本嵌入 ──┘
深度图作为额外条件,约束生成图像的空间结构
与 Img2Img 的对比:
| 特性 | Img2Img | Depth2Img |
|---|---|---|
| 保留内容 | 颜色+结构 | 仅结构 |
| 创意自由度 | 中 | 高 |
| 适用场景 | 风格迁移 | 完全重新上色/材质 |
关键参数速查表
| 参数 | 范围 | 推荐值 | 影响 |
|---|---|---|---|
num_inference_steps | 10-100 | 30-50 | 质量 vs 速度 |
guidance_scale | 1-20 | 7-8.5 | prompt 遵循度 |
width/height | 8的倍数 | 512/768 | 图像尺寸 |
strength (img2img) | 0-1 | 0.5-0.8 | 保留原图程度 |
seed | 任意整数 | - | 可复现性 |
本章小结
Stable Diffusion = VAE (压缩) + U-Net (去噪) + CLIP (理解文本) + Scheduler (控制过程)
核心创新:
1. 潜在扩散 → 计算效率提升 ~50x
2. Cross-Attention → 灵活的文本控制
3. CFG → 无需额外分类器的高质量生成
生态扩展:
├── Img2Img → 图像编辑
├── Inpainting → 区域修复
├── Depth2Img → 结构保持重绘
├── ControlNet → 精细姿态/边缘控制
└── LoRA → 高效微调