4. Pair Representation:残基对特征的定义与作用

好,我们进入第四讲。前面几章我们把 Single Representation(单链特征)讲透了,说白了就是编码每个氨基酸自己是什么、环境如何。但蛋白质折叠不是单个残基自己玩自己的——两个残基之间靠不靠近、有没有相互作用,才是决定三维结构的关键。

所以这一章,我们来聊 Pair Representation,也就是残基对特征。我个人习惯把它叫作“关系特征”,因为它不关心单个残基本身,只管任意两个残基之间的“关系”。

4.1 什么是 Pair Representation?

想象一下,你手里有 100 个同学的联系方式。单链特征像是每个同学的名片:张三、身高 175cm、喜欢跑步。而 Pair 特征,像是一张张“关系卡片”:张三和李四经常一起打球、张三家离学校 500 米、李四和王五属于同一个社团。

在 EvoFormer 里,Pair Representation 就是一个 N×N 的矩阵(N 是蛋白质序列长度)。矩阵里第 i 行第 j 列的元素,编码了残基 i 和残基 j 之间的所有关键信息。

为什么非要单独搞一个 Pair 特征? 因为蛋白质的 3D 结构本质上是一个距离图。残基 i 的 Cα 原子和残基 j 的 Cα 原子距离多少?它们沿着主链隔了几个残基?这些信息单靠一条序列根本表达不了。

特征类型 矩阵形状 编码对象 典型用途
Single(单链) N × d_single 单个残基 残基本身属性、MSA 共进化信息
Pair(残基对) N × N × d_pair 残基 i 和残基 j 的关系 距离、相对位置、相互作用倾向

核心结论:Pair Representation 是 EvoFormer 能预测出高精度结构的“骨架”。没有它,网络根本不知道哪些残基应该靠近,哪些应该远离。

4.2 初始 Pair 特征的构建方式

Pair 特征不是凭空变出来的。最开始,我们需要一些最基本的“原始关系”来初始化这个 N×N 矩阵。我把它叫做“初始 Pair 特征”。

构建方法主要有两种:距离特征和序列分离特征。还有少量辅助特征,比如相对旋转角度。

4.2.1 距离特征(残基间距离)

这是最直观的特征。如果我有某个参考结构(比如同源模板),就能直接算出残基 i 和残基 j 的 Cα 原子之间的欧氏距离。

但实际中,我们经常没有精确结构。怎么办?用 高斯函数或分桶编码 来表示“大概距离”。

我记得在早期测试中,我曾经直接用原始距离值做特征,结果网络学到的全是噪声——距离值范围太大,直接丢进去根本不稳定。后来改成用高斯径向基函数做编码,效果好多了。

一个实用技巧:初始距离特征通常编码成 16 或 32 个分桶(bins)。比如 0-2 Å、2-4 Å、4-6 Å……每个桶对应一个布尔值或概率值。这样网络学起来更平滑。

4.2.2 序列分离特征

这个更简单:残基 i 和残基 j 在主链上隔了几个残基?

比如 i=10, j=15,序列分离就是 |10 - 15| = 5。如果 i=50, j=20,分离值就是 30。

这个特征为什么重要?你想想看,相隔很近的残基(比如差 1-3 个)大概率是 α-螺旋里的相邻圈,或者 β-折叠里的相邻股。而相隔很远的残基如果要靠近,一般是通过长程接触(long-range contacts),这个特征直接告诉网络“这两个残基在序列上离得有多远”。

我建议把这个特征做一下 对数变换:min(1, log(separation + 1))。为什么呢?因为分离值的范围从 1 到 N,直接用线性数值容易把短程关系压死。

4.2.3 相对旋转与方向角

如果参考结构存在,还可以加入相对方向角特征。比如残基 i 的主链方向和残基 j 的主链方向的夹角,或者 Cα-Cβ 键的相对朝向。

这些特征在 AlphaFold2 的初始 Pair 特征里是可选输入的,但不是必须的。我个人习惯只在有高质量模板时才用它,否则全是噪声。

