Know Your Wisdom

Stable Diffusion 学习笔记

2026-02-07

第一章:从零训练扩散模型

本章基于 Hugging Face Diffusers 教程,学习如何从零开始训练一个简单的扩散模型(以蝴蝶图片为例)。

核心训练流程

扩散模型的训练可以概括为以下五个步骤:

  1. 从训练数据中加载图片
  2. 向图片添加不同程度的噪声
  3. 将带噪图片输入模型
  4. 评估模型的去噪效果
  5. 根据损失更新模型权重,重复以上步骤

数据预处理

使用 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 论文的核心思想是前向扩散过程——逐步向图像添加高斯噪声:

q(xtxt1)=N(xt;1βtxt1,βtI)q(x_t|x_{t-1}) = \mathcal{N}(x_t; \sqrt{1-\beta_t}x_{t-1}, \beta_t\mathbf{I})

其中:

  • xtx_t 是第 t 步的带噪图像
  • βt\beta_t 是噪声调度参数,控制每一步添加多少噪声
  • 1βt\sqrt{1-\beta_t} 是对原图的缩放系数

直接计算任意时刻的噪声图像

q(xtx0)=N(xt;αˉtx0,(1αˉt)I)q(x_t|x_0) = \mathcal{N}(x_t; \sqrt{\bar{\alpha}_t}x_0, (1-\bar{\alpha}_t)\mathbf{I})

其中 αˉt=i=1tαi\bar{\alpha}_t = \prod_{i=1}^{t}\alpha_iαi=1βi\alpha_i = 1 - \beta_i

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(图像|条件)
推理时:指定条件 → 控制生成内容

三种传入条件信息的方式

  1. 作为额外输入通道
    • 将条件扩展为与输入图像相同的尺寸,拼接为额外通道
    • 适用于:分割掩码、深度图、模糊图像(超分辨率)、类别标签
  2. 投影到内部层输出
    • 将条件嵌入投影到与 UNet 内部层输出相同的维度,然后相加
    • 时间步条件就是这样处理的(每个 ResNet 块的输出都加上时间步嵌入)
    • 适用于:CLIP 图像嵌入等向量形式的条件
  3. 交叉注意力层(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)将时间步映射到高维空间:

PE(t,2i)=sin(t/100002i/d)\text{PE}(t, 2i) = \sin(t / 10000^{2i/d})PE(t,2i+1)=cos(t/100002i/d)\text{PE}(t, 2i+1) = \cos(t / 10000^{2i/d})

作用:让模型知道当前处于去噪过程的哪个阶段,不同阶段需要不同的去噪策略。

2. ResNet 块 (ResBlock)

输入 x ──────────────────────────────┐
   │                                 │ (残差连接)
   ▼                                 │
┌──────────────┐                     │
│ GroupNorm    │                     │
│ SiLU 激活    │                     │
│ Conv2d       │                     │
├──────────────┤                     │
│ + t_emb      │ ← 时间步嵌入注入     │
├──────────────┤                     │
│ GroupNorm    │                     │
│ SiLU 激活    │                     │
│ Dropout      │                     │
│ Conv2d       │                     │
└──────────────┘                     │
   │                                 │
   └────────────── + ◄───────────────┘
                   │
                   ▼
                输出 x'

原理

  • 残差连接:缓解梯度消失,允许训练更深的网络
  • GroupNorm:比 BatchNorm 更适合小 batch size
  • SiLU (Swish)SiLU(x)=xσ(x)\text{SiLU}(x) = x \cdot \sigma(x),平滑的非线性激活
  • 时间步注入:将 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 适合扩散模型?

  1. 输入输出同尺寸:扩散模型需要预测与输入相同尺寸的噪声
  2. 多尺度特征:下采样捕获全局语义,上采样恢复局部细节
  3. 跳跃连接:保留高频细节,生成清晰图像
  4. 灵活的条件注入:时间步、类别、文本都可以方便地融入网络

第三章:Stable Diffusion 深度解析

Stable Diffusion 是目前最流行的开源文生图模型,本章从架构、组件、术语等多个角度深入剖析。

核心术语表

术语英文定义作用
潜在空间Latent SpaceVAE 编码后的压缩表示空间降低计算量,512x512→64x64
潜在扩散Latent Diffusion在潜在空间而非像素空间进行扩散SD 的核心创新
文本编码器Text Encoder将文本转为嵌入向量的模型理解用户 prompt
分词器Tokenizer将文本切分为 token 的工具文本预处理
CFGClassifier-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,43216,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,408CLIP 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.x76812
SD 2.x102424

为什么用 CLIP?

  • CLIP 在 4 亿图文对上训练,学会了图像-文本对齐
  • 其文本编码器天然理解视觉相关的语义
  • 迁移学习:直接复用预训练权重

4. U-Net(噪声预测网络)

解决的问题:给定带噪潜在表示和条件,预测其中的噪声。

Stable Diffusion U-Net 与普通 U-Net 的区别

特性普通 U-NetSD U-Net
输入空间像素空间潜在空间
条件输入仅时间步时间步 + 文本嵌入
注意力类型Self-AttentionSelf + 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(调度器)

解决的问题:控制扩散过程中噪声的添加和去除策略。

常用调度器对比

调度器特点推荐步数速度
PNDMSD 默认,稳定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"       │
└────────────────────────────────────────────────────────┘

两种实现方式

  1. Legacy 方法:每步将非掩码区域替换回原图潜在表示
  2. 专用模型:UNet 额外接收掩码和原图作为输入(效果更好)

Depth2Img(深度引导)

解决的问题:保持原图的 3D 结构,完全改变纹理和颜色。

原图 ──► 深度估计模型 ──► 深度图 ──┐
                                   ├──► 微调的 UNet ──► 新图
              Prompt ──► 文本嵌入 ──┘

深度图作为额外条件,约束生成图像的空间结构

与 Img2Img 的对比

特性Img2ImgDepth2Img
保留内容颜色+结构仅结构
创意自由度
适用场景风格迁移完全重新上色/材质

关键参数速查表

参数范围推荐值影响
num_inference_steps10-10030-50质量 vs 速度
guidance_scale1-207-8.5prompt 遵循度
width/height8的倍数512/768图像尺寸
strength (img2img)0-10.5-0.8保留原图程度
seed任意整数-可复现性

本章小结

Stable Diffusion = VAE (压缩) + U-Net (去噪) + CLIP (理解文本) + Scheduler (控制过程)

核心创新:
1. 潜在扩散 → 计算效率提升 ~50x
2. Cross-Attention → 灵活的文本控制
3. CFG → 无需额外分类器的高质量生成

生态扩展:
├── Img2Img     → 图像编辑
├── Inpainting  → 区域修复
├── Depth2Img   → 结构保持重绘
├── ControlNet  → 精细姿态/边缘控制
└── LoRA        → 高效微调