本期是 Day 27 可解释性 的续篇:SAE 问「这里编码了什么」,本期的 J-lens 问「这里准备说什么」。与 Day 55 CoT 监控 互为一组——一个读写出来的推理,一个读没写出来的推理。
中间层的激活像一张旧版本 schema 的表,末层是新 schema。Logit lens 假设两边列名一致、直接 SELECT;J-lens 先算出一份平均意义上的迁移脚本,把旧表搬到新 schema,再用模型自己的「字典」(unembedding)去查。
Transformer 每层都往同一条残差流(residual stream,层与层之间传递的那个向量,可理解为共享的状态总线)上读写。想知道第 l 层「在想什么」,最朴素的办法是 logit lens(nostalgebraist 2020):把中间激活直接乘上末层的词表矩阵,看哪个词得分最高。问题是早中层的坐标系和末层并不对齐,读出来常是一堆逗号空格。Tuned lens(Belrose et al. 2023)给每层训练一个仿射翻译器去拟合最终输出,但它学的是「最像下一个词」,可能借翻译器之力读出模型本不用的东西。
Anthropic 2026-07 的论文《Verbalizable Representations Form a Global Workspace in Language Models》换了个问题:不问「这里像什么词」,而问「推一下这里,模型之后说出哪些词的概率会变」。
lens_l(h) = unembed( J_l · h ), J_l = E[ ∂h_final / ∂h_l ]
直觉:真实网络是非线性的,但取平均后的雅可比是它的一阶线性近似,相当于一张「各层到输出的平均影响力地图」。它从模型自身的导数推出来,而不是拟合一个解码目标,所以不太会凭空「替模型说话」。官方配套代码说明:拟合用约 1000 条 128-token 的通用网页文本,约 100 条已可用。
import torch from transformers import GPT2LMHeadModel, GPT2Tokenizer tok = GPT2Tokenizer.from_pretrained("gpt2"); m = GPT2LMHeadModel.from_pretrained("gpt2").eval() L = 6 # 观察第 6 层 def tail(h): # 从第 L 层跑到末层(含 final LayerNorm) for blk in m.transformer.h[L:]: out = blk(h); h = out[0] if isinstance(out, tuple) else out return m.transformer.ln_f(h) def hidden(text): ids = tok(text, return_tensors="pt").input_ids return m(ids, output_hidden_states=True).hidden_states[L].detach() J, corpus = torch.zeros(768, 768), ["The weather today is", "In 1905 Einstein", "My favorite food is"] for text in corpus: # 玩具版:只看末位置→末位置,真实版还会累加未来位置 hs = hidden(text) def f(v): h = hs.clone(); h[0, -1] = v; return tail(h)[0, -1] J += torch.autograd.functional.jacobian(f, hs[0, -1], vectorize=True) / len(corpus) h = hidden("The capital of France is")[0, -1] with torch.no_grad(): for name, v in [("logit", m.transformer.ln_f(h)), ("J", J @ h)]: print(name, [tok.decode(i) for i in m.lm_head(v).topk(5).indices])
残差流像一台服务器的全部内存,J-space 则是挂在共享消息总线上的那几个 topic:大部分数据是各模块的本地状态,只有很少一部分被发布到总线上,任何下游(包括「把它说出来」的模块)都能订阅。
有了 J-lens,就能为词表里每个词找到「推动模型将来说出它」的那个方向。把这些方向张成的空间叫 J-space。论文的几何发现有三条:
关键结论是:方差小 ≠ 不重要。一个概念的表示里,正是那一小块 J-space 分量决定了它能不能被报告出来。这挑战了一个常见的默认假设——「激活里方差大的方向才是主要信息」(PCA 式直觉)。在这里,决定行为的恰恰是不起眼的低方差子空间。
import numpy as np rng = np.random.default_rng(0) d, k, n = 512, 25, 2000 # 维度 / 可言说子空间秩 / 样本数 Q = np.linalg.qr(rng.normal(size=(d, d)))[0] # 随机正交基 J_dirs, other = Q[:, :k], Q[:, k:] # 前 k 维当作 J-space # 模拟激活:J 分量幅度小,其余方向幅度大(如字面、位置噪声) acts = rng.normal(size=(n, k)) * 1.0 @ J_dirs.T + rng.normal(size=(n, d - k)) * 1.3 @ other.T var_J = ((acts @ J_dirs) ** 2).sum() / (acts ** 2).sum() print(f"J-space 方差占比: {var_J:.1%}") # 约 3%:PCA 会先把它扔掉 # 若「说出口」只读 J 分量,按方差降维反而会删掉最关键的信息 top_pc = np.linalg.svd(acts - acts.mean(0), full_matrices=False)[2][:50] print("前 50 主成分里保留的 J 能量:", round(float(((top_pc @ J_dirs) ** 2).sum() / k), 3))
审计日志和业务主表的区别:很多系统的「报告」是另写的一份日志,可能和真实业务脱节;论文主张 J-space 更像报告直接读主表——模型汇报「我在想什么」时,读的就是推理正在用的那几行。
Day 55 讲过:思维链(CoT)可能不忠实,写出来的理由不一定是真理由。那么不写出来的推理呢?论文用「改一处、看行为变不变」的因果干预论证三件事:
合起来,命题是:可报告的表示与内部推理使用的表示是同一批。另一面同样重要——选择性:流畅续写这类自动化任务不太依赖 J-space,灵活推理和「汇报体验」才依赖。这正是「全局工作空间」该有的样子:不是所有计算都走总线,只有需要被多个模块协同调用的才走。
import numpy as np rng = np.random.default_rng(1) d, k = 64, 8 Q = np.linalg.qr(rng.normal(size=(d, d)))[0]; J, R = Q[:, :k], Q[:, k:] w = J @ rng.normal(size=k) # 下游「回答」只读 J 子空间 def make(c): # c=概念向量;非 J 部分方差更大 return J @ c + R @ rng.normal(size=d - k) * 3 def swap(a, b, P): # 把 a 在子空间 P 上的分量换成 b 的 return a - P @ (P.T @ a) + P @ (P.T @ b) flips_J = flips_R = 0 for _ in range(1000): a, b = make(rng.normal(size=k)), make(rng.normal(size=k)) if np.sign(w @ a) == np.sign(w @ b): continue # 只看答案本就不同的对 flips_J += np.sign(w @ swap(a, b, J)) == np.sign(w @ b) flips_R += np.sign(w @ swap(a, b, R)) == np.sign(w @ b) print("换 J 分量翻转:", flips_J, " 换非 J 分量翻转:", flips_R) # 玩具里后者为 0
全局工作空间理论把大脑看成一群并行微服务加一条容量有限的广播总线:进入总线的信息被全网订阅,就成为「意识到的」;更强的版本还要求进入总线是全有或全无的,像分布式提交要么整体生效、要么不生效,而不是渐进的。
全局工作空间理论(Global Workspace Theory, GWT)由 Bernard Baars 在 1988 年提出,后经 Dehaene 等人发展为神经版本。它试图解释意识通达(access consciousness,信息可被报告、可被推理调用的那种「意识到」):大量处理在无意识中并行进行,只有少量内容赢得竞争、进入全局空间后被广播。神经版本还强调点燃(ignition):信息一旦越过阈值,会突然、非线性地扩散到全脑。
论文列出 J-space 满足的五条「工作空间式」性质:
「灵活泛化」指同一个 J 向量能被多种下游操作当作参数使用——就像总线上的一条消息被不同服务各取所需。论文自己承认:与 GWT 的架构对应是松散的,容量限制和选择性已有迹象,但完整的「点燃」机制还没找到。
公开评议集中在三点:其一,功能通达 ≠ 现象体验——哲学家 Ned Block 早就区分了「通达意识」与「现象意识」,J-space 至多对应前者;其二,有评论认为为了让定义成立,GWT 的许多要求被放宽了,剥掉这层框架后,它是一个关于计算组织的发现,而非关于心智的发现;其三,前述小模型复现失败提示,结论的适用范围还需更多独立检验。论文本身没有宣称机器有意识,但措辞留了想象空间。
import numpy as np # 对比两种「进入工作空间」的方式:线性渐进 vs 点燃式(全有或全无) def linear_access(s): return s / s.sum() # 强度成比例地分享总线 def ignition_access(s, k=3, theta=0.6, gain=8): top = np.argsort(s)[-k:] # 容量限制:只有前 k 个参与竞争 out = np.zeros_like(s) act = 1 / (1 + np.exp(-gain * (s[top] - theta))) # 陡峭 S 形 ≈ 阈值 out[top] = np.where(act > 0.5, 1.0, 0.0) # 越过阈值→整体广播,否则沉默 return out s = np.array([0.2, 0.55, 0.65, 0.9, 0.1, 0.58]) # 6 个候选概念的激活强度 print("线性:", linear_access(s).round(2)) print("点燃:", ignition_access(s)) # 0.58 与 0.65 很接近,结局却一个 0 一个 1