Skip to content

2.2 Transformer 架构核心:Self-Attention 图解

承前 在上一节中,我们走完了文本进入大模型的第一道关卡——分词。一段自然语言被切分为 Token 序列,再映射为整数 ID,最终转化为 Embedding 向量。到此为止,模型拥有的只是一串"各自为政"的数字:每个 Token 知道自己是谁,却不知道上下文中其他 Token 在说什么。然而,语言的含义恰恰诞生于 Token 之间的相互关系——"我"需要知道"爱"修饰的是谁,"你"需要知道它指向的是哪个人称。模型有了 Token 之后,究竟如何处理它们、如何让彼此"看见"对方?这正是本节要回答的核心问题。Transformer 架构用一种叫 Self-Attention 的机制,优雅地解决了这个问题。


2.2.1 Transformer 的诞生:一场架构革命

2017 年 6 月,Google 研究团队在论文《Attention Is All You Need》中提出了 Transformer 架构。这个标题本身就是一个宣言:你们之前依赖的那些东西——RNN、LSTM、GRU——都不需要了,只需要注意力机制就够了。

在 Transformer 出现之前,自然语言处理的序列建模几乎被循环神经网络(RNN)及其变体(LSTM、GRU)垄断。这些模型虽然在过去十年中取得了不少成果,但存在两个难以逾越的工程瓶颈:

  • 串行计算瓶颈:RNN 的核心特征是"逐步推进"——第 t 个时间步的计算必须等待第 t-1 个时间步的输出完成后才能开始。这意味着一个长度为 1000 的序列需要串行执行 1000 次前向传播,无法利用 GPU 的并行计算能力。训练一个大模型可能需要数周甚至数月。
  • 长距离依赖衰减:RNN 通过隐藏状态在时间步之间传递信息,但随着序列增长,早期 Token 的信息在传递过程中会不断衰减。虽然 LSTM 引入门控机制缓解了梯度消失问题,但当序列超过数百个 Token 后,模型仍然难以记住开头的内容。

Transformer 用 Self-Attention 机制一次性解决了这两个问题:所有 Token 同时计算彼此之间的关联,天然支持并行;同时,无论两个 Token 相隔多远,它们之间的注意力路径长度始终为 O(1),信息可以直接传递。

历史小记:Transformer 原本是作为机器翻译模型提出的——输入英文,输出中文。但人们很快发现,它的核心组件(Self-Attention、FFN、残差连接)远比翻译任务本身更有价值。随后 BERT、GPT 等模型接连诞生,最终演化为今天的大语言模型。一篇翻译论文,无意中开启了 AI 的新时代。

2.2.2 Self-Attention 的核心思路

理解 Self-Attention 之前,先思考一个问题:人类在阅读一句话时,大脑是如何运作的?

当你读到"这只猫没有过马路,因为它太累了"时,你的大脑会自动将"它"与"猫"联系起来,而不是"马路"。你之所以能做到这一点,是因为你在阅读每个词时,会"回看"之前读过的内容,判断哪些词与当前词最相关。

Self-Attention 就是让模型模拟这个过程:让序列中的每个 Token 都能直接关注到序列中所有其他 Token,并计算一个"相关度"权重,然后据此调整自己的表示

整个流程可以概括为四步:

第 1 步:输入句子 → "我 爱 你"

第 2 步:为每个 Token 生成 Q、K、V 三个向量

第 3 步:计算注意力分数(Q × K^T),经过 Softmax 归一化

第 4 步:用归一化后的权重对 V 加权求和,得到最终输出

看似简单,但其中每一步都有深刻的含义。接下来我们逐一拆解。

2.2.3 Q、K、V:图书馆找书的完整类比

Self-Attention 中最让初学者困惑的概念,莫过于 Query、Key、Value 这三个向量。它们的名字来自信息检索领域,但放到语言模型中,含义并不那么直观。我们用一个完整的图书馆类比来理解。

场景设定:你走进一个图书馆,想找一本关于"深度学习入门"的书。

Query(查询)—— 你手里的需求清单

你心里有一个明确的需求:"我想找一本深度学习入门的书,最好有代码示例。" 这个需求就是你的 Query。在 Self-Attention 中,当前 Token 就是那个"提需求的人",它生成一个 Query 向量,表示"我在当前上下文中,想关注什么样的信息"。

比如在句子"我爱北京"中,"爱"这个 Token 的 Query 可能编码了这样的意图:"我需要一个主语和一个宾语。"

Key(键)—— 书架上每本书的标签

图书馆里每本书的封面都贴有标签——"深度学习""机器学习""量子力学""中国历史"等。这些标签就是 Key。在 Self-Attention 中,每个 Token 都会生成一个 Key 向量,表示"我是什么样的信息"。

当你的需求(Query)和某本书的标签(Key)高度匹配时,这本书就更有可能被你选中。

