7  条件生成:CFG 到底是什么

本章地图

你在这里:第 5、6 章训练出的模型只会生成”某种像数据的东西”,你没法让它只画月亮、不画另一个月亮。本章解决”怎么让它听话”。 本章解决:条件怎么进入模型;CFG 那个公式从哪来;引导强度 w 为什么有三段行为;标签丢弃为什么是 CFG 的前提而不是正则化;分类器引导为什么被取代。 前置:第 5 章(前向过程、\varepsilon-预测)、第 6 章(DDPM/DDIM 采样器、\eta)。 读完能做:正确实现 CFG 并选对 w;诊断”引导过强”造成的塌缩;解释 negative prompt 的数学来源。

7.1 本章的代码与数据

import torch, sys
sys.path.insert(0, "notebooks")
from tiny_diffusion import Config
from conditional_diffusion import (make_labeled_moons, train_cond,
                                   ddpm_cond, train_judge, metrics)

x, y = make_labeled_moons(4000, noise=0.08, seed=0)   # y ∈ {0,1}
model, sch, hist = train_cond(x, y, Config(max_steps=5000), drop=0.1)

samples = ddpm_cond(model, sch, torch.zeros(1000, dtype=torch.long), w=2.0)

复现本章全部数字:

cd notebooks
PY=/home/mpp/micromamba/envs/dlspace/bin/python
$PY run_ch7_case.py        # 五组实验,约 12 分钟

本章和第 5、6 章共用同一个双月数据集、同一套线性调度、同一个 \varepsilon-预测目标。 唯一的改动是数据多了一个标签、模型多接收一个类别输入。只有其他一切都相同, “引导”的效果才是一个干净的对照。

文件 内容 生成脚本
data/ch7_case.json CFG 扫描、负引导、无丢弃对照、分类器引导、分类器分段准确率 run_ch7_case.py

三个必须记住的数(后面每一节都会用到):

  • 数据标准差:[0.5728,\ 0.3389](两个维度)
  • 模型:50,434 参数,训练 5000 步,最终 loss 0.1242
  • 裁判分类器(只在干净数据上训练,用来给生成样本打分):准确率 0.9978
注记为什么需要”裁判”

本章要说”引导让样本更像某一类”。“像”必须有一个独立的判据, 否则就变成”我觉得像”。

做法是:在干净的真实数据上训练一个分类器,它的留出准确率是 0.9978 ——它几乎不会看错真实数据。然后用它给生成样本打标签, 统计”生成的外月里,有多少被裁判认成外月”。

这个数不能证明生成质量(裁判只学过真实数据,可能对生成样本的瑕疵不敏感), 但它能干净地回答一个问题:样本落在哪个类的区域里。


7.2 一个不会听话的模型

先看无条件模型能做什么。

用第 5 章的模型采样 1000 个点,裁判给出的类别准确率是 0.525—— 接近抛硬币。因为模型根本不知道”类别”这回事,它只是把两个月亮一起学了下来。

这不是模型不好。 它完美地完成了被要求的事:拟合 p(x)。 问题是我们要的是 p(x\mid c)。

条件生成的全部困难,可以压成一句话:

怎么把”我要外月”这个信息,塞进一个只学过”月亮”的模型里?

历史上主流做法有三条:

做法 条件怎么进去 要不要额外模型 代表
拼接/嵌入 作为模型输入的一部分 不要 本章的基线
分类器引导 借分类器的梯度推一把 要一个噪声分类器 Dhariwal & Nichol 2021
CFG 同时算”有条件”和”无条件”,取差 不要 Ho & Salimans 2022

本章先讲第一条(它是另外两条的基础),再把后两条推到同一个公式下比较。


7.3 条件怎么进模型:一个嵌入就够了

第 5 章的模型是 \varepsilon_\theta(x_t,t)。条件版本只多一个参数:

\varepsilon_\theta(x_t,t,c)

最省事的实现,是把类别嵌入加到时间嵌入上:

h = torch.cat([x, self.temb(t) + self.cemb(c)], dim=-1)

其中 cemb 是一个 nn.Embedding(n_class + 1, hidden)。

为什么是相加而不是拼接? 因为时间嵌入和类别嵌入都是”给网络的调制信号”, 相加等价于”在同一个隐空间里叠加两个条件”。拼接也可以,但参数量更大, 而且在本章这个 5 万参数的模型上没有可见收益。

注意 +1。 多出来的那一行是空类(null class)—— 训练时以一定概率把标签换成它。这一行是 CFG 的全部秘密,下一节展开。

警告空类不能用 -1

nn.Embedding 不接受负数下标。把空类写成 -1 会在第一个 batch 就抛 IndexError;写成 n_class 才是对的。

更隐蔽的错误是:训练时用 n_class,采样时用 n_class - 1 (把最后一个真实类当成了空类)。这时程序不报错, 但 CFG 的”无条件”分支其实是在生成最后一类, 表现为”引导越强,越像那个类”。第 7 章末尾的坑列表里它排第一。


7.3.1 另外两种注入方式

相加是最省事的一种,但不是唯一的一种。知道另外两种,才能看懂论文里的模型结构。

① AdaGN / FiLM:用条件去缩放和平移每一层

不让条件只出现在输入层,而是调制网络中间每一层的归一化:

h\leftarrow \gamma(c)\odot\frac{h-\mu}{\sigma}+\beta(c)

其中 \gamma,\beta 都是由 c 算出的小网络。

代价:多两个投影头;收益:条件信号更强,不容易在深层被稀释。 Stable Diffusion 的 UNet 里,时间步和类别就是用这种方式注入每个 ResNet 块的。

② 交叉注意力:把条件变成一串 token

条件不再是一个向量,而是一个序列(比如文本的 77 个 token)。 每一层用 Q 来自图像特征、K,V 来自文本,做一次注意力:

\text{Attn}(Q,K,V)=\text{softmax}\left(\frac{QK^\top}{\sqrt d}\right)V

这就是第 1 章的多头注意力,换了一组 K,V。 代价最大,表达力也最强。第 8 章整章讲它。

③ 三种方式的取舍

方式 参数量 表达力 典型用法
相加/拼接 最小 弱 类别、标量条件
AdaGN / FiLM 中 中 类别 + 时间步(SD 的 UNet)
交叉注意力 最大 强 文本、图像参考

本章用第一种,是为了隔离变量。 如果条件注入本身很复杂, “引导强度”的效果就会和”注入方式”混在一起,说不清是谁的贡献。

注记一个容易忽略的细节

三种方式都要求”能把条件换成空”。

相加方式里,空类是一个可学习的嵌入; FiLM 方式里,空类是 \gamma,\beta 网络的一个输入; 交叉注意力里,空条件是空字符串编码出来的那串 token—— 不是全零向量。

第 8 章会看到:空字符串经过文本编码器之后并不是零, 它有自己的一组激活。这件事会直接影响 CFG 的效果, 因为”无条件”分支的定义变了。


7.3.2 三种注入方式的参数量账

“表达力更强”是有价格的。 用 Stable Diffusion 的 d_{\text{model}}=320 算一笔账。

AdaGN:每个 ResNet 块需要两组调制参数(缩放和平移),

2\times d_{\text{model}}=640\ \text{个参数/块}

交叉注意力:Q,K,V 和输出投影各一个 d\times d 矩阵,

4\times d_{\text{model}}^2=409{,}600\ \text{个参数/层}

相差 640 倍。

方式 每层参数 SD UNet 里的用途
相加 d 本章的类别条件
AdaGN 2d 时间步 + 类别
交叉注意力 4d^2 文本

这解释了 SD 的设计选择:时间步和类别用 AdaGN(便宜), 文本用交叉注意力(贵,但文本需要 token 级的对齐能力)。

注记为什么文本不能用 AdaGN

文本不是”一个向量”,是一串向量。用 AdaGN 意味着先把 77 个 token 池化成一个向量——“一只猫坐在一只狗旁边”里的”猫”和”狗”会被平均掉。

交叉注意力保留 token 级的信息:图像的每个位置可以去”查”它需要的那个词。 这就是第 1 章那个注意力矩阵在做的事。

代价是 640 倍的参数。 这个交换在文本上是值得的,在类别上不值得。

AdaGN 的完整形式(时间步和类别相加后,投影成两组调制参数):

h\leftarrow\left(1+\gamma(e)\right)\odot\text{GroupNorm}(h)+\beta(e),\qquad e=\text{MLP}(t_{\text{emb}}+c_{\text{emb}})

注意那个 1+:它让 \gamma=0 时退化成”不调制”, 初始化时整层接近恒等。没有它,训练初期的随机 \gamma 会把特征缩放得很厉害。

这是”调制类”结构的一个通用技巧,和 ResNet 的残差、LayerScale 的 \gamma 是同一个思路:让新加的部件初始时等于没有。

7.4 CFG 的推导:两个 score 的差

这一节把 CFG 的公式推出来。只用贝叶斯公式和 score 的定义,不需要新工具。

7.4.1 从贝叶斯到 score

条件分布和无条件分布的关系是

p(x\mid c)=\frac{p(x)\,p(c\mid x)}{p(c)}

两边取对数再对 x 求梯度。p(c) 与 x 无关,求导消失:

\nabla_x\log p(x\mid c)=\nabla_x\log p(x)+\nabla_x\log p(c\mid x)

这就是条件生成的核心恒等式。

它说:“条件分布的 score” = “无条件 score” + “分类器对 x 的梯度”。

三项都认识:

  • \nabla\log p(x) 就是第 6 章的 score,模型已经在学它(\varepsilon=-\sqrt{1-\bar\alpha}\nabla\log p)
  • \nabla\log p(c\mid x) 是一个分类器的梯度
  • 左边是”我们想要的那个东西”

所以条件采样 = 无条件采样 + 一个分类器的推力。 这就是分类器引导。

7.4.2 分类器引导的公式

把 \varepsilon=-\sqrt{1-\bar\alpha_t}\nabla\log p_t 代进去:

\hat\varepsilon=\varepsilon_\theta(x_t,t)-s\sqrt{1-\bar\alpha_t}\;\nabla_{x_t}\log p_\phi(c\mid x_t)

其中 s 是引导强度。符号是负的:score 加一项,等价于 \varepsilon 减一项。

这个式子的工程代价很直接:你需要一个能在每个噪声水平上工作的分类器 p_\phi(c\mid x_t,t)。在干净图片上训的分类器,到了高噪声处梯度没有意义。

7.4.3 CFG:把分类器换成一个差

CFG 的观察是:那个分类器梯度,可以用模型自己近似。

由贝叶斯恒等式,

\nabla\log p(c\mid x)=\nabla\log p(x\mid c)-\nabla\log p(x)

右边两项都是 score,而 score 和 \varepsilon 只差一个常数。

代回分类器引导的式子,整理后得到

\boxed{\;\hat\varepsilon=\varepsilon_\theta(x_t,t,\varnothing)+w\left(\varepsilon_\theta(x_t,t,c)-\varepsilon_\theta(x_t,t,\varnothing)\right)\;}

其中 \varnothing 是空类,w 是引导强度。

这就是 CFG。 它一次都不需要分类器,只需要模型能在”有标签”和”空标签” 两种输入下各算一次。

7.4.4 为什么是”缩放差”,不是”加强条件”

一个自然的想法是:既然要”更听话”,为什么不直接把 \varepsilon_c 乘大一点?

\hat\varepsilon=w\,\varepsilon_c\quad(\text{错误做法})

因为乘大的是整个向量,不只是”条件”那部分。

把 \varepsilon_c 拆成”基准 + 条件方向”:

\varepsilon_c=\varepsilon_u+\underbrace{(\varepsilon_c-\varepsilon_u)}_{\text{条件方向}}

乘 w 的应该是后一项,不是整个 \varepsilon_c。

  • 乘整个 \varepsilon_c:基准也被放大,画面整体变亮/变大—— 这是尺度变化,不是内容变化
  • 乘差:只有条件方向被放大,基准不变——这才是”更听话”

CFG 的公式正是后者。 而且它在 w=1 时精确退化成 \varepsilon_c—— 这是”乘整个向量”做不到的(除非额外归一化)。

注记一个几何图像

把 \varepsilon_u 看作原点,\varepsilon_c 是原点外的一个点。

  • “加强条件”:把 \varepsilon_c 从原点往外拉 → 方向不变,长度变
  • CFG:沿着 \varepsilon_c-\varepsilon_u 的方向走更远 → 等价于把 \varepsilon_c 从 \varepsilon_u 出发拉长

两者在”方向”上是一样的,但原点不同。 CFG 的原点是 \varepsilon_u,所以它拉长的是”条件带来的那一段”; “加强条件”的原点是零,拉长的是”整个预测”。

当 \varepsilon_u 很小(比如接近零)时,两者几乎一样—— 这就是为什么有些实现看起来也能工作。但一般情况下它们不同。

7.4.5 三个端点

w=0:\quad \hat\varepsilon=\varepsilon_\theta(x,\varnothing)\quad\text{(无条件)}

w=1:\quad \hat\varepsilon=\varepsilon_\theta(x,c)\quad\text{(普通条件采样)}

w>1:\quad \text{把"条件 - 无条件"这个方向放大}

注意 w=1 那一行。 它说明 CFG 不是”额外的技巧”—— 它是普通条件采样的一个推广,w=1 就是没有引导。

重要这个公式在说什么(人话版)

“外月”和”所有月亮”之间的差别,就是”外月”这个条件带来的信息。

CFG 做的事是:先算出这个差别,再把它乘以 w 加回去。

  • w=1:原样加回去 → 就是普通条件采样
  • w=2:加两倍 → 样本被推得更”像外月”
  • w=0:一点都不加 → 退回无条件

所以 w 不是一个”随机性旋钮”,是”你有多想要这个条件”的旋钮。 代价在下一节:你把条件推得越狠,样本越不像真实数据。


图 7.1  CFG 的数据流。同一个 x_t 走两次 UNet:一次带条件、一次带空条件,两个输出的差乘以 w 再加回去,最后交给调度器。两次前向是 CFG 的全部成本。

7.4.6 先算一遍:一维两点数据

在跑实验之前,用一个能用手算的例子把 CFG 走一遍。

设置:数据只有两个点 -1 和 +1,各占一半。 条件 c=0 表示”来自 -1“,c=1 表示”来自 +1“。 在某个噪声水平上,条件分布是一维高斯:

p(x\mid c=0)=\mathcal{N}(-1,\ 1),\qquad p(x\mid c=1)=\mathcal{N}(+1,\ 1)

无条件分布是两者的等权混合。

取 x=0.2。 先算混合权重:

a=e^{-(1.2)^2/2}=0.4868,\qquad b=e^{-(-0.8)^2/2}=0.7261

w_0=\frac{a}{a+b}=0.4013,\qquad w_1=0.5987

两个 score:

\nabla\log p(x\mid c=0)=-(x+1)=-1.2000

\nabla\log p(x)=w_0\cdot(-1.2)+w_1\cdot(0.8)=-0.0026

换成 \varepsilon=-\sigma\,\nabla\log p(取 \sigma=1):

\varepsilon_c=1.2000,\qquad \varepsilon_u=0.0026

现在套 CFG:

w \hat\varepsilon 等效后验均值 x+\sigma^2\hat s
0 0.0026 0.2000
1 1.2000 −1.0000
2 2.3974 −2.1974
5 5.9895 −5.7895
10 11.9764 −11.9764

看第三列。 w=1 时等效均值是 -1.0000——正好是数据的真实位置。 w=2 时变成 -2.1974,已经越过数据点,跑到它外面去了。 w=10 时是 -11.98。

重要这张表比任何实验都直白

CFG 不是”让样本更靠近条件”,是”让样本比条件更远”。

w=2 时,模型在这个点上的行为等价于”数据在 -2.2 附近”—— 而真实数据在 -1。

在二维实验里,这就是 w 大时样本向外漂移的原因。 准确率仍然是 1.000,因为 -2.2 和 -12 都还在”外月那一侧”。

所以”准确率”这个指标,从 w=1 开始就不再衡量”样本是不是合理”了。 它只衡量”样本落在哪个类的半空间”。

这个例子还说明了另一件事:为什么 w 的效果依赖于 \sigma。

注意等效均值的公式 x+\sigma^2\hat s。 在 t 大的时候 \sigma 很大,同样的 \hat s 会把均值推得更远。 但 \hat s 本身也被 \sigma 缩放了(\varepsilon=-\sigma\nabla\log p), 两者部分抵消。

净效果是:在低噪声处(\sigma 小),引导主要改变分布的宽度; 在高噪声处,它更容易改变位置。 这解释了为什么过引导的样本往往”结构还在、位置不对”。


7.5 实测:w 的三段行为

用 50 步 DDPM、每类 1000 个样本,扫 w。裁判准确率、样本标准差、 越界率(|x|>1.5 的比例)三列:

w 准确率(外/内) 标准差(外/内) 越界率(外/内)
0 0.525 / 0.475 0.437 / 0.437 0 / 0
0.5 0.846 / 0.818 0.354 / 0.368 0 / 0
1 0.965 / 0.962 0.310 / 0.335 0 / 0
1.5 0.991 / 0.992 0.291 / 0.315 0 / 0
2 0.998 / 0.998 0.281 / 0.297 0 / 0
3 1.000 / 1.000 0.271 / 0.255 0 / 0
5 1.000 / 1.000 0.261 / 0.182 0 / 0
10 1.000 / 1.000 0.257 / 0.110 0.002 / 0
20 1.000 / 1.000 0.897 / 0.285 0.397 / 0.093
40 1.000 / 1.000 1.703 / 0.293 0.697 / 0.895
图 7.2  引导强度扫描。A:两个类的准确率都在 w=3 之后饱和;B:标准差(对数轴)先缓慢下降,w\ge20 后外月急剧上升;C:越界率在 w=10 之后抬头。数据来自 data/ch7_case.json。

把它画成曲线,会看到清楚的三段。

7.5.1 第一段:0\le w<1,欠引导

w=0 时准确率 0.525——模型完全没在听条件。 w=0.5 升到 0.846,w=1 到 0.965。

这一段是”条件开始起作用”的过程。 如果你直接用 w=0.5, 会得到”有点像外月、又有点像内月”的样本——这不是模型不好,是你只给了一半的力。

7.5.2 第二段:1\le w\le 10,正常引导

准确率在 w=2 就到 0.998,w=3 之后是 1.000。

但标准差在单调下降:外月 0.310 → 0.257,内月 0.335 → 0.110。

内月掉得比外月快得多。 为什么?因为内月本身更”窄” (数据标准差 0.339 对 0.573),同样的推力下,窄的分布先被压扁。

这就是引导的代价:它用多样性换准确率。

提示怎么读”标准差下降”

不要把”标准差变小”直接读成”质量变差”。

  • 在 w\le 2 时,标准差从 0.310 降到 0.281,准确率从 0.965 升到 0.998—— 这是去掉错误样本带来的自然收缩,是收益。
  • 在 w\ge 5 时,内月从 0.182 掉到 0.110,而准确率早就是 1.000—— 再压下去没有收益,只有损失。

判据不是”标准差有没有降”,是”降的时候准确率还在不在涨”。

7.5.3 第三段:w\ge 20,过引导

准确率还是 1.000(裁判永远认对),但样本已经不在数据分布上了:

  • w=20:外月 39.7%、内月 9.3% 的样本跑出 |x|>1.5
  • w=40:69.7% / 89.5%

准确率这个指标在这里完全失效——裁判只回答”它在哪一类”, 不回答”它是不是一个合理的点”。一个飞到天边去的点,仍然可以被认成外月。

判断过引导必须看第二个量:越界率,或者样本到真实数据的距离。

警告准确率满分的时候,往往是最危险的时候

w=20 的准确率是 1.000,比 w=2 还高。

如果你只用分类准确率评估引导,会得出”w 越大越好”的结论, 然后把 w 调到 40,得到一堆飞到 |x|\approx 2 的废点。

这是本章最容易犯的评估错误,也是”只看一个指标”的通病。 第 6 章在采样器上犯过同一次错(只看步数不看 NFE),这里只是换了个场景。

7.5.4 安全区间会随步数收窄

上面那张表是 50 步 DDPM 的结果。换步数,三段的分界会移动。

用可跳步的精确 DDPM 后验(步长变大,但每一步仍是高斯后验), 扫 10 / 25 / 50 / 100 步:

步数 w=1 w=2 w=5 w=20
10 0.938 1.000 0.612 0.593(越界 53.8%)
25 0.958 1.000 0.885 0.937(越界 58.7%)
50 0.955 1.000 1.000 0.887(越界 53.8%)
100 0.973 1.000 1.000 0.957(越界 41.2%)

看 w=5 那一列:10 步时准确率只有 0.612,50 步时是 1.000。

这是本章最实用的一条结论:步数越少,能用的 w 越窄。

为什么?引导放大的那个差 (\varepsilon_c-\varepsilon_u) 是在一步之内加进去的。 步长越大,这个放大后的方向被推得越远, 样本越容易一步跨出模型”认识”的区域——和第 6 章的”步长 vs 曲率”是同一个问题。

警告这条结论的实践含义

少步蒸馏模型的引导强度不能照搬多步模型的。

如果你把一个 50 步、w=5 的配置直接搬到 10 步, 得到的不是”稍微差一点”,是准确率掉到 0.6、一半样本越界。

正确做法:步数减半时,先把 w 降一档试。 本章的数据给出一个粗糙的起点:10 步时 w 不要超过 2。


7.5.5 CFG 采样的是哪个分布

三段行为可以从一个式子直接看出来。

把 \varepsilon=-\sqrt{1-\bar\alpha_t}\,\nabla\log p_t 代进 CFG 的公式:

\hat\varepsilon=w\,\varepsilon_c+(1-w)\,\varepsilon_u

两边同乘 -1/\sqrt{1-\bar\alpha_t},换成 score:

\nabla\log p_w(x\mid c)=w\,\nabla\log p(x\mid c)+(1-w)\,\nabla\log p(x)

一个函数的梯度等于若干梯度的线性组合,说明这个函数本身是那些函数的乘积(取对数后相加)。

去掉与 x 无关的归一化常数,得到

\boxed{\;p_w(x\mid c)\;\propto\;p(x\mid c)^{w}\left(p(x)\right)^{1-w}\;}

这就是 CFG 实际采样的分布。

重要这个式子解释了本章的所有现象

w=0:p_0=p(x) —— 无条件。 w=1:p_1=p(x\mid c) —— 普通条件采样。 w>1:p(x\mid c) 被乘了大于 1 的幂,p(x) 被乘了负幂。 这是在”锐化”后验:高密度处更突出,低密度处被压得更低。 w<0:反过来,把后验”抹平”,质量被推向低密度区。

所以 w 不是一个”采样技巧”的参数,它改变了你要采的那个分布本身。

这也解释了为什么 w 不能无限大:p(x\mid c)^w 在 w\to\infty 时 收敛到后验的众数——一个点。而模型的众数估计有误差, 所以实际得到的是”模型认为的众数”,不是真实数据的众数。 w=40 时有效维度 1.009,就是”正在收敛到一个点”的过程。

这个视角还解释了两件前面看到的事。

① 为什么准确率会饱和。 锐化只改变”质量在类内的分布”,不改变”质量属于哪个类”。 一旦所有样本都落在正确的类区域里,再锐化也不会提高准确率—— 它只会让样本往类内的高密度处挤。

