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

比维度还多的概念

前几章拆出了一些干净的零件:只看上一个 token 的头、照抄的归纳头。可如果你随手挑一个 GPT-2 的神经元,看看它在什么时候亮,多半会失望——它在一些毫不相干的地方都亮。可解释性研究在这里卡了好几年。这一章用一个只有 2 维的玩具模型解释原因:模型要记的概念比它的维度多得多,于是它学会了把好几个概念叠在同一组维度里。

5 个特征挤进 2 维五角星稀疏才挤得下
◉ 这一章在 GPT-2 的哪一段

▷ 先猜一下

一个玩具模型:有 5 个「特征」(可以想成 5 个概念),要先压进一个 2 维的向量,再从这 2 维里尽量还原出原来的 5 个。5 个特征重要程度不同,第 0 个最重要。

每个特征各自独立,只有 10% 的时候出现(出现时取 0 到 1 之间的随机值,其余时候是 0)。训练完之后,问:这 2 维里一共放下了几个特征?

A 2 个。两个维度,一个维度放一个,最重要的两个 B 3 个。挤一挤,多塞一个 C 4 个。两根轴,每根轴的正负两头各放一个 D 5 个。全放进去

答案在本章末尾。

一个神经元,一个意思?

可解释性研究最初的梦想是这样的:GPT-2 每层有 3072 个神经元,如果每个神经元负责一个概念——这个管「篮球」,那个管「法语」,另一个管「Python 的缩进」——那拆开模型,就是逐个给神经元贴标签。

现实是:有少数神经元确实很「专一」,但大多数都是多义的(polysemantic)。同一个神经元,可能在学术引用、韩文、扑克牌术语上都会亮,彼此毫无关系。为什么训练会产出这么「乱」的东西?

第 5 章埋了一个伏笔:在 768 维的空间里,随便两个方向有 99.4% 的机会几乎互相垂直。也就是说,这个空间能装下的「几乎互不干扰的方向」,远远多于 768 个。而模型要记的东西——人名、地名、语法规则、代码结构、事实——显然也远远多于 768 个。如果允许一点点互相干扰,它能不能把更多的概念塞进去?

一个玩具:5 个特征,2 个维度

2022 年 Anthropic 的论文《Toy Models of Superposition》用一个小到可以画在纸上的模型回答了这个问题。设定如下:

  • 5 个特征 x₀ 到 x₄,重要程度依次是 1、0.9、0.81、0.73、0.66(还原错了,越重要的扣分越多)。
  • 每个特征有概率 S 不出现(值为 0),否则取 0 到 1 之间的随机数。S 叫稀疏度。
  • 模型先用一个 2 × 5 的矩阵 W 把 5 维压成 2 维:h = W x;再还原:x' = ReLU(Wᵀ h + b)。W 的每一列,就是一个特征在 2 维平面上的「方向」。

训练 10000 步,然后把 W 的 5 列画成平面上的 5 支箭头——箭头越长,说明这个特征被存得越好;短到接近 0,就是被放弃了。四种稀疏度:

W 的 5 列画成 2 维平面上的 5 支箭头(箭头短到看不见 = 那个特征被放弃) 0 1 S = 0 · 放下 2 个 0 1 3 4 S = 0.7 · 放下 4 个 0 1 2 3 4 S = 0.9 · 放下 5 个
稀疏度 S(不出现的概率)放下了几个特征哪几个怎么摆的
0(每个特征都一直在)20、1互相垂直(相邻夹角 90.1°),其余三个放弃
0.740、1、3、4两根轴,每根轴正反两头各放一个(夹角都在 90° 上下)
0.95全部五角星,相邻夹角 69.7° 到 74.4°
0.995全部五角星,相邻夹角 70.6° 到 74.6°

当特征总是一起出现时(S = 0),模型的选择和你预期的一样:两个维度,放最重要的两个,互相垂直、互不干扰,剩下三个干脆不要(它们的还原值固定成一个平均数,靠偏置给出)。

可是当特征很少同时出现时(S = 0.9),它把 5 个全塞了进去,排成一个几乎正五边形的五角星——理想的正五边形相邻夹角是 72°,它学到的是 69.7° 到 74.4°。5 个概念共用 2 个维度。这就是叠加(superposition)。