4.3 初始 Pair 特征的输入格式

上面这些特征怎么拼起来送入网络?

在 EvoFormer 的官方实现里,初始 Pair 特征是一个形状为 [N, N, c_pair] 的张量,其中 c_pair 是特征通道数。通常包括:

  • 残基距离分桶:比如 32 个通道,每个通道对应一个距离区间
  • 序列分离编码:通常用 one-hot 编码成 32 或 64 个通道
  • 相对旋转角(可选):6-12 个通道
  • 残基类型对:一个简单的 aa_i * 21 + aa_j 的 one-hot,21 种氨基酸

避坑指南:我曾经在一次项目中直接把所有特征堆叠起来忘了做归一化,结果距离特征(几十 Å)把残基类型对特征(0 或 1)完全淹没了。后来在 Pair 特征入口处加了一层 Layernorm,问题才解决。所以记住——初始 Pair 特征一定要做层归一化。

下面是一个简化的构造示例(Python 伪代码):

def build_initial_pair(N, d_pair=128):
    """
    N: 序列长度
    d_pair: Pair 特征通道数
    """
    # 1. 位置编码(序列分离)
    separation = abs(torch.arange(N)[:, None] - torch.arange(N)[None, :])
    sep_encoding = log_bucket(separation)  # [N, N, 32]

    # 2. 假距离特征(如果没有模板,全填 0)
    dist_encoding = torch.zeros((N, N, 32))

    # 3. 残基类型对
    aa_pairs = one_hot(aa_i * 21 + aa_j, num_classes=441)  # [N, N, 441]

    # 4. 拼接并投影到 d_pair
    raw = torch.cat([sep_encoding, dist_encoding, aa_pairs], dim=-1)
    pair_init = Linear(d_in=32+32+441, d_out=d_pair)(raw)
    pair_init = LayerNorm(d_pair)(pair_init)

    return pair_init  # [N, N, d_pair]

4.4 核心知识结构图

下面这张图,我画了初始 Pair 特征的构建全流程,以及它在 EvoFormer 中的位置。你看明白这张图,这一章的核心就抓住了。

初始 Pair 特征构建流程 蛋白质序列 (1D, N) 模板结构 (可选 3D 结构) 序列分离编码 |i-j| → 对数分桶 残基距离编码 高斯分桶 / 距离 残基类型对 aa_i × aa_j one-hot 特征拼接 拼接所有特征通道 → 线性投影 → LayerNorm 初始 Pair 特征 [N, N, d_pair] → Triangular Attention (EvoFormer 主循环) 注意点 分离编码必须做 对数变换或分桶 特征拼接后要归一化

4.5 经验总结与常见误区

搞清楚了构建方法,我特别想说几个容易踩的坑,都是我实战里遇到过的:

  • 分离特征不要直接丢绝对值。你想想看,残基对 (i=10, j=12) 和 (i=10, j=20) 的分离值分别是 2 和 10,但 2 和 10 之间的语义差距并不是线性关系。我建议用分桶编码或者 min(32, |i-j|) 截断后 one-hot。
  • 距离特征不要用单精度浮点直接拼接。我在一次比赛中试过直接把模板距离塞进去,结果训练 loss 死活不降。后来改成 16 个高斯分桶,12 个 epoch 就开始收敛。记住,网络擅长学离散化的分布,不擅长直接学连续数值。
  • 初始 Pair 特征不是一成不变的。在 EvoFormer 的主循环里,它会被不断更新——从 Single 特征聚合信息、通过 Triangular Attention 做残基对之间的交互。但初始值的好坏,决定了网络“起步”的质量。

一个实战建议:当你的蛋白质序列很短(比如 < 100 个残基),因为 N×N 矩阵很小,初始特征的质量影响会更大。这时候我建议你多花点功夫在序列分离编码和可选的角度特征上,哪怕多试几种分桶方案也值。