Value(值)—— 书的实际内容

你选中了一本书后,真正对你有用的是书的内容,而不是它的标签。标签只能帮你找到这本书,但最终你要阅读的是内容。在 Self-Attention 中,每个 Token 生成一个 Value 向量,表示"如果你选择关注我,我能提供的信息就是这些"。

完整流程

步骤图书馆类比Self-Attention
1. 生成需求你想"深度学习入门"当前 Token 生成 Query 向量
2. 查看标签你浏览每本书的标签当前 Token 与所有 Token 的 Key 做点积
3. 打分标签越匹配,分数越高点积越大,注意力分数越高
4. 归一化你不会只选一本书,而是分配关注度Softmax 将分数转为概率分布
5. 阅读内容按关注度阅读选中的书用权重对 Value 加权求和,得到输出

这里有一个关键点值得强调:Q、K、V 不是预先设定的,而是模型在训练过程中学习到的。每个 Token 经过三个不同的线性变换(乘以三个不同的权重矩阵 W_Q、W_K、W_V),分别得到自己的 Query、Key、Value。这些权重矩阵的参数就是 Transformer 要学习的内容之一。

也就是说,"如何提问""如何描述自己""如何提供信息"这三件事,都是模型自己摸索出来的。

2.2.4 注意力公式:逐项拆解

理解了 Q、K、V 的含义后,我们来看 Self-Attention 的核心公式:

$$\text{Attention}(Q, K, V) = \text{softmax}!\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$

这个公式看起来简洁,但每一项都有明确的数学含义和设计动机。我们逐项拆解。

第一项:$QK^T$ —— 相似度计算

$QK^T$ 是 Query 矩阵和 Key 矩阵的转置做矩阵乘法。如果用单向量来看,它就是 Query 向量和 Key 向量的点积。

点积的几何意义是"两个向量的相似度"——方向越接近,点积越大;方向相反,点积为负。因此 $QK^T$ 的结果是一个 $n \times n$ 的矩阵(n 为序列长度),其中第 i 行第 j 列的元素表示第 i 个 Token 对第 j 个 Token 的关注程度(原始分数)。

举例:对于句子"我 爱 你"(3 个 Token),$QK^T$ 会产生一个 $3 \times 3$ 的注意力分数矩阵:

              我      爱      你
    我     [  2.1     0.5    -0.3  ]
    爱     [  1.8     3.2     1.9  ]
    你     [ -0.1     2.0     2.8  ]

第 2 行("爱")对应的分数为 [1.8, 3.2, 1.9],说明"爱"对"自己"的关注度最高(3.2),对"我"和"你"也有较高的关注度。这与我们的直觉一致:"爱"这个动词需要同时关注主语和宾语。

第二项:$\sqrt{d_k}$ —— 缩放因子

这是公式中最容易被忽视、却最关键的一步。为什么要在点积之后除以 $\sqrt{d_k}$(Key 向量维度的平方根)?

原因在于 softmax 函数的特性。当输入值很大时,softmax 的梯度会趋近于零——这被称为"梯度消失"。

假设 $d_k = 64$(Transformer 中常见的头维度),当 Q 和 K 的每个分量都服从均值为 0、方差为 1 的分布时,点积 $Q \cdot K = \sum_{i=1}^{64} q_i k_i$ 的方差为 $d_k = 64$(因为方差有可加性)。这意味着点积的值很容易达到几十甚至上百。

此时 softmax 的输入可能类似于 [100, -50, -30],经过 softmax 后会变成 [1.0, 0.0, 0.0]——某个位置的权重几乎为 1,其他全为 0。这种"赢家通吃"的分布意味着梯度几乎完全集中在那个位置,其他位置的梯度为零,模型几乎无法学习。

除以 $\sqrt{d_k} = \sqrt{64} = 8$ 后,点积的方差被缩放回 1 附近,softmax 的输入分布更加平缓,梯度也能正常传播。

一句话理解:$\sqrt{d_k}$ 是一个"温度调节器"。点积太大时 softmax 会变成"独裁"(一个位置垄断所有注意力),除以 $\sqrt{d_k}$ 让注意力分布更"民主",每个 Token 都有机会被关注到。

第三项:$\text{softmax}$ —— 归一化

Softmax 将原始分数转化为概率分布——每个分数变成 0 到 1 之间的值,且同一行所有分数之和为 1。这保证了注意力权重具有"比例"的含义:如果一个 Token 将 0.7 的注意力放在自己身上,0.2 放在前一个词身上,0.1 放在后一个词身上,那么它的输出主要由自己(70%)和前一个词(20%)的信息构成。

第四项:$\times V$ —— 加权求和

最后一步,用归一化后的注意力权重矩阵乘以 Value 矩阵,得到最终输出。对于每个 Token,它的输出就是所有 Token 的 Value 向量的加权和,权重就是刚才算出的注意力分数。

