2017年,Vaswani等人在论文《Attention Is All You Need》中提出了Transformer架构。这个标题本身就是一个宣言:注意力机制不再需要循环神经网络或卷积神经网络的辅助,它足以独立承担序列建模的全部任务。

七年后的今天,Transformer已经成为大语言模型的基石。GPT系列、BERT、LLaMA、Mistral——所有这些模型的核心都是同一个组件:Self-Attention。理解Self-Attention的计算流程,不只是学术要求,更是理解现代AI工作原理的必经之路。

注意力:从直觉到数学

在深入Self-Attention的具体计算之前,需要先理解"注意力"这个概念的本质。

传统神经网络处理序列时存在一个根本困境:序列中的每个元素应该关注哪些其他元素?在机器翻译中,翻译一个词可能需要参考原文中多个不同的词;在文本理解中,理解一个词的含义需要考虑上下文中的相关词汇。

注意力机制提供了一个优雅的解决方案:让每个位置动态地决定应该"关注"序列中的哪些其他位置。这不是一个硬编码的规则,而是一个可学习的过程。

flowchart LR
    subgraph 问题
        A[序列处理困境]
    end
    
    subgraph 传统方法
        B[RNN: 顺序处理]
        C[CNN: 局部窗口]
    end
    
    subgraph 注意力方案
        D[全局注意力]
        E[动态权重]
        F[并行计算]
    end
    
    A --> B
    A --> C
    A --> D
    D --> E --> F

Self-Attention中的"Self"意味着:同一个序列内部的元素相互关注。序列中的每个元素既作为查询者去寻找相关信息,也作为被查询者提供自己的信息。

Query、Key、Value:一个检索系统的隐喻

理解Self-Attention最直观的方式是把它想象成一个检索系统:

  • Query (查询):当前元素想要寻找什么样的信息
  • Key (键):每个元素能够提供什么样的信息
  • Value (值):每个元素实际的内容

当你在图书馆查找资料时,你手中的需求描述就是Query,每本书的标题和摘要就是Key,书的实际内容就是Value。通过比较Query和Key的相似度,你决定要阅读哪些书(Value)。

flowchart TB
    subgraph 图书馆隐喻
        Q[你的需求<br/>Query]
        K1[书A标题<br/>Key] 
        K2[书B标题<br/>Key]
        K3[书C标题<br/>Key]
        V1[书A内容<br/>Value]
        V2[书B内容<br/>Value]
        V3[书C内容<br/>Value]
        
        Match{相似度匹配}
        Result[阅读内容]
    end
    
    Q --> Match
    K1 --> Match
    K2 --> Match
    K3 --> Match
    Match -->|高相似度| V1
    Match -->|中相似度| V2
    Match -->|低相似度| V3
    V1 --> Result
    V2 --> Result
    V3 --> Result

在Self-Attention中,这三个向量都是通过线性变换从同一个输入向量得到的:

$$Q = XW^Q, \quad K = XW^K, \quad V = XW^V$$

其中 $X \in \mathbb{R}^{n \times d}$ 是输入序列(n个token,每个维度为d),$W^Q, W^K \in \mathbb{R}^{d \times d_k}$ 和 $W^V \in \mathbb{R}^{d \times d_v}$ 是可学习的投影矩阵。

Self-Attention的完整计算流程

第一步:输入嵌入与位置编码

假设输入句子是"Life is short",首先需要将每个词转换为向量表示。这个过程分为两步:

词嵌入(Word Embedding):每个词从词表中取出对应的嵌入向量。假设嵌入维度 $d_{model} = 512$,则每个词被表示为一个512维的向量。

位置编码(Positional Encoding):由于Self-Attention本身是位置不变的(交换输入序列中任意两个位置,输出只是相应交换),需要显式注入位置信息。原始Transformer使用正弦和余弦函数:

$$PE_{(pos, 2i)} = \sin(pos / 10000^{2i/d_{model}})$$

$$PE_{(pos, 2i+1)} = \cos(pos / 10000^{2i/d_{model}})$$

