2、经典注意力机制:加性注意力、点积注意力、缩放点积注意力
聊到注意力机制,很多人第一反应就是 Transformer 里的缩放点积注意力。但说实话,我在早期做机器翻译项目时,最先接触的其实是加性注意力。那时候还没有 Transformer,大家用的都是 Bahdanau 那套东西。
今天咱们就把这三种经典注意力掰开揉碎讲清楚。我会结合自己踩过的坑,帮你理解它们各自的脾气秉性。
2.1 加性注意力(Additive Attention)
加性注意力是 Bahdanau 在 2014 年提出的。它的核心思路很简单:用一个小的前馈网络来学习 query 和 key 之间的匹配分数。
公式长这样:
score(q, k) = v^T * tanh(W_q * q + W_k * k)
其中 W_q、W_k 和 v 都是可学习的参数。说白了,就是先把 query 和 key 分别映射一下,然后加在一起过个 tanh,最后用 v 做内积得到标量分数。
优点:
- query 和 key 的维度可以不一样。这在跨模态任务里特别有用——比如文本 query 匹配图像 key,维度不同也能算。
- 表达能力更强。因为引入了可学习的非线性变换,理论上能拟合更复杂的匹配关系。
缺点:
- 计算慢。每个 query-key 对都要过一遍小网络,没法用矩阵乘法加速。
- 参数量大。W_q、W_k、v 这些矩阵都要学,模型容易变胖。
适用场景:
- query 和 key 维度不一致时(比如多模态任务)
- 需要较强表达能力的场景(比如小规模数据上的复杂匹配)
- 序列长度较短的任务(否则计算量吃不消)
我的经验:我曾经在一个图文匹配项目里用过加性注意力。当时文本用 BERT 编码(768维),图像用 ResNet 特征(2048维),维度差了一倍多。用点积注意力根本没法算,加性注意力就完美解决了这个问题。不过训练速度确实慢,一个 epoch 要多跑 40% 的时间。
2.2 点积注意力(Dot-Product Attention)
点积注意力就直白多了。它直接拿 query 和 key 做内积,得到相似度分数。
公式:
score(q, k) = q^T * k
你想想看,两个向量内积越大,说明它们方向越一致,相似度越高。这个直觉非常自然。
优点:
- 计算极快。内积可以用矩阵乘法一次性算完,GPU 特别喜欢这种操作。
- 没有额外参数。省内存,也省训练时间。
缺点:
- query 和 key 的维度必须相同。否则内积没法算。
- 当维度 d 很大时,内积的方差会变大。导致 softmax 后的分布过于尖锐(有的接近1,有的接近0),梯度容易消失。
适用场景:
- query 和 key 维度一致且维度较小(比如 d < 100)
- 对计算速度要求高的场景
- 作为缩放点积注意力的简化版来理解原理
注意:我在做一个小型文本分类模型时,试过直接用点积注意力。当时 d=64,效果还行。但后来把维度升到 512,训练直接崩了——loss 降不下去,梯度全消失了。这就是典型的「维度灾难」。所以千万别在大维度上裸用点积注意力。
2.3 缩放点积注意力(Scaled Dot-Product Attention)
这个就是 Transformer 里用的那个。它解决了点积注意力在大维度上的问题。
公式:
score(q, k) = (q^T * k) / sqrt(d_k)
其中 d_k 是 key 的维度。除以 sqrt(d_k) 是为了把方差拉回到 1 左右。
为什么会这样?我简单解释一下:假设 q 和 k 的每个元素都是独立同分布,均值为0,方差为1。那么 q^T * k 的均值是0,方差是 d_k。除以 sqrt(d_k) 后,方差就变成1了。这样 softmax 的输入就不会太大,梯度也能正常传播。
优点:
- 解决了大维度下的梯度消失问题
- 计算效率高(和点积注意力一样快)
- 实际效果稳定,是当前最主流的注意力实现
缺点:
- query 和 key 维度仍需一致
- 表达能力不如加性注意力(但实践中差距不大)
适用场景:
- 几乎所有现代 Transformer 模型(BERT、GPT、ViT 等)
- 大维度场景(d_k > 100)
- 需要高效并行计算的场景
我的习惯:现在做新项目,我默认就用缩放点积注意力。除非遇到 query 和 key 维度不一致这种特殊情况,才会考虑加性注意力。说白了,缩放点积注意力就是「通用解」,省心又省力。
2.4 三种注意力对比总结
| 特性 | 加性注意力 | 点积注意力 | 缩放点积注意力 |
|---|---|---|---|
| 计算复杂度 | O(n²·d) 且无法矩阵化 | O(n²·d) 可矩阵化 | O(n²·d) 可矩阵化 |
| 额外参数 | 有(W_q, W_k, v) | 无 | 无 |
| 维度要求 | query 和 key 维度可不同 | 维度必须相同 | 维度必须相同 |
| 大维度稳定性 | 稳定 | 不稳定(梯度消失) | 稳定 |
| 实际应用 | 较少(多模态等特殊场景) | 极少(仅用于教学或小维度) | 最广泛(Transformer 系列) |
2.5 代码实现对比
下面我用 PyTorch 把三种注意力都实现一遍。你可以直观感受下它们的差异。
import torch
import torch.nn as nn
import torch.nn.functional as F
class AdditiveAttention(nn.Module):
"""加性注意力"""
def __init__(self, query_dim, key_dim, hidden_dim):
super().__init__()
self.W_q = nn.Linear(query_dim, hidden_dim, bias=False)
self.W_k = nn.Linear(key_dim, hidden_dim, bias=False)
self.v = nn.Linear(hidden_dim, 1, bias=False)
def forward(self, query, key, value, mask=None):
# query: [batch, tgt_len, query_dim]
# key: [batch, src_len, key_dim]
# value: [batch, src_len, value_dim]
q = self.W_q(query).unsqueeze(2) # [batch, tgt_len, 1, hidden]
k = self.W_k(key).unsqueeze(1) # [batch, 1, src_len, hidden]
scores = self.v(torch.tanh(q + k)).squeeze(-1) # [batch, tgt_len, src_len]
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
attn_weights = F.softmax(scores, dim=-1)
return torch.bmm(attn_weights, value)
class DotProductAttention(nn.Module):
"""点积注意力"""
def forward(self, query, key, value, mask=None):
# query, key, value 维度必须相同: [batch, seq_len, d]
scores = torch.bmm(query, key.transpose(1, 2)) # [batch, tgt_len, src_len]
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
attn_weights = F.softmax(scores, dim=-1)
return torch.bmm(attn_weights, value)
class ScaledDotProductAttention(nn.Module):
"""缩放点积注意力"""
def __init__(self, d_k):
super().__init__()
self.scale = d_k ** 0.5
def forward(self, query, key, value, mask=None):
scores = torch.bmm(query, key.transpose(1, 2)) / self.scale
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
attn_weights = F.softmax(scores, dim=-1)
return torch.bmm(attn_weights, value)
避坑指南:我曾经在实现加性注意力时,忘了把 query 和 key 分别 unsqueeze 到正确的维度。结果 q + k 的广播机制出了问题,debug 了一下午才发现。嗯,写代码时一定要把张量的形状写清楚,别偷懒。
2.6 选型建议
最后给点实际建议。如果你刚开始做注意力相关项目,我推荐这样选:
- 默认选缩放点积注意力。90% 的场景它都够用,而且生态最好(各种预训练模型都用它)。
- 只有 query 和 key 维度不同时,才考虑加性注意力。比如做跨模态对齐、或者用不同编码器提取特征时。
- 点积注意力基本可以忘掉。除非你在写教材或者做非常小维度的实验,否则别碰它。
说白了,技术选型就是个「够用就好」的事。别为了炫技去用加性注意力,也别为了省参数去用点积注意力。缩放点积注意力就是那个「中庸之道」,稳得很。