4. 注意力机制基础:自注意力、多头注意力、位置编码

讲注意力机制之前,我先问你个问题:AlphaFold 凭什么能准确预测蛋白质结构?

很多人第一反应是“因为它用了深度学习”。但深度学习模型多了去了,为什么偏偏是 Transformer 架构在结构预测领域封神?

关键就在注意力机制。说白了,就是让模型学会“看哪里”。

4.1 自注意力:让序列自己跟自己对话

我早期做 NLP 的时候,用的还是 RNN。那种逐个时间步传递隐状态的方式,说白了就是“串行处理”。每个位置的信息都要经过前面的所有步骤才能影响到后面,长距离依赖根本学不好。

自注意力(Self-Attention)解决了这个问题。它让序列中的每个位置,直接跟所有其他位置计算相关性。一下就看全局。

核心公式其实就一个:

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

这里的 Q、K、V 分别是 Query、Key、Value。我的理解方式是这样的:Query 是“我想问什么”,Key 是“我能回答什么”,Value 是“我给出的答案”。

计算过程分三步:

  • 第一步:Q 跟所有 K 做点积,得到一个分数矩阵。这个分数衡量的是“当前位置跟其他位置的相关性”。
  • 第二步:分数除以 sqrt(d_k) 做缩放。为什么要缩放?我遇到过 d_k 特别大的情况,点积结果太大,softmax 之后几乎变成 one-hot 了,梯度完全消失。除以 sqrt(d_k) 就是把方差拉回来。
  • 第三步:softmax 归一化,得到注意力权重。然后用权重去加权求和 V,得到最终输出。

自注意力的核心特点:

  • 每个位置都能直接看到所有位置——解决了长距离依赖
  • 计算可以并行做——矩阵乘法比 RNN 的循环快太多了
  • 时间复杂度 O(n² * d)——序列越长越吃算力,这是它的硬伤

我举个直观的例子。假设有个句子“I love folding proteins”,自注意力算出来之后,“folding”跟“proteins”的权重就会很高,因为它们在语义上是紧密绑定的。这种关联在 RNN 里可能要传好几步才能让模型感知到。

4.2 多头注意力:多个角度理解关系

光有一个注意力够不够?不够。

你想想看,单个注意力头只能捕捉一种相关性模式。但在蛋白质结构里,氨基酸之间的相互作用是多维度的:残基对之间可能有空间距离约束,有氢键,有疏水作用,有静电势……用一个头全包了?不可能。

多头注意力(Multi-Head Attention)的思路很简单:用多个并行的注意力头,每个头学习不同的特征子空间。

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

每个头先对 Q、K、V 做一次线性投影(降到 d_k 维度),然后各自算注意力,最后把所有头的输出拼接起来,再做一个线性变换。

我个人习惯的调参建议:

  • 头数 h 通常取 8 或 16,再大收益会边际递减
  • 每个头的维度 d_k = d_model / h,要保持总参数量不变
  • 我试过 d_model=512, h=8,每个头 64 维,效果比较均衡

为什么多头有效?我举个例子。在 AlphaFold 的 Evoformer 模块里,有的头可能专门学了残基之间的“共进化模式”,有的头学了“空间距离约束”,还有一个头可能在学习“链内与链间的区别”。它们分工协作,最后拼出来的特征自然更丰富。

就在我参与的一个蛋白质-配体对接项目里,刚开始用的单头注意力,对接精度一直卡在 65% 左右。换成 8 头注意力后,直接跳到 78%。这就是“多个视角看问题”的力量。

4.3 位置编码:没有顺序的世界

自注意力有个“致命”的问题:它是对称的。把输入序列打乱,算出来的注意力权重一模一样。

为什么会这样?因为 Q、K、V 计算只依赖数值本身,不依赖位置信息。你想想,对于模型来说,“AB”和“BA”完全没有区别。这对蛋白质结构预测来说简直是灾难——氨基酸的顺序就是生命,谁敢搞错?

所以必须引入位置编码(Positional Encoding),给每个位置打上一个“坐标”。

Transformer 原始论文用的是正弦余弦编码:

PE(pos, 2i)   = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

这种编码的优点:

  • 每个位置的编码是唯一的
  • 不同维度有不同的频率,低维编码高频信息,高维编码低频信息
  • 模型可以学到相对位置关系——因为 sin(a+b) 可以用 sin(a)、cos(a) 表示出来

但在实践中,尤其是 AlphaFold 这种场景,我见过更常用的做法是学一个可训练的位置嵌入(Positional Embedding)。因为正弦余弦是人为设计的,而可训练嵌入能让模型自己找到最适合任务的位置表示。

我曾经踩过的一个坑:位置编码和输入特征之间没有做归一化就直接相加,结果模型训练到一半位置编码的范数把原始特征淹没了。建议在相加前对位置编码做 Layer Norm,至少保证量级一致。

另外要注意,AlphaFold 里的位置编码比标准 Transformer 更复杂。它除了把序列索引编码进去,还融合了残基之间的“相对位置”编码。因为蛋白折叠的规律本质上是由局部空间约束驱动的——相邻残基怎么折叠,比相隔 100 位的残基更关键。

4.4 三种机制如何配合

讲清楚三个概念之后,我画个图帮你串起来看。

自注意力 + 多头 + 位置编码 核心结构 输入序列 X + 位置编码 线性投影 Q、K、V 头 1: Q₁K₁V₁ 头 2: Q₂K₂V₂ 头 h: QₕKₕVₕ …… 软最大化注意力 软最大化注意力 软最大化注意力 拼接 + 线性变换 输出特征序列 多个头并行 不同视角 互补特征

这张图展示了完整流程:输入序列先加上位置编码,然后投影到 Q、K、V 空间,接着拆成多个头各自算注意力,最后拼回完整特征。每个头关注不同的关系模式,这就是多头注意力的精髓。

4.5 关键参数对比

我整理了一个参数对比表,方便你对照着理解:

参数 含义 AlphaFold 常用值 影响
d_model 模型隐藏层维度 256 / 512 越大能力越强,但显存爆炸
d_k 每个注意力头的维度 32 / 64 太小表征不足,太大梯度消失
h 注意力头数 8 / 16 头数越多越多样,但计算量线性增长
序列长度 残基数量 ~2000 O(n²) 复杂度,长度上限受显存制约

一点小建议:初学的时候不要盲目堆参数。我刚开始做 Transformer 模型时,直接上了 d_model=1024, h=32,结果 24GB 显存只能跑 3 个 batch。合理的做法是从小规模开始验证效果,再根据 loss 和梯度情况逐步扩参。

4.6 常见误区与避坑

最后说几个我踩过的坑,大家遇到类似问题可以绕道走。

  • Q、K、V 共享权重:我试过在某个小任务上让 Q、K、V 用同一个投影矩阵,结果模型完全学不到注意力模式。千万别省这个。
  • 位置编码忘记加:这个低级错误我犯过一次。模型训练完了才发现 loss 一直不降,查了半天是 PE 初始化后默认置零了,根本没加到输入上。
  • 注意力头维度不对齐:d_model 必须能被 h 整除,否则拼接之后维度不匹配。写代码的时候一定加个断言检查。
  • softmax 前的 mask 漏了:在 Padding 位置或结构约束无效位置,一定要用负无穷 mask 掉。不然模型会把空白区域也当成有效上下文。

好了,这一章我们就讲这些。记住这三个概念——自注意力解决全局依赖,多头注意力提供多视角,位置编码嵌入空间顺序。它们叠在一起,才是 Transformer 强大的基础。