在准备涉及 大语言模型(Large Language Model, LLM) 的面试时,我意识到,自己对自注意力、位置编码、LayerNorm 等单个组件虽然都有比较完整的理解,也大致有一套整体图景,但这些内容一直停留在脑子里零散的笔记和记忆中。这时,我联想到了《Computer Networking: A Top‑Down Approach》中的 A Day in the Life of a Web Page Request。那一节通过追踪一次简单的网页请求,把分散在各章的协议栈知识串成了一条连续的数据流。本文的写作动机,正是尝试用类似的方式对大语言模型的推理过程和相关知识做一次回顾,同时在写作过程中巩固我的知识。另外,这也算是为一年又一个月没有更新的博客找一个合适的话题。
具体来说,本文会以 “Next-token Prediction” 为主线,从数据流的角度出发,按时间顺序梳理这条链路: 从输入文本到 token 序列,从 token 到向量表示,从向量在多层 Transformer [1] 中的传递与变换,一直到输出层给出概率分布并采样出下一个 token。文中不会展开训练细节和工程实现,而是聚焦于模型结构本身以作为之后复习的索引。
本文将包含四个主要部分,沿着数据流向依次展开:
- 输入:从文本到向量:简述自回归生成的模式,介绍 分词 与 Embedding 如何将人类语言转换为机器可读的向量。
- 总览 Transformer:从宏观视角理解为什么使用 Transformer,以及 Encoder/Decoder 架构的选择与区别。
- 走进 Transformer:本文将深入单一 Transformer Block 内部,详解 多头自注意力、RoPE、前馈网络、残差连接 与 层归一化 等组件。
- 输出:从向量到文本:解析如何将隐状态向量映射回概率分布,并经由采样策略生成最终的文本回复。
输入:从文本到向量
当用户按下发送按钮的那一刻,输入到对话框中的那段文本就开始了它的旅程。它被编码为比特流,在各类网络协议的协作下,经由多级交换机、路由器和服务器转发,最终抵达某个数据中心中的推理服务器。在那里,这段文本将被大语言模型处理并转化为用户看到的回复,而本文关心的,正是从文本进入模型之后的这部分过程。
在深入技术细节之前,我们先明确一下大语言模型的核心任务:预测下一个词。简单来说,给定一串文本(Prompt),模型的目标就是根据上文预测下一个最可能出现的单位。 然而,大语言模型作为一种深度神经网络模型,其内部只能处理向量和矩阵,使用 Unicode 编码的离散文本数据并非神经网络可以直接处理的数据格式。为了解决这个问题,通常文本需要经过以下两步转换:
- 第一步是通过 分词器(Tokenizer),将连续的字符序列切分并映射为离散的 Token 序列,并进一步表示为一串整数 ID。
- 第二步是通过 嵌入(Embedding)层,把这些整数 ID 映射为稠密向量(即 Embedding),从而得到模型可以直接处理的向量表示。
预测下一个词与自回归生成
在进入数据处理的流程之前,我们需要先理解 LLM 的核心任务和生成机制。
大语言模型最根本的任务是预测下一个词(Next-token Prediction)。基于这一能力,模型采用 自回归(Auto-Regressive) 的方式进行连续文本生成:假设我们输入一段文本序列 $w_{1}, w_{2}, \dots, w_{t}$,模型的任务就是计算出下一个 Token $w_{t+1}$ 的条件概率分布 $P(w_{t+1} | w_{1}, \dots, w_{t})$。一旦模型输出了这个分布并采样得到了 $w_{t+1}$,这个新生成的 Token 就会被拼接到序列的末尾,作为下一轮推理的历史上下文。如此循环往复,直到模型生成了特殊的结束标记(End-of-Text)或达到了长度限制。
举个例子:
- 第一步:输入
为什么要输出演 - 第二步:输入
为什么要演输出奏 - 第三步:输入
为什么要演奏输出春 - 第四步:输入
为什么要演奏春输出日 - 第五步:输入
为什么要演奏春日输出影 - 第六步:输入
为什么要演奏春日影输出? - 第七步:输入
为什么要演奏春日影?输出! - 第八步:输入
为什么要演奏春日影?!输出 结束标记<|end_of_text|>,结束生成。
这种看似简单的「文字接龙」机制,是目前所有主流文本生成 AI 的基石。所有的复杂能力最终都收束于这唯一的训练目标:预测下一个词。
分词器(Tokenizer)
如上所述,分词器的作用是将连续的字符序列(可以是自然语言文本、对话、数学公式,也可以是代码、JSON 等任何由 Unicode 字符构成的字符串)按照预定义的词表切分成一串 Token。那么 Token 是什么呢?按照我的理解,Token 是模型能处理的基本输入单位:它可以是一个汉字,例如 言;也可以是英文里的单词或词根、词缀,例如 can、un-、-ing;还可以是标点符号 !、? 或者数学符号 +、= 等等。除此之外,还有一些功能性的特殊 Token,它们并不直接对应输入文本中的任何字符,例如标记说话角色的 <|role|>,以及标识文本结束的 <|end_of_text|>。需要注意的是,Token 不一定对应一个完整的语义单元,它更多是出于统计和效率考虑,从训练语料中选择的一组文本片段。
模型所能处理的 Token 由 词表(Vocabulary) 决定,而词表会在预训练开始之前,基于训练数据集中语料的统计分布构建出来。如果将语料切分得过细,例如全部退化为单个字符,虽然这样得到的词表很小,但单个 Token 承载的信息过少,相同文本切分后的 Token 序列会变得很长,不利于训练和推理;反过来,如果切分粒度过粗,词表规模会急剧膨胀,既不利于模型充分学习每个 Token,又会显著增大嵌入层的参数量,同样影响训练和推理效率。因此,一个合适的词表构建算法对模型的整体性能至关重要。目前主流的大语言模型大多采用基于子词的做法,例如 Byte-Pair Encoding (BPE) [2] 和 Unigram [3] 等。
在具体实现中,分词往往还会包含一个轻量的预处理阶段,称为 Pre-tokenization。典型操作包括:对 Unicode 文本做规范化处理(例如统一全角/半角、大小写、标点形式),按空白或简单规则先切出初始片段,以及在英文场景下按空格粗略分词等。这个步骤的意义是先将原始文本规整为相对「干净」的字符或 Byte 序列,并根据简单的启发式规则预先切出一部分边界(例如空格、标点处分段),从而减少后续子词算法需要考虑的组合空间,也让同一段文本在训练和推理阶段的切分结果更加稳定。
这里有几个例子说明这一步发生了什么(示例基于 gpt-oss-20b [4] 的 Tokenizer):
- 原始输入:「Internationalization is difficult and complex.」
- Pre-tokenization:按空白和简单规则切分得到粗粒度片段1
Internationalization,Ġis,Ġdifficult,Ġand,Ġcomplex,. - 子词切分:可以看到,长单词在这里会被进一步拆分为子词(Subwords)
International,ization,Ġis,Ġdifficult,Ġand,Ġcomplex,. - 最后,这些子词 Token 会查表映射为一串整数 ID:
[43804, 2860, 382, 6541, 326, 8012, 13],作为后续嵌入层的输入。
- Pre-tokenization:按空白和简单规则切分得到粗粒度片段1
- 原始输入:「The capital city of China is」
- Pre-tokenization:
The,Ġcapital,Ġcity,Ġof,ĠChina,Ġis - 子词切分2:
The,Ġcapital,Ġcity,Ġof,ĠChina,Ġis - 转化为整数 ID:
[976, 9029, 5030, 328, 8114, 382]
- Pre-tokenization:
- 原始输入:「为什么要演奏春日影?!!」
- Pre-tokenization:在 gpt-oss 的分词器中,会先按 Unicode 文本切出两段
「为什么要演奏春日影」、「?!!」 - 子词切分3:
为什么、要、演、奏、春、日、影、?、!!
可以看到,由于为什么在语料中出现的频率较高,因此拥有一个单独的编码。 - 转化为整数 ID:
[39865, 7724, 24221, 154594, 26926, 2292, 5235, 4802, 24526]
- Pre-tokenization:在 gpt-oss 的分词器中,会先按 Unicode 文本切出两段
这里的
Ġ(U+0120) 是 GPT 系列模型在 Byte-level BPE 实现中的产物。为了能无缝处理任何 Unicode 文本(包括 Emoji 和生僻字)且不产生<UNK>,Tokenizer 会先将输入文本按 UTF-8 编码转为字节序列。由于直接处理原始字节流会包含许多不可打印的控制字符,GPT Tokenizer 引入了一套可逆映射,将 256 个字节值一一映射为可打印的 Unicode 字符。在此规则下,字节0x20(空格)被映射为了Ġ,而汉字等多字节字符则会显示为多个对应的特殊字符(即示例 3 中的「乱码」)。 ↩︎所有词都是词表中的 Token,因此不会被切分。 ↩︎
由于 gpt-oss 使用的是 Byte-level BPE,这里实际上的输出是
为ä»Ģä¹Īè¦ģæ¼Ķå¥ıæĺ¥æĹ¥å½±ï¼Łï¼ģï¼ģ,这些「乱码」解码之后就是上面的文本了。 ↩︎
此外,我还在 LLM 的帮助下写了一个交互式小工具(基于 Svelte + transformers.js + Web Component),读者可以自己输入句子,观察分词结果与 token ID。为了降低网页中引入的文件体积,这里使用的是较小的 GPT-2 (small) [5] 的词表,所以结果会与上面的示例不同。
嵌入层(Embedding Layer)
嵌入层的作用,是把上一节得到的离散整数 ID 转换成模型内部可以操作的稠密向量表示。形式化地看,它可以被视为一个从 token ID 到向量空间的映射 $f: \{0, 1, \ldots, (V-1)\} \rightarrow \mathbb{R}^d$,其中 $V$ 是词表大小,$d$ 是模型内部向量(即 embedding)的维度。对于每一个 token ID,$f(t)$ 都给出一个唯一的 embedding 向量,在训练过程中逐渐学到这个 token 的语义及其典型使用环境。
从实现角度看,这个映射由一块可学习参数矩阵 $E \in \mathbb{R}^{V\times d}$ 来承载:可以把它理解为一个「词表大小为 $V$、每行维度为 $d$ 的大表」。在推理时,模型并不会真的构造 one-hot 向量,而是直接以 token ID 作为索引,从矩阵 $E$ 中取出对应行,作为该 token 的向量表示,这就是常说的 embedding lookup。
为了直观感受这些高维向量是如何捕捉语义信息的,我基于 plotly 绘制了 GPT-2 (small) [5] 模型中所有 token embedding 的 t-SNE 降维可视化图。你可以缩放、悬浮查看每个点对应的 token:
- 数字聚类:右下角蓝色的部分聚集了大量数字,其中在
(68, -33)附近,从 1850 到 2018 的年份 token 近似按顺序连成一条弧线,体现了模型对数字大小和时间顺序的感知(GPT-2 的训练数据截止到 2017 年 [6])。此外,如果放大还能看到在 1940-1945 附近有一个红色的 “WWII”,这很明显是将这些年份和第二次世界大战联系在一起了。在这条弧线的左上方还能看到一些和时间相关的词,例如 century、decade 等。 - 地理位置聚类:坐标
(-76, -18)附近(左侧偏下)有很多国家名和地名聚集在一起。 - 人名聚类:在
(-70, 25)区域(左侧偏上),聚集了大量英文人名,如 Marshall, Douglas, Gray 等等。 - 过去分词(-ed):右侧大片的绿色区域集中了以 -ed 结尾的过去分词。
- 现在分词(-ing):右上角的黄色区域集中了以 -ing 结尾的现在分词。
至此,我们已经得到了长度为 $L$、维度为 $d$ 的向量序列,可以作为任意序列模型的输入(包括循环神经网络)。接下来本文将聚焦于当前大语言模型最常用的一种:基于 Transformer 的自回归语言模型。
总览 Transformer
在跟着 Embedding 进入 Transformer 的结构之前,我们先解决三个问题:「什么是 Transformer」、「为什么是 Transformer」与「Encoder 和 Decoder 的异同」。
什么是 Transformer

