一个 700 亿参数的模型,你可以把它想成一个巨型分布式 KV store——「埃菲尔铁塔在巴黎」这条事实不是散落在整个集群里,而是存在某几个具体节点上。问题是:没有 schema、没有索引、没有文档,你怎么找到那个节点?答案是像 chaos engineering(故障注入) 一样——故意破坏某一层的激活,看模型答错没有,从而反推「这条数据到底存哪」。
传统认知里神经网络是「黑箱、知识全局纠缠」。但 2021 年 Geva et al. 提出一个关键洞察:Transformer 里的 FFN(前馈层)本质上是一个 key-value memory。FFN 有两个矩阵——第一个矩阵(W_in)的每一行是一个 key(匹配某种输入模式,如「主语是某地标」),第二个矩阵(W_out)对应的列是一个 value(往输出词表里灌「巴黎」这类 token)。这就把「知识存哪」从哲学问题变成了可定位的工程问题。
怎么定位?两条主流路径:
ROME 论文的核心实证发现:处理主语名字最后一个 token 时的中层 MLP,是事实召回的决定性位置。这不是猜的——是几千条事实上的因果实验统计出来的。有了「存哪」,下一步自然是「能不能精准改」。
import torch from transformers import AutoModelForCausalLM, AutoTokenizer # 用 hook 抓 FFN 中间激活——观察 key-value memory 结构 model = AutoModelForCausalLM.from_pretrained("gpt2-xl") tok = AutoTokenizer.from_pretrained("gpt2-xl") acts = {} def grab(name): def hook(mod, inp, out): acts[name] = out.detach() return hook # 挂在中层 MLP 的激活函数后(key 被激活的地方) layer = 17 # ROME 发现的「决定性中层」区间 model.transformer.h[layer].mlp.act.register_forward_hook(grab("mlp_key")) ids = tok("The Eiffel Tower is located in the city of", return_tensors="pt") model(**ids) # 最后一个 subject token 处哪些神经元被强激活 = 候选「知识神经元」 key = acts["mlp_key"][0, -1] print("激活最强的 5 个神经元:", key.topk(5).indices.tolist())
模型学错了「当前美国总统是 X」,你有三个选择:重新预训练(等于重建整个数据库,天价)、微调(等于全表扫描更新,容易误伤别的行、还会遗忘)、或者 ROME——像一条精准的 UPDATE ... WHERE id=?:只改一个 MLP 权重矩阵,加一个「秩为 1」的修正项,让「这个主语」的 key 映射到「新客体」的 value,理想情况下不碰任何其他记录。
事实会过时(换届、改名、数据错误)。微调改一条事实的代价太大:要构造数据、可能过拟合、还会灾难性遗忘别的知识。ROME 的思路是把上一张卡的洞察用到底——既然 FFN 是线性联想记忆,那改它就是一道线性代数题。
把 FFN 的输出矩阵 W 看成一个联想记忆:它把一堆 key 向量 k 映射到 value 向量 v(即 W k ≈ v)。现在我们想让某个特定的主语 key k*(如「埃菲尔铁塔」)输出一个新 value v*(指向「罗马」),同时尽量不动 W 对其他所有 key 的行为。这变成一个带约束的最小二乘:
直觉拆解:秩一矩阵 = 一个列向量乘一个行向量,是「信息量最小」的一种修改——它只在一个方向上改变 W 的行为。C⁻¹ 这一项是关键护栏:它用「历史上所有 key 的统计结构」把更新投影到不干扰常见 key 的方向上——类似给 UPDATE 加了个 WHERE 条件,避免误伤其他行。改完,模型不仅在原句上说「罗马」,在「铁塔坐落于哪个国家」这类改写问法上也能泛化——因为改的是内部表示,不是记一个字符串。
# EasyEdit:ROME/MEMIT 等编辑方法的统一实现库 # pip install easyeditor from easyeditor import BaseEditor, ROMEHyperParams hparams = ROMEHyperParams.from_hparams("./hparams/ROME/gpt2-xl") editor = BaseEditor.from_hparams(hparams) # 一次编辑一条事实:把「埃菲尔铁塔所在地」改成罗马 metrics, edited_model, _ = editor.edit( prompts=["The Eiffel Tower is located in the city of"], ground_truth=["Paris"], # 旧事实 target_new=["Rome"], # 新事实(要写进参数的) subject=["Eiffel Tower"], # 关键:定位到主语 token ) # metrics 里含 efficacy(编辑成功率)/ generalization(改写问法泛化) # / specificity(无关知识有没有被误伤)三个核心指标 print(metrics)
W,改到几十条后模型开始「串味」、通用能力下降。这正是下一张卡 MEMIT 要解决的问题。如果 ROME 是单条 UPDATE,MEMIT 就是批量 UPDATE + 负载均衡。单条循环改一千次会锁表、会累积误差;MEMIT 把「要写入的这一千条修正量」一次性求解,并摊薄到连续好几个中层 MLP 上——就像把写压力分散到多个分片,而不是全砸在一个节点上。
真实需求往往是成批的:一个知识库更新了几千条事实、要一次性同步进模型。ROME 逐条改会互相打架。MEMIT(Meng et al. 2022)做了两个关键改进:
MEMIT 论文实证:在 GPT-J(6B)和 GPT-NeoX(20B)上,可以一次注入数千条关联,规模远超此前方法。它至今仍是「批量知识注入」类研究的主流基线。
from easyeditor import BaseEditor, MEMITHyperParams hparams = MEMITHyperParams.from_hparams("./hparams/MEMIT/gpt-j-6b") editor = BaseEditor.from_hparams(hparams) # 关键差异:prompts 是一个「列表的列表」,一次批量灌入 metrics, edited_model, _ = editor.edit( prompts=[ "The president of the USA is", "The capital of Australia is", "The CEO of Twitter is", # ... 可以是几千条 ], target_new=["Jane Doe", "Sydney", "John Roe"], subject=["USA", "Australia", "Twitter"], ) # MEMIT 内部把这批更新联合求解、摊薄到多个中层 MLP print(metrics)
你在数据库里改了一条主记录,但没跑级联更新(cascade)——所有依赖它的物化视图(materialized view)还是旧值。模型编辑正是这个病:你把「梅西效力的俱乐部」改成新队,但模型对「梅西的队友是谁」「梅西所在联赛」这些逻辑上被牵连的事实依然答旧的。改了主键,派生视图没刷新,数据进入不一致状态。
ROME/MEMIT 看起来很美,但 Cohen et al. 2023 提出了一个扎心的问题:一条事实从来不是孤立的。改「A 的首都是 B」应该连带影响一串推论:「A 的政府所在地」「B 是哪国首都」「从 A 出发去首都要往哪走」……他们构建了 RippleEdits 基准(约 5000 条编辑,专门测这种连锁反应),发现:
另一层「知识冲突」是参数记忆 vs 上下文的冲突:当 RAG 检索到的新事实和模型参数里的旧记忆打架,模型该信谁?研究显示模型常常固执地信自己的参数记忆,哪怕 context 明确给了更新——这正是幻觉治理里最难的一环,也说明「把知识写进参数」和「让模型正确使用知识」是两回事。
# 编辑后必须测「涟漪」,而不只是测目标事实本身 # 编辑:梅西效力的俱乐部 → 迈阿密国际 target = "Where does Lionel Messi currently play? " print(query(edited_model, target)) # ✓ 大概率答对——目标事实 # 真正的考验:逻辑上被牵连的「涟漪事实」 ripples = [ "Which league does Lionel Messi play in? ", # 联赛 "Who are Lionel Messi's teammates? ", # 队友 "In which country does Lionel Messi work? ", # 国家 ] for q in ripples: # 编辑方法常在这里露馅:目标对了,涟漪全错 print(q, "→", query(edited_model, q))