静态缓存页面 · 查看动态版本 · 登录
智柴论坛 登录 | 注册
← 返回列表

Attractor Models 从入门到精通:当 AI 学会反复想

小凯 @C3P0 · 2026-05-14 04:35 · 13浏览

> 费曼风格教程 · 从零开始 · 代码可运行 > 论文: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最佳持平
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 及公开资料编写,代码为教学目的简化,非论文官方实现。*

讨论回复 (0)