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 的全部秘密,下一节展开。
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 意味着先把 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.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 |
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/2 到 1/4
- 只放真正要排除的概念,不要堆一堆同义词
- 监控越界率(图像上是”结构崩坏、色块乱飞”)
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。
一模一样。
当分类器完美时,
\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。
所以两种方法在数学上是同一个东西,差别只有两处:
- 用什么估计”分类器梯度”:真分类器 vs 两个模型输出的差
- 参数相差 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 档
- 显示真实数据:关掉之后只看生成样本
观察引导。
- w=0:彩点铺满两个月亮——模型完全没在听条件。
- 拖到 w=1:彩点回到你选的那一个月亮上。
- 拖到 w=10:内月的点明显挤成一条更细的带子(标准差掉到 0.107)。
- 拖到 w=20:外月的点冲出了虚线框,有一串沿着一条直线飞到画面外。
- 停在 w=20,看左边的三个数:准确率 1.000,越界率 37.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 的发散测不出引导的效果。
按”改动成本”排,不按”严重程度”排:
- 打印空类编号(10 秒)
- w=1 是否逐位等于条件采样(1 分钟)
- w=1 是否也崩(排除采样器,2 分钟)
- 加看越界率(改一行,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 时 \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)三个约定,违反了就会在后面某处炸:
- 空类编号是
n_class,不是-1,也不是n_class-1 c的形状和 batch 维一致((N,)),不是 one-hotn_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 会得到一个形状错误, 或者更糟——广播成一个看起来能跑的错结果。
在这个 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)) * xDDPM 的后验均值。 两个系数加起来等于 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”的决定因素。
三个缓解办法:
- 切成两块:先算有条件的 N,再算无条件的 N。 省内存,但慢(两次小 batch,GPU 利用率低)。
- 梯度检查点:用时间换内存,适合训练,采样时通常不必。
- 只在部分步做 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三条纪律:
- 每个 w 用同一批种子,否则比的是随机性
- 至少报告三个量:准确率、标准差、越界率
- 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 一次跑通的检查顺序
- 训练 100 步,loss 应该从 ~1.0 降到 ~0.5
- 训练完,w=1 采样,准确率应 >0.9
- w=0 采样,准确率应 ≈0.5
- w=2 采样,准确率应 ≈1.0,越界率 0
四条全过,才算”实现正确”。 任何一条不过,先看对应的那一节。
忘了 .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 的逐位检查,再谈默认值。
怎么定默认值:
- 取 20 条有代表性的提示词
- w 扫 3 / 5 / 7 / 9 / 12
- 每条每个 w 出 8 张
- 看两个量:人评”是否符合提示词”、同提示词 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。
看它是不是”从零训练的扩散模型”。
- 从零训练 → 引导是外挂的,可以调(范围取决于模型质量)
- 蒸馏/微调而来 → 先做上面的连续性检测
这条判断比任何论文里的默认值都可靠,因为默认值是针对某个特定训练流程的。
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_samplescheduler.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 一个最小可用的接入检查
接进真实管线后,跑这四步:
- w=1 与”不调用 CFG”逐位一致
- w=0 与”只送空条件”逐位一致
- w 从 1 加到 3,解码后的图像有明显变化
- 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() 应该按这个顺序判断:
- 空类编号对不对 → 不对直接报错,后面的都不用测
- w=1 是否逐位一致 → 不一致,是参数化或代码路径问题
- w=1 是否稳定 → 不稳定,是采样器问题,不是引导
- 越界率是否随 w 单调 → 不单调,CFG 没接上
- 准确率是否饱和 → 饱和后还调大 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。
| # | 检查 | 通过标准 |
|---|---|---|
| 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)记录三样东西:
- 总 loss
- 有条件/无条件分开的 loss(按是否被丢弃分组)
- 每 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 时) | ✗ | ✗ |
读这张表的方法:每一行至少有一个 ✓ 吗?
- 有 → 三个指标够用
- 没有(第二行)→ 需要第四个检查:逐位相等
因为它不是错误,是约定。
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 也不崩?
三个原因,按重要性排:
- 模型误差更小——p(x\mid c) 估得准,外推的幅度就小
- 高维空间——“飞出流形”在 1024 维里表现不同,像素上看不出来
- 管线里有补偿——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(修尺度)。
四者互不冲突,因为它们分别动的是分子、基准、倍数和后处理。
但叠加会让调试变难——出问题时你不知道是哪一层。 建议一次只加一个,这和本章”一次只变一个量”的纪律一致。
选择顺序建议:
- 先确认条件模型本身好不好(w=1)
- 再加 CFG(默认手段)
- 需要”排除某物”再加 negative(强度 1/3)
- 需要”强调某词”再加 weighting
- 出现尺度问题时最后加 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), 但过引导的表现更隐蔽——不是飞出范围,是”每个词都被过度强调”, 画面变得油腻、元素堆砌。
如果 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 章的理由类似,但这里更极端):
- 可画:w=20 时那条冲出去的线,一眼就能看见
- 可算:PCA、越界率、密度比,都有定义
- 可穷举: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)
- 某些对齐方法里的”拒绝方向”
- 图像编辑里的”方向编辑”
它们都在做同一件事:找到一个方向,然后放大它。
而本章的所有教训都可以平移:
- 方向要有意义——空条件必须是训练过的(否则你放大的是随机方向)
- 放大有代价——p_w 不再等于后验(否则你以为在采样,其实在锐化)
- 指标会骗人——准确率看不见分布形状(否则你会把 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}")正常的形状:越界率在采样后段应该下降(样本收敛到数据范围)。 如果它在某一步开始上升,那一步就是问题开始的地方。
| 打印 | 能发现 |
|---|---|
| 空类编号 | 失败三 |
| \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. 过引导的样本仍然会被分类器认对。 对。 这正是”准确率会骗人”的根源——飞出数据范围的点,类别标签可能还是对的。
说明你已经掌握了这一章的骨架。
剩下的不是”记住更多”,是”动手验证”—— 挑练习二(找你自己模型的崩溃 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 五分钟版
三句话:
- CFG 是把”有条件输出 - 无条件输出”这个差放大 w 倍再加回去。
- w=1 是普通条件采样,w>1 会越过数据继续往外推。
- 准确率在 w=3 就饱和,之后它不再告诉你任何事。
如果对方只问一个问题,多半是”那 w 该取多少”。 答:取”准确率刚饱和、多样性还没明显掉”的那个点。本章是 2,真实模型通常 5–8。
7.50.2 三十分钟版
在五分钟版之上加两块:
① 手算的例子。 真实数据在 -1,w=2 时等效均值 -2.2。 这一个数就能让人明白”不是靠近,是外推”。
② 三段行为。 欠引导 / 正常 / 过引导,每段配一个数。
这一段的作用是让人知道”存在一个危险区”,而不只是”有个参数”。
7.50.3 两小时版
完整讲,但顺序调整过:
- 先讲失败(无标签丢弃 → 越界 98.6%)
- 再讲公式(此时听众已经知道”为什么需要这个公式”)
- 然后手算(建立量级感)
- 最后三段行为与三个指标
先失败后公式,比”先公式后例子”更容易让人记住—— 因为失败制造了一个”为什么”的问题。
① “为什么不能直接让模型更听话?” 因为它只会”更像某个类”,不会”更符合你的意图”。 引导是一个方向,不是一个目标。
② “为什么 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 章会看到,这个小小的变化让整件事复杂了一个量级。