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 的词袋是相同的—— 都是”一个红、一个蓝”,只是顺序不同。

如果你的文本编码器把两个词取平均,这两个类会得到完全相同的条件向量。 模型收到同样的输入,却要生成两种不同的输出。

这不可能做到。

重要这就是”绑定问题”(binding problem)

“有一个红和一个蓝”和”左边红、右边蓝”是两句话,但不是同一件事。

  • 词袋只记录”出现了哪些词”,不记录”哪个词对应哪个位置”
  • 注意力可以让每个位置自己去”查”它需要的那个词

本章的全部内容,可以看成这一句话的展开: 为什么均值池化会失败,交叉注意力怎么解决它,以及代价是什么。

这个问题不是玩具。 真实提示词里,“一只猫坐在一只狗旁边”和 “一只狗坐在一只猫旁边”是同一个词袋,但生成结果应该不同。 所有文本到图像的模型都必须解决它。


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 倍。 这就是”更强的文本编码器”的真实成本——不只在编码器本身,还在每一层交叉注意力上。

注记一个反直觉的对比:MMDiT 的注意力并不更贵

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。

重要这 0.35 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
重要那个 0.000 是这一章最重要的一行

不是”差不多”,是逐位相同——差值的最大值恰好是零。

因为均值池化的条件向量在两种顺序下是同一个向量: \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
图 8.1  绑定是学出来的吗。 A:均值池化从第 500 步到 6000 步一直停在 0.5 附近,交叉注意力在第一个检查点就到 1.000。B:对调后的最大逐元素差——只有真正用了顺序的变体才接近 1。数据来自 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 空字符串也会被编码

把 "" 送进一个真实的文本编码器:

  1. tokenizer 会加上特殊 token——通常是 [BOS] 和 [EOS], 有时还有 [PAD]
  2. 这些 token 查表得到向量
  3. 经过若干层自注意力
  4. 输出一个非零的、维度正常的向量

所以”空字符串”不是”没有输入”,是”两个特殊 token 的编码结果”。

这个向量是在训练中见过的——训练时以 10% 的概率把文本换成空串, 模型学会了”这个向量表示没有条件”。

重要和第 7 章的实验对上了

第 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 训练完之后、在潜变量上统计出来的一个常数,不是超参数。

警告用错 scaling factor 会怎样

模型看到的输入尺度不对,等价于换了一个噪声调度。

症状:生成结果整体偏灰或过饱和,而且怎么调 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)

