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 为什么点积注意力成了主流?

我个人觉得,原因有三点:

  1. 硬件友好:GPU 对矩阵乘法有专门的优化,点积操作可以批量执行。
  2. 实现简单:几行代码就能搞定,调试也方便。
  3. 可扩展性强:多头注意力、因果掩码等改进都是基于点积注意力构建的。

但加性注意力也有它的优势。比如当 Query 和 Key 来自不同模态(文本 vs 图像),维度差异很大时,加性注意力通过可学习的映射能更好地融合信息。我在做早期的视觉问答模型时,就发现加性注意力在某些场景下比点积注意力高出 1-2 个点。

避坑指南:不要盲目跟风。如果你的任务中 Query 和 Key 的语义空间差异很大(比如文本描述和图像区域),加性注意力可能更适合。先在小规模数据上对比一下,再做决定。

2.5 核心逻辑结构图

下面这张图展示了两种注意力的核心计算流程对比:

加性注意力 vs 点积注意力 核心流程对比 加性注意力 Query (q) + Key (k) 拼接 [q; k] W·[q;k] + b → tanh v^T · tanh(...) → 标量分数 softmax → 加权求和 点积注意力 Query (q) + Key (k) q · k^T(点积) ÷ √d_k(缩放) 标量分数(直接得到) softmax → 加权求和 加性注意力:可学习非线性映射,灵活但慢 | 点积注意力:纯线性计算,高效且可并行

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)

我的建议:在实际项目中,除非你有特殊需求(比如跨模态维度不匹配),否则默认选点积注意力。它简单、高效、容易调试。如果发现效果不好,再考虑加性注意力作为备选方案。