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 中的位置。你看明白这张图,这一章的核心就抓住了。
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 矩阵很小,初始特征的质量影响会更大。这时候我建议你多花点功夫在序列分离编码和可选的角度特征上,哪怕多试几种分桶方案也值。