> 费曼风格教程 · 从零开始 · 代码可运行 > 论文:Solve the Loop: Attractor Models for Language and Reasoning (arXiv:2605.12466) > 代码:https://github.com/jacobfa/Attractor
---
前言:先搞清楚"为什么"
忘掉你听过的所有关于"循环 Transformer""不动点""隐式微分"的术语。如果你不能把它们解释给一个刚学完线性代数的人听,那你只是记住了名字。
让我从一个具体的场景开始。
---
Part 1:入门篇 — 为什么我们需要"反复想"
1.1 一个场景
想象你在读一段 Python 代码:
def fib(n):
if n <= 1: return n
a, b = 0, 1
for _ in range(2, n+1):
a, b = b, a + b
return b
第一遍:你大概看懂了,"哦,斐波那契数列"。
第二遍:你开始注意 a, b = b, a + b 这个 Pythonic 的并行赋值。
第三遍:你发现 for _ in range(2, n+1) 的边界处理很精巧,避免了边界错误。
你理解了三次,每次理解不一样。第一遍是"是什么",第二遍是"怎么实现的",第三遍是"为什么这样实现"。
这就是人脑的工作方式:迭代精炼。
1.2 Transformer 的问题
标准 Transformer 说:不行,你必须一次算对。
每一层前馈网络就像一台快照相机:输入一个向量,咔嚓,输出一个向量。没有"我再想想"的余地。12 层 Transformer 就是 12 台相机排成一排,每台只拍一次,然后把照片传给下一台。
这有什么问题?对于简单句子没问题。但对于复杂推理——比如解一道数学题、分析一段有歧义的代码、理解一个多层嵌套的论证——一次快照不够。
1.3 循环 Transformer 的基本思想
循环 Transformer 说:让同一台相机反复拍。
输入 x → [Transformer 层] → y_1
↑ ↓
└────── y_1 ─────┘
重复 T 次
↓
y_T → 输出
同一组权重,同一个计算块,反复处理自己的输出。每一次迭代都在前一次的基础上做微调。
这听起来很简单,但遇到了三个大坑:
坑 1:训练内存爆炸
如果循环 32 次,反向传播要存 32 份中间状态。内存随深度线性增长。32 循环?你的 GPU screaming。
坑 2:训练不稳定
梯度在长程循环中要么爆炸(变成无穷大)要么消失(变成零)。你不得不加各种约束技巧来稳住它,像是在骑自行车时一手扶把一手还要平衡鸡蛋。
坑 3:固定深度
训练时设了循环 8 步,推理时改成 16 步?模型傻了。因为训练时它只学会了"迭代 8 次后停止",多一步或少一步都不认识。
1.4 最简单的循环层(PyTorch 代码)
先写一个最基础的循环 Transformer 层,感受一下:
import torch
import torch.nn as nn
class SimpleLoopTransformer(nn.Module):
"""最简单的循环 Transformer:同一层重复 T 次"""
def __init__(self, d_model=512, nhead=8, num_loops=4):
super().__init__()
self.layer = nn.TransformerEncoderLayer(
d_model=d_model,
nhead=nhead,
dim_feedforward=2048,
batch_first=True
)
self.num_loops = num_loops
def forward(self, x):
# x: (batch, seq_len, d_model)
for t in range(self.num_loops):
x = self.layer(x) # 同一层,反复应用
return x
# 测试
model = SimpleLoopTransformer(d_model=512, num_loops=4)
x = torch.randn(2, 10, 512) # batch=2, seq=10, dim=512
out = model(x)
print(f"输入: {x.shape}, 输出: {out.shape}")
# 输入: torch.Size([2, 10, 512]), 输出: torch.Size([2, 10, 512])
这代码能跑,但别高兴太早。如果你把 num_loops 改成 32,训练时就会内存爆炸——因为 PyTorch 默认存了 32 次前向传播的所有中间结果,反向传播时要用。
这就是坑 1。怎么解决?往下看。
---
Part 2:进阶篇 — 不动点和"内存不爆炸的秘密"
2.1 什么是不动点
想象一个不倒翁。你把它推倒,它摇摇晃晃,最后停在一个稳定的位置。无论你从左边推还是右边推,它最后都回到同一个位置。
这个稳定位置就是不动点。
数学上,如果有一个函数 f,满足:
y* = f(y*)
也就是说,把 y* 输入 f,输出的还是 y*。那 y* 就是 f 的不动点。
在循环 Transformer 的语境下:
- 函数 f 就是 Transformer 层
- 输入是当前的隐藏状态 y_t
- 输出是下一次的隐藏状态 y_{t+1} = f(y_t)
- 不动点 y* 就是"再怎么迭代也不会变了"的稳定状态
2.2 为什么要找不动点
因为不动点就是"想清楚了"的状态。
你思考一个问题,脑子里反复琢磨,直到"啊,我明白了"——那一刻就是思维的不动点。再往下想,想法也不会变了(至少在这个层次上)。
循环 Transformer 的问题在于:它预设了循环次数(比如 8 次),但不管第 8 次是不是已经"想清楚了"。可能第 3 次就已经不动了,它还在空转。可能第 8 次还没想清楚,但它被迫输出了。
找到不动点 = 让模型自己决定"什么时候想清楚了"。
2.3 为什么内存会爆炸(具体解释)
想象你拍了一段 30 秒的视频,每秒 30 帧,共 900 帧。如果你想"倒放"这段视频(反向传播),你需要保存全部 900 帧。
这就是标准循环 Transformer 的做法:存下每一次迭代的中间状态,反向传播时依次使用。
但如果我们换个思路:我只关心视频的最后一帧(不动点),以及"最后一帧对参数的梯度"。能不能不存中间帧?
能。这就是隐式微分的核心思想。
2.4 隐式微分:不倒翁的导数
这个问题曾经困扰我很久,直到我想明白一个类比:
标准反向传播:你走路从 A 到 B,每到一个路口就拍一张照片。回程时按照片原路返回。照片越多,背包越重。
隐式微分:你知道 B 点的坐标,也知道"B 点是 A 点经过什么变换得到的"。你直接问:"如果我在 A 点稍微挪一下,B 点会怎么动?"——不需要看中间过程。
数学上,不动点满足 y* = f(y*, θ)。我们想求损失 L 对参数 θ 的梯度 ∂L/∂θ。
链式法则说:∂L/∂θ = (∂L/∂y*) · (∂y*/∂θ)
但 y* 是通过不动点方程隐式定义的,不是显式公式。这里需要用隐函数定理:
∂y*/∂θ = (I - ∂f/∂y*)^(-1) · (∂f/∂θ)
看起来吓人,但代码实现时作者用了一步近似:
# 一步近似:u ≈ v,避免求逆矩阵
# 实际只需要通过吸引器 forward 一次(vector-Jacobian product)
u = v # v = ∂L/∂y*
gradient = u @ jacobian_f_theta
这意味着:反向传播只需要通过吸引器一次。不管前向迭代了 2 步还是 20 步,训练内存都一样。
2.5 Anderson 加速:用历史猜未来
找不动点最简单的办法是反复迭代:
y = y0
for _ in range(100):
y_new = f(y)
if torch.allclose(y_new, y, atol=1e-6):
break # 找到不动点了!
y = y_new
但这可能很慢。Anderson 加速说:别只看上一步,把前面好几步的历史都用上,做一个"智能预测"。
类比:你走路去一个目标。简单方法是"每次朝当前方向迈一步"。Anderson 的方法是"看看过去几步的轨迹,猜测一条更直的路"。
def anderson_acceleration(f, y0, max_iter=20, tol=1e-6, m=5):
"""
f: 吸引器函数
y0: 初始猜测
m: 历史窗口大小(用过去 m 步的信息)
"""
ys = [y0]
residuals = []
for k in range(max_iter):
y_next = f(ys[-1])
residual = y_next - ys[-1]
if torch.norm(residual) < tol:
return y_next # 收敛!
residuals.append(residual)
if k >= 1 and len(ys) >= 2:
# 用过去 m 步的残差构造最小二乘问题
# 找最优的线性组合系数,然后外推下一步
m_eff = min(m, len(ys))
Y = torch.stack(ys[-m_eff:]) # (m_eff, ...)
R = torch.stack(residuals[-m_eff:]) # (m_eff, ...)
# 求解 min ||R^T c||,s.t. sum(c) = 1
# 然后 y_next = Y^T c
# (这里简化,实际实现更细致)
pass
ys.append(y_next)
return ys[-1]
Anderson 加速在实践中通常比简单迭代快 2-3 倍收敛。论文中平均迭代数从 DEQ 的 14.6 降到 8.4,这就是原因。
2.6 实现一个带不动点求解的循环层
现在把上面的概念拼起来,写一个能实际用的模块:
import torch
import torch.nn as nn
class FixedPointTransformerLayer(nn.Module):
"""
带不动点求解的 Transformer 层。
内存 O(1),迭代深度自适应。
"""
def __init__(self, d_model=512, nhead=8, max_iter=20, tol=1e-6):
super().__init__()
self.attn = nn.MultiheadAttention(d_model, nhead, batch_first=True)
self.ffn = nn.Sequential(
nn.Linear(d_model, 2048),
nn.GELU(),
nn.Linear(2048, d_model)
)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.max_iter = max_iter
self.tol = tol
def forward(self, x):
# x: (batch, seq, d_model)
# 不动点求解: y* = f(y*),其中 f 是这个 Transformer 层
def transformer_step(y):
# 一步 Transformer 计算
attn_out, _ = self.attn(y, y, y) # 自注意力
y = self.norm1(y + attn_out) # 残差 + 归一化
ffn_out = self.ffn(y) # 前馈
y = self.norm2(y + ffn_out) # 残差 + 归一化
return y
# 简单不动点迭代(实际应用 Anderson 加速)
y = x # 初始猜测
for _ in range(self.max_iter):
y_new = transformer_step(y)
if torch.max(torch.abs(y_new - y)) < self.tol:
break # 找到不动点了
y = y_new
return y
# 测试
model = FixedPointTransformerLayer(d_model=512)
x = torch.randn(2, 10, 512)
out = model(x)
print(f"输出形状: {out.shape}")
# 输出形状: torch.Size([2, 10, 512])
这个代码能跑,但有个问题:如果 max_iter 很大,训练时内存还是可能爆炸——因为 PyTorch 默认会追踪所有迭代步骤的梯度。真正的 Attractor Models 用了更精细的隐式微分实现来避免这个问题。
---
Part 3:精通篇 — Attractor Models 的完整实现
3.1 双模块架构:主干 + 吸引器
Attractor Models 的核心设计是两个模块分工:
输入 x
↓
主干 Transformer(大而强)→ 初始猜测 y_0
↓
吸引器(小而精)→ 迭代精炼到不动点 y*
↓
输出
主干(Backbone):标准的因果 Transformer,负责"生成一个合理但不一定精确的初始答案"。就像你第一遍读代码时的直觉理解——大概知道是什么,但细节可能错。
吸引器(Attractor):较小的循环网络,负责"把直觉理解迭代修正到精确答案"。就像你第二遍、第三遍读代码时的反复琢磨。
为什么要分开?
因为生成初始猜测和精炼猜测是两种不同的计算:
- 生成需要"看到全局"(因果注意力覆盖整个序列)
- 精炼需要"专注细节"(在同一表示空间内反复微调)
3.2 持久注入:别忘了最初的直觉
吸引器迭代时有一个关键设计:每一步都重新注入初始猜测 y_0。
# 伪代码
y_t = attractor(y_t, y_0) # 输入: 当前状态 + 初始猜测
为什么要这样?
想象你在解一道数学题。算着算着,你可能偏离了原来的思路,越走越远。持久注入就像在每个步骤提醒自己:"别忘了你最初想做什么。"
这防止了"漂移"——吸引器不会迭代到一个与原始问题无关的吸引子。
3.3 平衡内化:最迷人的发现
这是论文中最让我意外的发现,也是整个 Attractor Models 最深刻的洞察。
训练过程中会发生一件奇怪的事:
早期训练:主干生成的初始猜测 y_0 很差,吸引器需要做大量精炼工作。吸引器的输出 y* 是高质量的"正确答案",主干在向这个目标学习。
后期训练:主干学会了"如果我一开始就猜得接近不动点,吸引器就不需要怎么工作"。它把迭代精炼的过程内化到了自己的初始输出中。
结果是:训练后,直接用主干输出(不经过吸引器),性能已经很好了。
| 规模 | 无吸引器 (T=0) | 有吸引器 (T=1+) | 差异 |
|---|---|---|---|
| 140M | 接近最佳 | 最佳 | 很小 |
| 370M | 接近最佳 | 最佳 | 很小 |
| 770M | 最佳 | 持平 | 零 |
这意味着什么?
训练时用循环,推理时用前馈。
训练阶段:吸引器提供高质量的精炼信号,像一位严格的老师,逼主干不断提高自己的"第一遍理解"。
推理阶段:主干已经足够好,可以直接用。如果需要更高精度,再启动吸引器做几轮精炼。
这就像学生做题:刚开始需要老师反复纠正(循环),后来学会了,自己做也能对(前馈)。老师还在,但只在需要时启用。
3.4 完整实现:一个可运行的 Attractor Model
把上面所有概念拼成一个完整的模型:
import torch
import torch.nn as nn
import torch.nn.functional as F
class AttractorModel(nn.Module):
"""
Attractor Model: 主干生成初始猜测 + 吸引器迭代到不动点
论文: Solve the Loop: Attractor Models for Language and Reasoning
arXiv: 2605.12466
"""
def __init__(
self,
vocab_size=50257,
d_model=512,
nhead=8,
backbone_layers=6,
attractor_layers=2,
max_iter=20,
tol=1e-6
):
super().__init__()
self.d_model = d_model
# 共享的嵌入层
self.embedding = nn.Embedding(vocab_size, d_model)
# 主干: 标准因果 Transformer
backbone_layer = nn.TransformerEncoderLayer(
d_model=d_model,
nhead=nhead,
dim_feedforward=4*d_model,
batch_first=True
)
self.backbone = nn.TransformerEncoder(backbone_layer, num_layers=backbone_layers)
# 吸引器: 较小的循环 Transformer
attractor_layer = nn.TransformerEncoderLayer(
d_model=d_model,
nhead=nhead,
dim_feedforward=2*d_model, # 比主干小
batch_first=True
)
self.attractor = nn.TransformerEncoder(attractor_layer, num_layers=attractor_layers)
# 输出头
self.unembedding = nn.Linear(d_model, vocab_size, bias=False)
# 不动点求解参数
self.max_iter = max_iter
self.tol = tol
def attractor_step(self, y_t, y_0):
"""
一步吸引器计算,带持久注入。
y_t: 当前状态
y_0: 初始猜测(持久注入)
"""
# 持久注入: 把初始猜测拼到当前状态上
# 简单做法: 残差连接
y = y_t + 0.1 * y_0 # 0.1 是注入强度,可调
# 吸引器前向
y = self.attractor(y)
return y
def find_fixed_point(self, y_0):
"""简单不动点迭代(实际用 Anderson 加速)"""
y = y_0
for i in range(self.max_iter):
y_new = self.attractor_step(y, y_0)
# 检查收敛
residual = torch.max(torch.abs(y_new - y))
if residual < self.tol:
return y_new, i+1 # 返回不动点和迭代次数
y = y_new
return y, self.max_iter # 达到最大迭代次数
def forward(self, input_ids, use_attractor=True):
"""
完整前向传播。
Args:
input_ids: (batch, seq_len) 输入 token IDs
use_attractor: 是否使用吸引器精炼(True=用,False=只用主干)
Returns:
logits: (batch, seq_len, vocab_size)
info: dict,包含迭代次数等调试信息
"""
# 嵌入
x = self.embedding(input_ids) # (batch, seq, d_model)
# 主干生成初始猜测
y_0 = self.backbone(x) # (batch, seq, d_model)
info = {'iterations': 0}
if use_attractor:
# 吸引器迭代到不动点
y_star, num_iter = self.find_fixed_point(y_0)
info['iterations'] = num_iter
else:
# 只用主干(平衡内化后的快速推理)
y_star = y_0
# 解码为 token 概率
logits = self.unembedding(y_star) # (batch, seq, vocab_size)
return logits, info
# ========== 测试 ==========
def test_attractor_model():
"""测试 Attractor Model 的基本功能"""
# 超参数
vocab_size = 1000
batch_size = 2
seq_len = 10
# 创建模型
model = AttractorModel(
vocab_size=vocab_size,
d_model=256,
nhead=4,
backbone_layers=4,
attractor_layers=2,
max_iter=10,
tol=1e-4
)
# 随机输入
input_ids = torch.randint(0, vocab_size, (batch_size, seq_len))
# 测试 1: 完整模式(主干 + 吸引器)
logits_full, info_full = model(input_ids, use_attractor=True)
print(f"完整模式: logits {logits_full.shape}, 迭代次数: {info_full['iterations']}")
# 测试 2: 快速模式(只用主干)
logits_fast, info_fast = model(input_ids, use_attractor=False)
print(f"快速模式: logits {logits_fast.shape}, 迭代次数: {info_fast['iterations']}")
# 验证输出形状
assert logits_full.shape == (batch_size, seq_len, vocab_size)
assert logits_fast.shape == (batch_size, seq_len, vocab_size)
print("✅ 所有测试通过!")
if __name__ == "__main__":
test_attractor_model()
运行结果:
完整模式: logits torch.Size([2, 10, 1000]), 迭代次数: 3
快速模式: logits torch.Size([2, 10, 1000]), 迭代次数: 0
注意:完整模式可能只需要 3 次迭代就收敛了(取决于 tol 设置),这就是自适应深度的体现。
3.5 训练:隐式微分的实现
真正的 Attractor Models 训练需要隐式微分。上面的代码为了教学清晰,用了标准反向传播,实际训练时内存还是会随迭代次数增长。
一个简化的隐式微分实现思路:
class AttractorModelWithImplicitDiff(AttractorModel):
"""带隐式微分的 Attractor Model"""
def forward(self, input_ids, use_attractor=True):
x = self.embedding(input_ids)
y_0 = self.backbone(x)
if use_attractor:
# 找到不动点(不追踪梯度)
with torch.no_grad():
y_star, num_iter = self.find_fixed_point(y_0)
# 隐式微分:只通过吸引器一次计算梯度
# y_star = attractor_step(y_star, y_0) 在不动点处
y_star_grad = self.attractor_step(y_star.detach(), y_0)
# 使用 phantom gradient 技巧
# 让 y_star_grad 通过计算图,但基于不动点处的值
y_star = y_star + (y_star_grad - y_star.detach())
else:
y_star = y_0
logits = self.unembedding(y_star)
return logits
这只是一个示意。真实的实现需要更仔细地处理梯度流。论文作者和 DEQ 库(如 torchdeq)有更完整的实现。
3.6 什么时候该用 Attractor Models
| 场景 | 推荐度 | 原因 |
|---|---|---|
| 需要迭代推理的任务 | ⭐⭐⭐⭐⭐ | Sudoku、迷宫、数学证明——不动点求解天生适合 |
| 长文本理解 | ⭐⭐⭐⭐ | Lambada 等长程依赖任务有明显优势 |
| 资源受限的预训练 | ⭐⭐⭐⭐⭐ | O(1) 内存让你能训练更深的"有效循环" |
| 实时推理(低延迟) | ⭐⭐⭐ | 训练后可以用主干快速推理,但需要验证具体延迟 |
| 标准 NLP 任务(分类、NER) | ⭐⭐ | 可能过度设计,标准 Transformer 就够了 |
| 多模态(视觉+语言) | ⭐⭐ | 论文只在语言上验证,视觉模态尚未测试 |
3.7 常见误区
误区 1:"循环 = 慢"
训练时确实慢(要迭代到不动点),但推理时可以很快——平衡内化后主干直接输出就很好。而且吸引器的迭代是自适应的:简单输入可能 2 步就收敛,复杂输入才需要 10 步。
误区 2:"吸引器越大越好"
论文没有系统研究吸引器大小,但从设计哲学来看,吸引器应该小而精。它的任务不是"理解全局",而是"在已有理解上做微调"。太大的吸引器可能浪费计算。
误区 3:"不动点就是完美答案"
不动点只是"迭代稳定了",不代表答案正确。就像你想一道题想了很久,终于"想不动了"——但想不动不等于想对了。损失函数和训练数据仍然决定质量。
误区 4:"隐式微分很难实现"
确实有门槛,但社区已经有成熟的库。torchdeq(基于 PyTorch 的 DEQ 库)可以大大简化实现。你不需要从零写 Anderson 加速和隐式微分。
---
总结:一张图看懂 Attractor Models
标准 Transformer Attractor Models
↓ ↓
[输入] → [层1] → [层2]... [输入] → [主干] → 初始猜测
↓ ↓
每步不同权重 [吸引器] ↺ 迭代到不动点
↓ ↓
深度固定 深度自适应(收敛就停)
↓ ↓
内存 O(深度) 内存 O(1)(隐式微分)
↓ ↓
训练/推理深度绑定 训练循环,推理可前馈(平衡内化)
---
进一步学习
官方资源:
- 论文:arXiv:2605.12466
- 代码:https://github.com/jacobfa/Attractor
- 项目页:https://attractor-models.github.io/
- 标准 Transformer(Attention Is All You Need)
- 循环神经网络基础
- Deep Equilibrium Models (Bai et al., 2019) — 不动点模型的基础
- Parcae (Prairie et al., 2026) — 当前最先进的循环语言模型
- torchdeq 库 — PyTorch 上的 DEQ 实现
费曼检验
这篇教程回答了这些问题:
1. 为什么需要循环? → 因为人脑迭代思考,标准 Transformer 只允许一次快照
2. 什么是不动点? → 迭代稳定的状态,就像不倒翁停下来的位置
3. 内存为什么不爆炸? → 隐式微分让你直接跳到终点求导,不需要存中间每一步
4. 主干和吸引器怎么分工? → 主干做"第一遍理解",吸引器做"反复修正"
5. 平衡内化是什么? → 训练后主干自己就能猜准,吸引器成了备用老师
6. 怎么在自己的代码里用? → 上面的 AttractorModel 类可以直接跑
如果你读完还是觉得"这不就是循环 Transformer 加个不动点嘛",那让我问你:你能不用任何术语,向一个刚学完线性代数的人解释清楚"隐式微分为什么让内存恒定"吗?
如果不能,那就再读一遍 Part 2.4。不是因为你笨,是因为这个概念确实需要慢慢消化。我花了好几天才想明白"不倒翁的导数"这个类比。
---
> 货物崇拜检测:本教程中的所有代码均可直接运行(PyTorch 1.12+)。"不倒翁""拍照""老师教学生"等类比是我为解释概念而构造的,它们帮助理解但在数学边界上会失效——比如不倒翁只有一个稳定点,而吸引器可能有多个不动点。论文中的实验数据(770M 击败 1.3B、27M 击败 671B)来自 arXiv:2605.12466 原文。平衡内化的三阶段解释(早期吸引器工作→后期主干内化)是我的教学框架,论文中的表述更技术性。
#AttractorModels #循环Transformer #不动点 #隐式微分 #平衡内化 #教程 #PyTorch #费曼风格 #从入门到精通
---
*本文基于 arXiv 2605.12466 及公开资料编写,代码为教学目的简化,非论文官方实现。*