Transformer架构详解:自注意力与序列建模的基石 / Transformer Architecture: The Foundation of Self-Attention and Sequence Modeling
在深度学习处理序列数据的漫长历程中,循环神经网络(RNN)及其变体长期占据主导地位,但其固有的顺序计算特性成为了扩展模型规模、捕获长程依赖的瓶颈。2017年,Vaswani等人提出的Transformer架构,凭借其完全基于注意力机制的设计,彻底颠覆了这一范式。它摒弃了循环与卷积,实现了序列的并行化建模,其核心——自注意力(Self-Attention)机制——能够直接计算序列中任意两个位置之间的依赖关系,无论其距离多远。这一革命性设计,正是当今大语言模型(LLM)得以崛起的最根本基石。本章将深入Transformer的“心脏地带”,逐层解剖其核心组件:缩放点积自注意力、多头注意力、位置编码、前馈神经网络与层归一化。我们将通过严谨的数学推导、清晰的架构图示与实用的PyTorch代码片段,揭示其如何协同工作,在并行计算中捕获全局依赖,并最终对比分析编码器-解码器与仅解码器这两种主流架构的设计哲学与适用边界。
---
1.1 序列建模的范式转变:从循环到注意力
在Transformer诞生前,序列建模的主流是RNN及其门控变体(如LSTM、GRU)。其核心思想是顺序处理:每个时间步 $t$ 的隐藏状态 $h_t$ 依赖于当前输入 $x_t$ 和前一个隐藏状态 $h_{t-1}$,即 $h_t = f(x_t, h_{t-1})$。这种设计天然地建模了序列的时序性,但带来了两个根本性挑战:
1. 计算无法并行:第 $t$ 步的计算必须等待第 $t-1$ 步完成,严重限制了在长序列上的训练和推理速度。 2. 长程依赖衰减:信息需要通过多个时间步的“传递”才能从序列一端到达另一端,在传递过程中容易发生梯度消失/爆炸,导致模型难以学习远距离依赖关系。
卷积神经网络(CNN)在序列任务(如文本分类)中也被使用,通过一维卷积在局部窗口内提取特征。虽然计算可以并行,但其感受野(Receptive Field)受限于卷积核大小,需要堆叠多层才能捕获长程依赖,且位置信息处理不如RNN自然。
注意力的核心思想是:直接计算序列中所有元素两两之间的关联强度,然后根据这个强度对值(Value)进行加权求和。这允许模型在单次操作中“看到”序列中的所有位置,完美解决了并行化问题,并通过权重分配机制直接建模任意距离的依赖。Transformer将这一思想发挥到极致,构建了完全基于注意力的架构。
---
1.2 缩放点积自注意力:核心引擎的数学与实现
自注意力是Transformer处理序列的基本单元。对于输入序列 $X \in \mathbb{R}^{n \times d_{\text{model}}}$,其中 $n$ 是序列长度,$d_{\text{model}}$ 是模型维度,自注意力计算一个同样维度的输出序列 $O \in \mathbb{R}^{n \times d_{\text{model}}}$,其中每个位置的输出是序列中所有位置输入的加权和,权重由注意力分数决定。
1.2.1 数学推导与直觉
第一步:投影生成Q, K, V 首先,通过三个可学习的权重矩阵 $W^Q, W^K, W^V \in \mathbb{R}^{d_{\text{model}} \times d_k}$(通常 $d_k = d_{\text{model}} / h$,$h$ 为头数,后续多头部分详述),将 $X$ 投影为查询(Query)、键(Key)和值(Value)矩阵: $$ Q = X W^Q, \quad K = X W^K, \quad V = X W^V $$ 其中 $Q, K, V \in \mathbb{R}^{n \times d_k}$。Query 代表“当前正在计算的位置想了解什么”,Key 代表“其他位置能提供什么信息”,Value 代表“其他位置实际要传递的信息”。
第二步:计算注意力分数 计算 $Q$ 和 $K^T$ 的点积,得到 $n \times n$ 的分数矩阵 $S$: $$ S = Q K^T $$ $S_{ij}$ 表示第 $i$ 个位置对第 $j$ 个位置的关注程度(原始分数)。点积衡量了 $q_i$ 和 $k_j$ 的相似度:越相似,分数越高。
第三步:缩放(Scaling) 点积 $q_i \cdot k_j$ 的绝对值大小与 $d_k$ 相关。当 $d_k$ 较大时,点积的方差可能增大,导致softmax函数进入梯度极小的饱和区(梯度趋近于0),不利于训练。为此,Vaswani等人提出将分数除以 $\sqrt{d_k}$: $$ \text{Scores} = \frac{Q K^T}{\sqrt{d_k}} $$ 缩放因子 $\sqrt{d_k}$ 的理论依据:假设 $q_i$ 和 $k_j$ 的元素独立同分布,均值为0,方差为1,则 $q_i \cdot k_j$ 的均值为0,方差为 $d_k$。除以 $\sqrt{d_k}$ 后,方差变为1,使得softmax输入的方差保持稳定,缓解梯度问题。
第四步:应用掩码(可选,用于解码器) 在解码器中,为了防止位置 $i$ 看到未来位置 $j > i$ 的信息(保证自回归生成),需要应用一个下三角掩码矩阵 $M$: $$ \text{Scores}_{\text{masked}} = \text{Scores} + M, \quad \text{其中 } M_{ij} = \begin{cases} 0 & \text{if } j \leq i \\ -\infty & \text{if } j > i \end{cases} $$ 加上 $-\infty$ 后,经过softmax,对应位置的概率会变为0。
第五步:Softmax归一化 沿最后一维(即 $K$ 的维度,对应序列的其他位置)对分数应用softmax,得到注意力权重 $A$: $$ A = \text{softmax}\left(\frac{Q K^T}{\sqrt{d_k}} + M\right) \in \mathbb{R}^{n \times n} $$ $A_{ij}$ 表示在计算第 $i$ 个位置的输出时,对第 $j$ 个位置的 $V_j$ 的权重。所有权重之和为1。
第六步:加权求和输出 最终的输出 $O$ 是 $V$ 的加权和: $$ O = A V $$ $O_i = \sum_{j=1}^{n} A_{ij} V_j$。这就是自注意力的核心:每个位置的输出是其所有位置(包括自身)的值的加权组合,权重由查询和键的相似度动态决定。
1.2.2 PyTorch代码实现与剖析
以下是一个无掩码、单头缩放点积自注意力的简化实现,突出核心逻辑:
```python import torch import torch.nn as nn import torch.nn.functional as F
class ScaledDotProductAttention(nn.Module): """ 缩放点积自注意力(单头,无掩码) """ def __init__(self, d_k): super().__init__() self.d_k = d_k
缩放因子 1/sqrt(d_k) 作为缓冲区,不参与梯度更新
self.scale = torch.tensor(1.0 / (d_k ** 0.5))
def forward(self, Q, K, V, mask=None): """ Args: Q, K, V: [batch_size, n_head, seq_len, d_k] 或 [batch_size, seq_len, d_k] mask: [batch_size, 1, 1, seq_len] 或 [batch_size, 1, seq_len, seq_len] (可选) Returns: output: [batch_size, n_head, seq_len, d_v] 或 [batch_size, seq_len, d_v] attn_weights: 注意力权重矩阵 """
1. 计算缩放后的点积分数
scores = torch.matmul(Q, K.transpose(-2, -1)) * self.scale
scores shape: [..., seq_len, seq_len]
2. 应用掩码(如果提供)
if mask is not None:
mask中为True的位置会被填充一个极小的值
scores = scores.masked_fill(mask == 0, -1e9)
3. Softmax归一化,得到注意力权重
attn_weights = F.softmax(scores, dim=-1)
attn__weights shape: [..., seq_len, seq_len]
4. 加权求和输出
output = torch.matmul(attn_weights, V) return output, attn_weights
==================== 使用示例 ====================
batch_size, seq_len, d_model = 2, 10, 512 d_k = d_v = 64 # 通常 d_k = d_v = d_model / n_heads
模拟输入 (假设已通过线性层得到Q, K, V)
X = torch.randn(batch_size, seq_len, d_model) W_Q = torch.randn(d_model, d_k) W_K = torch.randn(d_model, d_k) W_V = torch.randn(d_model, d_v)
Q = torch.matmul(X, W_Q) # [2, 10, 64] K = torch.matmul(X, W_K) # [2, 10, 64] V = torch.matmul(X, W_V) # [2, 10, 64]
attention = ScaledDotProductAttention(d_k) output, weights = attention(Q, K, V) # output: [2, 10, 64] print("输出形状:", output.shape) print("注意力权重形状:", weights.shape) print("某一行权重和(应为1):", weights[0, 0].sum().item()) ```
关键点:
- **维度灵活性**:代码支持是否包含“头”维度。在多头注意力中,$Q, K, V$ 的形状通常是 `[batch, n_head, seq_len, d_k]`,此时 `transpose(-2, -1)` 交换最后两个维度(`seq_len` 和 `d_k`?不对,应该是交换 `seq_len` 和另一个 `seq_len`?仔细看:`K.transpose(-2, -1)` 对于形状 `[b, h, n, d_k]`,交换 `-2`(即 `n`)和 `-1`(即 `d_k`)?不对,点积需要 `[b, h, n, d_k] @ [b, h, d_k, n]`,所以 `K` 需要转置最后两个维度,从 `[b, h, n, d_k]` 变为 `[b, h, d_k, n]`。因此 `transpose(-2, -1)` 是正确的,它交换了 `n` 和 `d_k`。得到 `scores` 形状 `[b, h, n, n]`。然后对最后一个维度(即 `n`,对应 `K` 的序列长度)做softmax。
- **掩码应用**:`masked_fill` 是高效实现掩码的关键。掩码张量的形状需要与 `scores` 兼容,通常通过广播机制。
- **数值稳定性**:实践中,直接在softmax前对分数进行缩放(`* self.scale`)是标准做法。
---
1.3 多头注意力:并行捕获多样化的依赖模式
单头注意力将所有信息压缩到一个 $d_k$ 维的子空间。为了增强模型的表示能力,Transformer采用了多头注意力(Multi-Head Attention)。其思想是:将 $Q, K, V$ 的 $d_{\text{model}}$ 维度并行地拆分成 $h$ 个头,每个头拥有独立的 $d_k = d_v = d_{\text{model}} / h$ 维的子空间。在每个头内独立进行缩放点积注意力计算,最后将 $h$ 个头的输出拼接起来,再通过一个线性层投影回 $d_{\text{model}}$ 维。
1.3.1 数学形式化
设总维度 $d_{\text{model}}$,头数 $h$,则每个头的维度 $d_k = d_v = d_{\text{model}} / h$。
1. 分割与线性投影:将原始的 $W^Q, W^K, W^V \in \mathbb{R}^{d_{\text{model}} \times d_{\text{model}}}$ 视为 $h$ 个独立的权重矩阵堆叠,或者等价地,将 $X$ 投影到 $h$ 个不同的子空间: $$ \text{Head}_i = \text{Attention}(X W_i^Q, X W_i^K, X W_i^V) \in \mathbb{R}^{n \times d_v} $$ 其中 $i = 1, ..., h$,$W_i^Q, W_i^K \in \mathbb{R}^{d_{\text{model}} \times d_k}$,$W_i^V \in \mathbb{R}^{d_{\text{model}} \times d_v}$。
2. 并行计算:$h$ 个头的计算是完全独立的,可以完美并行。
3. 拼接与最终投影: $$ \text{MultiHead}(Q, K, V) = \text{Concat}(\text{Head}_1, ..., \text{Head}_h) W^O $$ 其中 $\text{Concat}(\text{Head}_1, ..., \text{Head}_h) \in \mathbb{R}^{n \times h d_v} = \mathbb{R}^{n \times d_{\text{model}}}$,$W^O \in \mathbb{R}^{d_{\text{model}} \times d_{\text{model}}}$ 是最终的输出权重矩阵。
1.3.2 为什么需要多头?直观理解
- **不同的表示子空间**:每个头可以学习到不同的注意力模式。例如,一个头可能关注语法结构(如主谓一致),另一个头可能关注指代关系(如代词与先行词),第三个头可能关注局部短语。这类似于CNN中多个滤波器的思想。
- **增强表达能力**:单头注意力的权重矩阵 $A$ 是 $n \times n$ 的,但受softmax归一化约束,其表达能力有限。多头允许模型在 $h$ 个不同的 $n \times n$ 权重矩阵上操作,极大地丰富了模型的交互能力。
- **促进优化**:多头提供了更多样的梯度路径,可能有助于训练稳定性。
论文中的实验表明,当 $h=8$(原始设置)时,性能显著优于单头($h=1$),且当 $d_k$ 固定时,增加头数 $h$(从而减小 $d_k$)在计算复杂度不变的情况下(因为 $h \cdot d_k = d_{\text{model}}$),效果更好。
1.3.3 PyTorch实现与集成
在PyTorch中,`nn.MultiheadAttention` 模块已高度优化。但理解其内部组合至关重要:
```python class MultiHeadAttention(nn.Module): """ 多头注意力(支持掩码) """ def __init__(self, d_model, n_heads): super().__init__() assert d_model % n_heads == 0, "d_model必须能被n_heads整除" self.d_model = d_model self.n_heads = n_heads self.d_k = self.d_v = d_model // n_heads
将单个大矩阵拆分为n_heads个,或者使用一个线性层后reshape
self.W_Q = nn.Linear(d_model, d_model) # 等价于 h个 [d_model, d_k] 堆叠 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)
self.attention = ScaledDotProductAttention(self.d_k)
初始化(略,通常使用Xavier)
def forward(self, Q, K, V, mask=None): batch_size = Q.size(0)
1. 线性投影并分割为多头
[batch, seq_len, d_model] -> [batch, seq_len, n_heads, d_k] -> [batch, n_heads, seq_len, d_k]
Q = self.W_Q(Q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2) K