Memorizing Transformers - KNN Memory · essay
11. Memorizing Transformers:把 KNN 检索塞进 Transformer 的记忆层
论文:Memorizing Transformers
本地原文:
![[papers/11-Memorizing-Transformers-2203.08913.pdf]]
论文定位与阅读路径
阅读边界
这篇笔记不是逐字搬运论文全文,而是“原文内嵌 + 逐节精读”。你在 Obsidian 里打开本文,上面就是 PDF 原文,下面是中文拆解;遇到不懂的段落,可以直接上下对照。
核心定位
这篇论文把语言模型的记忆从“只能靠参数和当前上下文”扩展成“模型运行时还能查一个外部 KNN 记忆库”。它更偏算法层,不是应用层的 Mem0/Zep。
如果用一句大二能懂的话说:
Transformer 原本只能看当前上下文;Memorizing Transformer 让它还能按相似度翻以前见过的 token 隐状态。
原文结构导读
| 章节 | 原文在讲什么 | 你要抓住什么 |
|---|---|---|
| Abstract | 不更新权重,也能在推理时“读入并记住”新数据 | 记忆是外部 KNN key-value 存储 |
| 1 Introduction | 为什么只靠训练/微调不够 | 新知识写进参数太贵,推理时记忆更灵活 |
| 2 Related Work | Transformer-XL、kNN-LM、检索增强模型 | 它不是普通 RAG,而是在模型内部层做检索 |
| 3 Method | 核心方法:memorizing layer + KNN lookup | 重点看 key/value 怎么存、query 怎么查 |
| 4 Experiments | C4、arXiv、PG-19、GitHub、Isabelle 等任务 | 证明记忆库越大,困惑度通常越低 |
| 5 Conclusion | 外部记忆可持续扩大模型能力 | 参数量不是唯一存知识的方法 |
证据链阅读
| 证据点 | 读什么 | 工程判断 |
|---|---|---|
| Memorizing layer | 在哪一层查 KNN memory | 它不是应用层 RAG,而是模型层检索 |
| Key/value 设计 | hidden state 如何入库和检索 | 记忆质量取决于表示空间 |
| 不同语料实验 | C4、arXiv、代码等是否都改善 | 判断 KNN memory 对哪些分布有效 |
| memory size 曲线 | 库越大是否持续收益 | 外部记忆不是越大越好,还要看检索成本 |
研究背景与问题定义
背景:普通 Transformer 的记忆在哪里
普通 LLM 的记忆主要有两种:
| 记忆位置 | 例子 | 优点 | 缺点 |
|---|---|---|---|
| 参数记忆 | 预训练权重 | 读取快 | 更新很贵,要训练 |
| 上下文记忆 | prompt 里的文本 | 易控制 | 长度有限,贵 |
Memorizing Transformers 加了第三种:
| 记忆位置 | 例子 | 优点 | 缺点 |
|---|---|---|---|
| 外部隐状态记忆 | past hidden states 的 key-value | 可扩展、可在推理时写入 | 需要高效检索和管理 |
这里的关键是:它记的不是人类可读文本,而是模型中间层的向量表示。
方法框架与核心机制
核心结构
这个图要这样理解:
- 当前 token 进入模型。
- 在某一层,模型产生 query。
- query 去外部记忆库里找相似 key。
- 找到的 value 参与 attention。
- 当前序列自己的 attention 和外部记忆 attention 融合。
- 当前输入也会变成新的 key-value,继续写回记忆库。
key、value、query 到底是什么
先别被公式吓住。你可以把它理解成搜索系统:
| 术语 | 搜索系统类比 | 在论文里的含义 |
|---|---|---|
| query | 搜索词 | 当前 token 的隐状态变换 |
| key | 索引向量 | 过去 token 的隐状态表示 |
| value | 被取回的内容 | 过去 token 对应的值向量 |
| KNN | 找相似笔记 | 找距离最近的 k 个 key |
普通 RAG 是:
代码块收起展开
问题文本 -> embedding -> 查文档 chunkMemorizing Transformer 更像:
代码块收起展开
模型中间层 query -> 查历史 hidden states -> 融入 attention所以它比应用层 RAG 更贴近模型内部。
Method 逐层拆解
1. Memorizing layer
论文不是在每一层都查外部记忆,而是在 Transformer 里选某些层作为 memorizing layer。
这样做有两个原因:
- 每层都检索成本太高。
- 中间层的表示往往比原始 token 更适合相似性匹配。
对你来说,重点不是背具体层数,而是理解这个设计原则:
记忆检索发生在模型内部表示空间,而不是只发生在输入文本空间。
2. 外部 memory 是非可微的
论文中的 memory store 不是训练参数的一部分。它更像一个外部数据库。
这带来两个结果:
| 结果 | 意义 |
|---|---|
| 不需要反向传播更新 memory | 推理时也能写入新记忆 |
| KNN 检索不可微 | 训练时不能像普通神经层那样端到端优化全部检索过程 |
这就是它和 MemoryLLM、Titans 这类“可学习记忆模块”的区别。
3. 近似 KNN
真正的记忆库会很大,暴力比较所有 key 太慢,所以需要 approximate nearest neighbor。
学习者视角:
代码块收起展开
暴力搜索:一本书一页页翻。
近似 KNN:先按章节/索引缩小范围,再找最像的几页。工程上类似 FAISS、ScaNN、HNSW 这类向量检索结构。
4. 记忆与注意力融合
普通 attention 只看当前上下文内的 key-value。论文加了外部 memory key-value。
可以理解成:
代码块收起展开
当前上下文 attention:看眼前这段话。
记忆 attention:翻以前见过的相似片段。
最终输出:两边信息合并后预测下一个词。这也是为什么它能处理比上下文窗口更远的信息。
相关工作与方法对比
这篇和 RAG 的关系
| 维度 | 普通 RAG | Memorizing Transformer |
|---|---|---|
| 检索对象 | 文档 chunk | 模型内部 hidden states |
| 发生位置 | 模型外,prompt 前 | 模型层内部 |
| 可解释性 | 较强,能看到文本 | 较弱,取回的是向量 |
| 工程门槛 | 较低 | 较高,需要改模型结构 |
| 适合谁 | 应用开发 | 算法研究 |
所以你现在做 Java 后端 Agent,不需要真的实现 Memorizing Transformer。但你要吸收它的思想:
记忆不一定只是文本数据库,也可以是模型中间状态的可检索缓存。
实验论证与评价方式
实验阅读方法
论文在多类数据上评估,包括网页文本、论文、书籍、代码、形式化定理。它想证明的不是“某个任务赢了”,而是:
- 外部记忆对不同类型语言建模都有帮助。
- memory size 变大时,效果可以继续提升。
- 检索到的历史片段能补足有限上下文。
你读实验时不要死抠每个数字,先抓三个问题:
- baseline 是什么。
- 加 memory 后 perplexity 有没有下降。
- 记忆库变大时趋势是否稳定。
perplexity 可以粗暴理解为:
代码块收起展开
模型对下一个词越不困惑,perplexity 越低。概念校准
你考试做编程题时,脑子里能记住当前题目,这相当于上下文。你以前写过很多类似代码,但不可能全部背下来。于是你有一个代码片段库,看到当前题目后检索最相似的旧代码,再辅助当前推理。
Memorizing Transformer 做的就是这个,只不过它检索的不是代码文本,而是模型内部向量。
工程映射与实践启发
工程落地启发
你暂时不用改模型,但可以借鉴这个分层:
| 论文概念 | Java Agent 工程映射 |
|---|---|
| key-value memory | 向量库里的 embedding + 原始内容 |
| query vector | 当前用户问题 embedding |
| approximate KNN | Milvus/Qdrant/pgvector/HNSW |
| memorizing layer | ContextBuilder 里的检索阶段 |
| memory size | 可控的长期记忆容量 |
一个简化实现:
flowchart TD
A[用户问题] --> B[生成 embedding]
B --> C[向量库 KNN 检索]
C --> D[取回相似记忆]
D --> E[重排 rerank]
E --> F[拼入 prompt]
F --> G[LLM 回答]
这不是论文原方法,但工程精神是一致的:把“过去经验”变成可检索的外部记忆。
局限、对比与阅读检查
局限与边界
- 需要改模型结构,普通 API 用户做不了。
- 记忆是向量,不如文本记忆可解释。
- KNN 检索会带来额外延迟和存储成本。
- 记住旧数据可能带来隐私与版权问题。
- 它更擅长“找相似历史”,不等于真正理解长期因果关系。
阅读检查清单
读这篇后,你至少要能说清:
- 什么是外部 KNN 记忆。
- 为什么它不是普通 RAG。
- key/value/query 在记忆检索里分别是什么。
- 为什么 memory size 能影响模型表现。
- 为什么它属于模型驱动派,而不是应用驱动派。
和后续论文的关系
Memorizing Transformer 是“模型内部记忆”路线的早期代表。后面的 MemoryLLM、Memory3、WISE、Titans 都在继续问同一个问题:
新知识到底应该放在外部数据库、模型参数、激活缓存,还是一个专门设计的记忆模块里?
深度精读:KNN memory 是怎么接到 Transformer 里的
1. 它和 kNN-LM 的关系
这篇不是凭空冒出来的。它继承了一个直觉:
代码块收起展开
语言模型在预测下一个 token 时,如果能找到过去很相似的上下文,就能参考过去那个位置后面出现了什么。kNN-LM 更像在输出层外接一个检索器;Memorizing Transformer 更进一步,把检索接进 Transformer 的中间层。
差别:
| 方法 | 检索发生在哪里 |
|---|---|
| kNN-LM | 输出概率附近 |
| RAG | 输入 prompt 前 |
| Memorizing Transformer | Transformer hidden state 中间 |
所以它更“内生”,但也更难工程复现。
2. key/value 不是数据库字段,而是模型内部状态
在普通后端里 key/value 可能是:
代码块收起展开
key = user_id
value = user_profile在这篇里:
代码块收起展开
key = 过去某个 token 在某层的隐藏向量
value = 与这个位置相关的 value 向量模型当前处理 token 时生成 query,去找相似 key。找到后,把对应 value 当成额外注意力材料。
这就是为什么它不容易解释:取回的不是文本,而是向量。
3. 为什么要 approximate KNN
如果记忆库有几千万个 hidden states,暴力搜索就是:
代码块收起展开
当前 query 和每个 key 算一次距离这不可接受。所以要近似搜索。
工程上你可以类比:
| 精确搜索 | 近似搜索 |
|---|---|
| 查全表 | 建索引 |
| 结果最精确 | 结果可能近似 |
| 慢 | 快 |
| 小数据可用 | 大数据必须用 |
这和向量数据库里的 HNSW、IVF、ScaNN 是同一类问题。
4. 为什么 memory size 越大可能越好
模型参数固定时,参数里能记的知识有限。外部记忆库变大,相当于能查的历史经验更多。
但它不是无限好:
- 检索越大,延迟越高。
- 相似但错误的记忆会干扰。
- 旧数据可能过时。
- 隐私和数据隔离更复杂。
所以真实系统一定要有 memory eviction / filtering / scope。
5. 它对应用层 RAG 的启发
虽然你不会改 Transformer,但可以学到三点:
| 论文思想 | 应用层做法 |
|---|---|
| 检索过去相似内部状态 | 检索过去相似任务/对话 |
| 记忆库持续增长 | 长期经验库 |
| 检索结果进入推理 | 检索结果进入 prompt/context |
例如编程 Agent:
代码块收起展开
当前 bug 栈信息 -> 检索相似历史 bug -> 取回修复步骤 -> 注入上下文这就是“外部经验增强推理”。
6. 为什么它不是万能长上下文
KNN memory 更擅长“局部相似”。但有些任务需要:
- 全局结构理解。
- 长距离因果推理。
- 多个证据合成。
- 时间变化判断。
这些不能只靠相似 hidden state 解决。
所以它和 Zep/HippoRAG 是互补关系:
代码块收起展开
KNN memory: 找相似模式
Graph memory: 找实体关系
Temporal memory: 找时间有效事实
Reflection memory: 找经验教训7. 读公式的最低要求
你不需要推完整公式,但要知道:
代码块收起展开
query q:当前 token 表示
memory keys K:过去 token 表示
memory values V:过去可被 attention 读取的信息
KNN(q, K):找最相似的 k 个历史位置
attention(q, K_mem, V_mem):把取回的历史信息融合进当前表示把这几行翻成人话,你就不会被公式吓住。
8. 如果写成工程系统
代码块收起展开
record MemoryVector(
String id, // 向量记忆 ID。
float[] keyEmbedding, // 用于 KNN 检索的 key 表示。
String payloadRef, // 指向原始 token 片段、文档块或外部内容。
Instant createdAt, // 写入时间,可用于过期或分层索引。
String scope // 作用域,避免不同用户/任务的记忆混检。
) {}
// retrieve = 给当前 query 表示找最近的 k 条历史记忆。
List<MemoryVector> retrieve(float[] query, String scope, int k) {
// vectorIndex.search 应同时使用向量相似度和 scope 过滤。
return vectorIndex.search(query, scope, k);
}注意:应用层的 payloadRef 可以指向文本、代码、工具日志;论文里的 payload 是模型内部 value。思想相同,层级不同。
论文精读主线:问题-方法-验证-局限-启发
1. 核心结论
Memorizing Transformers 把历史 hidden states 存入外部 KNN memory,让 Transformer 在生成时能检索更远历史的内部表示。
本篇的阅读重心是:在 Transformer 中间层检索历史 hidden states,扩展模型内部可访问记忆。读完后要能说清它改变的是 runtime、存储、检索、更新还是评测层;否则只是记住了论文名。
2. 问题背景
普通 Transformer 的上下文有限,长距离信息离开窗口后就无法直接注意到。外部文本 RAG 检索的是人类可读文本,但模型内部其实可以记住隐藏状态表示;问题是如何把这种内部记忆接回注意力机制。
把问题具体化为失败样例:模型需要回忆远早于当前窗口的局部模式,文本级 RAG 太粗,输出层 kNN 又介入太晚。用这个样例反推论文动机,比泛泛说“上下文不够”或“记忆不准”更有判断力。
3. 核心方法
模型在特定层把 key/value hidden states 写入外部 memory。生成时当前 query 去 KNN memory 找相似 key,取回 value,并和普通 attention 结果融合。
flowchart TD
H[历史 token hidden states] --> K[写入 KNN key/value memory]
Q[当前 query 表示] --> N[KNN 检索近邻]
K --> N
N --> V[取回历史 value]
V --> F[和 attention 融合]
F --> O[预测下一个 token]
方法部分按数据流读:训练或推理时保存 hidden state key/value,后续在中间层按相似 hidden state 检索并融合。每一步都要能标出输入、输出、控制点和失败后该记录的日志。
4. 技术细节
这里的 key/value 不是数据库字段,而是模型内部向量。memory 通常非可微,依赖近似最近邻检索。它更像模型层面的长程缓存,不是应用层知识库。
代码块收起展开
final class KnnTransformerMemory {
private final HiddenStateWriter stateWriter; // 从指定层抽取 key/value hidden states 写入外部索引
private final ApproxKnnIndex knnIndex; // 近似最近邻检索,控制大规模 memory 的延迟
private final MemoryAttentionMixer mixer; // 把 KNN 取回的 value 和普通 attention 输出融合
private final MemoryEvictionPolicy evictionPolicy; // 控制 memory 容量、过期和采样策略
MemoryResult handle(MemoryInput input) {
// 1. 从指定层抽取 key/value hidden states 写入外部索引
// 2. 近似最近邻检索,控制大规模 memory 的延迟
// 3. 把 KNN 取回的 value 和普通 attention 输出融合
// 4. 控制 memory 容量、过期和采样策略
// 返回前附带 evidenceIds、scoreBreakdown、traceId,避免只看到答案看不到记忆来源。
return MemoryResult.withTrace(input.traceId());
}
}字段检查重点是 layerId、hiddenKey、valueState、knnIndex、retrievalK、mixingWeight 是理解重点。这些字段不是装饰,它们决定后续能否检索、审计、回滚、限权和做离线评测。
5. 实验验证
实验看语言建模困惑度、长上下文任务和 memory size 消融。要确认收益来自 KNN memory,而不是更多参数或更长窗口。随着 memory 变大性能是否稳定提升,是这类方法的重要证据。
实验阅读重点:看长序列语言建模、稀有模式回忆和不同层接入消融;不要把它和普通文档 RAG 混为一谈。如果论文没有直接给出这层证据,复现时就要自己补机制级指标。
6. 局限与启发
它难解释、难用闭源 API 复现,也不适合作为用户可控记忆。启发是应用层 RAG 可以借鉴 key/value 分离思想:检索 key 用于匹配,value 保存更完整证据。
迁移到你的项目时,结论是:这类方法暂不适合作为 Java 后端首版,但能启发你区分“文本证据记忆”和“模型表示记忆”。先把边界做对,再追求更复杂的模型或更大的记忆规模。
7. 关键术语表
| 术语 | 在论文里的意思 | 工程落地时要看什么 |
|---|---|---|
| KNN memory | 最近邻外部隐藏状态库 | 向量索引而非文本库 |
| Key/value hidden states | 模型内部检索键和值 | 指定层输出的向量 |
| Approximate KNN | 近似最近邻 | 用延迟换可扩展性 |
| Memory layer | 接入外部记忆的 Transformer 层 | 决定写入和读取位置 |
8. 复现难点
实现需要改模型结构,训练和推理耦合强。memory 太大延迟高,太小收益低。内部状态无法人工审计,写入噪声也难发现。
复现入口应从失败集开始:最小复现可用 toy Transformer + FAISS 模拟 hidden state 检索,观察 k、层位置和缓存大小的影响。只有先证明旧方案会坏,才知道新机制是否真的解决了问题。
9. 代码框架建议
如果只做工程类比,可实现 RetrievalKey(textEmbedding) 与 EvidenceValue(rawChunk) 分离:key 负责匹配,value 保留原文、来源、时间和权限。
代码验收门槛:任何内部记忆方案都要回答污染、回滚、权限和解释问题;回答不了就只能做研究实验。这条门槛应写进测试或 trace 字段里,避免实现只停留在 demo。
10. 相关论文对比
它和 RAG 都是检索增强,但检索对象不同:RAG 检索文本,Memorizing Transformer 检索 hidden states。Titans 进一步引入可学习神经长期记忆,MemLong 更偏文本历史缓存。
横向比较时抓这个角度:它比 MemLong 更贴近模型内部,比 MemoryLLM 更偏检索缓存,比 WISE 少了参数编辑语义。比较对象不是谁“更先进”,而是谁负责哪一层失败。
11. 批判性思考
这类方法证明模型内部记忆有潜力,但产品可控性弱。对个人知识 Agent,更适合作为思想来源,而不是第一阶段技术路线。
批判边界:hidden state 记忆难以解释给用户看;一旦记错,很难像外部数据库那样定位和删除。这不是否定论文,而是把论文结论放回真实系统的成本、权限和恢复约束里。