为什么需要注意力

在 Transformer 之前,处理序列数据的主流方案是 RNN 和 LSTM。它们按顺序逐个处理 token——第 3 个 token 必须等第 2 个 token 算完。

这个”必须等待”带来了两个致命问题:不能并行训练,长序列的早期信息会被遗忘。

2017 年,Vaswani 等人的《Attention Is All You Need》提出一个激进的想法:完全抛弃 RNN,只用注意力。这让整个序列可以同时处理,并行训练成为可能。关于论文本身的精读见 《Attention Is All You Need》论文精读

graph LR
    A[输入序列] -->|线性投影| B[Q K V 矩阵]
    B -->|Q×K^T| C[注意力分数矩阵]
    C -->|Softmax| D[注意力权重]
    D -->|×V| E[输出向量]

Self-Attention 的四步计算

以翻译句子 “The cat sat on the mat” 为例,当模型处理 “sat” 这个词时:

第一步:创建 Q、K、V

每个输入 token 的 embedding 通过三个不同的权重矩阵投影:

Q = input @ W_Q  # Query: 我在找什么?
K = input @ W_K  # Key:   我有什么信息?
V = input @ W_V  # Value: 我的实际内容是什么?

这三个矩阵的维度通常是 (seq_len, d_model),其中 d_model = 512(原始 Transformer)。

第二步:计算注意力分数

将 Q 和 K 做点积,得到每个 token 对每个其他 token 的”关注度”:

scores = Q @ K.T  # shape: (seq_len, seq_len)
# scores[i][j] = token i 对 token j 的关注程度

第三步:缩放 + Softmax

点积结果可能很大(尤其是 d_model 较大时),这会导致 softmax 的梯度消失。除以 sqrt(d_k) 缩放后解决这个问题:

d_k = Q.shape[-1]
attention_weights = softmax(scores / sqrt(d_k), dim=-1)
# 每一行的和为 1,表示一个 token 对所有 token 的注意力分布

第四步:加权求和

output = attention_weights @ V  # shape: (seq_len, d_v)

每个 token 的输出是它关注的 token 的 Value 加权求和。

完整实现

import torch
import torch.nn.functional as F

def self_attention(X, W_Q, W_K, W_V):
    Q = X @ W_Q
    K = X @ W_K
    V = X @ W_V
    d_k = Q.shape[-1]
    scores = Q @ K.T / torch.sqrt(torch.tensor(d_k))
    weights = F.softmax(scores, dim=-1)
    return weights @ V, weights

# 示例:6 个 token,每个 512 维
X = torch.randn(6, 512)
W_Q = torch.randn(512, 64)  # d_k = 64
W_K = torch.randn(512, 64)
W_V = torch.randn(512, 64)

output, attention_map = self_attention(X, W_Q, W_K, W_V)
print(f"Output shape: {output.shape}")     # (6, 64)
print(f"Attention map shape: {attention_map.shape}")  # (6, 6)

attention_map 就是常说的”注意力热力图”——(6, 6) 的矩阵,第 i 行第 j 列表示 token i 对 token j 的关注权重。

Multi-Head Attention

单头注意力只能关注一种特征关系。多头注意力并行做多组 Q-K-V 投影,每组关注不同的特征子空间:

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model=512, n_heads=8):
        super().__init__()
        self.d_k = d_model // n_heads  # 每头的维度
        self.n_heads = n_heads
        # 合并所有头的投影矩阵
        self.W_Q = nn.Linear(d_model, d_model)
        self.W_K = nn.Linear(d_model, d_model)
        self.W_V = nn.Linear(d_model, d_model)
        self.W_O = nn.Linear(d_model, d_model)

    def forward(self, x):
        B, N, D = x.shape  # batch, seq_len, d_model
        # 投影并拆分多头
        Q = self.W_Q(x).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        K = self.W_K(x).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        V = self.W_V(x).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        # 缩放点积注意力
        scores = Q @ K.transpose(-2, -1) / math.sqrt(self.d_k)
        weights = F.softmax(scores, dim=-1)
        out = weights @ V
        # 合并多头并输出
        out = out.transpose(1, 2).contiguous().view(B, N, D)
        return self.W_O(out)

多个头各自学到不同的语言特征——有的关注语法结构,有的关注语义相似度,有的关注位置邻近性。

为什么注意力机制如此重要

Attention 不仅解决了并行训练问题,它还让模型具有了可解释性——你可以可视化注意力权重,看到模型在做预测时”关注”输入的哪个部分。这对于调试和信任 AI 系统至关重要。

GPT 系列的每一代都建立在 Transformer 架构之上,详见 GPT 系列模型演进史。推理阶段的效率优化也是注意力机制的延伸,参考 LLM 推理优化