输出_i = Σ (注意力权重_ij × Value_j)
        j

这意味着:如果一个 Token 对另一个 Token 的注意力权重很高,那么它的输出就会更多地"吸收"那个 Token 的 Value 信息。信息就这样在 Token 之间流动了。

公式的完整直觉:整个 Attention 公式可以理解为"根据相似度加权聚合信息"。$QK^T$ 算出谁和谁相关,$\sqrt{d_k}$ 确保计算稳定,softmax 把相关度变成比例,最后用这个比例去 Value 里取信息。四个步骤环环相扣,构成了 Transformer 的信息处理核心。

2.2.5 Multi-Head Attention:多个视角看同一句话

单头 Self-Attention 有一个局限:它只能学到一种"关注模式"。但语言中的关系是多元的——一个词可能同时需要关注语法结构、语义关联、指代消解等不同层面的信息。

回到图书馆的类比:你找书时可能不止有一个标准。你既关心主题是否匹配,也关心出版年份是否够新,还关心作者是否有名。如果你只用一个"标签匹配"的标准,可能会错过一些好书。更好的做法是:同时从多个维度去评估每本书,最后综合所有维度的判断。

Multi-Head Attention 正是这个思路:把 Q、K、V 拆分成多组,每组独立计算注意力,最后拼接合并

具体来说,假设模型维度 $d_{model} = 512$,头数 $h = 8$,则每个头的维度 $d_k = 512 / 8 = 64$。每个头使用自己独立的 W_Q、W_K、W_V 权重矩阵,在不同子空间中计算注意力。

数学表达:

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

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

不同头可以学习到不同类型的关系。以句子"The animal didn't cross the street because it was too tired"为例:

注意力头可能学到的关系关注目标
Head 1语法结构(主谓宾)"it" → "animal"(主语指代)
Head 2语义关联(同义/反义)"tired" → "animal"(疲劳的主体)
Head 3位置关系(相邻词)"cross" → "street"(动宾搭配)
Head 4长距离依赖"it" → "animal"(跨越多个词)
Head 5功能词关联"because" → "tired"(因果连接)
Head 6-8其他隐含模式训练中自动发现

一个常见的疑问:8 个头的总参数量和单头一样吗?

答案是基本一样。单头 Attention 用一个 $512 \times 512$ 的 W_Q 矩阵;8 头 Attention 用 8 个 $512 \times 64$ 的 W_Q 矩阵,拼接后仍然是 $512 \times 512$。计算量也大致相同。多头并不是"多算",而是"换一种切分方式"——把高维空间拆成多个低维子空间,让模型在不同子空间中独立学习不同的关系模式。

设计直觉:想象一个有 512 个评分维度的评审系统。单头是让一个评审员用全部 512 个维度打分;多头是把 512 个维度拆成 8 组,让 8 个评审员各用 64 个维度独立打分,最后汇总。后者更灵活——不同评审员可以关注不同方面,不会互相干扰。

2.2.6 FFN:注意力之后的"思考层"

Self-Attention 让 Token 之间交换了信息,但光交换是不够的——每个 Token 还需要对交换来的信息进行"加工"和"提炼"。这个工作由前馈神经网络(Feed-Forward Network,FFN)完成。

每个 Attention 层之后,都会接一个 FFN,它对每个位置独立地进行两层线性变换和一次非线性激活:

$$\text{FFN}(x) = \text{ReLU}(xW_1 + b_1)W_2 + b_2$$

Transformer 原论文使用的激活函数是 ReLU,但在后续的模型中,人们发现 GELU(BERT、GPT-2)和 SwiGLU(Llama)效果更好。FFN 的典型维度设置是:隐藏层维度为模型维度的 4 倍。例如 $d_{model} = 512$ 时,FFN 的隐藏层维度为 2048。这种"先扩展再压缩"的瓶颈结构,让模型有足够的中间维度来表达复杂的非线性变换。

FFN 的核心作用包括:

  • 引入非线性:Attention 本身只涉及线性变换(矩阵乘法)和 softmax,表达能力有限。FFN 通过激活函数引入非线性,使模型能够拟合更复杂的函数。
  • 增加模型容量:FFN 的参数量通常占整个 Transformer 层参数的约 2/3,是模型"知识"的主要存储位置。研究表明,大模型的大部分事实知识存储在 FFN 的权重中。
  • 逐位置独立处理:FFN 对序列中的每个位置独立处理,不进行跨位置的交互。跨位置的交互由 Attention 负责,位置内的变换由 FFN 负责——两者分工明确。

分工直觉:Attention 负责"横向沟通"(Token 之间交换信息),FFN 负责"纵向深化"(每个 Token 独立加工信息)。一个 Transformer 层 = 一次"沟通" + 一次"沉思"。