位置编码被直接加到词嵌入上:

$$X = \text{Embedding}(\text{tokens}) + PE$$
flowchart LR
    subgraph 输入处理
        Tokens["Life, is, short"]
        Embed["词嵌入<br/>[3, 512]"]
        PE["位置编码<br/>[3, 512]"]
        Add["相加"]
        X["输入表示 X<br/>[3, 512]"]
    end
    
    Tokens --> Embed --> Add
    PE --> Add --> X

第二步:计算Query、Key、Value

对于序列中的每个位置 $i$,通过三个不同的线性变换得到 $q^{(i)}, k^{(i)}, v^{(i)}$:

$$q^{(i)} = W^Q x^{(i)}$$

$$k^{(i)} = W^K x^{(i)}$$

$$v^{(i)} = W^V x^{(i)}$$

用矩阵形式表示整个序列:

$$Q = XW^Q, \quad K = XW^K, \quad V = XW^V$$

假设序列长度为 $n$,Query和Key的维度为 $d_k$,Value的维度为 $d_v$,则 $Q, K \in \mathbb{R}^{n \times d_k}$,$V \in \mathbb{R}^{n \times d_v}$。

import torch
import torch.nn as nn

d_model = 512
d_k = 64
d_v = 64

# 假设输入序列
X = torch.randn(1, 10, d_model)  # batch_size=1, seq_len=10

# Q, K, V投影
W_Q = nn.Linear(d_model, d_k, bias=False)
W_K = nn.Linear(d_model, d_k, bias=False)
W_V = nn.Linear(d_model, d_v, bias=False)

Q = W_Q(X)  # [1, 10, 64]
K = W_K(X)  # [1, 10, 64]
V = W_V(X)  # [1, 10, 64]

第三步:计算注意力分数

这是Self-Attention的核心计算。对于查询位置 $i$ 和键位置 $j$,注意力分数是它们的点积:

$$\omega_{ij} = q^{(i)} \cdot k^{(j)}$$

用矩阵形式一次性计算所有位置的注意力分数:

$$\Omega = QK^T$$

这里 $\Omega \in \mathbb{R}^{n \times n}$,其中 $\Omega_{ij}$ 表示位置 $i$ 对位置 $j$ 的"关注程度"(归一化前)。

# 计算注意力分数
scores = torch.matmul(Q, K.transpose(-2, -1))  # [1, 10, 10]
flowchart TB
    subgraph 注意力分数计算
        Q["Query<br/>[n, dk]"]
        K["Key<br/>[n, dk]"]
        KT["K^T<br/>[dk, n]"]
        MM["矩阵乘法<br/>Q × K^T"]
        Scores["注意力分数<br/>[n, n]"]
    end
    
    Q --> MM
    K --> KT --> MM --> Scores

第四步:缩放因子 $\sqrt{d_k}$

在应用Softmax之前,需要将注意力分数除以 $\sqrt{d_k}$:

$$\Omega_{scaled} = \frac{QK^T}{\sqrt{d_k}}$$

为什么需要这个缩放?

假设 $Q$ 和 $K$ 的元素是均值为0、方差为1的独立随机变量。两个 $d_k$ 维向量的点积:

$$q \cdot k = \sum_{i=1}^{d_k} q_i k_i$$

其方差为 $d_k$(每个 $q_i k_i$ 的方差为1,求和后方差累加)。当 $d_k$ 很大时(如512),点积的绝对值会很大。

Softmax函数对输入的幅度非常敏感:

$$\text{softmax}(x_i) = \frac{e^{x_i}}{\sum_j e^{x_j}}$$

当输入值很大时,Softmax会进入饱和区,输出趋近于one-hot向量,梯度趋近于零。这会导致训练困难。

除以 $\sqrt{d_k}$ 将方差归一化回1,使Softmax的输入保持在合理的范围内。

import math

