第20章 Transformer:现代大模型的核心结构
假设你正在翻译一句话。读到最后一个词时,前面的内容仍可能决定它的含义。循环神经网络的办法,是从左到右维护一份不断更新的隐藏状态;第 19 章的注意力机制则允许模型在需要时回看不同位置。2017 年,论文《Attention Is All You Need》提出了更彻底的方案:把注意力放到主体位置,让每个位置直接从其他相关位置收集信息。(Vaswani 等 2017年)
这个架构被命名为 Transformer。今天的大语言模型与原论文相比已经改变了位置编码、归一化方式、激活函数和训练规模,但一条核心数据流仍然清晰可辨:
Token 先变成向量;位置信息进入向量;自注意力在 Token 之间传递信息;前馈网络在每个位置加工信息;许多层重复这一过程;输出层把最终向量变成下一个 Token 的概率。
本章要做的,是跟随一批数据完整走过这条路。普通读者可以先抓住每个部件“负责什么”;技术深潜则会把矩阵形状、掩码、训练目标和计算代价逐项展开。
为什么不再排队传话
循环神经网络(RNN)像一支排队传话的队伍。第 \(t\) 个位置接收第 \(t-1\) 个位置留下的隐藏状态,再与当前输入一起产生新状态。顺序天然包含在计算过程中,但也带来两个困难。
第一,长距离信息要经过许多步才能到达。即使 LSTM 的门控机制缓解了梯度消失,早期信息仍需穿过一条长路径。第二,训练时各位置存在前后依赖,难以像矩阵乘法那样充分并行。
自注意力改变了信息路线。句子中的每个位置都可以直接计算它与其他位置的相关程度,并把相关位置的信息加权汇总。两个相距很远的词,在一层注意力中就可能建立联系。
| 结构 | 信息怎样移动 | 主要优势 | 主要代价 |
|---|---|---|---|
| RNN | 沿序列逐步传递隐藏状态 | 顺序自然、单步状态紧凑 | 路径长,训练并行度有限 |
| 自注意力 | 任意允许的位置可直接交互 | 路径短,训练可并行 | 标准注意力矩阵随序列长度平方增长 |
“不使用循环”并不意味着模型不再关心顺序。恰恰相反,顺序必须通过另一个部件明确加入,否则交换两个 Token 后,自注意力本身无法知道谁先谁后。
第一步:把文本变成 Token
模型不能直接接收“春风又绿江南岸”这串视觉字符。文本首先经过 Tokenization,被切分为一串离散单位,每个单位称为 Token。Token 可能是一个汉字、一个词、一个子词片段、标点,甚至是字节序列的一部分。
例如,一个只按汉字切分的玩具 Tokenizer 可能得到:
原文:春风又绿江南岸
Token:春 | 风 | 又 | 绿 | 江 | 南 | 岸
编号: 17 | 42 | 08 | 31 | 55 | 12 | 63
真实大模型通常使用子词或字节级方案,在词表大小、序列长度和生僻文本处理之间折中。BPE 一类方法通过反复合并常见符号对,构造可复用的子词单位。(Sennrich 等 2016年) Token 不是天然存在于语言中的唯一正确分界;它是模型设计的一部分。
Tokenization 会影响三件实在的事:
- 同一段文字占用多少上下文长度;
- 模型需要学习多少个词表条目;
- 生僻词、数字、代码和不同语言被拆成什么样。
因此,“模型读了多少字”和“模型处理了多少 Token”不是同一个量。
第二步:编号变成向量
Token 编号只是词表中的索引。编号 63 不比编号 17 “更大”或“更有意义”。模型通过一个可训练的 嵌入矩阵(embedding matrix)把每个编号查成向量。
设词表大小为 \(V\),模型宽度为 \(d_{\text{model}}\),嵌入矩阵为:
\[ E \in \mathbb{R}^{V \times d_{\text{model}}} \]
输入批次包含 \(B\) 段序列,每段长度为 \(T\),则 Token 编号的形状为 \(B\times T\)。查表后得到:
\[ X \in \mathbb{R}^{B \times T \times d_{\text{model}}} \]
现在,每个位置不再是一个整数,而是一个长度为 \(d_{\text{model}}\) 的向量。训练开始时,这些向量通常没有语言意义;在预测误差的反向传播中,经常出现在类似上下文中的 Token 会受到相关的参数更新,表示逐渐形成。
核心机制:表示是训练出来的
Embedding 不是一本由人提前填写的词义词典。它是模型参数的一部分,由训练目标间接塑造。模型学到的是“哪些向量有助于降低预测误差”,不保证每个维度都对应一个可命名的人类概念。
第三步:告诉模型位置
如果只把一组向量交给自注意力,“猫追狗”和“狗追猫”包含相同的 Token 集合。要区分顺序,模型必须获得位置信息。
原始 Transformer 使用固定的正弦、余弦位置编码,把不同频率的周期信号加到 Token Embedding 上。其一组分量写作:
\[ \begin{aligned} PE(pos, 2i) &= \sin\left(pos / 10000^{2i/d_{\text{model}}}\right) \\ PE(pos, 2i+1) &= \cos\left(pos / 10000^{2i/d_{\text{model}}}\right) \end{aligned} \]
这里 \(pos\) 是位置,\(i\) 是向量维度索引。不同频率让每个位置拥有不同模式,也让相对位移能够从这些模式中被利用。
位置进入模型不只有这一种办法。后来系统使用过可学习的绝对位置向量、相对位置偏置、旋转位置编码等。它们的共同任务不是“给每个词贴页码”这么简单,而是让注意力计算能够区分顺序和距离。
第四步:每个位置提出查询
进入自注意力层时,每个位置的向量会经过三组不同的线性变换,得到 Query(查询)、Key(键) 和 Value(值):
\[ Q=XW^Q, \qquad K=XW^K, \qquad V=XW^V \tag{1}\]
可以借用检索的直觉理解它们:
- Query 表示“当前位置正在寻找什么信息”;
- Key 表示“这个位置提供的信息适合被怎样匹配”;
- Value 表示“如果这个位置被选中,实际传递什么内容”。
这个比喻只能帮助入门。Q、K、V 都是由数据学习出的向量,并没有人给某一维标记“主语”或“时间”。同一个输入向量经过不同矩阵投影,是为了把“怎样匹配”和“传递什么”分开。
对于一个注意力头,先计算每个 Query 与所有 Key 的点积,再除以 \(\sqrt{d_k}\):
\[ S = \frac{QK^T}{\sqrt{d_k}} \tag{2}\]
点积越大,表示当前 Query 与某个 Key 在模型学到的匹配空间里越契合。除以 \(\sqrt{d_k}\) 是为了控制数值尺度;当维度增大时,未经缩放的点积方差会变大,softmax 容易进入梯度很小的饱和区域。
接下来对每一行做 softmax,得到总和为 1 的权重,再加权汇总 Value:
\[ \operatorname{Attention}(Q,K,V) = \operatorname{softmax}\left(\frac{QK^T}{\sqrt{d_k}} + M\right)V \tag{3}\]
\(M\) 是可选的掩码矩阵。输出中每个位置的向量,已经混入了它所关注位置的 Value 信息。
一个四 Token 的形状追踪
暂时忽略批次,设序列有 4 个 Token,模型宽度为 8,一个头的 Query 和 Key 宽度为 4:
| 张量 | 形状 | 含义 |
|---|---|---|
| \(X\) | \(4\times8\) | 4 个位置,每个位置 8 维 |
| \(Q,K,V\) | \(4\times4\) | 每个位置投影出 4 维查询、键和值 |
| \(QK^T\) | \(4\times4\) | 每个位置对每个位置的匹配分数 |
| \(A=\operatorname{softmax}(S)\) | \(4\times4\) | 每行是一组注意力权重 |
| \(AV\) | \(4\times4\) | 每个位置汇总后的信息 |
\(4\times4\) 的注意力矩阵是关键:行表示“谁在查询”,列表示“它查看谁”。序列长度从 4 增至 4,000 时,这个矩阵的元素数量从 16 增至 1,600 万,而且每个批次、每层、每个头都要进行相应计算。
因果掩码:不能偷看答案
要训练一个从左到右预测下一个 Token 的语言模型,位置 \(t\) 只能使用位置 \(t\) 及以前的信息。如果它在训练时看到未来 Token,任务就会泄漏答案。
因果掩码(causal mask)把未来位置的注意力分数设为负无穷。softmax 后,这些位置的权重变成 0:
\[ M_{ij}= \begin{cases} 0, & j\le i \\ -\infty, & j>i \end{cases} \]
四个位置允许查看的范围如下:
| 查询位置 | 可见位置 1 | 可见位置 2 | 可见位置 3 | 可见位置 4 |
|---|---|---|---|---|
| 1 | 可见 | - | - | - |
| 2 | 可见 | 可见 | - | - |
| 3 | 可见 | 可见 | 可见 | - |
| 4 | 可见 | 可见 | 可见 | 可见 |
这里有一个容易混淆的事实:训练时,整段序列的所有位置仍然可以用矩阵并行计算。掩码限制的是信息流向,不是强迫硬件从左到右逐位置运行。
多头注意力:同时学习不同匹配方式
单个注意力头只在一组投影空间里比较 Token。多头注意力(multi-head attention)把模型宽度分成多个头,每个头拥有自己的 \(W^Q,W^K,W^V\),独立产生注意力结果,再拼接并投影回模型宽度:
\[ \begin{aligned} \text{head}_h &= \operatorname{Attention}(XW_h^Q,XW_h^K,XW_h^V) \\ \operatorname{MHA}(X) &= \operatorname{Concat}(\text{head}_1,\ldots,\text{head}_H)W^O \end{aligned} \tag{4}\]
不同头有机会学习不同关系,例如局部搭配、远距离指代或某类结构线索,但不能机械地认为“一个头就对应一种语法规则”。头的功能可能重叠、随层次改变,也会被后续计算重新组合。
技术深潜:把批次和头维度放回来
设批次大小为 \(B\),序列长度为 \(T\),头数为 \(H\),每头维度为 \(d_h\),通常有 \(d_{\text{model}}=H d_h\)。实现中的典型形状是:
| 阶段 | 形状 |
|---|---|
| 输入 \(X\) | \([B,T,d_{\text{model}}]\) |
| 线性投影后的 \(Q,K,V\) | \([B,T,Hd_h]\) |
| 拆分头并转置 | \([B,H,T,d_h]\) |
| 注意力分数 | \([B,H,T,T]\) |
| 与 \(V\) 相乘 | \([B,H,T,d_h]\) |
| 合并各头 | \([B,T,Hd_h]\) |
| 输出投影 | \([B,T,d_{\text{model}}]\) |
很多实现错误不是公式理解错,而是在 reshape 或 transpose 后混淆了 \(T\)、\(H\) 与 \(d_h\)。一个可靠习惯是为关键张量写出形状断言,并用很小的 \(B,T,H\) 做单元测试。
注意力权重上的 dropout 应用于 softmax 之后;因果掩码必须在 softmax 之前加入分数。如果先做 softmax 再把未来权重设为 0,剩余权重不再自动归一化为 1。
注意力之后,为什么还要前馈网络
如果把 Transformer 简化成“很多注意力”,会漏掉几乎每层都存在的另一半:前馈网络(feed-forward network,FFN)。它对每个位置分别应用相同的两层非线性变换:
\[ \operatorname{FFN}(x)=W_2\,\sigma(W_1x+b_1)+b_2 \]
中间维度通常比模型宽度大。注意力负责在位置之间搬运和混合信息;FFN 负责在每个位置的特征维度上进行非线性加工。二者分工不同,缺一不可。
每个子层外还有残差连接,把输入直接加回输出:
\[ y=x+\operatorname{Sublayer}(x) \]
残差路径让深层网络更容易传递信息和梯度,这一思想在深度残差网络中得到系统展示。(He 等 2016年) 层归一化(Layer Normalization)则按单个样本的特征维度调整数值尺度。(Ba 等 2016年)
原始 Transformer 采用子层之后归一化的 Post-LN 结构。许多现代大语言模型改用 Pre-LN,也就是先归一化,再进入注意力或 FFN:
原始 Post-LN:x -> 子层 -> 加上 x -> LayerNorm
常见 Pre-LN: x -> LayerNorm -> 子层 -> 加上 x
Pre-LN 往往让很深网络的优化更稳定,但“Transformer 必须采用哪一种”并没有脱离具体模型与训练设置的唯一答案。
一个解码器 Block 的完整旅程
现代自回归语言模型常使用仅解码器(decoder-only)结构。一个 Pre-LN Block 可以概括为:
第一条支路让每个位置读取允许范围内的其他位置;第二条支路对汇总后的表示进行逐位置加工。输出 \(y\) 再成为下一层的输入。堆叠许多层后,早期的局部线索可以被反复组合成更复杂的上下文表示。
原始论文用于机器翻译,包含编码器和解码器。编码器允许所有输入位置彼此注意;解码器除因果自注意力外,还通过交叉注意力读取编码器输出。后来出现了三条常见路线:
| 架构 | 注意力方式 | 典型训练任务 |
|---|---|---|
| 仅编码器 | 输入内部通常双向可见 | 掩码词预测、表示学习 |
| 编码器-解码器 | 编码器双向;解码器因果;二者交叉注意 | 翻译、条件生成 |
| 仅解码器 | 对已有上下文做因果注意力 | 下一个 Token 预测、开放式生成 |
大语言模型常见的仅解码器路线,并不等于 Transformer 的全部定义。
从隐藏向量到下一个 Token
最后一层为每个位置输出一个 \(d_{\text{model}}\) 维向量。线性输出层把它投影到词表大小 \(V\),得到 logits:
\[ L = HW_{\text{out}} + b, \qquad L\in\mathbb{R}^{B\times T\times V} \]
softmax 把某一位置上的 \(V\) 个 logits 转成概率分布。训练数据中的目标向右错开一位:
输入: 人 工 智 能 正
目标: 工 智 能 正 在
模型在每个位置预测真实下一个 Token,通常使用交叉熵损失。若真实目标是 \(y_t\),模型给出的条件概率为 \(p_\theta(y_t\mid y_{<t})\),一段序列的平均负对数似然为:
\[ \mathcal{L}(\theta)=-\frac{1}{T}\sum_{t=1}^{T} \log p_\theta(y_t\mid y_{<t}) \tag{5}\]
反向传播会把这一个标量损失的梯度送回输出层、每个 Transformer Block、位置表示和 Token Embedding。模型不是直接收到“这句话的语法规则是什么”的讲解,而是在海量预测错误中调整参数。
核心机制:并行训练,顺序生成
训练时,已知整段正确文本,因果掩码允许同时计算所有位置的下一 Token 损失。生成时,未来 Token 尚不存在,模型必须先产生一个 Token,把它加入上下文,再产生下一个。Transformer 消除了 RNN 在训练数据流上的逐步依赖,却没有消除自回归生成本身的顺序依赖。
训练与生成不是同一条计算路径
训练阶段常使用 teacher forcing:模型在每个位置看到的历史前缀来自真实数据。生成阶段则看到自己此前生成的 Token。一个早期错误可能改变后续上下文,错误由此累积。
生成时,模型先根据提示词计算概率分布,再选择下一个 Token。总选最高概率称为贪心解码;按分布采样可以增加多样性;温度、top-k 和 top-p 等参数改变候选分布。它们改变的是选择策略,不会为模型补充训练中没有学到的事实。
为了避免每一步都重新计算整个前缀,推理系统通常缓存每层过去 Token 的 Key 和 Value,即 KV Cache。生成第 \(t+1\) 个 Token 时,只需为新位置计算新的 Q、K、V,再让新 Query 读取缓存。缓存以显存换取速度,并且会随层数、上下文长度、批次和 KV 头数增长。
它为什么能扩展,又在哪里昂贵
Transformer 的成功不来自一个神奇部件。至少有四个因素彼此配合:
- 训练时的位置计算高度并行,适合现代加速器;
- 注意力让远距离位置之间拥有短的信息路径;
- 同一 Block 可以稳定堆叠,扩大参数量和表示深度;
- 下一个 Token 预测能从未经人工逐条标注的文本中构造海量训练信号。
标准全注意力也有明确代价。注意力分数含 \(T\times T\) 的位置对,计算量和中间存储随序列长度快速增长。FlashAttention 等方法通过减少高带宽内存与片上存储之间的数据搬运,使精确注意力更快、更省显存,但不改变标准注意力关于位置对数量的基本二次关系。(Dao 等 2022年)
更长上下文因此不是“把一个数字调大”这么简单。它同时涉及位置表示能否外推、训练样本是否覆盖长依赖、KV Cache 容量、注意力计算和模型能否真正找到远处信息。能接收很长输入,也不保证能稳定利用其中每一处内容。
注意力权重告诉了我们什么
注意力矩阵可以可视化。我们可能观察到某个头偏向相邻 Token,或某层对匹配的标点和指代位置给出较大权重。这类图对发现行为线索很有价值。
但注意力权重不是模型推理过程的完整解释。原因包括:
- Value 向量传递的内容同样重要;
- 多个头和多层会反复混合信息;
- 残差连接绕过注意力支路传递内容;
- FFN 继续进行大量非线性计算;
- 高权重表示这一步混合得多,不自动等于对最终答案有因果决定作用。
因此,可以说“这个头在此输入上重点读取了某些位置”,不应仅凭一张热力图就断言“模型正按人类的某条规则思考”。
一次完整的数据流核对
现在把主要步骤压缩在一张表中。设批次为 \(B\)、长度为 \(T\)、模型宽度为 \(D\)、头数为 \(H\)、每头宽度为 \(D_h\)、词表为 \(V\):
| 步骤 | 输入形状 | 输出形状 | 学习参数 |
|---|---|---|---|
| Token 查表 | \([B,T]\) | \([B,T,D]\) | 嵌入矩阵 |
| 加入位置 | \([B,T,D]\) | \([B,T,D]\) | 取决于位置方案 |
| Q/K/V 投影 | \([B,T,D]\) | 三组 \([B,H,T,D_h]\) | \(W^Q,W^K,W^V\) |
| 因果注意力 | Q/K/V | \([B,H,T,D_h]\) | 无新增参数 |
| 合并头与投影 | \([B,H,T,D_h]\) | \([B,T,D]\) | \(W^O\) |
| 残差与归一化 | 两组 \([B,T,D]\) | \([B,T,D]\) | 归一化缩放与偏置 |
| 前馈网络 | \([B,T,D]\) | \([B,T,D]\) | 两层权重与偏置 |
| 输出到词表 | \([B,T,D]\) | \([B,T,V]\) | 输出矩阵 |
| 交叉熵 | logits 与目标 \([B,T]\) | 标量 | 无新增参数 |
遇到一个新模型时,先问清这张表中的每一项,通常比先记住模型商品名更有用:它怎样切 Token?如何表示位置?使用什么掩码?注意力头如何分组?FFN 多宽?归一化放在哪里?训练目标是什么?这些选择共同决定模型的能力与成本。
Transformer 没有解决什么
Transformer 解决了序列建模中的一组结构和优化问题,不等于自动解决智能的全部问题。
它没有保证事实正确。下一个 Token 概率来自训练分布与当前上下文,不是一个内置事实数据库的查询结果。它没有保证长程任务可靠。生成步骤越多,局部错误、工具失败和错误假设越可能累积。它也没有消除数据问题:偏差、重复、低质量内容和版权来源会通过训练过程影响模型。
更重要的是,架构本身不等于最终系统。现代助手还会叠加大规模预训练、指令微调、偏好优化、检索、工具调用、安全策略和推理时搜索。后续章节会逐层加入这些部分。
实践路径:P08 训练迷你 Transformer
阅读公式能够理解各部分关系,亲手跟踪张量则能发现理解中的空白。P08 使用项目自带的短篇中文语料,训练一个字符级、仅解码器的迷你 Transformer。它有意保持很小,不追求流畅的通用语言能力。
实践将验证四件事:
- 输入与目标为何错开一个 Token;
- 因果掩码怎样阻止未来信息泄漏;
- \([B,T,D]\) 如何拆成 \([B,H,T,D_h]\) 再合并;
- 损失下降与生成改善为何不能证明模型获得了通用理解。
完整步骤见实践 P08。普通读者可以跳过实践,直接进入下一章;后文不会默认你已经运行代码。
本章小结
- Tokenizer 把文本变成离散编号,Embedding 再把编号变成可训练向量。
- 位置信息补上自注意力本身无法区分的顺序。
- Query 与 Key 决定匹配权重,Value 决定被汇总的内容;缩放和掩码保证数值与信息流符合任务要求。
- 多头注意力在不同投影空间中并行匹配,FFN 则逐位置加工特征;残差和归一化支撑深层训练。
- 自回归模型训练时可并行计算多个位置,生成时仍必须逐 Token 推进,KV Cache 用空间换速度。
- 注意力矩阵随序列长度平方增长;工程优化能显著减少实际开销,但长上下文仍有计算与有效利用问题。
- Transformer 是现代大模型的核心结构,不是完整助手的全部。预训练、对齐、检索和工具会在后续章节加入。
思考问题
- 如果删除位置编码,又不给注意力加入任何相对位置信息,模型会难以区分哪些句子?
- 因果掩码允许训练并行,却不允许生成并行,这两件事为什么不矛盾?
- 把上下文长度扩大两倍时,标准注意力分数矩阵的元素数量变成多少倍?
- 为什么看到某个注意力头对一个词给出最高权重,仍不足以证明这个词决定了最终输出?
延伸阅读
- 原始 Transformer 论文适合对照编码器、解码器、多头注意力和位置编码的最初组合。(Vaswani 等 2017年)
- Layer Normalization 与残差网络论文分别解释深层训练中两个基础部件的来源。(Ba 等 2016年; He 等 2016年)
- FlashAttention 论文适合进一步理解:同一个数学算法,在硬件内存层级上的实现方式为何会显著改变速度。(Dao 等 2022年)