2.2.7 残差连接与 LayerNorm:训练稳定的基石

深层神经网络训练中最大的敌人是梯度消失——随着层数增加,梯度在反向传播过程中不断衰减,导致底层参数几乎不更新。Transformer 通过两个组件来对抗这个问题。

残差连接(Residual Connection)

残差连接的做法非常简单:把子层的输入直接加到输出上。

$$\text{output} = x + \text{Sublayer}(x)$$

这意味着每个子层实际学习的不是"完整的目标函数",而是"目标函数与输入之间的残差"。当残差接近零时,子层可以"什么都不做"(恒等映射),这给网络提供了退路——如果某个层不需要做太多变换,它可以学一个接近零的残差,而不需要费力地把输出学成输入。

从梯度流动的角度看:反向传播时,梯度可以沿着残差连接"跳过"子层直接传递,不需要经过子层内部的参数,从而有效缓解梯度消失。

Layer Normalization(层归一化)

LayerNorm 对每个样本在特征维度上做归一化:计算一个 Token 所有特征维度的均值和方差,然后将其标准化为均值 0、方差 1,再通过可学习的缩放和平移参数恢复表达能力。

python
# LayerNorm 的核心计算(伪代码)
mean = x.mean(dim=-1, keepdim=True)    # 沿特征维度求均值
var = x.var(dim=-1, keepdim=True)      # 沿特征维度求方差
x_norm = (x - mean) / (var + eps).sqrt()  # 标准化
output = x_norm * gamma + beta          # 可学习的缩放和平移

与 BatchNorm 不同,LayerNorm 不依赖 batch 中的其他样本,因此对 batch size 不敏感,非常适合序列模型。

Transformer 中每个子层都使用 "Add & Norm" 结构:

$$\text{output} = \text{LayerNorm}(x + \text{Sublayer}(x))$$

后来的研究发现,先 LayerNorm 再做子层变换(Pre-LN 结构)比原始的 Post-LN 训练更稳定,因此大多数现代大模型(GPT、Llama)都采用 Pre-LN。

训练直觉:残差连接是"高速公路",让信息和梯度可以快速穿越多层;LayerNorm 是"路面维护",确保每一层的输入分布稳定。两者配合,使得 Transformer 可以堆叠到几十甚至上百层。

2.2.8 位置编码:让模型知道"谁在前"

Self-Attention 有一个本质特征:它对输入序列的顺序完全无感。如果你把"我 爱你"打乱成"你 爱 我",Self-Attention 的计算过程完全不变(只是结果不同),因为 Q、K、V 的计算不包含任何位置信息。

这在语言处理中显然不合理——"狗 咬 人"和"人 咬 狗"的含义截然不同。为了让模型感知 Token 的顺序,Transformer 引入了位置编码(Positional Encoding)。

原论文使用正弦/余弦函数生成固定位置编码:

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

不同维度使用不同频率的正弦波,使得每个位置的编码向量都唯一,且相邻位置的编码向量相似、距离较远的位置编码向量差异较大。位置编码被直接加到 Embedding 上,让模型在后续计算中可以利用位置信息。

后续的大模型对位置编码做了很多改进:

  • 可学习位置编码(BERT、GPT):不再用固定公式,而是为每个位置分配一个可学习的向量。简单有效,但无法外推到训练时未见过的长度。
  • 旋转位置编码(RoPE)(Llama、Qwen):不直接加到 Embedding 上,而是在 Q 和 K 上施加旋转操作,使得相对位置信息编码在 Q·K 的点积中。这是目前大模型最主流的位置编码方案,因为它支持长度外推——训练时见过的序列长度是 4096,推理时可以扩展到更长的上下文。
  • ALiBi(BLOOM):通过在注意力分数上加一个与距离成比例的偏置来编码位置,同样支持外推。

设计直觉:Self-Attention 是"近视眼"——它只看内容,不看位置。位置编码就是给它配的"眼镜",让它知道每个词站在哪个位置上。

2.2.9 Encoder-Decoder vs Decoder-Only:架构的岔路

Transformer 原论文使用的是完整的 Encoder-Decoder 架构,但后来发展出了三种主要变体。理解它们之间的差异,对于选择和使用大模型至关重要。

三种架构概览

架构类型代表模型注意力类型典型用途
Encoder-OnlyBERT、RoBERTa双向 Self-Attention理解类任务(分类、NER)
Encoder-DecoderT5、BARTEncoder 双向 + Decoder 单向 + Cross-Attention转换类任务(翻译、摘要)
Decoder-OnlyGPT、Llama、Qwen单向 Self-Attention(因果掩码)生成类任务(对话、写作)

Encoder-Decoder 详解