d_k = Q.size(-1)
scaled_scores = scores / math.sqrt(d_k)
flowchart LR
    subgraph 缩放的必要性
        A["原始点积<br/>方差=dk"]
        B["Softmax"]
        C["问题:<br/>饱和区/梯度消失"]
        D["÷ √dk<br/>方差=1"]
        E["稳定训练"]
    end
    
    A --> B --> C
    A --> D --> E

第五步:应用注意力掩码(可选)

在某些场景下,需要阻止某些位置的注意力:

填充掩码(Padding Mask):序列被填充到相同长度时,填充位置不应参与注意力计算。

因果掩码(Causal Mask):在自回归生成中,位置 $i$ 不应看到位置 $i+1, i+2, ...$ 的信息。

掩码通过将对应位置的分数设为 $-\infty$ 来实现(Softmax后这些位置的权重为0):

# 因果掩码示例(下三角矩阵)
seq_len = 10
causal_mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool()
scaled_scores = scaled_scores.masked_fill(causal_mask, float('-inf'))
flowchart TB
    subgraph 因果掩码示例
        M["掩码矩阵<br/>上三角为-inf"]
        S1["原始分数<br/>[n, n]"]
        S2["掩码后分数<br/>[n, n]"]
        
        Note["位置i只能<br/>看到位置1..i"]
    end
    
    M --> S2
    S1 --> S2
    S2 --> Note

第六步:Softmax归一化

将缩放后的分数转换为概率分布:

$$A = \text{softmax}(\Omega_{scaled})$$

每一行表示一个查询位置对所有键位置的注意力权重,和为1。

attention_weights = torch.softmax(scaled_scores, dim=-1)

第七步:加权求和

最后,用注意力权重对Value向量进行加权求和:

$$\text{Output}^{(i)} = \sum_{j=1}^{n} A_{ij} v^{(j)}$$

矩阵形式:

$$\text{Output} = AV$$
output = torch.matmul(attention_weights, V)  # [1, 10, 64]

完整公式

将上述步骤合并,得到Self-Attention的完整公式:

$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$
flowchart LR
    subgraph Input
        X[输入序列 X]
    end
    
    subgraph Projections
        WQ[W^Q]
        WK[W^K]
        WV[W^V]
        Q[Query Q]
        K[Key K]
        V[Value V]
    end
    
    subgraph Attention
        MM[矩阵乘法 QK^T]
        Scale[缩放 ÷√dk]
        Mask[掩码可选]
        SM[Softmax]
        Weighted[加权求和 AV]
    end
    
    subgraph Output
        Out[输出]
    end
    
    X --> WQ --> Q
    X --> WK --> K
    X --> WV --> V
    Q --> MM
    K --> MM
    MM --> Scale --> Mask --> SM
    V --> Weighted
    SM --> Weighted --> Out

多头注意力:并行处理多种关系

单个Self-Attention可能无法同时捕捉序列中的多种关系。比如,一个句子中可能同时存在语法依赖、语义关联、指代关系等不同类型的联系。

多头注意力(Multi-Head Attention)通过并行运行多个独立的Self-Attention来解决这个问题:

$$\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, ..., \text{head}_h)W^O$$

其中:

$$\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)$$

每个"头"有自己独立的投影矩阵 $W_i^Q, W_i^K, W_i^V$,可以学习关注不同类型的关系。

flowchart TB
    subgraph Input
        X[输入序列]
    end
    
    subgraph Heads
        H1[Head 1: 语法关系]
        H2[Head 2: 语义关联]
        H3[Head 3: 指代关系]
        H4[Head h: ...]
    end
    
    subgraph Outputs
        O1[输出1]
        O2[输出2]
        O3[输出3]
        O4[输出h]
    end
    
    subgraph Final
        Concat[拼接]
        Proj[线性投影 W^O]
        Result[最终输出]
    end
    
    X --> H1 --> O1
    X --> H2 --> O2
    X --> H3 --> O3
    X --> H4 --> O4
    
    O1 --> Concat
    O2 --> Concat
    O3 --> Concat
    O4 --> Concat
    
    Concat --> Proj --> Result