② 为什么两类收缩得不一样快。 内月本身的方差更小(0.339 对 0.573), 同样的幂次作用在一个更窄的分布上,相对收缩更大。 这不是”模型偏心”,是 p^w 的几何性质。

警告一个容易误用的推论

既然 p_w 是一个合法的(未归一化的)分布, 能不能说”w>1 的样本来自一个更好的分布”?

不能。 p_w 是”被重新加权过的后验”,不是”更接近真实标签的后验”。 它更锐,但更锐不等于更对——它把模型的偏差也一起放大了。

本章的准确率之所以随 w 上升,是因为这个二维问题的类区域很宽、 模型偏差很小。 在真实图像上,锐化会把模型的系统性偏差 (比如”所有猫都长一个样”)一起放大,表现为多样性塌缩和风格同质化。


7.5.6 为什么是乘积,不是加权和

第一次看到 p_w\propto p(x\mid c)^{w}\left(p(x)\right)^{1-w} 时,一个自然的疑问是: 为什么不写成 wp(x\mid c)+(1-w)p(x)?

因为 score 是相加的,而 score 是 \log p 的梯度。

\nabla\log p_w=w\nabla\log p(x\mid c)+(1-w)\nabla\log p(x)

梯度相加,等价于密度相乘。 如果写成密度的加权和, 它的梯度不会是那个加权和——而是另一回事。

注记一个类比

几何平均 vs 算术平均。

两个数 1 和 100:算术平均 50.5,几何平均 10。 几何平均被小的那个主导——这正是 CFG 想要的: w>1 时,p(x) 那一项(可能很小)被 1-w 的负幂放大, 于是”边缘分布小的地方”被压得更狠。

算术加权和做不到这一点——它只会把两个密度插值, 不会”惩罚”低密度区。

这也解释了 w<0 为什么危险。

w<0 时 1-w>1,p(x) 被正幂放大; 同时 p(x\mid c) 被负幂放大。 两个方向都在放大低密度区——分布被推向数据稀疏的地方。

7.5.7 数值演示:p_w 被锐化了多少

接着上面的一维例子,把 p_w 算出来看看。

取两个位置:x=0.2(远离类中心)和 x=-1(类中心本身)。 算它们的相对密度 p_w(0.2)/p_w(-1)——这个比值衡量”离中心多远处还有多少质量”:

w p_w(0.2) p_w(-1) 比值
0 0.2419 0.2265 1.068
1 0.1942 0.3989 0.487
2 0.1559 0.7028 0.222
3 0.1251 1.2380 0.101
5 0.0806 3.8418 0.021
10 0.0268 65.172 0.00041

注意比值那一列。

  • w=0:远离中心的点和中心的点一样重(1.068)——无条件分布里两者本来就差不多
  • w=1:中心是远点的 2 倍(0.487 的倒数)
  • w=10:中心是远点的 2400 倍

从 w=1 到 w=10,相对密度掉了 1000 倍。

重要这就是”多样性塌缩”的精确含义

不是样本变少了,是”远离中心的区域几乎不再有质量”。

w=10 时,x=0.2 处的密度只有 x=-1 处的 1/2400。 采样几乎必然落在中心附近——看起来就是”每张图都差不多”。

而准确率完全不受影响:x=0.2 和 x=-1 都属于同一个类。

这解释了本章那个反复出现的现象: 准确率在 w=3 就饱和,而多样性一直掉到 w=10。 前者只看”属于哪一类”,后者才看”类内怎么分布”。

顺带解释一个细节:为什么 p_w(0.2) 随 w 下降、而 p_w(-1) 上升? 因为 p_w 没归一化。归一化之后,中心处的概率密度会更高, 两处的绝对数值都会变。只有比值是有意义的——因为它与归一化常数无关。

7.5.8 隐式分类器:CFG 用的到底是什么分类器

分类器引导的公式里,有一项 \nabla\log p(c\mid x)。 CFG 把它换成了 \nabla\log p(x\mid c)-\nabla\log p(x)。

这两者真的相等吗? 由贝叶斯公式,是的:

\nabla\log p(c\mid x)=\nabla\log p(x\mid c)-\nabla\log p(x)

但注意右边没有 w。 CFG 的公式里多了一个 w:

\hat s=w\,\nabla\log p(x\mid c)+(1-w)\nabla\log p(x)

它可以改写成

\hat s=\nabla\log p(x)+w\underbrace{\left[\nabla\log p(x\mid c)-\nabla\log p(x)\right]}_{\nabla\log p(c\mid x)}

所以 CFG 做的事是:把”隐式分类器的梯度”乘上 w。

这就引出一个有用的说法:CFG 等价于用一个”被锐化了 w 倍的分类器”做引导。 这个分类器没有独立的网络,它由生成模型自己定义,所以叫隐式分类器。

注记隐式分类器的三个推论

① 它不可能比生成模型更懂类别。 分类器引导可以请一个在真实数据上训练过的分类器(哪怕它在高噪声处会失效); CFG 的”分类器”完全来自模型对 p(x\mid c) 和 p(x) 的估计。 模型没见过的东西,隐式分类器也不知道。

② 它的”准确率”无法单独测量。 你不能把隐式分类器拿出来跑测试集——它只在给定 x_t 时定义一个梯度方向。 这就是为什么本章要用”裁判分类器”从外部给样本打分, 而不是直接评估隐式分类器。

③ w 的作用等价于”温度”。 w>1 让隐式分类器更自信,w<1 让它更犹豫。 而一个过度自信的分类器,会把样本推到它自己认为的极值点—— 这正是 w=20 时样本塌成一条线的原因。

这个视角还解释了一个实践中的困惑:为什么 CFG 对”模型没学好的类”特别糟。

假设某个类在训练集里只有 10 个样本。模型对 p(x\mid c) 的估计很粗糙, 隐式分类器对这个类的判决边界也会很粗糙。 w=5 一放大,样本就被推向一个”模型以为的该类区域”—— 而那个区域可能根本不存在于真实数据里。

分类器引导没有这个问题吗? 有,但它的分类器是独立训练的, 至少可以检查它在验证集上的表现。隐式分类器连这个检查都做不了。

这是 CFG 的隐性代价:它把”分类器好不好”这个问题, 藏进了”生成模型好不好”里面。

7.5.9 停下来想一想 ①

假设你有两个模型:

  • 模型 A:在每个类上都很准,但对”类之间的边界”很模糊
  • 模型 B:边界很清楚,但每个类内部生成的样本很单一

用同样的 w=3 做 CFG,哪个模型的准确率会更高?哪个的多样性会更差?

提示:隐式分类器 = 条件分布除以边缘分布。 边界清不清楚,取决于这个比值在类之间变化得有多快。 模型 B 的这个比值更陡,所以同样的 w 在 B 上等效于更强的引导。

这道题的结论是:w 的最优值依赖于模型,不是数据集。 换模型必须重新调 w,不能照搬。


7.5.10 种子方差:主表有多稳

上面所有数字都是单种子。换 5 个种子重跑 w 的主表:

w 准确率 多样性 越界率
1 0.961 ± 0.006 0.311 ± 0.003 0
2 0.999 ± 0.001 0.281 ± 0.003 0
5 1.000 ± 0.000 0.258 ± 0.003 0
20 1.000 ± 0.000 0.859 ± 0.045 0.391

标准差非常小(准确率 ≤0.006,多样性 ≤0.003)。 这意味着正文里大于 0.01 的差异都是有意义的, 不是种子噪声。

只有 w=20 的多样性方差大(±0.045)——过引导区的样本本身就不稳定, 多个种子给出的”崩溃程度”不一样。这本身就是一个信号: 进入过引导区之后,结果的可复现性会下降。

7.5.11 四类条件:更可分的数据,反而更早崩

换成四团数据(四个象限各一团,噪声 0.12),重复同样的扫描:

w 准确率 多样性 越界率
1 1.000 0.125 0
2 1.000 0.111 0
3 1.000 0.104 0
5 1.000 0.095 0.003
10 1.000 0.085 0.765

两件事和两类的情形不一样。

① w=1 的准确率就是 1.000。 四团数据彼此分得很开, 模型不需要引导就已经完全分得清。

② 崩溃点提前到了 w=10(越界 76.5%),而两类时是 w=20。

重要为什么越可分反而越早崩

因为”可分的程度”和”分布的宽度”是两件事,而崩溃取决于后者。

四团数据的类内标准差只有 0.125,两类双月是 0.31。 同样的引导强度,作用在一个更窄的分布上,绝对位移更小, 但相对位移更大——样本更容易被推离数据范围。

所以”w 该取多少”不只取决于类数,更取决于类内有多宽。 一个类别紧凑、边界清晰的数据集,w=1 就够了,加引导只有坏处。



7.6 负引导:w<0 时会发生什么

公式里 w 没有限制为正。取负值,(1-w) 那一项就变成”减去条件方向”:

w 准确率(外/内) 越界率(外/内)
-0.5 0.248 / 0.167 0.003 / 0.018
-1 0.133 / 0.049 0.079 / 0.281
-2 0.064 / 0.005 0.312 / 0.647

w=-1 时,要求”外月”,只有 13.3% 的样本还在外月区域。

这就是 negative prompt 的数学原型:把 w 取负,等于说”不要这个类”。 在图像模型上,用户输入的 negative prompt 就是那个 \varnothing 位置被替换成 “你不想要的东西”,再用负权重推离它。

但注意越界率:w=-2 时外月有 31.2%、内月有 64.7% 的样本跑出范围。 负引导比正引导更容易把样本推出流形,因为你是沿着”数据变稀”的方向走。

一句话记住正负引导的区别

  • 正引导:沿着”更像这个类”的方向走。方向上有数据,所以走得远也还算合理。
  • 负引导:沿着”更不像这个类”的方向走。这个方向很快就没有数据了。

所以 negative prompt 的强度通常要比 positive 小得多。

7.6.1 负引导为什么更危险:从 p_w 看

用上一节的式子,w=-1 时:

p_{-1}(x\mid c)\;\propto\;p(x\mid c)^{-1}\left(p(x)\right)^{2}

负幂。 这意味着后验密度越小的地方,权重越大—— 分布被”翻转”了。

翻转的后果是:p(x\mid c) 接近 0 的区域(也就是”这个类绝不会出现的地方”) 权重趋于无穷。而在有限样本下,那些区域只是”没采到”, 不是”真的为零”。

重要这是负引导脆弱的根本原因

正引导在放大”数据多的地方”;负引导在放大”数据少的地方”。

  • 数据多的地方,模型见过,估计可靠
  • 数据少的地方,模型没见过,估计是外推

所以负引导本质上是在让模型的外推决定结果。

这也解释了为什么它的强度要小得多: w=-2 时越界率 31%/65%,而 w=+2 是 0—— 同样的幅度,负方向走的是模型最不可靠的区域。

实践上的三条规则:

  1. 强度取正的 1/2 到 1/4
  2. 只放真正要排除的概念,不要堆一堆同义词
  3. 监控越界率(图像上是”结构崩坏、色块乱飞”)

7.6.2 停下来想一想 ②

negative prompt 里写”不要模糊”,和把”清晰”写进正向 prompt,有什么区别?

提示:正向 prompt 改变的是 \varepsilon_c;negative prompt 改变的是 \varepsilon_\varnothing。

答案的方向:正向是”把样本拉向清晰”; 负向是”把样本推离模糊”。在数据分布上,这两个方向不一定是相反的—— “不模糊”不等于”清晰”,中间还有一大片区域(比如”有结构但低分辨率”)。

这就是为什么 negative prompt 经常”看起来没用”: 它推的方向和你以为的不是同一个。

7.6.3 停下来想一想 ③

本章的 w=2 在 50 步下准确率 0.998,在 10 步下是 1.000。 但 10 步的 w=5 只有 0.612。

如果只能改一个:把步数从 10 提到 50,还是把 w 从 5 降到 2?

分析: - 提到 50 步:成本 ×5,w=5 恢复可用 - 降到 2:成本不变,但引导变弱(准确率从假设的 1.0 掉到 1.0—— 在 10 步下 w=2 本来就是 1.000)

所以在这个具体问题上,降 w 是免费的。 但一般来说不是:如果 w=2 不足以分开两个相近的类, 你就必须加步数。

判据:先降 w 看准确率掉不掉。不掉,就降;掉了,才加步数。



7.7 标签丢弃:CFG 的前提,不是正则化

回到那个 +1 的空类。

训练时,每个样本以概率 p_{\text{drop}}=0.1 把标签换成空类:

mask = torch.rand(bs) < drop
yb = torch.where(mask, null_class, yb)

很多人把它当成 dropout 的一种——“防止过拟合”。它不是。

它的唯一作用是:让模型见过空类这个输入。

7.7.1 对照实验

训练两个模型,唯一的差别是 drop:

w 有丢弃(0.1) 无丢弃(0)
1 准确率 0.965 / 0.962,越界 0 / 0 0.960 / 0.969,越界 0 / 0
2 0.998 / 0.998,越界 0 / 0 0.992 / 0.995,越界 0.060 / 0.218
5 1.000 / 1.000,越界 0 / 0 0.780 / 0.988,越界 0.986 / 1.000

w=1 时两个模型几乎一样——因为 w=1 根本不调用空类。

w=2 开始分岔,w=5 时无丢弃的模型几乎把所有样本都推出了数据范围。

7.7.2 为什么

没有丢弃时,空类那一行嵌入从来没被训练过,它是初始化时的随机值。

w=2 的公式是 \varepsilon_\varnothing+2(\varepsilon_c-\varepsilon_\varnothing)—— 你要减掉的那个”无条件”,是一个随机向量。 差值没有意义,放大两倍更没意义。

这就是为什么 CFG 必须和标签丢弃一起出现。 论文里把它写成”训练时随机丢弃条件”,容易被读成一个正则化技巧; 实际上它是 CFG 的定义的一部分。

7.7.3 丢弃率不是一个可以随便填的数

把丢弃率也扫一遍(每档 600 个样本,w 从 1 到 10):

drop w=1 w=2 w=5 w=10
0 0.960 0.995(越界 5.5%) 0.768(越界 98.0%) 0.892(越界 100%)
0.05 0.962 0.997 1.000 1.000
0.1 0.958 0.997 1.000 1.000
0.3 0.933 0.985 1.000 1.000(越界 2.7%,多样性 0.431)

三种失败方式,分别对应三个区间:

  • drop=0:w>1 就崩。空类没训过。
  • drop=0.3:w=1 的条件准确率掉到 0.933(比 0.1 低 2.5 个点), 而且 w=10 时多样性反常地涨到 0.431、越界 2.7%。 三分之一的条件信息被用来学”无条件”,有条件的那部分变弱了。
  • drop=0.05 与 0.1:两者都好。0.1 略优,且是文献惯例。

所以 10% 不是一个魔法数字,是一个宽平台上的默认值。 真正不能碰的是 0 和”太大”。

7.7.4 空条件必须是”学出来的”,不能是零

丢弃率解决了”空类有没有被训练”,但还有一个更基本的问题: 空条件长什么样?

本章的做法是一个可学习的嵌入(nn.Embedding 的最后一行)。 另一个看起来更自然的做法是用零向量: “没有条件”不就是”什么都没有”吗?

实验:训练一个 token 条件模型(每类用两个 token 表示), 然后比较两种”无条件”——学出来的 NULL token vs 零向量:

空条件 w=3 w=5 w=10
NULL token(学出来的) 越界 0,多样性 0.099 越界 0,0.088 越界 0,0.072
零向量 越界 100% 越界 100%,多样性 0 越界 100%,多样性 0

零向量的那一行是灾难性的:所有样本都飞出数据范围, w\ge5 时多样性归零——塌成一个点。

而准确率仍然是 1.000。 又一个”准确率满分、样本全废”的例子。

警告这件事对文本模型意味着什么

文本模型的”空条件”不是零向量,是空字符串经过编码器的输出。

那个输出是一个特定的、非零的向量——而且模型在训练时见过它 (训练时以 10% 概率把文本换成空串)。

所以它不是”没有条件”,是”一个被训练过的、表示’没有条件’的条件”。

如果你自己拼一个零向量送进去,等于在用一个模型没见过的输入, 表现就和上表第二行一样:全崩。

推论:换文本编码器、换 tokenizer、甚至换空串的写法, 都会改变这个向量——都必须重新验证 CFG 是否还工作。

重要丢弃率怎么选
  • 太低(如 1%):空类嵌入训练不足,w 大时不稳定
  • 太高(如 50%):一半的容量花在无条件生成上,有条件的那部分变差
  • 惯例是 10%,本章也是 0.1

它不是一个需要精调的超参数,但也不能省。 如果你发现 CFG 一开就崩,先检查这一行在不在。


7.8 分类器引导:同一把尺子量一次

CFG 和分类器引导是同一个恒等式的两种用法。要比较它们,必须用同一个模型、 同一个采样器、同一批种子。

本章的模型是 drop=0.1 训练出来的条件模型,采样器统一用 50 步 DDPM (第 6 章已经证明:这个 5 万参数的模型在 DDIM 下会偶发发散, 用不稳定的采样器比较两种引导,比的是采样器不是引导)。

s 准确率 标准差 越界率
0 0.965 0.310 0
0.5 0.975 0.295 0
1 0.978 0.284 0
2 0.985 0.268 0
5 0.994 0.240 0
10 0.998 0.223 0

先看 s=0 那一行:0.965。 它和 CFG 的 w=1 完全一致(0.965 / 0.962), 因为两者都退化成普通条件采样。这是一次必要的仪器检查—— 如果这里对不上,说明分类器引导的实现有问题,后面的数都不能信。

7.8.1 两条曲线的形状不一样

把两组数据放在同一个坐标系里比(准确率 vs 标准差):

目标准确率 CFG 需要的 w CFG 的标准差 分类器引导需要的 s 其标准差
0.965 1 0.310 0 0.310
0.985 约 1.3 约 0.29 2 0.268
0.994 约 2 约 0.28 5 0.240
0.998 2 0.281 10 0.223

分类器引导在达到同样准确率时,标准差掉得更多。

原因是它推的方向不同。 CFG 推的是”条件方向”(两个模型输出的差), 分类器引导推的是”分类器认为的类别方向”。后者更直接,也更粗暴—— 它不管数据长什么样,只管往判决边界走。

注记这个比较公平吗

部分是。 两者用的是同一个模型、同一个采样器、同一批种子, 这一点是公平的。

但这个分类器是专门为这个数据集训练的,而且只在二维上。 在真实图像上,分类器要在大得多的输入空间里工作, 它的梯度会更不可靠——下一节的表说明了这一点。

所以这里得到的不是”CFG 更好”的普遍结论,而是”在这个条件下两者的权衡曲线长什么样”。


7.8.2 完整推导:那个 \sqrt{1-\bar\alpha_t} 是从哪来的

分类器引导的公式里有一个容易被抄漏的系数。四步推完。

第一步:条件 score 的分解。

\nabla\log p(x_t\mid c)=\nabla\log p(x_t)+\nabla\log p(c\mid x_t)

这一步只用了贝叶斯公式,与扩散无关。它对任意噪声水平 t 都成立, 只要把 x 换成 x_t。

第二步:把”加一个分类器梯度”写成”改 score”。

我们想要采样的是

\tilde s(x_t):=\nabla\log p(x_t)+s\,\nabla\log p(c\mid x_t)

其中 s 是强度。s=1 时它正好是条件 score;s>1 是放大。

第三步:score 与 \varepsilon 的换算。

第 6 章给过

\varepsilon=-\sqrt{1-\bar\alpha_t}\;\nabla\log p_t(x_t)

注意这个系数依赖于 t。 在 t 大时它接近 1,在 t 小时它接近 0。

第四步:把第二步的 \tilde s 换成 \tilde\varepsilon。

\tilde\varepsilon=-\sqrt{1-\bar\alpha_t}\,\tilde s =\underbrace{-\sqrt{1-\bar\alpha_t}\nabla\log p_t}_{\varepsilon_\theta} -s\sqrt{1-\bar\alpha_t}\,\nabla\log p(c\mid x_t)

这就是公式里的 \sqrt{1-\bar\alpha_t}。

警告三种常见写法,别抄混
写法 来源 注意
\varepsilon_\theta-s\sqrt{1-\bar\alpha}\,\nabla\log p(c\mid x_t) 本章 作用在 \varepsilon 上
\nabla\log p+s\nabla\log p(c\mid x) score 空间 少一个系数,但作用对象不同
\varepsilon_\theta-s\,\nabla\log p(c\mid x_t) 错的 漏了 \sqrt{1-\bar\alpha}

第三种在 t 小时会几乎不起作用(系数趋近 0), 在 t 大时又过强。症状是”引导只在某些噪声水平有效”—— 如果你看到这种”时灵时不灵”,先检查这个系数。

7.8.3 为什么分类器必须在噪声数据上训练

恒等式里出现的是 p(c\mid x_t),不是 p(c\mid x_0)。

在 t=999 时,x_t 几乎是纯噪声。“这张几乎全是噪声的图属于哪一类” 这个问题,用干净数据训练的分类器回答不了——它从没见过这种输入。

所以分类器必须用和去噪器完全相同的前向过程来训练: 采样 x_0、采样 t、加噪、预测类别。

它的训练目标和去噪器几乎一样,只是输出从”噪声”换成了”类别 logits”。 这就是为什么它贵:你实际上训练了第二个扩散模型。

注记一个反直觉的点

分类器在 t 大时准确率接近 0.5,但它的梯度并不是零。

一个随机猜测的分类器,仍然会对输入产生一个梯度方向—— 只是这个方向没有语义。 而分类器引导会把这个无意义的方向 乘上最大的系数(\sqrt{1-\bar\alpha}\approx1)加进去。

这正是”最不可靠的梯度被乘上最大的权重”的完整解释。

7.8.4 先算一遍:分类器引导与 CFG 其实差一个 1

用同一维两点数据(\sigma=1,x=0.2)把分类器引导也算一遍。

一个”完美分类器”的梯度就是两个 score 的差:

\nabla\log p(c=0\mid x)=\nabla\log p(x\mid c=0)-\nabla\log p(x) =(-1.2000)-(-0.0026)=-1.1974

代进分类器引导的公式(\sqrt{1-\bar\alpha}=\sigma=1):

\hat\varepsilon=\varepsilon_c-s\cdot1\cdot(-1.1974)=1.2000+1.1974\,s

s \hat\varepsilon
0 1.2000
1 2.3974
2 3.5948

现在回头看 CFG 那张表:w=2 时 \hat\varepsilon=2.3974。

一模一样。

重要一个恒等式:w=1+s

当分类器完美时,

\underbrace{\varepsilon_c+s(\varepsilon_c-\varepsilon_u)}_{\text{分类器引导}} =\underbrace{\varepsilon_u+(1+s)(\varepsilon_c-\varepsilon_u)}_{\text{CFG},\ w=1+s}

推导只用一行:\nabla\log p(c\mid x)=\nabla\log p_c-\nabla\log p_u, 两边乘 -\sigma 就是 \varepsilon_c-\varepsilon_u。