这是 Transformer 最原始的架构,由两部分组成:

  • Encoder(编码器):使用双向 Self-Attention,每个 Token 可以看到前后所有 Token。它的任务是"理解"输入序列,将其编码为丰富的上下文表示。典型的 Encoder 有 6 层(原论文设置),每层包含 Self-Attention + FFN + Add & Norm。
  • Decoder(解码器):使用单向 Self-Attention(因果掩码,只能看到当前位置及之前的 Token)+ Cross-Attention(关注 Encoder 的输出)。它的任务是"生成"输出序列,每一步根据已生成的 Token 和 Encoder 的表示来预测下一个 Token。

Cross-Attention 是 Encoder-Decoder 的独特组件:Decoder 中的 Query 来自 Decoder 自身,而 Key 和 Value 来自 Encoder 的输出。这就像翻译时"看着原文写译文"——Decoder 每生成一个词,都会去 Encoder 的表示中"查询"相关的源语言信息。

Decoder-Only 详解

Decoder-Only 是 GPT 系列开创的架构,它去掉了 Encoder,只保留 Decoder——但简化了 Decoder 的结构,去掉了 Cross-Attention,只保留因果 Self-Attention 和 FFN。

Encoder-Decoder 的一个 Decoder 层:
    → Masked Self-Attention → Add & Norm
    → Cross-Attention        → Add & Norm    ← 独有
    → FFN                    → Add & Norm

Decoder-Only 的一个层:
    → Masked Self-Attention → Add & Norm
    → FFN                    → Add & Norm

为什么 Decoder-Only 成为主流?

  1. 架构简洁:没有 Encoder 和 Cross-Attention,参数更少,实现更简单,训练和推理的工程复杂度更低。
  2. 自回归生成天然适配:GPT 式的"预测下一个 Token"任务天然适合因果掩码的结构——当前位置只能看到前面已生成的内容,正好模拟了"从左到右生成文本"的过程。
  3. 训练效率高:因果掩码使得训练时可以一次性计算所有位置的预测(用 mask 遮住未来的 Token),而无需像 RNN 那样逐步计算。一个序列的每个位置都提供一个训练信号,训练效率极高。
  4. Scaling Law 验证:OpenAI 的研究发现,在参数量和训练数据量增大时,Decoder-Only 架构的性能提升最稳定、最可预测。这为后来"大力出奇迹"的 GPT-3 及后续大模型铺平了道路。
  5. 通用性:Decoder-Only 通过不同的 prompt 就能完成理解、翻译、摘要等各种任务,不需要像 Encoder-Only 那样为每种任务设计特定的输出头。这种"一个模型做所有事"的特性,正是 Agent 时代所需要的。

详细对比表

对比维度Encoder-Only (BERT)Encoder-Decoder (T5)Decoder-Only (GPT/Llama)
Self-Attention 方向双向Encoder 双向,Decoder 单向单向(因果掩码)
Cross-Attention
训练目标Masked Language ModelSpan CorruptionNext Token Prediction
推理方式非自回归自回归(Decoder 部分)自回归
长度外推受限受限支持(RoPE 等技术)
参数效率中等较低(参数分散在两部分)较高
生成能力弱(非原生)最强
理解能力最强较强(足够大多数场景)
工程复杂度最低
适合场景文本分类、NER、句向量翻译、摘要对话、写作、代码、Agent

选型建议:如果你要做纯粹的文本理解任务(如情感分类),Encoder-Only 仍然有优势。但如果你要构建 Agent、做对话或文本生成,Decoder-Only 是当前事实上的标准。本书的后续内容都基于 Decoder-Only 架构展开。

2.2.10 代码实现:手写 Self-Attention

纸上得来终觉浅。下面我们用 PyTorch 从零实现一个完整的 Multi-Head Self-Attention,并在每一步标注张量维度变化,帮助你建立从公式到代码的直觉。

python
import torch
import torch.nn as nn
import torch.nn.functional as F