从结构上看,Transformer 是一种由多层堆叠的自注意力块和前馈网络组成的深度神经网络。每一层大致包含:
- 多头自注意力(Multi-Head Self-Attention),在整个序列范围内聚合上下文信息;
- 按位置独立的 前馈网络(Feed-Forward Network, FFN),对每个位置的表示做非线性变换;
- 残差连接(Residual Connection) 与 层归一化(Layer Normalization),用于稳定训练并便于加深网络。
在《Attention is All You Need》[1] 发表七年后,如今大语言模型的具体结构已经和论文里最初的 Transformer 有了不少差异(例如更深的 Decoder-only 堆叠、不同的归一化与位置编码形式等),但从宏观上看,依然可以理解为由上述这些基本组件按层堆叠而成。 后面我们会按顺序展开这些组件的结构和作用。
为什么是 Transformer
在 Transformer 出现之前,主流的序列建模方法以 循环神经网络(Recurrent Neural Network, RNN) 一类的 LSTM [7] 和 GRU [8] 为代表。它们的基本思路是:给定输入序列 $x_1, x_2, \dots, x_T$,模型从左到右依次读取 Token,每一步根据当前输入和上一时刻的隐藏状态 $h_{t-1}$ 计算出新的隐藏状态 $h_t$,同时给出当前时刻的输出 $y_{t}$。
这样一来,所有历史信息都会被不断压缩进一个固定维度的向量中,这势必会导致在输入序列较长时出现信息丢失,让 RNN 的记忆能力和上下文感知能力受到了极大的限制。此外,这种按时间步逐个更新隐藏状态的设计,一方面导致 RNN 在时间维度上高度顺序依赖,难以并行计算;另一方面使得长距离依赖很难稳定地通过梯度传播。
在这个背景下,2017 年推出的 Transformer 不再沿着时间轴维护一条单向传递的隐藏状态链路,而是把整个序列在每一层都视作一个整体:给定一组输入向量,模型在同一层内同时为所有 Token 计算新的表示,其中每个位置都可以通过自注意力机制与序列中其他位置的 Token 交互,再配合多头注意力和前馈网络,对整条序列做一次更新。
这样的设计,一方面天然适合在时间维度上做大规模并行计算,大幅提升训练和推理时的吞吐速度;另一方面让长距离依赖可以通过注意力机制直接建立联系,从而更容易稳定地建模远距离关系。同时,由于每一层都在全局上下文的基础上更新整条序列的表示,随着层数和参数规模的增加,Transformer 在表达能力和上下文建模能力上也更容易随之扩展。
当然,这种优势也不是没有代价的。如果观察一层的计算量,设序列长度为 $n$、隐藏维度为 $d$,Transformer 需要处理所有 Token 之间的交互,其时间和显存复杂度大致为 $O(n^2 d)$;而单层 RNN 每一步只处理当前输入和上一个隐藏状态,时间复杂度约为 $O(n d^2)$。在实际系统中,隐藏维度 $d$ 作为超参数往往是固定的,而上下文长度 $n$ 则可以从几百一路扩展到几千甚至上万。因此,当我们尝试把上下文拉长时,Transformer 在 $n$ 上的二次增长会比 $d$ 带来更显著的计算和显存开销。
下面的表格给出了两类架构在复杂度上的对比:在不考虑大规模并行计算的前提下,RNN 在时间和空间复杂度上都更具优势;但是在实际任务中,Transformer 的建模效果和扩展性远远胜出,再加上 NVIDIA 芯片的发展与 CUDA 等大规模并行计算框架的推动,于是 Transformer 取代了 RNN,成为当今序列建模的主流范式。 尽管去年也出现了以 Mamba [9] 为代表的新一代线性时间模型,在部分场景中展现出一定的竞争力,但就目前公开的大模型实践而言,Transformer 依然是绝对主流架构。
| 场景 (输入序列长 $n$) | 模型 | 时间复杂度(单层)[1] | 显存复杂度(单层,不含固定参数) |
|---|---|---|---|
| 训练 | RNN / LSTM / GRU | $O(n d^2)$ | $O(n d)$ |
| 训练 | Transformer | $O(n^2 d + n d^2)$ | $O(n^2 + n d)$ |
| 推理(生成长度 $l$) | RNN / LSTM / GRU | $O\big((n + l) d^2\big)$ | $O(d)$ |
| 推理(生成长度 $l$) | Transformer (Decoder-only) | $O\big((n + l)^2 d\big)$ | $O\big((n + l) d\big)$ [10] (使用 KV cache) |
| 推理 | Transformer (Encoder-only) | $O(n^2 d)$ | $O(n^2 + n d)$ |
这里的 $n$ 表示输入序列长度(或上下文长度),$l$ 表示在自回归场景下需要额外生成的 Token 数,$d$ 表示隐藏维度。为了对比结构差异,我们忽略 Head 数、常数因子以及低阶项,只保留关于 $n, l, d$ 的主导项。
训练阶段
Transformer(训练,序列长 $n$)
对一层来说,大致可以拆成四块:- 线性变换得到 $Q, K, V$:$O(n d^2)$;
- 计算注意力分数 $QK^\top$:$O(n^2 d)$;
- 对 $V$ 做加权求和:$O(n^2 d)$;
- 前馈网络(FFN):$O(n d^2)$。
合并得到单层时间复杂度约为 $O(n^2 d + n d^2)$。 显存方面,需要保存:
- 每个位置的中间激活:$O(n d)$;
- 注意力矩阵(或其等价形式):$O(n^2)$。
合起来记作 $O(n^2 + n d)$。
RNN / LSTM / GRU(训练,序列长 $n$)
每个时间步更新隐藏状态的代价约为 $O(d^2)$(来自若干 $d \times d$ 的线性变换),整条序列 $n$ 步,因此时间复杂度为 $O(n d^2)$。训练时为了做反向传播,需要保存每个时间步的隐藏状态 $h_t \in \mathbb{R}^d$,所以显存与序列长度线性相关,为 $O(n d)$。
推理阶段
Transformer Encoder-only(推理,序列长 $n$)
一次前向计算和训练的前向部分类似,同样需要为所有 Token 对计算注意力分数并做聚合,时间复杂度仍为 $O(n^2 d)$。 显存方面,需要保留:- 当前层的 $Q, K, V$ 等激活:$O(n d)$;
- 注意力权重或中间结果:$O(n^2)$。
记作 $O(n^2 + n d)$,主导项随 $n$ 近似二次增长。
Transformer Decoder-only(推理,上下文长 $n$,生成长度 $l$)
自回归生成时,假设一开始有长度为 $n$ 的上下文,之后按步生成 $l$ 个 Token:- 首先对长度为 $n$ 的上下文做一次前向,注意力部分约为 $O(n^2 d)$;
- 之后第 $t$ 个生成步($t=1, \dots, l$)会看到长度约为 $n+t$ 的序列,其注意力计算量约为 $O((n+t)d)$,总和为 $O((n l + l^2)d)$。
合起来,一层的时间复杂度可以写为 $ O\big((n^2 + n l + l^2)d\big)$,通常为了简洁近似为 $O\big((n + l)^2 d\big)$。 为了避免重复计算,Decoder 会为每一层缓存所有历史位置的 $K, V$(即 KV cache),其规模与总长度 $n + l$ 成正比,因此显存复杂度约为 $O((n + l) d)$。
RNN / LSTM / GRU(推理,上下文长 $n$,生成长度 $l$)
在推理时,RNN 每一步只依赖当前输入和上一个隐藏状态:- 共需要处理 $n + l$ 个时间步,每步 $O(d^2)$,时间复杂度为 $O((n + l) d^2)$;
- 显存上,只需保存当前的隐藏状态(以及若干门的内部状态),规模为 $O(d)$,与序列长度 $n, l$ 无关。
整体来看,RNN 在 $n$ 上的时间和显存复杂度都是线性的,而 Transformer 在训练和 Encoder-only 推理时都具有 $O(n^2)$ 级别的注意力开销;在 Decoder-only 自回归生成时,通过 KV cache 在「时间仍为二次、显存降为线性」之间做了权衡。
Encoder 和 Decoder 的异同
在 Vaswani 等人提出的原始 Transformer [1] 中,他们针对翻译任务设计的是 Encoder–Decoder 架构 的 Transformer。其中,编码器(Encoder) 接收来自源语言的文本(比如 “Hello world”),通过多层自注意力机制和前馈网络生成一串上下文表示;解码器(Decoder) 通过交叉注意力机制读取 Encoder 的输出,并在 因果掩码(Causal Mask) 的约束下,自回归 地生成目标语言的文本序列(比如 “你好世界”)。
在实际使用中,根据任务不同,常见有三种变体:
- 只使用 Encoder 的 Encoder-only 模型:例如 BERT [11] 一类的掩码语言模型、句向量编码器等,它们的输出通常是编码了上下文语义信息的稠密向量,可用于分类、检索,或者在检索增强生成(Retrieval-Augmented Generation, RAG)场景中对给定文本生成摘要向量;
- 只使用 Decoder 的 Decoder-only 模型:例如 GPT 系列、Llama 系列、Qwen 系列、DeepSeek 系列的自回归语言模型,目前几乎所有大语言模型都是 Decoder-only 模型;
- 完整的 Encoder–Decoder 模型:例如机器翻译、部分 Seq2Seq 任务(输入输出是不同模态或不同语言的序列)。
| 模型类型 | 结构特点 | 典型任务 / 输出 | 代表模型示例 |
|---|---|---|---|
| Encoder-only 模型 | 只使用 Encoder 堆叠 | 输出每个位置的上下文语义向量 用于分类、检索、RAG 摘要向量等 | BERT [11]、 LLaDA [12] 等 |
| Decoder-only 模型 | 只使用 Decoder 堆叠 + 因果掩码 | 自回归地生成序列,例如文本生成任务 | GPT 系列、 Qwen 系列 |
| Encoder–Decoder 模型 | Encoder + Decoder + 交叉注意力 | 源序列 → 目标序列的 Seq2Seq 任务 (跨模态或跨语言转换) | 翻译模型等 |
从网络结构的角度,Encoder-only 模型和 Decoder-only 模型是非常相似的,二者的主要差异体现在工作模式上: Decoder 在自注意力机制中使用因果掩码,输出的是下一个位置 Token 的概率分布; 而 Encoder 通常直接输出每个位置的向量表示,用于下游判别或匹配任务。 虽然近年也出现了基于 Encoder-only 模型的掩码扩散大语言模型 [12],但目前主流的大语言模型仍然采用 Decoder-only 结构,因此本文也主要关注这一类模型中的数据流。
走进 Transformer
在上文中,我们已经对 Transformer 的总体结构有了一个大致的印象,也从工作模式上区分了 Encoder、Decoder。接下来,我们将把视角进一步聚焦于 单层 Transformer Block,沿着数据的路径,依次拆解多头自注意力、前馈网络、残差连接与层归一化等组件是如何作用在同一串向量上的。
多头自注意力机制
正如那篇经典论文 [1] 的标题「Attention is All You Need」所暗示的那样,注意力机制(Attention Mechanism)是 Transformer 的核心,也是这一架构如此有效的根本原因。目前在 Decoder-only 模型中最常采用的是多头自注意力机制,其中包含三个要素:「多头」「自」和「注意力」,我们将从后往前依次展开;随后还会介绍在 Decoder-only 模型中同样重要的两个组成部分:旋转位置编码(RoPE) 和 因果掩码(Causal Mask)
注意力机制
从历史上看,注意力机制并不是 Transformer [1] 独有的发明,而是首先出现在基于 RNN 的序列到序列模型 [13] 中,用来缓解机器翻译等任务中的长距离依赖问题:编码器输出整条源序列的一串隐藏状态,解码器在生成每个目标 Token 时,对源序列不同位置分配不同权重,从而避免把整句信息强行压缩进一个固定维度的向量里。 尽管如此,在那个阶段注意力机制只是 RNN 的一个辅助模块,无法摆脱 RNN 结构在长距离建模上的局限;而 Transformer 则抛弃了 RNN 的递归结构,反而把注意力机制提升为网络的核心,从整体架构上重塑了序列建模方式。
从直觉上看,可以把注意力理解为对所有值向量(Value)$\mathbf v_i$ 做一次加权求和,其中每个位置的权重由当前查询向量(Query)$\mathbf q$ 与对应键向量(Key)$\mathbf k_i$ 的相似度决定。给定查询向量 $\mathbf q$,模型会使用向量内积 $\mathbf k_i\cdot \mathbf q$ 在归一化后作为权重,再用这些权重对值向量 $\mathbf v_i$ 做加权求和,得到查询向量 $\mathbf q$ 对应的输出。这样一来,模型在更新查询向量 $\mathbf q$ 的时候,可以参考序列中任意位置的 $\mathbf v_i$,直接聚合与当前语义最相关的信息。

