一条大家都往上写的总线
「12 层」这个说法很容易让人想到一条流水线:第 1 层处理完交给第 2 层,第 2 层再交给第 3 层……少了哪一道工序,产品就出不来。GPT-2 不是这样的。它更像一条总线:每个位置有一个 768 维的向量从头流到尾,每一层都只是读一读它,然后往上加一笔。这一章用三个实验把这个结构看清:整层删掉会怎样、在中间截下来读会读到什么、最终的答案是谁写上去的。
GPT-2 small 有 12 层(这一章层号从 0 数:第 0 层到第 11 层)。在第 4 章那篇文章上,正常情况下每个 token 的意外是 4.53 比特。
现在把第 6 层整个删掉——它的注意力和 MLP 都不再往上写任何东西,其余 11 层照常。问:每个 token 的意外会变成多少?
答案在本章末尾。
总线:每一层只加一笔
GPT-2 的一层,写成代码就两行:
x = x + attention(layernorm(x)) # 注意力:从别的位置取信息,加到自己身上 x = x + mlp(layernorm(x)) # MLP:在自己身上做一次加工,结果也加回来
关键是那两个 x = x + …。每个部件不替换 x,只往 x 上加东西。这条从嵌入一路加到最后的向量,叫残差流(residual stream)——「残差」这个名字来自 2015 年的 ResNet,当时的初衷只是让很深的网络能训得动;但它带来的结构后果比初衷重要得多:
- 每个部件读的是「到目前为止所有部件写下的东西之和」,写的是「一笔新的修正」。
- 最后的那张候选表,只看总线最终的样子。所以最终答案 = 嵌入 + 所有 24 个部件(12 个注意力 + 12 个 MLP)各自写下的那一笔,是一个加法。
这和 Android 里的事件总线、或者一块所有人都能往上写的白板很像:没有谁是谁的上游,每个人看着白板、补一句、离开。少了一个人,白板上少一句话,但会议照样开。
实验一:整层删掉
如果这是总线,删掉一层就只是少写了一笔,不该天塌下来。删掉每一层(注意力和 MLP 都不写),量同一篇文章的意外:
| 删掉的层 | 每个 token 的意外 | 比正常多 |
|---|---|---|
| (不删) | 4.53 | — |
| 0 | 12.94 | +185% |
| 1 | 4.56 | +0.6% |
| 2 | 4.65 | +2.6% |
| 6 | 4.63 | +2.2% |
| 10 | 4.89 | +7.9% |
| 11 | 6.16 | +35.9% |
中间的任何一层(第 1 到第 9 层)删掉,代价都在 0.6% 到 5% 之间;删掉第 1 层只多了 0.6%。一条真正的流水线不可能这样——拆掉中间一道工序,下游拿到的就是半成品。只有总线式的结构,才能让「少一层」变成「少一点修正」。
两个例外也说明问题:第 0 层删掉是灾难(+185%),因为它的 MLP 实际上在给词嵌入「补课」——后面所有层都默认读到的是被它加工过的 token 向量;最后一层删掉也很伤(+35.9%),因为它是最后一个往总线上写的,离出口最近,没有谁能替它补。
实验二:在半路截下来读
既然每一层之后,总线上都是一个「到目前为止的草稿」,那能不能把半路上的草稿直接送去最后那一步(最后的 LayerNorm + 乘词嵌入表),看它此刻会写什么?这个技巧叫 logit lens(直译「logit 透镜」)。拿 Mary 那句话做:
| 在哪里截下来读 | 此刻最可能写的 | Mary 的名次 | John 的名次 | 总线的长度 |
|---|---|---|---|---|
| 刚查完嵌入 | ' destro' | 43494 | 34686 | 4.5 |
| 第 0 层之后 | ' get' | 5778 | 3680 | 51.2 |
| 第 4 层之后 | ' drink' | 2624 | 3280 | 66.0 |
| 第 7 层之后 | ' the' | 316 | 643 | 90.1 |
| 第 8 层之后 | ' them' | 24 | 69 | 118.3 |
| 第 9 层之后 | ' them' | 2 | 11 | 166.6 |
| 第 10 层之后 | ' Mary' | 1 | 3 | 254.8 |
| 第 11 层之后(最终) | ' Mary' | 1 | 4 | 477.6 |
前八层,Mary 一直埋在几百上千名之后,总线上的「草稿」在想的是动词、介词——␣get、␣drink、␣the。Mary 是在第 9 层之后才突然冒头的:从第 24 名跳到第 2 名。第 9 层之后那一刻的候选表是这样的:
␣them 75.77%,Mary 23.07%——它已经知道「这里该是一个接收者」,还在「他们」和「Mary」之间摇摆。再过一层,Mary 登顶。最后一列也值得一看:总线的长度(向量的模)从 4.5 一路涨到 477.6,每一层都在往上加东西,越往后加得越多。
不过要提醒一句:logit lens 是一个近似。中间层的草稿并不是为了「直接被读出来」而写的,前几层读出来的东西(比如那个奇怪的 ' destro')多半没有意义。它在后半段最有用:看一个答案大约在第几层成形。
实验三:答案是谁写上去的
既然最终答案是所有部件写下的东西加起来,那就可以拆账:把最终「Mary 的分数减 John 的分数」拆回到每一个部件头上,看谁写了多少。(技术上要把最后那个 LayerNorm 的缩放当作固定值,这样整件事就是线性的,可以精确地拆。这叫直接 logit 归因。)
| 部件 | 对「Mary 胜过 John」的贡献 |
|---|---|
| 第 9 层注意力 | +1.95 |
| 第 11 层注意力 | +0.40 |
| 第 7 层注意力 | +0.37 |
| 第 8 层注意力 | +0.34 |
| 第 10 层 MLP | +0.29 |
| 第 10 层注意力 | −0.24 |
| ……(其余 18 个部件和嵌入,各自不到 ±0.21) | |
| 所有部件之和 | +3.30 |
所有部件加起来是 +3.30(再加上最后那个 LayerNorm 自带的偏置 −1.30,正好等于最终的 logit 差 2.01,也就是 Mary 的概率是 John 的 e2.01 ≈ 7.4 倍)。第 9 层的注意力一家就写了 +1.95,占 59%——上一章那个 9.6 号「搬名字的头」就在这一层。第 10 层的注意力整体是负的:里面那个看 Mary 看得最多的 10.7 号头,在往回拉。
拆到单个头上,正负都很显眼:
9.6 +1.16 ← 搬名字的头:推 Mary 10.7 ⟦−1.03⟧ ← 看 Mary 最多(58.2%),却在压 Mary 10.0 +0.72 10.2 ⟦−0.68⟧ 10.10 +0.58 11.10 ⟦−0.57⟧ ← 另一个「反向」的头
这就是总线结构给可解释性研究的最大礼物:因为一切都是加法,所以一切都能拆账。一个 1.24 亿参数的黑箱,在这句话上,你可以指着第 9 层第 6 个头说「主要是它干的」。
transformers 可以把每一层之后的总线状态(hidden_states)都交出来;自己补上最后的 LayerNorm 和反嵌入,就是 logit lens:
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
tok = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2").eval()
x = tok("When Mary and John went to the store, John gave a drink to", return_tensors="pt").input_ids
with torch.no_grad():
hs = model(x, output_hidden_states=True).hidden_states # 13 份:嵌入之后 + 每一层之后
mary, john = tok.encode(" Mary")[0], tok.encode(" John")[0]
for L, h in enumerate(hs):
h = h[0, -1] if L == 12 else model.transformer.ln_f(h[0, -1]) # 最后一份已经过了 ln_f
p = torch.softmax(model.lm_head(h), -1) # 「logit lens」:提前读出
rank = lambda t: (p > p[t]).sum().item() + 1
where = "嵌入之后 " if L == 0 else f"第 {L - 1:2d} 层之后"
print(f"{where} 最可能 {tok.decode(p.argmax())!r:12} Mary 第 {rank(mary):5d} 名 John 第 {rank(john):5d} 名")
本机输出:
嵌入之后 最可能 ' destro' Mary 第 43494 名 John 第 34686 名 第 0 层之后 最可能 ' get' Mary 第 5778 名 John 第 3680 名 第 1 层之后 最可能 ' get' Mary 第 4585 名 John 第 3624 名 第 2 层之后 最可能 ' be' Mary 第 2220 名 John 第 1945 名 第 3 层之后 最可能 ' fill' Mary 第 1896 名 John 第 2415 名 第 4 层之后 最可能 ' drink' Mary 第 2624 名 John 第 3280 名 第 5 层之后 最可能 ' drink' Mary 第 1741 名 John 第 1653 名 第 6 层之后 最可能 ' accompany' Mary 第 1529 名 John 第 1272 名 第 7 层之后 最可能 ' the' Mary 第 316 名 John 第 643 名 第 8 层之后 最可能 ' them' Mary 第 24 名 John 第 69 名 第 9 层之后 最可能 ' them' Mary 第 2 名 John 第 11 名 第 10 层之后 最可能 ' Mary' Mary 第 1 名 John 第 3 名 第 11 层之后 最可能 ' Mary' Mary 第 1 名 John 第 4 名
删层实验用 transformers 也能做(给某一层的模块挂一个返回零的钩子);本书用的是自己写的 numpy 版,在仓库 scripts/probe-next/gpt2np.py 的 forward(zero_attn=…, zero_mlp=…)。
pip install torch transformers && python 10.py
想系统地做这类实验:TransformerLens 是专为可解释性研究写的库,GPT-2 是它的头号支持对象,拆账、删层、换头都是一行。
- 「残差连接」几乎出现在所有现代深度网络里,不只是语言模型。它让上百层的网络能被训练,因为梯度可以顺着那条加法的直路一路传回去,不会在层层相乘里消失。
- 大模型的「层剪枝」——直接去掉一些中间层来提速——之所以可行,靠的就是这一章的第一个实验:中间层的作用是修修补补,拿掉几层,模型变笨一点,但不会崩。
- 「提前退出」(简单的 token 在中间层就输出,难的才走完全程)利用的是第二个实验:很多 token 的答案在半路就已经成形了。
「12 层就是 12 道工序:第 1 层做分词级的事,第 2 层做语法……每一层把结果交给下一层,缺一不可。」
更准确的图像是一条总线:每一层读总线上到目前为止的所有东西,加一笔修正,不替换任何东西。所以删掉中间一层只多 0.6% 到 5% 的意外;所以可以在半路把草稿读出来;所以最终答案可以精确地拆回到每个部件头上。
「浅层管词、深层管意思」作为大趋势是对的(Mary 在第 9 层之后才冒头),但它们之间不是交接关系,而是大家在同一块白板上接力补充。
正确答案是 C:删掉第 6 层,每个 token 的意外从 4.53 变成 4.63,多 2.2%。
删掉第 1 层更少,只多 0.6%;只有第 0 层(+185%)和最后一层(+35.9%)删不得。
《拨动》讲的「干预」——把一个变量强行设成某个值,看下游怎么变——就是这一章删层、换头实验的方法论;可解释性研究里管这个叫「因果干预」,用的就是那本书里的 do 算子的思路。《张成》第 20 章讲神经网络的前向就是矩阵乘加非线性,这一章补上的是那条贯穿始终的加法。
这一章的一句话
Transformer 是一条总线,不是一条流水线:每一层只读总线、加一笔修正。所以删掉中间一层只是少一笔,答案可以在半路被读出来,最终判断也可以精确地拆回到每个部件头上。
注意力负责「搬」,那「知道」的部分在哪?下一章去找 GPT-2 记住「Michael Jordan 打篮球」的地方。MLP 占了每一层三分之二的参数;可是你会发现,在 ␣Jordan 这个位置上删掉任何一层 MLP,basketball 的概率最多也只是从 13.53% 降到 5.72%——没有哪一处是「那一条记录」。