为什么稀疏就挤得下

5 支箭头挤在一个平面里,它们不可能互相垂直:相邻两个之间的余弦大约是 0.3,隔一个的大约是 −0.8。于是当特征 0 出现时,还原特征 2 的那一路会读到一个不小的负数——这叫串扰(interference)。

模型的应对办法很巧妙,看它学到的偏置 b:S = 0.9 时,5 个偏置全是负的(−0.21 到 −0.23)。还原时先加上这个负偏置,再过 ReLU(负数一律变成 0)——小的串扰和负的串扰,统统被削成了 0。只有真正出现的那个特征,读数足够大,能越过这道门槛。

这个办法有一个前提:同一时刻只能有一两个特征出现。如果好几个特征同时出现,它们的串扰会叠在一起,越过门槛,还原就乱了。所以特征越稀疏,模型越敢挤;特征一直都在的时候,它宁愿放弃也不挤。

真实世界的概念恰好极其稀疏:一篇文章里,绝大多数 token 和「篮球」无关,和「法语」无关,和「Python 缩进」无关。对一台语言模型来说,叠加不是例外,而是默认状态。一个 token 的向量里,同时叠着它可能的好几种意思,靠上下文把用得上的那一种放大:

The bank of the ▍——bank 同时带着「河岸」和「银行」 The ␣bank ␣of ␣the 下一个 token 概率 意外 ' river' 3.30% 4.92 比特 ' Nile' 1.22% 6.35 比特 ' United' 1.13% 6.47 比特 ' world' 0.87% 6.85 比特 ' city' 0.79% 6.99 比特 ' River' 0.57% 7.44 比特 熵 11.79 比特

The bank of the 后面,␣river(河岸)排第一,可是 ␣United(Bank of the United States)、␣world 也在榜上。␣bank 这一个 token 同时带着「河岸」和「银行」两个方向,这里的上下文还不足以把它们分开。

这对「读懂模型」意味着什么

如果概念是叠在一起存的,那么:

  • 一个神经元会对好几个不相干的概念有反应——因为每个概念的方向都在好几个神经元上有分量,每个神经元也都参与了好几个概念的方向。
  • 该找的是方向,不是神经元。近两年的主流工具叫稀疏自编码器(SAE):再训练一个「把模型的内部向量展开成很多很多个、每次只亮几个的特征」的小网络,相当于把叠在一起的概念重新拆开。

2024 年 Anthropic 在《Scaling Monosemanticity》里用这种方法,从自家的大模型 Claude 3 Sonnet 里拆出了几百万个特征,其中有一个专门对应「金门大桥」;把这个特征人为调大,模型就开始在每个回答里扯到金门大桥——他们还把这个版本放出来让人玩了一阵,叫 Golden Gate Claude。一个概念是一个方向;沿着这个方向推一把,模型的「想法」就跟着偏。

⌨ 自己跑一遍

玩具模型只要 numpy,二十几行,几秒钟跑完:

import numpy as np

def train(S, n=5, m=2, steps=10000, seed=0, lr=1e-2):
    """5 个特征挤进 2 维:x' = ReLU(Wᵀ W x + b)。S = 每个特征「这一次不出现」的概率"""
    rng = np.random.default_rng(seed)
    I = 0.9 ** np.arange(n)                              # 重要性:第 0 个最重要
    W = rng.normal(0, 1, (m, n)) * np.sqrt(2 / (m + n)); b = np.zeros(n)
    st = [np.zeros_like(W), np.zeros_like(W), np.zeros_like(b), np.zeros_like(b)]
    for t in range(1, steps + 1):
        x = rng.random((1024, n)) * (rng.random((1024, n)) > S)
        h = x @ W.T; pre = h @ W + b
        d = 2 * (np.maximum(pre, 0) - x) * I * (pre > 0) / 1024
        gW, gb = h.T @ d + (d @ W.T).T @ x, d.sum(0)
        for P, g, mm, vv in ((W, gW, st[0], st[1]), (b, gb, st[2], st[3])):   # Adam
            mm *= 0.9; mm += 0.1 * g; vv *= 0.999; vv += 0.001 * g * g
            P -= lr * (mm / (1 - 0.9 ** t)) / (np.sqrt(vv / (1 - 0.999 ** t)) + 1e-8)
    return W