class MultiHeadSelfAttention(nn.Module):
    def __init__(self, d_model=512, n_heads=8):
        """
        初始化 Multi-Head Self-Attention
        
        参数:
            d_model: 模型维度,默认 512
            n_heads: 注意力头数,默认 8
        """
        super().__init__()
        self.d_model = d_model          # 模型维度: 512
        self.n_heads = n_heads          # 头数: 8
        self.d_k = d_model // n_heads   # 每个头的维度: 512 // 8 = 64

        # 四个线性变换矩阵,每个都是 (d_model, d_model) 的权重
        # W_q: 将输入投影为 Query
        # W_k: 将输入投影为 Key
        # W_v: 将输入投影为 Value
        # W_o: 多头输出合并后再做一次线性变换
        self.W_q = nn.Linear(d_model, d_model)  # (512, 512)
        self.W_k = nn.Linear(d_model, d_model)  # (512, 512)
        self.W_v = nn.Linear(d_model, d_model)  # (512, 512)
        self.W_o = nn.Linear(d_model, d_model)  # (512, 512)

    def forward(self, x, mask=None):
        """
        前向传播
        
        参数:
            x: 输入张量,形状 (batch_size, seq_len, d_model)
            mask: 可选的注意力掩码,形状 (batch_size, 1, seq_len, seq_len)
        
        返回:
            output: 注意力输出,形状 (batch_size, seq_len, d_model)
            attn_weights: 注意力权重,形状 (batch_size, n_heads, seq_len, seq_len)
        """
        # ──────────────────────────────────────
        # 第 1 步:生成 Q, K, V
        # ──────────────────────────────────────
        # 输入 x 形状: (batch_size, seq_len, d_model) = (2, 10, 512)
        batch_size, seq_len, _ = x.shape

        # 线性变换: x @ W^T
        # (2, 10, 512) @ (512, 512) → (2, 10, 512)
        Q = self.W_q(x)  # Query: (2, 10, 512)
        K = self.W_k(x)  # Key:   (2, 10, 512)
        V = self.W_v(x)  # Value: (2, 10, 512)

        # ──────────────────────────────────────
        # 第 2 步:拆分为多头
        # ──────────────────────────────────────
        # 将最后一维 d_model=512 拆分为 (n_heads=8, d_k=64)
        # (2, 10, 512) → view → (2, 10, 8, 64)
        # 然后 transpose 交换第 1 维和第 2 维:
        # (2, 10, 8, 64) → transpose(1,2) → (2, 8, 10, 64)
        # 最终形状: (batch_size, n_heads, seq_len, d_k)
        Q = Q.view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
        # Q: (2, 8, 10, 64)
        K = K.view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
        # K: (2, 8, 10, 64)
        V = V.view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
        # V: (2, 8, 10, 64)

        # ──────────────────────────────────────
        # 第 3 步:计算注意力分数
        # ──────────────────────────────────────
        # Q @ K^T: 每个 head 内部做点积
        # Q: (2, 8, 10, 64), K^T: (2, 8, 64, 10)
        # matmul → (2, 8, 10, 10)
        # 这就是注意力分数矩阵,每个 head 一个 10×10 的矩阵
        scores = torch.matmul(Q, K.transpose(-2, -1))
        # scores: (2, 8, 10, 10)

        # 缩放: 除以 √d_k = √64 = 8
        # 防止点积过大导致 softmax 梯度消失
        scores = scores / (self.d_k ** 0.5)
        # scores: (2, 8, 10, 10)  ← 维度不变,只是数值缩放

        # 如果提供了 mask(如因果掩码),将需要遮蔽的位置设为 -inf
        # 这样 softmax 后这些位置的权重为 0
        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))

        # ──────────────────────────────────────
        # 第 4 步:Softmax 归一化
        # ──────────────────────────────────────
        # 沿最后一维(seq_len 维)做 softmax
        # 每行的分数变成概率分布,和为 1
        # (2, 8, 10, 10) → softmax(dim=-1) → (2, 8, 10, 10)
        attn_weights = F.softmax(scores, dim=-1)
        # attn_weights: (2, 8, 10, 10)  ← 每行和为 1

        # ──────────────────────────────────────
        # 第 5 步:加权求和
        # ──────────────────────────────────────
        # 注意力权重 @ V
        # attn_weights: (2, 8, 10, 10), V: (2, 8, 10, 64)
        # matmul → (2, 8, 10, 64)
        # 每个 Token 的输出 = 所有 Token 的 Value 加权和
        output = torch.matmul(attn_weights, V)
        # output: (2, 8, 10, 64)

        # ──────────────────────────────────────
        # 第 6 步:合并多头
        # ──────────────────────────────────────
        # 先 transpose 回来: (2, 8, 10, 64) → (2, 10, 8, 64)
        # 再 view 合并最后两维: (2, 10, 8, 64) → (2, 10, 512)
        # contiguous() 确保内存连续(transpose 后需要调用)
        output = output.transpose(1, 2).contiguous().view(
            batch_size, seq_len, self.d_model
        )
        # output: (2, 10, 512)  ← 恢复为模型维度

        # ──────────────────────────────────────
        # 第 7 步:输出投影
        # ──────────────────────────────────────
        # 最后一次线性变换,让多头信息融合
        # (2, 10, 512) @ (512, 512) → (2, 10, 512)
        output = self.W_o(output)
        # output: (2, 10, 512)

        return output, attn_weights