为什么是"头分割"而不是"完整投影"?

在实现中,通常将 $d_{model}$ 维的输入投影到 $d_{model}$ 维,然后分割成 $h$ 个头,每个头维度为 $d_k = d_{model}/h$:

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super().__init__()
        assert d_model % num_heads == 0
        
        self.d_model = d_model
        self.num_heads = num_heads
        self.head_dim = d_model // num_heads
        
        # 单个大投影矩阵,后续分割成多个头
        self.qkv_proj = nn.Linear(d_model, 3 * d_model)
        self.out_proj = nn.Linear(d_model, d_model)
    
    def forward(self, x, mask=None):
        batch_size, seq_len, _ = x.size()
        
        # 投影并分割成Q, K, V
        qkv = self.qkv_proj(x)
        qkv = qkv.reshape(batch_size, seq_len, 3, self.num_heads, self.head_dim)
        qkv = qkv.permute(2, 0, 3, 1, 4)  # [3, batch, heads, seq, head_dim]
        Q, K, V = qkv[0], qkv[1], qkv[2]
        
        # Scaled Dot-Product Attention
        scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))
        attention = torch.softmax(scores, dim=-1)
        
        # 加权求和
        output = torch.matmul(attention, V)
        
        # 拼接所有头
        output = output.transpose(1, 2).contiguous()
        output = output.view(batch_size, seq_len, self.d_model)
        
        # 最终投影
        return self.out_proj(output)

这种设计有一个重要优势:参数量和计算量与单头注意力相同。每个头的维度减少了 $h$ 倍,但头的数量增加了 $h$ 倍,总体保持平衡。

多头注意力的几何直觉

可以把多头注意力理解为:在多个不同的"表示子空间"中并行计算注意力。每个头专注于不同类型的关系:

  • 某个头可能专注于相邻词之间的语法关系
  • 另一个头可能捕捉长距离的语义关联
  • 还有一个头可能关注指代消解

这种多样化的注意力模式是Transformer强大表达能力的重要来源。

残差连接与层归一化:稳定训练的关键设计

Transformer的每个子层(Self-Attention和Feed-Forward)都被包裹在残差连接和层归一化中:

$$\text{Output} = \text{LayerNorm}(x + \text{Sublayer}(x))$$
flowchart TB
    subgraph Transformer子层结构
        X[输入 x]
        Sub[子层计算<br/>Self-Attention/FFN]
        Drop[Dropout]
        Add[残差相加]
        LN[Layer Normalization]
        Out[输出]
    end
    
    X --> Sub --> Drop --> Add --> LN --> Out
    X -.->|"恒等映射"| Add

残差连接的作用

残差连接(Skip Connection)最初在ResNet中提出,在Transformer中同样关键:

梯度流动:深层网络面临梯度消失问题。残差连接提供了一条"高速公路",梯度可以直接流向浅层。对于 $L$ 层网络,梯度只需经过 $L$ 次矩阵乘法,而不是 $L$ 次连乘。

信息保留:Self-Attention是位置不变的,位置信息只能通过位置编码注入。残差连接确保原始位置信息不会在第一层就丢失。

训练稳定性:残差连接使每层只需学习"残差"——对输入的修改,而非全新的表示。这降低了学习难度。

层归一化的作用

Batch Normalization在NLP任务中表现不佳,因为:

  • 不同句子的词分布差异大
  • Batch size通常较小(受GPU内存限制)
  • 长序列中稀有词的特征方差更大

Layer Normalization在每个样本内部进行归一化:

$$\text{LN}(x) = \gamma \cdot \frac{x - \mu}{\sigma + \epsilon} + \beta$$

其中 $\mu$ 和 $\sigma$ 是该样本在特征维度上的均值和标准差,$\gamma$ 和 $\beta$ 是可学习的缩放和偏移参数。

Layer Normalization确保每个位置的表示在相似的数值范围内,加速训练并提供轻微的正则化效果。