for S in [0.0, 0.7, 0.9]:
    W = train(S)
    norm = np.linalg.norm(W, axis=0)
    kept = np.where(norm > 0.5)[0]
    ang = np.sort(np.degrees(np.arctan2(W[1, kept], W[0, kept])) % 360)
    gaps = np.diff(np.r_[ang, ang[0] + 360]).round(0)
    print(f"S={S}  2 维里放下了 {len(kept)} 个特征 {kept.tolist()}  相邻夹角 {gaps.tolist()}")

本机输出:

S=0.0  2 维里放下了 2 个特征 [0, 1]  相邻夹角 [90.0, 270.0]
S=0.7  2 维里放下了 4 个特征 [0, 1, 3, 4]  相邻夹角 [90.0, 90.0, 91.0, 90.0]
S=0.9  2 维里放下了 5 个特征 [0, 1, 2, 3, 4]  相邻夹角 [74.0, 71.0, 70.0, 71.0, 73.0]

改 n 和 m 试试:比如 20 个特征挤 5 维。还可以把重要程度改成全一样,看看五角星会不会更正。

pip install numpy && python 13.py

原论文有大量交互图:transformer-circuits.pub/2022/toy_model。

▸ 在现实里
  • 压缩感知(compressed sensing)是同一个数学:一个信号如果是稀疏的,就能用远少于它维度的测量把它还原出来。核磁共振扫描能比过去快好几倍,靠的就是这个——你在医院里躺的时间,部分是被「稀疏就能挤」省下来的。
  • 布隆过滤器是工程师熟悉的版本:用很少的比特位记很多元素,允许一点点冲突,换来空间上的巨大节省。冲突率随塞进去的东西增多而上升——和串扰一样。
  • 「让模型更诚实 / 更安全」的一类研究,正在用这一章的思路:找到对应某种行为的方向,然后在推理时把它调大或调小。这比重新训练便宜得多,也比改提示词更「深」。
✗ 这个直觉是错的

「每个神经元负责一个概念;模型能记住的概念数,不会超过它的神经元数(或维度数)。」

2 维的玩具模型记住了 5 个特征;条件是这些特征很少同时出现。在这种条件下,概念被存成方向,而方向可以比维度多得多,代价是一点点串扰,再用负偏置加 ReLU 把串扰滤掉。

所以「找到代表篮球的那个神经元」往往是一个没有答案的问题:篮球是一个方向,散落在很多神经元上;而每个神经元也同时参与了很多别的方向。要读懂模型,得先把叠在一起的东西拆开。

◇ 揭晓

正确答案是 D:5 个特征全部放进 2 维,排成五角星,相邻夹角 69.7° 到 74.4°(正五边形是 72°),5 个偏置都是负的。

A 「一个维度放一个」——这是特征一直都在(S = 0)时的答案。直觉默认了「每个概念要独占一个维度」,而稀疏性打破了这个前提。 B 「挤一挤,多塞一个」——叠加的几何偏爱对称的排法(正多边形、正反成对),很少出现「多塞一个」的不规则形状。 C 「正负两头各放一个」——这正是 S = 0.7 时的答案:两根轴、四个特征,同一根轴上的两个方向相反,ReLU 让它们互不干扰。稀疏度再高一点,它就敢放下第五个了。

这一章的一句话

当概念很少同时出现时,模型会把比维度多得多的概念叠在同一组维度里,每个概念是一个方向,靠负偏置和 ReLU 滤掉串扰。所以神经元大多是多义的;要读懂模型,找的应该是方向,而不是神经元。

卷 III 拆完了。下一卷回到出口:那张表出来之后,怎么变成一段话?最直接的办法是每一步都挑第一名。可是让 GPT-2 用这个办法续写「The meaning of life is」,200 个 token 里,不重复的 4 词片段只剩 8%——它写了一句「生命的意义不同于死亡的意义」,然后把这句话原样重复到了最后。