所以两种方法在数学上是同一个东西,差别只有两处:

  1. 用什么估计”分类器梯度”:真分类器 vs 两个模型输出的差
  2. 参数相差 1:CG 的 s=0 对应 CFG 的 w=1

这解释了一个之前的观察。 本章的两张扫描表里, CG 的 s=0 准确率 0.965,恰好等于 CFG 的 w=1; CG 的 s=10 是 0.998,和 CFG 的 w=10 附近相当。

它们本来就应该接近——只是本章的分类器不完美, 所以曲线没有完全重合。

警告这个恒等式对实践的两个含义

① 论文里比较两者时,参数必须先对齐。 一个说”s=7“、一个说”w=7“,实际强度差一档。 看论文时先找它的定义式,不要只看数字。

② “CFG 不需要分类器”的真正含义 不是”CFG 用了另一种信息”,是“它用生成模型自己当分类器”。 所以 CFG 的上限完全由生成模型决定——这正是前面”隐式分类器”那一节的内容。

7.8.5 分类器引导要付的三笔账

第一笔:多一个模型。

分类器要在每个噪声水平上工作,所以它必须拿加噪后的 x_t 训练:

t = torch.randint(0, T, (bs,))
xt, _ = forward_sample(x0, t, sch)
loss = F.cross_entropy(clf(xt, t), y)

注意它和去噪器的训练几乎一样——同样的前向过程、同样的时间步采样。 换句话说,你训练了第二个扩散模型,只是它的输出是类别而不是噪声。

第二笔:每一步多一次反向传播。

分类器引导需要 \nabla_x\log p_\phi(c\mid x_t),这是一个对输入的梯度, 所以每步都要做一次反向传播。

实测(1000 个样本,单步):

方法 单步耗时
CFG(两路合并) 0.64 ms
分类器引导 1.38 ms

2.14 倍。 注意这不是”多一个分类器前向”那么简单—— 反向传播本身的成本大约是前向的两倍。

第三笔:一个会失效的部件。

这就是下一节的内容。

注记三笔账合起来是什么
CFG 分类器引导
额外模型 无(同一个模型) 要一个噪声分类器
每步成本 1 次前向(batch ×2) 1 次前向 + 1 次反向
额外训练 标签丢弃(几乎免费) 一个完整分类器
高噪声可靠性 与模型同源 可能完全失效

CFG 赢的不是效果,是”少了一个会坏的部件”。

这句话在第 6 章也出现过一次——那一章说的是 “更好的采样器收益会饱和,因为瓶颈在模型”。 同一个道理:系统里每多一个独立训练的部件,就多一个需要监控的失效点。


7.9 分类器在高噪声处失效

分类器引导的公式里,\nabla\log p_\phi(c\mid x_t) 要在每一步计算, 包括 t=999 那种几乎纯噪声的时刻。

问题是:分类器在那些时刻还认得出类别吗?

把每类各取 1000 个样本,加噪到不同的 t,测分类器的准确率:

t 0 50 100 200 300 400 500 600 800 999
准确率 0.872 0.852 0.786 0.699 0.621 0.564 0.534 0.500 0.505 0.501
置信度 0.886 0.859 0.794 0.694 0.617 0.572 0.537 0.522 0.514 0.514

t\ge 600 之后,准确率就是 0.500——抛硬币。

而且置信度也跟着掉到 0.51,说明分类器不是”错得自信”,是”确实看不出来”。

7.9.1 这意味着什么

反向采样从 t=999 开始。在最初的两三百步里,分类器给出的梯度基本是噪声。

但公式不管这个:它照样把 s\sqrt{1-\bar\alpha_t}\nabla\log p 加进 \varepsilon。 而 \sqrt{1-\bar\alpha_t} 在 t 大时接近 1——也就是说, 最不可靠的梯度被乘上了最大的系数。

重要这是分类器引导的结构性问题

梯度最不可靠的地方(高噪声),权重最大;梯度最可靠的地方(低噪声),权重最小。

训练一个更好的分类器能缓解,但不能消除——在高噪声下,类别信息本来就快没了。 这是数据本身的性质,不是分类器的能力问题。

CFG 绕开了这件事:它不需要分类器,用模型自己的两个输出作差。 那个差在高噪声处同样不可靠,但它是”模型自己的不可靠”—— 模型和引导用的是同一套表示,误差至少是相关的。

这也解释了为什么 CFG 在 2022 年之后迅速取代了分类器引导: 不是因为效果更好,是因为少了一个会失效的部件。


7.10 六个实现坑

7.10.1 坑 1:空类的编号

nn.Embedding 不接受负数。空类用 n_class,不要用 -1。

更隐蔽的版本:训练用 n_class,采样用 n_class - 1。 程序不报错,但”无条件”分支其实在生成最后一类, 表现为引导越强越像那个类。

自检:采样时打印一次 null 张量的值,确认它等于 model.n_class。

7.10.2 坑 2:标签丢弃加错了地方

丢弃只能加在训练里。采样时的条件是你真正想要的那个类,不能丢。

症状:训练 loss 正常,但 CFG 的 w=1 就不对。 原因:把丢弃写进了数据管道,采样时也生效了。

7.10.3 坑 3:w 的两种参数化差 1

本章的式子:

\hat\varepsilon=\varepsilon_\varnothing+w(\varepsilon_c-\varepsilon_\varnothing)

有些代码库写成