Pre-Norm vs Post-Norm

原始Transformer使用Post-Norm:先计算子层,再加残差,最后归一化。现代大模型(如GPT、LLaMA)普遍使用Pre-Norm:先归一化,再计算子层,最后加残差。

Pre-Norm的优势:

  • 训练更稳定,不需要学习率预热
  • 梯度流动更平滑
  • 可以堆叠更多层
# Post-Norm (原始Transformer)
x = x + dropout(sublayer(layer_norm(x)))

# Pre-Norm (现代大模型)
x = x + dropout(sublayer(layer_norm(x)))

注意力掩码:控制信息流动

注意力掩码是Transformer中一个容易被忽视但极其重要的机制。

flowchart TB
    subgraph 掩码类型
        Padding["填充掩码<br/>Padding Mask"]
        Causal["因果掩码<br/>Causal Mask"]
        Combined["组合掩码"]
    end
    
    subgraph 应用场景
        P1["变长序列处理"]
        C1["自回归生成"]
        C2["GPT/LLaMA推理"]
    end
    
    Padding --> P1
    Causal --> C1
    Padding --> Combined
    Causal --> Combined --> C2

填充掩码(Padding Mask)

变长序列需要填充到相同长度。填充位置不应参与注意力计算:

def create_padding_mask(seq, pad_token=0):
    """seq: [batch_size, seq_len]"""
    return (seq != pad_token).unsqueeze(1).unsqueeze(2)
    # 返回 [batch_size, 1, 1, seq_len]

因果掩码(Causal Mask)

自回归生成要求:位置 $i$ 只能看到位置 $1, 2, ..., i$:

def create_causal_mask(seq_len):
    """创建下三角掩码矩阵"""
    mask = torch.tril(torch.ones(seq_len, seq_len))
    return mask.unsqueeze(0).unsqueeze(0)  # [1, 1, seq_len, seq_len]

因果掩码确保了:

  • 训练时:使用完整的序列,但每个位置只能看到之前的信息
  • 推理时:生成是自回归的,每个新token只依赖于之前生成的token

掩码的实现细节

掩码在Softmax之前应用,将需要屏蔽的位置设为 $-\infty$:

scores = scores.masked_fill(mask == 0, float('-inf'))

Softmax后,这些位置的权重为:

$$\text{softmax}(-\infty) = \frac{e^{-\infty}}{\sum_j e^{x_j}} = 0$$

复杂度分析:Self-Attention的代价

时间复杂度

Self-Attention的时间复杂度是 $O(n^2 \cdot d)$:

  • $QK^T$:$n \times d_k$ 和 $d_k \times n$ 的矩阵乘法 → $O(n^2 \cdot d_k)$
  • Softmax:对 $n \times n$ 矩阵的每行归一化 → $O(n^2)$
  • $AV$:$n \times n$ 和 $n \times d_v$ 的矩阵乘法 → $O(n^2 \cdot d_v)$

当序列长度 $n$ 远大于嵌入维度 $d$ 时,$O(n^2)$ 成为瓶颈。

空间复杂度

存储注意力矩阵 $A \in \mathbb{R}^{n \times n}$ 需要 $O(n^2)$ 空间。对于长序列(如 $n = 100,000$),这个矩阵需要约 40GB内存(float32)。

flowchart LR
    subgraph 复杂度对比
        A["Self-Attention<br/>O(n² × d)"]
        B["RNN<br/>O(n × d²)"]
        C["CNN<br/>O(k × n × d)"]
    end
    
    subgraph 适用场景
        D["短序列<br/>n < d"]
        E["长序列<br/>n > d"]
    end
    
    A --> D
    B --> E
    C --> E

优化方向

FlashAttention通过分块计算和重计算策略,将注意力矩阵的内存需求从 $O(n^2)$ 降到 $O(n)$,同时保持相同的计算复杂度。这是目前处理长序列的标准方案。

稀疏注意力、线性注意力等方法尝试将复杂度降到 $O(n)$ 或 $O(n \log n)$,但通常会牺牲一定的模型性能。

