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 三种机制如何配合
讲清楚三个概念之后,我画个图帮你串起来看。
这张图展示了完整流程:输入序列先加上位置编码,然后投影到 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 强大的基础。