今天的大模型 Agent、RAG、长期记忆系统都在讨论一个问题:模型到底应该把信息记在哪里?
是全部塞进参数里?
是放进 Prompt?
还是放进数据库、向量库、文件系统,再由模型按需读取?
这个问题其实在 2014 年 DeepMind 的一篇经典论文里就已经被系统性地提出了。这篇论文就是:
《Neural Turing Machines》
作者:Alex Graves、Greg Wayne、Ivo Danihelka
论文地址:arXiv:1410.5401
一句话概括这篇论文:
Neural Turing Machine,简称 NTM,试图把神经网络和一个可读写的外部记忆连接起来,让模型不仅能识别模式,还能学习一些类似程序的操作,比如复制、排序、重复、联想回忆。
一、论文内容
1. 论文想解决的问题
传统神经网络,尤其是 RNN / LSTM,理论上可以处理序列,也可以在 hidden state 中保存上下文。但问题是:hidden state 的记忆容量有限,而且很难像程序一样进行精确读写。
比如我们希望模型学会这样的操作:
看到一段序列
↓
把它暂存起来
↓
遇到一个分隔符
↓
再把前面的序列原样输出
普通 RNN / LSTM 可以尝试把所有信息压缩进 hidden state,但这就像要求一个人只靠脑子记住一整页内容,然后再一字不差地背出来。
NTM 的想法是:
不要让神经网络只靠脑内记忆,而是给它一个外部 memory,让它学会读写这个 memory。
论文摘要里明确说,NTM 通过 attention 机制把神经网络和外部记忆资源连接起来,整体结构类似图灵机或冯·诺依曼架构,但它是端到端可微的,因此可以用梯度下降训练。
2. NTM 的基本结构
NTM 主要由两部分组成:
diff
Controller 控制器
+
Memory Bank 外部记忆矩阵
Controller 可以是普通前馈网络,也可以是 LSTM。它类似计算机里的 CPU,负责接收输入、产生输出,并决定如何读写 memory。
Memory Bank 是一个矩阵:
ini
Memory = N × M
也就是 N 个 memory slot,每个 slot 是一个 M 维向量。
论文中也明确描述,NTM 架构包含两个基础组件:一个神经网络 controller 和一个 memory bank;controller 与外部世界通过输入输出向量交互,同时也通过选择性的 read / write 操作与 memory matrix 交互。
可以把它理解成:
markdown
普通 RNN:
输入 → hidden state → 输出
NTM:
输入 → controller → 读 memory
→ 写 memory
→ 输出
所以,NTM 的关键不是"多了一个矩阵",而是:模型要学会如何使用这个矩阵。
3. 读操作:Read Head
NTM 不是像普通计算机那样读取某一个确定地址:
css
memory[5]
而是让 read head 生成一个对所有 memory slot 的权重分布:
ini
w = [0.01, 0.02, 0.91, 0.03, 0.03]
这表示模型主要关注第 3 个 memory slot。
读取结果就是 memory 的加权求和:
ini
read_vector = Σ weight_i × memory_i
这种方式叫 soft attention。它的好处是:整个过程可微,可以反向传播。
4. 写操作:Write Head
写操作也不是直接覆盖某个地址,而是分成两步:
csharp
erase:擦除旧内容
add:写入新内容
具体来说,write head 会生成:
写入位置权重 write_weight
擦除向量 erase_vector
添加向量 add_vector
然后对 memory 做连续可微的更新。
这个设计很像 LSTM 里的 forget gate 和 input gate:不是粗暴清空,而是选择性擦除、选择性写入。
5. 寻址机制:Content-based + Location-based
NTM 的寻址机制是整篇论文最核心的部分之一。
它有两类寻址方式。
第一类是 content-based addressing,也就是按内容找。Controller 生成一个 key,然后和 memory 中每个 slot 做相似度匹配,找到最相似的位置。
这和今天的向量检索很像:
sql
query embedding → search memory → retrieve relevant item
第二类是 location-based addressing,也就是按位置移动。比如当前 head 在第 5 个 slot,下一步可以移动到第 6 个 slot。
这对于复制、遍历、循环、排序等任务非常重要,因为很多算法不是只靠"找相似内容",而是需要"沿着位置一步一步移动"。
完整的寻址流程大概是:
bash
根据内容找到位置
↓
和上一步位置进行插值
↓
做相对位移 shift
↓
sharpen,让注意力更集中
↓
得到最终读写权重
这也是 NTM 和普通 attention 的重要区别:
普通 attention 更像"看哪里相关";
NTM 的 location shift 更像"程序里的指针移动"。
6. 论文实验
论文没有把 NTM 用在普通分类任务上,而是设计了一组"算法学习任务"。
Copy Task
输入一段二进制序列,遇到 delimiter 后,模型需要原样输出前面的序列。
例如:
xml
输入:
101
011
110
<delimiter>
输出:
101
011
110
这个任务考察模型能否学会:
diff
顺序写入 memory
+
顺序读取 memory
Repeat Copy Task
输入一段序列和一个重复次数,模型需要把序列重复输出多遍。
例如:
css
输入序列:A B C
重复次数:3
输出:A B C A B C A B C
这个任务比 copy 更难,因为它不仅要记住序列,还要学会计数和循环。
Associative Recall
给模型一组 item,然后查询其中一个 item,要求模型输出它后面的 item。
例如:
css
输入:
A → B → C → D
查询:
B
输出:
C
这考察的是联想记忆能力:
模型需要根据内容找到 memory 位置,然后读取相邻内容。
Priority Sort
输入一组带优先级的向量,模型需要输出优先级最高的一部分。
这相当于让模型学习一种简单的排序算法。
论文摘要也总结说,NTM 可以从输入输出样例中推断出 copying、sorting、associative recall 等简单算法。
二、论文提出的创新点和关键技术
1. 神经网络 + 外部可读写记忆
NTM 最大的创新是:
把神经网络和外部 memory 连接起来。
传统神经网络的知识主要存在参数里,短期信息存在 hidden state 里。NTM 则额外引入一个 memory matrix,用来存储当前任务过程中的临时信息。
这个思想非常重要,因为它把两件事分开了:
模型参数:学习如何操作
外部 memory:存储当前任务的信息
这和现代 Agent 系统很像:
LLM:负责推理和规划
数据库 / 向量库 / 文件系统:负责存储信息
检索器:负责读取相关内容
记忆模块:负责写入新的状态
DeepMind 后续提出的 Differentiable Neural Computer,简称 DNC,也延续了类似思想:DNC 的核心是一个 controller,它负责接收输入、读写 memory,并产生输出;memory 是一组可以存储向量信息的位置。
2. 可微分读写机制
普通计算机访问内存是离散操作:
arduino
read memory[5]
write memory[8]
但离散地址选择没法直接用梯度下降训练。
NTM 的做法是:
不选择单个地址,而是对所有地址生成一个权重分布。
读操作是加权求和,写操作是加权 erase / add。
这样一来,内存访问就从离散操作变成了连续操作,也就可以反向传播。
这就是 NTM 的核心技术路线:
离散寻址
→ soft attention 寻址
→ 可微读写
→ 端到端训练
3. Content-based Addressing:按内容检索
Content-based addressing 让模型可以根据内容找 memory。
例如,模型之前写入了:
css
用户 A 的订单信息
用户 B 的订单信息
用户 C 的订单信息
现在输入"用户 B",模型可以根据相似度找到用户 B 对应的位置。
这和今天 RAG 里的 embedding search 很像:
用户问题 → 向量化 → 检索相似文档 → 注入上下文
所以从架构思想上看,NTM 可以被看作一种很早期的"神经网络 + 可检索记忆"的模型。
4. Location-based Addressing:按位置移动
只靠内容检索不够,因为算法任务经常需要顺序操作。
例如 copy task 中,模型需要:
写入第 1 个元素
写入第 2 个元素
写入第 3 个元素
然后从第 1 个元素开始依次读出
这就需要一种类似"指针"的能力。
NTM 通过 location-based addressing 让 head 可以基于上一步位置进行移动,比如向前或向后 shift。
这让 NTM 不只是能"找东西",还可以"遍历东西"。
5. Erase + Add 写入机制
NTM 的写入不是简单覆盖,而是:
csharp
先 erase
再 add
这使模型可以对 memory 做细粒度修改。
如果把 memory 想象成一张纸,那么 NTM 不是把整张纸撕掉重写,而是:
擦掉一部分
再写上一部分
这种机制使 memory 更新更加平滑,也更适合梯度训练。
6. 从模式识别走向算法学习
传统神经网络更擅长做模式识别,比如分类、回归、序列预测。
NTM 试图往前走一步:
让神经网络从样例中学习算法。
比如:
看到很多复制任务样例
→ 学会复制算法
看到很多排序任务样例
→ 学会排序策略
看到很多联想回忆样例
→ 学会根据线索检索后继项
这也是这篇论文真正有启发性的地方:它不是为了在某个 benchmark 上刷分,而是在探索一种更接近"神经计算机"的架构。
三、论文的实际应用场景
这里要先说清楚:原始 NTM 本身并不是今天工业界常用的模型。
它更多是一个研究原型。它证明了"神经网络 + 外部记忆 + 可学习读写"这条路是可行的,但原始 NTM 训练复杂、扩展性有限,而且论文实验主要集中在 toy tasks。
不过,它的架构思想影响很大。今天很多 Agent Memory、RAG、长期记忆、工作流自动化系统,都可以从 NTM 中看到影子。
1. 长序列复制与格式转换
适合处理这类任务:
输入一段序列
暂存起来
按规则重新输出
例如:
javascript
日志标准化
表单转 JSON
事件流重排
文档结构化抽取
代码片段改写
论文里的 copy task 就是这个方向的最小原型。PyTorch NTM 实现中也把 copy task 定义为:输入随机 bit 序列和 delimiter,模型需要复制原始序列。
2. 联想记忆和知识检索
NTM 的 associative recall 对今天的知识库问答非常有启发。
实际应用可以是:
objectivec
客户 ID → 最近订单
学生错题 → 对应知识点
会议议题 → 后续 action item
代码函数 → 相关调用链
商品 SKU → 历史价格与库存
核心逻辑是:
根据线索找到 memory 位置
↓
读取相关内容
↓
生成答案或下一步动作
这和现代 RAG 很像,只不过现代系统通常用向量数据库、全文搜索、reranker 和 LLM,而不是直接训练一个 NTM。
3. AI Agent 的长期记忆
这是我认为最值得关注的方向。
一个 Agent 如果只靠 prompt,很快就会遇到上下文窗口限制。
如果只靠模型参数,又无法记住用户的项目状态、偏好、历史任务和长期目标。
NTM 给出的架构思想是:
ini
Controller = LLM / Agent Planner
Memory = 向量库 / SQL / 文件系统 / 知识图谱
Read Head = 检索模块
Write Head = 总结与记忆写入模块
Addressing = 语义相似度 + 时间顺序 + 实体关系
这可以用于:
个人知识库 Agent
企业 SOP 自动化 Agent
代码开发 Agent
项目管理 Agent
长期学习助手
交易研究 Agent
4. 企业 SOP 和工作流自动化
在企业场景中,很多任务是流程型的:
收到需求
↓
查 SOP
↓
判断节点
↓
调用工具
↓
更新状态
↓
通知相关人员
这类系统非常适合借鉴 NTM:
arduino
SOP 文档 = memory
当前任务状态 = controller 输入
流程节点 = location-based pointer
工具调用结果 = write back memory
也就是说,Agent 不是每次都"重新想一遍",而是应该能读流程、记状态、沿着流程推进。
5. 图结构推理和路径规划
NTM 的后续模型 DNC 更强调复杂数据结构。DeepMind 在 DNC 介绍中提到,他们希望机器能够自己学习形成和导航复杂数据结构。
这类思想可以对应到:
知识图谱推理
供应链路径规划
代码依赖分析
企业组织关系推断
工作流节点跳转
当然,在真实工业场景中,我们今天更可能使用图数据库、图算法、GNN、LLM + 工具调用,而不是直接使用原始 NTM。
四、最小可运行 Demo:Mini NTM 学习 Copy Task
下面给一个最小 PyTorch Demo。
它不是完整论文级 NTM,而是一个简化版,用来帮助理解核心机制:
保留:
bash
外部 memory
read head
write head
soft attention 读写
erase + add 写入
简化:
bash
没有完整实现 content addressing
没有完整实现 location shift
没有 sharpening
没有多 read/write heads
也就是说,它是一个"教学版 Mini NTM"。
1. 安装依赖
pip install torch
## 2. 新建文件 **mini_ntm_copy.py**
ini
import torch
import torch.nn as nn
import torch.nn.functional as F
def generate_copy_data(batch_size=32, seq_len=5, bit_dim=8, device="cpu"):
"""
Copy Task 数据生成器。
输入 x:
- 前 seq_len 步:随机 bit 序列
- 第 seq_len 步:delimiter = 1
- 后 seq_len 步:空输入,要求模型开始输出前面的序列
x shape: [B, 2L + 1, bit_dim + 1]
y shape: [B, 2L + 1, bit_dim]
"""
seq = torch.randint(0, 2, (batch_size, seq_len, bit_dim)).float().to(device)
x = torch.zeros(batch_size, 2 * seq_len + 1, bit_dim + 1).to(device)
y = torch.zeros(batch_size, 2 * seq_len + 1, bit_dim).to(device)
# 输入阶段:写入随机 bit
x[:, :seq_len, :bit_dim] = seq
# delimiter 标记
x[:, seq_len, bit_dim] = 1.0
# 输出阶段:目标是复制原始序列
y[:, seq_len + 1:, :] = seq
return x, y
class MiniNTMCell(nn.Module):
def __init__(
self,
input_dim,
output_dim,
memory_slots=16,
memory_dim=16,
hidden_dim=64,
):
super().__init__()
self.memory_slots = memory_slots
self.memory_dim = memory_dim
self.hidden_dim = hidden_dim
# Controller:这里使用 LSTMCell
self.controller = nn.LSTMCell(input_dim + memory_dim, hidden_dim)
# Read Head:产生读取权重
self.read_key = nn.Linear(hidden_dim, memory_slots)
# Write Head:产生写入权重、erase 向量、add 向量
self.write_key = nn.Linear(hidden_dim, memory_slots)
self.erase = nn.Linear(hidden_dim, memory_dim)
self.add = nn.Linear(hidden_dim, memory_dim)
# 输出层
self.output = nn.Linear(hidden_dim + memory_dim, output_dim)
def initial_state(self, batch_size, device):
memory = torch.zeros(batch_size, self.memory_slots, self.memory_dim).to(device)
read_vec = torch.zeros(batch_size, self.memory_dim).to(device)
h = torch.zeros(batch_size, self.hidden_dim).to(device)
c = torch.zeros(batch_size, self.hidden_dim).to(device)
return memory, read_vec, h, c
def forward(self, x_t, state):
memory, read_vec, h, c = state
# Controller 输入 = 当前输入 + 上一步从 memory 读出的向量
controller_input = torch.cat([x_t, read_vec], dim=-1)
h, c = self.controller(controller_input, (h, c))
# soft attention 权重
read_w = F.softmax(self.read_key(h), dim=-1) # [B, N]
write_w = F.softmax(self.write_key(h), dim=-1) # [B, N]
# erase / add 向量
erase = torch.sigmoid(self.erase(h)) # [B, M]
add = torch.tanh(self.add(h)) # [B, M]
# 写入 memory:erase + add
write_w_expanded = write_w.unsqueeze(-1) # [B, N, 1]
erase_expanded = erase.unsqueeze(1) # [B, 1, M]
add_expanded = add.unsqueeze(1) # [B, 1, M]
memory = memory * (1 - write_w_expanded * erase_expanded)
memory = memory + write_w_expanded * add_expanded
# 读取 memory:加权求和
read_w_expanded = read_w.unsqueeze(1) # [B, 1, N]
read_vec = torch.bmm(read_w_expanded, memory).squeeze(1)
# 输出
out = self.output(torch.cat([h, read_vec], dim=-1))
return out, (memory, read_vec, h, c)
class MiniNTM(nn.Module):
def __init__(
self,
input_dim,
output_dim,
memory_slots=16,
memory_dim=16,
hidden_dim=64,
):
super().__init__()
self.cell = MiniNTMCell(
input_dim=input_dim,
output_dim=output_dim,
memory_slots=memory_slots,
memory_dim=memory_dim,
hidden_dim=hidden_dim,
)
def forward(self, x):
batch_size, time_steps, _ = x.shape
device = x.device
state = self.cell.initial_state(batch_size, device)
outputs = []
for t in range(time_steps):
out, state = self.cell(x[:, t, :], state)
outputs.append(out)
return torch.stack(outputs, dim=1)
def train():
device = "cuda" if torch.cuda.is_available() else "cpu"
bit_dim = 8
input_dim = bit_dim + 1
output_dim = bit_dim
model = MiniNTM(
input_dim=input_dim,
output_dim=output_dim,
memory_slots=16,
memory_dim=16,
hidden_dim=64,
).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
loss_fn = nn.BCEWithLogitsLoss()
steps = 3000
batch_size = 32
seq_len = 5
for step in range(1, steps + 1):
x, y = generate_copy_data(
batch_size=batch_size,
seq_len=seq_len,
bit_dim=bit_dim,
device=device,
)
logits = model(x)
# 只在 delimiter 之后计算 loss
loss = loss_fn(
logits[:, seq_len + 1:, :],
y[:, seq_len + 1:, :],
)
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 10.0)
optimizer.step()
if step % 300 == 0:
print(f"step={step}, loss={loss.item():.4f}")
return model, device
def test(model, device):
model.eval()
bit_dim = 8
seq_len = 5
x, y = generate_copy_data(
batch_size=1,
seq_len=seq_len,
bit_dim=bit_dim,
device=device,
)
with torch.no_grad():
logits = model(x)
pred = torch.sigmoid(logits)
pred_bits = (pred > 0.5).float()
print("\nInput sequence:")
print(x[0, :seq_len, :bit_dim].cpu().int())
print("\nTarget output:")
print(y[0, seq_len + 1:, :].cpu().int())
print("\nModel output:")
print(pred_bits[0, seq_len + 1:, :].cpu().int())
if __name__ == "__main__":
model, device = train()
test(model, device)
3. 运行
python mini_ntm_copy.py
你可能会看到类似结果:
ini
step=300, loss=0.6901
step=600, loss=0.6712
step=900, loss=0.5218
step=1200, loss=0.2145
step=1500, loss=0.0881
...
step=3000, loss=0.0302
Input sequence:
tensor([[1, 0, 1, 1, 0, 0, 1, 0],
[0, 1, 1, 0, 1, 0, 0, 1],
[1, 1, 0, 0, 1, 0, 1, 1],
[0, 0, 1, 1, 0, 1, 0, 0],
[1, 0, 0, 1, 1, 1, 0, 1]])
Target output:
tensor([[1, 0, 1, 1, 0, 0, 1, 0],
[0, 1, 1, 0, 1, 0, 0, 1],
[1, 1, 0, 0, 1, 0, 1, 1],
[0, 0, 1, 1, 0, 1, 0, 0],
[1, 0, 0, 1, 1, 1, 0, 1]])
Model output:
tensor([[1, 0, 1, 1, 0, 0, 1, 0],
[0, 1, 1, 0, 1, 0, 0, 1],
[1, 1, 0, 0, 1, 0, 1, 1],
[0, 0, 1, 1, 0, 1, 0, 0],
[1, 0, 0, 1, 1, 1, 0, 1]])
如果 loss 不稳定,可以先把:
ini
seq_len = 5
改成:
ini
seq_len = 3
或者把训练步数改成:
ini
steps = 6000
4. Demo 代码对应 NTM 的哪些概念?
外部 Memory
ini
memory = torch.zeros(batch_size, self.memory_slots, self.memory_dim)
对应论文里的 memory bank。
Controller
ini
self.controller = nn.LSTMCell(input_dim + memory_dim, hidden_dim)
对应 NTM 的 controller。
它接收当前输入和上一步读出的 memory 内容,然后决定下一步怎么读写。
Read Head
ini
read_w = F.softmax(self.read_key(h), dim=-1)
read_vec = torch.bmm(read_w_expanded, memory).squeeze(1)
对应 NTM 的 soft read。
它不是读取一个固定地址,而是对所有 memory slot 加权求和。
Write Head
ini
write_w = F.softmax(self.write_key(h), dim=-1)
memory = memory * (1 - write_w_expanded * erase_expanded)
memory = memory + write_w_expanded * add_expanded
对应 NTM 的 erase + add 写入机制。
5. 这个 Demo 和完整 NTM 的区别
完整 NTM 还包含:
bash
content-based addressing
interpolation gate
convolutional shift
sharpening
multiple read/write heads
而这个 demo 为了最小化,只保留了最核心的外部记忆读写机制。
所以它适合理解思想,但不适合拿来做严肃实验。更完整的 PyTorch NTM 实现可以参考开源项目,例如 loudinthecloud/pytorch-ntm,其中包含 copy task,并说明 copy task 用来测试 NTM 存储和回忆任意长信息序列的能力。
另一个实现记录也说明,copy task 的输入是随机二进制向量序列加 delimiter,目标是复制输入序列,并且输出阶段不给模型额外辅助输入。
五、总结:这篇论文真正有价值的地方
《Neural Turing Machines》的价值不在于它今天还能不能直接打败 Transformer,也不在于它是否已经工业落地。
它真正重要的地方在于提出了一种架构范式:
diff
神经网络 Controller
+
外部 Memory
+
可学习 Read / Write
+
可微寻址机制
=
可以从样例中学习简单算法的神经计算机
它试图回答一个非常关键的问题:
神经网络能不能不只是识别模式,而是学会使用记忆、操作数据结构、执行类似程序的过程?
从今天看,NTM 的很多想法已经以工程化形式出现在现代 AI 系统中:
sql
NTM Controller → LLM / Agent Planner
NTM Memory Bank → 向量数据库 / SQL / 文件系统
Read Head → Retrieval / Rerank / Context Injection
Write Head → Memory Update / Summary / State Store
Content Addressing → Embedding Search
Location Addressing → 时间线 / 流程节点 / 状态机
所以这篇论文最适合给今天做 Agent、RAG、长期记忆系统的人一个启发:
不要把智能系统设计成一个只会"当场回答"的模型,而应该把它设计成一个会读、会写、会记、会沿着状态推进的计算系统。
换句话说,NTM 的核心思想不是"给神经网络加一块内存"这么简单,而是:
让模型从被动预测器,走向主动使用工具和记忆的计算机。