四行,对应注意力公式的四步:

  1. query 从点和时间步来(q_in)
  2. key 从 token 嵌入 + 位置来(k)
  3. 相似度 → softmax(att)
  4. 加权求和(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:右点是蓝
  • 四个象限就是四个类

观察引导。

  1. 选 RB,编码器选均值池化。 样本同时在右下(RB)和左上(BR)两个象限,各占一半。
  2. 切到交叉注意力。样本只落在右下。
  3. 回到均值池化,切到 BR。 图案几乎一模一样——因为这两个类拿到的条件向量完全相同。
  4. 切到 RR 或 BB。两种编码器都是 100%。 词袋唯一确定排列时,绑定不是问题。
重要第 3 步是这一节的全部意义

同一个词袋,两种顺序,模型给出同一个分布。

这不是”生成质量差”,是”条件里没有信息”。 模型被要求从同一个输入产生两种输出,它只能二选一、或者各出一半。

判据:两个”应该不同”的类,在同一个编码器下呈现出镜像对称的两团。 看到这个图案,就是绑定失败。


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.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%。

重要这 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 症状一:改了提示词,图完全没变

三十秒检验:固定种子,把提示词整个换掉(不是改一个词),出两张。

如果还是没变,问题不在”某个词没生效”,在文本根本没进模型。

按可能性排序:

  1. 条件传错了位置——encoder_hidden_states 传成了空
  2. CFG 的 w=0——两个分支被加权成”只用无条件”
  3. 文本缓存没刷新——改了提示词但复用了旧的 embedding
  4. 模型是纯无条件模型——根本没有文本输入

判据: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 症状八:中文提示词效果明显差于英文

三个叠加的原因:

  1. tokenizer 对中文效率低——同样一句话占更多 token
  2. 训练数据以英文为主——中文描述在 caption 分布里很少
  3. 文本编码器的中文语义弱——CLIP 的中文能力来自翻译数据,不是原生

三十秒检验:把同一句中文机翻成英文,对比输出。

如果英文明显更好,三个原因都可能;如果差不多,只是 tokenizer 的问题。

8.19.9 症状九:负面提示词把主体也删了

机制:负面提示词参与的是 \varepsilon_{\text{neg}} 那一路, 它和正面提示词共享同一个 softmax 空间。

如果负面词和主体在语义上接近(“不要模糊的猫”里的”猫”), 它会把主体一起推走。

三十秒检验:把负面提示词逐条删掉,看哪一条导致主体消失。

修法:负面词只写”属性”,不写”主体”。

8.19.10 症状十:同样的参数,两次结果不一样

按可能性排序:

  1. 种子没固定(最常见)
  2. dtype 不同(fp16 vs bf16 的舍入不同)
  3. 调度器的随机性(有些采样器每步加噪声)
  4. 批处理顺序影响了 RNG(一次出 4 张和分 4 次出,噪声序列不同)
提示排错的一般顺序

先排除”没接上”,再排除”接错了”,最后才怀疑”模型不行”。

  1. 条件在不在?(症状一)
  2. 尺度对不对?(症状三)
  3. 有没有被截断/切碎?(症状四)
  4. 是不是分布不匹配?(症状七、八)
  5. 才是机制问题(症状五、六)

大部分”模型不听话”的报告,停在前两步。


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 怎么测量属性泄漏

一个不依赖人工的协议:

  1. 准备 N 个”两物体两属性”的提示词(颜色、材质、大小各一组)
  2. 每个提示词生成 M 张
  3. 用一个检测器(或人工)判断:每个物体的属性是否与提示词一致
  4. 报告属性准确率,而不是”整图像不像”

关键:要分别报告”物体 A 的属性”和”物体 B 的属性”。 只看总体,会把”一个对一个错”平均成 50%,看不出泄漏的方向。

注记和第 7 章的”准确率饱和”是同一类陷阱

第 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 mean

t=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 的图,平均质量更高”。

这解释了两件事:

  1. 为什么魔法词换模型就失效——新模型的 caption 分布里没有这个相关性
  2. 为什么魔法词有时会带来副作用——它同时拉动了训练数据里的其它属性

8.31.3 caption 的长度分布也在被学习

如果训练数据里 90% 的 caption 短于 20 个 token, 模型就没见过”40 个 token 的详细描述”。

后果:长提示词的后半段不是被截断,是被忽略—— 模型没见过”后面还有信息”这种模式。

这和 77 token 的硬截断是两回事:

硬截断 软忽略
原因 位置编码用完 训练分布里没有长 caption
症状 第 78 个 token 之后完全无效 长句的后半段权重很低
修法 换编码器 / 截短 换模型 / 用它的风格写

很多”长提示词没用”的报告,其实是第二种。

8.31.4 一个可以自己做的检验

取一批你的提示词,做两件事:

  1. 把后半句删掉,看输出变化大不大
  2. 把同一句话改写成”关键词堆砌”和”自然语言”两版,对比

如果第 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。 信息有三条路:

  1. 权重 a_k
  2. 值 v_k(含位置嵌入)
  3. 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)是模型理解语法”

不是。 大多数实现里,权重语法在编码器之前就被处理掉了:

  1. 解析 word:1.3
  2. 单独编码 word
  3. 把它的向量乘 1.3
  4. 拼回序列

模型看到的不是”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。

未知:是”权重本身没信息”,还是”权重有信息但被下游掩盖”?

实验设计:

  1. 硬注意力:把 softmax 换成 argmax(或 straight-through),重训
  2. 权重冻结:训练后固定注意力权重,只训后面的 MLP
  3. 权重干预:人为把左点的权重设成 0.9/0.1,看输出是否改变

判据:

  • 如果硬注意力仍然能绑定,说明绑定不依赖权重的精细分布
  • 如果冻结权重后性能不变,说明权重根本不是瓶颈
  • 如果人为干预能改变输出,说明权重是因果链的一环,只是自然训练时不承担这个角色

我的预期(可证伪):硬注意力会掉点,但不会归零; 冻结权重会掉很多——因为权重仍然携带”哪个点是哪个点”的信息, 只是这些信息不是以人类可读的方式编码的。

8.41.2 问题二:三个以上的对象会怎样?

已知:两个对象、两种排列,交叉注意力 1.000,均值池化 0.550/0.449。

未知:对象数从 2 增加到 3、4、5 时,准确率怎么衰减?

实验设计:

  1. 槽位数 k=2,3,4,每个槽 c 种颜色
  2. 类别数 c^k:4,8,16
  3. 每种配置训 3 个种子,每类 300 个样本
  4. 画”对象数 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?

实验设计:

  1. 取 CLIP-L、CLIP-G、T5-XXL 三个编码器
  2. 编码 "",得到三个向量
  3. 算两两余弦相似度
  4. 在同一个 UNet 上(如果可能)扫 w

判据:如果余弦相似度低于 0.5,说明”空条件”在三者之间不可迁移—— 换编码器必须重新定标 w,不能沿用旧值。

8.41.5 问题五:文本编码器该不该微调?

已知:大多数模型冻结编码器,理由是省显存、省配对数据、稳定。

未知:微调带来的收益,值不值这个代价?

