EvoFormer模块特征机制学习讲义 - 第3节

一、序列编码:从原始序列到数字信号

我们开始处理 MSA 之前,面临的第一个问题其实很朴素:氨基酸序列怎么喂给神经网络?

不能用字母直接跑。模型只认得数字和向量。

所以这一步,我们得把每个氨基酸残基映射成一个数字 ID,然后转成计算机能理解的嵌入向量。这个过程我习惯叫它「序列编码 + 嵌入查找」。

1.1 词汇表构建

标准的蛋白质序列有 20 种常见氨基酸,加上一些特殊信号,比如 gap(-)、未知氨基酸(X)、起始符、终止符,甚至 mask 标记。

在 AlphaFold 的 EvoFormer 实现里,词汇表大致长这样:

20 种标准氨基酸: A、R、N、D、C、Q、E、G、H、I、L、K、M、F、P、S、T、W、Y、V
特殊标记: - (gap)、X (unknown)、、、、

每个 token 对应一个唯一的整数索引。我举个例子:

A → 0
R → 1
…
V → 19
- → 20
X → 21
<cls> → 22

用这样的方式,一条长 L 的序列就变成了一个长度为 L 的整数列表。说白了,就是查字典。

1.2 嵌入查找(Embedding Lookup)

整数 ID 还不够。它们只是离散的编号,没有语义距离。比如「A」和「R」在生化性质上差异很大,但整数 0 和 1 在数值上只差 1,这不符合直觉。

所以我需要把每个整数 ID 映射到一个高维连续向量上。这就是嵌入层干的事。

在 PyTorch 里它就是一个 nn.Embedding:

self.residue_embedding = nn.Embedding(
    num_embeddings=vocab_size,    # 词汇表大小
    embedding_dim=c_m             # 嵌入维度
)

关键参数:vocab_size 是词汇表大小,embedding_dim 代表每个 token 被映射到的向量维度。在 EvoFormer 中,默认 c_m = 256。

这样一来,一条 MSA 中每个位置上的每个序列 token,都被替换为一个 256 维的浮点向量。

二、位置编码:让模型知道「谁在哪儿」

序列编码做完后,有个问题摆在我面前:Transformer 本身没有顺序的概念。它只看到一堆向量,不知道哪些向量是第一个残基,哪些是第二个。

你想想看,如果去掉位置信息,模型会把「MET」和「TEM」当成同一回事。这在蛋白质结构预测里是致命的。

2.1 残基位置编码

每个残基在它所属的序列里有个索引。我需要把这个索引也变成一个向量,加到残基的嵌入上。

在 EvoFormer 中,这个位置编码用的是相对位置编码,而不是传统的绝对正弦波编码。

我记得第一次在项目里试绝对编码时,效果并不理想。后来换成相对编码,收敛速度明显快了。为什么?因为蛋白质的功能域常常只关心残基之间的相对距离,而不是它们在序列中的绝对编号。

实现起来大致是:

# 计算相对位置偏移
# positions: [seq_len, seq_len] 的矩阵
# 每个元素 [i][j] = j - i
relative_positions = positions.unsqueeze(0) - positions.unsqueeze(1)
# 截断到 [-k, k] 区间
relative_positions = torch.clamp(relative_positions, -max_rel_pos, max_rel_pos)
# 偏移到 [0, 2k] 范围
relative_positions = relative_positions + max_rel_pos

然后把截断后的偏移值当作索引,再查一次嵌入表,得到每个残基对的相对位置偏置。

我的个人习惯:一般把 max_rel_pos 设为 32。超过这个范围的残基对,模型不再区分它们谁更远。这在处理长序列时能有效减少参数量。

2.2 序列索引位置编码(Pair-wise 层用到)

除了残基位置,MSA 里还有一个维度:序列的索引。同一个 MSA 中有很多条序列,它们之间也有先后关系(比如同源性的差异)。

不过在 MSA Representation 的构建阶段,序列之间的顺序一般不包含语义信息。常用的做法是只对「残基在序列中的位置」做编码,而序列索引位置不做特殊处理——让它跟着注意力机制自己去学。

三、嵌入的最终整合

有了残基嵌入和位置编码,下一步就是把它们加起来,形成 MSA 的初始表示。

对于 MSA 中的每一条序列,形状是 [N_seq, N_res](N_seq:序列条数,N_res:残基长度)。编码后变成 [N_seq, N_res, c_m]。

代码逻辑大概是这样:

# msa_input: [N_seq, N_res] 整数序列
# 残基嵌入
residue_emb = self.residue_embedding(msa_input)  # [N_seq, N_res, c_m]
# 位置编码
pos_encoding = self.position_encoding(N_res)       # [N_res, c_m]
# 广播相加
msa_rep = residue_emb + pos_encoding.unsqueeze(0) # [N_seq, N_res, c_m]

3.1 为什么要相加而不是拼接?

好问题。相加可以保留维度不变,不会因为拼接而让每个 token 的向量长度翻倍。在 Transformer 里,所有计算层(自注意力、前馈网络)对输入维度一致,这样模型参数规模更可控。

我曾经试过拼接的方式,结果模型参数翻倍,训练速度明显下降,性能提升却微乎其微。

四、完整流程图

下面我用一张图把整个过程串起来。你看一遍就清楚了。

MSA Representation 构建流程 原始 MSA 序列 词汇表映射 (Token ID) 残基嵌入查找 (c_m=256) 残基位置编码 Embedding ⊕ Position [N_seq, N_res, c_m] 输入格式 N_seq 条序列 × N_res 个残基 位置编码方式 相对位置编码,截断 ±32

五、避坑指南与实用建议

这部分是我在实际工程中踩过的坑,拿出来分享一下。

5.1 词汇表统一性

我曾经在训练和推理时用了不一致的词汇表,少了几个 gap 标记,结果推理出来的结构偏差大得离谱。后来我在代码里加了一个词汇表校验脚本:

def check_vocab_consistency(training_vocab, inference_vocab):
    if training_vocab != inference_vocab:
        raise RuntimeError("词汇表不一致!请检查特殊标记映射。")

绝对不要在训练和推理之间修改词汇表的索引顺序。哪怕只加一个 token,之前训练的嵌入层权重就全部错位了。

5.2 嵌入维度的选择

c_m = 256 是 AlphaFold 的默认值。但如果你处理的是小型蛋白质(比如 < 150 残基),我建议可以降到 128,训练速度和内存占用都会明显优化,精度几乎不受影响。

在项目里我试过从 256 降到 128,MSA 数量在 1000 条以下的场景下,F1 指标基本持平,但训练时间缩短了约 30%。

5.3 位置编码截断范围

max_rel_pos 设得太大,模型会学到很多稀有的远距离对,容易过拟合。设得太小,远距离残基对之间的信息就全丢了。我个人建议保守点,初始用 32,如果序列长度很短(比如 100),可以调到 16。

六、小结

MSA Representation 的构建,说白了就是两步:查字典转数字 → 查嵌入变向量 → 加上位置信息。看起来简单,但每一步的参数和选择都会直接影响后续 EvoFormer 模块的输入质量。

在代码里调试这块时,我建议你打印一下 [N_seq, N_res, c_m] 的张量形状和数值范围,确保没有 NaN 或过大值。嗯,这一步做好了,后面的注意力机制才能正常运转。