3. AlphaFold2 原理简介:Evoformer、结构模块、回收机制

说实话,刚接触 AlphaFold2 那会儿,我也觉得它像个黑盒子——输入序列,输出结构,中间发生了什么完全不清楚。后来啃了论文、调过几次模型,慢慢才摸清它的三个核心部件:Evoformer、结构模块、还有那个让人又爱又恨的回收机制。今天咱们就把这三个东西拆开看看。

3.1 整体流程:一副骨架图

先上一张我习惯画在纸上的草图。AlphaFold2 的推理流程大概长这样:

AlphaFold2 核心推理流程 输入序列 MSA 搜索 Evoformer 48 层迭代 MSA 表示 Pair 表示 相互更新 (轴向注意力 + 三角乘法) 结构模块 8 层 IPA 框架 → 扭转角 侧链构建 回收 3 次 最终结构 回收时 Evoformer 会重新运行 但参数是共享的

我个人习惯在讲这块时先把回收机制画成虚线环,不然学员容易忘记模型会反复走好几趟。你想想看,AlphaFold2 不是一次性出结果的,它要“改作业”三遍。

3.2 Evoformer:信息的“搅拌机”

Evoformer 是整个模型的信息处理核心。它同时维护两套表示:

  • MSA 表示(Multiple Sequence Alignment Representation)—— 形状为 seq_len × N_seq × d,记录每条同源序列在每个位置上出现的概率
  • Pair 表示(Pair Representation)—— 形状为 seq_len × seq_len × d,记录任意两个残基之间的相互作用

这两套表示在 48 层 Evoformer 里交替更新,互相“喂信息”。

关键操作有三个:

  1. 轴向注意力:沿着 MSA 的行和列分别做自注意力,捕捉不同序列之间的进化关系
  2. 三角乘法更新:用 Pair 表示自身做三角闭合运算,确保距离和角度的一致性——说白了就是让残基对之间“互不打架”
  3. Pair→MSA 投影:把 Pair 的全局信息注入到 MSA 中,帮助模型理解“这两条序列为什么看起来像”

我在项目中遇到过一个问题:当同源序列特别少(比如孤儿抗体 CDR-H3 区域),Evoformer 的 MSA 分支几乎得不到有效信息,结构预测质量会断崖式下跌。后来我们用了基于语言模型的伪 MSA 生成来补数据,效果才稳定下来。

来个配置示例感受一下 Evoformer 的参数量级(AlphaFold2 官方 OpenFold 实现中的默认配置):

model_config = {
    "heads": 8,
    "pair_heads": 4,
    "c_m": 256,          # MSA 表示维度
    "c_z": 128,          # Pair 表示维度
    "c_t": 64,           # 模板特征维度
    "evoformer_num_blocks": 48,
    "extra_msa": {       # 额外 MSA 的处理
        "num_blocks": 4,
        "c_m": 64,
        "c_z": 64
    }
}

你看,48 个 block 每个都重复着“MSA 注意力→Pair 注意力→三角乘法→投影”,像个大型搅拌机。嗯,这里要注意:Evoformer 里没有残差连接是跑不动的,每一层后都有 LayerNorm + dropout。

3.3 结构模块:从抽象表示到原子坐标

Evoformer 产出的 Pair 表示和 MSA 表示,经过一个“单表示”投影(取 MSA 行平均值)后,就轮到结构模块登场了。

结构模块只有 8 层,但每层都包含一个叫 IPA(Invariant Point Attention) 的核心操作。啥是 IPA?说白了就是“考虑几何不变性的注意力”。

常规 Transformer 注意力只关心“序列位置 i 和 j 的特征有多像”,但 IPA 额外加入了一个条件:两个残基在 3D 空间里的实际距离也要参与注意力权重计算。模型会反复猜测当前的结构,然后用这个猜测来调整下一步的注意力。

我曾经掉过的一个坑:在复现时以为 IPA 里的“点”直接用 Ca 坐标就行,结果发现 AlphaFold2 用的是“刚体框架”——每个残基带一个旋转矩阵和一个平移向量。坐标只是结果之一,框架才是内部表示。后来我把中间变量打印出来看才搞明白。

结构模块的输出是一套残基框架(N、Ca、C 三个主链原子 + 侧链扭转角)。可以用这一段 Python 伪代码概括:

def structure_module(single_repr, pair_repr, recycling_idx):
    # 初始化框架为“伸直”状态
    frames = init_frames(batch_size, seq_len)
    for layer in range(8):
        attn_weights = IPA(frames, single_repr, pair_repr)
        frame_updates = predict_frame_update(attn_weights, single_repr)
        frames = compose_frames(frames, frame_updates)
        single_repr = single_repr + mlp(single_repr)
    angles = predict_backbone_angles(frames)
    return frames, angles

注意:结构模块的每一层都 有权重共享?不对,这里有个容易混淆的点——结构模块内部 8 层的参数是独立的,但回收循环中多次调用结构模块时参数是共享的。

3.4 回收机制:迭代“改作业”

这是 AlphaFold2 里最容易被人忽略的设计。模型不是跑一次 Evoformer + 结构模块就完事了,它要重复跑 3 次(回收次数 = 3)。

具体流程:

  1. 第 1 次:Evoformer 从零开始,结构模块输出一个粗糙的结构
  2. 第 2 次:把上一轮的结构特征(比如残基距离、方向)拼回到 Pair 表示中,重新过 Evoformer
  3. 第 3 次:同样的方法,再次优化

为什么这样做?你想想看,第一次跑的时候模型对结构完全没有概念,只能靠序列保守性和进化信息猜一个粗骨架。第二轮有了“上一轮猜的结构”作为 bias,Evoformer 就可以聚焦在那些不合理的区域做修正。说白了,这是把“迭代细化”做进了端到端训练里。

回收次数 Pair 表示输入来源 典型 pLDDT 提升 备注
0(无回收) 仅 MSK + 模板 基准 -5~10 仅用于消融实验
1 随机初始化结构 ≈ 75 大部分抗体 CDR 可达及格线
2 第 1 轮结构 +3~8 主链骨架明显改善
3(默认) 第 2 轮结构 +1~3 侧链 rotamer 更准确

回收带来的计算开销是线性的——跑 3 次就是 3 倍时间。但你别想省这一步。我曾经试过把回收次数减到 1,结果在一个抗体-抗原复合物预测上 RMSD 从 1.2Å 飙到了 4.5Å。回收机制对 界面残基 的优化尤其关键。

避坑指南:回收时的结构特征怎么拼回去?官方的做法是计算“预测距离分布”和“预测方向分布”,然后对 Pair 表示做残差加法。如果你自己实现,别忘了把回收特征的维度对齐到 c_z。

3.5 三个组件如何协同工作

简单总结一下三者的职责:

  • Evoformer:负责理解“这些序列是怎么进化来的”,输出稳定的 Pair 和 MSA 表示
  • 结构模块:把这些抽象表示“翻译”成具体的 3D 框架和角度
  • 回收机制:让模型有机会“回头看”,利用刚猜出的结构再次修正 Pair 表示,形成一个闭环

用句大白话说:Evoformer 是读题、结构模块是答题、回收机制是检查答案后重做。三者缺一不可。

我个人建议,如果你只是在做抗体设计中应用 AlphaFold2(比如用 ColabFold 跑预测),不需要自己实现回收逻辑。但如果你想微调模型或做结构感知的 MSA 嵌入,理解回收机制能帮你少走很多弯路。