从形式化的角度看,可以把上面的直觉抽象成统一的数学形式:给定查询向量 $\mathbf q_j \in \mathbb{R}^{d_k}$,一组键向量 $\mathbf k_i \in \mathbb{R}^{d_k}$,以及对应的值向量 $\mathbf v_i \in \mathbb{R}^{d_v}$,注意力机制首先对每个键计算一个实数作为注意力分数 $\mathrm{score}(\mathbf q_j, \mathbf k_i) \in \mathbb{R}$,然后通过 Softmax 函数得到归一化权重: $$\alpha_i = \mathrm{softmax}_i\big(\mathrm{score}(\mathbf q_j, \mathbf k_i)).$$ 最后用这些权重对值向量 $\mathbf v_i$ 做加权求和: $$\mathrm{Attention}(\mathbf q_j, {\mathbf k_i}, {\mathbf v_i}) = \sum_i \alpha_i \mathbf v_i \in \mathbb{R}^{d_v}.$$
在 Transformer 中使用的是缩放点积注意力(Scaled Dot-Product Attention),其注意力分数是两个向量的内积乘上一个缩放因子: $$\mathrm{score}(\mathbf q_j, \mathbf k_i) = \frac{\mathbf q_j^\top \mathbf k_i}{\sqrt{d_k}},$$ 其中 $d_k$ 是键 / 查询向量的维度,分母的 $\sqrt{d_k}$ 用来缓解内积随维度增长而数值过大的问题,否则在 $d_k$ 较大时注意力分数会进入 Softmax 函数的饱和区,导致权重退化为 One-hot 向量。
为了简化分析,这里设 $\mathbf q_j$ 和 $\mathbf k_i$ 的每一维都是独立同分布、均值为 $0$、方差为 $\sigma^2$ 的随机变量。 未缩放时的点积为 $$s = \mathbf q_j^\top \mathbf k_i = \sum_{\ell=1}^{d_k} q_\ell k_\ell.$$ 由于各维独立,有 $$ \mathbb E[q_\ell k_\ell] = 0,\quad \mathrm{Var}(q_\ell k_\ell) = \mathbb E[q_\ell^2]\mathbb E[k_\ell^2] = \sigma^4. $$ 因此整体方差为 $$ \mathrm{Var}(s) = \sum_{\ell=1}^{d_k} \mathrm{Var}(q_\ell k_\ell) = d_k \sigma^4. $$ 也就是说,标准差会按 $O(\sqrt{d_k})$ 的速度增长:维度越大,$s$ 的取值就越容易变得很大,从而让 Softmax 的输入落在饱和区,注意力权重趋近 One-hot,此时绝大多数位置的分数和梯度均接近 0,不利于训练与 Softmax 的数值稳定。
为了让注意力分数的方差与 $d_k$ 无关,我们设注意力分数为 $\tilde s = c \cdot s = c \cdot \mathbf q_j^\top \mathbf k_i$, 其中 $c\in \mathbb{R}^+$ 是待定常数。不难推出 $$ \mathrm{Var}(\tilde s) = \mathrm{Var}(c \cdot s) = c^2 \mathrm{Var}(s) = c^2 d_k \sigma^4. $$ 令其与 $d_k$ 无关,即 $$ c^2 d_k = 1 \quad\Longrightarrow\quad c = \frac{1}{\sqrt{d_k}}. $$
这说明只要在点积前乘上系数 $c = 1/\sqrt{d_k}$,缩放后的 $\tilde s$ 的方差就不再依赖于 $d_k$,从而避免了上述问题。
如果把所有查询向量拼成矩阵 $Q = (\mathbf q_j) \in \mathbb{R}^{L_q \times d_k}$,所有键和值向量分别拼成矩阵 $K = (\mathbf k_i)\in \mathbb{R}^{L_k \times d_k}$、$V = (\mathbf v_i) \in \mathbb{R}^{L_k \times d_v}$,则可以写成更紧凑的矩阵形式: $$\mathrm{Attention}(Q, K, V) = \mathrm{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}\right) V.$$
这种形式本身并不限制 $Q, K, V$ 来自哪一条序列:可以是「源序列 → 目标序列」的交叉注意力(Cross-Attention),也可以是后面将要介绍的自注意力(Self-Attention),其中 $Q, K, V$ 均源自同一个序列。
自注意力机制
如上所述,自注意力机制就是让注意力中的 $Q, K, V$ 都源自同一条输入序列,从而在同一串 Token 上计算「每个位置对其他位置的注意力」。
设一层的输入为 $X \in \mathbb{R}^{L \times d_{\text{model}}}$($L$ 为序列长度,$d_{\text{model}}$ 为隐藏维度),如果直接令 $Q = K = V = X$,那么 Query、Key 和 Value 在同一子空间里完全共享一套表示,表达能力会受到限制。实际的 Transformer 会为三者分别学习独立的线性投影矩阵: $$ Q = X W^Q,\quad K = X W^K,\quad V = X W^V, $$ 其中 $W^Q, W^K \in \mathbb{R}^{d_{\text{model}} \times d_k}$,$W^V \in \mathbb{R}^{d_{\text{model}} \times d_v}$。 这样一来,模型可以在同一串输入 $X$ 上,用不同的线性变换分别提取用于匹配和打分的特征 $Q, K$ 与最终被聚合的内容 $V$,从而让自注意力在同一层内具备更灵活的表示能力。
不过,到目前为止的注意力机制还有两个关键问题没有解决:
- 其一,它本身是位置无关的:在不考虑额外信息的情况下,如果我们同时对 $Q, K, V$ 做相同的重排,得到的注意力结果不会发生变化,这意味着模型难以区分「同一组 Token 不同顺序」之间的差异。
- 其二,在生成任务中,原始的注意力计算对所有位置一视同仁,这导致训练时当前位置可以直接看到将来出现的 token,但在推理时这是不可能的,违背了自回归语言建模的因果性。
为了解决这两个问题,实际的 Transformer 会一方面通过 旋转位置编码(RoPE) 向 $Q$ 和 $K$ 中注入位置信息,另一方面在注意力分数矩阵上施加 因果掩码(Causal Mask),强制当前 token 只能关注自身及其之前的位置。在进入完整的多头自注意力机制之前,我们分别从 RoPE 和因果掩码两部分展开介绍。
因果掩码(Causal Mask)
在自回归语言建模中,第 $i$ 个 Token 只能依赖位置 $\le i$ 的历史信息,不能偷看将来的 Token。但前面给出的注意力却对所有位置一视同仁:第 $i$ 行可以看到第 $j>i$ 列的键值,违背了因果性约束。
通常的做法是构造一个上三角的因果掩码矩阵 $M \in \mathbb{R}^{L\times L}$: $$ M_{ij} = \begin{cases} 0, & j \le i, \\ -\infty, & j > i, \end{cases} $$ 然后在进入 Softmax 之前把它加到打分矩阵上: $$ \mathrm{Attention}_\text{causal}(Q, K, V) = \mathrm{softmax}\left(\frac{QK^\top}{\sqrt{d_k}} + M\right) V. $$
这样一来,$j>i$ 的位置在对应行上的注意力得分是 $-\infty$,Softmax 之后权重变为 0,这样第 $i$ 个位置就只能关注自身及其之前的 Token 了。
旋转位置编码(RoPE)
在目前为止的讨论中,注意力分数只依赖于 $\mathbf q_j$ 和 $\mathbf k_i$ 本身的数值,而与它们在序列中的位置无关:如果我们同时对 $Q, K, V$ 按相同方式打乱顺序,得到的注意力结果不会改变。换句话说,在没有位置编码时,模型只看到无序的一袋向量,难以区分同一组 Token 不同排列方式之间的差异,例如「一把把把把住了」这类句子中,多次出现的「把」在不同位置分别充当量词、动词、介词等不同角色,而纯注意力机制并不会区分它们。为了解决这个问题,我们需要在向量中注入位置信息。
在原始的 Transformer [1] 中,位置编码(Positional Encoding) 是通过在嵌入层之后直接把一个位置向量 $\mathbf p_i$ 加到 Token Embedding 上来实现的,即对第 $i$ 个位置有 $$ \mathbf x_i = \mathbf e_i + \mathbf p_i, $$ 其中 $\mathbf e_i$ 是 Token Embedding,$\mathbf p_i$ 是对应位置的编码。这种做法实现简单,但有一个小问题:语义信息和位置信息被混合在同一个向量里,后续所有层的隐藏状态既承担语义表示,又携带位置编码,但是实际上我们只需要为注意力机制引入位置信息。
相比于原始的 Transformer,大语言模型更倾向于让位置编码直接作用在 $Q$ 和 $K$ 上,而不是一开始就写进所有隐藏状态中。旋转位置编码(Rotary Positional Embedding, RoPE) [15] 通过对 $Q$ 和 $K$ 施加与位置相关的旋转,确保注意力分数只依赖于 Token 之间的 相对位置,同时又保持自注意力整体结构不变。
从实现上看,RoPE 并不是对 $X$ 直接加上位置向量,而是对已经线性变换后的 $Q, K$ 的每两个维度为一组,将其看成 $L d_k/2$ 个二维向量,并对每个二维向量做一次旋转,旋转角度是 Token 位置 $i$ 和维度 $k\in{1, 2, \ldots, d_k/2}$ 的函数 $$\theta_{ik} = \frac{i}{10000^{2k / d_k}}.$$
为了更直观地理解 RoPE 中 $\theta_{i,k}$ 的取值规律,我用 plotly 导出了一幅可交互的热力图:横轴是位置 $i$,纵轴是二维子空间索引 $k$,颜色表示对应的旋转角度(以度数表示,并对 $360^\circ$ 取模)。可以看到,不同 $k$ 上角度随 $i$ 的变化频率不同,越小的 $k$ 对应越高的旋转频率,越大的 $k$ 对应越低的频率。
这样的目的是让不同子空间在不同尺度上编码位置信息,旋转频率快的负责刻画局部、细粒度的相对位置变化,旋转频率慢的负责长程趋势,相当于用一组不同频率的正弦基在注意力中构造出多尺度的位置表示,从而提升模型对各种距离范围内相对位移的表达能力。
对每个位置 $i$,根据上面的角度 $\theta_{i,k}$,可以把对 $d_k$ 维子空间的旋转写成一个按二维子空间分块的分块对角矩阵 $R_i \in \mathbb{R}^{d_k \times d_k}$。若令第 $k$ 个二维子空间(对应维度 $(2k, 2k+1)$)的旋转角为 $\theta_{ik}$,则有 $$ R_i = \begin{pmatrix} \cos\theta_{i,0} & -\sin\theta_{i,0} & 0 & 0 & \cdots & 0 & 0 \\ \sin\theta_{i,0} & \cos\theta_{i,0} & 0 & 0 & \cdots & 0 & 0 \\ 0 & 0 & \cos\theta_{i,1} & -\sin\theta_{i,1} & \cdots & 0 & 0 \\ 0 & 0 & \sin\theta_{i,1} & \cos\theta_{i,1} & \cdots & 0 & 0 \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & 0 & 0 & \cdots & \cos\theta_{i,d_{k}/2-1} & -\sin\theta_{i,d_{k}/2-1} \\ 0 & 0 & 0 & 0 & \cdots & \sin\theta_{i,d_{k}/2-1} & \cos\theta_{i,d_{k}/2-1} \end{pmatrix}. $$ 然后对 $\mathbf k_i$ 和 $\mathbf q_i$ 分别应用这个线性变换,写成向量就是(注意 $R_i$ 和位置 $i$ 有关,因此不同位置的 Token 的旋转矩阵是不一样的。) $$ \mathbf q_i’ = R_i \mathbf q_i,\quad \mathbf k_i’ = R_i \mathbf k_i. $$
我们考察 $\mathbf k_i$ 和 $\mathbf q_j$ 的注意力分数 $$\mathrm{score}(\mathbf q_j, \mathbf k_i) = \frac{(R_j\mathbf q_j)^\top (R_i\mathbf k_i)}{\sqrt{d_k}} = \frac{\mathbf q_j^\top (R_j^\top R_i)\mathbf k_i}{\sqrt{d_k}} $$ 根据旋转矩阵的性质,以及 $\forall k, \theta_{ik} \propto i$(即旋转角是位置的线性函数),可以推出 $R_j^{-1} = R_j^\top = R_{-j}$,因此有 $R_j^\top R_i = R_j^{-1} R_i = R_{i-j}$。也就是说 $$\mathrm{score}(\mathbf q_j, \mathbf k_i) = \frac{\mathbf q_j^\top R_{i-j}\mathbf k_i}{\sqrt{d_k}}.$$ 这说明在 RoPE 下,第 $j$ 个位置对第 $i$ 个位置的注意力分数只依赖于两者的相对位移 $i-j$,而与它们各自的绝对位置无关。因此 RoPE 本质上是一种相对位置编码形式,同时又不破坏原有自注意力的结构与 KV cache 等推理加速手段。这样的良好性质让 RoPE 在近年的大语言模型中被广泛采用。
当然,在代码中并不会真的显式构造整个 $d_k\times d_k$ 的矩阵 $R_i$,而是对每个位置 $i$ 的查询 / 键向量 $\mathbf q_i,\ \mathbf k_i \in \mathbb{R}^{d_k}$ 按照两个为一组成对切分成若干个二维向量 $(q_{i,2k}, q_{i,2k+1})$、$(k_{i,2k}, k_{i,2k+1})$(其实就是 Reshape 操作),然后对每一对维度应用上式中的对应 $2\times 2$ 旋转块,其余维度保持不变。
- 绝对位置编码
直接为每个位置 $i$ 分配向量 $\mathbf p_i$,常见形式包括可学习的位置嵌入和固定的正弦/余弦编码。- 优点:实现简单直观,与 Embedding 形态一致,容易在现有架构中插入,几乎没有额外计算开销。
- 缺点:对超出训练长度的外推能力有限,模型也更难显式捕捉「距离差」这类相对关系。
- 相对位置编码
让注意力分数显式依赖于位置差 $j-i$,例如通过为不同相对距离引入 Bias 项或额外的相对位置向量。- 优点:更自然地刻画「谁离谁近」「先后顺序」,在长上下文和跨段对齐任务中往往效果更好。
- 缺点:实现与推导相对复杂,某些形式在长序列时会带来额外的计算或内存开销。
- RoPE(旋转位置编码)
通过对 $Q, K$ 施加位置相关的旋转,使注意力分数仅依赖于 $j-i$,本质上属于相对位置编码的一种。- 优点:在保持自注意力结构简洁的同时,引入了平移不变的相对位置信息,对长上下文插值和上下文长度扩展更友好,因此被广泛用于现代大模型(如 Llama 系列及许多变体)。
- 缺点:理解和实现上比简单的绝对编码更抽象,对部分结构改动(例如跨层共享、某些形式的剪裁 / 拼接)需要格外小心。
结合了上述的 RoPE 位置编码与自注意力机制,模型在处理实际文本时会形成怎样的注意力模式呢?
为了直观理解注意力机制,我从 Qwen3-0.6B [16] 中导出了模型推理时中间层的自注意力分数。输入文本为:「在自然语言处理任务中,注意力机制允许模型动态地关注输入序列中的不同部分,从而捕捉上下文依赖。」(为了展示清晰,在可视化时移除了通常作为 attention sink [17] 的首个 token。)
从图中可以直观地观察到,自注意力分数的分布呈现出明显的规律性:热力图中最深的颜色主要集中在对角线上,说明 token 在大多数时候首选关注自身;同时,高分值区域也密集分布在对角线附近的局部短语中(例如 “机制” 强烈关注 “注意力”),反映了模型对词法和短语结构的敏锐捕捉。此外,我们还能观察到部分 token 跨越了较长距离去关注之前的核心词,这种捕捉长程语义依赖的能力正是 Transformer 架构相比于传统 RNN 的核心优势所在。
多头自注意力机制
前面的小节一直在讨论「单头」注意力:给定一层输入 $X \in \mathbb{R}^{L \times d_{\text{model}}},$ 先通过线性变换得到 $$Q = X W^Q,\quad K = X W^K,\quad V = X W^V,$$ 再把注意力看作在同一个特征子空间里的单头匹配与聚合。
在实际的 Transformer 中,更常见的是让模型同时在多个子空间里做这种「匹配 + 聚合」,这就是多头自注意力(Multi-Head Self-Attention) [1]。具体做法是:为每个注意力头 $h\in \{1,\dots,H\}$ 各自学习一组投影矩阵 $$W_h^Q, W_h^K \in \mathbb{R}^{d_{\text{model} } \times d_k},\quad W_h^V \in \mathbb{R}^{d_{\text{model} } \times d_v},$$ 得到第 $h$ 个头上的 $$Q_h = X W_h^Q,\quad K_h = X W_h^K,\quad V_h = X W_h^V.$$ 然后对每个头分别应用前面介绍过的「带 RoPE 的因果自注意力」: $$ \mathrm{head}_h(X) = \mathrm{Attention}(Q_h, K_h, V_h) \in \mathbb{R}^{L \times d_v}.$$
最后,把所有头在特征维上拼接起来,再通过一个输出线性层 $W^O \in \mathbb{R}^{H d_v \times d_{\text{model} } }$ 做回投影: $$\mathrm{MultiHead}(X) = \mathrm{Concat}\big(\mathrm{head}_1(X),\dots,\mathrm{head}_H(X)\big)W^O. $$
直观上,多个注意力头的引入可以让每个注意力头在不同的子空间下,独立地学习一套对序列结构的建模方式:有的头可能偏向局部邻域,有的头偏向长程依赖,还有的头专门捕捉某类语法或语义关系。这样让模型在同一层中可以并行地学习不同的视角,提升模型整体的表达能力。
前馈网络(Feed-Forward Network)
在每一层自注意力之后,Transformer 会对每个位置单独再过一层前馈网络(Feed-Forward Network, FFN)。它本质上是一个对序列中每个 Token 的两层感知机:先升维再降维,中间插入非线性激活函数。设某层自注意力(加残差、归一化)后的输出为 $\mathbf h_i \in \mathbb{R}^{d_{\text{model}}}, i=1,\dots,L$,则对每个位置 $i$ 有 $$ \begin{align*} \mathbf z_i &= \phi(W_1 \mathbf h_i + \mathbf b_1), & \mathbf z_i &\in \mathbb{R}^{d_{\text{ff}}}, & W_1 &\in \mathbb{R}^{d_{\text{ff}} \times d_{\text{model}}}\\ \mathbf y_i &= W_2 \mathbf z_i + \mathbf b_2, & \mathbf y_i &\in \mathbb{R}^{d_{\text{model}}}, & W_2 &\in \mathbb{R}^{d_{\text{model}} \times d_{\text{ff}}} \end{align*} $$ 其中 $\phi(\cdot)$ 是非线性激活函数(比如 GELU),维度 $d_{\text{ff}}$ 一般是 $d_{\text{model}}$ 的 4 倍,相当于先把特征展开到更高维空间,在经过激活函数后压回原来的维度。
为什么在自注意力之外还需要这样一层前馈网络?一个重要原因是:注意力机制虽然包含 Softmax 非线性,但其对 Value 向量的操作本质上是线性的(即加权求和)。如果整层网络只包含线性运算,即使堆很多层,整体仍然退化为一个大线性变换,表达能力有限。FFN 通过中间的非线性 $\phi(\cdot)$ 引入了位置上的非线性特征变换,让模型不仅能通过自注意力机制聚合上下文信息,还能在每个位置上对聚合到的特征做复杂的模式变换。
此外,由于 FFN 的参数量通常远大于注意力部分,一些工作也观察到:大量「知识」更像是被存放在前馈网络中[18]。一种常见的非正式说法是:自注意力负责在序列各位置之间路由信息,而前馈网络则利用庞大的参数容量,对这些信息做高维查表和非线性映射,承担了相当一部分知识存储与提取的职责。
混合专家模型(MoE)
前面的讨论里,我们默认每一层只有一套共享的前馈网络:所有 Token 都要经过同一组参数做升维、非线性、降维。这虽然简单,但也带来一个矛盾:如果想提升模型表达能力,通常要把前馈网络做得很大;而一旦变大,参数量和计算开销都会增长,然而,很多参数在大部分样本上其实是冗余的。混合专家模型(Mixture of Experts, MoE) 想解决的,就是在不牺牲模型表达能力的前提下,减少单个 Token 在推理时所需的计算量。

