4. Transformer核心机制:自注意力、多头注意力、前馈网络

好,咱们正式进入Transformer的核心。我最早接触Transformer是在NLP任务里,后来发现肽段谱图匹配也能用,效果还出奇好。说白了,Transformer就是三个模块拼起来:自注意力、多头注意力、前馈网络。你想想看,没有循环,没有卷积,全靠注意力机制建模序列关系。

核心思路: 肽段序列(比如12个氨基酸)先转成向量,然后自注意力让每个位置看到所有其他位置,多头注意力并行看不同子空间,前馈网络做非线性变换。最终拿编码器输出去做谱图匹配。

4.1 自注意力 (Self-Attention)

自注意力的目标是计算序列中每个token与其他token的关联权重。我给肽段序列里的每个氨基酸都算一个“注意力分数”,高的说明它们互相影响大。

数学上就是三件套:Query、Key、Value。

输入序列 \( X \in \mathbb{R}^{n \times d} \),我把它分别投影到Q、K、V:

Q = X W_Q
K = X W_K
V = X W_V

然后计算注意力得分:

Attention(Q,K,V) = softmax( Q K^T / sqrt(d_k) ) V

这个 sqrt(d_k) 是缩放因子。为什么要除?如果不除,当 \( d_k \) 很大时,点积结果方差会变大,softmax 梯度会消失。我遇到过模型训练损失死活降不下来,检查发现就是忘了加缩放因子。

经验: 在肽段谱图匹配中,序列长度一般不超过50,所以d_k取64或128比较稳。太大反而容易过拟合。

计算过程可以用一张图说明(别急,SVG在下面)。自注意力矩阵是一个 n×n 的注意力权重,第i行第j列表示第i个氨基酸关注第j个氨基酸的强度。这就是所谓的“全局感受野”——每个位置都能直接看到所有位置。

4.2 多头注意力 (Multi-Head Attention)

一个自注意力头只能捕获一种关系模式。肽段里可能有化学修饰、疏水性、电荷等多种特征,一个头不够。所以我用多头注意力,把Q、K、V切成h份,每份独立算自注意力,最后拼起来。

公式更简洁:

MultiHead(Q,K,V) = Concat(head_1, ..., head_h) W_O
其中 head_i = Attention(Q W_Q_i, K W_K_i, V W_V_i)

h 通常取8或16。我习惯在肽段任务上用8个头,因为数据量不大,头太多容易过拟合。

参数 典型值 说明
num_heads (h) 8 并行注意力头数
d_k 64 每个头的Query维度
d_v 64 每个头的Value维度
d_model 512 模型总维度 = h × d_k

注意: 多头注意力参数量会随着头数线性增加。我曾经在GPU上跑一个12头模型,显存直接爆了。建议batch size调小,或者用梯度累积。

多头的好处:不同头可以学到不同的关系。比如头1关注氨基酸残基的共价键模式,头2关注质荷比(m/z)差异,头3关注位点修饰概率。最终把这些信息拼在一起,模型决策更鲁棒。

4.3 前馈网络 (Feed-Forward Network, FFN)

自注意力输出后,还要过一层前馈网络。它就是个两层的全连接,中间带个ReLU激活:

FFN(x) = max(0, x W_1 + b_1) W_2 + b_2

内层维度通常放大4倍,比如d_model=512,内层就是2048。为什么要放大?因为自注意力主要是线性变换,非线性靠这里提供。

整个Transformer block是:自注意力 → 残差连接 + LayerNorm → 前馈网络 → 残差连接 + LayerNorm。堆叠N层。

在肽段场景里,我一般堆6层编码器就够了。层数太多,长序列会丢失局部细节。你想想看,一个肽段也就十几个氨基酸,6层足够建模全局依赖。

4.4 整体流程与SVG结构图

下面这张图展示了Transformer block的完整结构。从输入开始,经过多头自注意力、残差连接、层归一化、前馈网络,再到输出。

Transformer Block 结构 输入序列 x 多头自注意力 (Multi-Head) Q, K, V 投影 → 缩放点积 → Concat 残差连接 + LayerNorm x + Attention(x) → LayerNorm 前馈网络 (FFN) 全连接 → ReLU → 全连接 残差连接 + LayerNorm x + FFN(x) → LayerNorm 输出特征 🔁 重复 N 层 典型 N=6 肽段任务常用4~8层

4.5 在肽段谱图匹配中的实际应用

我直接把Transformer编码器输出的每个token向量,和谱图(MS/MS)的碎片离子向量做比对。具体做法是:

  1. 肽段序列用BERT-style embedding转为密集向量。
  2. 经过N个Transformer block,得到[batch, seq_len, d_model]。
  3. 对序列维度做平均池化或最大池化,得到肽段全局向量。
  4. 与谱图的全局向量(用另一个编码器得到)计算余弦相似度。

这里有个小坑:肽段序列长度变化很大(7~40个氨基酸),所以padding很重要。我习惯用pytorch的pad_packed_sequence配合key_padding_mask,避免模型关注pad token。

# 示例:自注意力中的mask处理
def forward(self, x, mask=None):
    # x: [batch, seq_len, d_model]
    # mask: [batch, seq_len]  bool (True表示有效位置)
    Q = self.query(x)
    K = self.key(x)
    V = self.value(x)
    
    scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
    if mask is not None:
        # 将无效位置设为负无穷,softmax后为零
        scores = scores.masked_fill(~mask.unsqueeze(1).unsqueeze(2), float('-inf'))
    attn = F.softmax(scores, dim=-1)
    return torch.matmul(attn, V)

关键参数总结:

  • d_model: 512(肽段数据量小,可以降到256)
  • num_heads: 8
  • num_layers: 6
  • ffn_dim: 2048(d_model×4)
  • dropout: 0.1(防止过拟合)

4.6 常见问题与避坑

  • 梯度爆炸: 我遇到过训练初期loss直接变成nan。原因是LayerNorm位置放错了。正确的顺序是:自注意力/前馈网络 → 残差 → LayerNorm。
  • 过拟合: 肽段数据通常只有几万条,全量训练很快过拟合。我建议加dropout(0.1~0.2)和权重衰减。
  • 内存爆炸: 自注意力时间复杂度和序列长度平方成正比。如果肽段内嵌了长修饰序列,长度可能到200。这时候我建议用滑动窗口注意力,只关注附近7~15个位置。
  • 初始化: 使用Xavier初始化,尤其是Q、K、V的权重。否则前期梯度消失。

嗯,这些就是Transformer三大核心机制的全部内容。说白了,自注意力做全局交互,多头注意力做多模式并行,前馈网络做非线性变换。三者组合,在肽段谱图匹配上能碾压传统方法。