# ===============================
# 运行测试
# ===============================
if __name__ == "__main__":
    # 创建模型实例
    attn = MultiHeadSelfAttention(d_model=512, n_heads=8)

    # 创建模拟输入: batch=2, seq_len=10, d_model=512
    # 这模拟了 2 个句子,每个句子 10 个 Token,每个 Token 是 512 维向量
    x = torch.randn(2, 10, 512)

    # 前向传播
    output, weights = attn(x)

    # 验证维度
    print(f"输入形状:            {x.shape}")        # (2, 10, 512)
    print(f"输出形状:            {output.shape}")    # (2, 10, 512)
    print(f"注意力权重形状:      {weights.shape}")   # (2, 8, 10, 10)

    # 验证注意力权重的性质
    # 每一行应该和为 1(softmax 的性质)
    row_sum = weights[0, 0, 0, :].sum().item()
    print(f"第一行注意力权重之和: {row_sum:.6f}")     # 应接近 1.0

运行上述代码,你会看到如下输出:

输入形状:            torch.Size([2, 10, 512])
输出形状:            torch.Size([2, 10, 512])
注意力权重形状:      torch.Size([2, 8, 10, 10])
第一行注意力权重之和: 1.000000

维度变化速查表

步骤操作张量形状
输入(2, 10, 512)
线性变换x @ W_q(2, 10, 512)
拆分多头view + transpose(2, 8, 10, 64)
QK^Tmatmul(2, 8, 10, 10)
缩放÷√d_k(2, 8, 10, 10)
Softmaxsoftmax(dim=-1)(2, 8, 10, 10)
加权求和@ V(2, 8, 10, 64)
合并多头transpose + view(2, 10, 512)
输出投影@ W_o(2, 10, 512)

2.2.11 可视化注意力权重

光看代码和数字还不够直观,我们可以将注意力权重可视化为热力图,直观感受模型"在看哪里"。

python
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

# 取第一个样本、第一个注意力头的权重矩阵
# weights 形状: (2, 8, 10, 10)
# 取 [0, 0] 后形状: (10, 10)
head_weights = weights[0, 0].detach().numpy()

# 假设我们的 10 个 Token 对应句子:
tokens = ["我", "爱", "北", "京", "天", "安", "门", "广", "场", "。"]

plt.figure(figsize=(10, 8))
sns.heatmap(
    head_weights,
    annot=True,              # 在每个格子中显示数值
    fmt='.2f',               # 保留两位小数
    cmap='Blues',            # 使用蓝色色系
    xticklabels=tokens,      # X 轴标签(被关注的 Token)
    yticklabels=tokens,      # Y 轴标签(发起关注的 Token)
    linewidths=0.5,          # 格子之间的线宽
)
plt.title('Self-Attention 权重热力图 (Head 1)')
plt.xlabel('Key (被关注的 Token)')
plt.ylabel('Query (发起关注的 Token)')
plt.tight_layout()
plt.savefig('/tmp/attention_weights.png', dpi=150)
plt.show()

在这张热力图中,颜色越深表示注意力权重越高。你可以观察到:

  • 对角线通常颜色较深——每个 Token 对自己的关注度较高
  • "天""安""门"三个字之间可能有较高的注意力权重——因为它们组合成一个地名
  • "爱"字可能同时关注"我"和"北京"——因为它需要连接主语和宾语

实践建议:注意力可视化是理解模型行为的重要工具。在实际工作中,你可以用 transformers 库的 BertVizattention-viz 等工具可视化真实大模型的注意力分布,观察不同层、不同头关注的模式。

2.2.12 因果掩码:Decoder 的"未来不可见"

在上面的代码中,有一个 mask 参数我们没有详细解释。对于 Decoder-Only 架构,这个参数至关重要。

在自回归生成中,当前位置的 Token 只能看到它之前的 Token,不能"偷看"未来。否则,模型在训练时就直接知道了答案,不需要学习任何东西。

实现方式是使用一个上三角矩阵掩码:

python
# 创建因果掩码(Causal Mask)
# seq_len = 10
causal_mask = torch.tril(torch.ones(10, 10))
# causal_mask:
# [[1, 0, 0, ..., 0],
#  [1, 1, 0, ..., 0],
#  [1, 1, 1, ..., 0],
#  ...
#  [1, 1, 1, ..., 1]]

# 在 attention 计算中应用
# mask == 0 的位置填 -inf,softmax 后变为 0
scores = scores.masked_fill(causal_mask == 0, float('-inf'))

这样,第 1 个 Token 只能关注自己,第 2 个 Token 可以关注前两个,以此类推。这保证了训练时模型无法"作弊"。

2.2.13 常见误区

在学习 Transformer 的过程中,初学者容易产生以下误解。我们逐一澄清。

误区 1:"注意力权重就是注意力机制"

这是一个术语混淆。注意力权重(attention weights)是 Self-Attention 计算过程中 softmax 输出的那个矩阵——它只是"谁关注谁、关注多少"的数值。而注意力机制(attention mechanism)是一种计算框架——通过 Q、K、V 三个投影和点积运算来动态决定信息如何流动。前者是后者的输出之一,后者是前者的计算过程。