典型的 MoE 层可以看作是前馈网络的一种稀疏扩展:不再只有一个 FFN,而是有 $N$ 个按相同结构堆叠的专家(Expert)FFN,再加上一个小的 门控网络(Router)。对某一层第 $i$ 个位置的输入向量 $\mathbf h_i$,大致流程是:
- 门控网络根据 $\mathbf h_i$ 输出一组权重 $g_i \in \mathbb{R}^N$,通常只保留 Top-$k$ 个最大的分量,其余置零。
- 对于被选中的少数几个专家 $E_{j}$,分别计算各自的 FFN 输出 $E_j(\mathbf h_i)$。
- 按门控权重把这些专家输出加权求和,作为这一层在位置 $i$ 的 FFN 输出: $$ \mathrm{MoE}(\mathbf h_i) = \sum_{j \in \text{TopK}(g_i)} g_{i,j}, E_j(\mathbf h_i).$$
在这里,门控网络决定当前这个 Token 更应该交给哪几个专家去处理,而不是让所有 Token 都用同一套参数。这样一来,在参数量上,模型可以拥有远多于单一 FFN 的总参数;在计算量上,每个 Token 只会激活其中少数几个专家,单步推理的计算量增幅相对温和;在表示能力上,不同专家可以关注不同的模式,形成一定程度的专家分工。比如在 Mixtral 8×7B 的分析中,就观察到有的专家更多地处理数字和标点等特殊 Token,有的主要服务于自然语言词片,还有的则偏向代码相关 Token [19]。
当然 MoE 也是有代价的,它需要额外的门控网络和训练时的负载均衡 Loss,来避免所有 Token 都挤到同一个专家上,因此其在训练和推理时的实现也会更复杂。不过,得益于稀疏激活带来的巨大计算性能优势和优秀的扩展性,MoE 仍然在近年的大模型设计中被广泛采用,例如 gpt-oss [4]、DeepSeek-V3 [20]、DeepSeek-R1 [21]、Qwen3 系列的一部分模型 [16]、Kimi-K2 [22] 等等。
残差连接(Residual Connection)
残差连接最早由何恺明等人在 ResNet [23] 中系统提出,其核心思想是在一段非线性变换 $F(\cdot)$ 外面再加上一条恒等映射的捷径: $$\mathbf y = \mathbf x + F(\mathbf x),$$ 其中 $\mathbf x$ 是块的输入,$F(\mathbf x)$ 是若干层卷积 / 线性层加激活函数后的输出。相比于单纯学习 $\mathbf y = F(\mathbf x)$,这种写法让网络只需学习输出相对于输入的 残差(Residual),即 $\mathbf y - \mathbf x$。
从优化角度看,残差连接为梯度提供了一条近似恒等的直通路径:在反向传播时,损失对 $\mathbf x$ 的梯度除了会经过 $F$ 的链式求导外,还会直接通过加法中的那条支路传回。这在网络堆叠得很深时,可以显著缓解梯度消失和退化问题,使得更深的网络仍然可训练,也是现代深层网络得以显著加深的重要前提之一。
在 Transformer 中,每一层的自注意力和前馈网络周围都会包一层这样的残差连接,例如(忽略 LayerNorm 的细节): $$\begin{aligned} \mathbf H^{(l)}_\mathrm{attn} &= \mathbf H^{(l-1)} + \mathrm{MultiHeadAttention}\big(\mathbf H^{(l-1)}\big), \\ \mathbf H^{(l)} &= \mathbf H_\mathrm{attn}^{(l)} + \mathrm{FFN}\big(\mathbf H_\mathrm{attn}^{(l)}\big), \end{aligned}$$
其中 $\mathbf H^{(l-1)}$ 是第 $(l-1)$ 层的输出,$\mathbf H^{(l)}$ 是第 $l$ 层的最终输出。这样一来,即使注意力或 FFN 中某些子结构在初始阶段还没学到有用的变换,信息也可以通过残差支路几乎不受损地向前和向后传递。
直观地说,残差连接为每一层的梯度提供了一个捷径,模型可以在此基础上逐层叠加小的修正项,而不用担心网络深度带来的优化困难。下一小节中引入的 LayerNorm 则会和这种残差结构搭配,进一步改善训练稳定性。
层归一化(Layer Normalization)
在训练过程中,由于模型前面层的参数在不断更新,后面层看到的输入分布也会随之发生改变,相当于后面层要不断适应一个在训练过程中缓慢变化的任务,这不利于后面层的训练。为此我们需要引入 归一化层(Normalization Layer),其的目的就是在不改变模型表达能力的前提下,在训练时先把每一层的输入/输出标准化到一个稳定的范围内,从而削弱这种分布漂移对训练过程的干扰。
按照归一化操作所作用的维度不同,常见的做法包括 批次归一化(BatchNorm) [24] 和 层归一化(LayerNorm) [25] 等。在 Transformer 中,最常用的是后者 层归一化 [25]。它对每一个样本的每一个 Token,在特征维度上单独做归一化。例如,对某一层第 $i$ 个 Token 的隐藏向量 $\mathbf h_i \in \mathbb{R}^{d_{\text{model}}}$,LayerNorm 的计算大致是: $$ y_{i,j} = \gamma_j \frac{h_{i,j} - \mu_i}{\sqrt{\sigma_i^2 + \varepsilon}} + \beta_j. $$ 其中,$\mu_i$ 和 $\sigma_i^2$ 分别是 $\mathbf h_i$ 内共 $d_{\text{model}}$ 个元素的均值和方差,$\varepsilon$ 是防止除零的小常数,$\gamma_j,\beta_j \in \mathbb{R}$ 是每个维度之间独立的可学习参数。可以看到,对同一位置 $i$ 来说,所有维度共享同一组统计量 $(\mu_i,\sigma_i^2)$,而不同维度有各自的缩放 / 平移参数 $(\gamma_j,\beta_j)$。这既让各维的数值范围更稳定,又保留了模型按维度重新标定特征的重要自由度。
顺带一提,近年的一些大模型还会采用 RMSNorm 这一变体。与 LayerNorm 先减去均值、再按方差归一化不同,RMSNorm 只对向量的均方根做缩放,不再显式去中心化,其形式大致是: $$ y_{i,j} = \gamma_j \frac{h_{i,j}}{\sqrt{\frac{1}{d_{\text{model}}}\sum_{k} h_{i,k}^2 + \varepsilon}}. $$ 这样一方面保留了「按整体尺度规范激活」的核心效果,另一方面实现更简单、在部分大模型实践中也被观察到具有略好的数值稳定性和收敛表现,因此逐渐成为 LayerNorm 的常见替代方案。
对比 BatchNorm
从归一化的角度看,BatchNorm 和 LayerNorm 的核心差异在于统计量计算所依赖的维度不同。BatchNorm 更适合卷积网络中的图像场景:它对同一通道上的激活,在一个 Mini-Batch(以及可能的空间维)上共同计算均值和方差,因此统计量直接依赖于 Batch 的大小与组成,训练和推理时还需要维护全局均值 / 方差的估计。相比之下,LayerNorm 对于每个样本、每个位置,只在特征维度上计算均值和方差,与 Batch 里还有多少其他样本无关,这使得即便在 Batch Size 很小、甚至推理阶段单样本在线生成的情况下,归一化行为也能保持一致。
对 Transformer / LLM 来说,这一点非常关键。一方面,语言任务中的序列长度和 Batch 构成高度可变,BatchNorm 的 Batch 统计往往噪声较大,训练 / 推理之间的分布差异也更难控制;另一方面,自回归推理时常常是小 Batch 甚至单条请求逐 Token 生成,此时依赖 Batch 统计的 BatchNorm 会带来额外的工程复杂度和数值不稳定。相反,LayerNorm 完全不依赖 Batch 维度,只对当前位置的特征向量做归一化,天然适配「按 Token 逐位置处理、支持可变长度和小 Batch 推理」的使用模式。因此,现代 Transformer 和大语言模型几乎清一色采用 LayerNorm 或其变体 RMSNorm,作为标准的归一化方案。
Pre-LN 和 Post-LN

