卷 III · 肚子里CH 10深度 10/20

一条大家都往上写的总线

「12 层」这个说法很容易让人想到一条流水线:第 1 层处理完交给第 2 层,第 2 层再交给第 3 层……少了哪一道工序,产品就出不来。GPT-2 不是这样的。它更像一条总线:每个位置有一个 768 维的向量从头流到尾,每一层都只是读一读它,然后往上加一笔。这一章用三个实验把这个结构看清:整层删掉会怎样、在中间截下来读会读到什么、最终的答案是谁写上去的。

删掉一层只多 0.6%logit lens第 9 层写了 59%
◉ 这一章在 GPT-2 的哪一段

▷ 先猜一下

GPT-2 small 有 12 层(这一章层号从 0 数:第 0 层到第 11 层)。在第 4 章那篇文章上,正常情况下每个 token 的意外是 4.53 比特。

现在把第 6 层整个删掉——它的注意力和 MLP 都不再往上写任何东西,其余 11 层照常。问:每个 token 的意外会变成多少?

A 翻倍以上。12 道工序少了一道,整条线就废了 B 涨 50% 左右。伤筋动骨 C 涨 2% 左右。几乎没感觉 D 一点不变。第 6 层什么都没干

答案在本章末尾。

总线:每一层只加一笔

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 删第 1 层 4.56 删第 2 层 4.65 删第 3 层 4.67 删第 4 层 4.74 删第 5 层 4.70 删第 6 层 4.63 删第 7 层 4.76 删第 8 层 4.71 删第 9 层 4.71 删第 10 层 4.89 删第 11 层 6.16
删掉的层每个 token 的意外比正常多
(不删)4.53—
012.94+185%
14.56+0.6%
24.65+2.6%
64.63+2.2%
104.89+7.9%
116.16+35.9%

中间的任何一层(第 1 到第 9 层)删掉,代价都在 0.6% 到 5% 之间;删掉第 1 层只多了 0.6%。一条真正的流水线不可能这样——拆掉中间一道工序,下游拿到的就是半成品。只有总线式的结构,才能让「少一层」变成「少一点修正」。

两个例外也说明问题:第 0 层删掉是灾难(+185%),因为它的 MLP 实际上在给词嵌入「补课」——后面所有层都默认读到的是被它加工过的 token 向量;最后一层删掉也很伤(+35.9%),因为它是最后一个往总线上写的,离出口最近,没有谁能替它补。

实验二:在半路截下来读

既然每一层之后,总线上都是一个「到目前为止的草稿」,那能不能把半路上的草稿直接送去最后那一步(最后的 LayerNorm + 乘词嵌入表),看它此刻会写什么?这个技巧叫 logit lens(直译「logit 透镜」)。拿 Mary 那句话做:

在每一层之后截下来读:Mary(绿)和 John(红)的名次(纵轴对数,越低越靠前) 1 10 100 1000 10000 嵌入 0 1 2 3 4 5 6 7 8 9 10 11 第几层之后 名次
在哪里截下来读此刻最可能写的Mary 的名次John 的名次总线的长度
刚查完嵌入' destro'43494346864.5
第 0 层之后' get'5778368051.2
第 4 层之后' drink'2624328066.0
第 7 层之后' the'31664390.1
第 8 层之后' them'2469118.3
第 9 层之后' them'211166.6
第 10 层之后' Mary'13254.8
第 11 层之后(最终)' Mary'14477.6

前八层,Mary 一直埋在几百上千名之后,总线上的「草稿」在想的是动词、介词——␣get、␣drink、␣the。Mary 是在第 9 层之后才突然冒头的:从第 24 名跳到第 2 名。第 9 层之后那一刻的候选表是这样的:

在第 9 层之后截下来读(logit lens):同一句话 … ␣store , ␣John ␣gave ␣a ␣drink ␣to 下一个 token 概率 意外 ' them' 75.77% 0.40 比特 ' Mary' 23.07% 2.12 比特 ' the' 0.28% 8.46 比特 ' him' 0.20% 8.97 比特 ' everyone' 0.19% 9.02 比特 ' his' 0.15% 9.40 比特 熵 0.91 比特 · 最终答案是 Mary 44.60%

␣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%)删不得。

A 「整条线就废了」——这是流水线的直觉。在总线上,删掉的只是一笔修正;其余 11 层写的东西都还在,它们会大致把局面撑住。 B 「伤筋动骨」——对第 11 层来说接近事实(+35.9%)。离出口越近的层,越没有后来者能替它补漏。 D 「什么都没干」——它干了,只是它干的事有冗余:别的层在做相近的事,而且总线上的大部分内容不是它写的。2.2% 的意外,在 7000 多个 token 上累积起来,就是一个确实变笨了的模型。

这一章的一句话

Transformer 是一条总线,不是一条流水线:每一层只读总线、加一笔修正。所以删掉中间一层只是少一笔,答案可以在半路被读出来,最终判断也可以精确地拆回到每个部件头上。

注意力负责「搬」,那「知道」的部分在哪?下一章去找 GPT-2 记住「Michael Jordan 打篮球」的地方。MLP 占了每一层三分之二的参数;可是你会发现,在 ␣Jordan 这个位置上删掉任何一层 MLP,basketball 的概率最多也只是从 13.53% 降到 5.72%——没有哪一处是「那一条记录」。