误区 2:"Transformer 就是 GPT"

Transformer 是一种架构,GPT 是基于 Transformer 的一种具体模型。它们的关系类似于"框架"和"应用":Transformer 定义了 Self-Attention、FFN、残差连接等基本组件,而 GPT 选择使用其中的 Decoder-Only 部分,配合特定的训练目标(Next Token Prediction)和训练数据。BERT、T5、ViT(视觉 Transformer)都基于 Transformer 架构,但它们是不同的模型。

误区 3:"Self-Attention 是 Transformer 的专利"

实际上,注意力机制在 Transformer 之前就已被广泛使用。2014 年 Bahdanau 在神经机器翻译中提出了注意力机制,让 Decoder 动态地关注 Encoder 的不同位置。Transformer 的创新在于将注意力机制用于"自注意力"(Self-Attention),即让序列内的 Token 互相做注意力,而不需要外部的 Encoder-Decoder 交互。

误区 4:"多头越多越好"

并非如此。头数的选择取决于模型维度和任务特点。如果头数过多而每个头的维度太小(如 $d_k = 8$),单个头的表达能力会不足,注意力分布可能退化。实践中,$d_k$ 通常在 64 到 128 之间。Llama 7B 使用 32 个头、$d_k = 128$;GPT-3 175B 使用 96 个头、$d_k = 128$。

误区 5:"Self-Attention 已经是完美的,不需要改进了"

Self-Attention 的一个主要局限是其计算复杂度为 $O(n^2 d)$,其中 $n$ 是序列长度。当序列长度达到数万(如长文档处理)时,计算和内存开销会急剧增长。这也是为什么后续研究提出了 Flash Attention(GPU I/O 优化)、Linear Attention(线性近似)、滑动窗口注意力等优化方法。Transformer 的基本原理虽然经典,但在工程层面仍在不断演进。

误区 6:"注意力权重高就代表模型认为这个词重要"

注意力权重反映的是"在当前层、当前头中,某个 Token 从其他 Token 那里获取了多少信息",但不等同于"重要性"。同一个 Token 在不同层、不同头中的注意力分布可能完全不同,而且高权重可能只是因为两个 Token 在语法上紧密关联(如冠词和名词),而非语义上重要。将注意力权重直接作为"模型可解释性"的工具时需要谨慎。

2.2.14 本节小结

要点说明
Self-Attention 核心让每个 Token 直接与所有 Token 计算相关度,实现信息全局流动
Q/K/V 三角色Q 是"我在找什么",K 是"我是什么",V 是"我能提供什么"
注意力公式$\text{softmax}(QK^T/\sqrt{d_k})V$,缩放因子防止梯度消失
Multi-Head Attention将 Q/K/V 拆分到多个子空间并行计算,捕捉不同维度的关系
FFN两层线性变换 + 激活函数,引入非线性,存储知识
残差连接 + LayerNorm缓解梯度消失,稳定深层网络训练
位置编码为位置无感的 Self-Attention 注入顺序信息(RoPE 为主流方案)
因果掩码Decoder-Only 的核心机制,确保训练时不"偷看"未来 Token
三种架构Encoder-Only 适合理解,Encoder-Decoder 适合转换,Decoder-Only 适合生成(主流)

回顾本节,我们从一个核心问题出发——"Token 之间如何相互感知"——逐步拆解了 Transformer 的信息处理引擎:Self-Attention 让 Token 互相"看见",Multi-Head 让模型从多个角度"理解"关系,FFN 让每个 Token 独立"深思",残差连接和 LayerNorm 让整个系统"稳定运行"。这套机制虽然看似简洁,却支撑了从 BERT 到 Llama 的所有现代大模型。

启后 理解了 Transformer 的架构原理,下一个自然的问题是:这个架构是如何从零开始被训练成一个"会说话"的模型的?预训练阶段发生了什么?SFT 和 RLHF 又在做什么?下一节"大模型训练流程"将带你走完从架构到模型的最后一程。


参考资料

  1. Attention Is All You Need (Vaswani et al., 2017) — Transformer 原论文 https://arxiv.org/abs/1706.03762

  2. The Illustrated Transformer (Jay Alammar) — 图解 Transformer,最直观的教程 https://jalammar.github.io/illustrated-transformer/

  3. The Annotated Transformer (Harvard NLP) — 逐行注释的 Transformer 实现 https://nlp.seas.harvard.edu/2018/04/03/attention.html

  4. Transformer 架构详解 (CSDN) — 中文详细解读 https://blog.csdn.net/TracelessLe/article/details/107132038

  5. GPT-2 论文 — Decoder-Only 架构的代表作 https://d4mucfpksywv.cloudfront.net/better-language-models/language_models_are_unsupervised_multitask_learners.pdf