右图 (b) 是后续广泛采用的 Pre-LN 结构:先对输入做 LayerNorm,再进入子层,最后直接与原输入相加。
(图片来自 Xiong et al. [26])
在前面介绍了 LayerNorm 的作用之后,一个自然的问题是:它在 Transformer 的残差结构里到底放在哪个位置更合适?主流有两种设计,分别是 Post-LN 和 Pre-LN,上图给出了这两种设计的对比,从其中可以更直观地看到两种设计的差异。
左侧的 Post-LN 中,每个子层(多头注意力或前馈网络)先对输入做变换,再把结果与残差加到一起,最后整体通过一次 LayerNorm。这样一来,沿着残差路径向上传递的信号,每经过一层都会被 LayerNorm 和非线性再加工一次;当网络足够深时,梯度在层与层之间会被反复缩放和扰动,训练过程容易变得不稳定。
右侧的 Pre-LN 则把 LayerNorm 挪到了子层之前:先对当前表示做一次归一化,再送入多头注意力或前馈网络,子层输出直接与原输入相加,形成下一层的残差起点。对主干残差通路来说,这更接近一条「恒等映射加小修正」的线路:即便子层一开始还没学到有用的变换,信息和梯度也可以几乎不受干扰地一路向前和向后流动,更有利于在几十层甚至上百层的深度下保持训练稳定。
此外,一些系统性的实验 [26] 对这两种结构做了对比,结果表明:在网络加深时,Post-LN 更容易出现梯度衰减和训练不稳定的问题,而 Pre-LN 在大多数配置下能以更大的学习率稳定收敛;Post-LN 往往需要更保守的学习率和更复杂的 Warmup 策略才能勉强训练起来。因此,虽然最初的 Transformer [1] 采用的是 Post-LN,但在后续的 LLM 实践中,Pre-LN(以及只保留缩放的 RMSNorm 等变体)逐渐成为事实上的主流选择,现代的模型基本都采用这类结构。
小结
在前面几节里我们分别拆开讲了注意力、RoPE、因果掩码、多头机制、前馈网络、残差连接和归一化。这里不再继续引入新符号,而是回到那条简单的输入序列 [The, capital, city, of, China, is]
在章节末尾,我们从数据流的视角,把一层 Transformer Block 里发生的事情串起来看一遍。
上一层的输出作为这一层的输入表示
经过嵌入层和若干层 Transformer 之后,我们在第 $l-1$ 层出口处得到六个 Token 的隐藏向量[The, capital, city, of, China, is]分别对应 $H^{(l-1)}_1,\dots,H^{(l-1)}_6$。
你可以把它们看成「当前模型对每个位置的语义理解」,这一层的任务就是在此基础上再做一次全局信息路由与非线性变换。LayerNorm
对于The、capital、city等每个位置的 Token,各自根据隐状态 $H^{(l-1)}_i$ 在特征维度上的均值和方差做归一化,再对每个维度 $j$ 乘上可学习的缩放系数 $\gamma_j$ 并加上偏置 $\beta_j$,得到的结果记作 $\tilde H^{(l-1)}_i$,它们将作为本层注意力子层的输入。- 对每个注意力头,分别从 $\tilde H^{(l-1)}_i$ 通过可学习的参数线性投影出 $Q, K, V$(实际部署时还会通过 KV Cache、批量调度等手段避免重复计算前缀、提升吞吐,这些属于系统实现层面的优化,本文不展开);
- 在 $Q, K$ 上应用 RoPE:把 $Q, K$ 的维度两两分为一组,对不同 Token 和不同子空间应用不同角度的旋转作为位置编码,让注意力分数只依赖 Token 之间的相对位移;
- 在注意力分数矩阵上施加因果掩码:
让
city在这一层只能看到[The, capital, city]三个 Token; 对位置is,它可以看到[The, capital, city, of, China, is],但看不到后面还没生成的 Token。 - 每个头各自完成一次带 RoPE 和因果掩码的自注意力,得到六个位置的上下文向量,再在特征维上拼接,通过输出线性层合并成本层注意力的输出 $\Delta_{\text{attn}}$。
- 最后,把这个输出与原始输入 $H^{(l-1)}$ 相加(残差连接),得到 $H^{(l)}_{\text{attn}} = H^{(l-1)} + \Delta_{\text{attn}}.$
在直觉上,可以理解为:在这一层里,
is会查看前面所有的 Token,结合当前上下文,对自己在这一步该承载什么信息做一次加权汇总;此外,原来的表示 $H^{(l-1)}_i$ 通过残差连接被完整保留下来,给梯度留出一条稳定的直通路径。- 先对注意力子层的输出 $H^{(l)}_{\text{attn}}$ 做一次 LayerNorm 得到 $\tilde H^{(l)}_{\text{attn}}$,保证送入前馈网络的输入在尺度上也是稳定的;
- 对每个位置各自应用同一组两层感知机:先升维、过一次激活函数 $\phi$,再降回原来的维度 $$ \Delta_{\text{FFN}} = W^{(l)}_2 \phi\big(W^{(l)}_1 \tilde H^{(l)}_{\text{attn}, i} + \mathbf b^{(l)}_1\big) + \mathbf b^{(l)}_2.$$
- 最后再和这一层的输入做一次残差相加,得到这一层最终的输出 $H^{(l)}_i = H^{(l)}_{\text{attn}} + \Delta_{\text{FFN}}.$
把上面的「LayerNorm → MHA + RoPE + 因果掩码 → 残差连接 → LayerNorm → 前馈网络 → 残差连接」视作一个基本单元,沿深度方向堆叠几十层之后,最后一个位置的 Token is 会在每一层中回看前面的 [The, capital, city, of, China],在全局范围内不断重组、强化与当前预测相关的上下文线索;同时,每一层的 FFN 又在局部为当前 Token 提供更抽象、更任务相关的特征变换。等到整段 [The, capital, city, of, China, is] 最终到达输出层时,输出的隐状态是经过多层 Transformer 共同塑造出的高维表示,这为后续 Logits 的计算提供了充足的信息。
输出:从向量到文本
经过多层 Transformer 的处理,输入序列的最后一个 Token 已经被编码为一个高维的隐状态向量,其中聚合了生成下一个词所需的上下文信息。接下来,我们需要将这个连续的向量表示转换回人类可读的离散文本。这个过程主要包含三个步骤:逆嵌入(Unembedding)、采样(Sampling) 和 逆分词(Detokenization)。
逆嵌入(Unembedding)
当经过深层网络处理后的隐状态向量 $H^{(L)}$(对应当前处理的 Token is)离开最后一层的 Transformer Block 时,它首先会经过一次最终的 LayerNorm 以统一特征分布。紧接着,这个维度为 $d_{\text{model}}$ 的向量会被送入逆嵌入层(Unembedding Layer),其本质上是一个线性投影矩阵 $W_U \in \mathbb{R}^{d_{\text{model}} \times V}$,它将高维的语义特征映射回词表空间 $\mathbb{R}^{V}$。这个矩阵乘法的输出是一个长度为词表大小 $V$ 的向量,其中的每一个数值代表了模型认为词表中对应 Token 是下一个词的打分,这些未归一化的分数被称为 Logits。
Logits 本身的数值范围通常在 $(-\infty, +\infty)$ 之间,且难以直接解释。为了确定下一个词是谁,我们需要将这些分数转换为概率分布。这里使用的是 Softmax 函数。在这个过程中,为了控制生成的创造性或确定性,通常在推理时会引入一个重要的超参数:温度(Temperature)。
经过温度调整后的完整概率计算公式为 $$\mathbf p_i = \mathrm{softmax}\left(\frac{1}{T}\cdot W_U\cdot\mathrm{LayerNorm}(\mathbf h^{(L)}_i)\right),$$ 其中 $\mathbf p_i \in \mathbb{R}^{V}$ 是第 $(i+1)$ 个输入 Token 的概率分布。直观地看,温度 $T$ 起到了调节分布集中程度的作用:当 $T < 1$ 时,原本得分较高的 Token 的概率会被进一步放大,分布变得尖锐,信息熵降低,在采样时倾向于选择最稳妥的词,适合推理或问答;当 $T > 1$ 时,分布趋于平坦,信息熵升高,原本概率较低的 Token 也有了被选中的机会,这增加了生成结果的多样性,适合创意写作。
采样(Sampling)
得到了概率分布 $\mathbf p$ 之后,最后一步就是根据这个分布选择一个具体的 Token ID 作为当前的输出,并将其拼接到输入序列的末尾,开始下一轮的循环。这个过程决定了模型生成的风格,主要有确定性和随机性两大类策略。
在早期的序列生成模型(如机器翻译)中,束搜索(Beam Search) 曾经是标配。它不局限于眼下的这一步,而是时刻维护 $k$ 个候选序列(Beam),每一步都搜索所有可能的扩展路径并保留得分最高的 $k$ 个,试图找到全局最优解。然而,在现代的 Decoder-only 大模型中,Beam Search 的使用频率大大降低了。原因主要有二:一是它需要同时维护 $k$ 份 KV Cache,显存和计算开销成倍增加,对于长文本生成难以承受;二是研究发现,在开放式生成中,概率最高的全局最优解往往也是最平庸、最容易陷入重复循环的文本 [27]。
因此,现在的 LLM 推理主要采用单步解码策略,其中最基础的就是贪心搜索(Greedy Search):在每一步直接选择概率最大的那个 Token。这种策略计算效率最高,适合数学推理、代码生成等追求唯一正确解的任务;但对于创意写作,它的结果是确定的,且不受温度的控制,因此可能会显得过于死板和枯燥。
为了让生成更像人类,我们通常会引入随机采样,即从概率分布中随机抽取下一个 Token。最常用的策略有两种:
- Top-k 采样 [28]:只从概率最高的前 $k$ 个 Token 里进行采样。这虽然排除了长尾的离谱选项,但 $k$ 值是固定的,无法适应模型信心变化(在不确定时 $k$ 太小会丢掉合理选项,在确定时 $k$ 太大又会引入噪声)。
- Top-p (Nucleus) 采样 [27]:这是目前最主流的方法。它不固定候选词的数量,而是选择概率总和达到 $p$(例如 0.9)的最小 Token 集合。这是一种动态的策略:当模型很确定时(分布尖锐),候选集可能只有 1-2 个词;当模型不确定时(分布平坦),候选集会自动扩展到几十个词。这种方式完美平衡了生成的多样性和合理性。
在实际应用中,通常会将温度与 Top-p 结合使用,先用温度调整分布形状,再用 Top-p 截断长尾,最后在候选集合内根据重新归一化后的概率分布进行采样。
逆分词 (Detokenization)
当模型通过采样选定了一个 Token ID(例如 11618)之后,我们通过查阅分词器的词表将其还原为对应的文本片段(例如 " Beijing")。这个过程即逆分词(Detokenization)。
对于 BPE 类分词器 [2],这个过程本质上是字节序列的拼接。模型不断生成新的 Token,我们将这些 Token 对应的字符串依次追加到输出缓冲区中。需要注意的是,由于 BPE 可能会把一个完整的 Unicode 字符切分成多个字节,有时刚生成的 Token 只是一个不完整的字节片段(如汉字 Unicode 编码的前半部分),此时直接解码会产生乱码。因此,稳健的实现通常会维护一个字节缓冲区,只有当缓冲区内的字节能组成合法的 Unicode 字符时才进行解码并展示给用户。
走入轮回
至此,我们已经完成了一次完整的预测下一个词的过程:从输入的文本序列,经过分词、嵌入、多层 Transformer 的变换,最终从最后一个 Token is 对应的输出概率分布采样得到了一个新的 Token ID(例如 11618,对应单词 Beijing)。
但这并不是终点。正如我们在 预测下一个词与自回归生成 一节中所述,大语言模型的生成过程是自回归的。这个新生成的 Token ID 会被追加到当前输入序列的末尾,成为下一轮推理的历史上下文的一部分。
紧接着,新一轮的循环开始了。这个新的整数 ID 会再次被送入 嵌入层,转化为向量表示,并再次流经那一层层复杂的 Transformer 结构。模型会基于包含了 Beijing 的新上下文,去预测下一个可能出现的词(比如句号 .)。
这个「预测 - 采样 - 拼接 - 再预测」的循环将一直持续下去,直到模型采样到了代表结束的特殊标记 <|end_of_text|>,或者触碰到了设定的最大长度限制。到那时,这段从用户指尖出发的文本,在经历了无数次矩阵乘法与非线性变换的漫长旅程后,终于完成它的使命,化作屏幕上一段完整的、富有逻辑的回复,呈现在你的面前。
后记
呼~终于写完了!这应该是我第一次主动挑战写这么长篇幅的技术文章。回顾下来,LLM 推理相关的数据流和模型结构知识点大概都覆盖到了。文中如果有遗漏或错误之处,烦请在评论区斧正,如果本文有帮到你的话也欢迎在评论区留下评论哦!
当然这篇文章也不全是我一个人敲出来的。为了达到复习巩固的目的,我在列好大纲之后对每一节采用了「我撰写初稿 → LLM 事实核查 → 我修改 → LLM 润色」的工作流。在这个过程中,LLM 确实帮我纠正了不少错误理解(比如我之前一直以为 RoPE 是像传统的位置编码一样在输入层直接作用于 Embedding 的),同时也补充了许多我未曾注意到的细节,帮助确实很大。
另外,这也是我首次尝试在博客文章中嵌入可交互组件(比如文中用 Svelte 写的 Tokenizer 演示和 Plotly 导出的可交互图表)。这种可交互组件的存在确实相比于单纯的图片更有助于加深理解,这也算是独立博客相比于知乎等第三方平台的优势之一吧。后续可能会水一篇文章讲讲我是如何实现和插入这些交互式组件的。