Self-Attention的反向传播

理解Self-Attention的反向传播对于调试和优化至关重要。

flowchart TB
    subgraph 前向传播
        X[输入 X] --> QKV[Q, K, V]
        QKV --> Scores[注意力分数]
        Scores --> Attn[注意力权重 A]
        Attn --> Out[输出 Y=AV]
    end
    
    subgraph 反向传播
        dL_dY["∂L/∂Y"]
        dL_dA["∂L/∂A = ∂L/∂Y · V^T"]
        dL_dV["∂L/∂V = A^T · ∂L/∂Y"]
        dL_dScores["∂L/∂Scores<br/>(Softmax梯度)"]
        dL_dQKV["∂L/∂Q, ∂L/∂K"]
        dL_dX["∂L/∂X"]
    end
    
    dL_dY --> dL_dA
    dL_dA --> dL_dScores
    dL_dScores --> dL_dQKV --> dL_dX
    dL_dY --> dL_dV --> dL_dX

注意力权重的梯度

设损失函数为 $L$,输出为 $Y = AV$。根据链式法则:

$$\frac{\partial L}{\partial A} = \frac{\partial L}{\partial Y} \cdot V^T$$

Softmax的梯度

Softmax的雅可比矩阵是对角占优的,梯度计算需要考虑所有输出之间的耦合:

$$\frac{\partial L}{\partial \omega_i} = \sum_j \frac{\partial L}{\partial a_j} \cdot a_j (\delta_{ij} - a_i)$$

其中 $\delta_{ij}$ 是Kronecker delta,$a_j$ 是Softmax输出。

Q、K、V的梯度

$$\frac{\partial L}{\partial Q} = \frac{\partial L}{\partial \Omega} \cdot K / \sqrt{d_k}$$

$$\frac{\partial L}{\partial K} = Q^T \cdot \frac{\partial L}{\partial \Omega} / \sqrt{d_k}$$

$$\frac{\partial L}{\partial V} = A^T \cdot \frac{\partial L}{\partial Y}$$

梯度流动的关键观察:$Q$ 和 $K$ 的梯度来自注意力分数,$V$ 的梯度来自最终输出。这种分离使得模型能够独立学习"关注什么"和"如何使用关注的信息"。

常见面试问题与误区

问题一:为什么使用点积而不是其他相似度度量?

点积有两个优势:

  1. 计算效率:点积可以通过高度优化的矩阵乘法实现
  2. 可学习性:通过 $W^Q$ 和 $W^K$ 的学习,点积可以隐式地学习复杂的相似度函数

原始论文还尝试了加性注意力(Additive Attention),但在实践中点积注意力的性能相当且计算更快。

问题二:为什么Query和Key的维度相同?

点积要求两个向量维度相同。但Value的维度可以不同——它决定了输出的维度。

在实际实现中,通常设置 $d_k = d_v = d_{model}/h$,但这不是必须的。

问题三:Self-Attention和Cross-Attention有什么区别?

  • Self-Attention:$Q = K = V = X$,同一序列内部的注意力
  • Cross-Attention:$Q$ 来自一个序列,$K, V$ 来自另一个序列,用于编码器-解码器架构

Cross-Attention允许解码器"查询"编码器的输出。

flowchart TB
    subgraph Self-Attention
        X1[序列 X] --> Q1[Q]
        X1 --> K1[K]
        X1 --> V1[V]
        Q1 --> Attn1[注意力计算]
        K1 --> Attn1
        V1 --> Attn1
    end
    
    subgraph Cross-Attention
        X2[解码器输出] --> Q2[Q]
        X3[编码器输出] --> K2[K]
        X3 --> V2[V]
        Q2 --> Attn2[注意力计算]
        K2 --> Attn2
        V2 --> Attn2
    end

问题四:注意力权重可以直接解释吗?

这是一个常见的误解。注意力权重表示"模型关注了什么",但不一定表示"模型为什么关注它"。

