8 文字如何进入图像
本章地图
你在这里:第 7 章的条件是一个离散标签(“第 0 类”)。真实模型的条件是一整段文字。从”一个数”到”一串向量”,中间隔着一个文本编码器和一层交叉注意力。 本章解决:文字怎么变成向量;交叉注意力为什么能”绑定”属性而词袋不能;注意力图能不能用来解释;空字符串为什么不是零向量。 前置:第 1 章(多头注意力)、第 2 章(Block)、第 5–6 章(扩散与采样)、第 7 章(CFG)。 读完能做:读懂一个文本到图像的 UNet 的结构;诊断”词都认识但位置搞反”的绑定失败;判断一张注意力图值不值得信。
8.1 本章的代码与数据
import torch, sys
sys.path.insert(0, "notebooks")
from tiny_diffusion import Config, make_schedule
from text_conditioning import (make_two_point_data, train_text_model,
sample_text, slot_accuracy)
x, y = make_two_point_data(4000, noise=0.08, seed=0) # 4 维,4 个类
model, hist = train_text_model(x, y, cfg, sch, encoder="cross", steps=6000)
samples = sample_text(model, sch, tok, w=1.0)复现本章全部数字:
cd notebooks
PY=/home/mpp/micromamba/envs/dlspace/bin/python
$PY run_ch8_case.py # 三组实验,约 8 分钟| 文件 | 内容 | 耗时 |
|---|---|---|
notebooks/text_conditioning.py |
两种文本编码器 + 条件去噪器 + 采样 | — |
notebooks/run_ch8_case.py |
绑定实验、注意力分析、CFG 扫描 | ~8 min |
notebooks/run_ch8_ablation.py |
对调实验(决定性证据) | ~4 min |
notebooks/run_ch8_ablate2.py |
三个消融、学习曲线、注意力熵 | ~30 min |
notebooks/run_ch8_capacity.py |
容量对照(损失差不是参数少) | ~3 min |
notebooks/bpe_demo.py |
手写 BPE + 分词效率实测 | ~7 min |
data/ch8_*.json |
全部实测数据 | — |
qa/gen_fig_ch8.py → assets/fig/8-1-binding.svg |
图 8-1 | — |
experiments/exp-8-1.html |
交互实验 8-1 | — |
全部在单张 RTX 3090 上跑完,总耗时约 50 分钟。 没有任何一步需要多卡或大显存。
本章和第 7 章共用同一套调度、同一个 \varepsilon-预测目标、同一个 DDPM 采样器。 唯一的改动是条件的表示:从一个类别下标,变成一串 token。
8.2 动手之前:三十分钟复现核心结论
如果你手边有机器,先跑一遍再读。 这一章的所有论证,都建立在三个数上。
8.2.1 第 1 步:训练两个模型
cd notebooks
PY=/home/mpp/micromamba/envs/dlspace/bin/python
$PY run_ch8_ablation.py这个脚本会训两个模型(均值池化和交叉注意力),各 6000 步。 在 RTX 3090 上约 4 分钟。
8.2.2 第 2 步:看三个数
脚本会打印:
mean RB: left_blue 0.455 right_blue 0.55
mean BR: left_blue 0.455 right_blue 0.55
mean: 对调后最大逐元素差 = 0.000e+00
cross RB: left_blue 0.0 right_blue 1.0
cross BR: left_blue 1.0 right_blue 0.0
cross: 对调后最大逐元素差 = 9.722e-01
三个数分别是:
| 数 | 均值池化 | 交叉注意力 | 说明 |
|---|---|---|---|
| RB 的右蓝比例 | 0.55 | 1.00 | 该蓝的地方蓝了吗 |
| BR 的左蓝比例 | 0.455 | 1.00 | 对调之后呢 |
| 对调差 | 0.000 | 0.972 | 同种子下两次输出的最大差 |
如果只允许看一个数,看第三个。
8.2.3 第 3 步:自己加一个检查
在脚本里加一行,打印”两个镜像条件的右蓝比例之和”:
print(swap["RB"]["right_blue"] + (1 - swap["BR"]["left_blue"]))均值池化应该接近 1,交叉注意力接近 2。
这个判据比看单个准确率可靠—— 它不会被”互补计数”骗过去。
因为这一章的结论有一个反直觉的地方: “准确率 0.602 可能是零信息”。
你亲手跑出这个数,再看到对调差是 0.000, 那种”被数据纠正”的体感,是读文字得不到的。
这也是本书每一章都要求”先算一遍”的原因。
8.3 案例:同一袋词,两种顺序
先看一个具体的问题。
数据是四个点对,每个点对有两个点:左点和右点。 每个点是”红”或”蓝”,颜色由它的 y 坐标决定(上红下蓝)。 于是有四个类:
| 类 | 左 | 右 |
|---|---|---|
| RR | 红 | 红 |
| RB | 红 | 蓝 |
| BR | 蓝 | 红 |
| BB | 蓝 | 蓝 |
条件是一句话,两个 token:[左色, 右色]。
现在注意一件事:RB 和 BR 的词袋是相同的—— 都是”一个红、一个蓝”,只是顺序不同。
如果你的文本编码器把两个词取平均,这两个类会得到完全相同的条件向量。 模型收到同样的输入,却要生成两种不同的输出。
这不可能做到。
“有一个红和一个蓝”和”左边红、右边蓝”是两句话,但不是同一件事。
- 词袋只记录”出现了哪些词”,不记录”哪个词对应哪个位置”
- 注意力可以让每个位置自己去”查”它需要的那个词
本章的全部内容,可以看成这一句话的展开: 为什么均值池化会失败,交叉注意力怎么解决它,以及代价是什么。
这个问题不是玩具。 真实提示词里,“一只猫坐在一只狗旁边”和 “一只狗坐在一只猫旁边”是同一个词袋,但生成结果应该不同。 所有文本到图像的模型都必须解决它。
8.4 文字变成向量:三步
第一步:分词。 把字符串切成 token。
中文按字或子词切,英文按 BPE(byte-pair encoding)切。 切法由 tokenizer 决定,不由模型决定——换 tokenizer 等于换了一门语言。
第二步:查表。 每个 token 查一个 d 维向量。
这一步和大模型里的词嵌入没有区别:一个 nn.Embedding(vocab, d)。 真正的信息不在这一层,而在下一层。
第三步:编码器。 让 token 之间互相看,产生”带上下文”的表示。
这一步才是 CLIP / T5 / Qwen 文本编码器的核心。 第 1 章的 Transformer Block 在这里原样出现:自注意力 + FFN + 残差 + 归一化。
8.4.1 三种编码器的内部长什么样
“文本编码器”不是一个东西,是一族结构。 三个最常见的:
| CLIP 文本编码器 | T5 编码器 | Qwen 系列 | |
|---|---|---|---|
| 层数 | 12 | 12 / 24(base / XXL) | 24–80 |
| 宽度 | 768 | 768 / 4096 | 1024–8192 |
| 注意力 | 因果(只能看左边) | 双向 | 因果 |
| 上下文 | 77 token | 变长(相对位置) | 长上下文 |
| 输出 | 序列 + 池化向量 | 序列 | 序列 |
“因果”那一行值得停一下。
CLIP 的文本编码器是因果注意力——每个 token 只能看到它前面的。 这不是为了生成,而是因为它沿用了 GPT 式的结构。
后果:最后一个 token 的表示”看过”整句,而第一个 token 只看到自己。 所以 CLIP 的池化向量取的是 [EOS] 位置的输出——那是唯一”看过全句”的位置。
如果你从 CLIP 的序列里随便取一个位置当”整句向量”,会取到不完整的信息。
[EOS] 位置才等于”整句摘要”。 很多实现出错就出在这里——它们用了 last_hidden_state[:, 0](第一个 token), 那是”句首”,不是”全句”。
正确做法:用 pooler_output(它内部就是取 [EOS]), 或者自己按 argmax(input_ids == eos_token_id) 取。
T5 是双向的,所以没有这个问题——每个位置都看过全句。 这也是 Imagen 选择 T5 的一个附带好处。
8.4.2 一个句子在编码器里经历什么
"一只红色的猫"
↓ tokenizer(BPE / SentencePiece)
[101, 3721, 4895, 1996, 8920, 102] # 含特殊 token
↓ embedding
(6, 768) # 6 个 token,每个 768 维
↓ + 位置编码
(6, 768)
↓ 12 × [自注意力 + FFN + 残差 + LN]
(6, 768) # 每个 token 都吸收了上下文
↓ 取 [EOS] 位置
(768,) # 整句表示
注意中间那行 (6, 768) 在每一层都保持形状不变。 编码器不改变序列长度,只改变每个位置的内容——这是 Transformer 的通用性质(第 2 章)。
对交叉注意力来说,最重要的是最后那个 (6, 768) 序列, 不是 (768,) 的池化向量。因为图像需要”每个词各是什么”,不是”整句大概是什么”。
8.4.3 词表有多大
CLIP:49,408 个 token。T5:32,000。Qwen:150,000 以上。
词表越大,单个 token 承载的语义越少,但句子越短。
中文尤其明显:一个 5 万词表的 tokenizer 可能把一个汉字切成 1–3 个 token, 而 15 万词表的可能一个字一个 token。这直接影响 77 这个上限能装多少内容。
这就是为什么中文提示词在 CLIP 模型上”更费 token”—— 同样一句话,中文可能占 30 个 token,英文只占 12 个。
8.4.4 分词器到底在做什么:手写一个 BPE
“按子词切”这句话太抽象。下面是一个 20 行的 BPE,跑在本书自己的正文上。
算法只有三步,循环执行:
① 把文本编码成 UTF-8 字节,每个字节是一个符号(初始词表 256)
② 数所有相邻符号对,找出现最多的那一对
③ 把这一对合并成一个新符号,词表 +1
就这么简单。GPT-2、CLIP、LLaMA 的分词器都是这个算法的变体。
为什么从字节开始? 因为任何 Unicode 文本都能表示成字节, 不会出现”词表里没有这个字”的情况——最坏情况是一个字被切成 3 个字节 token。
在本书第 5–8 章的正文(约 32 万字符)上训练,得到:
| 合并次数 | 词表 | 汉字/token(训练集) | 汉字/token(留出集) | 77 token ≈ |
|---|---|---|---|---|
| 300 | 556 | 0.670 | 0.625 | 48 个汉字 |
| 1000 | 1,256 | 1.049 | 0.967 | 74 个汉字 |
| 2000 | 2,256 | 1.318 | 1.210 | 93 个汉字 |
“留出集”是第 8 章本身——训练时没见过。
两列很接近,说明这个测量不是死记硬背。 但也别太当真:语料只有 32 万字,真实词表是 5 万级。
几个具体例子(1000 次合并后):
| 字符串 | token 数 | 说明 |
|---|---|---|
扩散模型 |
2 | 高频搭配被合并了 |
红色 |
5 | 在这批语料里少见,没有合并 |
一只红色的猫 |
11 | 7 个汉字,占了 11 个 token |
cross attention |
10 | 英文反而更费——因为语料是中文 |
① 一个常用汉字是 3 个字节,所以要两次合并才合成一个 token。 词表小的时候,一个汉字占 2–3 个 token 是常态。
② 高频词会被合并,低频词不会。 扩散模型 变成 2 个 token,红色 还是 5 个—— 不是”红色”更复杂,是它在这批语料里出现得少。
③ 77 个 token 不是 77 个词,更不是 77 个字。 在这个玩具分词器上,77 个 token 只装得下约 50–93 个汉字——词表越小,装得越少。
真实 CLIP 的词表大得多,但中文仍然吃亏: 一个常用汉字通常 1 个 token,生僻字 2–3 个。 所以一段 100 字的中文提示词,很可能超过 77 的上限。
这也解释了为什么”中文提示词要写短”—— 不是模型不懂中文,是 token 预算被字节吃掉了。
一个可以自己做的检验: 用你手上的分词器,分别数一句中文和它的英文翻译有多少 token。 如果中文是英文的 2–3 倍,你就知道该把预算花在哪了。
8.4.5 为什么不能只做前两步
因为同一个词在不同上下文里的意思不一样。
“苹果”在”吃苹果”和”苹果发布会”里应该有不同的向量。 只查表的话,两者完全相同。
编码器的作用是让每个 token 的表示吸收上下文—— 自注意力让”苹果”看到”吃”还是”发布会”,从而调整自己的向量。
本章用一个极简的替代品:两个 token(左色、右色), 每个查一个嵌入,然后要么取平均,要么做交叉注意力。
没有自注意力层,没有 FFN。 这是刻意的: 本章要回答的是”条件向量怎么被图像使用”, 而不是”文本编码器内部长什么样”。
前者是交叉注意力的问题,后者是 Transformer 的问题—— 后者第 1、2 章已经讲过。
8.5 交叉注意力:图像问,文本答
第 1 章的注意力公式:
\text{Attn}(Q,K,V)=\text{softmax}\left(\frac{QK^\top}{\sqrt d}\right)V
在自注意力里,Q,K,V 都来自同一个序列(文本自己看自己)。
在交叉注意力里:
| 量 | 来自 | 含义 |
|---|---|---|
| Q | 图像特征 | “我这个位置需要什么信息” |
| K | 文本 token | “我这里有什么信息” |
| V | 文本 token | “我能提供什么内容” |
所以交叉注意力是”图像问、文本答”。
每个图像位置都会得到一个自己的条件向量—— 不是全图共用一个,而是”这个位置该听哪些词”。
条件不能是一个”丢了顺序”的向量。
- 均值池化:整张图共用一个与顺序无关的向量
- 交叉注意力:每个位置各拿一个,而且每个位置拿到的可以不同
注意”可以不同”——不是”必须不同”。
本章后面的消融会给出一个反直觉的结果: 只要条件里带着顺序,哪怕所有位置共用同一个向量,下游也能把它分配到正确的位置。 但那是学出来的;交叉注意力把这件事变成了结构。
这个区别很重要,它决定了你换模型时要验证什么。
8.5.1 参数量:交叉注意力为什么贵
第 7 章算过一笔账(d=320):
| 方式 | 每层参数 |
|---|---|
| 相加 / AdaGN | 2d=640 |
| 交叉注意力 | 4d^2=409{,}600 |
相差 640 倍。 本章的小模型里,这个差距是 67,332 vs 100,740 参数 (差 1.5 倍,因为 d=128 且只有一层注意力)。
8.5.2 真实模型的账
上面的 640 倍是拿”类别条件”和”交叉注意力”比。 真实模型里,条件维度不是 d 而是文本宽度,账要重算。
交叉注意力一层的参数(d 是特征宽度,d_c 是文本宽度):
\underbrace{d^2}_{Q}+\underbrace{2\,d\,d_c}_{K,V}+\underbrace{d^2}_{\text{out}} =2d^2+2dd_c
Stable Diffusion 1.5(d_c=768):
| 特征宽度 d | 每层参数 | 层数 | 小计 |
|---|---|---|---|
| 320 | 696,320 | 5 | 3,481,600 |
| 640 | 1,802,240 | 5 | 9,011,200 |
| 1280 | 5,242,880 | 6 | 31,457,280 |
| 16 | 43,950,080 |
约 4400 万,占 UNet 8.6 亿参数的 5.1%。
这个比例可能比你预期的小。 交叉注意力的”贵”不在参数,在前面算过的 N\times M 计算。
SDXL(d_c=2048,两个 CLIP 拼接):
| d | 每层参数 | 比 SD1.5 同宽度 |
|---|---|---|
| 320 | 1,515,520 | 2.18× |
| 640 | 3,440,640 | 1.91× |
| 1280 | 8,519,680 | 1.62× |
文本宽度从 768 涨到 2048,交叉注意力的参数涨了 1.6–2.2 倍。 这就是”更强的文本编码器”的真实成本——不只在编码器本身,还在每一层交叉注意力上。
SD3 Medium(d=1536,24 层)的联合注意力,每层 4d^2=4\times1536^2=9{,}437{,}184 个参数。
和 SDXL 的 1280 宽交叉注意力(8,519,680)几乎一样。
但 MMDiT 真正的开销在另一个地方:adaLN 每层 2048\times6\times1536=18{,}874{,}368——是注意力本身的两倍。
24 层加起来 4.53 亿。
所以”MMDiT 参数量大”的直觉是对的,但归因错了: 大头不是联合注意力,是每个 Block 都要从池化文本重新算一遍调制参数。
这也解释了为什么 DiT 类模型对”池化文本向量”的维度很敏感—— 它直接乘在 adaLN 的参数上。
所以”交叉注意力是文本控制图像的价格”这句话,要说准确一点: 参数上它只占 5%,计算上它占的是”每个潜位置都要问一遍”。 后者才是真正的价格。
8.6 手算一遍:两行矩阵怎么完成绑定
公式看过很多遍,不如自己算一遍。 这一节用一个小到可以手算的交叉注意力,把”绑定”拆到每一个数字上。
8.6.1 设定:2 维、2 个 token、2 个点
为了能手算,把维度压到 2。真实的 d 是 768,但算术的形状完全一样。
词嵌入(两个颜色):
e_{\text{红}}=\begin{pmatrix}2\\0\end{pmatrix},\qquad e_{\text{蓝}}=\begin{pmatrix}0\\2\end{pmatrix}
位置嵌入(第 0 个词、第 1 个词):
p_0=\begin{pmatrix}0.5\\0\end{pmatrix},\qquad p_1=\begin{pmatrix}0\\0.5\end{pmatrix}
两个点的 query(左点稍微偏向”问第一个词”,右点偏向”问第二个词”—— 这是模型学出来的,这里直接给定):
q_L=\begin{pmatrix}1.5\\0.3\end{pmatrix},\qquad q_R=\begin{pmatrix}0.3\\1.5\end{pmatrix}
K 和 V 用同一份表示(真实模型里是两组不同的投影矩阵,这里合并以简化):
k_i=e_{w_i}+p_i,\qquad v_i=k_i
缩放因子 \sqrt{d_k}=\sqrt2\approx 1.414。
8.6.2 条件 RB:\langle\text{红},\text{蓝}\rangle
两个 key:
k_0=e_{\text{红}}+p_0=\begin{pmatrix}2.5\\0\end{pmatrix},\qquad k_1=e_{\text{蓝}}+p_1=\begin{pmatrix}0\\2.5\end{pmatrix}
左点的分数:
q_L\cdot k_0=1.5\times2.5+0.3\times0=3.75,\qquad q_L\cdot k_1=1.5\times0+0.3\times2.5=0.75
除以 \sqrt2:2.652 与 0.530。softmax:
w_L=\frac{e^{2.652}}{e^{2.652}+e^{0.530}}=\frac{14.18}{14.18+1.70} =\begin{pmatrix}0.893\\0.107\end{pmatrix}
左点 89.3% 看第一个词(红)。
右点对称:q_R\cdot k_0=0.75,q_R\cdot k_1=3.75, softmax 得到 \begin{pmatrix}0.107\\0.893\end{pmatrix}——89.3% 看第二个词(蓝)。
输出:
o_L=0.893\begin{pmatrix}2.5\\0\end{pmatrix}+0.107\begin{pmatrix}0\\2.5\end{pmatrix} =\begin{pmatrix}2.232\\0.268\end{pmatrix}
o_R=\begin{pmatrix}0.268\\2.232\end{pmatrix}
左点拿到的是”偏红”的向量,右点拿到的是”偏蓝”的向量。 这正是条件”左边红、右边蓝”该有的样子。
8.6.3 条件 BR:\langle\text{蓝},\text{红}\rangle
只把两个词对调,其他一切不变。 两个 key 变成:
k_0=e_{\text{蓝}}+p_0=\begin{pmatrix}0.5\\2\end{pmatrix},\qquad k_1=e_{\text{红}}+p_1=\begin{pmatrix}2\\0.5\end{pmatrix}
左点的分数:
q_L\cdot k_0=1.5\times0.5+0.3\times2=1.35,\qquad q_L\cdot k_1=1.5\times2+0.3\times0.5=3.15
除以 \sqrt2 得 0.955 与 2.227,softmax:
w_L=\frac{e^{0.955}}{e^{0.955}+e^{2.227}}=\frac{2.60}{2.60+9.27} =\begin{pmatrix}0.219\\0.781\end{pmatrix}
注意这个数字。
左点的注意力从”89.3% 看第 0 个词”变成了”78.1% 看第 1 个词”—— 它看的词的下标翻转了,但它看的内容仍然是”红”。
这就是绑定的全部机制: query 由位置决定,key 由”词 + 位置”决定, 所以”左点该看哪个位置”和”那个位置上是什么词”是两个独立的因素, 模型可以把它们组合起来。
8.6.4 把三个结果并排放
| 左点权重 | 左点输出 | 右点权重 | 右点输出 | |
|---|---|---|---|---|
| RB | 0.893 / 0.107 | (2.232, 0.268) | 0.107 / 0.893 | (0.268, 2.232) |
| BR | 0.219 / 0.781 | (1.672, 0.828) | 0.781 / 0.219 | (0.828, 1.672) |
| 均值池化 | — | (1.000, 1.000) | — | (1.000, 1.000) |
最后一行是重点。
均值池化对两种条件给出的是同一个向量 (1,1)—— x 方向和 y 方向各占一半,既不说左,也不说右。
两个不同的条件、同一个输出。 模型再聪明也没法把它变成两种结果。
① 绑定不是”注意力很聪明”,是”下标和内容被分开了”。 权重管的是下标(第几个词),V 管的是内容(那个词是什么)。 两者相乘,才得到”这个位置该拿什么”。
② 位置嵌入必须进 K。 如果去掉 p_i,k_0 和 k_1 就只剩 e_{\text{红}} 和 e_{\text{蓝}},下标信息彻底消失,权重再也无法”翻转到第 1 个词”。
③ 均值池化丢的不是”精度”,是”下标”这个维度。 平均之后,第 0 个和第 1 个在数学上不再有区别。
手算到这里,本章剩下的实测都是在验证这三句话。 下一节先看真实训练出来的模型是不是这样。
手算里,位置嵌入加在 K 上。如果改加在 Q 上,会怎样?
提示:Q 来自图像(点的坐标),K 来自文本(第几个词)。
(答案:加在 Q 上,编码的是”这个点是第几个点”,不是”这个词是第几个词”。
绑定需要的是后者。 把位置加到 Q 上,左点和右点会被标成”第 0 个”“第 1 个”, 但两个词在 K 里仍然无法区分——顺序信息还是没有进入打分。
位置该加在谁身上,取决于”顺序”属于谁。 词序属于文本,所以加在 K(和 V)。 图像的空间位置属于图像,所以加在 Q(或图像特征)。 两边都需要时,两边都加——但那是两个不同的位置概念,不能共用一个嵌入表。)
8.7 实测:词袋在 RB/BR 上只有 50%
两个模型,唯一的差别是条件怎么算:
- 均值池化:\text{cond}=\frac12(e_{c_L}+e_{c_R}),67,332 参数
- 交叉注意力:每个点一个 query,100,740 参数
每类 300 个样本 × 5 个种子,判据是两个色槽都摆对的比例:
| 类 | 均值池化 | 交叉注意力 |
|---|---|---|
| RR | 1.000 ± 0.000 | 1.000 ± 0.000 |
| RB | 0.550 ± 0.034 | 1.000 ± 0.000 |
| BR | 0.449 ± 0.035 | 1.000 ± 0.000 |
| BB | 1.000 ± 0.000 | 1.000 ± 0.000 |
看中间两行。
均值池化在 RB 和 BR 上分别只有 55.0% 和 44.9%—— 就是抛硬币。而它在 RR 和 BB 上是完美的 1.000。
为什么? 因为 RB 和 BR 的词袋相同: \frac12(e_{\text{红}}+e_{\text{蓝}}) 对两者是同一个向量。 模型收到同样的输入,只能随机选一种摆法。
55.0% 和 44.9% 加起来是 99.9%——它们其实是同一个 50/50 分布的两半。 这不是”稍微差一点”,是”完全没有信息”。
它不是”整体变差”,是”只在需要绑定的类上归零”。
- 纯色类(RR、BB):词袋唯一确定排列,均值池化够用
- 混色类(RB、BR):词袋有两个排列,均值池化无解
如果你只看总体准确率,均值池化是 (1+0.55+0.45+1)/4=75\%—— 看起来”还行”,但它在一个子集上是 0 分。
8.7.1 训练损失已经给出了答案
在跑任何采样之前,训练曲线里就有一个信号。
| 模型 | 参数 | 最终训练损失 |
|---|---|---|
| 均值池化 | 67,332 | 0.0476 |
| 交叉注意力 | 100,740 | 0.0295 |
均值池化高出 61%。
但这里有一个混淆:它的参数也少 33%。有没有可能只是模型太小?
做一次容量对照:把均值池化的 MLP 从 3 层加到 6 层。
| 层数 | 参数 | 训练损失 |
|---|---|---|
| 3 | 67,332 | 0.0476 |
| 4 | 83,844 | 0.0469 |
| 5 | 100,356 | 0.0480 |
| 6 | 116,868 | 0.0482 |
参数从 6.7 万加到 11.7 万,损失几乎没有变化。
在 10.0 万参数这个点上直接对比: 均值池化 0.0480,交叉注意力 0.0295——差 63%。
两个模型参数量相同、数据相同、优化器相同、训练步数相同。
唯一的差别是条件怎么算。
损失差不是”学不会”,是”学不了”—— 均值池化的条件向量对 RB 和 BR 完全相同, 它要给两个不同的目标输出同一个预测。 最优解是两者的平均,而那个平均的误差是下不去的。
这就是信息论意义上的下界: H(\varepsilon\mid c_{\text{bag}})>H(\varepsilon\mid c_{\text{pos}})。
训练损失曲线不会告诉你”哪里错了”,但它会告诉你”有没有错”。
这个做法值得记住:当你怀疑”模型容量不够”时, 先做一个容量对照,再下结论。 如果加参数不降损失,问题在输入,不在模型。 ### 用信息量算一遍:那 63% 是多少比特
两个模型的损失差,可以换算成”条件里少了多少信息”。
假设(本章的数据近似满足):给定条件 c,噪声 \varepsilon 近似高斯, 模型学到的是条件均值,于是
\text{MSE}=\mathbb{E}\left[\operatorname{Var}(\varepsilon\mid c)\right]
对高斯分布,微分熵和方差的关系是
H(\varepsilon\mid c)=\tfrac12\log\left(2\pi e\cdot \operatorname{Var}\right)
所以两个模型的条件熵之差:
\Delta H=\tfrac12\log\frac{\text{MSE}_{\text{mean}}}{\text{MSE}_{\text{cross}}} =\tfrac12\log\frac{0.0480}{0.0295} =\tfrac12\log 1.627=0.243\ \text{nat}
换成比特:0.243/\ln 2=0.351 bit。
它是”顺序”这个信息,在每一个维度、每一个时间步上的平均含量。
听起来很小? 因为这是一个二元信息(左红还是左蓝), 理论上最多 1 bit。0.35 bit 说明模型只榨出了其中的一部分—— 剩下的被噪声和有限容量吃掉了。
重要的是它不是零。 均值池化那条路,理论上是 0 bit: 条件向量逐位相同,I(\varepsilon;c)=0。
实测的 0.35 bit 来自”条件不同”,而均值池化的条件是同一个。 这就是”信息被销毁”和”信息没学好”的区别。
这个换算有一个前提:\varepsilon 在给定条件下近似高斯。 本章的数据是高斯噪声加在四个点上,这个假设成立得不错。 换成真实图像,假设会变差,但”损失差 → 信息差”的方向仍然成立。
实践含义:当你想知道”某个条件到底带了多少信息”, 不要看它的维度,看它把损失降了多少。 维度可以是 768,信息可以是 0.35 bit。
8.7.2 交叉注意力为什么能解决
因为每个点拿到了不同的条件向量。
左点的 query 来自左点的坐标,右点的 query 来自右点的坐标。 两个 query 不同,注意力权重就不同,得到的条件向量也不同。
于是: - 对 RB,左点的条件更偏”红”,右点更偏”蓝” - 对 BR,反过来
模型不再需要”从一个向量里猜两种排列”—— 它每个位置都有自己的一份信息。
但这是”交叉注意力怎么做”,不是”绑定必须这么做”。 后面的拆零件实验会证明: 把 query 里的坐标抹掉、让两个点拿到完全相同的条件,绑定仍然成立。 所以”每个位置一份不同条件”是充分条件,不是必要条件。
8.7.3 决定性实验:对调两个词
准确率只是间接证据。 更直接的做法是:固定种子、固定初始噪声,只把两个词对调。
如果模型真的用了顺序,对调之后输出应该翻转; 如果没用,输出应该一模一样。
| 编码器 | RB(左蓝/右蓝) | BR(左蓝/右蓝) | 对调后的最大逐元素差 |
|---|---|---|---|
| 均值池化 | 45.5% / 55.0% | 45.5% / 55.0% | 0.000 |
| 交叉注意力 | 0.0% / 100% | 100% / 0.0% | 0.972 |
不是”差不多”,是逐位相同——差值的最大值恰好是零。
因为均值池化的条件向量在两种顺序下是同一个向量: \frac12(e_{\text{红}}+e_{\text{蓝}})=\frac12(e_{\text{蓝}}+e_{\text{红}})。
同一批初始噪声 + 同一个条件 + 同一个模型 = 同一组输出。 没有任何随机性可以掩盖它。
而交叉注意力下,对调让输出翻转了 0.972—— 两个点的 y 坐标从 +0.45/-0.45 换到了 -0.45/+0.45,差值是 0.9, 加上噪声正好接近 1。
这个实验比注意力图强在哪:
- 注意力图是内部量,它的含义依赖你怎么解释
- 对调词是外部干预,因果方向清楚
这也是本章的评估建议:想验证”文本有没有起作用”, 就改条件、看输出,不要去看权重。
8.8 拆零件:三个消融
对调实验证明了”交叉注意力用了顺序”。但它没说是哪一部分在起作用。
于是拆掉三个零件,各训一次:
| 变体 | 改了什么 | 参数 |
|---|---|---|
full |
原样 | 100,740 |
no_pos |
K 里去掉 token 的位置嵌入 | 100,740 |
no_xy |
query 里把点的坐标抹成 0 | 100,740 |
mean |
条件取平均(对照) | 67,332 |
no_pos 和 no_xy 的参数和 full 完全一样—— 只改信息,不改容量。这是消融该有的样子。
8.8.1 结果:两个意外
| 变体 | RR | RB | BR | BB | 对调差 |
|---|---|---|---|---|---|
full |
1.000 | 1.000 | 1.000 | 1.000 | 0.972 |
mean |
1.000 | 0.558 | 0.442 | 1.000 | 0.000 |
no_pos |
1.000 | 0.602 | 0.397 | 1.000 | 0.000 |
no_xy |
1.000 | 1.000 | 1.000 | 1.000 | 0.931 |
data/ch8_ablate2.json。
图 8.1 把两个维度放在一起:左边是”学得快不快”,右边是”有没有真的用上顺序”。 注意 no_pos 在 A 里没有画——它的准确率曲线看起来像”部分成功”, 但这张图 B 面板里的 0.000 才是真相。
先看 no_pos。 0.602 / 0.397——看起来像”部分绑定”,对吧?
再看对调差:恰好 0.000。
(表里 mean 的 0.558 / 0.442 和前面主实验的 0.550 / 0.449 略有不同—— 两次独立训练的抽样噪声,两个版本都满足”和 ≈ 1”。)
8.8.2 第一个意外:0.60 是假的
no_pos 的输出,在 RB 和 BR 下逐位相同。
所以那 0.602 和 0.397 是同一批样本被从两边数: 某个样本是”左红右蓝”,在 RB 下算对,在 BR 下算错。
两个数加起来 = 0.999。
对镜像的两个类(RB 和 BR),看两个准确率之和:
| 和 | 含义 |
|---|---|
| \approx 1.000 | 完全没有用顺序(每个样本只能在一边算对) |
| \approx 2.000 | 完全用上了顺序 |
| 中间 | 部分用了 |
这个判据适用于任何”镜像提示词对”: “猫在狗的左边” vs “狗在猫的左边”、 “红色方块在蓝色球上” vs “蓝色方块在红色球上”。
它比”看单个准确率”强得多: 0.602 单独看像”六成对”,加上 0.397 才看得出是”零信息”。
本章之前的 0.550 / 0.449 也是同一回事——加起来 0.999。 那不是”一半对”,是”完全没有顺序信息”。
为什么 no_pos 的输出是逐位相同的? 因为注意力对 token 的排列是等变的: 把 K 的顺序换一下,加权求和的结果不变。
没有位置嵌入,K 里只剩下词的内容, “红在第一个”和”红在第二个”在数学上无法区分。
8.8.3 第二个意外:no_xy 居然完美
no_xy 把 query 里的点坐标抹成 0——两个点问出同一个问题。
按理说,两个点应该拿到相同的条件向量,绑定应该失败。
实测:1.000,对调差 0.931。完全正常。
它怎么做到的?
关键:只有当两个点真的拿到相同向量时,绑定才会失败。 而 no_xy 里,两个点的 query 相同、注意力权重相同、输出也相同—— 确实相同。
但那个相同的向量,在 RB 和 BR 之间是不同的。
因为 K 里还有位置嵌入: RB 的 k_0=e_{\text{红}}+p_0,BR 的 k_0=e_{\text{蓝}}+p_0。 同一个 query 对两组不同的 key 打分,得到的加权和不同。
于是条件 c 携带了”顺序”这个信息,只是它是一份全局的、两个点共用的条件。
那模型怎么知道把红色放在左边?
它看输入 x。 x 的第一个点是 (-0.6, y_L),第二个是 (+0.6, y_R)—— 坐标本身就把两个点区分开了。 下游 MLP 拿到 [x, t, c],可以学出: “如果 c 说(红,蓝),就把 x=-0.6 那个点的 y 推向 +0.45。”
“条件必须是每个位置各一个向量”——这句话太强了。
实测告诉我们:
- 必要:条件里必须带着顺序
- 不必要:每个位置必须拿到不同的条件
no_xy 满足前者、不满足后者,仍然 1.000。
交叉注意力的价值不是”制造不同的向量”,是”让每个位置自己去取”。 它把”分配”这件事从下游 MLP 手里,挪到了结构里。
那为什么真实模型还要用交叉注意力?
因为对象一多,“全局条件 + 下游分配”就不行了。
k 个对象有 k! 种排列。 全局向量要一次性编码”哪一种排列”,下游 MLP 要学会 k! 种映射。 而交叉注意力把这件事分解成”每个位置各查一次”, k 个对象只需要 k 组对应关系。
这就是”组合性”: 全局条件需要指数级的数据,逐位置条件只需要线性级的。
8.8.4 注意力熵:越”专注”越没用
这一组数据是本章最反直觉的。
在 t=50 上统计注意力的熵(两个 token 时最大熵是 1 bit):
| 变体 | 平均熵(bit) | 左点权重 | 右点权重 | 绑定 |
|---|---|---|---|---|
full |
0.984 | 0.579 | 0.554 | ✅ |
no_xy |
0.998 | 0.525 | 0.525 | ✅ |
no_pos |
0.685 | 0.259 | 0.844 | ❌ |
看 no_xy:注意力完全均匀(0.525/0.525,熵 0.998), 绑定完美。
看 no_pos:注意力很尖锐(0.259/0.844,熵 0.685), 完全不绑定。
“注意力越集中,模型越’专注’在这个词上”—— 这个直觉在两个方向上都被推翻了。
no_pos 的注意力确实很集中:左点 84% 看某个 token。
但它看的是”红”这个内容,不是”第一个位置”。
于是 RB 时它看红,BR 时它还是看红—— 注意力图案一模一样,输出自然一模一样。
权重很尖锐,信息量为零。
这比”权重均匀”更能说明问题: 均匀的权重至少还可能是”分散地携带信息”; 尖锐的权重可能只是”稳定地携带同一个错误”。
所以本章的结论要再收紧一格:
注意力权重既不能证明模型在”看对的地方”,也不能证明它在”看错的地方”。 唯一能判断的是干预实验:换掉被关注的对象,看输出变不变。
前面一直在对调”词”。如果对调的是”两个点”呢?
提示:左点固定在 x=-0.6,右点在 x=+0.6。
(答案:对调两个点,等价于把整幅图左右翻转。
一个训练好的模型应该输出镜像的结果: 原来的”左红右蓝”变成”左蓝右红”。 这测试的是”位置路由”,不是”词序”。
两种对调要分开做: - 对调词 → 测”条件里的顺序有没有被用上” - 对调点 → 测”条件有没有被正确分配到位置”
本章只做了第一种。 第二种需要改数据生成,是一个可以接着做的实验。)
8.9 注意力图可信吗
你可能会想:那看看注意力权重,不就知道每个点在听哪个词了吗?
本章试了。答案是:不可信。
8.9.1 权重几乎是均匀的
在 t=50(低噪声、输入接近数据)上,按类统计每个点对两个 token 的平均注意力:
| 类 | 左点 → [红, 蓝] | 右点 → [红, 蓝] |
|---|---|---|
| RR | 0.462 / 0.538 | 0.447 / 0.553 |
| RB | 0.583 / 0.417 | 0.553 / 0.447 |
| BR | 0.482 / 0.518 | 0.436 / 0.564 |
| BB | 0.583 / 0.417 | 0.517 / 0.483 |
没有任何一行是”左点 0.9 看红、右点 0.9 看蓝”。 所有权重都在 0.44–0.58 之间。
8.9.2 权重也不能预测绑定
更直接的检查:对每个样本,看”左点更关注第一个 token”是否对应”左点是红”。
- 相关系数:−0.079(几乎为零,甚至是负的)
- 用注意力偏好猜左色的准确率:47.0%(低于抛硬币)
也就是说:注意力权重里读不出”哪个点该是什么颜色”, 但模型的输出却 100% 正确。
“交叉注意力解决了绑定”和”注意力权重解释了绑定”是两件事。
本章的数据支持前者(1.000 vs 0.550),不支持后者(相关 −0.079)。
为什么? 因为信息不只走”权重”这一条路:
- V 里也带着 位置嵌入(token 0 和 token 1 的位置不同)
- query 本身带着点的坐标
- 后面还有一层 MLP,可以把微小的权重差异放大
权重只是这条链上的一环,而且是可解释性最弱的一环。
第 4 章讲过同一件事:注意力图能告诉你”模型看了哪里”, 但不能告诉你”为什么这么看”。本章在文本条件上又遇到了一次。
实践含义:不要用注意力图去调试”为什么文字没生效”。 它可能看起来完全正常,而问题在别处。
那该用什么? 用消融:把某个 token 换掉、删掉、调换顺序, 看输出怎么变。这是行为层面的证据,不依赖对内部权重的解读。
8.9.3 如果一定要画注意力图,怎么才算不自欺
“注意力图不可信”不等于”不能画”。 它等于”画之前要设一个基线”。
三种常见画法,可信度依次上升:
① 原始权重(raw attention):直接把某一层的 \text{softmax}(QK^\top) 画出来。
问题:只反映一层、一个头。而模型有几十层、几十个头。
② 注意力 rollout(Abnar & Zuidema, 2020):把各层的注意力矩阵乘起来, 近似”信息从输入流到这一层的总路径”:
\tilde A=\prod_{l=1}^{L}\left(0.5A_l+0.5I\right)
加 0.5I 是因为残差——每一层都有”原样传下去”的那条路。 没有这一项,rollout 会高估注意力的作用。
③ 梯度加权(Chefer et al., 2021):用输出对注意力的梯度给每个头加权, 再加权求和。它回答的是”哪些注意力对输出影响大”,不是”模型看了哪里”。
① 必须报基线。 一张”模型看了猫”的热图, 如果随机噪声也能画出类似的斑块,它就没有信息。
② 必须报层和头。 “第 5 层第 3 头”和”平均所有头”是两回事。 只挑好看的那一张,就是在挑数据。
③ 必须做干预。 把被高亮的 token 换掉,输出变了吗? 没变,这张图就不解释任何事。
④ 不要用注意力图下因果结论。 “模型关注了 ‘red’,所以它生成了红色”—— 这句话需要的是消融,不是热图。
本章的实测正好是第 ③ 条的反例: 注意力权重几乎均匀(0.44–0.58), 用权重猜左色的准确率 47.0%,低于抛硬币, 而模型的输出准确率是 1.000。
权重和输出之间,隔着一个模型。 把权重当成模型的”想法”,是把中间那几十层全忽略了。
8.9.4 那到底该看什么
看行为,不看内部。
| 想知道 | 该做的实验 | 不该做的 |
|---|---|---|
| 文本有没有生效 | 换词 / 删词 / 对调,看输出差多少 | 看注意力热图 |
| 哪个词重要 | 逐个 token 消融,排序 | 看权重大小 |
| 顺序有没有被用 | 对调两个词,看输出是否翻转 | 看位置编码的范数 |
| 空条件对不对 | w=0 时输出是否与提示词无关 | 看空串向量的范数 |
这张表的每一行都是”外部干预 → 观察输出”, 和本章的对调实验是同一个方法论。
第 4 章讲过同一件事:注意力图能告诉你”模型看了哪里”, 不能告诉你”为什么这么看”。本章在文本条件上又验证了一次。
8.10 空字符串不是零向量
第 7 章的 CFG 需要一个”无条件”分支。类别模型里,那是一个可学习的空类嵌入。
文本模型里,那是空字符串经过编码器的输出。
8.10.1 空字符串也会被编码
把 "" 送进一个真实的文本编码器:
- tokenizer 会加上特殊 token——通常是
[BOS]和[EOS], 有时还有[PAD] - 这些 token 查表得到向量
- 经过若干层自注意力
- 输出一个非零的、维度正常的向量
所以”空字符串”不是”没有输入”,是”两个特殊 token 的编码结果”。
这个向量是在训练中见过的——训练时以 10% 的概率把文本换成空串, 模型学会了”这个向量表示没有条件”。
第 7 章做过一个对照:同一个模型,把”无条件”分别实现成 学出来的 NULL token 和 零向量。
| 空条件 | w=3 | w=5 |
|---|---|---|
| 学出来的 NULL token | 越界 0,多样性 0.099 | 越界 0,0.088 |
| 零向量 | 越界 100% | 越界 100%,多样性 0 |
文本模型里,“零向量”就是”自己拼一个全零张量当空条件”—— 它和”空字符串的编码”不是一回事。
前者的结果和上表第二行一样:全崩。
8.10.2 三件必须一起换的东西
空条件的定义依赖三个组件,任何一个变了都要重新验证:
| 组件 | 变了会怎样 |
|---|---|
| tokenizer | "" 切出的特殊 token 变了 |
| 文本编码器 | 同样 token 的编码结果变了 |
| 训练时的丢弃率 | 空条件被训练的程度变了 |
第 7 章的上线清单里有一条:“换模型/换编码器之后重新验证”。 在文本模型上,这条要再加一个 tokenizer。
8.11 文本条件下的 CFG
公式一个字都不用改:
\hat\varepsilon=\varepsilon_\varnothing+w\left(\varepsilon_c-\varepsilon_\varnothing\right)
其中 \varepsilon_\varnothing 是空字符串那一路的输出。
8.11.1 实测:这次的引导几乎没有收益
在第 7 章的二维类别任务上,w 从 1 到 2 能把准确率从 0.965 推到 0.998。 本章的绑定任务上呢?
| w | RB 的双槽准确率 |
|---|---|
| 1 | 1.000 |
| 2 | 1.000 |
| 5 | 1.000 |
| 10 | 0.999 |
完全没有变化。
因为 w=1 时准确率已经是 1.000 了。
第 7 章说过:引导放大的是”条件方向”, 当所有样本都已经落在正确的类里,再放大不会提高准确率。
本章的任务更简单(四个模式、两个 token),模型在 w=1 就完全学会了。 引导没有可提升的空间,只有成本。
w=10 时的 0.999 就是那个成本的第一丝迹象。
这不是”CFG 在文本条件下失效”—— 如果换一个更难的提示词(比如需要组合三个属性), w=1 达不到 1.000,引导应该还有收益。
但本章没有测那个场景。 这是一个明确的边界。
8.12 文本条件改变了什么:一个概率的视角
前面都是”怎么算”。这一节退一步问:“文本到底改变了什么分布?”
答案可以用一行贝叶斯写出来,而这一行能解释本章的很多现象。
8.12.1 条件分数 = 无条件分数 + 对齐项的梯度
扩散模型学的不是分布本身,是分数 \nabla_x\log p_t(x)(第 5 章)。
加了条件之后,要学的是 \nabla_x\log p_t(x\mid c)。由贝叶斯公式:
p_t(x\mid c)\propto p_t(x)\,p_t(c\mid x)
两边取对数再求梯度:
\underbrace{\nabla_x\log p_t(x\mid c)}_{\text{条件分数}} =\underbrace{\nabla_x\log p_t(x)}_{\text{无条件分数}} +\underbrace{\nabla_x\log p_t(c\mid x)}_{\text{“这张图有多符合这句话”}}
条件扩散 = 无条件扩散 + 一个”图文匹配”项的梯度。
这个”匹配项” p_t(c\mid x) 就是第 7 章那个隐式分类器—— 只不过那里的 c 是一个类别下标,这里的 c 是一整句话。
所以 CFG 的公式
\hat\varepsilon=\varepsilon_\varnothing+w(\varepsilon_c-\varepsilon_\varnothing)
做的正是”把匹配项的梯度放大 w 倍”。 公式一个字没变,变的只是 c 的表示。
第 7 章的结论全部适用: w 太大 → 分布被改形;条件太弱 → 匹配项太平 → 放大也没用。
8.12.2 为什么”条件太弱”时 CFG 无效
看匹配项 \nabla_x\log p_t(c\mid x)。
如果对所有的 x,p_t(c\mid x) 都差不多—— 也就是”这句话对哪张图都成立”——它的梯度接近零。
这时 \varepsilon_c\approx\varepsilon_\varnothing, CFG 放大的是零。
什么情况下会这样?
- 提示词太泛:“a picture”、“high quality”
- 提示词和图像无关:模型没见过这种配对的训练数据
- 条件被截断:真正有区分度的词在 77 之后
本章的 w 扫描(1 到 10 基本全是 1.000,w=10 时 0.999)也是同一个道理的反面: 模型在 w=1 时已经把匹配项学得很准了,放大它没有额外信息。
8.12.3 为什么 caption 质量决定条件能力
p_t(c\mid x) 是模型从训练数据里学出来的。
如果训练数据的 caption 都是”一张图片”, 那么 p(c\mid x) 对任何 x 都相同——梯度为零。
模型学到的就是”文本没有信息”。
反过来,如果 caption 写得具体、有区分度, p(c\mid x) 在 x 上变化剧烈,梯度大,条件就强。
这解释了两件事:
① 为什么”重新标注”能显著提升模型(SDXL、SD3 都做了)—— 它直接改变了 p(c\mid x) 的锐度。
② 为什么”加更多形容词”不一定有用—— 如果这些形容词在训练数据里和图像不相关, 它们只会让 p(c\mid x) 更平。
8.12.4 一个可以验证的推论
推论:如果在训练时人为地把 caption 和图像打乱配对(随机 caption), 模型应该完全学不会条件——p(c\mid x) 变成常数。
这是一个”应该成立”的预测,本章没有跑。 但它给出了一个明确的实验: 把 caption 的配对随机打乱,重训,看条件是否失效。 如果还部分有效,说明模型从别的地方(比如图像统计)学到了匹配。
它把”文本条件”从一个工程细节,变成了一个概率对象。
你不再问”交叉注意力该怎么写”,而是问: “p(c\mid x) 学得准不准?它在 x 上的梯度大不大?”
前一个问题对应编码器和训练数据, 后一个问题对应提示词的具体程度。
两个问题都比”调 w“更接近根因。
8.13 潜空间:真正发生交叉注意力的地方
本章的”图像”是 4 个数字。真实的图像是 512\times512\times3\approx 78 万个数。
直接在上面做扩散太贵。 所以真实模型先把它压到潜空间。
8.13.1 VAE 做了什么
编码器把 512\times512\times3 压成 64\times64\times4:
512\times512\times3=786{,}432 \quad\longrightarrow\quad 64\times64\times4=16{,}384
压缩了 48 倍。 扩散过程在这个 64\times64\times4 的张量上进行, 采样完再由解码器还原成像素。
三个必须记住的数:
| 量 | 值 | 含义 |
|---|---|---|
| 下采样倍数 | 8 | 512/64 |
| 潜变量通道 | 4 | 而不是 3 |
| scaling factor | ≈0.18215 | 编码后要乘它 |
scaling factor 不是超参数,是 VAE 训练时定下的常数。 用错它,扩散模型看到的就是一个尺度不对的分布。
8.13.2 交叉注意力在潜空间里算什么
潜变量的每个位置(64\times64=4096 个)都是一个 query。
文本是 77 个 token(CLIP 的上限),所以:
\text{注意力矩阵}=4096\times77
约 31 万个元素,每个头一份。
这个规模是可以接受的,但也是为什么交叉注意力只放在 UNet 的 若干层而不是每一层——它是注意力里最贵的部分。
不是像素,是潜变量的一个格子。
一个格子对应原图的 8\times8 区域。所以交叉注意力的分辨率是”8 像素一格”。
这解释了一个常见现象:文字能控制”左边有一只猫”, 但很难精确控制”猫的左耳”——因为耳朵小于一个格子。
VAE 的潜变量为什么是 4 通道,而不是 3?
提示:3 通道正好对应 RGB。
(答案:4 不是”多存了一点颜色”,是”给结构留了一维”。
3 通道会强迫潜空间同时编码”颜色”和”结构”, 两者纠缠在一起,解码时容易互相干扰。
多一维之后,网络可以学出”一维管结构、三维管颜色”这样的分工—— 但这是学出来的,不是设计出来的。 4 这个数是实验选出来的。
更好的答案可能是”没有理论原因”。 4 通道在重建质量和扩散难度之间取得了平衡, 换成 3 或 8 都有人试过,效果不同但没有一条公式告诉你该选几。)
8.13.3 为什么一定要压缩
因为注意力的成本是平方的。
如果直接在 512\times512 的像素上做自注意力, 序列长度是 512\times512=262{,}144——注意力矩阵是 2.6\times10^{10} 个元素。 任何显卡都放不下。
压到 64\times64=4096 之后,矩阵是 1.7\times10^7——可行,但仍然很大。
所以潜空间不是”为了省显存”的小优化,是”能不能跑”的分界线。
交叉注意力的成本是 N\times M,不是 N^2。
- N = 图像位置数(4096)
- M = 文本 token 数(77)
4096 × 77 ≈ 31.5 万,比自注意力的 4096^2\approx 1678 万小 53 倍。
这就是为什么交叉注意力可以放在很多层,而自注意力不行。
8.13.4 VAE 的两个方向
图像 (512×512×3)
↓ 编码器(卷积 + 下采样 3 次)
潜变量 (64×64×4)
↓ × 0.18215 ← scaling factor
扩散模型的输入
...
↓ 采样完成
潜变量 (64×64×4)
↓ ÷ 0.18215
↓ 解码器(上采样 3 次)
图像 (512×512×3)
三次下采样,每次减半:512\to256\to128\to64。 所以一个潜变量格子对应 8\times8 的像素块。
scaling factor 的作用是把潜变量的标准差拉到 1 附近, 让扩散模型的噪声调度(\beta 从 10^{-4} 到 0.02)处在合适的范围。 它是在 VAE 训练完之后、在潜变量上统计出来的一个常数,不是超参数。
模型看到的输入尺度不对,等价于换了一个噪声调度。
症状:生成结果整体偏灰或过饱和,而且怎么调 w 都没用。
判据:编码一张真实图片,看潜变量的标准差。 它应该接近 1。 偏离一个数量级,就是 scaling factor 用错了。
8.14 动手实现:两种编码器的代码
本章的两种编码器各只有十几行。 看懂它们,就看懂了真实模型的骨架。
8.14.1 均值池化
class MeanPoolEncoder(nn.Module):
def __init__(self, hidden):
super().__init__()
self.emb = nn.Embedding(3, hidden) # 0=红 1=蓝 2=NULL
def forward(self, tok, x=None, t=None):
return self.emb(tok).mean(dim=1) # (N, d)mean(dim=1) 就是全部问题所在。 它把”顺序”这个维度平均掉了。
8.14.2 交叉注意力
def forward(self, tok, x, t):
pts = torch.stack([x[:, 0:2], x[:, 2:4]], dim=1) # (N,2,2)
q_in = torch.cat([pts, t[:, None, :].expand(N, 2, -1)], dim=-1)
q = self.q(q_in).view(N, 2, self.n_head, self.dh)
k = (self.emb(tok) + self.pos(...)).view(N, 2, self.n_head, self.dh)
att = torch.einsum("nphe,nkhe->nhpk", q, k) / self.scale
att = att.softmax(dim=-1)
out = torch.einsum("nhpk,nhkd->nphd", att, k.permute(0, 2, 1, 3))
return out.reshape(N, -1)四行,对应注意力公式的四步:
- query 从点和时间步来(
q_in) - key 从 token 嵌入 + 位置来(
k) - 相似度 → softmax(
att) - 加权求和(
out)
注意 self.pos。 token 的位置嵌入是绑定能工作的另一半—— 没有它,两个 token 在 K 里是对称的,模型仍然分不清”第一个词”和”第二个词”。
q 的输入里有 t(时间步嵌入)。
为什么?因为在不同噪声水平上,“该听哪个词”可能不一样: 高噪声时结构还没定,低噪声时细节已经清楚。 让 query 知道自己在哪个阶段,注意力才能随阶段变化。
真实 UNet 里,query 来自图像特征本身(已经包含了时间信息), 所以不显式拼接——但作用是同一个。
8.14.3 接进 UNet 的位置
x_t → [Down Block] → [Mid Block + CrossAttn] → [Up Block + CrossAttn] → ε
↑ ↑
text tokens text tokens
交叉注意力通常插在 Mid 和 Up 的若干层,不在 Down 的最浅层—— 因为那里分辨率太高,4096\times77 会变成 16384\times77。
这是一个纯粹的成本决定,不是理论决定。
8.15 交互实验 8-1 绑定实验室
怎么用。 每个点是一个生成样本,画在”左点 y × 右点 y“的平面上。
- 横轴 >0:左点是红;<0:左点是蓝
- 纵轴 >0:右点是红;<0:右点是蓝
- 四个象限就是四个类
观察引导。
- 选 RB,编码器选均值池化。 样本同时在右下(RB)和左上(BR)两个象限,各占一半。
- 切到交叉注意力。样本只落在右下。
- 回到均值池化,切到 BR。 图案几乎一模一样——因为这两个类拿到的条件向量完全相同。
- 切到 RR 或 BB。两种编码器都是 100%。 词袋唯一确定排列时,绑定不是问题。
同一个词袋,两种顺序,模型给出同一个分布。
这不是”生成质量差”,是”条件里没有信息”。 模型被要求从同一个输入产生两种输出,它只能二选一、或者各出一半。
判据:两个”应该不同”的类,在同一个编码器下呈现出镜像对称的两团。 看到这个图案,就是绑定失败。
8.16 它在哪里失效
按第 6、7 章的格式:材料 → 步骤 → 预期 → 实测 → 判据 → 修法。
8.16.1 失败一:用均值池化处理有顺序的条件
材料:本章的两个模型。
步骤:对 RB 和 BR 各采 300 个样本 × 5 个种子,统计”两个色槽都摆对”的比例。
预期:均值池化”稍微差一点”,交叉注意力”更好”。
实测:
| 模型 | RB | BR |
|---|---|---|
| 均值池化 | 0.550 ± 0.034 | 0.449 ± 0.035 |
| 交叉注意力 | 1.000 | 1.000 |
判据:两个”应该不同”的类,在某个模型上得到接近 50/50 的结果, 而且这两个数加起来约等于 1。这是”条件没有区分度”的指纹, 不是”质量差”。
修法:换成逐位置的条件(交叉注意力),或者至少保留位置信息 (拼接位置嵌入后不做平均)。
8.16.2 失败二:有交叉注意力,但忘了位置嵌入
材料:把 self.pos 从 K 里去掉(variant="no_pos"),参数一个不变。
步骤:重训,重复失败一的测量,再加一次对调实验。
预期:既然有注意力,应该能区分顺序。
实测:
| 指标 | 数值 |
|---|---|
| RB 准确率 | 0.602 |
| BR 准确率 | 0.397 |
| 两者之和 | 0.999 |
| 对调后的最大逐元素差 | 0.000 |
判据:看两个镜像类准确率之和。 \approx 1 就是”完全没用顺序”——每个样本只能在一边算对。
修法:位置嵌入(或任何能区分 token 位置的信号)必须进 K。 这是”注意力”和”顺序”之间的桥。
第一眼看到 0.602 / 0.397,我以为是”部分绑定”。
但对调实验给出 0.000——输出逐位相同。
0.602 是”同一批样本被两边数”的结果: 一个”左红右蓝”的样本,在 RB 下算对、在 BR 下算错。 两个数加起来正好 0.999。
如果只测准确率,会得出”位置嵌入有帮助但不是必需”的错误结论。 是干预实验把它纠正过来的。
很多人以为”用了注意力就自动有顺序”。
不是。 注意力本身是置换等变的: 把 K 的顺序打乱,输出只是跟着打乱,不会”知道”顺序变了。
顺序信息必须显式注入——位置嵌入、因果掩码、或者位置相关的 query。 Transformer 里的位置编码,和这里是同一个理由。
8.16.3 失败三:空字符串用零向量
材料:第 7 章实验四的结论,迁移到文本条件。
步骤:把无条件分支实现成全零张量(而不是空串编码)。
预期:w 小时可能还行。
实测(第 7 章):w=3 时越界 100%,w=5 时多样性归零。 准确率仍然 1.000。
判据:w=0 时输出应该和提示词无关。如果 w=0 仍然”听话”,或者崩坏, 说明无条件分支不对。
修法:用编码器对空串的输出,不要自己拼零向量。
8.16.4 失败四:换了 tokenizer 没有复验
材料:任何换过 tokenizer 或文本编码器的模型。
步骤:w=0 出 10 张,w=1 出 10 张,w=3 出 10 张。
预期:和第 7 章验过的行为一致。
实测(本章未做跨编码器实验):空字符串切出的特殊 token 会变, 编码结果也会变,所以”无条件”的语义变了。
判据:w=0 的输出分布和换之前明显不同。
修法:把”空条件向量”存下来,换组件时对比。 如果两个向量的余弦相似度很低,就必须重新定标 w。
8.16.5 失败五:提示词超长被截断
材料:CLIP 的 77 token 上限。
步骤:写一段 100 个 token 的提示词。
预期:模型”读到全部”。
实测(真实模型上的已知行为,本章未复现):77 之后的 token 被丢掉, 而且丢掉的往往是句尾的关键修饰语。
判据:把提示词从 60 token 加到 90 token,输出没有变化。
修法:把最重要的词放在前面;或者用支持长上下文的编码器(T5、Qwen)。
8.16.6 失败六:用注意力图调试
材料:本章的交叉注意力模型。
步骤:看注意力权重,判断”左点在听哪个词”。
预期:左点应该主要看第一个 token。
实测:权重全在 0.44–0.58 之间;用权重猜左色的准确率 47.0%, 低于抛硬币。
判据:注意力图和”输出是否正确”对不上。
修法:改用消融——换词、删词、调换顺序,看输出怎么变。 行为层面的证据,不依赖对权重的解读。
8.17 同一个注意力,三种用法
第 1 章的自注意力、本章的交叉注意力、SD3 的联合注意力, 公式是同一个:
\text{Attn}(Q,K,V)=\text{softmax}\left(\frac{QK^\top}{\sqrt d}\right)V
区别只在 Q,K,V 从哪来。
| 自注意力(第 1 章) | 交叉注意力(本章) | 联合注意力(MMDiT) | |
|---|---|---|---|
| Q 来自 | 同一序列 | 图像 | 图像 + 文本 |
| K,V 来自 | 同一序列 | 文本 | 图像 + 文本 |
| 序列长度 | N | N 与 M | N+M |
| 矩阵大小 | N\times N | N\times M | (N+M)^2 |
| 谁被更新 | 全部 | 只有图像 | 全部 |
| 典型位置 | 每个 Block | UNet 的若干层 | 每个 Block |
三行值得展开。
8.17.1 第一行:谁被更新
自注意力里,每个 token 都更新自己。 交叉注意力里,只有图像被更新——文本 token 是只读的。
这是一个不对称的设计。 文本提供信息,但不接收信息。所以文本的表示在整个 UNet 里保持不变, 每一层看到的都是同一份 K,V。
好处:可以缓存。77 个 token 的 K,V 算一次,所有层复用。 坏处:文本不能根据图像调整自己——“这只猫”没法在生成过程中变成”这只橘猫”。
联合注意力取消了这个不对称:文本也会被图像更新。 代价是文本表示不再可缓存,每一层都要重算。
8.17.2 第二行:矩阵大小
N\times M 和 (N+M)^2 的差别,在 M\ll N 时很小。
前面算过:N=4096、M=154 时,联合注意力只比”自注意力 + 交叉注意力”多 3.8%。
但如果 M 接近 N,差别就大了。 比如图像编辑任务里,参考图也被编码成 4096 个 token—— 这时的联合注意力是 (8192)^2,是原来的 4 倍。
这就是为什么”参考图 + 文本”的多图条件模型特别吃显存。
8.17.3 第三行:掩码
三种用法都能加掩码,但掩码的含义不同。
| 用法 | 常见掩码 | 作用 |
|---|---|---|
| 自注意力(语言模型) | 因果掩码 | 不能看未来 |
| 自注意力(图像) | 无 | 全图互相看 |
| 交叉注意力 | 区域掩码 | 某块区域只能看某些词 |
| 联合注意力 | 模态掩码 | 文本不能看图像(可选) |
因果掩码是”结构性的”(第 1 章的 CLIP 文本编码器用它), 区域掩码是”任务性的”(本章”空间条件”一节)。
同一个 masked_fill(-inf),两种完全不同的用意。
三种注意力都是”用 Q 去 K 里找 V“。
- 自注意力:在自己里面找
- 交叉注意力:去别人那里找
- 联合注意力:把自己和别人拼在一起找
所以”注意力机制”不是一种结构,是一个查询接口。 换的永远是”查什么”和”谁能查”,不是公式。
看懂这一点,你拿到任何新架构(DiT、MMDiT、U-ViT、VAR) 都能在三十秒内定位它的条件路径。
8.18 条件注入的五代做法
“文字怎么进模型”不是一个问题,是五个。 每一代做法解决前一代的一个具体缺陷,同时也带来一个新的代价。 看懂这张谱系,你拿到任何一个新模型都能立刻定位它在哪一格。
| 代 | 做法 | 条件粒度 | 每层参数(d=320, 文本 768) | 代表 |
|---|---|---|---|---|
| ① | 拼接(concat) | 全局 | 348,160 | 早期条件 GAN、class-conditional diffusion |
| ② | 相加 / AdaGN | 全局 | 245,760 / 491,520 | 类别条件、DiT 的 adaLN |
| ③ | 交叉注意力 | 逐位置 | 696,320 | SD 1.x / 2.x / XL |
| ④ | 联合注意力(MMDiT) | 逐位置 + 双向 | 见下 | SD3、FLUX |
| ⑤ | 旁路注入 | 结构 / 参考图 | 另算 | ControlNet、IP-Adapter |
(① 的 348,160 = (320+768)\times320,因为它要把拼接后的 1088 维投回 320 维; ② 的 245,760 = 768\times320,只是一次投影。)
这张表最容易误读的是”每层参数”那一列。 ③ 比 ② 只贵 40%,为什么大家说交叉注意力”贵”? 因为参数不是成本,计算才是。 ② 每层做一次 d 维加法, ③ 每层要做 N\times M 次点积——N=4096、M=77 时是 31 万次。 贵的是那 31 万次,不是那 69 万个参数。
8.18.1 ① 拼接:最早,也最笨
做法:把条件向量复制到每个空间位置,拼在通道维上,再过一层卷积压回去。
特征 (B, 320, 64, 64) ⊕ 文本 (B, 768) 广播到 (B, 768, 64, 64)
↓ 拼接
(B, 1088, 64, 64)
↓ 1×1 卷积
(B, 320, 64, 64)
问题:条件向量是全局的,每个位置拿到的完全一样。 它能说”这是一只猫”,不能说”猫在左边”。
这一代没有消失——今天你仍能在一些轻量模型里看到它,因为它最便宜。
8.18.2 ② 相加与 AdaGN:让条件调制特征
相加:h \leftarrow h + W c。零参数以外的开销,但表达力也最弱。
AdaGN(自适应组归一化):先把特征归一化,再用条件算出逐通道的缩放和平移:
\text{AdaGN}(h,c)=\gamma(c)\cdot\frac{h-\mu}{\sigma}+\beta(c)
它比相加强在哪:条件不再只是”加一个偏移”, 而是乘性调制——可以放大某些通道、压掉另一些。 这相当于让条件控制”哪些特征重要”,而不只是”往特征里掺一点东西”。
DiT 把这一招用到了极致:每个 Block 的 LayerNorm 后面接一个 \text{adaLN}(c),一次算出 6 组参数(注意力和 MLP 各三组:shift、scale、gate)。
算一笔账(SD3 Medium,d=1536,池化文本 2048 维):
2048\times(6\times1536)=2048\times9216=18{,}874{,}368
每个 Block 一千九百万参数,24 个 Block 就是 4.5 亿。 这比很多人以为的”AdaGN 很便宜”要贵得多——因为 DiT 的 d 大、而且要出 6 组。
①② 两代的共同点是:整张图共用同一个条件向量。
它们能表达”是什么”,不能表达”在哪里”。 本章的 RB/BR 实验就是这条天花板的直接证明: 条件向量相同 → 输出只能二选一。
这不是模型不够大,是信息在入口就没了。
8.18.3 ③ 交叉注意力:每个位置自己问
这是本章的主角,也是 Stable Diffusion 1.x/2.x/XL 的做法。
它解决 ①② 的方式不是”更强的调制”,是”取消全局”—— 不再有一个”整图条件向量”,而是每个位置现算一个。
代价刚才算过:每层 N\times M 次点积。
一个常被忽略的事实:交叉注意力的参数量对 M 不敏感。 K、V 的投影是 d\times d_{\text{ctx}},和 token 数无关。 所以 77 个 token 和 7 个 token,参数一样多,计算差 11 倍。
8.18.4 ④ 联合注意力:把两串 token 拼成一条
SD3 的 MMDiT 做了一件很反直觉的事:它取消了交叉注意力。
做法:把文本 token 和图像 token 拼成同一个序列,做一次自注意力:
[文1 文2 … 文77 | 图1 图2 … 图4096] ← 一条序列,4250 个 token
↓ 一次自注意力(每个 token 看所有 token)
↓ 各自过自己的 FFN
[文1' … | 图1' …]
“双向”是关键词:图像能看文本,文本也能看图像。 交叉注意力里文本是只读的(Q 只来自图像),联合注意力里文本会被图像更新。
为什么这可行?算一下成本。
设 N=4096(图像 token)、M=154(CLIP 77 + T5 77)。
| 方案 | 注意力元素数 |
|---|---|
| 自注意力 + 交叉注意力(SDXL 式) | N^2+NM=16{,}777{,}216+630{,}784=17{,}408{,}000 |
| 联合注意力(MMDiT 式) | (N+M)^2=4250^2=18{,}062{,}500 |
只多了 3.8%。
很多人以为”联合注意力是 4250^2,太贵了”。
但基线不是 N^2,是 N^2+NM——因为原来的模型本来就要做自注意力。 把两条注意力合成一条,省掉了一次独立的 QK^\top, 代价只是多出来的 M^2 项(154^2=23{,}716,占 0.1%)。
所以 MMDiT 不是”更贵的架构”,是”用同样的预算买到了双向信息流”。
这也是为什么 SD3 之后的模型几乎都换成了联合注意力—— 它不是奢侈,是划算。
代价在别处:文本 token 现在也要过 FFN,也要占显存; 而且文本表示会被图像”污染”——第 30 层之后,文本 token 里装的不再只是语义。 这对可解释性是个坏消息(本章的注意力图问题在这里更严重)。
8.18.5 ⑤ 旁路注入:不改主干,加一条支路
ControlNet 和 IP-Adapter 解决的不是”文字怎么进”,是”别的东西怎么进”。
ControlNet(2023):复制一份 UNet 的编码器,原模型冻结, 副本接收额外条件(边缘图、深度图、姿态), 通过零初始化卷积把副本的输出加到原模型上。
h_{\text{out}}=h_{\text{orig}}+\mathcal{Z}(h_{\text{copy}})
零卷积的意义:训练开始时 \mathcal{Z}\equiv0,输出和原模型逐位相同。 这保证了”加条件”不会破坏已经学好的生成能力—— 从一个”什么都不加”的模型开始,慢慢学会加。
IP-Adapter(2023):解耦交叉注意力。 在原来的文本交叉注意力旁边,再挂一个图像交叉注意力:
\text{out}=\text{Attn}(Q,K_{\text{text}},V_{\text{text}}) +\lambda\cdot\text{Attn}(Q,K_{\text{img}},V_{\text{img}})
文本那一路冻结,只训图像那一路,\lambda 控制参考图的强度。
因为它们都不改主干的权重。
- ControlNet:主干冻结,只训副本
- IP-Adapter:主干冻结,只训新增的 K,V 投影
代价是参数量:ControlNet 的副本约等于半个 UNet; IP-Adapter 只有几十兆,因为它只加了两组投影。
这也是判断一个”新条件”该用哪种注入的实用准则: 要改的是”结构”(边缘、深度、姿态)→ ControlNet; 要改的是”内容/风格”(参考图)→ IP-Adapter; 要改的是”语义”(文字)→ 交叉注意力或联合注意力。
这张谱系到这里就完整了。 本章剩下的内容,都是在 ③ 这一格上做深—— 因为它是”文字控制图像”最直接、也最容易讲清楚的一种。
8.19 现场排错:十个症状
失败模式那一节讲的是”机制上会怎么坏”。这一节讲”坏了怎么查”。
每一条都按同一个格式:症状 → 先做一个三十秒的检验 → 它排除了什么。
8.19.1 症状一:改了提示词,图完全没变
三十秒检验:固定种子,把提示词整个换掉(不是改一个词),出两张。
如果还是没变,问题不在”某个词没生效”,在文本根本没进模型。
按可能性排序:
- 条件传错了位置——
encoder_hidden_states传成了空 - CFG 的 w=0——两个分支被加权成”只用无条件”
- 文本缓存没刷新——改了提示词但复用了旧的 embedding
- 模型是纯无条件模型——根本没有文本输入
判据:w=0 和 w=1 的输出应该不同。相同,就是条件没接上。
8.19.2 症状二:w 越大,图越”焦”
症状:颜色过饱和、边缘发硬、构图趋向居中、多样性下降。
三十秒检验:w 从 1 扫到 10,看多样性(同样提示词不同种子的差异)怎么变。
这是过引导的典型曲线:准确率上升→平台→下降,多样性单调下降。
修法:降 w;或者用第 7 章的 rescale;或者用 w 随步数递减的调度。
不要做的:继续加 w 试图”让提示词更听话”。 过了平台期,加 w 只会更糟。
8.19.3 症状三:颜色整体偏灰或偏亮
三十秒检验:编码一张真实图片,看潜变量的标准差。应该接近 1。
偏离一个数量级 → scaling factor 用错了,或者根本没除。
另一个可能:init_noise_sigma 用错。 两者都会表现为”整体色调不对,且调 w 无效”。
8.19.4 症状四:提示词里明明写了,图里没有
三十秒检验:把这个词删掉,看输出变不变。
不变 → 这个词没被用起来。三个原因:
| 原因 | 怎么确认 |
|---|---|
| 被截断(超过 77) | 把它挪到句首,看是否出现 |
| tokenizer 切碎了 | 打印 token 序列,看它变成几个 token |
| 训练数据里这个概念很少 | 换一个同义但更常见的词 |
第二种最隐蔽:一个生僻词被切成 5 个 token, 每个 token 单独看都没有意义,模型自然拼不出这个概念。
8.19.5 症状五:两个物体的颜色串了
这是本章的属性泄漏。
三十秒检验:把提示词改成两个单独的句子,分两次生成, 再用图像编辑合成。如果分开生成是对的,合起来是错的, 就是交叉注意力里的竞争,不是模型不认识颜色。
修法:区域提示词、结构化引导、或者分步生成。
8.19.6 症状六:位置总在画面中间
三十秒检验:在提示词里加”on the far left”,看物体是否移动。
移动了一点但不到位 → 文本对位置的控制是统计性的(本章”空间条件”一节)。 完全不动 → 模型的空间概念很弱,需要外部条件(ControlNet / 区域掩码)。
8.19.7 症状七:换了一个模型,同一个提示词效果差很多
这不是 bug。 两个模型的 caption 分布不同(本章”描述文字”一节)。
三十秒检验:把提示词改写成新模型的风格(关键词 vs 自然语言),再试。
如果改写法之后恢复了,说明是分布匹配问题,不是模型能力问题。
8.19.8 症状八:中文提示词效果明显差于英文
三个叠加的原因:
- tokenizer 对中文效率低——同样一句话占更多 token
- 训练数据以英文为主——中文描述在 caption 分布里很少
- 文本编码器的中文语义弱——CLIP 的中文能力来自翻译数据,不是原生
三十秒检验:把同一句中文机翻成英文,对比输出。
如果英文明显更好,三个原因都可能;如果差不多,只是 tokenizer 的问题。
8.19.9 症状九:负面提示词把主体也删了
机制:负面提示词参与的是 \varepsilon_{\text{neg}} 那一路, 它和正面提示词共享同一个 softmax 空间。
如果负面词和主体在语义上接近(“不要模糊的猫”里的”猫”), 它会把主体一起推走。
三十秒检验:把负面提示词逐条删掉,看哪一条导致主体消失。
修法:负面词只写”属性”,不写”主体”。
8.19.10 症状十:同样的参数,两次结果不一样
按可能性排序:
- 种子没固定(最常见)
- dtype 不同(fp16 vs bf16 的舍入不同)
- 调度器的随机性(有些采样器每步加噪声)
- 批处理顺序影响了 RNG(一次出 4 张和分 4 次出,噪声序列不同)
先排除”没接上”,再排除”接错了”,最后才怀疑”模型不行”。
- 条件在不在?(症状一)
- 尺度对不对?(症状三)
- 有没有被截断/切碎?(症状四)
- 是不是分布不匹配?(症状七、八)
- 才是机制问题(症状五、六)
大部分”模型不听话”的报告,停在前两步。
8.20 设计你自己的条件接口:六个问题
假设你要给一个扩散模型加一种新条件——不是文字,可能是点击、骨架、音频、布局。
别急着写代码。先回答六个问题。 每一个问题都会砍掉一半的设计空间。
8.20.1 问题一:这个条件是”全局的”还是”逐位置的”?
“这张图是一只猫”是全局的。“猫在左上角”是逐位置的。
全局条件用 AdaGN 或相加就够了——每层一次 d 维调制,便宜。 逐位置条件必须用交叉注意力或联合注意力——每层 N\times M,贵。
但要注意:全局条件不等于”弱条件”。 它只是不能表达”哪里”。如果任务不需要位置,别为它付交叉注意力的钱。
8.20.2 问题二:条件是一串还是一个?
一个向量(类别、池化文本、音频片段)→ 拼接 / AdaGN。 一串向量(文本 token、骨架关键点、布局框)→ 交叉注意力。
中间情况:固定长度的一小组向量(比如 10 个关键点)。 可以拼平成一个大向量,也可以用交叉注意力。 拼平的代价是模型要自己学会”第 3 个点和第 4 个点不该混”—— 这正是本章讲的绑定问题。
8.20.3 问题三:需不需要双向?
文本 → 图像是单向的:文本不变,图像被更新。
如果你的条件也需要”看图像”(比如”根据当前画面决定下一步画什么”), 就需要双向——联合注意力,或者让条件也做一次自注意力。
双向的代价: 条件表示每一层都变,不能缓存;而且条件会被图像”污染”, 调试时更难判断”是条件错了还是模型错了”。
8.20.4 问题四:条件在采样过程中会变吗?
文字不变:同一个 K,V 用 1000 次。
有些条件会变: - 视频生成里的”上一帧” - 编辑任务里的”原图” - 强化学习里的”当前状态”
会变的条件下,缓存失效,每一步都要重算。 这是视频模型比图像模型贵得多的一个原因—— 不只是帧数多,是条件也不能复用。
8.20.5 问题五:训练时怎么”丢弃”这个条件?
CFG 需要一个无条件分支。所以训练时必须随机丢弃条件。
丢弃率不是一个无关紧要的超参数:
- 太低(1%):模型很少见到无条件,\varepsilon_\varnothing 学不好 → w 一大就崩
- 太高(50%):一半的训练样本浪费在”无条件”上 → 条件能力弱
- 常用值 10%:经验平衡点
而且丢弃必须是”整段丢弃”,不能丢一半留一半—— 否则模型学到的”无条件”是”部分条件”,和推理时用的空条件不匹配。
8.20.6 问题六:多种条件怎么组合?
真实系统常常同时有文字、参考图、姿态、区域掩码。
三种组合方式:
| 方式 | 做法 | 问题 |
|---|---|---|
| 串联 | 条件 A 的输出当条件 B 的输入 | 误差累积 |
| 并联相加 | \sum_i \lambda_i c_i | 尺度难调,条件之间会抵消 |
| 解耦注意力 | 每种条件一组 K,V,输出相加 | 参数多,但可控 |
IP-Adapter 用的是第三种(本章”条件注入”一节)。 它最好调:每个 \lambda_i 独立,关掉一个不影响另一个。
先问”全局还是逐位置”——它决定了架构。 再问”一串还是一个”——它决定了张量形状。 最后才问丢弃率和组合——它们是训练和推理的细节。
很多人从”用什么注意力变体”开始设计,这是反的。 先确定信息要表达什么,再选结构。
8.21 文字说不清的地方:空间条件
本章一直在讲”左边红、右边蓝”。真实提示词里写”左边一只猫,右边一只狗”会怎样?
大概率两只都在中间,或者位置随机。
这不是模型不听话,是文字这个接口本身不擅长表达坐标。
8.21.1 三个结构性的原因
① 文本里没有坐标。 “左边”是一个相对词,它的所指依赖画布。 模型必须从训练数据里学会”左边 = 图像的左半部分”, 而这个映射在数据里是统计性的,不是硬约束。
② 交叉注意力是软的。 每个潜位置对 77 个 token 做加权平均。 即使”猫”这个 token 的权重在左侧更高,它也不会把其它位置清零。 softmax 永远给出一个概率分布,不给”只在这里”。
③ 分辨率不匹配。 一个潜格子对应 8\times8 像素。“猫的左耳”小于一个格子, 文字再精确也落不到那个尺度上。
很多人以为加更多形容词就能控制位置。
不会。 因为问题不在”文字的含义”,在”接口的带宽”—— 77 个 token 到 4096 个位置,是一条多对多的软连接,没有坐标通道。
要精确控制位置,必须加一条新的通道。
8.21.2 做法一:区域提示词(regional prompting)
思路:把画布切成几块,每块用一段独立的提示词,在交叉注意力里加掩码。
设区域 r 的掩码是 M_r\in\{0,1\}^{N}(哪些潜位置属于这个区域), 则区域 r 的交叉注意力改成:
a_{i,k}^{(r)}=\frac{M_{r,i}\exp(q_i^\top k_k/\sqrt d)} {\sum_{j}M_{r,j}\exp(q_i^\top k_j/\sqrt d)}
M_{r,i}=0 的位置,这个区域的词根本进不来。
实现上就是在 softmax 之前把 logits 加上 \log M(0 变 -\infty)。
代价:
| 问题 | 原因 |
|---|---|
| 接缝 | 硬掩码在边界处不连续 |
| 区域内的词仍然软 | 区域内还是加权平均 |
| 多区域冲突 | 重叠区域的归一化要重新设计 |
所以实践里常用”软掩码”:把 0/1 换成高斯权重,边界羽化几个格子。
8.21.3 做法二:把坐标变成 token(GLIGEN)
GLIGEN(2023)的做法更彻底:给每个框一个 token。
[猫][框 x1,y1,x2,y2] [狗][框 x1,y1,x2,y2] ...
框坐标经过傅里叶编码变成一个向量,和词嵌入拼在一起, 再通过一个门控交叉注意力注入 UNet:
h\leftarrow h+\tanh(\gamma)\cdot\text{Attn}(h,K_{\text{ground}},V_{\text{ground}})
\gamma 初始化为 0——和 ControlNet 的零卷积同一个思路: 从”完全不用”开始,学会用多少。
它比区域提示词强在哪:框是可学习的连续量,不是硬掩码。 代价:需要带框标注的训练数据。
8.21.4 做法三:用另一张图当条件(ControlNet)
如果”位置”其实来自一张参考图(草图、深度图、姿态), 那就不是文本的问题,是结构条件的问题。
这正是 ControlNet 的定位(上一节 ⑤)。
三种做法对比:
| 做法 | 输入 | 精度 | 需要什么 |
|---|---|---|---|
| 区域提示词 | 文本 + 掩码 | 中(格子级) | 无额外训练(免训) |
| GLIGEN | 文本 + 框 | 高 | 带框数据 |
| ControlNet | 文本 + 结构图 | 最高 | 成对数据(图-结构) |
问自己:这个约束能不能用”词”表达?
- 能(“红色的猫”)→ 文本条件,别加复杂度
- 不能,但能用”哪块区域”表达(“左上角有太阳”)→ 区域提示词
- 不能,且需要精确形状(“这个人的姿势”)→ ControlNet
三种条件的成本差一个数量级。先用最便宜的那个。
本章讲的交叉注意力,是这三条通道里最基础的一条。 另外两条不是替代它,是在它上面加约束。
8.22 属性泄漏:当”红色”跑到球上
比”位置不对”更常见的一种失败:颜色、材质、大小串到别的物体上。
提示词:a red cube and a blue sphere(一个红方块和一个蓝球)
常见输出:一个紫方块和一个蓝球,或者一个红方块和一个紫球。
两个物体的颜色被平均了。
8.22.1 机制:一次 softmax,所有词竞争
交叉注意力的每个位置只做一次 softmax。
对一个落在方块上的潜位置,它的 query 要和所有 token 打分: a、red、cube、and、a、blue、sphere。
“red”和”blue”都会得分,“cube”和”sphere”也都会得分。 softmax 之后,这个位置可能同时拿到”红”和”蓝”的成分—— 于是它变成了紫色。
理想情况应该是:
a_{\text{方块},\text{red}}\approx 1,\qquad a_{\text{方块},\text{blue}}\approx 0
实际是:
a_{\text{方块},\text{red}}\approx 0.4,\qquad a_{\text{方块},\text{blue}}\approx 0.3
剩下的份额给了 a、and 这些虚词。
模型完全知道红和蓝是什么。 它的问题是:没有一个机制强制”红色只属于方块”。
文本编码器输出的是一串 token,token 之间没有”修饰关系”这张图。 “red”和”cube”在序列里相邻,但交叉注意力不知道”相邻 = 修饰”。
本章的 RB/BR 实验是这个问题的最小版本: 两个 token、两个位置,模型都可能绑错。 真实提示词有 77 个 token、几十个位置,难度是指数级的。
8.22.2 为什么加大 w 不解决
引导放大的是”条件方向与无条件方向之差”。
如果条件本身把红和蓝混在一起,放大只是把”紫色”放大得更饱和。
这和第 7 章的结论一致: 引导不能创造信息,只能放大已有的方向。
8.22.3 三种针对性的做法
① 拆分提示词,分别算注意力(Structured Diffusion Guidance, 2023)
把一句话解析成若干子句:
a red cube | a blue sphere
每个子句单独过文本编码器,得到各自的 K,V, 再在交叉注意力里按空间掩码分配给不同区域。
它把”竞争”变成了”分区”。
② 强制每个主语都被注意到(Attend-and-Excite, 2023)
做法:在采样过程中监控每个主语 token 的最大注意力, 如果某个主语的最大注意力低于阈值,就优化潜变量让它升上去。
\mathcal{L}_{\text{attn}}=\sum_{i}\max\left(0,\ \tau-\max_{p}a_{p,\text{subject}_i}\right)
注意这是在第 6 章的采样循环里加一步优化—— 它改的是采样过程,不是模型权重。
③ 让大模型重写提示词
用 LLM 把”红方块和蓝球”改写成更明确的描述, 或者拆成多个生成步骤再合成。
它有用,但没有解决机制——只是把难题换成了一个更容易的表述。 而且在”必须精确保持用户原话”的场景下不能用。
8.22.4 怎么测量属性泄漏
一个不依赖人工的协议:
- 准备 N 个”两物体两属性”的提示词(颜色、材质、大小各一组)
- 每个提示词生成 M 张
- 用一个检测器(或人工)判断:每个物体的属性是否与提示词一致
- 报告属性准确率,而不是”整图像不像”
关键:要分别报告”物体 A 的属性”和”物体 B 的属性”。 只看总体,会把”一个对一个错”平均成 50%,看不出泄漏的方向。
第 7 章:整体准确率 1.000,但样本已经冲出数据范围。 本章:CLIP score 很高,但两个物体的颜色串了。
两个例子说的是同一件事: 一个把结构平均掉的标量指标,会在结构出错时保持漂亮。
所以评估必须”分槽”——本章的双槽准确率就是一个分槽指标。
8.23 文本编码器怎么选
真实项目里,这一步通常不是”设计”,是”继承”——你用的底座模型已经定了编码器。 但知道取舍,能帮你判断它为什么在某些提示词上失灵。
8.23.1 三种主流选择
| 编码器 | 输出 | 典型用途 | 特点 |
|---|---|---|---|
| CLIP | 77×d 序列 + 一个池化向量 | Stable Diffusion 1.x/2.x | 图文对比学习,擅长”像不像” |
| T5 | 变长序列 | Imagen、SD3 | 纯文本预训练,擅长长句与组合 |
| Qwen-VL 等 | 变长序列 | Qwen-Image | 多语言、长上下文 |
CLIP 的关键限制是 77 个 token。 超出会被截断。 T5 没有这个硬限制(相对位置编码),所以 SD3 换成了 T5。
8.23.2 三个要问的问题
① 输出是序列还是向量?
- 序列:可以接交叉注意力(本章的做法)
- 向量:只能接 AdaGN 或拼接(第 7 章的类别条件)
CLIP 两种都有:last_hidden_state(序列)和 pooler_output(池化)。 SD 1.x 用的是序列,池化向量被丢掉了——因为序列的信息更多。
② 是不是冻结的?
大多数文生图模型冻结文本编码器,只训 UNet。 好处:省显存、不用文本-图像配对数据。 代价:编码器不懂图像生成需要什么—— 比如它不知道”红色”对像素意味着什么。
③ 空串编码是什么?
不同编码器对 "" 的处理不同: CLIP 给 [BOS][EOS],T5 可能给一个 [PAD] 序列。 换编码器必须重新验证 w=0(失败四)。
文本编码器通常不参与扩散训练。
它输出的向量分布是固定的,UNet 只是学会”怎么用这些向量”。 所以:
- 换编码器 = 换了一门语言,UNet 得重训
- 微调编码器 = 改变了这门语言的词义,同样要重训 UNet
“只换编码器不重训”是不成立的,除非两个编码器的输出分布几乎一样。
8.24 CLIP 是怎么训出来的(以及它为什么不懂顺序)
上一节说”编码器通常是继承的”。那它是怎么来的?
绝大多数文生图模型用的文本编码器,都是对比学习训出来的。 理解这个目标函数,你就能预测它在哪类提示词上会失灵。
8.24.1 一个批次,一个矩阵
训练数据是图文对:(I_1,T_1),\dots,(I_N,T_N)。
两个塔:图像塔把 I_i 编成 u_i,文本塔把 T_i 编成 v_i,都归一化到单位长度。 相似度就是点积 s_{ij}=u_i^\top v_j。
一个批次 N=32{,}768,就得到一个 32768\times32768 的相似度矩阵。 对角线是”配对的”,其余 10^9 个格子全是”不配对的”。
损失(InfoNCE,两个方向对称):
\mathcal{L}=-\frac{1}{N}\sum_{i=1}^{N}\log \frac{\exp(s_{ii}/\tau)}{\sum_{j=1}^{N}\exp(s_{ij}/\tau)}
\tau 是温度,CLIP 里初始化为 0.07,并且可学习。
8.24.2 先算一遍:三个样本
设某张图与三条文本的相似度是 0.30,\ 0.10,\ -0.10,\tau=0.07。
第一步,除以温度:
0.30/0.07=4.29,\qquad 0.10/0.07=1.43,\qquad -0.10/0.07=-1.43
第二步,softmax:
p=\frac{e^{4.29}}{e^{4.29}+e^{1.43}+e^{-1.43}} =\frac{72.9}{72.9+4.17+0.24}=0.943
第三步,取负对数:
\mathcal{L}=-\log 0.943=0.059
现在把正确的那条换掉——让第二列成为正样本:
p=\frac{e^{1.43}}{72.9+4.17+0.24}=0.054,\qquad \mathcal{L}=-\log 0.054=2.916
同一个相似度矩阵,正样本选对了损失 0.059,选错了 2.916——差 49 倍。
它只奖励一件事:在这一批里,把正确的那个挑出来。
- 拉近配对:s_{ii} 变大
- 推远批内其他所有:s_{ij} 变小
注意加粗的”批内”。 负样本不是精心挑的,是同一批里恰好排在一起的。 这决定了模型学到什么、学不到什么。
8.24.3 为什么这解释了”不懂顺序”
问一个问题:模型有没有动力去区分”红方块在蓝球上”和”蓝方块在红球上”?
只有当这两条描述同时出现在一个批次里、且分别配着不同的图时,才有动力。
算一下概率。
- 训练集 4 亿对
- 批次 32,768
- 随机抽到某条描述”顺序对调版”的概率:
\frac{32{,}768}{4\times10^{8}}=8.2\times10^{-5}
一万次里不到一次。
所以对比学习几乎从不要求模型区分词序。 它要求的是”把这一批的 3 万条描述彼此区分开”—— 而这一批里,绝大多数差异是”猫 vs 狗 vs 汽车”,不是”猫在左 vs 猫在右”。
结果:CLIP 的文本表示对内容敏感,对结构迟钝。
这不是 bug,是目标函数没有要求。 2023 年的一项系统研究标题就叫《视觉语言模型何时以及为何表现得像词袋》 (When and Why Vision-Language Models Behave Like Bags-of-Words), 结论和上面的推理一致:交换主语和宾语,CLIP 的相似度几乎不变。
本章的均值池化是一个”故意做错”的模型。 CLIP 不是均值池化——它有 12 层自注意力,能看到顺序。
但它的训练目标没有逼它用顺序。 所以它在”红在左还是红在右”这类问题上,表现得像一个很贵的词袋。
这就是为什么 SD3 要加一个 T5: T5 是在纯文本上做去噪预训练的,预测被挖掉的词必须看上下文, 所以它对语法结构敏感得多。 两个编码器并联,是在”语义对齐”和”结构敏感”之间取长补短。
8.24.4 模态鸿沟:一个附带现象
训练完之后,图像向量和文本向量并不在同一个区域。
它们的方向能对齐(点积大),但范数分布不同—— 图像向量倾向于聚集在更窄的锥形里,文本向量铺得更开。 这个现象叫模态鸿沟(modality gap),2022 年被系统测量过。
对文生图的影响:
- 文本编码器的输出分布是”偏”的,UNet 必须学会适应
- 这正是”换编码器要重训 UNet”的深层原因——不是维度对不上,是分布对不上
- 也是 scaling factor 那类”看起来多余、其实必须”的常数的来源
8.24.5 三个可以直接用的推论
① 提示词里的词越”独特”,CLIP 越敏感。 因为对比学习奖励的是区分度。“一只猫”里的”一只”没有区分度, “猫”有。所以删掉虚词通常不掉点,删掉名词立刻变。
② 同义词替换通常是安全的,语序调整通常不安全。 同义词在原训练分布里经常互换出现(“小猫/猫咪”), 语序对调几乎从不作为负样本出现。
③ 否定词是已知的弱项。 “没有猫”和”猫”在词袋层面高度重叠,CLIP 区分它们的能力很差。 这是”负面提示词”要用单独机制的另一个原因(第 7 章的负向引导)。
8.25 教模型一个新词:三种改法,三种代价
前面说”换编码器要重训 UNet”。那如果只是想让它认识一个新概念呢?
比如你自己的脸、一个原创角色、一种画风。
有三种做法,改的东西一层比一层多。
8.25.1 ① Textual Inversion:只学一个向量
做法(Gal et al., 2022):选一个占位词 <my-concept>, 只优化它的词嵌入向量,模型其余部分全部冻结。
\mathcal{L}=\mathbb{E}_{t,x_0,\varepsilon}\left\|\varepsilon-\varepsilon_\theta \left(x_t,t,\ \text{Enc}(\texttt{<my-concept>})\right)\right\|^2
要学的参数只有一个 768 维向量——768 个数。
对比一下:UNet 是 8.6 亿参数。比例是 9\times10^{-7}。
为什么这么少也能work? 因为文本编码器的输出空间里, 已经存在”能描述这个概念”的点——只是没有一个词指向它。 Textual Inversion 做的事,是在空间里找那个点,不是创造新知识。
它的边界也在这里: 如果概念需要”新的组合方式”(比如”这个人穿这种盔甲”), 一个向量装不下。它擅长”是什么”,不擅长”怎么组合”。
8.25.2 ② LoRA:给权重加一个低秩补丁
做法:不动原权重 W,在旁边加一个低秩分解:
W'=W+\frac{\alpha}{r}BA,\qquad B\in\mathbb{R}^{d\times r},\ A\in\mathbb{R}^{r\times k}
r 通常取 4–64。
参数量从 d\times k 降到 r(d+k)。 以 d=k=768、r=8 为例:
768^2=589{,}824\quad\longrightarrow\quad8\times(768+768)=12{,}288
少 48 倍。
LoRA 可以只加在文本编码器上,也可以同时加在 UNet 上。
- 只加文本编码器:改变”词义”,UNet 得适应新分布 → 通常效果不稳
- 同时加两边:效果最好,但参数量大
- 只加 UNet:学”画法”,不改词义 → 最常见
8.25.3 ③ DreamBooth:直接微调 UNet
做法(Ruiz et al., 2023):用一个罕见标识词(如 sks)指代主体, 微调整个 UNet,同时用”先验保持损失”防止遗忘:
\mathcal{L}=\mathcal{L}_{\text{diff}}+\lambda\,\mathcal{L}_{\text{prior}}
先验项:用模型自己生成的”这一类物体”的图做正则, 防止模型把”狗”这个词拉向你的那只狗。
| 做法 | 改什么 | 参数量 | 适合 |
|---|---|---|---|
| Textual Inversion | 一个词向量 | 768 | 新概念、新风格 |
| LoRA | 低秩补丁 | 1–10M | 画风、角色、构图习惯 |
| DreamBooth | 整个 UNet | 860M | 高保真主体复现 |
判断准则:问”这个概念在不在模型的词表里”。
- 在,只是没有词指向它 → Textual Inversion
- 不在,是一种”画法” → LoRA
- 要一模一样 → DreamBooth,并接受过拟合风险
这三种做法都绕开了”重训 UNet”—— 但它们各自付出的是表达力: 改得越少,能表达的新东西越受限。
这和第 7 章”引导不能创造信息”是同一条原则的另一个面。
8.26 端到端:一句话变成一张图
把本章和前面几章串起来。 从提示词到像素,一共八步。
8.26.1 第一步:分词与编码
tok = tokenizer(prompt, padding="max_length", max_length=77,
truncation=True, return_tensors="pt")
cond = text_encoder(tok.input_ids).last_hidden_state # (1, 77, d)注意 truncation=True 和 max_length=77——超出的部分在这里就没了。
8.26.2 第二步:空条件的编码
uncond = text_encoder(empty_ids).last_hidden_state # (1, 77, d)empty_ids 是空字符串的分词结果,不是全零。
8.26.3 第三步:初始化潜变量
latent = torch.randn(1, 4, 64, 64) * scheduler.init_noise_sigma从标准正态开始,形状是 (4,64,64)。
8.26.4 第四步:每一步做两次前向
for t in timesteps:
latent_in = scheduler.scale_model_input(latent, t)
noise_c = unet(latent_in, t, encoder_hidden_states=cond).sample
noise_u = unet(latent_in, t, encoder_hidden_states=uncond).sample
noise = noise_u + w * (noise_c - noise_u) # ← CFG
latent = scheduler.step(noise, t, latent).prev_sample这一循环里有本章的全部内容:
encoder_hidden_states=cond走交叉注意力- 两个分支共用同一个 UNet
w就是第 7 章那个引导强度
8.26.5 第五步:解码
latent = latent / vae.config.scaling_factor
image = vae.decode(latent).sample除以 scaling factor(编码时是乘)。
| 步骤 | 常见错误 | 症状 |
|---|---|---|
| 一 | 忘了 truncation | 长提示词后半段无效 |
| 二 | 用零向量当空条件 | w>0 就崩 |
| 三 | 用了错误的 init sigma | 画面偏灰或过曝 |
| 五 | 忘了除 scaling factor | 颜色整体偏移 |
四个错误里,两个和”文本”有关(一、二),两个和”潜空间”有关(三、五)。 这就是本章的两个主题。
8.26.6 一个容易被忽略的顺序
scheduler.scale_model_input 必须在两次 UNet 之前调用,而且只调用一次。
有些实现为了省事,在 CFG 之后又 scale 一次——那会让两个分支的输入不一致。
症状:w 调大时画面出现规律的色带。
8.27 走一遍 SDXL 的文本路径
抽象讲完,走一遍真实的。
这一段代码是 diffusers 里 SDXL 管线做过的事,每一步都标了形状。
8.27.1 第 1 步:两个分词器,两条路
tok1 = tokenizer(prompt, padding="max_length", max_length=77,
truncation=True, return_tensors="pt") # CLIP-L
tok2 = tokenizer_2(prompt, padding="max_length", max_length=77,
truncation=True, return_tensors="pt") # CLIP-G两个编码器各有自己的分词器(词表不同),但都是 77 个位置。
所以同一句提示词被切了两次,切法可能不一样。 一个在 CLIP-L 里是 5 个 token,在 CLIP-G 里可能是 6 个。
8.27.2 第 2 步:取倒数第二层
out1 = text_encoder(tok1.input_ids, output_hidden_states=True)
out2 = text_encoder_2(tok2.input_ids, output_hidden_states=True)
emb1 = out1.hidden_states[-2] # (1, 77, 768)
emb2 = out2.hidden_states[-2] # (1, 77, 1280)[-2] 而不是 [-1]
CLIP 的最后一层是专门为对比学习目标优化的。
它的输出被拉向”和图像对齐”的方向, 语义细节(比如”左边”“两只”)在这个过程中被压掉了。
倒数第二层还保留着更”语言”的表示。
这是一个纯经验的工程选择,论文里没有严格证明。 但它说明一件事: “最后一层”不等于”最好的表示”——要看你拿它做什么。
8.27.3 第 3 步:拼起来
prompt_embeds = torch.cat([emb1, emb2], dim=-1) # (1, 77, 2048)768 + 1280 = 2048。
这不是”平均”,是”拼接”。 两个编码器的表示被并排放着,让 UNet 自己决定用哪一个。
拼接而不是相加,是因为两者没有对齐过—— CLIP-L 的第 3 维和 CLIP-G 的第 3 维没有任何关系。 相加会制造一个没有意义的混合向量。
8.27.4 第 4 步:池化向量走另一条路
pooled1 = out1.pooler_output # (1, 768)
pooled2 = out2.pooler_output # (1, 1280)
pooled = torch.cat([pooled1, pooled2], dim=-1) # (1, 2048)序列 (77, 2048) 进交叉注意力。 池化 (2048,) 进时间步嵌入,参与 AdaGN。
所以 SDXL 同时用了本章讲的两种条件注入方式: 逐位置的交叉注意力 + 全局的 AdaGN。
这是它比 SD1.5 多出来的东西: SD1.5 只用序列,池化向量被丢掉了。
8.27.5 第 5 步:空条件走完全相同的路
uncond_embeds = encode("") # (1, 77, 2048)
uncond_pooled = encode_pooled("") # (1, 2048)注意:空字符串也要过两个分词器、两个编码器、同样的拼接。
它不是零,不是 None,是一个完整的、2048 维的、被训练过的表示。
8.27.6 第 6 步:UNet 里发生什么
prompt_embeds (1, 77, 2048)
↓ 每层一个 Linear(2048 → d)
K, V (1, 77, d) ← 16 层交叉注意力共用同一份
↑
Q 来自图像特征 (1, N, d)
↓
输出 (1, N, d)
16 层,每层都把 2048 投影到自己的 d(320/640/1280)。
这就是前面算过的 4400 万参数的去处。
文本序列在 16 层里是共享的,但投影矩阵不共享。
每层都有自己的 to_k 和 to_v—— 所以”同一个文本”在浅层和深层被投影成不同的 K,V。
这不是冗余,是必要的: 浅层需要”词长什么样”,深层需要”词和当前语义的关系”。
这也是为什么”缓存文本 embedding”只能省编码器那一步, 省不掉 UNet 里的 16 次投影。
8.28 训一个文本条件模型:和类别模型的三处不同
训练循环几乎一样:采样 x_0、加噪、预测 \varepsilon、MSE。 不同的只有三处。
8.28.1 第一处:条件是序列,不是标量
cond = text_encoder(tok).last_hidden_state # (B, 77, d)注意它是三维的——batch × 序列 × 维度。 UNet 的交叉注意力需要这个三维形状,类别模型里的一维向量在这里不适用。
代价:显存。77 个 token × d 的激活要一直留着。
8.28.2 第二处:文本丢弃要丢”整段”
第 7 章的类别模型丢的是一个下标;这里丢的是整段文本:
mask = torch.rand(B) < 0.1
tok[mask] = empty_token_ids # 换成空串的分词结果关键:empty_token_ids 必须是空字符串真实的 tokenize 结果, 不是全零、也不是全 [PAD]。
这就是第 7 章”空条件必须被训练过”在文本上的版本。
8.28.3 第三处:编码器通常冻结
text_encoder.requires_grad_(False)为什么冻结?
| 理由 | 说明 |
|---|---|
| 省显存 | 编码器动辄几亿参数,反向传播的激活很贵 |
| 省数据 | 不需要”文本-图像”配对来训编码器 |
| 稳定 | 条件分布固定,UNet 只需要学”怎么用” |
代价:编码器不懂”生成需要什么”。 它只知道”这段文字和那张图配不配”,不知道”红色对像素意味着什么”。
UNet 必须适应编码器的”怪癖”。
比如 CLIP 的因果注意力让 [EOS] 位置变成”整句摘要”, 但序列里其它位置只看到前半句。UNet 学会的是”从这 77 个向量里取信息”, 而它取的方式依赖 CLIP 的这种特性。
所以换编码器不是”换个更好的”,是”换一套要重新适应的输入”。
8.28.4 一个数据上的坑
图文配对的质量比数量重要。
如果 caption 只写”一张图片”,模型学到的就是”忽略文本”—— 因为文本不提供任何区分信息。
在文本丢弃率 10% 的情况下,90% 的样本需要文本真正有用, 否则模型会退化成”主要看空条件”。
判据:训练后做一次”换文本”测试—— 同一个噪声、两个不同提示词,输出应该明显不同。 如果几乎一样,说明文本没被用起来。
8.29 代码逐行:训练循环与采样器
前面给了编码器的十几行。这一节把外面那层壳也拆开—— 训练循环和采样器各有三处容易写错的地方。
8.29.1 训练循环
def train_text_model(x0, y, cfg, sch, encoder="cross", steps=6000, drop=0.1):
tok = torch.stack([y // 2, y % 2], dim=1) # ① 类别 → 两个 token
model = TextCondDenoiser(cfg, encoder=encoder)
opt = torch.optim.Adam(model.parameters(), lr=cfg.lr)
for step in range(steps):
idx = torch.randint(n, (cfg.batch_size,), generator=g)
tk = tok[idx].clone() # ② 必须 clone
mask = torch.rand(len(idx), generator=g) < drop
tk[mask] = 2 # ③ NULL token
t = torch.randint(0, cfg.n_steps, (len(idx),), generator=g)
xt, eps = forward_sample(x0[idx], t, sch)
loss = F.mse_loss(model(xt, t, tk), eps)
opt.zero_grad(set_to_none=True)
loss.backward()
opt.step()① y // 2 和 y % 2。
类别编号 0,1,2,3 拆成两个二进制位: 0\to(0,0)、1\to(0,1)、2\to(1,0)、3\to(1,1)。 这正好是”左色、右色”两个 token。
为什么不用 nn.Embedding(4, d) 直接查? 因为那样就退化成第 7 章的类别条件了——没有”两个词”这个结构。
② tok[idx].clone()。
没有 clone() 会出大错:tk[mask] = 2 是原地赋值, 会把整个 tok 表里的样本也改掉。 下一批再取到它们时,条件已经被污染了。
症状:训练损失正常下降,但推理时准确率莫名其妙地低。 因为训练时有一半样本的条件被永久改成了 NULL,而你没发现。
③ tk[mask] = 2。
这是第 7 章”标签丢弃”的文本版本。 注意它丢的是整段——两个 token 都变成 NULL, 不是随机丢其中一个。
只丢一个会怎样? 模型会学到”部分条件”这个中间状态, 而 CFG 需要的是完整的无条件。两者不是一回事。
8.29.2 采样器
@torch.no_grad()
def sample_text(model, sch, tok, w=1.0, seed=0):
x = torch.randn(len(tok), 4)
null = torch.full_like(tok, 2)
for t in range(T - 1, -1, -1):
eps_c = model(x, tt, tok)
if w != 1.0:
eps_u = model(x, tt, null)
eps_c = eps_u + w * (eps_c - eps_u)
x0 = ((x - (1 - ab).sqrt() * eps_c) / 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)这里有三处值得停一下。
第一处:x0 后面的 .clamp(-3, 3)。
\hat x_0=\frac{x_t-\sqrt{1-\bar\alpha_t}\,\hat\varepsilon}{\sqrt{\bar\alpha_t}}
当 t 很大时,\sqrt{\bar\alpha_t} 很小,分母接近 0。 \hat\varepsilon 的一点点误差会被放大成巨大的 \hat x_0。
clamp 是数值保护,不是数学推导的一部分。 去掉它,采样会在高噪声步炸掉。
第二处:tt 是标量张量。
模型要的是每个样本一个时间步,不是整个 batch 一个。 这里所有样本同步采样,所以可以共用一个 t。 如果做部分重绘(inpainting),不同区域的时间步不同,就不能共用。
第三处:最后一步不加噪声。
x = mean + noise if t > 0 else meant=0 时后验方差为 0(\beta_0 很小但不为零,理论上还该加)。 实践里直接取均值,因为最后一步的噪声只会让画面更脏。
w != 1.0 时,每一步要做两次前向。
如果写成”两次前向 + 一次 step”,计算量正好翻倍。 很多人以为 CFG 是免费的——它不是,它是 2× 推理成本。
省一半的办法:把条件和空条件拼成一个 batch,一次前向:
x2 = torch.cat([x, x])
t2 = torch.cat([tt, tt])
tok2 = torch.cat([tok, null])
eps2 = model(x2, t2, tok2) # 一次前向,batch 翻倍
eps_c, eps_u = eps2.chunk(2)batch 翻倍和两次前向的计算量一样,但少了一次 kernel 启动。 在 GPU 上通常快 10–20%。
8.30 梯度怎么流回文本
交叉注意力里,文本是被”读”的。那它能不能被”改”?
能——只要它参与计算,梯度就会流回去。 这一节把这条路径算清楚,它能解释好几件工程上的事。
8.30.1 从输出往回推
设一个位置的输出:
o=\sum_k a_k v_k,\qquad a_k=\text{softmax}(s)_k,\qquad s_k=\frac{q^\top k_k}{\sqrt d}
损失 L 对 v_k 的梯度:
\frac{\partial L}{\partial v_k}=a_k\frac{\partial L}{\partial o}
很直白:注意力权重就是”这个 token 分到多少梯度”。
对 a_k 的梯度:
\frac{\partial L}{\partial a_k}=\frac{\partial L}{\partial o}\cdot v_k
记 g_k=\frac{\partial L}{\partial o}\cdot v_k——“第 k 个 token 的内容对输出的贡献”。
softmax 的雅可比是 \frac{\partial a_j}{\partial s_k}=a_j(\delta_{jk}-a_k),于是
\frac{\partial L}{\partial s_j}=a_j\left(g_j-\sum_k a_k g_k\right)
① 括号里是”这个 token 的贡献,减去平均贡献”。 只有”比平均更好或更差”的 token 才拿到梯度。 如果所有 token 对输出的贡献一样,梯度为零——没什么可学的。
② 前面的 a_j 是”话语权”。 权重小的 token,梯度也小。 没有阈值,但有一个乘数。
③ 这个乘数不会因为权重”均匀”而消失。 a_j=0.5 时,梯度是 0.5\times(\text{偏差})——仍然有一半。 这就是”均匀注意力仍然能学习”的数学原因。
8.30.2 算一个具体的数
两个 token,a=(0.9,0.1),贡献 g=(1.0,0.0)。
平均贡献:\bar g=0.9\times1.0+0.1\times0.0=0.9。
\frac{\partial L}{\partial s_1}=0.9\times(1.0-0.9)=0.09 \frac{\partial L}{\partial s_2}=0.1\times(0.0-0.9)=-0.09
两个梯度大小相同、符号相反。 softmax 的输出和恒为 1,所以梯度也必须和为零—— 一个上去,另一个就得下来。
如果 a=(0.5,0.5) 呢?
\frac{\partial L}{\partial s_1}=0.5\times(1.0-0.5)=0.25,\qquad \frac{\partial L}{\partial s_2}=0.5\times(0.0-0.5)=-0.25
梯度反而更大。
因为均匀分布离”极端”最远,softmax 在这里的斜率最大。
很多人看到权重接近 0.5,就认为”模型没学到东西”。
上面的计算说明:均匀恰恰是梯度最大的位置。
本章的实测也支持这一点: no_xy 的注意力完全均匀(0.525/0.525),绑定照样 1.000。
权重均匀不代表没学习,可能代表”每个词都同等重要”。
8.30.3 梯度继续往回:到 q 和 k
\frac{\partial L}{\partial q}=\sum_j\frac{\partial L}{\partial s_j}\cdot\frac{k_j}{\sqrt d}, \qquad \frac{\partial L}{\partial k_j}=\frac{\partial L}{\partial s_j}\cdot\frac{q}{\sqrt d}
注意 k_j 的梯度里带着 q。 如果某个位置的 query 从来不和某个 key 打分(被掩码挡住), 那个 key 就永远拿不到梯度。
这就是区域掩码的副作用: 被掩掉的位置,对应的词学不到任何东西。 所以掩码要”轮换”——否则模型会对某些词完全失明。
8.30.4 如果文本编码器是冻结的
梯度到 k,v 就停了。 编码器不更新,文本表示不变。
所以”冻结编码器”的真实含义是: 交叉注意力的 K,V 投影会学,但文本本身的表示不会。
这两件事经常被混为一谈:
| 冻结什么 | 谁还在学 | 效果 |
|---|---|---|
| 编码器 + 投影 | 只有 UNet 其余部分 | 文本完全固定 |
| 只冻结编码器 | K,V 投影 + UNet | 模型学”怎么读”,不学”词义” |
| 全都不冻结 | 全部 | 词义会漂移,需要更多数据 |
大多数实现是第二种。 说”文本编码器冻结了所以文本不变”是对的; 说”所以交叉注意力不学习”是错的。
8.31 描述文字是怎么写出来的
模型能听懂什么样的提示词,不由架构决定,由训练数据的 caption 决定。
这一节解释一个很实际的现象:为什么同一个提示词, 在 SD1.5 上要写成关键词堆砌,在 SDXL 上要写成短句,在 SD3 上要写成长句。
8.31.1 三代 caption,三种写法
| 代 | 数据来源 | caption 长什么样 | 适合的提示词 |
|---|---|---|---|
| SD1.5 | LAION 的网页 alt-text | a photo of a cat, high quality |
关键词堆砌 |
| SDXL | 用 VLM 重新描述 | A cat sitting on a windowsill, sunlight... |
短句 |
| SD3 / FLUX | 更细的 VLM 重述 | 完整段落,含空间关系 | 自然语言长句 |
第一代的问题:alt-text 是网页作者随手写的, 短、噪声大、经常和图像无关(“点击这里”“产品图 3”)。
模型从这种数据里学到的是: “文本大概指示一下主题就行,细节靠先验。” 所以 SD1.5 对长句不敏感,对关键词敏感。
第二代的转折:SDXL 用了一个在小型高质量数据上微调过的 captioner, 把训练图重新描述一遍。caption 变长、变准、含空间关系。
第三代:SD3 用更强的 VLM 重述,caption 里开始出现 “左边”“背后”“拿着”这类关系词。
提示词的风格要和训练 caption 的风格匹配。
- 在 SD1.5 上写一段优美的英文长句 → 大部分 token 是分布外的
- 在 SD3 上堆
masterpiece, best quality, 8k→ 这些词在它的 caption 里几乎不出现 - 在任何一个模型上,用它的数据分布里常见的句式,效果最好
这不是玄学,是”训练/推理分布匹配”—— 和失败四(换 tokenizer)是同一个道理的另一个版本。
8.31.2 为什么”魔法词”会存在
早期社区发现某些词(特定的艺术家名、masterpiece、8k uhd) 能显著提升观感。
机制:这些词在 LAION 的 alt-text 里和高质量图片强相关—— 不是因为它描述了什么,而是因为写这些词的人倾向于配好图。
所以模型学到的不是”8k 意味着高清”, 是”出现 8k 的图,平均质量更高”。
这解释了两件事:
- 为什么魔法词换模型就失效——新模型的 caption 分布里没有这个相关性
- 为什么魔法词有时会带来副作用——它同时拉动了训练数据里的其它属性
8.31.3 caption 的长度分布也在被学习
如果训练数据里 90% 的 caption 短于 20 个 token, 模型就没见过”40 个 token 的详细描述”。
后果:长提示词的后半段不是被截断,是被忽略—— 模型没见过”后面还有信息”这种模式。
这和 77 token 的硬截断是两回事:
| 硬截断 | 软忽略 | |
|---|---|---|
| 原因 | 位置编码用完 | 训练分布里没有长 caption |
| 症状 | 第 78 个 token 之后完全无效 | 长句的后半段权重很低 |
| 修法 | 换编码器 / 截短 | 换模型 / 用它的风格写 |
很多”长提示词没用”的报告,其实是第二种。
8.31.4 一个可以自己做的检验
取一批你的提示词,做两件事:
- 把后半句删掉,看输出变化大不大
- 把同一句话改写成”关键词堆砌”和”自然语言”两版,对比
如果第 1 步几乎没变化,说明模型在软忽略长句; 如果第 2 步差距很大,说明它偏向某一种 caption 风格。
这两个检验都不需要看模型内部,五分钟能做完。
8.32 这一章站在前七章的哪里
这本书是一条线,不是八个孤岛。这一节把线接回去。
8.32.1 第 1 章:注意力的公式
本章的交叉注意力和第 1 章的自注意力是同一个公式:
\text{softmax}\left(\frac{QK^\top}{\sqrt d}\right)V
第 1 章讲的是”Q,K,V 都来自同一串 token”, 本章讲的是”Q 来自图像、K,V 来自文本”。
同一个公式,换了三个输入,就变成了另一个机制。 这就是第 1 章反复强调”注意力是一个查询接口”的意义。
本章还用了第 1 章的两个结论: \sqrt d 的缩放不能省(不然 softmax 会饱和), 多头让不同的头学不同的关系(本章的 n_head=2)。
8.32.2 第 2 章:Block 的结构
文本编码器就是若干个标准 Block 堆起来的: 自注意力 + FFN + 残差 + LayerNorm。
本章没有展开讲这些,因为第 2 章已经讲过。 但有一个细节值得回想:残差让”位置信息”能穿过很多层, 这也是为什么位置嵌入加在输入上、而不是每一层都加。
本章的交叉注意力没有残差(它是一个独立模块,输出被拼进条件), 真实 UNet 里的交叉注意力是有残差的—— h = h + attn(h, text)。这一条残差让模型可以选择”不听文本”。
8.32.3 第 3 章:训练与推理的分离
第 3 章讲”训练时并行、推理时逐 token”。
本章的对应物是: 训练时每个样本随机采一个 t,一次前向; 推理时要走 1000 步,每步两次前向(CFG)。
训练和推理的计算模式完全不同,这是扩散模型和第 3 章语言模型的共同点。
8.32.4 第 4 章:注意力图能解释什么
第 4 章在语言模型上找到了”盯住前一位”的头, 也讲了”注意力图不等于解释”。
本章在文本条件上又遇到了一次,而且更彻底: 权重完全均匀时绑定仍然成立(no_xy), 权重很尖锐时绑定完全不成立(no_pos)。
第 4 章的教训是”别假设”,本章的贡献是”给了一个反例的定量版本”。
8.32.5 第 5 章:前向、反向、\varepsilon 预测
本章的 \varepsilon-预测目标、噪声调度、后验均值,全部来自第 5 章。
唯一的改动是:模型多了一个输入 c, 损失从 \|\varepsilon-\varepsilon_\theta(x_t,t)\|^2 变成 \|\varepsilon-\varepsilon_\theta(x_t,t,c)\|^2。
一行代码的差别,一整章的内容。
8.32.6 第 6 章:采样器
本章用的是 DDPM 采样(1000 步)。
第 6 章的 DDIM 结论在这里同样适用: 少步数采样会放大模型误差,而条件误差也会被放大。 本章没有测”少步数 + 文本条件”的组合——这是一个明确的空白。
一个可以预期的现象:步数少时,绑定准确率会先于整体质量下降, 因为绑定依赖精细的注意力分配,而少步数采样对细节最不友好。
8.32.7 第 7 章:CFG
这是联系最紧的一章。
| 第 7 章 | 本章 |
|---|---|
| 条件是类别下标 | 条件是一串 token |
| 空条件是 NULL 类嵌入 | 空条件是空串的编码 |
| 注入方式是相加/AdaGN | 注入方式是交叉注意力 |
| w 从 1 到 2 有明显收益 | w 从 1 到 10 没有收益 |
最后一行不是矛盾,是”任务难度不同”: 第 7 章的任务在 w=1 时只有 0.965,有提升空间; 本章的任务在 w=1 时已经 1.000。饱和了,就没有收益。
两章共享的是同一条原则: 引导放大的是”条件带来的差”,差为零时放大零还是零。
8.32.8 一条贯穿全书的线
如果把八章连起来看,主线是”信息怎么在模型里流动,以及在哪里被丢掉”。
- 第 1 章:注意力让信息在 token 之间流动
- 第 2 章:残差让信息跨层流动
- 第 3 章:训练让信息进入权重
- 第 4 章:我们试图”读”这些信息,发现读不准
- 第 5 章:扩散把信息从噪声里逐步恢复
- 第 6 章:采样器决定恢复的精度
- 第 7 章:引导放大某个方向的信息
- 第 8 章:文本把信息送进这个过程
本章的位置是”入口”: 如果信息在入口就被池化掉了,后面七章的一切都救不回来。
8.33 为什么更强的编码器不一定带来更好的绑定
一个自然的猜想:换一个更强的文本编码器,绑定应该更好。
实测不是这样。
SD1.5 用 CLIP-L(12 层,768 维),SDXL 用 CLIP-L + CLIP-G(2048 维), SD3 再加一个 T5-XXL(4096 维)。
编码器一代比一代强,但”两只猫”还是两只猫,“左边”还是经常跑到右边。
为什么?三个原因。
8.33.1 原因一:瓶颈可能不在编码器
编码器负责”把话说清楚”,UNet 负责”把话用起来”。
如果 UNet 没有能力把”左边”映射到左半张图, 编码器把”左边”表示得再精确也没用。
本章的实验正好说明了这一点: no_xy 的条件向量两个点完全一样,绑定照样 1.000—— 因为下游 MLP 有能力用 x 的坐标做路由。
反过来,如果下游没有这个能力,再好的条件也白搭。
所以”换编码器”的收益,取决于”UNet 是否已经用尽了现有条件的信息”。 大多数情况下,没有。
8.33.2 原因二:维度越高,越难对齐
T5-XXL 输出 4096 维。
UNet 要把它投影到 320/640/1280 维。 这是一个 4096\times1280 的矩阵,每一层一个。
维度越高,投影矩阵越大,越难训。
而且高维表示里”有用的方向”可能很稀疏—— 4096 维里真正和”红色”相关的可能只有几十维。 UNet 要从 4096 维里找出那几十维,比从 768 维里找更难。
这就是”更强的编码器”的隐性成本: 它把”理解语言”的负担转移给了 UNet 的投影层。
8.33.3 原因三:训练目标不匹配
CLIP 是对比学习,T5 是去噪。两者的表示空间结构完全不同。
T5 的表示更适合”理解句子结构”,但它没有和图像对齐过。 UNet 必须从头学”T5 的哪个方向对应红色”。
SD3 同时用 CLIP 和 T5,本质上是在做”对齐 + 结构”的折中: CLIP 提供已经和图像对齐过的方向,T5 提供结构信息。
当你觉得”文本没被理解”时,先分清是哪一层的问题:
| 现象 | 可能的位置 | 怎么验证 |
|---|---|---|
| 词根本不认识 | 编码器 / 分词器 | 换同义词,看是否恢复 |
| 词认识但位置错 | UNet 的路由 | 加区域掩码,看是否变好 |
| 两个物体属性串 | 交叉注意力的竞争 | 分开生成,看是否各自正确 |
| 整体风格不对 | 训练数据分布 | 换提示词风格,看是否恢复 |
只有第一行是编码器的问题。 后三行换编码器都解决不了。
这也解释了为什么”换更大的模型”经常不如”换更好的条件结构”。
模型的能力上限由架构决定,但达到上限的方式由条件决定。 一个 10 亿参数的模型配一个糟糕的条件,不如一个 1 亿参数的模型配一个好条件。
本章的全部实验,都可以看成这句话的注脚。
8.34 文本条件的数据从哪来
模型的文本能力,是数据喂出来的。这一节说清楚”喂的是什么”。
8.34.1 第一代:网页抓取
LAION-5B(2022)是最大的公开图文数据集: 从 Common Crawl 抓取网页,把 <img> 的 alt-text 当 caption, 用 CLIP 过滤掉图文相似度低的配对,得到 58.5 亿对。
它的三个特点,直接塑造了第一代模型:
① caption 是网页作者写的,不是为生成写的。 “点击查看大图”“产品图 3”这类噪声大量存在。 模型学到的文本表示,带着这种噪声。
② 语言以英文为主。 这是”中文提示词效果差”的最直接原因—— 不是模型不愿意学中文,是数据里中文太少。
③ 有版权和内容风险。 2023 年,LAION-5B 因为包含违法内容被撤下。 这件事之后,“数据从哪来”从技术问题变成了合规问题。
8.34.2 第二代:重新标注
SDXL 的做法:训练一个 captioner,把训练图重新描述一遍, 得到更长、更准、含空间关系的 caption。
这一步的收益很大—— 同样的图像,caption 的信息量提高了,p(c\mid x) 变得更尖锐(见”概率的视角”一节)。
SD3 更进一步:用多模态大模型重述, caption 里开始出现”左边”“拿着”“背后”这类关系词。
代价:重述 10 亿张图,需要大量的推理算力。 这是”数据工程”变成”模型工程”的一个标志。
8.34.3 第三代:授权数据
Adobe Firefly、Getty 等选择只使用授权图库。
优点:合规、可商用。 缺点:规模小、分布偏(图库照片的风格很统一)。 这直接影响了生成结果的多样性—— 用图库训练的模型,倾向于生成”商业摄影”风格的图。
① 语言覆盖:数据里有什么语言,模型就懂什么语言。 ② 描述粒度:caption 有多细,条件就能有多细。 ③ 风格分布:图库图和互联网图,生成的”默认审美”完全不同。**
这三件事都不是”换个更好的编码器”能改变的。 它们是数据属性,不是模型属性。
8.34.4 过滤:看不见的第四代
所有大规模数据集都经过过滤:
- NSFW 过滤:去掉色情、暴力内容
- 水印过滤:去掉带明显水印的图
- 美学过滤:用一个打分模型保留”好看”的图
- 去重:防止同一张图出现几百次
过滤标准本身就是一种”条件”。 它决定了模型认为”什么是一张好图”—— 而这个判断,用户是看不见的。
这也是为什么不同模型有不同”默认风格”: 不是架构不同,是过滤器的偏好不同。
8.35 十五个常见问题
这些是讲这一章时被问得最多的问题。答案尽量短,需要展开的地方指回正文。
问一:为什么不用 RNN 或 CNN 编码文本?
因为要建模的是”任意两个词之间的关系”,而卷积的感受野有限、RNN 的路径太长。 自注意力一步就能让第 1 个词和第 77 个词直接交互,这是它取代前两者的核心理由。 代价是 O(M^2) 的计算——77 个 token 时完全不是问题。
问二:交叉注意力和自注意力能互换吗?
不能直接换,因为 Q,K,V 的来源不同。 但可以把两串 token 拼起来做自注意力——那就是联合注意力(MMDiT)。 所以”交叉”和”联合”是可以互相转化的,代价是计算量和双向性。
问三:为什么是 77 个 token,不是 512?
因为 CLIP 用的是可学习的绝对位置嵌入,位置表只有 77 行。 第 78 个 token 没有位置可加,只能截断。 T5 用相对位置,所以没有这个限制——这是 SD3 换 T5 的直接原因之一。
问四:可以不训练文本编码器吗?
可以,而且大多数模型就是这么做的。 但”冻结编码器”不等于”文本不变”:交叉注意力的 K,V 投影仍然在学。 冻的是词义,学的是”怎么读”。
问五:为什么 CFG 要两次前向?
因为要同时算 \varepsilon_c 和 \varepsilon_\varnothing。 可以把两个条件拼成一个 batch 做一次前向,计算量一样,但少一次 kernel 启动。 它不能被省成一次,因为两个分支的输入条件不同。
问六:为什么真实模型的 w 常取 5–8,而本章只要 2?
因为真实模型的条件更弱、更嘈杂。 w 要大到”把条件方向从噪声里拉出来”,而本章的玩具模型在 w=1 就已经完全学会了。 w 的最优值和任务的难度、条件的质量都有关,不能跨模型照搬。
问七:提示词里的顺序重要吗?
对 CLIP 系模型,重要性低于你的直觉;对 T5 系模型,重要得多。 机制在”CLIP 是怎么训出来的”那一节:对比学习很少要求区分词序。 但”不重要”不等于”没用”——本章的实测显示,交叉注意力确实用上了顺序。
问八:为什么中文提示词效果差?
三个叠加的原因:分词效率低(一个汉字 1–3 个 token)、 训练数据以英文为主、编码器的中文语义弱。 三者里最容易解决的是第一个——把提示词写短、把关键词提前。
问九:注意力图能做图像编辑吗?
能,Prompt-to-Prompt 一类的方法就是用注意力图做局部替换。 但这不等于”注意力图解释了模型”—— 它只是提供了一个”可操作的旋钮”,旋钮有用不代表刻度是准的。
问十:为什么 SD3 放弃了交叉注意力?
不是因为它不好,是因为联合注意力在同样预算下多拿到了双向信息流(多 3.8%)。 交叉注意力的优势是缓存——文本的 K,V 算一次用 16 层。 联合注意力放弃了这个优势,换来了文本和图像的互相调整。
问十一:文本条件能控制数量吗?
很弱。 “两只猫”和”三只猫”在 CLIP 的表示里差别很小, 而且训练数据里数量词的分布很偏。 这是组合性失败的一个典型例子,和第 7 章”准确率饱和”一样, 需要专门的指标才能测出来。
问十二:为什么负面提示词有效?
因为它参与的是 \varepsilon_{\text{neg}} 那一路, 在 CFG 的差里被减掉。 它不是”告诉模型不要什么”,是”把生成方向往远离那个词的方向推”。 所以负面词和主体语义太近时,会把主体一起推走。
问十三:能不能只用池化向量(像 CLIP score 那样)?
能,但会丢掉位置信息。 SD1.5 只用序列,SDXL 两个都用——序列进交叉注意力,池化进 AdaGN。 池化向量的优势是便宜(每层一次 d 维调制),劣势是不能逐位置。
问十四:文本编码器能换成 LLM 吗?
可以,而且已经有模型这么做(用解码器式的 LLM 当文本塔)。 但要注意:LLM 的输出是”下一个 token 的分布”,不是”句子表示”。 要么取隐藏状态,要么加一个投影头——这两种做法效果不同,需要实验。
问十五:为什么”红色”会染到别的东西上?
因为交叉注意力是一次 softmax、所有词竞争。 “red”和”blue”在同一个位置上都有分数,加权平均就成了紫色。 这不是模型不认识颜色,是没有机制强制”红色只属于方块”。 解法是改条件结构(区域掩码、结构化引导),不是改措辞。
8.36 从提示词工程到条件工程
这一章讲的是机制。最后这一节讲一个判断:提示词工程正在变成条件工程。
先看提示词工程都在做什么。
最典型的三类技巧,本质上都是在弥补同一个缺陷——文字这个接口没有结构。
第一类是词汇层面的:加”masterpiece”、加艺术家名字、加”8k uhd”。 这些词之所以有效,是因为它们在训练数据里和高质量图像相关。 它们不描述任何东西,只是把生成往某个统计区域推。 这是一种用”相关”代替”控制”的做法。
第二类是数值层面的:(word:1.3)、CFG 的 w、负面提示词。 它们调的是同一个东西——某个条件在计算里的分量。 权重放大的是嵌入向量,w 放大的是条件与无条件的差,负面词放大的是反方向。 三者都是在”已有的方向上加减”,没有一个能创造新的结构。
第三类是结构层面的:区域提示词、ControlNet、IP-Adapter。 这一类开始不一样了——它们不再修改文字的权重,而是增加新的条件通道。 区域提示词增加了”哪里”,ControlNet 增加了”什么形状”,IP-Adapter 增加了”像什么”。
从第一类到第三类,是一个从”调参”走向”改接口”的过程。 这就是我说的”从提示词工程到条件工程”。
因为文字的信息带宽是有限的。
77 个 token,每个 768 维,要描述一张 512\times512 的图像。 图像的自由度是 78 万个数,文本的自由度是 5.9 万个数—— 差了 13 倍。
这不是”文字还不够好”,是”文字这个模态本身装不下”。
所以每一次精度提升,最终都要靠”加一条新通道”: 要位置,加框;要形状,加边缘图;要风格,加参考图。 文字退回到它最擅长的位置——描述”是什么”,而不是”在哪里、什么形状”。
那未来的接口长什么样?
我猜是”文本 + 结构化条件”的组合,而不是”更长的文本”。
结构化条件可能是一张场景图: 物体、属性、关系,每个都是一条可寻址的记录。 模型对每个物体单独算注意力,再按关系组合。
也可能是一个可编辑的中间表示: 先生成一个粗糙的布局,再逐块细化—— 这和本章讲的”区域提示词”是同一个思路,只是把”掩码”换成了”可以反复修改的对象”。
共同点是:条件从”一串不可分的 token”变成”一组可寻址的结构”。 一旦可寻址,“绑定”就不再需要在注意力里隐式地学——它可以被显式地指定。
这也是本章实验的启示: 交叉注意力能学出绑定,但要花数据和参数; 一个显式的结构,可能只需要几行代码。
当然,显式结构也有代价:它要求用户提供更多信息, 而大多数人只想写一句话。
所以短期内不会有一方取代另一方—— 而是”一句话”负责大多数场景,“结构化条件”负责少数高精度场景。
本章讲的交叉注意力,是这两者共同的底座。 看懂了它,你才知道什么时候该加一条新通道,什么时候该把提示词写得更准。
8.37 怎么读一篇新的文生图论文
这一年新出的文生图论文,一个月可能有十几篇。 有了这一章的框架,你可以用四个问题筛掉大部分。
问题一:条件从哪来?
是文本、图像、布局,还是多模态? 如果是文本,用的是什么编码器、多少 token、什么维度?
这一步决定了它的”条件带宽”。 一个用 CLIP-L 的模型和一个用 T5-XXL 的模型, 后面的所有设计都不能直接比较。
问题二:怎么进模型?
在五个格子里找一个:拼接、AdaGN、交叉注意力、联合注意力、旁路。
如果作者说”我们提出了新的条件注入方式”, 先看它属于哪一格,再看它改了什么。
大多数”新方法”是某一格的变体: 换了门控、换了位置、换了归一化。 真正的结构性创新(比如从交叉到联合)很少。
问题三:在哪一层?
同一栋楼,装在几层,效果差很多。
交叉注意力装在 UNet 的 16 层还是 4 层, 参数量差 4 倍,效果也可能差很多。
论文里如果只说”我们加了条件”,没说”加在哪几层”, 这篇论文的可复现性就要打折扣。
问题四:怎么训练?
三个数:丢弃率、batch size、caption 来源。
丢弃率决定 CFG 能不能用;caption 来源决定条件能力强不强; batch size 决定对比学习(如果有)的质量。
这三个数经常被放在附录里,但它们是理解结果的关键。
很多论文的”提升”,在四个问题里有现成的解释。
比如:“我们的方法在组合性上提升了 15%”—— 如果它同时把 caption 换成了更细的版本,提升可能来自数据,不是方法。
控制变量,是读论文时最该做的事。 而这一章教你的,正是”怎么找出该控制的变量”。
8.38 延伸阅读
8.38.1 ① CLIP(2021)
Radford et al., Learning Transferable Visual Models From Natural Language Supervision。arXiv:2103.00020。
读它能得到什么:为什么文本和图像可以放进同一个空间。
读的时候注意:
- 它是对比学习:匹配的图文对拉近,不匹配的推远
- 它没有”生成”能力,只判断”配不配”
- 77 token 的限制来自它的位置编码
8.38.2 ② Imagen(2022)
Saharia et al., Photorealistic Text-to-Image Diffusion Models with Deep Language Understanding。arXiv:2205.11487。
读它能得到什么:用 T5 而不是 CLIP 的理由,以及第 7 章讲过的 rescale。
关键实验:他们对比了 CLIP、T5、以及两者的组合, 发现纯 T5 更好——尽管 CLIP 是图文对齐训练的。
这个结论值得记住:“图文对齐”不等于”适合做条件”。 生成需要的是”文本内部的语义结构”,T5 的纯文本预训练反而更合适。
8.38.3 ③ Stable Diffusion(2022)
Rombach et al., High-Resolution Image Synthesis with Latent Diffusion Models。arXiv:2112.10752。
读它能得到什么:潜空间扩散的完整设计——VAE、交叉注意力的插入位置、 以及为什么要下采样 8 倍。
读的时候注意:它的交叉注意力只在 UNet 的特定层,不是每层都有。 论文里有那张结构图,对照本章”接进 UNet 的位置”那一节看。
8.38.4 ④ 关于注意力图的批评
Jain & Wallace (2019), Attention is not Explanation; Wiegreffe & Pinter (2019), Attention is not not Explanation。
读它能得到什么:为什么”看注意力权重”不能直接当解释。
本章的负结果(相关 −0.079)是这一讨论在文本条件上的一个实例。 两篇论文观点相反,对照着读更有价值。
8.38.5 ⑤ 组合性与绑定
关于 diffusion 模型组合能力的工作(如 Compositional Visual Generation、 以及各类 benchmark)讨论的就是本章的绑定问题: 模型能不能正确地把属性分配给对象。
本章的二维实验是它的最小版本:两个属性、两个位置、四个组合。
8.38.6 六条机理,逐条展开
上面是”怎么发现”,这里是”为什么会这样”。
8.38.6.1 机理一:均值是一个置换不变的运算
定义:f 是置换不变的,如果 f(x_1,\dots,x_n)=f(x_{\pi(1)},\dots,x_{\pi(n)}) 对任何排列 \pi 成立。
均值就是这样的运算:\frac12(a+b)=\frac12(b+a),一模一样。
所以”用均值当条件”这个设计,在数学上就不可能区分顺序。 不是训练不够、不是数据不够——是信息在那一步就被销毁了。
任何”池化”都在丢信息:
| 池化 | 丢掉什么 |
|---|---|
| 均值 | 顺序、数量结构 |
| 最大 | 除最大值外的一切 |
| 求和 | 顺序(和均值一样) |
注意力池化(用 query 加权)不丢顺序——因为权重依赖位置。 这是交叉注意力和均值池化的本质区别,不是”参数量”的区别。
8.38.6.2 机理二:注意力本身是置换等变的
如果不加位置嵌入,K 里两个 token 除了内容没有区别。
把 token 顺序换一下,K 只是换了个顺序。 加权求和 \sum_k a_k v_k 对 k 的排列不变—— 输出逐位相同,不是”跟着换”。
这一点被实测证实了:no_pos 的对调差是 0.000。 它的准确率仍然显示 0.602 / 0.397,因为同一批样本被从两边数。
所以顺序信息必须显式注入:
- 位置嵌入(本章的做法)
- 因果掩码(CLIP 用的)
- 或者让 query 本身带位置
“用了注意力就有顺序”是一个常见的误解。
8.38.6.3 机理三:零向量在模型的训练分布之外
模型训练时见过的”空条件”是 emb[NULL],一个特定向量。
零向量是另一个点——它可能在训练分布之外,也可能恰好落在某个真实条件附近。
第 7 章的实测:零向量当空条件,越界 100%、多样性 0, 而准确率 1.000。
机理:CFG 的公式是 \varepsilon_u+w(\varepsilon_c-\varepsilon_u)。 如果 \varepsilon_u 是一个”模型不知道怎么处理”的输入, 它的输出就是一个任意的方向——放大这个差,等于放大一个随机方向。
8.38.6.4 机理四:换 tokenizer 等于换了一门语言
空字符串在不同 tokenizer 下切出的特殊 token 不同:
- CLIP:
[BOS] ... [EOS](2 个) - T5:可能是一串
[PAD] - 某些 tokenizer:只有
[CLS]
这些 token 的嵌入完全不同,编码结果自然不同。
而 UNet 是在”某一个编码器”的输出上训练的—— 换了编码器,输入的分布变了,UNet 的权重不再匹配。
这就是”换编码器必须重训 UNet”的机制。
8.38.6.5 机理五:77 是一个硬截断
CLIP 的位置编码只有 77 个位置。 第 78 个 token 没有位置可加。
实现上通常直接截断(或者用滑动窗口,但那样会丢全局)。
后果:句尾的修饰语被丢掉。而中文提示词更容易超限(机理见前面”词表”那一节)。
判据:把提示词从 60 token 加到 90 token,如果输出完全没变, 说明后半段被截断了。
8.38.6.6 机理六:注意力权重只是链条上的一环
交叉注意力的输出是 \sum_k a_k v_k。 信息有三条路:
- 权重 a_k
- 值 v_k(含位置嵌入)
- query 本身(含点的坐标)
本章的测量只看了第 1 条,发现它几乎均匀、与绑定无关。 但第 2、3 条仍然携带着信息,而且后面的 MLP 可以把微小差异放大。
所以”注意力图不可解释”不等于”注意力没起作用”—— 它只是说”权重这一条路不是解释”。
它们都不是”训练不充分”,是”结构上做不到”。
- 均值:信息被销毁
- 无位置:对称性未被打破
- 零向量:输入在分布外
- 换 tokenizer:训练/推理不匹配
- 截断:信息根本没进来
- 误读注意力:解释的路径选错了
前五条改数据或改结构就能解决;第六条改的是”你怎么看它”。
8.39 从二维到真实模型:三处不同
本章用两个 token、两个点、四个类。真实模型用 77 个 token、4096 个潜位置。
哪些能搬?
8.39.1 第一处:token 的数量
本章 2 个,真实模型 77 个。
这改变的是”绑定”的难度: 两个 token 只有两种排列,模型很容易学; 77 个 token 的排列组合是天文数字,模型只能学会”常见的那些”。
这解释了为什么真实模型在”三个以上对象的关系”上经常出错—— 不是机制不对,是训练分布覆盖不到。
8.39.2 第二处:位置的含义
本章的两个位置是”左点”和”右点”,是固定的两槽。
真实图像的 4096 个位置是二维网格,而且经过多层卷积/注意力之后, 每个位置感受野不同、语义不同。
所以真实模型里”位置”不是一个编号,是一个二维坐标 + 层级。 位置编码通常是二维的(或者用 RoPE 的二维版本)。
8.39.3 第三处:文本编码器本身
本章的”编码器”只是一张查找表——没有自注意力、没有上下文。
真实编码器有 12–24 层 Transformer, 所以”红色”的向量会受”深红色”“红色背景”影响。
| 差异 | 对应的失败 |
|---|---|
| token 多 | 对象多于两个时绑定出错 |
| 位置是二维网格 | 空间关系(“在……左边”)难精确 |
| 编码器有上下文 | 换 tokenizer / 换模型后行为漂移 |
本章能验证的是机制(绑定需要逐位置的条件), 不能验证的是规模效应。
8.40 五个常见误解
8.40.1 误解一:“提示词写得越长越好”
错,而且有两个上限。
硬上限:CLIP 的 77 个 token。超出的部分直接消失, 而且丢掉的往往是句尾——人写提示词时习惯把重要的放最后。
软上限:编码器的注意力会被稀释。 77 个 token 里如果 60 个是废话,真正有用的词在交叉注意力里的份额会变小。
实践含义:短提示词 + 具体名词,通常好过长提示词 + 形容词堆砌。
8.40.2 误解二:“换一个更强的文本编码器就能提升效果”
不一定,因为 UNet 是在旧编码器的输出分布上训练的。
换编码器之后:
- 输出维度可能变(768 → 4096)
- 语义空间完全不同(CLIP 的”红”和 T5 的”红”不是同一个方向)
- 空条件的编码也变了
所以换编码器 = 换了一门语言 = 必须重训 UNet。 SD3 换 T5 是重训过的,不是”插上就用”。
8.40.3 误解三:“同义词可以随便替换”
在嵌入空间里,同义词不是同一个点。
“红色”和”赤色”的向量不同,模型对它们的行为可能不同—— 尤其当训练数据里”赤色”很少时。
这也解释了一个现象:某些”魔法提示词”(比如特定的艺术家名字) 效果特别好,因为它们在训练数据里有密集的对应。 换成同一个人名的另一种写法,效果可能差很多。
8.40.4 误解四:“提示词权重(word:1.3)是模型理解语法”
不是。 大多数实现里,权重语法在编码器之前就被处理掉了:
- 解析
word:1.3 - 单独编码
word - 把它的向量乘 1.3
- 拼回序列
模型看到的不是”1.3 这个数”,是一个被放大过的向量。
所以权重的效果依赖实现:不同管线对权重的归一化方式不同, 同一个 1.3 在不同模型上强度不同。
8.40.5 误解五:“注意力图能告诉我模型在看哪个词”
本章的实测:不能。
权重 0.44–0.58,与绑定属性相关 −0.079,用它猜颜色 47.0%。
能告诉你的是:“这个位置和这个词的 key 比较相似”。 不能告诉你的是:“这个位置的信息来自这个词”。
区别在 V 和后面的 MLP——信息可以从别处流进来。
它们都把”文本侧”和”图像侧”当成可以独立优化的两部分。
- 一:以为编码器能装下任意长
- 二:以为编码器可以单独升级
- 三:以为词义是稳定的
- 四:以为语法被理解了
- 五:以为内部权重等于因果
实际上,文本和图像是通过”训练时一起见到的分布”绑定的。 任何一侧单独改变,都会打破这个绑定。
8.41 本章没有回答的问题
每一章都应该留下几个”知道怎么查、但这一章没查”的问题。 下面每个问题都配了一个可执行的实验设计——它们是可以直接开工的题目。
8.41.1 问题一:为什么注意力图不可解释?
已知:权重几乎均匀(0.44–0.58),与绑定属性的相关 −0.079, 用它猜颜色 47.0%,而输出准确率 1.000。
未知:是”权重本身没信息”,还是”权重有信息但被下游掩盖”?
实验设计:
- 硬注意力:把 softmax 换成 argmax(或 straight-through),重训
- 权重冻结:训练后固定注意力权重,只训后面的 MLP
- 权重干预:人为把左点的权重设成 0.9/0.1,看输出是否改变
判据:
- 如果硬注意力仍然能绑定,说明绑定不依赖权重的精细分布
- 如果冻结权重后性能不变,说明权重根本不是瓶颈
- 如果人为干预能改变输出,说明权重是因果链的一环,只是自然训练时不承担这个角色
我的预期(可证伪):硬注意力会掉点,但不会归零; 冻结权重会掉很多——因为权重仍然携带”哪个点是哪个点”的信息, 只是这些信息不是以人类可读的方式编码的。
8.41.2 问题二:三个以上的对象会怎样?
已知:两个对象、两种排列,交叉注意力 1.000,均值池化 0.550/0.449。
未知:对象数从 2 增加到 3、4、5 时,准确率怎么衰减?
实验设计:
- 槽位数 k=2,3,4,每个槽 c 种颜色
- 类别数 c^k:4,8,16
- 每种配置训 3 个种子,每类 300 个样本
- 画”对象数 vs 双槽/全槽准确率”
判据:如果准确率随 k 指数衰减,说明模型在学”每个排列”而不是”绑定规则”; 如果是线性衰减,说明它学到了可组合的规则。
这个区分很重要:前者意味着”再加一个对象就要加数据”, 后者意味着”规则可以外推”。
8.41.3 问题三:位置嵌入是否足够?
已知:去掉 token 的位置嵌入,准确率从 1.000 掉到 0.602/0.397, 但对调实验显示输出逐位相同——那 0.602 是”同一批样本被从两边数”的假象。
未知:不同的位置表示,哪一种最适合绑定?
实验设计:四种编码,同一套训练:
| 编码 | 形式 |
|---|---|
| 无 | k_i=e_{w_i} |
| 可学习绝对 | k_i=e_{w_i}+p_i |
| 正弦绝对 | k_i=e_{w_i}+\text{Sin}(i) |
| 相对 | 在打分里加 b(i-j) |
判据:不光看准确率,还要看对调差。 准确率会被”互补假象”骗,对调差不会。
8.41.4 问题四:空字符串的编码有多稳定?
已知:空条件必须是”训练时见过的那个向量”,零向量会崩。
未知:不同编码器对 "" 的输出,彼此有多远?这个距离会不会改变最优 w?
实验设计:
- 取 CLIP-L、CLIP-G、T5-XXL 三个编码器
- 编码
"",得到三个向量 - 算两两余弦相似度
- 在同一个 UNet 上(如果可能)扫 w
判据:如果余弦相似度低于 0.5,说明”空条件”在三者之间不可迁移—— 换编码器必须重新定标 w,不能沿用旧值。
8.41.5 问题五:文本编码器该不该微调?
已知:大多数模型冻结编码器,理由是省显存、省配对数据、稳定。
未知:微调带来的收益,值不值这个代价?
实验设计:
- 固定 UNet,只微调文本编码器的最后 n 层(n=0,2,4,8)
- 用同一批图文对,同样的训练步数
- 测三个指标:绑定准确率、CLIP score、生成多样性
判据:如果 n=2 就能带来明显提升,说明”冻结全部”是过度保守; 如果必须 n=8 才有收益,而多样性明显下降,说明冻结是对的。
8.41.6 问题六:能不能”学”出一个绑定头?
这是本章最想做但没做的一个实验。
想法:在交叉注意力之后加一个小的”绑定模块”, 显式地把 token 和位置配对,再喂给 UNet。
它相当于把”绑定”从隐式学习变成显式结构**。
风险:真实提示词没有”哪个词属于哪个对象”的标注, 这个模块没有监督信号。
一个可能的弱监督:用现成的目标检测器, 在生成图上检测物体,反推”哪个 token 应该对应哪个框”。
如果这个思路成立,它会是一个比”加大模型”更便宜的改进方向。
它们都不是”再多训一会儿”能回答的。
每一个都需要改结构、换数据、或者设计新的测量。 这就是”研究问题”和”工程问题”的区别—— 工程问题是”怎么让它跑起来”,研究问题是”为什么它这样跑”。
8.42 每一节的边界
| 结论 | 前提 | 什么时候失效 |
|---|---|---|
| 均值池化会丢绑定 | 条件里有顺序/对应关系 | 只有一个对象时不丢(RR/BB 是 1.000) |
| 交叉注意力能绑定 | K 里有位置信息 | 去掉位置嵌入后对调差 0.000(准确率 0.602 是互补假象) |
| 注意力图不可解释 | 这个 2 层 MLP 的模型 | 更深的网络、更大的数据可能不同 |
| 空串是”被训练过的条件” | 训练时做过文本丢弃 | 没做过丢弃 → 空串没被训练过 |
| CFG 收益为零 | w=1 已饱和 | 更难的任务上仍有收益 |
| 77 token 上限 | CLIP 的位置编码 | T5/Qwen 没有这个限制 |
最需要警惕的是第三行。
本章的”注意力图不可解释”是在一个 10 万参数的玩具模型上测的。 它不能推广成”所有注意力图都没用”——第 4 章在语言模型上就找到了 可解释的头。
能推广的是方法论:别假设注意力图能解释,先做一次消融验证。
8.43 术语对照
| 中文 | English | 代码里 |
|---|---|---|
| 分词器 | tokenizer | tokenizer |
| 词嵌入 | token embedding | embed_tokens |
| 位置编码 | positional encoding | position_ids、pos_embed |
| 交叉注意力 | cross-attention | attn2、encoder_hidden_states |
| 自注意力 | self-attention | attn1 |
| 潜空间 | latent space | latents、latent_image |
| 缩放因子 | scaling factor | vae.config.scaling_factor |
| 空字符串 | empty prompt | ""、uncond_prompt |
| 绑定 | binding / attribute binding | — |
encoder_hidden_states 在 diffusers 的 UNet 里指文本, 不是图像编码器的隐藏状态。
看到它出现在 attn2(交叉注意力)的输入里,就是文本那一路。
8.44 文本条件怎么评估
不能只看”像不像”,也不能只看 CLIP score。
8.44.1 三种评估,对应三个问题
| 问题 | 指标 | 它能发现什么 |
|---|---|---|
| 图和文字配不配? | CLIP score | 整体语义偏离 |
| 属性有没有绑对? | 受控提示词的槽位准确率 | 绑定失败 |
| 改一个词,输出变不变? | 消融差分 | 文本有没有被用起来 |
本章的”双槽准确率”就是第二种——它要求两个位置都摆对, 而不是”整体像不像”。
8.44.2 CLIP score 的盲区
CLIP score 是”整图 vs 整句”的相似度。
它看不见:
- 顺序:“猫在狗左边”和”狗在猫左边”的 CLIP score 可能一样高
- 数量:“两只猫”和”三只猫”
- 否定:“没有猫”
所以一个模型可以 CLIP score 很高,绑定全错。
CLIP score 是一个标量,它把”哪里对、哪里错”平均掉了。
当一个指标只给一个数,它必然丢掉结构信息。
所以:至少加一个”结构化的”指标——槽位准确率、数量准确率、 或者简单的消融差分。
8.44.3 真实模型怎么测:把提示词拆成原子事实
本章的”双槽准确率”在玩具上是够的。真实模型怎么测?
核心思路是一样的:把一句话拆成若干条可以独立判定的”原子事实”。
例:a red cube and a blue sphere
| 原子事实 | 怎么判 |
|---|---|
| 有一个方块 | 检测器找到 cube |
| 有一个球 | 检测器找到 sphere |
| 方块是红的 | 在方块框里测主色 |
| 球是蓝的 | 在球框里测主色 |
四条全对,才算这张图”完全符合”。
这就是 2023 年之后几个组合性 benchmark 的基本做法:
- T2I-CompBench:用 VQA 模型回答”这个物体是什么颜色”之类的问题
- GenEval:用目标检测器定位物体,再判颜色/数量/位置
- 两者共同的假设:一个足够好的检测器 + 一个足够好的 VQA,可以代替人
① 检测器漏检 = 判成”错”。 小物体、遮挡、非典型姿态,检测器经常找不到。 模型可能画对了,但被判错。
② VQA 模型有它自己的先验。 问”这个球是蓝色的吗”,VQA 可能因为”球通常是某种颜色”而答是。 这是把偏差从一个模型转移到了另一个模型。
③ 原子事实之间不独立。 “有一个方块”和”方块是红的”其实共享同一个检测框。 分开判会把一个错误数两次。
所以 benchmark 的分数只能横向比较,不能当成”准确率”。
一个更稳的做法:报告每个原子事实的通过率,而不是总分。
总分会把”颜色全对、数量全错”和”颜色错一半、数量对一半”压成同一个数。 而这两种失败对应完全不同的修法。
8.44.4 用 VLM 当裁判:能做,但要校准
近两年很多工作直接用多模态大模型打分: 把图和提示词一起给 VLM,问”符合吗,1–5 分”。
优点:不需要预先定义原子事实,能处理复杂提示词。
缺点:
- 分数漂移:同一个模型,换个问法,分数变
- 偏宽松:VLM 倾向于给”大致符合”高分
- 不可复现:模型版本一变,历史分数不可比
校准的办法:
- 用一批人工标注的样本,测 VLM 和人的一致率
- 报告一致率,不只报告分数
- 固定 VLM 版本,写进实验记录
如果一致率低于 80%,这个裁判不能用。
8.44.5 本章的测量为什么不需要这些
因为本章的数据是四个点,判定是”符号对不对”。
这是一个”零歧义”的任务: x 坐标的符号就是颜色,没有检测器、没有 VQA、没有主观判断。
这正是选择玩具模型的价值: 把”测量”这件事从问题里去掉,只剩下”机制”。 真实模型上,测量本身就是一个研究方向。
8.44.6 一个五分钟的消融
不需要标注数据,也不需要训练分类器:
- 固定种子,用提示词 A 出 4 张
- 固定种子,把 A 里一个词换掉(比如”红”→“蓝”)出 4 张
- 比较两组的差异
如果几乎一样,说明那个词没被用起来。
这比看注意力图可靠——它是行为证据,不依赖内部解读。
8.45 本章数字总表
所有数都可以用 notebooks/run_ch8_*.py 复现。这一节按”来源”归类,方便核对。
8.45.1 来自 run_ch8_case.py
| 量 | 值 | 说明 |
|---|---|---|
| 均值池化参数 | 67,332 | 3 层 MLP |
| 交叉注意力参数 | 100,740 | 同样 3 层 |
| 训练损失(均值 / 交叉) | 0.0476 / 0.0295 | 6000 步,同数据同优化器 |
| RB 准确率(均值 / 交叉) | 0.550 / 1.000 | 5 个种子 × 300 样本 |
| BR 准确率(均值 / 交叉) | 0.449 / 1.000 | 同上 |
| 注意力相关 | −0.079 | 与左色 |
| 用注意力猜色 | 47.0% | 二元分类 |
8.45.2 来自 run_ch8_ablation.py
| 量 | 值 |
|---|---|
| 均值池化对调差 | 0.000 |
| 交叉注意力对调差 | 0.972 |
8.45.3 来自 run_ch8_ablate2.py
| 量 | 值 |
|---|---|
no_pos RB / BR |
0.602 / 0.397(和 0.999) |
no_pos 对调差 |
0.000 |
no_xy RB / BR |
1.000 / 1.000 |
no_xy 对调差 |
0.931 |
| 学习曲线(mean,500→6000 步) | 0.400 / 0.443 / 0.497 / 0.497 / 0.500 |
| 学习曲线(cross) | 1.000(每一步) |
| 注意力熵(full / no_xy / no_pos) | 0.984 / 0.998 / 0.685 bit |
8.45.4 来自 run_ch8_capacity.py
| 层数 | 参数 | 损失 |
|---|---|---|
| 3 | 67,332 | 0.0476 |
| 4 | 83,844 | 0.0469 |
| 5 | 100,356 | 0.0480 |
| 6 | 116,868 | 0.0482 |
8.45.5 来自 bpe_demo.py / 留出集测量
| 合并次数 | 留出集汉字/token | 77 token 装得下 |
|---|---|---|
| 300 | 0.625 | 48 字 |
| 1000 | 0.967 | 74 字 |
| 2000 | 1.210 | 93 字 |
它让你能一眼看出”哪些数互相矛盾”。
比如:no_pos 的 0.602 和它的对调差 0.000 放在一起, 矛盾立刻暴露——准确率说”部分成功”,干预说”完全失败”。
如果这两个数分散在两节里,读者很可能只记住 0.602。
所以”数字总表”不是附录,是校验工具: 任何一对数放在一起读,都应该讲同一个故事。**
8.46 一张速查卡
贴显示器旁边的那种。
8.46.1 三步
| 步骤 | 做什么 | 关键 |
|---|---|---|
| 分词 | 字符串 → token | 词表大小决定”一句话多长” |
| 查表 | token → 向量 | 和语言模型一样 |
| 编码 | 向量带上下文 | 自注意力 + FFN |
8.46.2 交叉注意力
| 量 | 来自 |
|---|---|
| Q | 图像位置(+ 时间步) |
| K | 文本 token(+ 位置嵌入) |
| V | 文本 token(+ 位置嵌入) |
8.46.3 八个数
| 量 | 值 |
|---|---|
| 均值池化 RB/BR | 0.558 / 0.442(和 0.999) |
| 交叉注意力 RB/BR | 1.000 / 1.000(和 2.000) |
| 对调差:完整 / 均值 / 无位置 / 无坐标 | 0.972 / 0.000 / 0.000 / 0.931 |
| 注意力相关(与左色) | −0.079 |
| 用注意力猜色 | 47.0% |
| 注意力熵:完整 / 无坐标 / 无位置 | 0.984 / 0.998 / 0.685 bit |
| 损失:均值(同参数)vs 交叉 | 0.0480 vs 0.0295 |
| CLIP 上下文 | 77 token |
8.46.4 五个坑
- 均值池化 → 顺序信息被销毁
- 没有位置嵌入 → 输出逐位不变;准确率 0.602 是互补假象
- 零向量当空条件 → 输入在分布外
- 换 tokenizer 不复验 → 空条件变了
- 用注意力图判断 → 尖锐的权重也可能零信息(熵 0.685 却不绑定)
如果只能记一行:先看镜像类准确率之和——等于 1 就是没有顺序,等于 2 才是真的绑上了。
8.47 这一章的方法论
为什么用”两个点、两个词、四个类”这么小的设定?
因为要回答的问题很具体:顺序信息能不能传到输出。
这个问题的答案有三种证据,强度依次递增;最后再加一条边界。
8.47.1 第一层:代数
均值是置换不变的。 这一点不需要实验—— \frac12(a+b)=\frac12(b+a) 是恒等式。
所以”均值池化会丢顺序”是一个定理,不是一个发现。
但定理只告诉你”会丢”,不告诉你”丢了之后表现成什么样”。
8.47.2 第二层:统计
训练两个模型,测四类的准确率。
本章的结果:均值池化在 RB/BR 上 0.550/0.449, 交叉注意力 1.000/1.000。
这一层告诉你”丢多少”,但不告诉你”模型内部怎么走的”。
8.47.3 第三层:行为
对调两个词,看输出变不变。
- 均值池化:条件向量相同 → 输出逐位一致
- 交叉注意力:输出翻转
这一层最强,因为它不依赖任何内部量的解读。
注意力权重是内部量,它的含义依赖你怎么解释。 本章的权重看起来均匀,但绑定确实发生了——解释失败了,行为没失败。
对调词是外部干预:你改变了输入,观察输出。 因果方向清楚,不需要解释中间发生了什么。
这也是第 4 章那个教训的延续: 要证明”某部分在起作用”,改它、看输出,而不是看它的数值。
8.47.4 第四层:规模——这一层本章给不了
前三层都能在玩具上完成。第四层不能。
“这个机制在 10 亿参数的模型上还成立吗?” 本章的每一个结论,都只在 10 万参数、4 维数据上验证过。
规模可能改变什么?
① 数据量可以让”学不会”变成”学得会”。 本章的 no_xy 在 2 个对象上成功了。 4 个对象时,下游 MLP 要学的映射从 2! 种变成 4! 种—— 可能需要的数据量差一个数量级。
② 更深的网络可以补偿更弱的条件。 本章的下游是 3 层 MLP。 如果换成 30 层,均值池化的条件向量仍是同一个—— 它不会因为网络更深而获得信息。 但”全局条件 + 下游路由”这条路,会随着深度变得更可行。
③ 真实数据的分布比四个点复杂得多。 本章的”左/右”是坐标的符号,真实图像的”左/右”要先被卷积层理解成空间概念。 这一步的难度,玩具模型完全没有体现。
能推广的是”必要条件”,不是”数值”。
- 能推广:条件必须携带顺序;池化会销毁它;注意力权重不是因果
- 不能推广:0.550 这个数、0.972 这个差、熵 0.984 这个值
看到一篇论文说”我们的方法在 XXX 上提升了 3 个点”, 先问:它改变的是”信息”,还是”优化”? 本章的容量对照(参数相同、损失差 63%)就是区分这两者的办法。
8.47.5 三个设计准则
如果你要做一个类似的”最小实验”,这三条值得抄。
准则一:让”错误做法”在结构上必然失败,而不是碰巧失败。
均值池化不是”训练不好”,是”数学上不可能”—— \frac{1}{2}(a+b)=\frac{1}{2}(b+a) 是恒等式。 这样你不需要解释”为什么它没学会”,只需要解释”它为什么不可能学会”。
反过来,如果你的错误做法只是”效果差一点”,你就无法排除”再多训一会儿就好了”。
准则二:把”容量”和”信息”分开。
本章做了两次: 训练损失(0.0476 vs 0.0295)和容量对照(参数加到 11.7 万,损失不变)。
没有这两步,“均值池化更差”可以被解释成”它参数少”。 有了这两步,只剩一个解释:条件里的信息少了。
准则三:至少有一个”不依赖准确率”的指标。
本章有三个:对调差、镜像类准确率之和、注意力熵。
为什么需要它们? 因为 0.602 那个假象—— 准确率会在”互补计数”时给出一个看起来像”部分成功”的数。 换一个指标,假象立刻消失。
8.47.6 这套方法可以搬到哪
任何”条件怎么进模型”的问题,都可以用同样的三层:
- 代数:这个操作对什么不变?(均值对顺序不变、注意力对位置等变)
- 统计:在受控数据上测准确率
- 行为:改条件、看输出
再加一条贯穿始终的原则:每次看到一个”变好了”或”变差了”的数, 先问”有没有另一种解释”。
本章出现过两次”另一种解释”: 均值池化的损失差可能是容量(用容量对照排除), 无位置嵌入的 0.602 可能是部分绑定(用对调实验排除)。
两次都是”看起来合理”的解释,两次都被实验推翻了。
这就是这一章真正想教的东西: 不是交叉注意力怎么算,是”怎么知道自己算对了”。
8.48 动手练习
六个练习,从”复现一个数”到”自己设计一种条件”。 前三个只需要本章的代码,后三个要改结构。
8.48.1 练习一:复现对调实验
跑 run_ch8_ablation.py,确认你能得到:
| 编码器 | 对调后的最大逐元素差 |
|---|---|
| 均值池化 | 0.000 |
| 交叉注意力 | 0.972 |
然后只改一个变量:把采样种子从 7 换成 8,重跑。
预测:两个数会变吗?
提示:想清楚”逐位一致”是结构性的还是运气好。 如果是结构性的,换任何种子都应该是 0.000。 如果你测出非零,先检查是不是浮点求和顺序造成的(10^{-7} 量级不算)。
8.48.2 练习二:一个看起来对、其实错的修法
有人说:“均值池化丢顺序,那我改成求和 + 位置嵌入不就行了? 每个 token 加上它的位置,再求和。”
任务:
- 先不跑代码,写出 RB 和 BR 的条件向量,看它们是否相同。
- 再改成”不池化,直接返回两个位置的向量”,重跑。
提示:求和是交换的;位置嵌入是按下标加的,不是按词加的。
设 RB 的两个词是 w_0=\text{红},w_1=\text{蓝},BR 是 w_0=\text{蓝},w_1=\text{红}。
求和 + 位置:
c_{\text{RB}}=(e_{\text{红}}+p_0)+(e_{\text{蓝}}+p_1) c_{\text{BR}}=(e_{\text{蓝}}+p_0)+(e_{\text{红}}+p_1)
两式完全相等。 加法交换,p_0 和 p_1 只是被重新分组。
所以这个”修法”一点用都没有。 问题不在”有没有位置信息”,在”池化这个操作把位置抹掉了”。
正确的修法是取消池化:保留 (e_{w_0}+p_0,\ e_{w_1}+p_1) 这个序列, 让下游自己决定怎么用。
这道题的价值:它区分了”有位置信息”和”用了位置信息”。 两者不是一回事。
8.48.3 练习三:加第三个槽
本章是”左、右”两个槽。改成三个槽(左、中、右),四个颜色。
任务:
- 改
make_two_point_data,从 4 维改成 6 维,类别从 4 个变成 8 个 - 两种编码器各训一次
- 报告每个类的槽位准确率
预测:
- 均值池化在”三种颜色各不同”的类上,准确率应该是多少?
- 交叉注意力会掉点吗?为什么?
提示:三个槽、每种颜色可重复,排列数是 3!=6 对于三个不同色。 均值池化对这 6 种排列给出同一个向量。
第二个问题的答案取决于数据量:交叉注意力的参数量没变多少, 但要学的对应关系从 4 种变成了 8 种。样本效率会下降。
8.48.4 练习四:把 NULL token 换成零向量
本章的空条件是学出来的 NULL token(编号 2)。
任务:把它换成全零向量,重跑 CFG 扫描(w=1,3,5,10)。
预测:参考第 7 章的实测——零向量当空条件, w=3 时越界 100%、多样性归零。
然后解释:为什么”全零”比”学出来的 NULL”更糟? 提示:零向量在嵌入空间里是一个特定的点, 它离哪个词近、离哪个词远,是偶然的。
8.48.5 练习五:用掩码代替学习
本章的绑定是”学”出来的:模型自己学会左点看第一个词。
任务:不给它学——直接加硬掩码。
# 左点只能看 token 0,右点只能看 token 1
mask = torch.tensor([[[1., 0.], [0., 1.]]]) # (1, 2 点, 2 词)
att = att.masked_fill(mask == 0, float("-inf"))在 softmax 之前加这一行,重训,测四类准确率。
预测:应该 1.000——因为掩码直接告诉它”左点听第一个词”。
然后想一个问题:既然掩码这么有效,为什么真实模型不用它?
提示:掩码假设”第 0 个词对应左边”。 但真实提示词里,“左边”这个词可能出现在任何位置。 掩码编码的是位置的对应关系,不是语义的对应关系。
8.48.6 练习六:写一个绑定诊断器
把本章的诊断动作做成一个类:
class BindingDiagnoser:
def swap_test(self, model, sch, tok_a, tok_b): ...
def classify(self, samples): ...
def report(self): ...要求:
swap_test返回最大逐元素差,不是平均差classify要能处理”两个槽都摆对”和”只有一个槽对”report要给出三档判断:绑定成功 / 部分绑定 / 完全没绑定, 并且每一档都要有数字依据
判据建议:
| 判断 | 条件 |
|---|---|
| 完全没绑定 | 对调差 <10^{-6},且两类准确率之和 \approx 1,各 \approx 0.5 |
| 部分绑定 | 对调差明显 >0,但准确率在 0.6–0.95 |
| 绑定成功 | 对调差 >0.5,准确率 \ge0.99 |
六个练习大约四小时。
- 练习一、二:把”置换不变”从一个词变成你能手推的东西
- 练习三:看到规模效应——同样的机制,更多的组合,更难的优化
- 练习四:把第 7 章的结论迁移到文本条件
- 练习五:理解”掩码”和”学习”的分工
- 练习六:一套能直接用在真实模型上的诊断工具
如果只做一个,做练习二。 它用一个五行的代数推导,拆掉了”加位置嵌入就能救池化”这个很常见的错觉。
8.49 把这一章讲给别人听
同一份内容,三种时长。
8.49.1 五分钟版
三句话:
- 文本编码器把一句话变成一串向量,不是一个向量——77 个 token 就是 77 份条件。
- 交叉注意力让每个图像位置各自去问一遍;均值池化让所有位置共用同一个答案, 所以 RB 和 BR 在它眼里是同一个条件(0.550 / 0.449)。
- 注意力权重不能解释输出:权重几乎是均匀的(相关 −0.079), 但对调两个词,交叉注意力的输出翻转 0.972,均值池化的输出差恰好 0.000。
如果对方只问一个问题,多半是”那提示词怎么写才管用”。 答:写模型训练数据里常见的句式,把最重要的词放前面, 别指望用文字精确控制位置——那需要另一条通道。
8.49.2 三十分钟版
在五分钟版之上加两块:
① 手算的那个例子。 两个 token、两个点,左点的注意力从 0.893 翻到 0.781。 这一组数能让人看见”绑定的全部机制就是下标和内容被分开”。
② 空条件的那个坑。 零向量当空条件,w=3 就崩。 它说明”文本条件”不是一个可以随手拼出来的张量,是一个必须被训练过的分布。
8.49.3 两小时版
完整讲,但顺序调整过:
- 先讲失败(均值池化在 RB/BR 上 50/50)
- 再讲机制(手算交叉注意力)
- 然后实测(1.000 vs 0.550,以及对调实验的 0.000 / 0.972)
- 最后边界(注意力图不可信、空间控制要另开通道)
先失败后机制,比”先公式后例子”更容易让人记住—— 因为”同一个词袋、两种顺序”本身就是一个让人想不通的问题。
① “那我把提示词写详细一点,不就能控制顺序了吗?” 不能。详细只是增加 token,不改变”token 之间没有修饰关系”这个事实。 “红方块和蓝球”写成 50 个词,交叉注意力里红和蓝仍然在竞争。 要解决它,得换条件结构(区域掩码、结构化引导),不是换措辞。
② “注意力图看起来很正常,为什么输出还是错的?” 因为权重只是链条上的一环。 V 里带着位置,query 里带着坐标, 后面还有 MLP。权重均匀不代表信息没传过去。 想判断,就做干预实验,别看热图。
③ “换个更强的文本编码器有用吗?” 有,但必须重训 UNet。 编码器换了,条件分布就换了。 “插上就用”只在两个编码器输出分布几乎相同时成立,而那种情况很少。
8.50 如果你只有一小时
这一章约四万字。时间不够时,按这个顺序读。
| 时间 | 读什么 | 得到什么 |
|---|---|---|
| 0–12 分 | 案例 + 手算交叉注意力 | 绑定问题的全部机制 |
| 12–25 分 | 实测表 + 对调实验 | 0.550 与 1.000,0.000 与 0.972 |
| 25–40 分 | 注意力图 + 空字符串 | 两个最容易踩的坑 |
| 40–50 分 | 潜空间 + 端到端八步 | 真实模型长什么样 |
| 50–60 分 | 速查卡 + 排错十条 | 带走一页纸 |
可以跳过的:CLIP 的训练目标、条件注入的五代谱系、caption 工程、延伸阅读。
不要跳过的:对调实验与空条件。 前者教你”怎么证明文本真的起作用”,后者教你”为什么 w>0 会崩”。
8.51 一句话
条件不是”一句话”,是”带顺序的一份信息,加上一条取用的路径”。
把它拆开看:
- 带顺序:均值池化把两句话压成同一个点,信息在那一步就没了
- 取用路径:交叉注意力让每个位置自己来取;
no_xy让下游 MLP 用坐标来取 - 两者缺一不可:只有顺序没有路径,或者只有路径没有顺序,都绑不上
三句话对应三个坑:
- 不懂”带顺序” → 用池化,绑定失败还以为是训练不够
- 不懂”取用路径” → 换模型时不知道该检查哪一层
- 不懂”两者缺一不可” → 看到准确率 0.602 就以为”部分成功”
所以这一章的每一个实验,本质上都在测这两件事:顺序在不在,路径通不通。
8.52 几个术语的由来
术语不是随便起的。知道它从哪来,能少记一半。
“条件”(conditioning)来自概率论。 p(x\mid c) 读作”给定 c 时 x 的分布”。 它原本没有”控制”的意思——只是一个已知量。 工程上把它变成”控制”,是因为我们开始优化这个已知量。
“交叉注意力”(cross-attention)原本叫”编码器-解码器注意力”。 2017 年的 Transformer 用它让解码器看编码器的输出。 后来大家发现”两个不同序列之间的注意力”到处都是, 就改叫 cross-attention。本章的图像-文本只是其中一种。
“提示词”(prompt)来自自然语言处理。 最早是”给模型一个开头,让它续写”。 扩散模型借用了这个词,但含义变了: 它不再是”续写的起点”,是”生成的条件”。 同一个词,两种机制——这是很多混淆的来源。
“引导”(guidance)来自分类器引导。 2015 年的工作用一个单独训练的分类器给扩散过程指方向。 后来发现不需要分类器,条件模型自己就能提供方向(CFG)。 “无分类器引导”这个名字里还留着历史的痕迹。
“潜空间”(latent space)来自表示学习。 它指”压缩后的、没被解码的表示”。 注意它不是一个特定的空间:VAE 有 VAE 的潜空间,CLIP 有 CLIP 的嵌入空间。 本章说的”潜空间”特指 VAE 的那个。
“绑定”(binding)来自认知科学。 它研究的是”大脑怎么把颜色和形状组合成一个物体”。 视觉语言模型借用了这个词,因为失败的样子很像: 颜色和形状都在,但配错了对象。
“token”没有公认的中文翻译。 “词元”“标记”“令牌”都有人用,本书用”token”—— 因为它不完全等于”词”,也不完全等于”字”。 一个 token 可能是一个字、半个词、或者三个字节。
因为术语会误导设计。
如果你以为”条件”就是”控制”,就会奇怪为什么加大 w 不听话; 如果你以为”prompt”就是”开头”,就会奇怪为什么它不能续写; 如果你以为”注意力”就是”关注”,就会拿热图当解释。
每一个误解,都对应一个术语的历史包袱。
8.53 写在这一章的最后
这一章讲了四万字,如果只留下一件事,我希望是这个:
接口的形状,比模型的大小更重要。
本章的两个模型,参数只差 1.5 倍。 一个在 RB/BR 上是 0.550,一个是 1.000。 差别不在容量,在”条件怎么进来”。
这件事在真实模型上同样成立。 同一个 UNet,换一种条件注入方式,效果可以差一个档次; 同一个编码器,换一种提示词写法,效果也可以差很多。 而我们花在”换更大的模型”上的时间,往往多于花在”改接口”上的时间。
第二个想留下的是方法论。
这一章有两次”差点被骗”:
第一次,均值池化的损失更高(0.0476 vs 0.0295), 看起来像”模型太小”。容量对照把它排除了。
第二次,无位置嵌入的准确率是 0.602 / 0.397, 看起来像”部分绑定”。对调实验把它排除了。
两次都是”一个看起来很合理的解释”, 两次都被一个更简单的实验推翻。
而这两个实验都不复杂: 一个是”把参数加到一样多”,一个是”把两个词对调”。 难的不是做实验,是想出”该做哪个实验”。
第三个想留下的是一个边界。
本章的所有结论,都建立在一个 10 万参数的玩具模型上。 它能告诉你”必要条件”,不能告诉你”数值”。
真实模型的绑定失败,可能来自完全不同的原因: 训练数据的 caption 不够细、UNet 的容量不够、采样步数太少。 本章的机制只是其中一条。
所以读完之后,不要急着把”均值池化 0.550”套到 Stable Diffusion 上。 要问的是:这个模型的”池化”在哪里?它的”位置嵌入”是什么?
找到了,本章的方法就能用;找不到,说明你还没看懂它的条件路径。
最后一句,给要做研究的人:
这一章留下了六个问题,每一个都可以做成一篇论文。 它们都不需要更大的算力,只需要更好的实验设计。 而”更好的实验设计”,正是这本书想教的东西。
8.54 本章回顾
把这一章压缩成六句话。
一、文字进入图像要过三步:分词、查表、编码。 前两步只是”换一种表示”,第三步(编码器)才让词与词互相影响。
二、交叉注意力是”图像问、文本答”。 Q 来自图像位置,K,V 来自文本 token。 每个位置得到自己的条件向量,而不是全图共用一个。
三、绑定的必要条件是”条件里带着顺序”,不是”每个位置各一份”。 均值池化把顺序平均掉了,RB/BR 是 0.550/0.449——加起来 0.999,即零信息。 交叉注意力是 1.000/1.000——加起来 2.000,才是真的绑上了。
四、注意力权重不能解释绑定,而且”越尖锐越有用”也是错的。 完整模型的权重几乎均匀(熵 0.984); 去掉坐标后完全均匀(熵 0.998),绑定依然完美; 去掉位置嵌入后权重很尖锐(熵 0.685),却完全不绑定。
五、空字符串不是零向量。 它是 [BOS][EOS] 之类特殊 token 的编码结果,是一个被训练过的表示。 自己拼零向量当空条件,第 7 章的实测是越界 100%。
六、文本条件下的 CFG 公式不变,但收益可能为零。 本章的 w 扫描从 1 到 10,准确率一直是 1.000(w=10 时 0.999)—— 因为任务在 w=1 时已经解决了。引导没有可提升的空间,只有成本。
如果把这一章压成一张表:
| 问题 | 答案 | 本章的数 |
|---|---|---|
| 条件怎么进模型 | 每个位置一个向量(或一份全局顺序表示) | — |
| 为什么不能用均值 | 顺序信息被平均掉 | RB/BR 0.550/0.449,和 0.999 |
| 交叉注意力强在哪 | 条件带顺序 + 每位置可取 | 四类全 1.000 |
| 去掉位置嵌入会怎样 | 输出逐位不变 | 对调差 0.000 |
| 注意力图能信吗 | 不能 | 熵 0.998 仍绑定;熵 0.685 不绑定 |
| 空条件是什么 | 编码器的非零输出 | 零向量 → 越界 100% |
| CFG 还有用吗 | 取决于 w=1 是否已饱和 | w=1..10 均 ≥0.999 |
最后留一个问题
本章把 query 里的坐标抹掉,两个点问出完全一样的问题, 注意力权重完全均匀(0.525/0.525)。
结果:绑定依然 1.000。
那”每个位置各一份条件”这件事,到底是不是必需的?
本章的答案:不是必需的。必需的是”条件里带着顺序”。 只要 K 里的位置嵌入让 RB 和 BR 产生不同的全局向量, 下游 MLP 就能借助输入 x 里的坐标把颜色分配对。
那交叉注意力买到了什么?
买到的是”组合性”: 全局条件要背下 k! 种排列,逐位置条件只要学 k 组对应。 对象一多,前者需要指数级的数据。
这个问题还可以再推一步: 如果把对象数加到 4 个、6 个, “全局条件 + 下游路由”会在哪里崩? 本章只做到 2 个。这是一个明确留下的边界。
下一章是附录:数学补充、术语表、实验索引、notebook 环境与作业、延伸阅读。 但如果只带走一件事:先看镜像类的准确率之和——等于 1,说明顺序根本没进去。