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 选型建议

最后给点实际建议。如果你刚开始做注意力相关项目,我推荐这样选:

  1. 默认选缩放点积注意力。90% 的场景它都够用,而且生态最好(各种预训练模型都用它)。
  2. 只有 query 和 key 维度不同时,才考虑加性注意力。比如做跨模态对齐、或者用不同编码器提取特征时。
  3. 点积注意力基本可以忘掉。除非你在写教材或者做非常小维度的实验,否则别碰它。

说白了,技术选型就是个「够用就好」的事。别为了炫技去用加性注意力,也别为了省参数去用点积注意力。缩放点积注意力就是那个「中庸之道」,稳得很。