实验设计:

  1. 固定 UNet,只微调文本编码器的最后 n 层(n=0,2,4,8)
  2. 用同一批图文对,同样的训练步数
  3. 测三个指标:绑定准确率、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 很高,绑定全错。

警告和第 7 章”准确率会饱和”是同一类问题

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 倾向于给”大致符合”高分
  • 不可复现:模型版本一变,历史分数不可比

校准的办法:

  1. 用一批人工标注的样本,测 VLM 和人的一致率
  2. 报告一致率,不只报告分数
  3. 固定 VLM 版本,写进实验记录

如果一致率低于 80%,这个裁判不能用。

8.44.5 本章的测量为什么不需要这些

因为本章的数据是四个点,判定是”符号对不对”。

这是一个”零歧义”的任务: x 坐标的符号就是颜色,没有检测器、没有 VQA、没有主观判断。

这正是选择玩具模型的价值: 把”测量”这件事从问题里去掉,只剩下”机制”。 真实模型上,测量本身就是一个研究方向。

8.44.6 一个五分钟的消融

不需要标注数据,也不需要训练分类器:

  1. 固定种子,用提示词 A 出 4 张
  2. 固定种子,把 A 里一个词换掉(比如”红”→“蓝”)出 4 张
  3. 比较两组的差异

如果几乎一样,说明那个词没被用起来。

这比看注意力图可靠——它是行为证据,不依赖内部解读。


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 五个坑

  1. 均值池化 → 顺序信息被销毁
  2. 没有位置嵌入 → 输出逐位不变;准确率 0.602 是互补假象
  3. 零向量当空条件 → 输入在分布外
  4. 换 tokenizer 不复验 → 空条件变了
  5. 用注意力图判断 → 尖锐的权重也可能零信息(熵 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 这套方法可以搬到哪

任何”条件怎么进模型”的问题,都可以用同样的三层:

  1. 代数:这个操作对什么不变?(均值对顺序不变、注意力对位置等变)
  2. 统计:在受控数据上测准确率
  3. 行为:改条件、看输出

再加一条贯穿始终的原则:每次看到一个”变好了”或”变差了”的数, 先问”有没有另一种解释”。

本章出现过两次”另一种解释”: 均值池化的损失差可能是容量(用容量对照排除), 无位置嵌入的 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 加上它的位置,再求和。”

任务:

  1. 先不跑代码,写出 RB 和 BR 的条件向量,看它们是否相同。
  2. 再改成”不池化,直接返回两个位置的向量”,重跑。

提示:求和是交换的;位置嵌入是按下标加的,不是按词加的。

注记这道题的答案

设 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 练习三:加第三个槽

本章是”左、右”两个槽。改成三个槽(左、中、右),四个颜色。

任务:

  1. 改 make_two_point_data,从 4 维改成 6 维,类别从 4 个变成 8 个
  2. 两种编码器各训一次
  3. 报告每个类的槽位准确率

预测:

  • 均值池化在”三种颜色各不同”的类上,准确率应该是多少?
  • 交叉注意力会掉点吗?为什么?

提示:三个槽、每种颜色可重复,排列数是 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 五分钟版

三句话:

  1. 文本编码器把一句话变成一串向量,不是一个向量——77 个 token 就是 77 份条件。
  2. 交叉注意力让每个图像位置各自去问一遍;均值池化让所有位置共用同一个答案, 所以 RB 和 BR 在它眼里是同一个条件(0.550 / 0.449)。
  3. 注意力权重不能解释输出:权重几乎是均匀的(相关 −0.079), 但对调两个词,交叉注意力的输出翻转 0.972,均值池化的输出差恰好 0.000。

如果对方只问一个问题,多半是”那提示词怎么写才管用”。 答:写模型训练数据里常见的句式,把最重要的词放前面, 别指望用文字精确控制位置——那需要另一条通道。

8.49.2 三十分钟版

在五分钟版之上加两块:

① 手算的那个例子。 两个 token、两个点,左点的注意力从 0.893 翻到 0.781。 这一组数能让人看见”绑定的全部机制就是下标和内容被分开”。

② 空条件的那个坑。 零向量当空条件,w=3 就崩。 它说明”文本条件”不是一个可以随手拼出来的张量,是一个必须被训练过的分布。

8.49.3 两小时版

完整讲,但顺序调整过:

  1. 先讲失败(均值池化在 RB/BR 上 50/50)
  2. 再讲机制(手算交叉注意力)
  3. 然后实测(1.000 vs 0.550,以及对调实验的 0.000 / 0.972)
  4. 最后边界(注意力图不可信、空间控制要另开通道)

先失败后机制,比”先公式后例子”更容易让人记住—— 因为”同一个词袋、两种顺序”本身就是一个让人想不通的问题。

提示讲的时候最容易被追问的三个问题

① “那我把提示词写详细一点,不就能控制顺序了吗?” 不能。详细只是增加 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,说明顺序根本没进去。