
很多刚开始接触 Transformer 的同学在阅读《Attention Is All You Need》原文或者各种源码解析时常常会被“QKV 矩阵”、“多头注意力”、“位置编码”这些概念劝退。其实问题的关键并不在于 Transformer 本身有多难而在于我们跳过了它背后的前置知识直接一头扎进了最复杂的部分。这篇文章会把 Transformer 涉及的前置概念系统性地梳理一遍包括 Seq2Seq 架构、注意力机制的本质、RNN/CNN 的局限、位置编码的动机以及用 PyTorch 手写一个简化版的多头注意力。无论你是准备面试、写论文还是准备做 NLP 或 CV 的项目这篇复习笔记都能帮你把基础打牢。1. 为什么要复习 Transformer 前置概念1.1 Transformer 是什么Transformer 是一种基于自注意力Self-Attention机制的深度神经网络架构最早由 Vaswani 等人在 2017 年发表的论文《Attention Is All You Need》中提出。它最初被设计用来解决机器翻译问题但很快扩展到文本分类、命名实体识别、文本生成、图像分类、目标检测等多个领域。与传统的循环神经网络RNN不同Transformer 不依赖顺序地处理输入数据而是通过自注意力机制同时观察整个输入序列从而捕捉序列中任意两个位置之间的依赖关系。这种设计带来了两个直接的好处并行计算能力强训练速度远高于 RNN。可以建模长距离依赖不会像 RNN 那样随着序列变长而丢失早期信息。1.2 为什么需要先复习前置概念很多人在学习 Transformer 时遇到的第一个障碍并不是注意力机制本身的数学公式而是对“为什么要用注意力机制”“为什么不用 RNN 了”“Q、K、V 到底从哪里来”这些问题缺少上下文。举个例子如果不知道 Seq2Seq 模型在解码时需要依赖编码器的最后一个隐藏状态就很难理解为什么 Bahdanau Attention 要计算“编码器所有隐藏状态和解码器当前隐藏状态之间的相似度”。如果不知道 RNN 的隐藏状态是串行传递的就很难理解 Transformer 为什么强调并行计算。所以这篇文章的目标很明确不直接手撕完整版 Transformer 的每一层细节而是把读 Transformer 论文和源码之前必须具备的几个前置知识点讲透然后通过代码从零实现多头注意力模块最终帮你建立起从“传统序列模型”到“Transformer”的完整知识链条。1.3 适合哪些读者准备入门 NLP 或大模型方向的学生和开发者。已经用过 Hugging Face 的 Transformer 库但想深入了解底层原理的人。准备面试需要系统梳理注意力机制、位置编码等知识点的人。想复现论文或修改模型结构的研究人员。2. 前置基础一从 Seq2Seq 到编码器-解码器架构2.1 Seq2Seq 模型解决什么问题在 Transformer 出现之前机器翻译、文本摘要、对话生成等任务普遍采用 Seq2SeqSequence to Sequence模型。Seq2Seq 的核心思想是将一个输入序列映射到另一个输出序列。Seq2Seq 通常由两个部分组成编码器Encoder负责读取输入序列并将其压缩为一个固定长度的语义向量。解码器Decoder负责根据这个语义向量逐步生成输出序列。以中英翻译为例输入是中文句子“我爱编程”输出是英文句子“I love programming”。编码器逐步读取中文的每一个词最终得到一个表示整个句子语义的向量解码器再从这个向量出发一个一个词地生成英文结果。2.2 编码器-解码器的工作流程我们用公式来描述这个过程。假设输入序列是 x (x1, x2, ..., xn)编码器通过循环神经网络逐步计算隐藏状态h_t f(h_{t-1}, x_t)其中 f 是 RNN 单元h_t 是 t 时刻的隐藏状态。编码器读完整个输入序列后通常取最后一个隐藏状态 h_n 作为整个句子的语义向量 c。解码器在生成第 t 个输出词时会根据上一个时刻的隐藏状态 s_{t-1}、上一个生成的词 y_{t-1}以及语义向量 c 来更新状态s_t g(s_{t-1}, y_{t-1}, c)然后再通过一个线性层和 softmax 来预测当前时刻的输出词 y_t。2.3 固定长度向量的瓶颈Seq2Seq 模型最大的问题在于编码器必须把整个输入序列的信息压缩到一个固定长度的向量 c 中。当输入句子很长时早期的信息在经过多步传递后会被“稀释”甚至遗忘导致翻译质量明显下降。这就好比让你听完一段 10 分钟的话然后只允许用一句话复述全部内容。句子短的时候问题不大句子一长细节一定丢。为了解决这个问题Bahdanau 等人提出了注意力机制Attention Mechanism它的核心思想是解码器在生成每个词时不要只依赖一个固定向量而是从编码器的所有隐藏状态中动态地选取相关信息。这样就可以理解 Transformer 的位置了Transformer 直接把注意力机制从“辅助工具”升级为“核心架构”彻底抛弃了 RNN。3. 前置基础二注意力机制的本质3.1 注意力机制的直观理解注意力机制借鉴了人类的视觉注意力习惯当我们在看一幅画时不会均匀地关注画面上每一个像素而是会把注意力集中在自己感兴趣的局部区域。在序列模型中也是如此。解码器在生成某个词时并不需要平等地关注输入序列中的每一个词而应该重点关注与当前生成相关的词。例如在翻译“我喜欢猫”这句中文时生成英文单词“I”时应该重点关注“我”生成“cats”时应该重点关注“猫”。注意力机制要做的事情就是为输入序列中的每一个位置计算一个权重然后用这个权重对所有位置的信息进行加权求和。权重越大表示当前位置对当前输出越重要。3.2 Bahdanau Attention加性注意力Bahdanau 等人提出的注意力机制是经典的加性注意力。假设编码器的隐藏状态为 h_1, h_2, ..., h_n解码器在 t 时刻的隐藏状态为 s_t那么每个编码器位置的得分计算方式为e_{t,i} v^T tanh(W_1 h_i W_2 s_t)其中 W1、W2 和 v 都是可学习的参数。然后通过 softmax 将得分转换为概率分布α_{t,i} softmax(e_{t,i})最后解码器用这个概率分布对编码器的所有隐藏状态进行加权求和得到上下文向量 c_tc_t sum(α_{t,i} * h_i)3.3 从注意力机制到自注意力在 Bahdanau Attention 中注意力是“解码器对编码器”发起的也就是说查询来自解码器键和值来自编码器。这种机制被称为交叉注意力Cross-Attention。而 Transformer 中更核心的概念是自注意力Self-Attention。所谓自注意力是指序列中的每个位置都和其他所有位置计算注意力也就是说查询Query、键Key、值Value都来自同一个输入序列。自注意力的意义在于它能够直接建模输入序列内部任意两个位置之间的关系。比如句子“The animal didnt cross the street because it was too tired”模型可以通过自注意力知道句子中的“it”指代的是“animal”从而正确理解整个句子的语义。3.4 为什么需要 Q、K、V在自注意力中每个输入 token 都会生成三个向量Query、Key、Value。Query查询向量表示当前 token 想要“查找什么”。Key键向量表示当前 token 的“标签”或“索引”用于和其他 token 的 Query 匹配。Value值向量表示当前 token 真正携带的“内容信息”。注意力分数的计算过程可以理解为用某个 token 的 Query 去和序列中所有 token 的 Key 做点积得到相似度再通过 softmax 转成权重最后用权重对所有 Value 做加权求和。用一句通俗的话概括就是Query 问“你和我的相关度有多高”Key 负责回答“我们有哪些相似特征”Value 提供真正有用的信息。4. 前置基础三为什么是 Transformer 而不是 RNN/CNN4.1 RNN 的序列依赖问题RNN 处理序列数据的方式是从左到右逐个读取 token每一步的隐藏状态都依赖于上一步的隐藏状态。这种串行计算方式导致两个严重问题第一训练无法并行。因为第 t 步必须等第 t-1 步计算完成所以 RNN 很难充分利用 GPU 的并行计算能力训练速度成为瓶颈。第二长距离依赖难以捕捉。虽然 LSTM 和 GRU 通过门控机制缓解了梯度消失问题但在建模“距离较远的两个 token 之间的关系”时信息仍然需要经过多个时间步的“传递”有效信息很容易被噪声淹没。4.2 CNN 的局部视野问题CNN 通过卷积核来捕捉局部特征。对于序列数据一维卷积可以理解为一个滑动窗口在序列上滑动每次只关注窗口内的几个 token。CNN 的优势是能够并行计算因为不同的窗口互不依赖。但它的问题也很明显如果两个长距离依赖的 token 不在同一个卷积窗口内就需要叠加很多层卷积来扩大感受野这既增加了模型深度也增加了训练难度。在图像任务中普通 CNN 对“全局关系”的建模能力同样有限这也是后来 Vision TransformerViT出现的动机之一。4.3 Transformer 的核心贡献Transformer 之所以能取代 RNN 和 CNN 成为主流架构核心原因有二第一自注意力机制让序列中任意两个位置之间的距离变成了 1。无论两个 token 相隔多远它们之间的依赖都只需要一次注意力计算就能建立不存在信息传递损耗。第二自注意力天然可以并行计算。序列中所有 token 的 Q、K、V 可以通过矩阵乘法一次性计算出来注意力分数也可以通过矩阵运算同时得到。这让 Transformer 在大规模数据上的训练效率远超 RNN。不过Transformer 也并非没有缺点。它虽然解决了并行和长距离依赖问题但自注意力的计算复杂度是 O(n²)当序列长度非常长时计算开销会变得很大。这也是后续许多改进工作的切入点例如 Longformer、Linformer、Swin Transformer 等。5. 前置基础四位置编码与多头注意力5.1 为什么需要位置编码RNN 天然是顺序模型每个 token 的位置信息隐含在“第几步进入网络”里。CNN 也通过卷积核的滑动顺序保留了局部位置关系。但 Transformer 的自注意力机制对输入序列中的所有 token 一视同仁。如果直接把 token 的 embedding 输入模型模型会认为“我喜欢猫”和“猫喜欢我”是完全一样的因为两者的 token 集合相同只是顺序不同。为了让模型感知到 token 的顺序信息Transformer 在输入 Embedding 上叠加了位置编码Positional Encoding。5.2 正弦位置编码公式Transformer 原文中使用的是正弦位置编码计算公式如下PE(pos, 2i) sin(pos / 10000^(2i/d_model)) PE(pos, 2i1) cos(pos / 10000^(2i/d_model))其中 pos 表示 token 在序列中的位置i 表示编码维度索引d_model 表示 embedding 维度。这种设计的好处是编码值在 [-1, 1] 之间数值稳定。不同位置的编码向量不同模型可以区分顺序。由于三角函数的性质模型可以通过线性变换捕捉相对位置信息。在实际工程中很多现代模型如 BERT、GPT也使用可学习的位置嵌入Learnable Positional Embedding也就是把位置索引映射到一个可训练的向量。两种方式各有利弊正弦编码不需要训练参数可学习嵌入可以针对任务自适应调整。5.3 多头注意力的设计动机多头注意力Multi-Head Attention是 Transformer 的另一个核心设计。它的做法是将 Q、K、V 分别通过不同的线性变换投影到多个子空间然后在每个子空间中独立地计算注意力最后将多个子空间的结果拼接起来再经过一次线性变换。为什么要这么做用一个生活化的例子来理解当你在阅读一篇文章时你可能同时关注多个维度——语法结构、指代关系、情感色彩、上下文连贯性。如果只有一个注意力头模型只能从一个角度去计算 token 之间的关系而多头注意力允许模型同时从多个不同的表示子空间中学习关系这大大增强了模型的表达能力。在 Transformer 原文中d_model 512头数 h 8每个头计算的维度是 d_k d_v 64。8 个头的输出拼接后恢复为 512 维。5.4 多头注意力的数学表达多头注意力的计算过程可以表示为MultiHead(Q, K, V) Concat(head_1, ..., head_h) · W_O其中每个头的结果为head_i Attention(Q · W_Q_i, K · W_K_i, V · W_V_i)而缩放点积注意力Scaled Dot-Product Attention的计算公式为Attention(Q, K, V) softmax(Q · K^T / sqrt(d_k)) · V这里除以 sqrt(d_k) 的原因是为了防止点积结果过大导致 softmax 梯度消失。当 d_k 比较大时点积 Q·K^T 的方差也会变大所以需要缩放。6. 环境准备与 PyTorch 实现6.1 环境版本说明本文的核心代码基于 PyTorch 实现示例环境如下操作系统Windows 10 / Ubuntu 20.04 均可 Python3.8 或更高版本 PyTorch1.10 或更高版本2.x 完全兼容 CUDA可选CPU 环境也能运行代码示例 IDEPyCharm 或 Jupyter Notebook版本可以根据你的项目实际情况调整。本文的重点是演示实现思路所有代码均不依赖第三方高级库只需安装 PyTorch 即可运行。如果还没有安装 PyTorch可以执行以下命令pip install torch6.2 实现缩放点积注意力我们先实现最底层的缩放点积注意力机制。这个函数接收 Q、K、V 三个矩阵和可选的 attention mask返回注意力输出和注意力权重。import torch import torch.nn as nn import torch.nn.functional as F import math def scaled_dot_product_attention(query, key, value, maskNone): 缩放点积注意力 参数 query: [batch_size, heads, seq_len_q, d_k] key: [batch_size, heads, seq_len_k, d_k] value: [batch_size, heads, seq_len_v, d_v] mask: [batch_size, heads, seq_len_q, seq_len_k] 或 None 返回 output: [batch_size, heads, seq_len_q, d_v] attention_weights: [batch_size, heads, seq_len_q, seq_len_k] d_k query.size(-1) # 计算 Q 和 K 的点积然后除以 sqrt(d_k) scores torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k) # 如果提供了 mask则把 mask 中为 0 的位置替换为一个极小的数 if mask is not None: scores scores.masked_fill(mask 0, -1e9) # 在最后一个维度上做 softmax attention_weights F.softmax(scores, dim-1) # 对 V 进行加权求和 output torch.matmul(attention_weights, value) return output, attention_weights这里需要注意masked_fill操作。在训练 Transformer 解码器时我们需要用一个上三角 mask 挡住未来的 token防止模型在预测第 i 个词时 “偷看” 第 i1 个词以及后面的内容。这被称为 Look-Ahead Mask 或 Causal Mask。-1e9是一个很小的数经过 softmax 后对应的概率会趋近于 0这样未来位置的信息就不会被纳入当前步的注意力计算中。6.3 实现多头注意力层接下来我们在缩放点积注意力的基础上实现一个完整的多头注意力层。这个层要做的事情有三件将输入的 Q、K、V 通过线性变换并拆分成多个头。对每个头分别计算注意力。将多头的输出拼接起来经过最后的线性变换输出。class MultiHeadAttention(nn.Module): def __init__(self, d_model, num_heads): 参数 d_model: 输入和输出的特征维度例如 512 num_heads: 多头注意力的头数例如 8 super(MultiHeadAttention, self).__init__() assert d_model % num_heads 0, d_model 必须能被 num_heads 整除 self.d_model d_model self.num_heads num_heads self.d_k d_model // num_heads # 定义 Q、K、V 的线性变换层 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 split_heads(self, x, batch_size): 将输入张量从 [batch_size, seq_len, d_model] 拆分为 [batch_size, num_heads, seq_len, d_k] x x.view(batch_size, -1, self.num_heads, self.d_k) return x.transpose(1, 2) def forward(self, query, key, value, maskNone): batch_size query.size(0) # 1. 线性变换并拆分多头 Q self.split_heads(self.w_q(query), batch_size) K self.split_heads(self.w_k(key), batch_size) V self.split_heads(self.w_v(value), batch_size) # 2. 计算缩放点积注意力 attn_output, attention_weights scaled_dot_product_attention(Q, K, V, mask) # 3. 拼接多头结果 attn_output attn_output.transpose(1, 2).contiguous().view( batch_size, -1, self.d_model ) # 4. 输出线性变换 output self.w_o(attn_output) return output, attention_weights如果用一句话总结这个类的逻辑先通过三个 Linear 把 Q、K、V 投影到目标维度然后拆成 num_heads 个“小头”每个小头单独计算注意力最后拼起来再投影一次。6.4 实现位置编码为了不依赖第三方库我们用 PyTorch 直接实现正弦位置编码。位置编码矩阵的 shape 为 [seq_len, d_model]初版只需要实现公式即可。class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len512): super(PositionalEncoding, self).__init__() # 初始化一个形状为 [max_len, d_model] 的位置编码矩阵 pe torch.zeros(max_len, d_model) # position 的形状为 [max_len, 1]表示每个位置的索引 position torch.arange(0, max_len, dtypetorch.float).unsqueeze(1) # div_term 的计算是公式中的分母部分 # 这里的 arange 步长为 2生成 0, 2, 4, ..., 对应公式中的 2i div_term torch.exp( torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model) ) # 偶数维度使用 sin奇数维度使用 cos pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) # pe 的形状为 [max_len, d_model]注册为 buffer不参与梯度更新 pe pe.unsqueeze(0) # [1, max_len, d_model] self.register_buffer(pe, pe) def forward(self, x): # x 的形状为 [batch_size, seq_len, d_model] seq_len x.size(1) return x self.pe[:, :seq_len, :]这里建议用register_buffer而不是普通的成员变量因为 buffer 会随着模型一起移动到 GPU因此self.pe不需要手动.cuda()也能在model.to(device)时自动迁移。6.5 构建一个简化的 Transformer Encoder 层有了多头注意力和位置编码我们可以拼装一个简化的 Transformer Encoder 层。这里省略了 LayerNorm 和前馈网络Feed-Forward Network但我会给出完整结构示例。class TransformerEncoderLayerSimple(nn.Module): def __init__(self, d_model, num_heads, d_ff, dropout0.1): super(TransformerEncoderLayerSimple, self).__init__() self.self_attn MultiHeadAttention(d_model, num_heads) self.feed_forward nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model), ) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.dropout nn.Dropout(dropout) def forward(self, x, maskNone): # 自注意力子层 残差连接 层归一化 attn_output, _ self.self_attn(x, x, x, mask) x x self.dropout(attn_output) x self.norm1(x) # 前馈网络子层 残差连接 层归一化 ff_output self.feed_forward(x) x x self.dropout(ff_output) x self.norm2(x) return x这段代码体现了 Transformer 中最重要的三个工程技巧残差连接Residual Connection让梯度可以跨层传递防止深层网络退化。层归一化Layer Normalization稳定训练过程。Dropout防止过拟合。6.6 运行与验证我们将上面几个模块组合起来构造一个简单的输入测试效果。假设 batch_size 2序列长度 10输入维度 d_model 648 个注意力头。batch_size 2 seq_len 10 d_model 64 num_heads 8 d_ff 128 # 随机生成输入张量 x torch.randn(batch_size, seq_len, d_model) # 位置编码 pe PositionalEncoding(d_model, max_lenseq_len) x pe(x) # Transformer Encoder 层 encoder_layer TransformerEncoderLayerSimple(d_model, num_heads, d_ff) output encoder_layer(x) print(输入形状:, x.shape) print(输出形状:, output.shape) print(输出数值范围:, output.min().item(), 到, output.max().item())预期输出类似输入形状: torch.Size([2, 10, 64]) 输出形状: torch.Size([2, 10, 64]) 输出数值范围: -2.0321 到 2.1876输出形状与输入形状保持一致这也是 Transformer Encoder 的一个特点序列长度和特征维度都不发生变化只对每个 token 的特征表示进行精炼。7. 常见问题与排查思路7.1 d_model 无法整除 num_heads错误现象AssertionError: d_model 必须能被 num_heads 整除可能原因MultiHeadAttention在初始化时要求 d_model 能整除 num_heads。因为每个头的维度 d_k d_model / num_heads如果除不尽拆分张量时就会出现形状不匹配。解决思路检查模型配置让 d_model num_heads × d_k。常见配置组合d_model512, heads8d_model768, heads12d_model1024, heads16。7.2 注意力 weights 出现 NaN错误现象attention_weights 中包含 nan可能原因输入张量中有 NaN通常是因为上游数据预处理的问题。学习率过大导致训练不稳定。softmax 的输入过大指数运算溢出。解决思路检查数据预处理确认输入中没有 NaN 或 inf。降低学习率。使用更大的数值精度或者添加torch.clamp限制 score 范围。7.3 解码器训练时未来信息泄露错误现象训练损失很低但生成效果很差。可能原因在解码器训练时没有使用 look-ahead mask模型在预测第 i 个词时能够看到第 i1 个词以及后续词。到了推理阶段模型只能看到已经生成的词如果训练阶段偷看了未来信息生成的性能自然会崩。解决思路在解码器自注意力中传入上三角 mask。上三角 mask 的构建方式如下def create_look_ahead_mask(seq_len): mask torch.triu(torch.ones(seq_len, seq_len), diagonal1) return mask 0这个 mask 中对角线和下三角位置为 True表示允许关注上三角位置为 False表示禁止关注。7.4 位置编码没有随模型迁移到 GPU错误现象运行model.to(cuda)后位置编码部分报错 “Expected all tensors to be on the same device”。可能原因位置编码被定义成了普通张量而不是通过register_buffer注册。解决思路使用self.register_buffer(pe, pe)注册位置编码。或者在前向传播中手动执行self.pe self.pe.to(x.device)。8. 最佳实践与工程建议8.1 理解 Mask 的三种类型Transformer 中会碰到三种 Mask建议在学习阶段就区分清楚Mask 类型用途形状Padding Mask忽略 padding 位置[batch_size, seq_len]Look-Ahead Mask防止解码器看到未来 token[seq_len, seq_len]组合 Mask同时忽略 padding 和未来 token[batch_size, seq_len, seq_len]在很多开源代码中Padding Mask 和 Look-Ahead Mask 会做成一个 mask 传入。每次使用时先理解清楚 mask 中 True/False 的含义再决定是masked_fill成 0 还是 -1e9这样能避免不少隐藏的 bug。8.2 注意数值稳定性在实现注意力时把 mask 位置填充为-1e9是为了让 softmax 输出趋近于 0。但如果 d_k 很大score 的数值本身就很大-1e9仍然可能导致数值不稳定。可以将 score 归一化后再填充或者在计算前先对 score 做torch.max平移。当然在工程中我们通常直接使用 PyTorch 的F.scaled_dot_product_attention或 Hugging Face 的Attention实现它们已经处理好了数值稳定性问题没必要自己造轮子。8.3 学习资源与调试建议在复习 Transformer 前置概念时建议按照下面的顺序逐步深入先用本篇文章的代码实现多头注意力和位置编码。阅读 Transformer 原文中的图 1 和图 2把每个模块和代码对应起来。打印中间张量的形状确认每个模块输入输出的 shape 变化。尝试去掉位置编码、去掉多头、去掉残差连接观察训练效果变化。对比 RNN/LSTM、CNN、Transformer 在同一个文本分类任务上的表现差异。如果想把 Transformer 技术纵深搞清楚还可以继续学习 Vision TransformerViT、Swin Transformer 以及基于时间卷积网络TCN与 Transformer 结合的时序预测模型。8.4 工程中的性能考虑虽然 Transformer 效果强大但在实际项目中需要注意自注意力 O(n²) 的复杂度。当输入序列长度超过 1024 或 2048 时训练和推理速度会明显下降。常见方案有对长文本做切分例如按 512 个 token 切块。使用稀疏注意力、滑动窗口注意力等变体。在图像任务中使用 Swin Transformer它把注意力限制在窗口内计算复杂度大幅降低。在推理阶段使用 KV Cache 缓存历史 Key 和 Value避免重复计算。这些优化方案都是在理解了基础 Transformer 之后才能做到的所以前置概念的基础越扎实后面学习优化方法的成本就越低。这篇文章从 Seq2Seq 的瓶颈讲起梳理了注意力机制、自注意力、位置编码、多头注意力的来龙去脉并用 PyTorch 手写了核心模块代码。理解了这些前置概念之后再去看完整的 Transformer 源码你会发现每个模块都有迹可循。如果这篇文章对你有帮助可以收藏备用需要的时候随时回来复习。