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 里,所有计算层(自注意力、前馈网络)对输入维度一致,这样模型参数规模更可控。
我曾经试过拼接的方式,结果模型参数翻倍,训练速度明显下降,性能提升却微乎其微。
四、完整流程图
下面我用一张图把整个过程串起来。你看一遍就清楚了。
五、避坑指南与实用建议
这部分是我在实际工程中踩过的坑,拿出来分享一下。
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 或过大值。嗯,这一步做好了,后面的注意力机制才能正常运转。