2. 经典注意力公式
注意力机制的核心,说白了就是回答一个问题:给定一个查询(Query),我应该关注哪些信息(Key),以及如何把这些信息加权组合成最终输出(Value)?
在视觉语言模型里,这个机制无处不在。比如让模型描述一张图片,它需要知道“狗”这个单词应该对应图片里哪块区域。这就是注意力在起作用。
我刚开始接触这个领域时,看到一堆公式也头疼。但后来发现,其实就两种主流方案:加性注意力和点积注意力。今天咱们就把它们彻底讲透。
2.1 加性注意力(Additive Attention)
加性注意力是早期比较经典的做法,由 Bahdanau 等人在 2014 年提出。它的核心思路是:把 Query 和 Key 拼接起来,扔进一个单层神经网络,让网络自己学出它们之间的匹配分数。
公式长这样:
score(q, k) = v^T · tanh(W_q · q + W_k · k + b)
其中:
- q:查询向量(比如解码器当前时刻的隐状态)
- k:键向量(比如编码器每个位置的隐状态)
- W_q, W_k:可学习的权重矩阵
- b:偏置项
- v:可学习的向量,用于把隐层结果映射成一个标量分数
- tanh:激活函数,引入非线性
关键理解:加性注意力本质上是用一个全连接层来学习 Query 和 Key 之间的交互。它不要求 q 和 k 的维度相同,灵活性更高。
我记得在早期做机器翻译项目时,用的就是加性注意力。当时有个坑——训练速度确实慢。因为每个时间步都要对每个 Key 做一次矩阵运算,计算量不小。
我曾经踩过的坑:加性注意力在序列长度较长时,显存占用会线性增长。有一次我处理 500 帧的视频特征,结果显存直接爆了。后来才意识到,加性注意力虽然效果好,但不适合长序列场景。
2.2 点积注意力(Dot-Product Attention)
点积注意力就简单粗暴多了。它直接计算 Query 和 Key 的点积作为匹配分数。公式如下:
score(q, k) = q · k^T
如果考虑缩放因子,就是大名鼎鼎的 Scaled Dot-Product Attention:
score(q, k) = (q · k^T) / √d_k
其中 d_k 是 Key 向量的维度。为什么要除以 √d_k?
原因很简单:当 d_k 比较大时,点积的结果会很大,导致 softmax 后的梯度极小,模型难以训练。缩放一下,就能把方差控制住。
我的个人习惯:只要条件允许,我优先用点积注意力。原因就一个——计算效率高。点积可以用高度优化的矩阵乘法库(比如 cuBLAS)来加速,比加性注意力快一个数量级。
你想想看,Transformer 为什么能处理超长序列?很大程度上就靠这个高效的点积注意力。
2.3 核心对比:一张表说清楚
| 对比维度 | 加性注意力 | 点积注意力 |
|---|---|---|
| 计算方式 | 拼接后过全连接层 + tanh | 直接点积(可缩放) |
| 计算复杂度 | O(n²·d)(含非线性变换) | O(n²·d)(纯矩阵乘法) |
| 实际速度 | 慢(无法利用矩阵乘法加速) | 快(高度并行化) |
| 维度要求 | q 和 k 维度可以不同 | q 和 k 维度必须相同 |
| 非线性能力 | 有(tanh 激活) | 无(纯线性操作) |
| 长序列表现 | 显存占用高,速度慢 | 相对高效,但仍有 O(n²) 问题 |
| 主流应用 | 早期 Seq2Seq、部分多模态模型 | Transformer 及其变体(主流) |
从表里能看出来,点积注意力在效率上完胜。这也是为什么现在几乎所有视觉语言模型(CLIP、BLIP、LLaVA 等)都用它。
2.4 为什么点积注意力成了主流?
我个人觉得,原因有三点:
- 硬件友好:GPU 对矩阵乘法有专门的优化,点积操作可以批量执行。
- 实现简单:几行代码就能搞定,调试也方便。
- 可扩展性强:多头注意力、因果掩码等改进都是基于点积注意力构建的。
但加性注意力也有它的优势。比如当 Query 和 Key 来自不同模态(文本 vs 图像),维度差异很大时,加性注意力通过可学习的映射能更好地融合信息。我在做早期的视觉问答模型时,就发现加性注意力在某些场景下比点积注意力高出 1-2 个点。
避坑指南:不要盲目跟风。如果你的任务中 Query 和 Key 的语义空间差异很大(比如文本描述和图像区域),加性注意力可能更适合。先在小规模数据上对比一下,再做决定。
2.5 核心逻辑结构图
下面这张图展示了两种注意力的核心计算流程对比:
2.6 代码实现对比
光说不练假把式。下面我用 PyTorch 实现两种注意力,你感受一下差异:
import torch
import torch.nn as nn
import torch.nn.functional as F
class AdditiveAttention(nn.Module):
"""加性注意力"""
def __init__(self, d_q, d_k, d_hidden):
super().__init__()
self.W_q = nn.Linear(d_q, d_hidden, bias=False)
self.W_k = nn.Linear(d_k, d_hidden, bias=False)
self.v = nn.Linear(d_hidden, 1, bias=False)
def forward(self, q, k, v):
# q: [batch, d_q], k: [batch, seq_len, d_k]
q_expanded = self.W_q(q).unsqueeze(1) # [batch, 1, d_hidden]
k_transformed = self.W_k(k) # [batch, seq_len, d_hidden]
scores = self.v(torch.tanh(q_expanded + k_transformed)).squeeze(-1)
attn_weights = F.softmax(scores, dim=-1)
return torch.bmm(attn_weights.unsqueeze(1), v).squeeze(1)
class DotProductAttention(nn.Module):
"""缩放点积注意力"""
def __init__(self, d_k):
super().__init__()
self.scale = d_k ** 0.5
def forward(self, q, k, v):
# q: [batch, seq_len_q, d_k], k: [batch, seq_len_k, d_k]
scores = torch.bmm(q, k.transpose(1, 2)) / self.scale
attn_weights = F.softmax(scores, dim=-1)
return torch.bmm(attn_weights, v)
我的建议:在实际项目中,除非你有特殊需求(比如跨模态维度不匹配),否则默认选点积注意力。它简单、高效、容易调试。如果发现效果不好,再考虑加性注意力作为备选方案。