2020年的论文《Attention is not Explanation》表明:不同的注意力分布可能产生相同的预测结果。注意力权重更多是模型内部的计算中间结果,而非人类可理解的决策解释。

问题五:为什么需要缩放因子?

面试中常见的回答是"防止点积过大"。更精确的解释涉及统计特性:

假设 $q_i, k_i \sim N(0, 1)$ 独立同分布,则 $q \cdot k = \sum_{i=1}^{d_k} q_i k_i$ 的方差为 $d_k$。

标准差为 $\sqrt{d_k}$,所以除以 $\sqrt{d_k}$ 将点积的方差归一化为1。

问题六:多头注意力和单头注意力参数量相同吗?

是的。如果单头注意力的投影矩阵维度为 $d \times d$,则 $h$ 头注意力的每个头维度为 $d/h$,参数量为 $h \times d \times (d/h) = d^2$。

计算量也相同,因为每个头的计算量减少,但头数增加。

从理论到实践:调试Self-Attention

常见问题诊断

注意力权重全为均匀分布:可能的原因

  • 学习率过大导致训练不稳定
  • 输入嵌入未正确归一化
  • 缩放因子被错误移除

某些头总是产生相同的注意力模式:可能的原因

  • 初始化问题
  • 正则化不足
  • 数据中的偏差

梯度消失或爆炸:检查

  • 残差连接是否正确实现
  • LayerNorm是否应用在正确的位置
  • 是否使用了合适的初始化方法

实用调试技巧

# 可视化注意力权重
def visualize_attention(attention_weights, tokens):
    import matplotlib.pyplot as plt
    import seaborn as sns
    
    plt.figure(figsize=(10, 8))
    sns.heatmap(attention_weights, 
                xticklabels=tokens, 
                yticklabels=tokens,
                cmap='viridis')
    plt.xlabel('Keys')
    plt.ylabel('Queries')
    plt.show()

# 检查注意力分布的熵
def check_attention_entropy(attention_weights):
    entropy = -torch.sum(attention_weights * torch.log(attention_weights + 1e-9), dim=-1)
    print(f"平均熵: {entropy.mean():.4f}, 最大可能熵: {math.log(attention_weights.size(-1)):.4f}")

总结

Self-Attention是Transformer架构的核心,其计算流程可以概括为七个步骤:

  1. 输入嵌入与位置编码
  2. 计算Query、Key、Value投影
  3. 计算注意力分数 $QK^T$
  4. 缩放 $\frac{1}{\sqrt{d_k}}$
  5. 应用掩码(可选)
  6. Softmax归一化
  7. 加权求和得到输出

多头注意力通过并行处理多种关系增强了模型的表达能力。残差连接和层归一化确保了深层网络的训练稳定性。注意力掩码控制了信息流动,支持填充处理和自回归生成。

理解这些机制不仅是掌握Transformer的关键,也是深入理解现代大语言模型工作原理的基础。无论是面试准备还是实际工程开发,对Self-Attention的深入理解都会带来显著的优势。

参考文献

  1. Vaswani, A., et al. “Attention Is All You Need.” NeurIPS 2017.
  2. Devlin, J., et al. “BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding.” NAACL 2019.
  3. Radford, A., et al. “Language Models are Unsupervised Multitask Learners.” OpenAI 2019.
  4. Dao, T., et al. “FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.” NeurIPS 2022.
  5. Joulin, A. “Gradient Flow in Transformers.” arXiv 2024.
  6. Tay, Y., et al. “Efficient Transformers: A Survey.” ACM Computing Surveys 2022.
  7. Bahdanau, D., et al. “Neural Machine Translation by Jointly Learning to Align and Translate.” ICLR 2015.
  8. He, K., et al. “Deep Residual Learning for Image Recognition.” CVPR 2016.
  9. Ba, J.L., et al. “Layer Normalization.” arXiv 2016.
  10. Jain, S., et al. “Attention is not Explanation.” NAACL 2019.