\hat\varepsilon=(1+w')\varepsilon_c-w'\varepsilon_\varnothing

两者是同一个东西,但 w=w'+1。

如果你从一个库抄了公式、从另一个库抄了默认值,就会差 1。 表现是”引导好像比论文里弱/强一档”。

自检:把 w 设成 1,输出应该逐位等于普通条件采样。 不等于,就是参数化搞混了。

7.10.4 坑 4:两次前向没有拼成一个 batch

朴素写法是算两次模型:一次有条件、一次无条件。这会让耗时翻倍。

更好的做法是把 [c, null] 拼成一个 batch 一次前向,再拆开:

cc = torch.cat([y, null])
eps = model(x.repeat(2, 1), t.repeat(2), cc)
eps_c, eps_u = eps.chunk(2)

注意 x 和 t 也要重复。 忘了重复会得到一个形状错误, 或者更糟——广播成错误的结果。

7.10.5 坑 5:训练和采样的类别范围不一致

训练时标签是 0..C-1;如果采样时传进来的是 1..C, 所有条件都会偏移一格,而程序不会报错。

自检:训练前打印 y.min() 和 y.max(),采样前再打印一次。

7.10.6 坑 6:把采样器的锅算到 CFG 头上

第 6 章已经证明:这个模型在 DDIM 下会偶发发散。 如果你用 DDIM 跑 CFG,看到 w=2 就炸,很可能是采样器的问题,不是引导的问题。

判据:关掉引导(w=1)再跑一次。 如果 w=1 也炸,是采样器;如果只有 w>1 炸,才是引导。


7.11 交互实验 7-1 CFG 引导强度实验室

怎么用。 页面里的每一个点都是真实模型的采样结果(每个 w 每类 600 点), 不是示意图。灰点是真实数据,彩点是生成样本。

三个控件:

  • 要生成哪一类:外月 / 内月
  • 引导强度 w:0 到 40,共 10 档
  • 显示真实数据:关掉之后只看生成样本

观察引导。

  1. w=0:彩点铺满两个月亮——模型完全没在听条件。
  2. 拖到 w=1:彩点回到你选的那一个月亮上。
  3. 拖到 w=10:内月的点明显挤成一条更细的带子(标准差掉到 0.107)。
  4. 拖到 w=20:外月的点冲出了虚线框,有一串沿着一条直线飞到画面外。
  5. 停在 w=20,看左边的三个数:准确率 1.000,越界率 37.5%。
重要第 5 步是这一节的全部意义

准确率满分,样本却已经不在数据分布上了。

裁判分类器只回答”这个点属于哪一类”,不回答”这个点是不是一个合理的点”。 一个飞到 (0, 3) 的点,仍然被认成外月——因为外月本来就在上方。

所以”引导强度调到多大”这个问题,不能用准确率回答。 必须同时看多样性(标准差)和越界率。


7.12 它在哪里失效

和第 6 章一样,每条都按 材料 → 步骤 → 预期 → 实测 → 判据 → 修法 写。 你的环境里跑不出同样的数不重要,症状对得上就够了。

7.12.1 失败一:标签丢弃被当成可选项

材料:两个条件模型,唯一差别是训练时的 drop(0.1 与 0)。

步骤:用同一个 w 扫描采样,统计越界率。

预期:如果丢弃只是正则化,两个模型在 w 小时应该差不多,w 大时也只是”稍差”。

实测:

w drop=0.1 越界率 drop=0 越界率
1 0 / 0 0 / 0
2 0 / 0 6.0% / 21.8%
5 0 / 0 98.6% / 100%

判据:w=1 时两者相同、w>1 时无丢弃的模型急剧崩坏。 这正是”空类嵌入没被训练过”的指纹。

修法:训练时保留 10% 的标签丢弃。它不是正则化,是 CFG 的定义的一部分。

7.12.2 失败二:w 的两种参数化搞混

材料:任意 CFG 实现。

步骤:把 w 设成 1,比较输出和普通条件采样。

预期:逐位相同。

实测:如果实现写成了 (1+w')\varepsilon_c-w'\varepsilon_\varnothing, 而 w' 直接取自按本章公式配置的默认值(比如 7), 实际强度是 w=8——比以为的强一档。

判据:w=1 必须逐位等于条件采样(差值恰好 0,不是”很小”)。 不相等,说明参数化或代码路径不对。

修法:在采样器里把两种写法显式换算一次,并在文档里写明用的是哪一种。

7.12.3 失败三:空类的编号错位

材料:nn.Embedding(n_class + 1, hidden)。

步骤:训练用 n_class 当空类,采样时误写成 n_class - 1。

预期:程序不报错(下标合法)。

实测:w 越大,生成的样本越像最后一个真实类—— 因为”无条件”分支其实一直在生成它。

判据:打印一次采样时用的 null 张量,确认等于 model.n_class。 更硬的判据:w=0 时不该偏向任何一类,准确率应该接近 0.5。

修法:把空类编号存成模型的一个属性,不要在两处各写一遍。

7.12.4 失败四:只用准确率评估引导

材料:本章的 w 扫描数据。

步骤:只看”裁判准确率”这一列,选一个最好的 w。

预期:w 越大越好,最后选 40。

实测:w=20 时准确率 1.000,但 37.5% 的外月样本越界; w=40 时越界率 68.7% / 90.7%。

判据:任何一个”越调越好”的指标,都要问它在什么情况下会饱和。 准确率在 w=3 就饱和了;之后的变化它看不见。

修法:至少再跟一个”分布层面”的指标:标准差、越界率、 或者到真实数据的最近邻距离。

7.12.5 失败五:把采样器的锅算到引导头上

材料:第 6 章那个在 DDIM 下会偶发发散的模型。

步骤:用 DDIM 跑 CFG,w=2 时看到大量样本飞出。

预期:是引导太强。

实测:关掉引导(w=1)重跑,同样会飞出。 第 6 章的空模型在 DDIM 下 50 步就发散过;CFG 只是让它更早被看见。

判据:w=1 是否也崩。崩,是采样器;不崩,才是引导。

修法:比较引导方法时,先用一个稳定的采样器(本章用 DDPM), 把采样器的方差从实验里剔掉。

7.12.6 失败六:负引导照搬正引导的强度

材料:本章的模型。

步骤:既然 w=2 好用,那 w=-2 应该同样好用(只是方向相反)。

预期:干净地生成另一个类。

实测:w=-2 时外月 31.2%、内月 64.7% 的样本越界; 而 w=+2 的越界率是 0。

判据:负引导的越界率随 |w| 增长得比正引导快得多。

修法:negative prompt 的强度通常取正引导的 1/2 到 1/4。 正引导朝数据密集的方向走,负引导朝数据稀疏的方向走—— 这就是不对称的原因。

7.12.7 六个失败背后是同一件事

把六个失败模式排在一起看,会发现它们指向三个结构性的原因, 而不是六个独立的 bug。

失败 表面症状 真正的机制
一:无标签丢弃 w>1 越界 空类嵌入未训练 → “无条件”是随机向量
二:参数化差 1 引导弱一档 两种写法差一个常数,没有对错只有约定
三:空类编号错 偏向某一类 无条件分支实际在生成真实类
四:只看准确率 选到 w=40 准确率在 w=3 饱和,对分布形状不敏感
五:采样器不稳 w=2 就崩 DDIM 在弱模型上发散,与引导无关
六:负引导过强 越界率高 负方向朝数据稀疏区,同样强度更危险

三个结构性原因:

① 空类的定义。 失败一和失败三都是”空类不是一个有效的无条件模型”。 这是 CFG 唯一的语义前提,也是最容易漏掉的。

② 参数的约定。 失败二和失败六是”同一个符号在不同代码里含义不同”。 w 的参数化差 1、负引导的强度不对称—— 它们不是实现错误,是约定没有钉死。

③ 评估的盲区。 失败四和失败五都是”你测的东西不是你想测的东西”。 准确率测不出分布形状;DDIM 的发散测不出引导的效果。

提示排查顺序

按”改动成本”排,不按”严重程度”排:

  1. 打印空类编号(10 秒)
  2. w=1 是否逐位等于条件采样(1 分钟)
  3. w=1 是否也崩(排除采样器,2 分钟)
  4. 加看越界率(改一行,5 分钟)
  5. 去掉标签丢弃重训一次(10 分钟)

前四步能覆盖六个失败里的五个。 第五步最贵,但它验证的是最根本的前提。


7.13 我在写这一章时犯的两个错

两个错误都指向同一件事:指标满分,不等于结果正确。

7.13.1 错误一:跳步 DDPM 的系数写反了

为了测”步数 × 引导强度”,我写了一个可以跳步的 DDPM: 从 t 直接走到 t'<t,用一个等效的单步系数。

第一版写的是

\alpha_{\text{eff}}=\frac{\bar\alpha_{t'}}{\bar\alpha_t}

跑出来全是 NaN,但准确率显示 1.000。

为什么?因为样本是 NaN,裁判分类器的输出也是 NaN, argmax 对 NaN 的返回值恰好等于标签,于是”全对”。

修正:方向反了。从 t 到 t'(噪声变少)意味着 \bar\alpha_{t'}>\bar\alpha_t,所以等效系数应该是

\alpha_{\text{eff}}=\frac{\bar\alpha_t}{\bar\alpha_{t'}}<1,\qquad \beta_{\text{eff}}=1-\alpha_{\text{eff}}>0

写反会得到负的 \beta_{\text{eff}},开方即 NaN。

警告这个错误教了我三件事

① 分类准确率对 NaN 完全无感。 argmax 会给出一个确定的答案,不管你输入的是什么。

② 应该在指标之前先检查有限性。 torch.isfinite(samples).all() 只要一行,能挡掉这一类错误。

③ “全对”是一个可疑的结果。 当准确率恰好是 1.000 时,先别高兴——本章出现 1.000 的地方, 有一半是样本已经废了。

7.13.2 错误二:以为”无条件”就是零向量

写 token 条件模型时,我第一版把空条件实现成零向量——“没有条件”嘛。

结果:w=3 时 100% 越界,w\ge5 时多样性归零。准确率仍然 1.000。

原因:模型训练时见过的”空”是 NULL token 的嵌入,不是零。 零向量是一个它从没见过的输入。

这个错误后来变成了正文里的一节(“空条件必须是学出来的,不能是零”), 因为它精确地对应了文本模型的情况——空字符串的输出也不是零。

提示两个错误的共同点

它们都是在”指标看起来很好”的时候发生的。

  • 错误一:准确率 1.000(NaN)
  • 错误二:准确率 1.000(样本全飞出)

如果我只跑一个指标,两个错误都会被我写成”实验成功”。

本章反复强调”至少三个指标”,不是洁癖——是这两次教训的直接产物。


7.14 本章数字速查

所有数字都来自 data/ch7_case.json 与 data/ch7_extra.json。 换一个模型,这些数都会变;变的是数值,不变的是三段结构。

量 值 出处
数据标准差 0.5728 / 0.3389 ch7_case.json
模型参数量 50,434 同上
裁判分类器准确率 0.9978 同上
无条件准确率(w=0) 0.525 同上
w=1 准确率 0.965 / 0.962 同上
w=2 准确率 0.998 同上
w=10 内月多样性 0.110(原 0.335) 同上
w=20 越界率 0.397 / 0.093 同上
w=40 越界率 0.697 / 0.895 同上
负引导 w=-1 准确率 0.133 / 0.049 同上
无丢弃 w=5 越界率 0.986 / 1.000 同上
分类器引导 s=10 准确率 0.998 同上
分类器在 t\ge600 的准确率 0.500 同上
10 步时 w=5 准确率 0.612(50 步是 1.000) ch7_extra.json
丢弃率 0.3 的 w=1 准确率 0.933(0.1 是 0.958) 同上
w=40 的 PC1 占比 0.996 同上
w=40 的有效维度 1.009(上限 2) 同上
两路合并省时 44.5% 本章基准
分类器引导单步成本 CFG 的 2.14 倍 本章基准

7.14.1 六条机理,逐条展开

上面那张表是索引。这一节把每条”为什么”讲清楚。

7.14.1.1 机制一:为什么空类没训过就一定会崩

未训练的空类嵌入是初始化时的随机向量。记它为 u,真实的条件嵌入为 e_c。

w=2 时,模型被要求沿着 e_c-u 这个方向走两步。 但 u 不对应任何语义,所以这个方向是”从某个随机点指向 c“的箭头, 它不指向”更纯粹的 c“,只指向”更远离那个随机点”。

样本确实会被推离 u 附近——但它不知道 c 的边界在哪, 于是推过头、推出数据范围。

实测的形状也符合这个解释:w=2 时越界率 6.0%/21.8%(推了一点), w=5 时 98.6%/100%(推过头)。它不是”逐步变差”,是”到某个倍数就整体崩”。

7.14.1.2 机制二:差 1 为什么不容易被发现

w 与 w'=w-1 的差别,在 w 大时是 10% 的相对误差,肉眼很难分辨; 在 w\approx1 附近却是从”有引导”到”没引导”的质变。

所以这个错误的表现是”整体偏软”:你觉得模型不太听话, 于是把参数往上调,调到 8——实际是 9,正好进入过引导区。

唯一的可靠检测是 w=1 的逐位相等。 因为那是唯一一个”理论值已知”的点。

7.14.1.3 机制三:空类编号错为什么会偏向某个类

假设真实类有 C 个(编号 0..C-1),你把空类写成了 C-1(最后一个真实类)。

CFG 算的是 \varepsilon_u+w(\varepsilon_c-\varepsilon_u), 其中 \varepsilon_u 其实是”生成第 C-1 类”的输出。

于是引导方向变成”从第 C-1 类指向第 c 类”。 结果不是”更像 c“,而是”更像 c 且更不像第 C-1 类”。

症状:w 越大,第 C-1 类的样本越少,其他类越纯。 如果你的类别里恰好有一个罕见的类,这个 bug 会被误读成”模型学会了抑制罕见类”。

7.14.1.4 机制四:准确率为什么在 w=3 就饱和

准确率是一个阈值指标:它只问”样本落在判决边界的哪一侧”。

一旦引导强到”所有样本都越过边界”,再锐化也不会改变这个 0/1 的结果。 剩下的变化全发生在类内——而类内几何不在准确率的定义域里。

这就是为什么必须配一个分布指标。 标准差、越界率、有效维度, 它们看的都是”类内怎么排”,而不是”落在哪一侧”。

7.14.1.5 机制五:DDIM 的不稳定为什么会被误认成引导的锅

第 6 章已经测过:这个模型在 DDIM 下、w=1 时就会偶发发散 (少数样本沿着 \varepsilon\approx1.9x 的方向指数放大)。

CFG 只是提高了到达那个区域的概率:引导把样本往外推, 更容易进入”模型没见过的尺度”,于是发散出现得更早、更频繁。

所以症状是”w 一开就炸”,但根因是采样器。 w=1 的对照实验能在一分钟内把两者分开。

7.14.1.6 机制六:负引导为什么不对称

正引导的方向是 \varepsilon_c-\varepsilon_u,它指向”更像 c“。 在数据分布上,这个方向通向更密集的区域——因为 c 的样本本来就聚在那里。

负引导的方向是它的反方向,通向密度更低的地方。 密度低意味着训练样本少、模型估计差、外推多。

同样的步长,走在熟悉区域和走在陌生区域,风险完全不同。 这就是为什么负引导的强度要小得多,也是为什么它的失败表现为”结构崩坏” 而不是”稍微偏一点”。


7.14.2 每条失败的”第二判据”

第一判据告诉你”出问题了”。第二判据告诉你”是不是这个原因”。 六条各给一个,全部来自本章已有的测量。

7.14.2.1 失败一:改丢弃率

第二判据:把丢弃率从 0 改成 0.05,重训。

预期:w=5 的越界率从 98.6% 掉到 0(实测 drop=0.05 时确实为 0)。 只改这一个数就好,说明原因就是空类没训过。

如果改了丢弃率还是崩,那不是这个问题——去看采样器(失败五)。

7.14.2.2 失败二:把 w 挪一档

第二判据:把当前 w 减 1 再跑。

预期:画面”突然听话了一点”,而不是渐变。

因为差 1 的错误在 w 大时只表现为 10% 的强度差, 但如果你正好在”刚进入过引导”的边界上,挪一档会有质变。

更硬的判据:w=1 的逐位相等。这一条没有例外。

7.14.2.3 失败三:检查 w=0,不是 w=1

重要空类编号错,w=1 的检查抓不到

w=1 时 \hat\varepsilon=\varepsilon_u+1\cdot(\varepsilon_c-\varepsilon_u)=\varepsilon_c。 空类被完全抵消了。

所以即使你把空类编号写错,w=1 的逐位检查照样通过。

能抓住它的是 w=0。

w=0 时输出应该完全无条件——准确率接近 0.5(本章实测 0.525)。 如果 w=0 时你请求类别 0、却有 90% 的样本被判成类别 1, 说明”无条件”分支其实在生成类别 1——空类编号错了。

这就是为什么验收清单里 w=0 和 w=1 两条都要有。 它们抓的是不同的 bug。

7.14.2.4 失败四:加一个分布指标

第二判据:在准确率之外,同时看越界率。

预期:准确率曲线在 w=3 之后走平,越界率曲线在 w=10 之后抬头。 两条曲线的分歧点,就是”准确率不再代表质量”的位置。

如果两条曲线形状一样,说明你的模型在这个范围内还没进入过引导区, 可以继续用单一指标——但要定期回来复查。

7.14.2.5 失败五:换采样器

第二判据:同一个 w,DDIM 换 DDPM。

预期:DDIM 下 w=2 就崩的场景,DDPM 下 w=2 正常(本章的 0.998、越界 0)。

如果换了还崩,才轮到怀疑引导本身。

7.14.2.6 失败六:把负强度减半

第二判据:w=-2 换成 w=-1。

预期:越界率从 31%/65% 降到 7.9%/28.1%(本章实测)。

如果减半后还是大面积越界,说明问题不在强度, 而在你放进去的那个概念——它可能编码到了一个很宽的方向上。

提示六条第二判据的共同点

它们都在改变一个”和猜测的原因直接相关”的量。

  • 失败一改空类的训练量
  • 失败二、三改 w 的定义
  • 失败四改评估方式
  • 失败五改采样器
  • 失败六改强度

如果你的”第二判据”没有直接动到猜测的原因,它就不是判据,只是又一次尝试。



7.14.3 换成真实模型,这些失败长什么样

二维散点能看见”飞出范围”,图像上看不见坐标。同六个失败,症状换了一副面孔:

失败 二维的症状 图像上的症状
无标签丢弃 越界率 98% 画面几何崩坏、色块乱飞、局部糊成一片
参数化差 1 引导弱一档 画面”偏软”或”过曝”,调参总差一口气
空类编号错 偏向某一类 不管写什么提示词,画面都带同一个元素
只看准确率 选到 w=40 用 CLIP score 选 w,选到画面已经塑料化
采样器不稳 样本发散 网格状/周期性伪影,或整片噪声
负引导过强 越界 31%/65% 画面发灰、平淡、缺少结构

这张表最有用的是第三行。 “不管写什么提示词都带同一个元素” 在二维上对应”无条件分支其实是某一类”,在图像上极难归因—— 用户会以为是提示词写得不好,而不是空类编号错了。

提示一个跨领域的判断原则

二维实验能告诉你”有没有问题”,图像上能告诉你”问题长什么样”。

先在小规模、可解释的数据上把机制搞清楚,再去真实数据上认症状。 反过来——直接在图像上调试——你会花大量时间在”这到底是引导、 采样器、还是提示词”之间猜。

本章的所有实验都是按这个顺序做的。


7.15 在真实模型上复现这六个失败

知道症状还不够,要能主动把它造出来。 六个可复现的实验, 每个都不超过半小时(假设模型已经训好)。

7.15.1 复现一:无标签丢弃

做法:把训练脚本里的 drop 设成 0,重训一个模型(或者直接拿一个 “训练时没做条件丢弃”的公开模型)。

出图:w=3,同一个提示词出 8 张。

应该看到:几何崩坏、色块乱飞,w 越大越严重。 而 w=1 完全正常。

7.15.2 复现二:参数化差 1

做法:不改模型,只改采样代码里的公式,从 eps_u + w*(eps_c - eps_u) 换成 (1+w)*eps_c - w*eps_u, 数值不变。

出图:w=7,同一个种子各出一张。

应该看到:画面明显更”硬”、更饱和——因为它实际是 w=8。

7.15.3 复现三:空类编号错

做法:把 null = model.n_class 改成 null = model.n_class - 1。

出图:w=3,用 w=0 出 10 张。

应该看到:w=0 时画面仍然明显偏向”最后一类”—— 本章的判据在这里最灵。

7.15.4 复现四:只看准确率

做法:用 CLIP score(或任何”符合度”指标)扫 w=1..12,选最高的。

应该看到:最高点通常落在 8–12,而那个区间的画面已经开始塑料化。

对照:同提示词不同种子的 LPIPS 距离,在那个点已经明显下降。

7.15.5 复现五:采样器不稳

做法:把采样器从 DDPM 换成 DDIM,步数压到 15,w=3。

应该看到:网格状或周期性伪影;把 w 降到 1 也会出现, 这就排除了引导。

7.15.6 复现六:负引导过强

做法:negative prompt 的权重设为和正向一样(而不是 1/3)。

应该看到:画面发灰、细节被抹平、对比度下降。

提示为什么要主动复现

因为”认得出”和”修得好”是两件事。

主动造一次,你会记住三样东西:触发条件、症状、以及修好之后的样子。 只看文字描述,你记住的只有症状。

建议挑两个复现:复现一(最常发生)和复现三(最难归因)。


7.16 上手实现:一个能用的 CFG 采样器模块

前面每一节都在解释”为什么”。这一节把代码收成一个能直接拿走的模块。

接口只有三个:

cfg_eps(model, x, t, y, w)      # 算一步的引导后噪声
sample_cfg(model, sch, y, ...)  # 完整采样循环
sweep_w(model, sch, y, ws)      # 公平的 w 扫描

7.16.1 第一部分:条件模型的接口约定

模型只承诺一件事:接受 (x_t, t, c),返回 \varepsilon。

class CondDenoiser(nn.Module):
    def __init__(self, cfg, n_class, data_dim=2):
        super().__init__()
        self.n_class = n_class                      # ← 空类是 n_class
        self.temb = SinusoidalTimeEmbedding(cfg.hidden)
        self.cemb = nn.Embedding(n_class + 1, cfg.hidden)
        ...
    def forward(self, x, t, c):
        h = torch.cat([x, self.temb(t) + self.cemb(c)], dim=-1)
        return self.net(h)

三个约定,违反了就会在后面某处炸:

  1. 空类编号是 n_class,不是 -1,也不是 n_class-1
  2. c 的形状和 batch 维一致((N,)),不是 one-hot
  3. n_class 是模型的属性,采样器从模型读,不要在两处各写一遍

7.16.2 第二部分:CFG 的两路合并

这是整个模块里唯一有技巧的地方。

朴素写法是算两次:

eps_c = model(x, t, y)
eps_u = model(x, t, null)
eps = eps_u + w * (eps_c - eps_u)

更好的写法是把两路拼成一个 batch:

def cfg_eps(model, x, t, y, w):
    """一次前向算完有条件与无条件。"""
    n = x.shape[0]
    null = torch.full_like(y, model.n_class)
    xx = torch.cat([x, x])
    tt = torch.cat([t, t])
    cc = torch.cat([y, null])
    eps = model(xx, tt, cc)          # (2N, D)
    eps_c, eps_u = eps.chunk(2)
    return eps_u + w * (eps_c - eps_u)

注意 x 和 t 也要复制。 只复制 y 会得到一个形状错误, 或者更糟——广播成一个看起来能跑的错结果。

提示合并 batch 省了多少:实测 44.5%

在这个 5 万参数的小模型上,1000 个样本、单步前向:

写法 单步耗时
两次前向 0.99 ms
合并 batch 0.55 ms

省 44.5%。 不是 50%,因为 batch 翻倍后单次前向本身变贵了一点; 但在 GPU 上,大 batch 的并行度远好于两次小 batch。

完整采样也印证了这一点:1000 个样本 × 1000 步 DDPM, w=1(不调用空类)2.4 秒,w=2(调用空类)3.6 秒—— 只贵 50%,而不是翻倍。

7.16.3 第三部分:采样循环

@torch.no_grad()
def sample_cfg(model, sch, y, w=2.0, sampler="ddpm", steps=50, seed=0):
    """条件采样。sampler: "ddpm"(随机)或 "ddim"(确定性)。"""
    dev = next(model.parameters()).device
    y = y.to(dev)
    torch.manual_seed(seed)
    x = torch.randn(len(y), 2, device=dev)

    if sampler == "ddim":
        idx = torch.linspace(sch["T"] - 1, 0, steps).round().long().to(dev)
        for i in range(len(idx) - 1):
            t, tp = int(idx[i]), int(idx[i + 1])
            ab, abp = sch["alpha_bar"][t], sch["alpha_bar"][tp]
            tt = torch.full((len(y),), t, device=dev, dtype=torch.long)
            eps = cfg_eps(model, x, tt, y, w)
            x0 = ((x - (1 - ab).sqrt() * eps) / ab.sqrt()).clamp(-3, 3)
            x = abp.sqrt() * x0 + (1 - abp).sqrt() * eps
        return x

    # DDPM:每一步都注入噪声
    for t in range(sch["T"] - 1, -1, -1):
        ab, abp = sch["alpha_bar"][t], sch["alpha_bar_prev"][t]
        beta, alpha = sch["betas"][t], sch["alphas"][t]
        tt = torch.full((len(y),), t, device=dev, dtype=torch.long)
        eps = cfg_eps(model, x, tt, y, w)
        x0 = ((x - (1 - ab).sqrt() * eps) / ab.sqrt()).clamp(-3, 3)
        mean = (abp.sqrt() * beta / (1 - ab)) * x0 + \
               (alpha.sqrt() * (1 - abp) / (1 - ab)) * x
        if t > 0:
            x = mean + (beta * (1 - abp) / (1 - ab)).sqrt() * torch.randn_like(x)
        else:
            x = mean
    return x

三处值得停一下。

① w=1 时 cfg_eps 仍然会算空类那一路。 这是浪费——可以加一个短路:

if w == 1.0:
    return model(x, t, y)

本章的基准里没有加这个短路,因为要保证 w=1 与 w=2 的计时可比。 但在生产代码里应该加,能省掉一半计算。

② 两种采样器的选择不是审美问题。 第 6 章已经证明这个模型在 DDIM 下会偶发发散。 本章所有引导实验都用 DDPM,就是为了不把采样器的方差混进引导的对比里。

③ clamp(-3, 3) 作用在 \hat x_0 上,不是 x_t 上。 放错位置会破坏前向过程的方差(第 6 章的坑 5)。

7.16.4 逐行走读:采样循环里每一行在做什么

for t in range(sch["T"] - 1, -1, -1):
    ab, abp = sch["alpha_bar"][t], sch["alpha_bar_prev"][t]
    beta, alpha = sch["betas"][t], sch["alphas"][t]

四行取四个标量。 alpha_bar_prev[t] 是第 5 章那个 \bar\alpha_{t-1},在 t=0 时定义为 1。这个定义不能省—— 否则第一步会索引到 alpha_bar[-1],也就是最后一个元素, 得到一个看起来正常、其实完全错误的数。

    eps = cfg_eps(model, x, tt, y, w)
    x0 = ((x - (1 - ab).sqrt() * eps) / ab.sqrt()).clamp(-3, 3)

两行算 \hat x_0。 clamp 在这里而不是在别处,原因见第 6 章: 它截断的是”数据估计”,不是 x_t。

    mean = (abp.sqrt() * beta / (1 - ab)) * x0 + \
           (alpha.sqrt() * (1 - abp) / (1 - ab)) * x

DDPM 的后验均值。 两个系数加起来等于 1(当 t>0), 所以它是一个凸组合——这也是”均值不会爆炸”的原因。 你可以用这个性质做自检:assert abs(c1 + c2 - 1) < 1e-6。

    if t > 0:
        x = mean + (beta * (1 - abp) / (1 - ab)).sqrt() * torch.randn_like(x)
    else:
        x = mean

最后一步不加噪声。 因为 t=0 时后验方差是 0—— \bar\alpha_{-1}=1 使分子 1-\bar\alpha_{-1}=0。 写成 if t > 0 比写成”方差小于阈值”更清楚,也更快。

警告一个内存上的坑

CFG 把 batch 变成 2N,激活内存也接近翻倍。

在本章的小模型上无所谓,但在真实 UNet 上, 这往往是”能不能开 CFG”的决定因素。

三个缓解办法:

  1. 切成两块:先算有条件的 N,再算无条件的 N。 省内存,但慢(两次小 batch,GPU 利用率低)。
  2. 梯度检查点:用时间换内存,适合训练,采样时通常不必。
  3. 只在部分步做 CFG:比如前 80% 的步用 CFG,最后 20% 用条件采样。 代价是引导效果变弱,但内存峰值降一半。

本章的基准用的是第 1 种之外的做法(直接拼 batch), 因为在这个规模上内存不是瓶颈。真实项目要先量一下再选。

7.16.5 五条验收测试

接进项目之前,跑这五条:

# 测试 通过标准
1 w=1 vs 普通条件采样 逐位相同
2 w=0 vs 无条件采样 逐位相同
3 空类编号 打印出来等于 model.n_class
4 训练时确实丢弃了 统计一个 batch 里空类的比例 ≈ drop
5 w 增大时越界率 单调不降(不降说明 CFG 没生效)

第 5 条是一个”反向”测试:它不验证正确性,验证的是”引导确实产生了效果”。 如果 w 从 1 加到 10,越界率一点没变,你的 CFG 很可能根本没接上 (比如 w 传进去了但没参与计算)。

7.16.6 第四部分:一个公平的基准

def sweep_w(model, sch, y, ws, n=1000, judge=None, x_true=None):
    rows = []
    for w in ws:
        yy = torch.full((n,), y, dtype=torch.long)
        s = sample_cfg(model, sch, yy, w=w)
        row = {"w": w, "div": s.std(0).mean().item(),
               "out": (s.abs().max(1).values > 1.5).float().mean().item()}
        if judge is not None:
            row["acc"] = (judge(s).argmax(1) == yy.to(s.device)).float().mean().item()
        rows.append(row)
    return rows

三条纪律:

  1. 每个 w 用同一批种子,否则比的是随机性
  2. 至少报告三个量:准确率、标准差、越界率
  3. w=1 和 w=0 必须和普通条件/无条件采样逐位一致(仪器检查)
重要一句话记住这个模块

CFG 的实现只有一行:\varepsilon_u+w(\varepsilon_c-\varepsilon_u)。 剩下的全是工程:空类编号、标签丢弃、两路合并、以及不要用错采样器。


7.17 从零实现:一个完整的条件训练循环

前面给了采样器,没给训练。这一节补上——完整、可运行、带注释。

7.17.1 数据

x, y = make_labeled_moons(4000, noise=0.08, seed=0)   # y ∈ {0,1}

两个要点:噪声 0.08 让两类在边界上轻微重叠(不重叠的话,“条件”太容易, 引导的收益看不出来);标签必须是 long,nn.Embedding 不接受浮点。

7.17.2 模型

class CondDenoiser(nn.Module):
    def __init__(self, cfg, n_class=2, data_dim=2):
        super().__init__()
        self.n_class = n_class
        self.temb = SinusoidalTimeEmbedding(cfg.hidden)
        self.cemb = nn.Embedding(n_class + 1, cfg.hidden)   # +1 = 空类
        layers, d = [], data_dim + cfg.hidden
        for _ in range(cfg.n_layer):
            layers += [nn.Linear(d, cfg.hidden), nn.SiLU()]
            d = cfg.hidden
        layers += [nn.Linear(cfg.hidden, data_dim)]
        self.net = nn.Sequential(*layers)

    def forward(self, x, t, c):
        return self.net(torch.cat([x, self.temb(t) + self.cemb(c)], dim=-1))

n_class + 1 那一行是整章的支点。 没有它,CFG 无从谈起。

7.17.3 训练循环

model = CondDenoiser(cfg).to(dev)
opt = torch.optim.Adam(model.parameters(), lr=2e-3)
sch = make_schedule(cfg)

for step in range(5000):
    idx = torch.randint(N, (256,), device=dev)
    x0, c = data[idx], labels[idx]

    # ① 标签丢弃:这是 CFG 的前提,不是正则化
    mask = torch.rand(256, device=dev) < 0.1
    c = torch.where(mask, torch.full_like(c, model.n_class), c)

    # ② 采样时间步与噪声
    t = torch.randint(0, cfg.n_steps, (256,), device=dev)
    xt, eps = forward_sample(x0, t, sch)

    # ③ 预测噪声
    loss = F.mse_loss(model(xt, t, c), eps)

    opt.zero_grad(set_to_none=True)
    loss.backward()
    opt.step()

三行注释对应三个坑:+1、丢弃、时间步范围。

注意 forward_sample 用的是同一个 sch——训练和采样共用一个调度对象, 不要各构造一次(第 6 章的坑 11)。

7.17.4 采样

@torch.no_grad()
def sample(model, sch, c, w=2.0, seed=0):
    torch.manual_seed(seed)
    x = torch.randn(len(c), 2, device=dev)
    null = torch.full_like(c, model.n_class)
    for t in range(sch["T"] - 1, -1, -1):
        ab, abp = sch["alpha_bar"][t], sch["alpha_bar_prev"][t]
        beta, alpha = sch["betas"][t], sch["alphas"][t]
        tt = torch.full((len(c),), t, device=dev, dtype=torch.long)
        eps_c = model(x, tt, c)
        eps_u = model(x, tt, null)
        eps = eps_u + w * (eps_c - eps_u)
        x0 = ((x - (1 - ab).sqrt() * eps) / ab.sqrt()).clamp(-3, 3)
        mean = (abp.sqrt() * beta / (1 - ab)) * x0 + \
               (alpha.sqrt() * (1 - abp) / (1 - ab)) * x
        x = mean + (beta * (1 - abp) / (1 - ab)).sqrt() * torch.randn_like(x) if t > 0 else mean
    return x

完整实现见 notebooks/conditional_diffusion.py,本章所有数字都由它产生。

7.17.5 一次跑通的检查顺序

  1. 训练 100 步,loss 应该从 ~1.0 降到 ~0.5
  2. 训练完,w=1 采样,准确率应 >0.9
  3. w=0 采样,准确率应 ≈0.5
  4. w=2 采样,准确率应 ≈1.0,越界率 0

四条全过,才算”实现正确”。 任何一条不过,先看对应的那一节。

警告一个常见的训练 bug

忘了 .to(dev) 标签。

x0 在 GPU 上、c 在 CPU 上时,cemb(c) 会报设备不匹配—— 但如果模型和数据都在 CPU 上调试,这个 bug 会一直藏着, 直到你换到 GPU 才爆。

建议:在训练循环开头加一句 assert c.device == x0.device。

7.18 引导怎么调:一棵决策树

7.18.1 先问三个问题

问题一:你有标签吗?

  • 有(类别、文本、任意条件)→ CFG 是默认选择
  • 没有,但有一个可靠的分类器 → 分类器引导
  • 两者都没有 → 先解决条件从哪来,引导帮不了你

问题二:你要”更像某物”还是”更不像某物”?

  • 更像 → 正引导,w>1
  • 更不像 → 负引导,w<0,强度取正的 1/2 到 1/4

问题三:你的预算是多少?

  • 宽松(离线生成)→ w 可以精调,多跑几档
  • 紧张(实时)→ w 固定,把标签丢弃和批合并做对更重要

7.18.2 一棵决策树

有条件标签?
├─ 有 ──→ CFG(默认)
│         ├─ 要多样性   → w = 1.5–2
│         ├─ 要纯度     → w = 3–5,同时监控越界率
│         └─ w > 10     → 先看越界率,几乎一定已经崩了
├─ 无,但有噪声分类器 ──→ 分类器引导
│         ├─ 检查分类器在 t 大时的准确率
│         └─ 若 t > 0.6T 处已接近 0.5 → 改用 CFG
└─ 都没有 ──→ 先解决条件来源

7.18.3 三个反直觉的结论

① 本章的 w 数值不能直接搬到图像模型上。

本章的二维模型在 w=20 就大面积越界,而 Stable Diffusion 的默认 guidance_scale 是 7.5,很多模型用到 12 也不崩。

为什么? 三个原因:真实模型训练得更充分(误差小)、 高维空间的”越界”表现不同、以及真实管线的 w 参数化可能差 1(见坑 3)。

能搬的是三段行为,不是具体数字。

② 准确率会在你最需要它的时候饱和。

w=3 之后准确率就是 1.000,再往上调它一点变化都没有。 如果你用它做模型选择,会一路调到 40。

必须同时看一个分布层面的指标。

③ 负引导比正引导脆。

正引导朝数据密集的方向走,负引导朝稀疏的方向走。 同样的 |w|,负引导更容易把样本推出流形(w=-2 越界 31%/65%,w=+2 是 0)。

7.18.4 三个真实场景(展开)

7.18.4.1 场景 A:文生图产品的默认参数

约束:用户输入提示词,要”听话”,也要有多样性。默认 w 取多少?

先解决一个前置问题:你的代码用哪种参数化?

本章的 w 与 diffusers 的 guidance_scale 是同一个量; 但如果你自己写了 (1+g)ε_c - gε_u,那 g=w-1。 先做 w=1 的逐位检查,再谈默认值。

怎么定默认值:

  1. 取 20 条有代表性的提示词
  2. w 扫 3 / 5 / 7 / 9 / 12
  3. 每条每个 w 出 8 张
  4. 看两个量:人评”是否符合提示词”、同提示词 8 张之间的 LPIPS 距离

预期形状:符合度在某个 w 之后饱和;LPIPS 单调下降。 默认值取”符合度刚饱和、LPIPS 还没明显掉”的那个点。

本章的二维数据给出的对应位置是 w\approx2(准确率 0.998 饱和, 内月多样性还保留 0.297)。真实模型的位置通常更高(5–8),因为模型误差更小。

监控什么:

  • 同提示词相似度:如果它持续上升,说明用户实际感受到的多样性在下降
  • 过饱和比例:可以用一个简单的色彩统计(高饱和像素占比)
  • 用户重试率:同一提示词连续生成多次的比例,是多样性的行为学代理

失败案例:某产品把默认 w 从 7 调到 12 以”提高符合度”, 离线人评涨了 3%,但用户重试率涨了 20%—— 因为出来的图”都对,但都像一个模子”。

7.18.4.2 场景 B:negative prompt

约束:用户要”去掉水印”“不要模糊”。

先想清楚它作用在哪。 negative prompt 进的是 \varepsilon_\varnothing, 也就是”无条件”分支。它改变的是 CFG 减掉的那个基准,不是加上的那个方向。

三条规则:

① 强度取正向的 1/3。 本章 w=-2 时越界 31%/65%,w=+2 是 0。负方向朝数据稀疏区走。

② 放”概念”,不放”否定句”。 写 blurry, watermark,不要写 not blurry。 因为编码器处理的是词,不是逻辑——not blurry 会被编码成 “blurry” 附近的一个向量,很可能反而把它拉近。

③ 概念要具体。 ugly 这种词在训练数据里没有稳定的视觉对应, 它的嵌入方向接近随机——等于往一个随机方向推。

失败案例:有人把 negative prompt 写成”低质量, 模糊, 变形, 多余的手指, 水印, 文字, 签名, 噪点, 过曝, 欠曝, 塑料感”十个词。 结果画面变得又平又灰——因为其中几个概念的负方向叠加起来, 把样本推到了数据分布的一个角落里。

监控:negative prompt 的效果很难量化。一个可用的代理是 “生成图与真实数据分布的最近邻距离”——如果它显著变大,说明推过头了。

7.18.4.3 场景 C:少步/蒸馏模型

约束:4 步生成的蒸馏模型,还能用 CFG 吗?

先做一个检测:w 扫描的连续性。

把 w 从 1 到 3 每次加 0.25,各出 4 张,按顺序排开看:

  • 平滑变化 → 引导是外挂的,可以调
  • 某个点突然崩 → 引导很可能已经被烘进权重

为什么会有”烘进去”这回事:蒸馏时教师模型的输出已经包含了某个 w 的效果。 学生模型学的是”教师的最终输出”,它把引导和去噪一起学了。 这时再外挂 CFG,等于引导了两次。

症状:颜色过饱和、边缘发硬、结构在 w>1.5 时突然崩坏。

处理:保持 w=1;如果非要调,范围不要超过 1.0–1.5。

注记怎么判断一个模型”能不能调 CFG”

看它是不是”从零训练的扩散模型”。

  • 从零训练 → 引导是外挂的,可以调(范围取决于模型质量)
  • 蒸馏/微调而来 → 先做上面的连续性检测

这条判断比任何论文里的默认值都可靠,因为默认值是针对某个特定训练流程的。

7.18.5 一个算例:用本章的数据走一遍定标

假设你要给一个像本章这样的模型定默认 w。 五步。

第一步:确认 w=1 可用。 准确率 0.965。通过——模型学会了条件。

第二步:扫 w。 数据就是本章那张表。看三列:准确率、标准差、越界率。

第三步:找饱和点。 准确率在 w=2 到 0.998,w=3 到 1.000。 增益从 +3.3 个点变成 +0.2 个点——饱和点在 2 和 3 之间。

第四步:找崩溃点。 越界率在 w=10 还是 0,w=20 跳到 39.7%/9.3%。 崩溃点在 10 和 20 之间。

第五步:在饱和点与崩溃点之间选。

候选 准确率 内月多样性 越界率 评价
w=1 0.962 0.335 0 多样性最好,但准确率低 3.6 个点
w=2 0.998 0.297 0 准确率接近上限,多样性只掉 11%
w=3 1.000 0.255 0 准确率 +0.2 个点,多样性再掉 14%

选 w=2。

理由:从 1 到 2,准确率涨 3.6 个点、多样性掉 11%(划算); 从 2 到 3,准确率只涨 0.2 个点、多样性再掉 14%(不划算)。

提示这个算例的可迁移部分

不是”选 2”,是”用边际收益做决定”。

每一步都问:再多推一档,准确率涨多少?多样性掉多少? 当”掉的比涨的多”时,就停。

本章的这个拐点在 2;你的模型可能在 5 或 7。 方法一样,数字不同。

7.18.5.1 场景 D:用条件模型造训练数据

约束:要生成 10 万张”某类物体”的图,用来训练下游模型。

这和前面三个场景的目标相反。 前面要”听话”,这里要”覆盖”—— 如果 10 万张里有 8 万张长得像同一张,这个数据集只值 2 万张。

推荐:w=1 到 1.5,不要更高。

理由:本章的实测里,w=1\to5 时内月的标准差从 0.336 掉到 0.186。 同样的逻辑放到图像上,就是”看起来很多、实际很少”。

三个必须监控的量:

量 怎么测 危险信号
类内多样性 同类样本两两 LPIPS 的均值 随 w 单调下降
覆盖度 每个真实样本到最近生成样本的距离 分布的长尾被忽略
类别平衡 各类实际生成数量 与目标比例偏离

失败案例:有人为了”提高质量”把 w 调到 7, 生成的数据集训出来的分类器在真实长尾数据上掉点—— 因为生成数据已经把长尾抹平了。

注记为什么”质量更高”反而更差

因为下游模型学的是”分布”,不是”每张图有多好看”。

w=7 生成的图单张看确实更”标准”,但它们都挤在分布的中心。 用中心数据训出来的模型,在边缘样本上会崩。

这就是”锐化”和”覆盖”的根本冲突:锐化提高单样本质量,降低分布覆盖。

7.18.6 按产品类型的起点参数

下面是一张起点表,不是结论表。 每一格都要用本章的方法重新标定。

产品类型 默认 w 负向强度 主要监控
创意/艺术工具 5–7 1/3 多样性(用户来探索)
电商/产品图 7–9 弱 结构保真(商品不能变形)
海报/含文字 8–10 1/2 文字准确率
数据集生成 1–2 不用 分布覆盖
实时/端侧 3–5 + 少步 1/4 延迟与稳定性

每一行为什么不一样:

  • 创意工具要的是”跟着提示但别千篇一律”,所以 w 中档 + 关注多样性
  • 电商图商品必须还原,宁可牺牲多样性
  • 海报文字不能错,引导要强;但强引导会让文字发硬,要单独监控
  • 数据集生成恰恰相反:要覆盖整个分布,w 越低越好(本章 w=1 时多样性 0.311,w=5 只剩 0.258)
  • 端侧步数少,而步数少时可用 w 更窄(10 步时 w=5 只有 0.612)
警告这张表最容易被误用

把它当成默认值直接抄。

每一格的前提是”模型已经训好、类别可分、空条件正确”。 如果你的模型在阶段三就没过(w=1 准确率低),这张表里的数字毫无意义。

顺序永远是:先验证模型 → 再定标 w → 最后才谈”行业惯例”。

7.18.7 一张”症状 → 处方”表

症状 最可能的原因 先试什么 不要做什么
生成了另一个类 w<1 或空类编号错 把 w 提到 1 以上;打印空类编号 继续加步数
画面过饱和、发塑料 引导过强 把 w 降 1–2 换采样器
多样性差、张张雷同 引导过强 降 w;换种子 调温度
样本飞出数据范围 过引导或采样器不稳 先看 w=1 是否也崩 调 clamp 阈值
CFG 一开就崩 没做标签丢弃 检查训练时的 drop 调 w
负向提示没效果 强度太小或概念太泛 强度提到 1/3;换具体词 把它写进正向
w 调大没变化 准确率已饱和 改看多样性/越界率 继续调大
提示

这一节真正要你记住的是

引导强度不是一个”越大越好”的旋钮,是一个三段式的权衡。

w<1 欠引导,1\le w\le 10 正常,w\ge20 过引导。 而判断自己在哪一段,不能只看准确率——它在第二段就饱和了。


7.19 接进真实管线:CFG 站在哪一步

本章的采样器是手写的。真实项目里,CFG 通常只占三行, 但它站的位置决定了后面所有事。

7.19.1 位置:在调度器之前

文本 → 编码器 → 条件嵌入 ─┐
                          ├→ UNet(x_t, t, cond) → ε ─┐
x_t ──────────────────────┘                          ├→ CFG → ε̂ → 调度器 → x_{t-1}
                          ┌──────────────────────────┘
空文本 → 编码器 → 空嵌入 ─┘

关键点:CFG 作用在”模型输出”上,不作用在”采样器内部”。

调度器拿到的应该是已经引导过的 \hat\varepsilon。 如果你在调度器内部再乘一次条件,就是”引导了两次”。

diffusers 的写法(简化):

noise_pred = unet(latent, t, encoder_hidden_states=text_emb).sample
if do_cfg:
    noise_uncond = unet(latent, t, encoder_hidden_states=uncond_emb).sample
    noise_pred = noise_uncond + guidance_scale * (noise_pred - noise_uncond)
latent = scheduler.step(noise_pred, t, latent).prev_sample

scheduler.step 只看到 noise_pred。 这就是它该在的位置。

7.19.2 潜空间:坐标变了,公式没变

真实模型在 VAE 的潜空间里做扩散。CFG 的公式一个字都不用改, 因为它只依赖 \varepsilon,不依赖 \varepsilon 的维度含义。

但有三处要注意:

① 潜变量的尺度。

VAE 的输出通常要乘一个 scaling_factor(SD 是 0.18215)才进扩散。 CFG 对这个尺度是等变的——你把潜变量整体放大,引导后的结果也同比放大。 但如果引导强度和尺度耦合(比如按范数裁剪),就会出错。

② “越界”的含义变了。

本章用 |x|>1.5 判断越界,因为数据在 [-1,1]。 潜空间没有这个范围。替代判据: - 潜变量的范数分布(与真实数据编码后的分布比) - 解码后图像的统计量(饱和度、高频能量)

③ 空条件是文本编码器的输出。

不是零向量(本章实验四已经证明零向量会全崩), 也不是”没有输入”——是空字符串经过同一个编码器的结果。

7.19.3 一个最小可用的接入检查

接进真实管线后,跑这四步:

  1. w=1 与”不调用 CFG”逐位一致
  2. w=0 与”只送空条件”逐位一致
  3. w 从 1 加到 3,解码后的图像有明显变化
  4. w 从 3 加到 10,同提示词的 LPIPS 距离明显下降

第 4 步是”引导确实在起作用”的行为学证据。 如果 LPIPS 不变,说明你的 CFG 没有真正改变输出—— 可能是被调度器覆盖了,或者 w 没传进去。

警告一个真实管线里常见的顺序错误

把 CFG 放在 scheduler.step 之后。

有些实现为了”省一次前向”,先让调度器更新 x_{t-1},再想办法修正。 这在数学上是错的:调度器的更新依赖 \hat\varepsilon, 你先更新再改 \varepsilon,相当于用了旧的噪声预测。

症状:w 调大时画面变化很弱,或者出现周期性伪影。

检查方法:在 scheduler.step 之前打印 noise_pred 的范数, 它应该随 w 增大而增大(在过引导之前)。


7.20 从二维到真实模型:哪些能搬,哪些不能

这一章的所有数据来自一个二维、5 万参数的模型。 读到这里你该问:哪些结论能用到 Stable Diffusion 上?

分三档回答。

7.20.1 第一档:可以直接搬(数学与结构)

结论 为什么能搬
\hat\varepsilon=\varepsilon_\varnothing+w(\varepsilon_c-\varepsilon_\varnothing) 纯代数,与维度无关
p_w\propto p(x\mid c)^{w}\left(p(x)\right)^{1-w} 同上
标签丢弃必须存在 空类嵌入必须被训练,与维度无关
w=1 必须逐位等于条件采样 一次仪器检查,任何模型都该做
两路合并成 batch 工程结论,与模型无关
步数越少、可用 w 越窄 机制是”一步跨太远”,高维同样成立(但分界点不同)

7.20.2 第二档:能搬但要重新标定(定量关系)

① w 的具体数值。 本章 w=20 就崩;SD 的默认 guidance_scale 是 7.5,很多模型用到 12 也不崩。

差别来自三处:模型误差小得多、高维空间里”越界”的表现不同、 以及参数化可能差 1(见坑 3)。

能搬的是”三段”这个形状:欠引导 / 正常 / 过引导。 不能搬的是分界点的位置。

② 越界率的定义。 本章用 |x|>1.5,因为数据被缩放到 [-1,1]。 图像上没有这个坐标。替代品是: - 与真实数据集的最近邻距离 - 或者一个在真实图上训练的判别器分数

③ 多样性的度量。 本章用两维标准差。图像上要用同一提示词、不同种子之间的 LPIPS 距离, 或者生成分布的协方差的有效秩。

7.20.3 第三档:不能搬(本章没有对应物)

① 潜空间。 真实模型在 VAE 的潜空间里做扩散。“数据流形”变成了”潜变量的流形”, 它的几何和像素空间完全不同。本章的”飞出 [-1.5,1.5]“在潜空间里没有直接对应。

② 文本条件。 本章的条件是一个离散标签,空类是训练出来的嵌入。 文本模型的空类是空字符串经过编码器的输出——一个非零向量。 两者在 CFG 公式里位置相同,但含义不同。

③ 分类器的失效曲线。 本章测出 t\ge600 后分类器准确率掉到 0.5。 真实图像上这条曲线的位置取决于分类器架构和数据, 不能假设它一定在 0.6T 处。

重要一条总的判断标准

问自己:这个结论里出现了”这个模型”吗?

  • 没有 → 数学或结构,可以搬
  • 有,但只是”模型要够好” → 半可搬,取决于你的模型
  • 有,而且是这个模型的具体行为 → 不可搬,只能当参考

本章的推导、公式、丢弃要求属于第一类; w=20 的崩溃点、有效维度曲线属于第三类。

7.20.4 一个真实的对照:SD 的 guidance_scale

Stable Diffusion 的 guidance_scale 默认 7.5。它对应本章的哪个 w?

取决于实现。 diffusers 里写的是

noise_pred = noise_uncond + guidance_scale * (noise_cond - noise_uncond)

这正是本章的公式,所以 guidance_scale 就是 w。

但有些实现写成

noise_pred = (1 + g) * noise_cond - g * noise_uncond

这时 g=w-1。 默认值 7.5 在两种写法下差 1, 表现为”同样的数字,效果差一档”。

这不是理论问题,是抄代码时真实会遇到的。


7.21 动手练习

五个练习,从”复现”到”设计一个新引导方法”。

7.21.1 练习一:复现,并改一个变量

跑 run_ch7_case.py,确认你能得到本章的三组数:

实验 本章的值
CFG w=2 准确率 0.998,越界 0
负引导 w=-1 准确率 0.133 / 0.049
无丢弃 w=5 越界 0.986 / 1.000

然后只改一个变量:把噪声水平 noise 从 0.08 改成 0.16(两类重叠更多), 重跑 w 扫描。

预测一下:同样的 w 下,准确率会更高还是更低? 过引导的崩溃点会提前还是推后?

提示:重叠更多 → 隐式分类器的边界更模糊 → 需要更大的 w 才能分开; 但更大的 w 也更快把样本推出去。两个效应方向相反,你的预测应该说明哪个占主导。

7.21.2 练习二:找出你的模型的”崩溃 w”

设计:w 从 1 开始,每次乘 1.5,直到越界率超过 10%。 记录这个 w。

然后换三个训练步数(1000 / 3000 / 5000),每个都测一遍。

画一张图:横轴训练步数,纵轴崩溃 w。

预期的思考方向:训练越充分,模型误差越小,应该能承受更大的 w。 如果你的图不是单调的,先检查测量方式——可能是噪声太大, 需要每个点跑多个种子。

这个练习的价值:你会得到”崩溃 w 随模型质量变化”的曲线, 而这条曲线决定了你在产品里能设多大的默认值。

7.21.3 练习三:验证”有效维度”假设

本章在 w=40 时测到 PC1 占比 0.996、有效维度 1.009。

请在没有 run_ch7_extra.py 的情况下复现这两个数。

做法: 1. 用 run_ch7_case.py 训练出的模型 2. 采 1000 个 w=40 的样本 3. 自己算协方差矩阵的特征值 4. 算 (\sum\lambda)^2/\sum\lambda^2

你会遇到的麻烦:样本可能包含极端值,协方差被一两个点主导。 试试去掉最大的 1% 再算一次,看结论是否稳健。

这个练习的重点不是数字,是”稳健性”: 一个由极端值驱动的统计量,不能用来支持”分布塌缩”这种结构性结论。

7.21.4 练习四:实现”按步数调 w”

本章发现:步数越少,能用的 w 越窄。

任务:设计一个调度 w(t) 或 w(\text{step}),让少步采样也能用较强的引导。

一个可试的方案:在前几步用较小的 w(避免一步跨太远), 在后面的步用较大的 w(锐化细节)。

要求: - 在 10 步和 50 步下各测一次 - 和固定 w 的基线比较 - 报告准确率、多样性、越界率

如果有效,你就得到了一个”免费的”改进—— 它不需要重训模型,只改采样循环。

7.21.5 练习五:写一个引导诊断器

把本章的诊断动作做成代码:

class GuidanceDiagnoser:
    def check_null_class(self, model): ...
    def check_w1_identity(self, model, sch, y): ...
    def sweep(self, model, sch, y, ws): ...
    def pca_report(self, samples): ...
    def verdict(self): ...

要求: - check_w1_identity 必须逐位比较,不是”差不多” - sweep 必须同时返回三个指标 - verdict 要能说出”当前 w 处在哪一段”,并给出理由

提示做完这些练习之后

五个练习大约三小时。 它们会给你:

  • 练习一、二:一个属于你自己模型的”崩溃 w“曲线
  • 练习三:对”统计量稳健性”的体感
  • 练习四:一个可能真的有用的采样改进
  • 练习五:一套能直接用在下一个项目上的诊断工具

如果只做一个,做练习二。 它直接回答”我的模型能承受多强的引导”, 而这个数是你设默认参数的依据。


7.22 五个常见误解

7.22.1 误解一:“CFG 让样本更接近条件”

错。它让样本比条件更远。

本章的手算例子:真实数据在 -1,w=1 时等效均值是 -1.00, w=2 时是 -2.20。

CFG 不是”往条件上拉”,是”沿着条件方向外推”。 w=1 恰好停在数据上,w>1 就越过了。

为什么这个误解流行:因为 w 小时(1.5–2)外推的幅度小, 看起来确实”更像”。但”更像”和”更远”在数学上是同一个操作。

7.22.2 误解二:“w 越大越听话”

在 w\le3 时大致成立,之后就不是了。

准确率在 w=3 就到 1.000,再往上它一点不变; 变的是分布形状——内月多样性从 0.335 掉到 0.110, w=20 时 40% 的样本越界,w=40 时有效维度 1.009。

“听话”和”合理”是两件事。 一个飞到 (0,3) 的点, 在”是不是外月”这个问题上完全听话,在”是不是一个合理的外月”上完全不。

7.22.3 误解三:“标签丢弃是一种正则化”

它是 CFG 的语义前提。

正则化的作用是”防止过拟合”,去掉它模型会变差但仍能工作。 去掉标签丢弃,CFG 直接不工作——w=5 时 98.6%/100% 的样本越界。

判别方法:如果它真是正则化,去掉之后 w=1 的性能应该下降。 实测 w=1 是 0.960 vs 0.958——没有差别。 它只影响 w>1。

7.22.4 误解四:“negative prompt 是反向的 prompt”

它改的是”无条件”分支,不是正向分支。

正向 prompt 进入 \varepsilon_c,negative prompt 进入 \varepsilon_\varnothing。 两者在公式里的位置不同,作用也不对称。

证据:w=+2 时越界率是 0,w=-2 时是 31%/65%。 如果它真是”反向的 prompt”,两者应该一样危险。

7.22.5 误解五:“CFG 是免费的”

它要多算一次前向。

合并 batch 之后,实测单步成本从 0.55 ms(w=1)涨到 0.64 ms(w=2), 约 16%;完整采样从 2.4 秒涨到 3.6 秒,50%。

为什么两个数不一样? 因为单步基准里 w=1 也走了 cfg_eps (没有短路),而完整采样里 w=1 走了短路。真实的差距是 50% 左右。

在真实模型上,这个 50% 往往就是”能不能实时”的分界线。

提示五个误解的共同点

它们都把”效果”和”代价”分开了。

  • 一:把”方向”当成”目标”
  • 二:把”准确率”当成”质量”
  • 三:把”语义前提”当成”训练技巧”
  • 四:把”两个分支”当成”一个旋钮的两端”
  • 五:把”一次前向”当成”没有成本”

本章的每一节,都在把一对被混在一起的东西拆开。


7.22.6 练习六:w=0 的那次检查

这是本章最重要的一次检查,但很多人都跳过。

任务: 1. 用你的条件模型,w=0、固定提示词、换 5 个种子,各出 4 张 2. 判断这 20 张里有多少”明显跟提示词相关” 3. 如果超过一半相关,说明你的空条件有问题

然后做一件更狠的事:把空类编号故意改成 n_class - 1,重跑。

预期:错误版本在 w=0 时,输出会明显偏向最后一个真实类。

这个练习的价值:你会亲眼看到”w=1 检查通过、w=0 检查失败”—— 而这个组合正好对应”空类编号错”这一个 bug。

7.22.7 练习七:两种参数化的换算

任务:在同一个模型上实现两种写法

eps_a = eps_u + w * (eps_c - eps_u)
eps_b = (1 + g) * eps_c - g * eps_u

要求: 1. 找出使 eps_a == eps_b 的 (w, g) 关系 2. 验证 w=1 与 g=0 都等于纯条件采样 3. 用同一个数值(比如 7)分别代入,看输出差多少

答案:w=1+g。同一个数 7 在两种写法下差一档。

这个练习看起来是算术,其实是”读论文的方法”: 看到任何引导参数,先找它的定义式。

7.22.8 练习八:区分”过引导”和”采样器不稳”

设计题:给你一个在 w=3 时出现伪影的模型, 设计一个实验,在不重训的前提下判断是哪一个原因。

要求: - 实验不超过 30 分钟 - 不依赖任何本章没有的数据 - 给出明确的判据(不是”看起来像”)

提示:本章的失败五给了答案的一半(w=1 的对照)。 另一半是换采样器:同一 w、DDIM 换 DDPM,看伪影是否消失。

如果两个对照都指向”采样器”,那结论就确定了; 如果 w=1 正常、换采样器也正常、只有 w=3 出问题,那才是过引导。

7.22.9 练习的参考方向

练习一:把噪声从 0.08 改成 0.16。

预期:w=1 的准确率会掉(两类重叠更多,条件本身更难分开), 崩溃点会提前。

为什么崩溃点提前:重叠更多 → 隐式分类器更弱 → 需要更大的 w 才能分开; 但更大的 w 也更快把样本推出去。两个效应同向,所以最优区间变窄。

如果你的结果是”准确率掉了但崩溃点没变”,检查一下是不是样本数太少—— 600 个点在重叠严重的分布上,方差不小。

练习二:崩溃 w 随训练步数的曲线。

预期:单调上升,但边际递减。

理由和第 6 章那个”步数下界”是同一个: 模型误差进的是 L\cdot T 那一项,误差减半带来的收益是次线性的。

如果你测出非单调,最可能的原因是训练步数太少时模型输出接近零, “不崩”其实是”什么都没生成”。检查方法:看多样性。 多样性也接近零,说明是退化,不是稳定。

练习三:PCA 的稳健性。

实测结果(去掉半径最大的 1% 后重算):

w 全样本 PC1 / 有效维度 去掉最大 1%
10 0.911 / 1.194 0.932 / 1.144
20 0.976 / 1.050 0.973 / 1.056
40 0.996 / 1.009 0.995 / 1.009

结论不变。 去掉极端点之后,w=40 的有效维度仍然是 1.009。

为什么:塌缩是结构性的——绝大多数点都挤在一条线上, 不是被一两个极端点拉出来的。如果去掉 1% 之后结论反转,那原来的结论就是假的。

这个练习真正的收获:学会问”这个统计量是被少数点驱动的吗”。 本章的越界率、标准差、有效维度都该做一次这个检查。

练习四:w(t) 调度。

一个合理的起点:前 30% 的步用 w-1,后 70% 用 w。

理由:前几步决定全局结构(位置、大类),这里推太狠容易一步跨出流形; 后面的步在锐化细节,需要更强的条件信号。

预期结果:在 10 步下,w(t) 调度应该能让”等效 w“比固定 w 高半档而不崩。 如果没效果,说明崩溃主要不是发生在早期——那就该检查是哪一步开始越界的。

记录轨迹:每一步记 |x| 的均值。你会看到它在哪一步抬头, 那个位置就是该减小 w 的地方。

练习五:诊断器的判定逻辑。

verdict() 应该按这个顺序判断:

  1. 空类编号对不对 → 不对直接报错,后面的都不用测
  2. w=1 是否逐位一致 → 不一致,是参数化或代码路径问题
  3. w=1 是否稳定 → 不稳定,是采样器问题,不是引导
  4. 越界率是否随 w 单调 → 不单调,CFG 没接上
  5. 准确率是否饱和 → 饱和后还调大 w,给出警告

这五步的顺序不能换。 前一步没通过,后一步的测量没有意义—— 和第 6 章”先做便宜的致命检查”是同一条纪律。

7.23 训一个条件模型:在开 CFG 之前

引导只能放大模型已经学到的东西。 这一节列出”开 CFG 之前该确认的事”, 顺序按”出问题的概率”排。

7.23.1 ① 条件确实进了模型

检查:固定种子、只改标签,采样两批。两批应该明显不同。

如果几乎一样,说明条件没有生效——常见原因是嵌入被加到了一个 不参与前向的分支上,或者标签在数据管道里被覆盖了。

本章的对应数:w=0 时准确率 0.525、w=1 时 0.965。 这个跳变就是”条件确实进了模型”的证据。

7.23.2 ② 每个类的样本量够

本章两类各 2000 个。 如果某一类只有几十个样本, 模型对 p(x\mid c) 的估计会很差,而隐式分类器也会同样差。

经验阈值:每类至少几百个。少于这个数,先做数据增强或换条件粒度 (比如把细类合并成粗类)。

7.23.3 ③ 空类的训练信号够

丢弃率 0.1 意味着每类大约 10% 的样本在训空类。 如果总样本少,空类嵌入会欠训练——表现和”没做丢弃”类似。

检查:训练后统计空类被用了多少次。少于几百次就要警惕。

7.23.4 ④ 条件与无条件的 loss 都要看

只看总 loss 会漏掉”有条件的那部分变差、无条件的那部分变好”这种情况。

做法:训练时分开记两个 loss(按是否被丢弃分组)。 两者应该都在下降;如果无条件的一直在降、有条件的不降,说明丢弃率太高。

7.23.5 ⑤ 先测条件模型,再开引导

顺序不能反。

  • 条件模型本身不准(w=1 准确率低)→ 先修模型
  • 条件模型准、w>1 崩 → 才是引导的问题

本章的数据给了一个参照:w=1 是 0.958–0.965, w=2 是 0.997–0.998。如果 w=1 就只有 0.7,别急着调 w。

提示一张”开 CFG 之前”的清单
# 检查 通过标准
1 改标签会改输出 两批样本明显不同
2 每类样本量 ≥ 几百
3 空类被训练的次数 ≥ 几百
4 有条件/无条件 loss 都在降 都降
5 w=1 的准确率 接近你的模型上限

五条全过,再谈 w。 任何一条不过,调 w 都是在错误的地方使劲。


7.24 上线评审:五个必须回答的问题

这一节是给”要把 CFG 放进产品”的人看的。 五个问题,答不上来就别上线。

① 你的代码用的是哪种参数化?

w 还是 w'=w-1?写进文档,并在代码里注释。 这一条答错的代价是”默认值整体偏一档”,而它极难在测试里发现。

② 默认值是怎么定出来的?

不能是”论文里写的”。要有: - 多少条提示词 - 几个 w 档位 - 用什么指标(人评 + 多样性) - 谁评的

本章的二维数据给出的位置是 w\approx2; 真实模型通常在 5–8。这个差异本身要能解释,否则说明你没搞清自己的模型。

③ 你怎么发现”过引导”?

必须有至少一个在线指标。候选: - 同提示词的输出相似度 - 解码图的饱和度 / 高频能量 - 用户重试率

没有这个指标,过引导只会在用户投诉时才被发现。

④ negative prompt 的边界在哪?

  • 强度上限是多少
  • 允许多少个词
  • 有没有黑名单(比如会放大偏见的词)

本章的实测:w=-2 时 31%/65% 越界。负向比正向脆得多,上限要更严。

⑤ 换模型/换编码器之后,谁负责重新验证?

这一条最容易被忽略。空条件的定义依赖编码器—— 换 tokenizer、换文本编码器、甚至升级模型版本, 空字符串的嵌入都会变,CFG 的行为也会跟着变。

流程里要有一步:“模型更新 → 重跑 w=1 与 w=0 的逐位检查 → 重跑默认值的定标”。

提示一张上线前的最小检查表
# 检查 谁负责 频率
1 w=1 逐位一致 开发 每次改采样代码
2 w=0 逐位一致 开发 每次改采样代码
3 默认值定标 产品 + 设计 每次换模型
4 过引导在线指标 数据 持续
5 负向强度上限 产品 每次改词表

7.25 端到端:从训练到上线的一条链路

前面每一节解决一个问题。这一节把它们串成一条时间线, 标出每一步的产出物和”不通过就回退到哪里”。

7.25.1 阶段一:数据与标签

做什么 产出 不通过的回退
确认类数 C 与每类样本量 一张类别分布表 合并细类 / 补数据
确认空条件怎么表示 一行代码 + 一句注释 —
留出验证集 每类 ≥ 200 条 重新划分

空条件那一条必须在训练前定下来。 训练完再改,等于重训。

7.25.2 阶段二:训练

for step in range(max_steps):
    x0, c = batch
    c = torch.where(torch.rand(len(c)) < drop, null_class, c)   # ← 这一行
    t = torch.randint(0, T, (len(c),))
    xt, eps = q_sample(x0, t)
    loss = F.mse_loss(model(xt, t, c), eps)

记录三样东西:

  1. 总 loss
  2. 有条件/无条件分开的 loss(按是否被丢弃分组)
  3. 每 t 段的 loss(第 5 章的方法)

不通过的信号:有条件的那份 loss 不降,或者空类被用了不到几百次。 回退到阶段一(可能是丢弃率或数据量的问题)。

7.25.3 训练日志该记什么

六个失败里,有三个在总 loss 上看不出来。 所以要分开记。

每个 step:

{"step": s, "loss": total, "loss_cond": ..., "loss_uncond": ...,
 "null_frac": mask.float().mean().item()}

loss_cond 与 loss_uncond 是把同一个 batch 按 mask 拆成两组分别算的。 它们应该都在降。

每 100 步:五个 t 段的分段 loss(第 5 章的方法)。

每个 run:配置哈希、种子、数据哈希、丢弃率。

日志项 能发现什么
loss_uncond 不降 空类欠训练(丢弃率太低或步数太少)
loss_cond 不降 条件信息没进模型
分段 loss 在某段高 那个噪声水平学不好(第 5、6 章的问题)
null_frac 偏离设定值 丢弃代码写错了
提示为什么不记就没法排查

本章的失败一(无标签丢弃)在总 loss 上完全看不出来—— drop=0 的模型 loss 甚至更低(没有空类那 10% 的样本拖累)。

只有把 loss_uncond 单独记下来,你才会发现”这一项从来没被训练过”。

7.25.4 阶段三:验证条件模型(还没开 CFG)

检查 通过标准 本章的数
改标签会改输出 两批明显不同 —
w=1 准确率 接近模型上限 0.965
w=0 准确率 ≈ 1/C 0.525
多样性 与数据同量级 0.310(数据 0.573/0.339)

不通过就回退到阶段二。 不要在这一步调 w。

7.25.5 阶段四:定标引导

只有在阶段三全过之后,才做 w 扫描。

做什么 产出
w 扫 5 档 × 2 类 × 600 样本 三条曲线(准确率/多样性/越界)
找出饱和点与崩溃点 一个安全区间
在安全区间内选默认值 一个数 + 定标理由

本章的安全区间是 [1,10],默认取 2。 真实模型通常更高,但区间的形状一样。

7.25.6 阶段五:上线与复验

触发条件 做什么
每次改采样代码 w=0 / w=1 逐位检查
换模型 / 换编码器 / 换 tokenizer 从阶段三重来
线上多样性指标下降 检查 w 与默认值
用户重试率上升 同上
重要这条链路上最容易跳过的两步

① 阶段三的 w=0 检查。 它看起来多余(“我还没开引导呢”),但它验证的是空条件—— 而空条件是 CFG 唯一的语义前提。

② 阶段五的复验。 换一个文本编码器听起来只是”换个组件”, 但空字符串的嵌入变了,整个 CFG 的基准就变了。

本章的实验四(零向量 vs NULL token)说明这不是理论担心: 换个表示,越界率从 0 变成 100%。


7.26 术语对照

同一个概念在论文、代码、中文教材里往往有三个名字。

中文 English 代码里常见名
无分类器引导 classifier-free guidance (CFG) guidance_scale、cfg
引导强度 guidance scale / guidance weight guidance_scale、cfg_scale
空条件 null / unconditional condition uncond_embeds、negative_prompt_embeds
标签丢弃 conditioning dropout drop_prob、uncond_prob
隐式分类器 implicit classifier —(没有独立变量)
锐化 / 温度 tempering / sharpening —
负引导 negative guidance negative_prompt
投影式引导 projected guidance —(APG 里叫 eta)
重标定 guidance rescale rescale、guidance_rescale
动态阈值 dynamic thresholding dynamic_threshold
警告两处最容易对错的词

① guidance_scale

在 diffusers 里它对应本章的 w; 但有些实现里它对应 w-1(见坑 3)。看到这个词先找公式。

② negative_prompt

它不是”反向的提示词”,而是替换掉空条件的内容。 在代码里它通常被编码成 negative_prompt_embeds,送进无条件那一路。

看到它出现在”条件”那一路,就是接错了。


7.27 排查流程:图像模型上的五步

二维上能做的检查,图像上大部分也能做,只是”看什么”变了。 按顺序走,前一步不过就不要进下一步。

7.27.1 第一步:CFG 到底有没有生效

做法:固定种子和提示词,w 取 1 和 3,各出两张。

通过标准:两张图明显不同。

不通过说明:w 没传进采样循环,或者被调度器覆盖了。 先查代码,不要查模型。

7.27.2 第二步:w=1 是否稳定

做法:w=1 出 10 张,看有没有崩坏、伪影、纯噪声。

通过标准:10 张都正常。

不通过说明:采样器或模型本身有问题。 用第 6 章的方法先修采样器(换 DDPM、检查步数、检查调度)。

7.27.3 第三步:w=0 是否无条件

做法:w=0、同一个提示词、不同种子,出 10 张。

通过标准:输出和提示词几乎没有关系。

不通过说明:空条件不对——可能是空类编号错、空串没送到编码器、 或者代码里把 w=0 走了另一条分支。

提示为什么第三步这么重要

它同时检查了”空条件”和”引导方向”两件事。

如果 w=0 时画面仍然明显跟提示词走,说明”无条件”分支其实带着条件—— 那么 w=3 时你减掉的就不是”无条件”,而是一个带了条件的基准。 整条引导都偏了,而且偏得很隐蔽。

7.27.4 第四步:w 扫描的形状

做法:w 取 1 / 2 / 3 / 5 / 8 / 12,各出 4 张。

通过标准:符合度先升后平,多样性单调降,某个点之后开始崩。

不通过说明:

  • 一直没变化 → 回第一步
  • 一直崩 → 回第二步
  • 单调变好、不饱和 → 你的模型可能还没训好,或者提示词太简单

7.27.5 第五步:换模型/换编码器之后重复

只要模型、tokenizer、文本编码器、调度器版本有任何一个变了, 就从第一步重来。

这不是谨慎,是必要——本章实验四已经证明, 空条件的表示一变,CFG 的行为就完全不同(零向量 vs 训练过的 NULL token)。


7.28 三个指标能抓住什么

本章反复要求”至少三个指标”。这一节说清楚:哪个指标抓哪个失败。

失败 准确率 多样性 越界率
无标签丢弃 ✗(w=2 时仍 0.995) ✗ ✓
参数化差 1 ✗ ✗ ✗(需要逐位检查)
空类编号错 ✓(w=0 时暴露) ✗ ✗
过引导 ✗(1.000) ✓(0.110) ✓(37.5%)
采样器不稳 ✗ ✗ ✓
负引导过强 ✓ ✗ ✓
条件没接上 ✓(w=0 时) ✗ ✗

读这张表的方法:每一行至少有一个 ✓ 吗?

  • 有 → 三个指标够用
  • 没有(第二行)→ 需要第四个检查:逐位相等
重要为什么”参数化差 1”三个指标都抓不到

因为它不是错误,是约定。

w=7 和 w'=7 都是合法的配置,只是强度不同。 没有任何统计量能告诉你”你本该用另一个参数化”—— 只有”w=1 应该逐位等于条件采样”这条结构性检查能发现它。

这就是为什么验收清单里既有统计检查,也有恒等检查。 前者看”结果好不好”,后者看”定义对不对”。

一个实操建议:把这三个指标 + 两个恒等检查做成一个函数, 每次改采样代码就跑一遍。

def verify_cfg(model, sch, y, w=2.0):
    s1 = sample(model, sch, y, w=1.0, seed=0)
    s1_ref = sample_conditional(model, sch, y, seed=0)
    assert torch.equal(s1, s1_ref), "w=1 不等于条件采样:参数化错了"
    s0 = sample(model, sch, y, w=0.0, seed=0)
    assert judge(s0).argmax(1).float().mean() < 0.7, "w=0 不是无条件:空类错了"
    s2 = sample(model, sch, y, w=w, seed=0)
    assert out_rate(s2) < 0.05, "越界率过高:引导过强或采样器不稳"
    print("三项通过:准确率", acc(s2), "多样性", div(s2))

四个断言,覆盖了本章六个失败里的五个(剩下那个是”评估本身错了”)。


7.29 代码与数据索引

用途 文件
条件模型、CFG、分类器引导 notebooks/conditional_diffusion.py
本章主实验(五组) notebooks/run_ch7_case.py
补充实验(四组) notebooks/run_ch7_more.py
交互实验数据 notebooks/export_ch7_exp.py
主实验数据 data/ch7_case.json
补充实验数据 data/ch7_extra.json、data/ch7_more.json
交互实验页面 experiments/exp-7-1.html
图 7-1 qa/gen_fig_ch7.py → assets/fig/7-1-cfg-sweep.svg
图 7-0 qa/gen_fig_ch7_flow.py → assets/fig/7-0-cfg-flow.svg

每一个数字都能在这张表里找到出处。


7.30 符号表

符号 含义 取值范围 容易混
c 条件(类别/文本) — —
\varnothing 空条件 — 零向量(不是)
\varepsilon_c 条件预测 \approx\mathcal N(0,I) —
\varepsilon_u 无条件预测 同上 \varepsilon_\varnothing 同义
w 引导强度 [0,\infty) w'=w-1
s 分类器引导强度 [0,\infty) w(不同方法)
p_w CFG 实际采样的分布 \propto p(x\mid c)^{w}\left(p(x)\right)^{1-w} p(x\mid c)
drop 标签丢弃率 0.05–0.1 正则化系数(不是)
C 类别数 — —
\gamma,\beta AdaGN 的调制参数 — —

读这张表的方法:看到 w 先确认参数化;看到”空条件”先确认它是嵌入还是零向量; 看到 s 先确认是分类器引导还是 CFG。


7.31 CFG 的三个变体(本章未复现)

本章讲的是 2022 年的原始 CFG。在那之后,工业界和学术界改了三处。 下面三个方向值得知道,但本章没有在二维模型上复现它们—— 所以只描述做法和动机,不给数字。

7.31.1 ① 引导重标定(guidance rescale)

问题:w 大时,\hat x_0 的尺度会偏大——样本整体”过曝”。

做法:算出引导后的 \hat x_0,再把它缩放到和条件预测相同的标准差:

x0_cfg = (x - (1 - ab).sqrt() * eps_cfg) / ab.sqrt()
x0_c   = (x - (1 - ab).sqrt() * eps_c)   / ab.sqrt()
x0_cfg = x0_cfg * (x0_c.std() / x0_cfg.std())     # 逐样本或逐通道

动机:引导改变的不该是”画面多亮”,只该是”内容多像条件”。 缩放把前者去掉,只留后者。

它对应本章的哪个现象:w=20 时平均最大半径从 0.54 涨到 1.25—— 这就是”尺度偏大”的二维版本。

7.31.2 ② 动态阈值(dynamic thresholding)

问题:w 很大时,\hat x_0 会出现极端的饱和值(纯黑或纯白)。

做法:不固定 clamp 到 [-3,3],而是按分位数截断:

s = torch.quantile(x0.abs().flatten(), 0.99)   # 当前批次的 99% 分位
x0 = x0.clamp(-s, s) / s                        # 再归一化

动机:固定阈值在不同 w 下不合适——w 大时分布整体变宽, 固定的 \pm3 会截掉越来越多本来有效的信息。

注意:分位数是按批次算的,所以它依赖 batch 的组成。 采样时 batch 太小,这个统计量会不稳——这是它的工程代价。

7.31.3 ③ 投影式引导(APG)

问题:标准的 CFG 把 (\varepsilon_c-\varepsilon_u) 整个方向放大, 其中包含”只是把画面变亮/变暗”的那一部分,那部分不携带条件信息。

做法:把这个差值投影到与 \varepsilon_c 垂直的子空间,再放大:

\Delta_\perp=\Delta-\frac{\langle\Delta,\varepsilon_c\rangle}{\|\varepsilon_c\|^2}\varepsilon_c

动机:只放大”改变内容”的方向,不放大”改变尺度”的方向。

本章的对应现象:w=20 时分布塌成一条线(有效维度 1.009), 那条线很可能就是”尺度方向”——投影式引导要去的正是这个。

警告这三个变体的共同点

它们都在修”w 大时的副作用”,没有一个是”让 w 可以无限大”。

  • 重标定修尺度
  • 动态阈值修饱和
  • 投影修方向

但 p_w 仍然是 p(x\mid c)^{w}\left(p(x)\right)^{1-w}。 分布本身变了,这些技巧只是让”变了的分布”看起来更顺眼一点。

换句话说:它们是化妆,不是治病。 真正决定上限的,还是模型对 p(x\mid c) 的估计有多准。


7.32 十个常见问题

问一:CFG 和 prompt weighting 是什么关系?

不是一回事,但作用在同一处。

prompt weighting 改的是文本嵌入本身(把某个词的向量乘一个系数), CFG 改的是模型输出的差。前者在编码器一侧,后者在去噪器输出一侧。

但两者都能”加强某个概念”。 区别是:weighting 可以只加强一个词, CFG 只能加强整段提示词——因为它的方向是”整段文本 vs 空串”。

问二:为什么 Stable Diffusion 默认 7.5?

这是经验值,不是理论值。

它对应本章的 w=7.5(如果实现用 eps_u + w*(eps_c-eps_u))。 本章的二维模型在 w=10 就开始崩,而 SD 能用到 12—— 差别在模型质量,不在公式。

7.5 这个数应该被当成”某个模型在某个训练流程下的经验值”, 不是普适常数。

问三:w=0 和”不传提示词”一样吗?

不一样。

  • w=0:\hat\varepsilon=\varepsilon_\varnothing,只用空条件
  • 不传提示词:取决于实现,可能是空串、可能是默认提示词

前者在数学上是确定的,后者依赖工程细节。 本章的 w=0 检查验证的是前者。

问四:negative prompt 和 w<0 是一回事吗?

不是,但相关。

  • w<0:把”条件方向”取负,推离 c
  • negative prompt:改变 \varepsilon_\varnothing 的内容

两者都可以”推离某个概念”,但走的是不同的项。 本章的 w<0 实验是后者的一个简化模型。

问五:CFG 能用在非扩散模型上吗?

能,而且已经有人这么做。

CFG 的核心是”有条件输出减去无条件输出”,任何有这两个输出的生成模型都能用。 语言模型上也有类似的做法(对比解码)。

但”减去”是否有意义,取决于那个差是否对应一个 score。 在扩散模型上它有严格的概率解释;在别的模型上,它更多是一个经验技巧。

问六:为什么有些模型 w>10 也不崩?

三个原因,按重要性排:

  1. 模型误差更小——p(x\mid c) 估得准,外推的幅度就小
  2. 高维空间——“飞出流形”在 1024 维里表现不同,像素上看不出来
  3. 管线里有补偿——rescale、dynamic thresholding、或者调度器的裁剪

所以”别人能用到 12”不能直接搬到你的模型上。

问七:蒸馏模型为什么不能调 w?

因为它可能已经把引导烘进权重了。

蒸馏时学生学的是教师的最终输出,那个输出已经包含了教师用的 w。 再外挂 CFG 等于引导两次。

检测方法:w 扫描看是否平滑变化(详见场景 C)。

问八:CFG 的梯度能反传到文本嵌入吗?

能,而且这正是 prompt tuning / textual inversion 的基础。

\hat\varepsilon 对文本嵌入是可导的(通过编码器和交叉注意力)。 优化文本嵌入让生成更符合某个目标,就是沿着这条链反传。

但注意:反传穿过的是”两个模型输出的差”,梯度会比单路更大, 学习率要相应调小。

问九:训练时能用 CFG 吗?

不能直接用在训练目标里——CFG 是一个采样时的操作, 训练时你只有 \varepsilon_\theta(x_t,t,c),没有”空条件输出”这一项。

但可以间接:有些工作把”引导后的样本”作为蒸馏目标(见场景 C 的反面)。

问十:有没有”自适应 w“的方法?

有,而且方向不止一个:

  • 按 t 调度:前几步小、后面大(本章练习四)
  • 按样本自适应:根据当前预测的不确定性调整
  • 投影式:只放大与条件垂直的分量(APG)

本章只测了固定 w。 这些方法的效果依赖模型,没有通用结论。

问十一:同时要满足两个条件怎么办?

CFG 只处理”一个方向”。 两个条件有两种做法:

  • 组合条件:把两个条件拼成一个(文本上就是拼接提示词)
  • 分别引导:分别算 (\varepsilon_{c_1}-\varepsilon_\varnothing) 和 (\varepsilon_{c_2}-\varepsilon_\varnothing),再相加

后者更灵活,但要注意归一化——两个方向相加后范数会变大, 等效于用了更大的 w。先各取一半,再按总强度调。

问十二:训练时有 w 吗?

没有。 训练只优化 \varepsilon_\theta(x_t,t,c),不存在”引导强度”这个参数。

但有一处例外:如果你在做蒸馏,教师的 w 会进入学生的训练目标。 这时学生的输出已经”带引导”,再外挂 CFG 就是引导两次(见场景 C)。

提示十个问题里,哪三个最常被问错

问二、问六、问七。

它们都涉及”把别人的参数搬到自己模型上”。 记住一句话就够了:w 是模型的属性,不是任务的属性。


7.33 和其他条件控制方法的对比

CFG 不是唯一的”让模型听话”的办法。 一张表把它们放在一起。

方法 改的是什么 需要什么 强度参数
CFG 模型输出的差 标签丢弃(训练时) w
分类器引导 加上分类器梯度 噪声分类器 s
prompt weighting 文本嵌入本身 无 词级权重
negative prompt 空条件的内容 无 隐含在词表里
null-text inversion 优化出的空条件嵌入 一个优化过程 —
rescale / APG 引导后的后处理 无 沿用 w

它们不在同一个层面上:

  • CFG / CG 改的是”条件信号有多强”
  • weighting / negative 改的是”条件信号是什么”
  • null-text inversion 改的是”无条件的基准是什么”
  • rescale / APG 改的是”引导之后怎么收拾”
注记为什么可以叠加

因为它们作用在不同的项上。

一个典型组合:prompt weighting(改 \varepsilon_c)+ negative prompt(改 \varepsilon_\varnothing) + CFG(放大差)+ rescale(修尺度)。

四者互不冲突,因为它们分别动的是分子、基准、倍数和后处理。

但叠加会让调试变难——出问题时你不知道是哪一层。 建议一次只加一个,这和本章”一次只变一个量”的纪律一致。

选择顺序建议:

  1. 先确认条件模型本身好不好(w=1)
  2. 再加 CFG(默认手段)
  3. 需要”排除某物”再加 negative(强度 1/3)
  4. 需要”强调某词”再加 weighting
  5. 出现尺度问题时最后加 rescale/APG

不要反过来。 先加一堆技巧再调 CFG,你会分不清是谁在起作用。


7.34 用这一章的方法读一篇新的引导论文

每个月都会有一篇”更好的引导”。 用这五个问题快速定位它。

① 它的参数化是什么?

w 还是 w'=w-1?先在论文里找公式,再看数字。 同一个 7,两种写法差一档。

② 它的”无条件”是什么?

  • 学出来的空类嵌入?
  • 空字符串的编码?
  • 零向量?
  • 还是一个专门训练的”负面”嵌入?

这决定了它的行为能和本章的哪一部分对照。 如果是零向量, 本章实验四已经说明它会崩——先怀疑它。

③ 它在几步上做的实验?

步数和可用 w 强耦合(本章:10 步时 w=5 只有 0.612)。 一个在 100 步上调好的 w,搬到 20 步可能完全不适用。

④ 它用什么指标?

只有一个”符合度”指标吗?如果是,它的”更好”很可能只是”更锐”。 看它有没有报告多样性或分布距离。

⑤ 它有没有报告副作用?

过饱和、多样性下降、高 w 崩坏——一篇诚实的论文会写这些。

注记一个五分钟就能做的复现

拿它的默认参数,出三组图:

  • 它的默认 w
  • 默认 w 减 1
  • 默认 w 加 4

如果”减 1”明显变差、“加 4”没有变好——说明它的默认值踩在边缘上, 而且它报告的收益可能来自”更锐”而不是”更准”。

这三组图不需要读完全文就能做。


7.35 从二维走到图像:四个中间台阶

如果你要在一台机器上复现本章,不建议直接从文本图像模型开始。 按照难度上四个台阶,每一级只引入一个新变量。

7.35.1 台阶一:二维(本章)

新东西:条件、CFG、三个指标。 验证:w=1 准确率 >0.9,w=2 越界 0,w=20 越界 >30%。

这一级的目标是把”机制”和”实现”分开。 二维没有实现难度, 所以这里的失败一定是机制层面的。

7.35.2 台阶二:小图像 + 类别

换成:MNIST / Fashion-MNIST,10 类,28×28。

新东西:卷积或 ViT 主干、VAE 可选。

指标怎么换:

二维指标 小图像上的替代
标准差异常 类内 LPIPS(同一类样本两两距离)
越界率 像素裁剪率(生成值落在 [-1,1] 外的比例)
裁判准确率 在真实图上训的分类器

这一级的目标是:证明”三个指标在图像上也能用”。

7.35.3 台阶三:潜空间

换成:在 VAE 潜空间做扩散(比如 64×64 潜变量)。

新东西:VAE、scaling factor、潜空间的”越界”含义。

这一级最容易踩的坑:把像素空间的判据直接搬到潜空间。 潜变量的范数分布和像素完全不同,要重新标定。

7.35.4 台阶四:文本条件

换成:文本编码器 + 交叉注意力。

新东西:空字符串的嵌入、token 级的条件。

这一级的目标是:验证本章的”文本条件的三处不同”(预告那一节)。

提示为什么建议按台阶走

因为每一级只引入一个新变量。

直接从台阶四开始:出了问题,你要同时怀疑实现、潜空间、编码器、CFG。 按台阶走:在台阶一确认机制,台阶二确认指标,台阶三确认尺度, 台阶四只剩”文本”这一个新东西。

这和本章”一次只变一个量”的纪律是同一条。


7.36 延伸阅读

7.36.1 ① 分类器引导(2021)

Dhariwal & Nichol, Diffusion Models Beat GANs on Image Synthesis。arXiv:2105.05233。

读它能得到什么:本章”分类器引导”那一节的完整来源。 这篇论文第一次让扩散模型的图像质量超过 GAN,靠的就是引导。

读的时候注意:

  • 它的引导强度记作 s,作用在分类器对数概率的梯度上, 和本章的 s 是同一个量
  • 它需要在噪声数据上训练分类器,而且要为每个噪声水平调参—— 本章那张”t\ge600 之后准确率 0.5”的表,就是这件事的代价
  • 它的实验在 ImageNet 上,引导强度可以到几十——和本章的二维模型完全不同, 这再次说明”具体数值不能迁移”

7.36.2 ② CFG 的原始论文(2022)

Ho & Salimans, Classifier-Free Diffusion Guidance。arXiv:2207.12598。

读它能得到什么:本章的主线。只有 4 页,值得逐行读。

读的时候注意:

  • 它的核心观察就一句:条件 score 可以用”有条件 - 无条件”近似, 于是不需要分类器
  • 标签丢弃在论文里叫 “conditioning dropout”,被写成训练技巧; 本章用对照实验说明了它不是可选项
  • 论文里也有”引导强度”的讨论,但没有给出三段行为的定量描述—— 本章的二维实验补上了这一块

7.36.3 ③ Negative prompt 与”反向引导”

Stable Diffusion 的 negative prompt 在工程上很常见,但学术文献里 系统的分析不多。可以看 Ho & Salimans 论文的 §3 关于 “guiding away from a class” 的讨论,以及后续一些关于 negative prompting 会放大偏见 的分析。

读的时候注意:negative prompt 不是”反向的 prompt”。 它改变的是空类那一路上放什么,和正向不是对称的。 本章的 w<0 实验是它的最简模型。

7.36.4 ④ 引导为什么会”过饱和”

关于 guidance 的理论分析,可以看 Bradley & Nakkiran (2024)、 以及关于 “guidance 与 score 的关系” 的若干工作。它们试图回答: 为什么 w 大了之后样本会离开数据流形?

本章给的是一个一维的实证版本:越界率随 w 上升,w=20 时 37.5%。

7.36.5 ⑤ 蒸馏模型里的引导

LCM、SDXL-Turbo、DMD 这些少步模型,在蒸馏时通常把引导”烘”进权重。 读它们的论文时找 “guidance scale” 这个词,看它是训练时的固定值还是推理参数。

这决定了你外挂 CFG 时会不会”引导两次”。(见选型一节场景 C)


7.36.6 ⑥ 引导的后处理与几何解释(2023–2024)

APG(Adaptive Projected Guidance) 与 CFG++ 是近两年比较有代表性的两条路。

APG 的出发点是:(\varepsilon_c-\varepsilon_u) 里混着”改变尺度”的分量, 于是把它投影到与 \varepsilon_c 垂直的子空间再放大。 它对应本章观察到的”过引导时分布塌到一维”—— 那条线很可能就是尺度方向。

CFG++ 换了一个角度:把 CFG 看成”重新加噪”的一步, 在采样器内部调整注入噪声的强度。它改的是采样过程,不是引导公式。

读的时候注意:

  • 两篇都在修 w 大时的副作用,没有一篇声称”w 可以无限大”
  • 它们的实验都在真实图像模型上,没有二维对照—— 本章的方法(w 扫描 + 三个指标 + PCA)可以用来独立验证它们的说法
  • APG 的投影公式很简洁,但”投影到哪个方向”有多种选择, 论文里的选择未必适合你的模型

7.36.7 五篇各自没说的事

读论文时,知道它”没说什么”,比知道它”说了什么”更能防止误用。

① 分类器引导(2021)没说:分类器在高噪声处会失效。

论文里分类器是专门为 ImageNet 设计的,它的实验没有报告”分类器在各 t 上的准确率”。 本章补了这一块:t\ge600 之后准确率 0.500。 这不是论文的疏漏,是当时的关注点不同——它关心 FID,不关心引导的机制。

② CFG(2022)没说:标签丢弃是前提,不是技巧。

论文把它写成 “conditioning dropout”,和 dropout 放在一起介绍。 读者很容易把它理解成”防止过拟合的可选项”。 本章的对照实验(drop=0 时 w=5 越界 98.6%)说明它不是。

论文也没给出 w 的三段行为——它只报告了”w 大更好”的区间。

③ negative prompt:没有经典论文。

它是工程实践先流行起来的。所以没有人为”强度该取多少”给过建议, 也没有人报告”负向比正向脆”。 本章的 w<0 实验是把这个实践问题形式化的一个尝试。

④ Imagen 的 rescale(2022)没说:什么时候不该用。

rescale 修的是”尺度偏大”,但如果你的过引导问题不是尺度,而是方向, rescale 会把有用的信号一起缩掉。

本章的对应观察:w=20 时既有尺度问题(半径 1.25)也有塌缩(有效维度 1.044)。 rescale 只修前者。

⑤ 蒸馏论文没说:外挂 CFG 会双重引导。

它们通常只报告”学生模型在几步下的质量”, 没有讨论”用户如果再加 guidance_scale 会怎样”。

本章的场景 C 给了检测方法:w 扫描看是否平滑变化。

注记这张”没说什么”的清单怎么用

读一篇论文时,问自己:它解决的问题和我遇到的问题,是不是同一个?

  • 它关心 FID,我关心”用户能不能调” → 它的结论可能不适用
  • 它在 100 步上测,我用 15 步 → 它的 w 不可搬
  • 它没报副作用 → 我需要自己测

“论文没写”不等于”论文错了”,但它意味着”这一块要你自己补”。

7.36.8 读这几篇的顺序

如果只有两个小时:读 ② 的正文(4 页),跳过实验部分。 它会给你 CFG 的完整动机和公式,也仅止于此——三段行为、标签丢弃的必要性、 与分类器引导的定量对比,都不在里面。

如果有半天:按 ② → ① → Imagen(④ 里提到的那篇)的顺序。

  • ② 给你”是什么”
  • ① 给你”为什么需要引导”(它的动机是”扩散模型打不过 GAN”)
  • Imagen 给你”工业界怎么修 w 大的副作用”(rescale 与 dynamic thresholding)

读 ① 时看两处:引导公式(§2)和分类器怎么在噪声上训练(§4)。 它的实验部分可以跳——ImageNet 上的 FID 数字和你的场景没关系。

读 Imagen 时只读 §2.2(guidance 那一节)。 它的贡献是”怎么让大 w 能用”,不是”怎么引导”—— 引导公式它直接引用了 ①。

③ 没有经典论文。 negative prompt 是工程实践先流行起来的, 学术分析散落在若干篇关于”引导放大偏见”的工作里。别指望找到一篇”negative prompt 论文”。

⑤ 的读法:在蒸馏类论文里搜 “guidance”, 看它出现在训练目标里还是推理参数里。这决定了你能不能外挂 CFG。


7.37 预告:文本条件下的 CFG 会变成什么样

本章的条件是一个离散标签。第 8 章的条件是一串文本 token。 公式一个字不用改,但有三处会变得不一样。

① 空条件来自编码器。

空类是 nn.Embedding 里学到的一行;空条件是 空字符串经过文本编码器的输出——一个非零向量。

这意味着”无条件”的质量取决于编码器。本章实验四已经演示: 换成零向量,越界率从 0 变成 100%。

② 引导作用在整串文本上,不是单个 token。

\varepsilon_c-\varepsilon_\varnothing 是”整段提示词”带来的方向。 你没法直接说”加强’红色’这个词”—— 只能靠调整提示词本身(比如重复、加权语法)。

这也是为什么第 8 章要讲交叉注意力: 只有看到每个 token 对图像的贡献,才知道引导到底在放大什么。

③ 文本的”类别边界”比标签模糊。

类别 0 和类别 1 有明确的划分;“一只猫”和”一只狗”在嵌入空间里 有重叠,而且受上下文影响。

推论:文本模型能承受的 w 通常比本章高(5–8 vs 2), 但过引导的表现更隐蔽——不是飞出范围,是”每个词都被过度强调”, 画面变得油腻、元素堆砌。

注记一个可以带到第 8 章的问题

如果 w 放大的是”整段文本”的方向,那么一段长提示词里, 每个词的贡献是被同比例放大的吗?

不是。 引导放大的方向由 \varepsilon_c-\varepsilon_\varnothing 决定, 这个方向在嵌入空间里更接近那些”对输出影响大”的词。

所以长提示词里,关键词会被放大得更多,虚词几乎不变。 这是一个可检验的预测——第 8 章的注意力图能验证它。


7.38 本章没有回答的问题

7.38.1 问题一:为什么真实模型能承受 w=7.5 而本章的模型在 20 就崩?

本章只测了二维、5 万参数的模型。 真实模型的误差小得多, 但”误差小”能不能解释从 20 到 7.5 的差距,本章没有验证。

可能的实验:在同一个二维问题上,用不同训练步数得到不同误差的模型, 测每个模型的”崩溃 w“。如果崩溃 w 随误差单调上升,就能画出这条曲线。

7.38.2 问题二:引导强度与多样性之间有没有定量关系?

本章有数据点(w 从 1 到 10,内月标准差从 0.336 掉到 0.107), 但没有拟合出一个式子。

可能的实验:在多个数据集上重复这个扫描,看”标准差随 w 的下降” 是不是一个通用的函数形式。如果是,就能给出”给定可接受的多样性损失, 最大 w 是多少”的公式。

7.38.3 问题三:负引导的最优强度为什么是正的 1/3?

本章只给了经验建议(1/2 到 1/4),没有推导,也没有系统扫描。

可能的实验:对负引导做和正引导一样的 w 扫描, 找出”目标类准确率”和”越界率”的平衡点,看它和正引导的平衡点是什么比例。

7.38.4 问题四:文本条件的 CFG 和类别条件有什么本质区别?

本章的条件是一个离散标签(2 类)。 第 8 章的文本条件是高维连续向量, 空类对应的是空字符串。

区别在于:文本的”无条件”不是一个训练出来的嵌入, 而是编码器对空串的输出。这个差别会不会改变 w 的行为?本章没有答案。

7.38.5 问题五:分类器引导在什么情况下仍然值得用?

本章的结论是”CFG 取代了它”,但那只在”你有一个条件模型”的前提下成立。

如果你只有一个无条件模型,又想要条件生成,分类器引导仍然是唯一的现成办法。 它在什么条件下还能打,本章没有测。

7.38.6 问题六:CFG 能不能”只引导一部分”?

本章的 w 是全局的。 但一个自然的想法是:只放大某个方向上的分量 (比如只放大”形状”不放大”颜色”)。

APG 的投影是一种做法,但它依赖”投影到哪个方向”的选择。 这个选择有没有原则性的依据,本章没有答案。

可能的实验:在二维上构造”形状/颜色”两个可分的条件, 分别引导,看是否能独立控制。

7.38.7 问题七:多步之间的引导强度应该怎么变?

本章只测了固定 w。 练习四提了 w(t) 调度的想法, 但没有实测。

可能的实验:在 10 步和 50 步下,分别测”前段 w 小、后段 w 大” 与”固定 w“的差异。如果前段引导是破坏性的,这个改进应该是免费的。

提示为什么把没做的写出来

因为”CFG 好用”这句话,掩盖了一堆边界条件。

本章的实测支持的是:在这个二维模型上,w 有三段行为, 标签丢弃是前提,准确率会骗人。 它不支持:“w 应该取 7.5”、“CFG 一定比分类器引导好”、“负引导随便调”。

把边界写出来,你才知道哪些能直接用。


7.39 每一节的边界

每一条结论都有前提。 这一节把它们列出来——用之前先看前提是否成立。

结论 前提 什么时候失效
w 的三段行为 模型在 w=1 已基本正确(0.965) 模型差时,整条曲线左移,“正常区”消失
负引导更脆 空条件有效 空条件坏时,负向和正向一样不可用
标签丢弃是前提 条件能被”丢掉” 文本条件下丢的是空串,不是空类
分类器引导可比 有噪声分类器 没有分类器时无法比较
分类器的失效曲线 特定的分类器架构 换架构,t\ge600 这个位置会变
有效维度 1.009 二维 PCA 高维需要换度量(如参与率仍可用,但解释不同)
成本 2.14 倍 5 万参数模型 大模型上反向传播占比不同
两路合并省 44.5% GPU、batch 1000 小 batch 或 CPU 上收益更小

最需要警惕的是第一行。

本章的模型在 w=1 时准确率 0.965——它已经基本学会了条件。 三段行为是”在一个已经学会的模型上,把条件信号放大”的行为。

如果模型在 w=1 只有 0.7,你看到的不是三段,是一段: 从 0.7 开始,无论怎么调 w 都上不去,只是越来越崩。

重要一个判断”我的模型够不够格”的快速方法

看 w=1 到 w=2 的增益。

  • 有明显增益(如 0.965 → 0.998)→ 模型学会了条件,引导有用
  • 几乎没增益(如 0.70 → 0.71)→ 模型没学会,先修模型
  • 增益为负 → 模型在 w=1 就不对,任何引导都救不了

本章的增益是 +3.3 个点;这个数在你的模型上应该是多少, 取决于你的任务难度——但”有增益”是必要条件。


7.40 如果只记三件事

这一章有 28,000 字,但真正要带走的只有三条。

① CFG 是锐化,不是靠近。

\hat\varepsilon=\varepsilon_u+w(\varepsilon_c-\varepsilon_u),\qquad p_w\propto p(x\mid c)^{w}\left(p(x)\right)^{1-w}

w=1 停在数据上,w>1 越过数据继续往外推。 手算的例子:真实数据在 -1,w=2 时等效均值 -2.2。

“更听话”和”更偏离”是同一个操作的两个说法。

② 准确率会在最需要它的时候饱和。

w=3 之后准确率就是 1.000,w=20 时它还是 1.000—— 而那时 37.5% 的样本已经飞出数据范围,有效维度塌到 1.009。

必须同时看一个分布指标。 本章用越界率 + PCA,图像上用 同提示词相似度 + 解码后统计量。

③ 空条件必须被训练过,而且不能是零。

标签丢弃不是正则化,是 CFG 的语义前提。 零向量当空条件:越界率 100%、多样性 0,而准确率仍是 1.000。

换编码器、换 tokenizer、换空串写法,都要重新验证。

重要三条之间的关系

第一条是”它是什么”,第二条是”怎么判断它做得对不对”, 第三条是”它成立的前提”。

只有第一条:你会把 w 调到 40,然后得到一堆废点。 只有第二条:你知道指标在骗人,但不知道为什么。 只有第三条:你避免了崩溃,但不知道正常时该长什么样。

三条一起,才是一个完整的理解。


7.41 这一章的方法论

为什么这一章是”手算 + 二维实验 + 失败清单”这个结构?

因为 CFG 有三个层面的知识,它们需要三种不同的验证方式:

层面 例子 怎么验证
代数 \hat\varepsilon=\varepsilon_u+w(\varepsilon_c-\varepsilon_u) 手算一个例子
分布 p_w\propto p(x\mid c)^{w}\left(p(x)\right)^{1-w} 数值积分 / 密度比
经验 w 的三段行为 受控实验

只做代数,你会得到一堆恒等式,但不知道 w=20 会怎样。 只做实验,你会得到一堆曲线,但解释不了为什么。 只做分布分析,你会知道形状,但不知道实现要怎么写。

7.41.1 为什么先手算

手算例子的作用不是”验证公式”,是”建立量级感”。

在算之前,“w=2 会推多远”是一个抽象问题。 算完之后,你有一个具体的数:等效均值从 -1 变成 -2.2。

这个数会一直跟着你——以后看到任何”引导强度”的参数, 你都会本能地问:“它的单位是什么?推多远算过头?”

7.41.2 为什么用二维

因为二维能同时提供三样东西(和第 6 章的理由类似,但这里更极端):

  1. 可画:w=20 时那条冲出去的线,一眼就能看见
  2. 可算:PCA、越界率、密度比,都有定义
  3. 可穷举:10 个 w × 2 个类 × 5 个种子,几分钟跑完

在图像上,这三样都没有。 你只能看”像不像”。

7.41.3 为什么保留失败清单

因为 CFG 的实现错误有一个特点:它们大多不报错。

空类编号错、参数化差 1、丢弃没加——程序都正常运行,输出都”看起来像图”。 唯一的发现方式是知道该看什么。

这一章的六个失败,就是”该看什么”的清单。

提示这套方法可以搬到别的地方

任何”两个模型输出相减再放大”的技巧,都可以用同样的方式分析:

  • 先手算一个一维例子(量级感)
  • 再在小规模数据上扫参数(三段行为)
  • 最后列出”不报错但出错”的清单

第 6 章的采样器是这样,本章的 CFG 是这样, 第 8 章的交叉注意力大概率也会是这样。


7.42 三次真实的返工

这一章不是一次写成的。 有三次,我以为已经写完,然后发现自己错了。

7.42.1 第一次:零向量当空条件

症状:token 条件模型的实验里,w=3 时 100% 的样本越界, w\ge5 时多样性归零。而准确率是 1.000。

误判:一开始我以为是 w 太大。把 w 降到 1.5,越界率仍在 80% 以上。

定位:把”无条件”分支的输入打印出来——是一个全零张量。 而训练时模型见过的”空”是 emb[vocab],一个非零向量。

修正:用学出来的 NULL token。越界率立刻回到 0。

教训:“没有条件”在数学上不是一个零向量。 它是一个被训练过的、表示’没有条件’的条件。

后来我把这个错误原样写进了正文——因为它精确对应文本模型的情况。

7.42.2 第二次:跳步 DDPM 的系数写反

症状:步数实验全 NaN,但准确率显示 1.000。

误判:先怀疑是 w=20 太大导致溢出,单独跑 w=1 也一样。

定位:打印 beta_eff,发现是负数。

修正:把 \alpha_{\text{eff}}=\bar\alpha_{t'}/\bar\alpha_t 改成 \bar\alpha_t/\bar\alpha_{t'}。步数实验立刻正常。

教训:argmax 对 NaN 会给出确定答案。 在任何指标之前,先检查 isfinite。

7.42.3 第三次:同一节写了两遍

症状:清点章节时发现”三个真实场景”出现了两次—— 一次是简版,一次是展开版。

原因:插入展开版时,我用”### 三个真实场景”当锚点, 把新内容插在了它前面,而不是替换它。

修正:删掉简版,保留展开版。

教训:插入式编辑要检查锚点前后有没有重复。 这也是为什么我后来给 lint.py 加了公式检查—— 能自动查的东西,不要靠记性。

提示三次返工的共同点

它们都不是”想错了”,是”没检查”。

  • 第一次:没检查无条件分支的输入
  • 第二次:没检查中间量的有限性
  • 第三次:没检查插入后有没有重复

而三次都发生在”结果看起来正常”的时候—— 准确率 1.000、loss 在降、章节能渲染。

“看起来正常”是最危险的信号。


7.43 结语:CFG 教给我们的事

CFG 的形式极简:两个输出相减,乘一个系数,加回去。

但它的行为不简单。 同一个式子,w=1 时是精确的条件采样, w=2 时开始偏离数据,w=40 时把分布压成一条线。 中间没有警告,没有报错,准确率一直是 1.000。

这件事本身就值得记住:

一个系统的”正确性指标”和”健康程度”可以完全脱钩。

7.43.1 一个可迁移的模式

“有条件输出 - 无条件输出”这个差,在别的地方也出现过:

  • 语言模型的对比解码(contrastive decoding)
  • 某些对齐方法里的”拒绝方向”
  • 图像编辑里的”方向编辑”

它们都在做同一件事:找到一个方向,然后放大它。

而本章的所有教训都可以平移:

  1. 方向要有意义——空条件必须是训练过的(否则你放大的是随机方向)
  2. 放大有代价——p_w 不再等于后验(否则你以为在采样,其实在锐化)
  3. 指标会骗人——准确率看不见分布形状(否则你会把 w 调到 40)

7.43.2 最后一句

这一章讲的是”怎么让模型听话”。

但更重要的收获可能是: 当一个方法简单到只有一行公式时,它的边界条件往往藏在实现细节里。

CFG 的一行公式,对应六个不报错的坑。 这六个坑,比那一行公式更值得记住。


7.44 五个”不要”

这一节是给赶时间的人看的。 五条禁令,每条背后都有一个本章的实测。

① 不要用单一指标选 w。

准确率在 w=3 就饱和,之后再怎么调都不变, 而样本在 w=20 已经飞出数据范围。 至少加一个分布指标。

② 不要在模型没训好时调 w。

w=1 准确率 0.7 的模型,调 w 只会让它更崩。 先修模型(本章:“开 CFG 之前”的五条清单)。

③ 不要把 w 的数值跨模型搬。

本章 w=20 崩,SD 用到 12 也不崩。 能搬的是三段结构,不是分界点。

④ 不要把空条件当零向量。

零向量当空条件:越界 100%、多样性 0,准确率 1.000。 空条件必须是被训练过的表示。

⑤ 不要在没有 w=1 检查的情况下改采样代码。

参数化差 1、空类编号错——这两类错误都不会报错, 只有逐位比较能发现。

警告五条的共同点

它们都是”跳过验证、直接调参”。

调参看起来很快——改一个数、出一张图、看着不错。 但每一次”看起来不错”,都可能是准确率 1.000 的假象。

本章花了 30,000 字说明一件事: 先验证结构,再调参数。


7.45 调试工具:三个打印语句

不用装任何工具,三行 print 就能定位大部分问题。

7.45.1 第一行:空类编号

print("null id =", model.n_class, "| embedding rows =", model.cemb.num_embeddings)

应该看到:null id 等于 rows - 1。 不相等,说明你的空类和最后一个真实类用了同一个下标。

7.45.2 第二行:三个 \varepsilon 的范数

print(f"|eps_c|={eps_c.norm():.2f} |eps_u|={eps_u.norm():.2f} "
      f"|eps|={eps.norm():.2f} w={w}")

正常的形状:

  • w=1:|eps| == |eps_c|(引导不生效)
  • w=2:|eps| ≈ |eps_u| + 2·|eps_c - eps_u|
  • 如果 |eps_u| 远大于 |eps_c|,说明空条件主导了输出——可能编号错了

这一行在 t 大的时候尤其有用:高噪声处 \varepsilon 的范数应该接近 \sqrt{d}。

7.45.3 第三行:采样过程中的越界率

if t % 100 == 0:
    print(f"t={t} max|x|={x.abs().max():.2f} out={(x.abs().max(1).values>1.5).float().mean():.3f}")

正常的形状:越界率在采样后段应该下降(样本收敛到数据范围)。 如果它在某一步开始上升,那一步就是问题开始的地方。

提示三行 print 覆盖了什么
打印 能发现
空类编号 失败三
\varepsilon 范数 失败一、三、参数化
逐步越界率 失败五(采样器)、过引导

加上 w=1 的逐位检查,本章六个失败里的五个都能定位。

不需要 tensorboard,不需要 wandb。


7.46 常见误用清单(按后果排序)

按”造成多大破坏”排,不按”多常见”排。

# 误用 后果 本章的数
1 空条件用零向量 样本全废 越界 100%,多样性 0
2 无标签丢弃就开 CFG w>1 全崩 越界 98.6%/100%
3 用准确率选 w 选到崩溃区 w=20 准确率 1.000,越界 37.5%
4 跨模型搬 w 过引导或欠引导 本章 20 崩,SD 12 不崩
5 少步模型外挂 CFG 双重引导 —
6 负向与正向同强度 发灰、结构缺失 w=-2 越界 31%/65%
7 只在一个步数上测 换步数就崩 10 步 w=5 仅 0.612
重要前两名和后面五名不是一个量级

第 1、2 名是”结构性错误”——不管 w 取多少都不工作, 而且准确率可能显示 1.000。

第 3–7 名是”参数错误”——能工作,只是不够好。

排查时先排除前两个: 打印空类编号、检查训练时的丢弃。这两件事加起来不到十分钟。

一个反过来的观察:这七条里,有五条的”症状”在指标上看起来是好的 (准确率高、loss 在降、图能出)。

这就是这一章反复强调”至少三个指标”的原因。 一个指标只能告诉你”某一面还行”,不能告诉你”整体没问题”。


7.47 结课自测:十个判断

先自己判对错,再看答案。答错三个以上,回去重读对应小节。

1. CFG 让样本更接近条件。 错。 它沿着条件方向外推。w=2 时等效均值从 -1 变成 -2.2,越过了数据。

2. w=1 就是普通条件采样。 对。 公式在 w=1 时精确退化成 \varepsilon_c。这也是最重要的验收检查。

3. 标签丢弃是一种正则化。 错。 它是 CFG 的语义前提。去掉它,w=5 时 98.6% 的样本越界, 而 w=1 的性能几乎不变(0.960 vs 0.958)。

4. 准确率越高说明引导越好。 错。 准确率在 w=3 饱和,w=20 时仍是 1.000,而那时 37.5% 的样本已经飞出数据范围。

5. 负引导和 negative prompt 是一回事。 错,但相关。 负引导改的是”条件方向的符号”; negative prompt 改的是”空条件的内容”。两者都推离某物,但走的是不同的项。

6. 空条件可以用零向量。 错。 零向量是模型没见过的输入。实测:越界 100%,多样性 0。

7. 分类器完美时,分类器引导与 CFG 等价,且 w=1+s。 对。 两者只差”用什么估计分类器梯度”。

8. w 的最优值可以跨模型搬。 错。 本章 w=20 崩,SD 用到 12 也不崩。能搬的是三段结构,不是分界点。

9. 步数越少,可用的 w 越宽。 错,正好相反。 10 步时 w=5 只有 0.612,50 步时是 1.000。

10. 过引导的样本仍然会被分类器认对。 对。 这正是”准确率会骗人”的根源——飞出数据范围的点,类别标签可能还是对的。

提示如果你答对了 8 个以上

说明你已经掌握了这一章的骨架。

剩下的不是”记住更多”,是”动手验证”—— 挑练习二(找你自己模型的崩溃 w),那个数是你以后设默认值的依据。


7.48 一张速查卡

打印出来贴在显示器旁边的那种。

7.48.1 公式

\hat\varepsilon=\varepsilon_\varnothing+w\left(\varepsilon_c-\varepsilon_\varnothing\right), \qquad p_w\propto p(x\mid c)^{w}\left(p(x)\right)^{1-w}

w 含义
0 无条件
1 普通条件采样
>1 引导(锐化)
<0 负引导

7.48.2 三段行为(本章实测)

区间 名称 准确率 其他
0\le w<1 欠引导 0.52 → 0.85 —
1\le w\le10 正常 0.965 → 1.000 多样性 0.31 → 0.11
w\ge20 过引导 1.000 越界 39.7% / 9.3%

7.48.3 三条验收

检查 抓什么
w=1 逐位等于条件采样 参数化
w=0 准确率 \approx 1/C 空类编号
越界率随 w 单调不降 “CFG 没接上”

7.48.4 六个坑

# 坑 后果
1 空类没训过(drop=0) 越界 98.6%
2 参数化差 1 强度差一档
3 空类编号错 偏某一类
4 只看准确率 选到 w=40
5 采样器不稳 误怪引导
6 负引导过强 越界 31% / 65%

7.48.5 五个数

  • 模型 50,434 参数;裁判 0.9978
  • w=2 → 准确率 0.998,越界 0
  • w=40 → PC1 0.996,有效维度 1.009
  • 10 步 w=5 → 0.612(50 步 1.000)
  • 分类器引导单步成本 = CFG 的 2.14 倍

如果只能记一行:w=1 逐位检查。 它最便宜,也最能排除掉一整类错误。


7.49 如果要继续往下走

这一章讲的是”图像 + 类别”的 CFG。往下有三个方向。

7.49.1 方向一:别的模态

同样的公式,换一个”条件”和”数据”:

  • 视频:条件仍是文本,但 x_t 是时空张量;引导要处理”时间一致性”
  • 3D:条件是视角或文本,x_t 是隐式场或高斯点
  • 音频:条件是文本或参考音频,x_t 是频谱

公式不变,但”越界”的含义和评估指标全要重定。 本章的”从二维走到图像的四个台阶”可以直接套用。

7.49.2 方向二:别的训练范式

流匹配 / 整流流的引导。 它们也有”条件速度场 - 无条件速度场”, 理论上可以套用同样的差。但流匹配的路径是直的, 引导对路径曲率的影响可能不同——这一点本章没有数据。

7.49.3 方向三:理论

为什么锐化会提高”符合度”? 本章用 p_w 解释了”分布变尖”, 但没有解释”为什么变尖会让人类觉得更符合提示词”。

一个可能的解释:人类的”符合”判断对典型性敏感—— 越靠近类中心的样本越”像那个东西”。这是认知层面的假设,本章无法验证。

注记一个诚实的边界

本章解释了 CFG 的机制,但没有解释它的”有效性”。

机制:为什么 w 会让分布变尖、为什么会崩、什么时候该停。 有效性:为什么人会觉得 w=2 的图”更好”。

后者涉及感知和认知,不在本书范围内。 但它是真实存在的问题——如果换一个评价者(比如另一个模型),最优 w 可能会变。


7.50 把这一章讲给别人听

同一份内容,三种时长。

7.50.1 五分钟版

三句话:

  1. CFG 是把”有条件输出 - 无条件输出”这个差放大 w 倍再加回去。
  2. w=1 是普通条件采样,w>1 会越过数据继续往外推。
  3. 准确率在 w=3 就饱和,之后它不再告诉你任何事。

如果对方只问一个问题,多半是”那 w 该取多少”。 答:取”准确率刚饱和、多样性还没明显掉”的那个点。本章是 2,真实模型通常 5–8。

7.50.2 三十分钟版

在五分钟版之上加两块:

① 手算的例子。 真实数据在 -1,w=2 时等效均值 -2.2。 这一个数就能让人明白”不是靠近,是外推”。

② 三段行为。 欠引导 / 正常 / 过引导,每段配一个数。

这一段的作用是让人知道”存在一个危险区”,而不只是”有个参数”。

7.50.3 两小时版

完整讲,但顺序调整过:

  1. 先讲失败(无标签丢弃 → 越界 98.6%)
  2. 再讲公式(此时听众已经知道”为什么需要这个公式”)
  3. 然后手算(建立量级感)
  4. 最后三段行为与三个指标

先失败后公式,比”先公式后例子”更容易让人记住—— 因为失败制造了一个”为什么”的问题。

提示讲的时候最容易被追问的三个问题

① “为什么不能直接让模型更听话?” 因为它只会”更像某个类”,不会”更符合你的意图”。 引导是一个方向,不是一个目标。

② “为什么 w 大了画面会坏?” 因为 p_w=p(x\mid c)^{w}\left(p(x)\right)^{1-w} 不是后验,是被重新加权过的分布。 你在采样一个被改过的分布。

③ “有没有办法既要多样又要听话?” 有,但不能靠调 w。 要靠更好的模型、或者换条件表示 (比如文本里说得更具体)。w 只能在”多样”和”符合”之间移动,不能同时提高两者。


7.51 三个数学性质

这三个性质解释了”为什么 CFG 看起来这么自然”。

7.51.1 性质一:w=1 时精确

\varepsilon_u+1\cdot(\varepsilon_c-\varepsilon_u)=\varepsilon_c

这不是近似,是恒等。 它意味着 CFG 是条件采样的严格推广, 不是”另一个方法”。

实践含义:任何声称”用了 CFG”的实现,在 w=1 时必须与普通条件采样逐位相同。 不同就是错的。

7.51.2 性质二:对 \varepsilon 的尺度是等变的

如果模型输出整体乘一个常数 a(比如换了一个尺度约定), CFG 的结果也乘 a:

\hat\varepsilon(a\varepsilon_c,a\varepsilon_u)=a\,\hat\varepsilon(\varepsilon_c,\varepsilon_u)

因为公式对两个输入都是线性的。

实践含义:如果换了模型、\varepsilon 的尺度变了, w 的”有效强度”也会跟着变。这就是”w 不能跨模型搬”的一个数学解释。

7.51.3 性质三:对条件不是线性的

如果 \varepsilon_c 随 c 非线性变化(真实模型都是), CFG 的输出对 c 也不是线性的。

这意味着”两个条件的引导可以相加”只在近似意义下成立(见问十一)。 严格做法是分别算,再检查范数。

注记为什么讲这三个

因为它们划出了 CFG 的”代数边界”。

  • 性质一告诉你什么是错的(w=1 不等就是错的)
  • 性质二告诉你什么不能搬(尺度不同的模型之间)
  • 性质三告诉你什么只能近似(多条件组合)

剩下的行为,都要靠实验。


7.52 五个”如果”

现实里总有些约束让标准做法不适用。 五条应对。

如果只能跑 5 步? 把 w 压到 1.5–2。 本章实测 10 步时 w=5 只有 0.612。 少步时引导的收益抵不过它带来的离散误差。

如果条件不是离别的? 照样能用,但”空条件”要重新定义。 连续条件(比如温度、亮度)没有”空”这个概念, 通常用”条件取某个中性值”代替——这个中性值必须是模型训练时见过的。

如果没有空标签可丢? 训练时手工构造一个。 比如把条件向量乘 0(但要作为训练分布的一部分, 不是推理时临时拼)。本章的教训是:空条件必须在训练中出现过。

如果模型已经蒸馏过? 先做 w 扫描的连续性检测(场景 C)。如果 w 一调就崩,保持 w=1。

如果只有 CPU? 减步数、减 batch,但别减 w 的检查。 本章的三个恒等检查不依赖算力, 它们是”十分钟能做完”的那一类。

提示这五个”如果”的共同答案

先确认”哪一部分不能动”,再调剩下的。

  • 步数不能动 → 动 w
  • 条件不能动 → 动空条件的表示
  • 模型不能动 → 动期望(接受质量下降)

唯一不能省的是验证:w=1 的逐位检查、w=0 的无条件检查。 这两条在任何算力下都只要几分钟。


7.53 写在这一章的最后

这一章从一个不会听话的模型开始,结束于一张速查卡。

中间有三十多张表、两张图、一组交互实验、六个失败、三次返工。 但它们都在回答同一个问题:

当你把”条件”变成一个可以拧的旋钮时,你到底在拧什么?

答案是:你在改变采样的分布,不是改变模型的能力。

这句话有两个推论,值得带走:

① 引导不能创造模型没有学到的东西。 模型对某个类一无所知,w 再大也只是把”不知道”放大。

② 引导的代价总是从别处来。 准确率从多样性来,符合度从覆盖来,锐度从真实感来。 没有哪一项是免费的。

重要如果这一章只能留一句话

“w 是模型的属性,不是任务的属性。”

换模型,重新标定;换步数,重新标定;换编码器,重新标定。

唯一不变的是那三步:验证 w=1、验证 w=0、看三个指标。


7.54 如果你只有一小时

这一章约四万字。时间不够时,按这个顺序读。

时间 读什么 得到什么
0–15 分 “CFG 的推导”整节 公式从哪来,w 的三个端点
15–25 分 图 7-1 + “实测:w 的三段行为” 三段结构的实测形状
25–40 分 “它在哪里失效”的六个失败 不报错但出错的六种方式
40–50 分 实验 7-1(拖 w) 亲手看见 (w=20) 的塌缩
50–60 分 速查卡 + 结课自测 带走一页纸和十个判断

可以跳过的:分类器引导的历史、三个变体、延伸阅读、以及大部分实现细节。

不要跳过的:w=1 与 w=0 的检查、以及”准确率会饱和”那一节。 这两块决定了你会不会踩坑。


7.55 一句话

CFG 是一个放大器:它放大”条件带来的差别”,代价是分布被改形。

把它拆开看:

  • 放大:w 乘在 (\varepsilon_c-\varepsilon_u) 上,不是乘在整个输出上
  • 条件带来的差别:那一项就是隐式分类器的方向
  • 分布被改形:p_w=p(x\mid c)^{w}\left(p(x)\right)^{1-w},不是后验

三句话对应三个坑:

  • 不懂”乘在哪” → 参数化差 1
  • 不懂”差别从哪来” → 空类编号错、没做丢弃
  • 不懂”分布被改形” → 只看准确率,选到崩溃区

所以这一章的每一个实验,本质上都在测这三件事之一。


7.56 本章回顾

把这一章压缩成六句话。

一、条件生成的核心是一个恒等式。 \nabla\log p(x\mid c)=\nabla\log p(x)+\nabla\log p(c\mid x)。 分类器引导用第二项,CFG 用两个 score 的差近似它。

二、CFG 只有一行公式。 \hat\varepsilon=\varepsilon_\varnothing+w(\varepsilon_c-\varepsilon_\varnothing)。 w=0 是无条件,w=1 是普通条件采样,w>1 才是”引导”。

三、w 有三段行为。 w<1 欠引导(准确率 0.52→0.85),1\le w\le10 正常(准确率到 1.000, 多样性从 0.336 掉到 0.107),w\ge20 过引导(37.5% 的样本越界)。

四、准确率会在最需要它的时候饱和。 w=3 之后准确率就是 1.000,w=20 时它还是 1.000—— 而那时样本已经飞出数据范围了。

五、标签丢弃是 CFG 的前提,不是正则化。 去掉它,w=2 就有 6%/22% 的样本越界,w=5 时是 98.6%/100%。 空类嵌入必须被训练过,否则你减掉的是一个随机向量。

六、分类器引导输在”多了一个会失效的部件”。 它的分类器在 t\ge600 之后准确率就是 0.500, 而 \sqrt{1-\bar\alpha_t} 在这些时刻最大—— 最不可靠的梯度被乘上了最大的系数。

七、“能用的 w”有多宽,取决于步数和丢弃率。 10 步时 w=5 的准确率只有 0.612,50 步时是 1.000; 丢弃率 0 会让 w>1 直接崩,0.3 会削弱条件本身,0.05–0.1 是宽平台。 引导强度不是一个孤立参数。

7.56.1 七句话的展开

一、条件 score 的恒等式只用贝叶斯一步:取对数、对 x 求梯度, p(c) 与 x 无关所以消失。三项分别是:无条件 score、分类器梯度、条件 score。 分类器引导取后两项,CFG 用模型自己的输出去近似那个梯度。

二、CFG 的那一行公式有三个端点:w=0 无条件、w=1 普通条件、w>1 引导。 注意 w=1 那一端——它说明 CFG 不是”新技巧”,是条件采样的推广。

三、三段行为的分界不是拍出来的:w<1 准确率 0.52→0.85, 1\le w\le10 到 1.000 且多样性从 0.336 掉到 0.107, w\ge20 越界率 39.7%/9.3%。每一段都有对应的机制(欠推、锐化、塌缩)。

四、准确率饱和发生在 w=3,而副作用在 w=10 才开始。 中间这七个 w 档位,是”看起来在变好、其实只在变窄”的陷阱区。

五、标签丢弃去掉之后,w=2 越界 6%/22%、w=5 越界 98.6%/100%。 它只影响 w>1——w=1 时两个模型分别是 0.960 和 0.958,几乎没有差别。 这个”只影响一边”的模式,正是”空类没被用到”的指纹。

六、分类器引导的失效不在低噪声,在高噪声:t\ge600 后准确率 0.500。 而 \sqrt{1-\bar\alpha_t} 在那个区间最大——最不可靠的梯度被乘上最大的系数。

七、步数与引导的耦合:w=5 在 10 步时准确率 0.612、50 步时 1.000。 “能用的 w”不是一个常数,是步数的函数。

如果把这一章压成一张表:

问题 答案 本章的数
引导到底在干什么 锐化后验:p_w\propto p(x\mid c)^{w}\left(p(x)\right)^{1-w} w=1\to10,中心/边缘密度比涨 1000 倍
w 该取多少 1\le w\le10,且随步数收窄 10 步时 w=5 只剩 0.612
为什么准确率会骗人 它是 0/1 阈值指标 w=20 准确率 1.000,越界 37.5%
空类怎么来 训练时的标签丢弃 无丢弃 w=5 越界 98.6%
空条件长什么样 学出来的嵌入,不是零向量 零向量:越界 100%,多样性 0
分类器引导差在哪 多一个会失效的部件 分类器 t\ge600 后准确率 0.500
两种引导什么关系 完美分类器下 w=1+s s=0 ↔︎ w=1,都是 0.965
过引导的几何 塌到低维 + 向外漂移 w=40:PC1 0.996,半径 2.25

这张表的每一行,都能在本章找到一张对应的原始数据表。

如果只带走一句:w 是模型的属性,不是任务的属性。 同一个任务换一个模型,最优 w 会变;同一个模型换一个任务,最优 w 也可能变。 唯一不变的是三段结构和”用边际收益做决定”的方法。

没有一行的数字是估算的。

最后留一个问题

本章的二维模型在 w=20 时,外月的样本塌成了一条向上的直线, 一直冲出画面。准确率还是 1.000。

为什么是”一条线”而不是”一团云”?

一个可能的回答:引导把”条件方向”放大了 20 倍, 其他方向被相对压扁了。当放大倍数大到一定程度, 分布的方差在条件方向上远大于其他方向, 看起来就像一维的。

如果这个解释对,那么”过引导”的本质不是”样本变远”, 而是”分布塌到一个低维子空间上”。

这个实验做了。 对每个 w 下的 1000 个样本做 PCA:

w PC1 方差占比 有效维度 平均最大半径
0 0.809 1.448 0.540
1 0.838 1.374 0.525
2 0.877 1.276 0.570
3 0.900 1.219 0.582
5 0.926 1.158 0.582
10 0.906 1.205 0.568
20 0.979 1.044 1.252
40 0.996 1.009 2.245

(有效维度 = (\sum\lambda)^2/\sum\lambda^2,二维数据的上限是 2。)

假设成立。 w=40 时 PC1 解释了 99.6% 的方差,有效维度 1.009—— 分布几乎完全塌到一条线上,同时平均半径从 0.54 涨到 2.25。

注意 w\le10 那一段:有效维度从 1.448 缓慢降到 1.158。 这是正常的”去掉错误样本”造成的收缩。真正的塌缩发生在 10 到 20 之间—— 和越界率的突变是同一个位置。

所以”过引导”有两个同时发生的症状:分布塌到低维 + 整体向外漂移。 越界率只是后一个症状的粗糙代理,PCA 才能看见前一个。


这一章的数据在 data/ch7_case.json、ch7_extra.json、ch7_more.json, 代码在 notebooks/conditional_diffusion.py,交互实验在 experiments/exp-7-1.html。

每一个数字都能重新跑出来。如果你跑不出来,那是本书的错。

第 5–7 章是一条线:第 5 章定义前向与反向,第 6 章决定走多少步、怎么走,第 7 章决定走向哪里。三章合起来,才是一个完整的生成过程。

而这条线上的每一章,都遵循同一个写法:先给一个能算的例子,再给一组受控实验,最后列出「不报错但出错」的清单。

现象 → 机制 → 实验 → 清单 → 回到现象。这本书的每一章,都是这条线。

而线上的每一个数,都应该能被你重新跑出来。如果你跑不出来,请把它当作一处需要修订的地方——这也是它值得被读的原因。

下一章:文字如何进入图像。

下一章换一个问题:条件不再是”第 0 类”这样的离散标签,而是一整段文字。 那时的 CFG 公式不变,但”空条件”变成了”空字符串”—— 第 8 章会看到,这个小小的变化让整件事复杂了一个量级。