Skip to content

第二阶段:理解原理 —— 详解

读完这篇,你会搞懂 LLM 底层到底在做什么:文字怎么变成数字、Transformer 怎么"理解"上下文、以及模型怎么一个字一个字地生成回复。 预计阅读时间:60-75 分钟。


目录

  1. 概览:一个 token 的旅程
  2. 第一站:Tokenization(分词)
  3. 第二站:Embedding(嵌入)
  4. 第三站:Self-Attention(自注意力)
  5. 第四站:Multi-Head Attention(多头注意力)
  6. 第五站:完整的 Transformer 层
  7. 第六站:从层到模型
  8. 第七站:自回归生成
  9. 本阶段小结

概览:一个 token 的旅程

在深入细节之前,先用一张全景图建立坐标系——一个词元从输入到输出,到底经历了什么:

Transformer 完整架构

上图:原始 Transformer 架构(Vaswani et al., 2017,"Attention Is All You Need")。左半边是编码器(Encoder),右半边是解码器(Decoder)。现代 LLM(GPT 系)只用了解码器部分(去掉 Encoder-Decoder Attention 那条线),但核心组件完全一样。

一条文本进来,大致走这几站:

LLM 处理流程

下面逐站拆解。


第一站:Tokenization(分词)

一句话定义:把原始文本切成模型认识的"最小单元"(词元 / token)。

BPE 算法动手走一遍

现代 LLM 普遍使用 BPE(Byte Pair Encoding,字节对编码)。它的核心思路很朴素:把经常一起出现的字符"粘"成一个词元。咱们用一个具体例子走一遍。

准备阶段:假设训练语料只有一句话(实际是万亿级文本),我们要从头训练一个词表大小为 12 的分词器。

语料: "low low low low low lower lower newest newest newest widest widest widest"

第一步:每个词拆成字符,末尾加上结束符 </w>

l o w </w>  (×5)
l o w e r </w>  (×2)
n e w e s t </w>  (×3)
w i d e s t </w>  (×3)

初始词表:{l, o, w, e, r, n, s, t, i, d, </w>} — 共 11 个,目标是扩展到 12。

第二步:统计所有相邻字符对的频率

字符对出现次数出现在哪些词里
(e, s)6newest(×3) + widest(×3)
(s, t)6newest(×3) + widest(×3)
(l, o)7low(×5) + lower(×2)
(o, w)7low(×5) + lower(×2)
(w, </w>)5low(×5)
(w, e)5lower(×2) + newest(×3)
(e, w)3newest(×3)
.........

第三步:合并频率最高的对(l, o) 7 次和 (o, w) 7 次并列最高,选 (l, o) → 新 token lo。词表变成 12 个,达到目标,过程结束。

如果我们让过程继续(实际词表通常做到 5 万-10 万),后续的合并顺序大概是:

合并 1: (l, o) → lo    — 频率最高
合并 2: (lo, w) → low  — lo 和 w 又成了最高频的邻居
合并 3: (e, s) → es    — 同时也是 (s, t),选哪个取决于实现
合并 4: (es, t) → est  — es 和 t 组合,est 作为一个整体出现频率高
合并 5: (e, w) → ew
合并 6: (n, ew) → new
...

推荐边看边操作:Cornell BPE 交互演示 —— 可以自己输入语料,一步步看合并过程。

最终效果:常见词的完整拼写(low, new, est)成为独立 token,罕见词的组成部分也被保留。新来一个词 lowest(训练语料里没有),可以拆成 low + est,两个子词模型都认识。

词表大小的权衡

词表大小优点缺点
太小 (如 1 万)每个 token 含义明确一个词被拆很碎,序列变长,推理变慢
太大 (如 10 万+)常见词不被拆,序列短词表大 → 嵌入矩阵大 → 显存占用大
中等 (3-5 万)目前主流选择,平衡效率和覆盖

各代模型的词表大小变化:GPT-2 为 50,257,GPT-3/4 约 10 万,DeepSeek-V3 约 12.8 万;到了 2026 年,Llama 4 约 20.2 万,DeepSeek V4 约 12.9 万。词表越大,嵌入矩阵越大(后面会讲),所以不能无限扩。

分词器带来的"怪癖"

这里把第一阶段提到过的几个经典问题收个尾:

  • "strawberry 里几个 r?"straw + berry,模型看不到单个字母 r
  • "9.11 和 9.9 谁大?" → 数字被切成 9 + . + 11,模型要做推理而非直接比较
  • 中文比英文"贵" → 同等语义,中文通常需要更多 token(一个汉字 ≈ 1-2 token,而一个英文单词 ≈ 1-2 token 但承载的语义少)

第二站:Embedding(嵌入)

一句话定义:给每个词元分配一个高维向量,让语义相近的词元在向量空间里也挨得近。

从 ID 到向量

分词器输出的是一串整数 ID(比如 [15496, 2159, 734, 3186]),计算机需要把这些整数变成它真正能计算的东西——向量

Embedding 层本质是一张巨大的查找表(lookup table)

词元 ID  →  Embedding 向量(d 维)
─────────────────────────────────
   0     →  [0.001, -0.023, 0.047, ..., 0.031]
   1     →  [0.012, 0.034, -0.002, ..., -0.019]
   2     →  [-0.008, 0.011, 0.029, ..., 0.044]
  ...    →  ...
 49999   →  [0.003, -0.015, 0.022, ..., -0.037]

这个表的大小是 词表大小 × 向量维度,里面的数字全部是训练学出来的。

这些数字是怎么来的? 训练刚开始时,嵌入表里的数字全是随机的——"苹果"和"香蕉"在向量空间里毫无关系。

训练过程大致是:喂给模型一句话(比如"我今天吃了一个"),让它猜下一个词。如果它猜"苹果"的概率只有 3%,但正确答案就是"苹果",就产生一个误差信号。这个误差由一个**损失函数(loss function)**算出来——它把"模型猜的概率分布"和"正确答案"之间的差距量化成一个数字:猜得越离谱,loss 越大。后面第七站讲温度采样时会见到的交叉熵,就是 LLM 最常用的损失函数。

有了 loss 之后,反向传播(backpropagation)从输出端往回走,逐层计算"每个参数对最终误差贡献了多少"——这个贡献量就是梯度(gradient)。某参数梯度大,说明调它能显著减小误差;梯度接近零,说明调它没啥用。最后根据梯度把所有参数往减小误差的方向微调一小步。

整个循环就是三步:前向传播(算预测 + 算 loss)→ 反向传播(算梯度)→ 参数更新(调权重)

经过万亿级别的训练文本反复迭代后,嵌入表里的数字被逐渐"驯化"——经常出现在相似上下文里的词在向量空间中自动靠近。第三阶段(训练过程)会详细展开这个完整的学习机制。

以 GPT-3(2020 年)为例:d = 12,288,词表 50,257,光这个嵌入表就有 50,257 × 12,288 ≈ 6.17 亿个参数。到了 2026 年,DeepSeek V4 Flash(d = 4,096,词表 129,280)嵌入表 ≈ 5.3 亿参数;Llama 4 Scout 更夸张——词表 202,048,d = 5,120,嵌入表 ≈ 10.3 亿参数

为什么必须是向量?

向量解决了一个根本问题:让计算机能"比较"两个词有多像。 "苹果"和"香蕉"这两个字符串,机器没法判断谁更接近谁。但转换成了两个向量之后,只需要算一个余弦相似度:

余弦相似度("苹果", "香蕉")   ≈ 0.85   ← 都是水果,挨得近
余弦相似度("苹果", "iPhone") ≈ 0.72   ← 苹果公司,也算相关
余弦相似度("苹果", "拖拉机")  ≈ 0.12   ← 没啥关系,离得远

有了向量,搜索、类比、聚类全部变成了数学运算:

搜索:   用户问题向量 · 文档向量 → 找余弦相似度最高的
类比:   "国王" - "男人" + "女人" ≈ "女王"
聚类:   把所有向量按距离分组 → 同组即同主题

余弦相似度与欧氏距离

其中最关键的两个数学工具,就是余弦相似度欧氏距离。它们回答了同一个问题——"两个向量有多像?"——但尺子不同。

余弦相似度:只看方向,不看长度

公式:

其中:

  • (向量点积)
  • (向量长度 / L2 范数)

直觉:把两个向量看成从原点出发的两支箭。余弦相似度量的是它们夹角的余弦值——箭头指向同一个方向 → cos ≈ 1,箭头垂直 → cos ≈ 0,箭头反向 → cos ≈ -1。

关键特性:向量的长度不影响结果。比如 [1, 1][100, 100] 方向完全一样,余弦相似度 = 1.0。

用一个二维例子直观感受:

A = [3, 4]    B = [30, 40]
|A| = 5       |B| = 50
A·B = 3×30 + 4×40 = 250
cos = 250/(5×50) = 1.0     ← 方向完全一致

A = [1, 0]    B = [0, 1]
cos = 0                     ← 正交,毫无关系

A = [1, 2]    B = [-1, -2]
cos = -1                    ← 完全相反

为什么在文本嵌入中更常用余弦相似度? 因为同一句话用不同方式表达,向量长度可能不同(短句向量短,长句向量长),但语义方向应该相近。"今天天气真好"和"今天的天气非常不错"——句子长度不同导致向量的 L2 范数不同,但语义方向应当接近。余弦相似度天然忽略了长度差异,只看方向。

欧氏距离:看两点之间的直线距离

公式:

直觉:就是 n 维空间里的"直尺距离"——中学学过的两点间距离公式,推广到高维。d 越小,两个点越近。

同样的二维例子:

A = [1, 1]    B = [4, 5]
d = √[(1-4)² + (1-5)²] = √[9 + 16] = 5

A = [1, 1]    C = [1.5, 1.2]
d = √[(1-1.5)² + (1-1.2)²] = √[0.25 + 0.04] ≈ 0.54   ← 更近

什么时候用欧氏距离? 当你既关心方向也关心大小的时候。比如聚类时,如果你希望"长度相近的向量也分到一组",欧氏距离就比余弦相似度更合适。但在语义搜索中,通常希望"长文档"和"短摘要"也能匹配上,所以余弦相似度是更主流的选择。

两者的联系与选择

特性余弦相似度欧氏距离
衡量什么方向是否一致位置是否接近
受向量长度影响
取值范围[-1, 1](实际嵌入通常 ≥ 0)[0, +∞)
典型场景语义搜索、文本相似度聚类(K-Means)、图像匹配
计算复杂度O(d)O(d)

一个关键细节:如果所有向量都预先做了 L2 归一化(把长度统一缩放到 1),那么余弦相似度和欧氏距离是等价的——两者之间存在严格的单调关系。很多向量数据库(如 Chroma、Milvus)内部会先把向量归一化,然后用欧氏距离近似余弦相似度来加速检索。

动手算一遍

假设我们的嵌入维度只有 3 维(实际是 4096 维),有三个词:

"苹果"  = [0.8, 0.2, 0.1]
"香蕉"  = [0.7, 0.3, 0.1]
"飞机"  = [0.1, 0.8, 0.8]

余弦相似度("苹果" vs "香蕉"):

点积 = 0.8×0.7 + 0.2×0.3 + 0.1×0.1 = 0.56 + 0.06 + 0.01 = 0.63
|苹果| = √(0.64 + 0.04 + 0.01) = √0.69 ≈ 0.831
|香蕉| = √(0.49 + 0.09 + 0.01) = √0.59 ≈ 0.768
cos = 0.63 / (0.831 × 0.768) ≈ 0.99   ← 几乎完美对齐(都是水果!)

余弦相似度("苹果" vs "飞机"):

点积 = 0.8×0.1 + 0.2×0.8 + 0.1×0.8 = 0.08 + 0.16 + 0.08 = 0.32
cos = 0.32 / (0.831 × 1.136) ≈ 0.34   ← 方向不太一致

欧氏距离("苹果" vs "香蕉"):

d = √[(0.8-0.7)² + (0.2-0.3)² + (0.1-0.1)²]
  = √[0.01 + 0.01 + 0]
  = √0.02 ≈ 0.14                   ← 很近

欧氏距离("苹果" vs "飞机"):

d = √[(0.8-0.1)² + (0.2-0.8)² + (0.1-0.8)²]
  = √[0.49 + 0.36 + 0.49]
  = √1.34 ≈ 1.16                   ← 远得多

两种度量给出一致的结论——"苹果"和"香蕉"近,"苹果"和"飞机"远。这就是为什么嵌入向量在 AI 中如此重要。

这就是为什么即使嵌入表动辄数亿参数、显存占用巨大,也必须走"文字→向量"这一步——纯文字做不了这些运算。

为什么是这个维度数(而不是 50 维或 50000 维)?

先看历史上这个数字是怎么演化的:

模型 (历史)嵌入维度 d总参数量年份
GPT-2 Small7681.24 亿2019
GPT-2 XL1,60015 亿2019
GPT-312,2881,750 亿2020
LLaMA-7B4,09670 亿2023
LLaMA-70B8,192700 亿2023
DeepSeek-V37,1686,710 亿(MoE)2024

再到 2026 年的选择:

模型 (2026)嵌入维度 d总参数量备注
DeepSeek V4 Flash4,0962,840 亿 (MoE, ~130 亿激活)轻量旗舰,性价比路线
Llama 4 Scout5,1201,090 亿 (MoE, 170 亿激活)10M 上下文,开源标杆
Llama 4 Scout (Instruct)6,1441,090 亿 (MoE, 170 亿激活)多模态变体,维度更大
DeepSeek V4 Pro7,1681.6 万亿 (MoE, ~490 亿激活)正式旗舰
Kimi K3未公开2.8 万亿 (MoE, ~500 亿激活)估计在 4096-7168 范围
Qwen 3.8-Max未公开2.4 万亿 (MoE)官方未放出技术报告

可以看到一个有趣的趋势:维度并没有随着模型变大而无限膨胀。GPT-3(2020)就用了 12,288 维,但后来的模型反而"收敛"到了 4,096-7,168 这个范围。原因也很直接——MoE 架构让模型用"更多专家"而非"更宽的维度"来增加容量,这样对 GPU 显存更友好。

选这个范围内的值,有三重考量:

① 表达能力 — 太小装不下。每个维度可以理解为模型学出来的一个"语义特征探测器"。比如某个维度可能对"动物程度"敏感(狗=0.8, 猫=0.7, 石头=0.05),另一个维度对"正式程度"敏感("阁下"=0.9, "您"=0.6, "你"=0.2, "喂"=-0.5)。维度太少,微妙差别就丢了:

50 维:   能区分"大/小"、"快/慢",但分不开"高兴"和"欣喜"
300 维:  经典 Word2Vec 维度,能做国王-男人+女人≈女王这种类比
4096 维: 足以编码讽刺语气、敬语层级、专业术语归属这类极微妙的信息

② 计算成本 — 太大算不动。Self-Attention 的计算量 = 是序列长度, 是每层向量维度。 翻倍,算力直接翻倍。更关键的是显存——Attention 中间矩阵是 级别:

d=768 (GPT-2 Small, 2019):     Attention 矩阵 ≈ 4MB
d=4096 (LLaMA-7B, 2023):       Attention 矩阵 ≈ 64MB
d=12288 (GPT-3, 2020):         Attention 矩阵 ≈ 576MB
── 2026 年 ──
d=4096 (DeepSeek V4 Flash):    Attention 矩阵 ≈ 64MB
d=5120 (Llama 4 Scout):        Attention 矩阵 ≈ 100MB
d=7168 (DeepSeek V4 Pro):      Attention 矩阵 ≈ 196MB

以上按 n=1024 估算。实际上下文窗口已达 1M token,Attention 矩阵会膨胀到 GB 级别——这也是现代模型大量采用 GQA/MQA(减少 KV 头数)和稀疏注意力(如 DeepSeek V4 的 CSA/HCA)的原因。

③ GPU 硬件偏好 — 2 的幂或 128 的倍数对齐。GPU 内存访问按 128 字节对齐,矩阵乘法在维度是 2 的幂或 128 的倍数时效率最高:

768  = 64 × 12   ← GPT-2 Small(2019),64 的倍数
1024 = 2^10      ← 经典 2 的幂
4096 = 2^12      ← LLaMA-7B(2023)& DeepSeek V4 Flash,正好 2 的幂
5120 = 128 × 40  ← Llama 4 Scout(2026),128 的倍数
7168 = 128 × 56  ← DeepSeek V4 Pro(2026),128 的倍数

所以 4096 本质上是"够表达 + 算得动 + GPU 喜欢"三个约束下的折中点。模型越大,d 就可以(也需要)越大,因为更大的模型有更多参数来"填满"这些维度,不会浪费。

向量空间里的语义几何

嵌入的妙处在于:语义相近的词,向量也相近。看一张实际的词嵌入 3D 可视化:

词嵌入 3D 可视化

上图:200 个单词的 Word2Vec 嵌入投影到 3D 空间(PCA 降维)。颜色代表语义聚类——动物(紫色)抱团、食物(黄色)扎堆、交通工具(青色)聚在一起。这就是向量空间里的"语义地图"。

更神奇的是,向量之间的方向编码了语义关系。最经典的例子:

"国王" 的向量 - "男人" 的向量 + "女人" 的向量 ≈ "女王" 的向量

或者说:从"国王"到"女王"的方向,和从"男人"到"女人"的方向几乎平行。这暗示向量空间不仅记住了"什么词像什么词",还学会了"性别"这个抽象概念的方向。

当然,这是静态嵌入(Word2Vec、GloVe 时代的技术)。静态嵌入的问题是:同一个词在所有上下文里只有一套向量。"苹果"不管是在讲水果还是讲手机,都是同一个向量。

现代 LLM 用的是上下文嵌入(contextual embedding)——同一个词元经过 Self-Attention 之后,会根据它周围是什么词,动态调整自己的向量表示。比如:

"我吃了一个苹果"      → "苹果" 的最终向量靠近 "水果"、"食物"
"我买了一个苹果手机"   → "苹果" 的最终向量靠近 "科技"、"电子产品"

输入的嵌入向量是一样的(都是"苹果"对应的那行),但经过 Attention 层的"上下文搅拌"后,输出的表示天差地别。

Positional Encoding(位置编码)

Transformer 有一个先天缺陷:Self-Attention 计算时,所有位置一视同仁,它不知道谁先谁后。对于模型来说,"我爱你"和"你爱我"的词袋(bag of words)完全一样。

解决方案:在词嵌入上叠加位置信号。原始论文用的是正弦/余弦函数:

正弦余弦位置编码

上图:位置编码的可视化。每一行是一个位置,每一列是一个维度。不同维度以不同的频率振荡——低频维度区分大范围位置,高频维度区分相邻位置。图片来源:dvgodoy / CC BY

公式

对于位置 和维度 ),原始 Transformer 定义:

其中:

  • = 词元在序列中的位置(0, 1, 2, ...)
  • = 嵌入向量的总维度
  • = 维度对的索引(
  • = 具体的偶数维度下标

每个位置的编码是一个固定的向量(不是学出来的),直接加到词嵌入上:

最终输入 = 词嵌入向量 + 位置编码向量

加完之后,位置 0 的"我"和位置 2 的"我"就有了不同的向量表示,模型就能感知词序了。

为什么是 sin 和 cos?动手算一个

用一个缩小的例子来直观感受。假设 d = 8(实际是 4096),位置 pos = 3:

k=0: 2k=0, 2k+1=1
  波长 = 10000^(0/8) = 10000^0 = 1
  PE(3, 0) = sin(3/1) = sin(3)    = 0.141
  PE(3, 1) = cos(3/1) = cos(3)    = -0.990

k=1: 2k=2, 2k+1=3
  波长 = 10000^(2/8) = 10000^(0.25) = 10
  PE(3, 2) = sin(3/10)            = 0.296
  PE(3, 3) = cos(3/10)            = 0.955

k=2: 2k=4, 2k+1=5
  波长 = 10000^(4/8) = 10000^(0.5) = 100
  PE(3, 4) = sin(3/100)           = 0.030
  PE(3, 5) = cos(3/100)           = 1.000

k=3: 2k=6, 2k+1=7
  波长 = 10000^(6/8) = 10000^(0.75) ≈ 1000
  PE(3, 6) = sin(3/1000)          ≈ 0.003
  PE(3, 7) = cos(3/1000)          ≈ 1.000

位置 3 的编码向量: [0.141, -0.990, 0.296, 0.955, 0.030, 1.000, 0.003, 1.000]

观察规律:

pos=0:  [0, 1, 0, 1, 0, 1, 0, 1]    ← sin(0)=0, cos(0)=1
pos=1:  [0.842, 0.540, 0.100, 0.995, 0.010, 1.000, 0.001, 1.000]
pos=2:  [0.909, -0.416, 0.199, 0.980, 0.020, 1.000, 0.002, 1.000]
pos=3:  [0.141, -0.990, 0.296, 0.955, 0.030, 1.000, 0.003, 1.000]

每个维度对是一个不同频率的振荡器。k=0 是最高频(波长 = 1,每个位置翻转一次),k=3 是最低频(波长 ≈ 1000,变化极慢)。低频维度几乎不变,相当于给句子远距离的"章节编号";高频维度快速变化,给相邻词元精确的"相邻指纹"。

核心特性:线性相对位置

选择 sin 和 cos 不是一个随意的决定——它们有一个精妙的数学性质:

位置 pos + offset 的编码,可以由位置 pos 的编码通过一个固定的线性变换得到。

用公式表达(对于每个固定的 ,令 ):

推导基础是三角恒等式 ,把 看作 看作

这意味着什么? "位置 5"和"位置 8"之间的关系(Δ = 3)可以直接从它们的编码中通过矩阵乘法算出来——不需要额外的学习。注意力机制在计算 Q·K 时,会自然地利用这个性质,让模型感知两个词元之间的相对距离而不仅是它们的绝对位置。这就是为什么即使训练时只见过长度 1024 的序列,模型也能在一定程度上泛化到更长的文本。

现代 LLM 大多已改用 RoPE(旋转位置编码,Rotary Position Embedding)。它直接在 Attention 的 Q、K 计算时注入位置信息,效果更好,尤其在长上下文场景下。原理是把向量在二维子空间里旋转一个与位置成正比的角度——位置差越大的两个词,Q 和 K 被旋转的角度差也越大。这个细节在进阶阶段再展开。


第三站:Self-Attention(自注意力)

这是 Transformer 的心脏。 理解了这一步,整个架构的大门就敞开了。

直观理解:图书馆检索的类比

想象你去图书馆找资料写一篇关于"苹果公司供应链"的报告。你不会把书架上所有书都翻一遍,而是:

  1. 脑子里有个查询意图("我需要找供应链管理方面的内容")
  2. 扫过每本书的标题和目录,判断相关性
  3. 对高度相关的书,仔细阅读并摘录内容
  4. 综合所有摘录,写进你的报告

Self-Attention 做的是同一件事,只不过是在向量空间里:

图书馆类比Self-Attention 中的对应
你的查询意图Q(Query,查询向量) — "我在找什么?"
每本书的标题/目录K(Key,键向量) — "我这本书讲什么?"
每本书的实际内容V(Value,值向量) — "我能提供什么信息?"
你根据相关性决定读多少Attention 权重 — Q·K 的点积,越大越相关
综合摘录写报告加权求和 — 所有 V 按权重加起来

Q、K、V 三步走

下面用代码的思维(而非数学公式)走一遍完整流程。假设输入是 3 个词元,每个的嵌入向量是 4 维:

python
# 输入:3 个词元,每个 4 维
X = [
    [1.0, 0.0, 1.0, 0.0],   # 词元 0: "我"
    [0.0, 2.0, 0.0, 2.0],   # 词元 1: "爱"
    [1.0, 1.0, 1.0, 1.0],   # 词元 2: "你"
]

第一步:生成 Q、K、V。每个词元的嵌入向量分别乘以三套不同的权重矩阵:

"我"的向量 [1, 0, 1, 0] × W_Q → Q₀ = [1, 0, 2]
"我"的向量 [1, 0, 1, 0] × W_K → K₀ = [0, 1, 1]
"我"的向量 [1, 0, 1, 0] × W_V → V₀ = [1, 2, 3]

每个词元都这样算一遍,得到三套向量:

Self-Attention QKV 向量

上图:每个词元通过乘以 W^Q、W^K、W^V 产生自己的 Q、K、V 向量。注意——同一个 W^Q 对所有词元复用,W^K 和 W^V 同理。图片来源:Jay Alammar

权重矩阵到底长什么样?

上面代码中的简化版本(4 维 → 3 维),W^Q、W^K、W^V 的形状各是 4 × 3。但在真实模型中,它们大得多。以 2026 年的 DeepSeek V4 Flash 为例:

嵌入维度 d_model = 4096
每个头的维度 d_k = 64(MLA 压缩后的等效维度)

W^Q: [4096, 64]    ← 4096 行,64 列,共 262,144 个参数
W^K: [4096, 64]    ← 同样大小
W^V: [4096, 64]    ← 同样大小

三个矩阵合计: 3 × 4096 × 64 = 786,432 个参数(仅一个注意力头!)

64 个头 × 786,432 = 约 5000 万个参数,仅 Attention 层的 QKV 投影就这么多。

DeepSeek V4 实际用了 MLA(Multi-head Latent Attention),先压缩到 1024 维的潜空间再投影到各头,实际参数比这个少。这里用简化数字帮助建立直觉。

这些数字是怎么来的?

训练开始时:W^Q、W^K、W^V 里的数字全是随机初始化的(通常用 Xavier 或 Kaiming 初始化,让初始输出不会太大也不会太小)。此时模型"瞎猜"——Q 和 K 的点积毫无意义。

训练过程中:模型读海量文本,每看到一个句子就去预测下一个 token。预测错了,反向传播会根据误差去微调 W^Q、W^K、W^V 里的每一个数字。经过几万亿 token 的训练后,这些矩阵逐渐"学会":

  • W^Q 学会把输入投影成"我想找什么"的信号——比如"动词后面通常跟名词",那"吃"这个位置的 Q 向量就会被训练成倾向于找食物类名词的 K
  • W^K 学会把输入投影成"我是什么类型"的标签——"苹果"的 K 向量既带一点"水果"标签也带一点"科技公司"标签
  • W^V 学会把输入投影成"我能提供什么信息"——当"苹果"被 Attention 选中时,它把自己在上下文中的实际语义贡献出去

为什么需要三套矩阵,一套不够吗?

关键在于 Q 和 K 的职责不同。用一个类比来理解:

你是侦探,要调查"谁和谁是同伙":

Q(Query / 查询):你手里拿的嫌疑人照片  → "我要找这样的人"
K(Key / 键):  每个人档案里的特征描述  → "我长这样"
V(Value / 值): 每个人的详细档案        → "抓到我的话,你能得到这些信息"

Attention 的流程就是:拿着照片(Q)去和每份档案的特征描述(K)比对,算出匹配度,然后按匹配度去调取详细档案(V)。

如果 Q 和 K 共享同一套权重,就相当于你用"照片"去比"照片"——只能判断两个人长相是否相似。这限制很大——有时候你想找的不是"长得像的人",而是"身高超过 180cm 的人"(Q 里可以编码身高需求,K 里编码实际身高)。Q 和 K 分开,让模型可以学到"用属性 A 去匹配属性 B",表达能力大幅提升。

Q/K 分离: "我想找【动作的承受者】" vs "我是【名词,可以当宾语】" Q/K 共享: "我想找【名词】" vs "我是【名词】" ← 浪费一套矩阵的表达空间

第二步:用 Q 和 K 算相关性。词元 0("我")想知道应该关注谁,就用 Q₀ 去和所有人的 K 做点积:

Q₀ · K₀ = 1×0 + 0×1 + 2×1 = 2    → "我"和自己:相关性 2
Q₀ · K₁ = 1×1 + 0×2 + 2×1 = 3    → "我"和"爱":相关性 3
Q₀ · K₂ = 1×1 + 0×0 + 2×1 = 3    → "我"和"你":相关性 3

值越大,"我"越应该关注那个词元。

第三步:Softmax 归一化。把得分转成一组"权重",总和为 100%。

什么是 Softmax?

Softmax 是一个把任意实数变成概率分布的函数——无论输入是正数、负数、零、还是很大很小的数,输出一定满足:

  1. 每个输出值在 0 到 1 之间
  2. 所有输出值加起来等于 1

公式:

其中 (自然常数)。

手算一遍

回到刚才的例子,输入是三个得分 [2, 3, 3]:

第一步:对每个得分取 e 的幂
  e² = 2.718²  = 7.389
  e³ = 2.718³  = 20.086
  e³ = 2.718³  = 20.086

第二步:求和
  7.389 + 20.086 + 20.086 = 47.561

第三步:每个 e^x 除以总和
  Softmax(2) = 7.389 / 47.561  = 0.155 → 15.5%
  Softmax(3) = 20.086 / 47.561 = 0.422 → 42.2%
  Softmax(3) = 20.086 / 47.561 = 0.422 → 42.2%

所以 Softmax([2, 3, 3])[0.155, 0.422, 0.422],总和 = 1.0。

注意:这里手算结果和前面注释里的 [0.06, 0.47, 0.47] 不同,因为前面用了简化数字。重点是理解计算过程。

为什么要用 e^x?不能直接拿原始得分做归一化吗?

不能。试试直接归一化 [2, 3, 3]:

直接归一化:  2 / (2+3+3) = 0.25    3 / 8 = 0.375    3 / 8 = 0.375
             结果: [0.25, 0.375, 0.375]

看起来还行?换个例子就看出来问题了——假设有负分:

得分: [1, -2, 1]

直接归一化:  1/(1-2+1) = 1/0  ← 除以零,炸了!
             即使分母不为零,负权重在语义上也无意义

Softmax:     e^1 = 2.718, e^(-2) = 0.135, e^1 = 2.718
             求和 = 5.572
             结果: [0.488, 0.024, 0.488]  ← 负分被压到接近 0,不会炸

e^x 的三个关键作用:

① 消除负数 永远大于 0,即使 是很大的负数(,极小但不为零)。这保证了 Softmax 输出永远非负。

② 放大差异。指数函数对差值非常敏感:

原始分差不大:  [1, 2]  →  直接归一化: [0.33, 0.67]
              →  Softmax: [0.27, 0.73]  ← 差距被拉大了

原始分差很大:  [1, 5]  →  直接归一化: [0.17, 0.83]
              →  Softmax: [0.018, 0.982] ← 差距被剧烈放大

这个"赢家通吃"效应正好是 Attention 需要的——"最相关的那几个 token",而不是"每个 token 都分一点关注"。

③ 可导。反向传播时,,导数就是它自己,梯度计算非常方便。这是工程上的重要考量。

完整的直观感受

用几个对比来感受 Softmax 的"性格":

输入                 →  Softmax 输出             直觉
[1, 1, 1]          →  [0.333, 0.333, 0.333]   平均分配,三个都一样
[1, 2, 3]          →  [0.090, 0.245, 0.665]   最大值拿走 2/3
[1, 3, 10]         →  [0.000, 0.001, 0.999]   近似独热,最大值几乎包揽全部
[1, -100, 1]       →  [0.500, 0.000, 0.500]   极低分直接被忽略

把 Softmax 用回 Attention 的语境中:

"我"的 Q 与各位置的 K 做点积 → [2, 3, 3]
       ↓ Softmax
关注权重: "我"关注自己 15.5%,关注"爱" 42.2%,关注"你" 42.2%

"我"这个字重点看"爱"和"你"——完全符合汉语中"我"的语义高度依赖谓语和宾语的直觉。

第四步:用权重去提取信息。把各位置的 V 按权重加起来:

Output₀ = 0.155 × V₀ + 0.422 × V₁ + 0.422 × V₂
        = 0.155 × [1, 2, 3] + 0.422 × [2, 8, 0] + 0.422 × [2, 6, 3]
        = [0.155, 0.310, 0.465] + [0.844, 3.376, 0.000] + [0.844, 2.532, 1.266]
        = [1.843, 6.218, 1.731]

这就是"我"经过这次 Self-Attention 后得到的新表示。它不再是孤立的"我",而是融合了整个句子上文信息之后的"我"。

矩阵形式:一次性算完

上面的计算在实际中是所有词元并行、一次矩阵乘法搞定的:

Self-Attention 矩阵计算

上图:把全部词元的嵌入堆成矩阵 ,一次性乘以 得到 矩阵,然后 。图片来源:Jay Alammar

python
# 实际的计算(伪代码)
Q = X @ W_Q    # (3×4) × (4×3) = (3×3)
K = X @ W_K    # 同上
V = X @ W_V    # 同上

scores = Q @ K.T / sqrt(d_k)    # (3×3) — 每对词元之间的相关性
weights = softmax(scores, dim=-1)  # 每行归一化为概率
output = weights @ V             # (3×3) — 加权融合后的新表示

Softmax 之前的 score 矩阵长这样

       K₀  K₁  K₂
Q₀  [  2   3   3  ]    ← "我"关注"爱"和"你"最多
Q₁  [  2   4   4  ]    ← "爱"也关注后面最多
Q₂  [  1   2   3  ]    ← "你"最关注自己(因为在句末,前文都是它的上下文)

经过 Softmax 后每行的和 = 1,变成了一套"关注度分配方案"。这就是 Attention 的魔力——每个词都在问"我应该多看谁?",然后用别人提供的信息来更新自己

为什么要除以 √dₖ

先看问题本身。Q 和 K 的每个维度在初始化时是独立的随机数(均值 0,方差 1)。点积 是把 个独立随机变量的乘积加起来:

每个 的方差是 1(两个独立标准正态变量的乘积方差 = 1), 个加起来,点积的方差 ≈

这意味着维度越大,Q·K 的值就越大,而且越分散。

举个具体例子

假设 ,Q 和 K 的每个分量都在 [-1, 1] 附近随机取值:

Q = [0.5, -0.3]    K₀ = [0.2, 0.8]    → Q·K₀ = 0.5×0.2 + (-0.3)×0.8 = -0.14
                    K₁ = [-0.7, 0.1]   → Q·K₁ = 0.5×(-0.7) + (-0.3)×0.1 = -0.38
                    K₂ = [0.6, 0.4]    → Q·K₂ = 0.5×0.6 + (-0.3)×0.4 = 0.18

三个得分: [-0.14, -0.38, 0.18] → Softmax → [0.31, 0.24, 0.44]
分布比较均匀,每条都分到了一些关注。

现在把 加大到 128(每项仍在 [-1, 1] 附近),方差 ≈ 128:

三个得分可能变成: [-5.2, 8.7, -3.1] → Softmax → [0.000, 0.999, 0.000]

                           8.7 把其他全压没了,几乎独热!

Softmax 输出接近 one-hot 时,梯度接近零——因为改变那个唯一的大数值之前的值,Softmax 输出几乎不变。模型无法从这样的梯度中学到任何东西。

为什么是 √dₖ 而不是 dₖ 或其他值?

目标是把点积的方差从 拉回 1。方差的性质:

而不是 ,是因为我们要的是对方差的线性缩放,而要缩放的量是标准差()。除以 会把方差压到 ,矫枉过正了。

总结

没有缩放除以
点积方差 ≈ (越大越分散)点积方差 ≈ 1(稳定)
Softmax 趋向 one-hotSoftmax 输出柔和
梯度接近零,学不动梯度健康,正常学习
大模型直接崩大模型正常训练

这个 是原始 Transformer 论文中最不起眼却最关键的细节之一——没有它,深层 Attention 模型根本训不起来。


第四站:Multi-Head Attention(多头注意力)

一句话定义:单头只能学一种"关系模式",多头并行就能同时关注语法、语义、指代等多个维度。

多头注意力

上图:Multi-Head Attention 的结构。每个 Head 有自己独立的 W^Q、W^K、W^V,在同样的输入上并行计算,最后把结果拼起来。图片来源:dvgodoy / CC BY

为什么一个头不够?

先回到单头的场景。单头 Attention 会产出一套注意力权重——"我"关注"爱"42%,关注"你"42%,关注"自己"16%。这套权重同时编码了所有关系类型:语法、语义、指代……全部搅在一起。

问题在于:Softmax 是一种竞争机制。权重总和强制为 100%,如果一个关系想要更多,就必须挤占其他关系的份额。比如"我"需要关注动词"爱"(语法需求),同时也需要关注后面的宾语(语义需求),但单头的蛋糕就这么大,只能折中分配。

多头解决了这个问题:每个头有自己的蛋糕(各自做独立的 Softmax),互不抢份额。"语法头"可以给动词 80% 的关注,"语义头"可以给宾语 70% 的关注——同时满足,无需妥协。

具体怎么做

把 Q、K、V 沿维度方向均切为 份,每个头拿一份缩小版的子空间:

原始: Q, K, V 各是 [seq_len, d_model]

切分: Q → [Q₁, Q₂, ..., Q_h]    每个 Q_i 是 [seq_len, d_model / h]
      K → [K₁, K₂, ..., K_h]    每个 K_i 是 [seq_len, d_model / h]
      V → [V₁, V₂, ..., V_h]    每个 V_i 是 [seq_len, d_model / h]

每个头独立做 Attention:

最后把所有头的输出拼接,过一个线性层 融合:

的形状是 ,负责把各头的信息"搅拌"成一个统一表示。

动手算一个迷你多头

还是"我爱你"那个例子,嵌入维度 d=6,头数 h=2,每个头拿到 d/h = 3 维:

输入 X (3 个词元,6 维):
  "我" = [1.0, 0.0, 1.0, 0.0, 0.5, 0.5]
  "爱" = [0.0, 2.0, 0.0, 2.0, 1.0, 0.0]
  "你" = [1.0, 1.0, 1.0, 1.0, 0.0, 1.0]

Head 1 拿前 3 维:                Head 2 拿后 3 维:
  "我"₁ = [1.0, 0.0, 1.0]          "我"₂ = [0.0, 0.5, 0.5]
  "爱"₁ = [0.0, 2.0, 0.0]          "爱"₂ = [2.0, 1.0, 0.0]
  "你"₁ = [1.0, 1.0, 1.0]          "你"₂ = [1.0, 0.0, 1.0]

两个头各自走一遍 Q·K → Softmax → ×V 流程:

Head 1 的注意力权重:               Head 2 的注意力权重:
  "我" → [自己:30%, 爱:40%, 你:30%]  "我" → [自己:10%, 爱:10%, 你:80%]
  "爱" → [我:25%, 自己:50%, 你:25%]  "爱" → [我:40%, 自己:20%, 你:40%]
  "你" → [我:30%, 爱:30%, 自己:40%]  "你" → [我:45%, 爱:10%, 自己:45%]

输出 Head 1: [3×3]                输出 Head 2: [3×3]

拼接后:

"我" 的输出 = [Head1的3维, Head2的3维] = 6 维向量

注意两个头给出了非常不同的关注方案——Head 1 相对均衡,Head 2 中"我"把 80% 的注意力给了"你"。这两套视角拼接后,6 维输出里同时携带了两类信息。

各头真的学到了不同的东西吗?

是的。研究者通过可视化 Attention 权重发现,不同头确实自发分化了分工。以下是一些被反复观察到的模式:

头类型行为特征例子
语法头关注相邻位置的句法搭配形容词 → 它修饰的名词
指代头关注前文中提到过的实体"它" → 三句话前的"那只猫"
分隔头关注标点或从句边界逗号、句号之间的片段
位置头关注固定偏移距离的词总是关注前一个或前两个词
冗余头和其他头学到几乎相同的模式约占 10-30%,可能浪费但不会损害性能

研究还发现:底层(靠近输入)的头分工更明显,高层(靠近输出)的头趋向均匀关注——底层在做具体的模式匹配,高层在做全局语义融合。

多头 vs 单大头:为什么必须"切"?

一个自然的疑问:多头的总参数量和"一个 d_model × d_model 的大头"一样,为什么非要分成多个小头?

关键是 Softmax 是全局归一化。一个大头只有一套 Softmax,所有维度的信息共享同一套竞争:语法关系必须和指代关系争夺注意力份额。 个小头有 套独立的 Softmax,每个可以有自己的"赢家"。

打个比方:一个评审委员会评审一个项目——

单头(一个大评委): 要同时评估"技术方案""团队能力""市场规模""财务预测"
  → 总分 100%,技术占了 40%,市场规模就只能拿 20%,各维度互相挤占

多头(h 个专业评委): 
  Head 1(技术评委): 只看技术方案,技术相关的 token 拿高分
  Head 2(团队评委): 只看团队背景,人名、履历类 token 拿高分
  Head 3(市场评委): 只看市场数据,数字、百分比类 token 拿高分
  → 每个评委独立打分,互不干扰

这就是为什么要切:不是因为参数量不够,而是因为 Softmax 的归一化范围限制。多头 = 多套独立的 Softmax 通道。


第五站:完整的 Transformer 层

一个完整的 Transformer 层把三样东西串联起来:Multi-Head Attention → Feed-Forward Network → 配套的残差 + 归一化。这三个组件互相配合,缺一不可。

残差连接(Residual Connection)

一句话:把输入原封不动地加回输出,给梯度一条不衰减的"直达通道"。

没有残差会怎样?

先借一个概念——下一站(第六站)会讲到,实际的 LLM 会把上面这个结构重复堆叠几十层。这里先用 48 层作为例子,看看多层叠加时会出什么问题:

最后一个 token 的预测误差要传给第一层的参数,在没有残差的情况下,必须穿过 48 层里的每一个矩阵乘法:

梯度₀ = 误差 × W₄₈ × W₄₇ × ... × W₂ × W₁

如果每个 让梯度乘上 0.9(小于 1 在深层网络中很常见),传到第一层时还剩:

梯度只剩原来的 0.6%,第一层的参数几乎不受更新。这叫梯度消失——网络越深,前面的层越学不动。反过来,如果乘数大于 1(比如 1.1),,梯度爆炸,训练直接崩。

加了残差之后

输出 = x + F(x)     (F 是 Attention 或 Feed-Forward Network——后面马上会讲,先把它理解为"对每个词元单独做处理的模块")

反向传播时:

那个 +1 就是关键。即使 非常小,梯度至少有一个保底值 1 能从输出直接传到输入。48 层过后,梯度不会衰减到 0。

这就是"高速公路"的含义—— 学的是输入需要改变多少,而不需要从头学整个输出。如果某层 F(x) 暂时没学到有用的东西,它的输出趋近于 0,残差连接保证 x 原样传到下一层,不会造成灾难性破坏。相当于每一层都在说:"先原样保留,我再试着优化一点。"

不止防衰减

残差连接还有一个意外好处:当网络有多层时(下一站会讲到),初始化时所有层的 输出接近 0(权重初始值很小),信号几乎纯粹通过残差通道往后传——相当于训练一开始时后面的层暂时"休眠",随着训练推进,各层的 逐渐"醒来"。残差让网络可以安全地从浅到深逐步学习。

Layer Normalization(层归一化)

它在做什么

LayerNorm 对一个样本内所有特征维度做标准化。公式:

其中:

  • (均值)
  • (标准差)
  • 是可学习参数(让模型自己决定要缩放到什么范围)

一个具体例子。假设某个 token 经过 Attention 后的向量是:

x = [0.2, 8.5, -3.1, 1.4]     ← 数值范围 -3.1 到 8.5,差距很大

μ = (0.2+8.5-3.1+1.4)/4 = 1.75
σ = sqrt(((0.2-1.75)²+(8.5-1.75)²+(-3.1-1.75)²+(1.4-1.75)²)/4)
   = sqrt((2.40+45.56+23.52+0.12)/4) = sqrt(17.90) ≈ 4.23

标准化后: [-0.37, 1.59, -1.15, -0.08]    ← 均值 0,标准差 1

数值不再"爆炸"也不"消失",始终在合理范围内。当网络堆叠多层时,每一层的激活值都可能偏离初始分布——LayerNorm 在每一层结束时把它们拉回来,防止逐层累积的数值漂移。

为什么是 LayerNorm 而不是 BatchNorm?

**BatchNorm(批归一化)**是归一化的另一种流派。在 2012-2017 年的 CNN(卷积神经网络)时代,BatchNorm 是标配——那个时期深度学习的主战场是图像识别(ImageNet 竞赛),ResNet、VGG 这些经典架构全都靠 BatchNorm 稳定训练。但 2017 年 Transformer 诞生后,NLP 成为新主战场,序列数据的变长特性让 BatchNorm 水土不服,LayerNorm 顺势成为新标准。进入 2024-2026 年,几乎所有前沿模型都进一步简化为 RMSNorm。它的做法是:对同一个特征维度,跨 batch 中所有样本求均值和方差,然后标准化。

用一个具体例子区分两者。假设有 3 个样本,每个样本是一个 4 维向量:

样本 1: [0.2, 8.5, -3.1, 1.4]
样本 2: [1.1, 2.3, 4.2, -0.5]
样本 3: [3.0, 1.2, 0.8, 2.1]
LayerNorm(逐样本):              BatchNorm(逐特征维度):

对样本 1 的 4 个数字求 μ,σ         对维度 0 的 [0.2, 1.1, 3.0] 求 μ,σ
→ 样本 1 标准化后的 4 个值          → 维度 0 的 3 个值标准化
对样本 2 的 4 个数字求 μ,σ         对维度 1 的 [8.5, 2.3, 1.2] 求 μ,σ
→ 样本 2 标准化后的 4 个值          → 维度 1 的 3 个值标准化
...                                ...

对比:

LayerNormBatchNorm
归一化方向沿特征维度(横向)沿 batch 维度(纵向)
统计范围一个样本内部一个特征跨所有样本
受 batch size 影响完全不受严重依赖大 batch
训练/推理一致完全一致需要切换(训练时用当前 batch 统计,推理时用历史 moving average)
序列长度变化天然兼容每个位置需要单独统计,变长时很麻烦

Transformer 选择 LayerNorm 的原因很直接:输入是变长序列,每个 token 位置独立计算,BatchNorm 跨序列统计时既会遇到变长带来的 padding 问题,又依赖足够大的 batch size 才能让统计量稳定。LayerNorm 只在每个 token 自己内部操作,天然适合 NLP 场景。

2026 年:从 LayerNorm 到 RMSNorm

现代 LLM 大多已改用 RMSNorm。它砍掉了"减均值"这一步,只做"除标准差":

省了一步计算(均值的加减),对几百亿参数的大模型来说每一丁点节省都很可观——推理时快约 5-10%,而效果几乎没差别。Llama 全系列、DeepSeek V3/V4、Kimi K3 都用 RMSNorm。

Pre-LN vs Post-LN

还有一个设计选择:LayerNorm 放在子层前面还是后面?

Post-LN(原始论文):        Pre-LN(现代做法):
  x + Attention(LN(x))       LN(x + Attention(x))    ← 先加残差再归一化
  或更常见的:                或写成:
  LN(x + Attention(LN(x)))   x + Attention(LN'(x))   ← LN 在前,残差在外

Pre-LN 的优势是训练更稳定——梯度不需要穿过 LayerNorm 的倒数,可以直接通过残差通道传播。所有 2026 年主流模型都用 Pre-LN(或 Pre-RMSNorm)。

Feed-Forward Network(前馈网络)

为什么 Attention 之后还需要 FFN?

Self-Attention 本质是线性加权——把不同 token 的 V 向量按权重加起来。加完之后,每个 token 的表示都是其他 token 的线性组合。但语言中的很多规律是非线性的:

"not good"   ≠ "not" + "good" 的平均
"very small" ≠ "very" + "small" 的加权和
"如果下雨,我就不去"  →  条件和结果的逻辑关系无法用加权求和表达

更关键的是,Attention 的运算是逐位置对称的——不管多少层 Attention 摞在一起,只要没有非线性,本质上还是在做加权求和(多个线性变换的复合仍是线性的)。FFN 负责打破这个限制:给每个 token 单独加入非线性变换,让它能从自己的表示里"提炼"出更高级的特征。

如果说 Attention 是"把相关的信息从别处拉过来",FFN 就是"把拉来的信息消化掉"。还有一个重要区别:FFN 是逐位置独立的——处理 token 时只看 token 自己的向量,同一个权重矩阵对所有位置通用。这意味着 FFN 学到的是"通用的知识提炼能力",不依赖上下文位置。

结构:膨胀再收缩

传统 FFN = 两个全连接层,中间夹激活函数,形状像一个沙漏——先膨胀 4-8 倍,再缩回原尺寸:

输入: [1, d_model]
  ↓ W₁ 投影
中间: [1, d_ff]     ← d_ff 通常是 d_model 的 4-8 倍
  ↓ Activation
  ↓ W₂ 投影
输出: [1, d_model]

用公式表达:

以一个简化的非 MoE 配置为例(d_model = 4096,膨胀 4 倍):

d_model = 4096, d_ff = 4096 × 4 = 16384

W₁: [4096, 16384]  ← 约 6710 万参数
W₂: [16384, 4096]  ← 约 6710 万参数

光一个 FFN 层就约 1.34 亿参数。同层的 Attention(Q、K、V、O 四个矩阵)只有约 万。FFN 占了模型参数的大头(约 2/3)——中间一膨胀,两个大矩阵吃掉大量参数。

现代模型大多使用 MoE(混合专家)架构,FFN 层被拆成多个专家,下一站会展开讲。这里先用简化数字建立直觉。

为什么膨胀之后再缩回去?高维中间层给模型足够的"容积"去记忆海量训练数据中的知识模式。具体机制见下一小节"键值记忆假说"。

激活函数详解:ReLU → GELU → SwiGLU

激活函数是 FFN 的灵魂——没有它,两层全连接矩阵乘在一起还是一个矩阵,非线性就无从谈起。

ReLU:最简单的激活函数,

输入:  [-3, -1, 0, 2, 5]
输出:  [ 0,  0, 0, 2, 5]    ← 负值全被"截断"

优点是计算极快(就是一个 if 判断),缺点也很明显:负值直接归零,梯度也为零。一旦某个神经元输出的加权和变成了负数,ReLU 就会让它"死掉"——从此不再更新。在早期 CNN 时代这问题不大(图像像素本身就是非负的),但对语言模型来说,很多有意义的信号恰好是负值(比如表示否定、转折的向量维度)。

GELU(Gaussian Error Linear Unit):ReLU 的平滑改良版。不再一刀切地清零,而是根据输入值的大小概率性地决定通过多少:

其中 是标准正态分布的累积分布函数——输入 越大,越确定它应该通过(接近 1);输入 越负,越确定它应该被抑制(接近 0);中间态的按比例衰减。这种平滑性让 GELU 在反向传播时梯度更稳定,BERT 和 GPT-2/3 时代的标配。

实践中用 这个近似公式计算,真实 太贵。

SiLU / Swish:进一步改进,用 Sigmoid 替代正态分布 CDF:

x = -3  →  SiLU(-3) = -3 × 0.047 ≈ -0.14   (轻微负值,不归零)
x =  0  →  SiLU( 0) =  0 × 0.5   =  0
x =  3  →  SiLU( 3) =  3 × 0.953 ≈  2.86   (几乎直线通过)

注意和 ReLU 的关键区别:负值不会被完全截断,而是逐渐衰减到接近零。这保留了负值携带的信息(比如否定语义),同时天然实现了"自门控"——Sigmoid 部分 充当了软开关。

SwiGLU:在 SiLU 基础上加入显式门控机制,把 FFN 从两层升级为三层:

然后经过第三层投影回原始维度:

这里有三步:

  1. Gate 分支 经过 投影到高维,再通过 SiLU 算出 0~1 之间的"门控系数"
  2. Up 分支 经过 投影到同一高维,作为待筛选的"信息候选"
  3. 逐元素相乘:门控系数 信息候选 = "选择性通过"
  4. Down 投影 把筛选后的高维表示压回

SwiGLU 门控 FFN 数据流

注意中间的维度从 变成了 14336。这不是随便选的——SwiGLU 有三个权重矩阵而不是两个,为了让总参数量保持和传统 FFN 一致,中间维度调整为 。数学上可以证明这个数字恰好让 SwiGLU FFN 和传统 FFN 的参数量相当。

门控的核心价值是什么?它让 FFN 学会根据输入内容动态选择激活哪些特征通道。"动词时态"相关的维度可能只在处理动词时被门控放行,其他时候被抑制;"数字计算"相关的维度在处理数学表达时打开。这种条件激活比 ReLU 的"一刀切"精细得多。

激活函数公式特点代表模型
ReLU最快,但负值归零早期 CNN
GELU平滑,概率性抑制BERT, GPT-2/3
SiLU/Swish自门控,负值不归零Llama 系列(SwiGLU 的组件)
SwiGLU显式门控,动态通道选择Llama 3/4, DeepSeek V4, Qwen 3, Kimi K3

FFN 为什么能存储知识:键值记忆假说

这是理解 FFN 最重要的一篇论文——2021 年 Anthropic 团队发现,Transformer 的 FFN 层可以理解为可训练的键值存储(Key-Value Memory)。

简化来看,传统 FFN 的公式:

  • 每一行相当于一个"键"(Key)——它检测输入向量中是否存在某个特定模式。如果输入 和某行的内积很大,说明输入匹配了这个模式。
  • 每一列相当于一个"值"(Value)——当对应的键被激活时,这个值就被写入输出向量。

用一个具体例子来建立直觉:

输入 x 中包含了 "埃菲尔铁塔" + "位于" + 地点询问模式

W₁ 第 k 行的 pattern ≈ "首都/地标 + 位置关系"
    ↓ 内积很大 → 激活该行
    ↓ Activation 输出该行的激活值给 W₂
    
W₂ 对应列输出 → "巴黎" + "法国" + "欧洲" 等维度被增强

换句话说,一个 FFN 神经元就是一条"如果输入满足 A 条件,就输出 B 信息"的规则。训练数据中反复出现的模式("巴黎是法国首都"、"水的化学式是 H₂O"、"1+1=2")会被编码进 的权重中。

这就是为什么膨胀倍数很重要。 越大,每层就能存储越多的键值对。一个 d_model=4096、d_ff=16384 的 FFN 层有 16384 个"知识槽位"。48 层加起来就是约 78 万个潜在的知识存储位置。

这也解释了为什么 FFN 占模型参数的大头:语言模型的知识存储在 FFN 里,Attention 负责的是"当前这段文本在说什么"——注意力决定从上下文的哪些位置提取信息,FFN 决定提取出来的信息意味着什么、应该转换成什么输出。

这一假说也催生了 模型编辑(Model Editing)方向——如果能精确定位"埃菲尔铁塔的高度是 330 米"存在 FFN 的哪一层哪一行,就可以直接修改权重来纠正错误事实,而不需要重新训练整个模型。

为什么是 4 倍膨胀?

业界普遍采用 4 倍膨胀(Llama 系列、DeepSeek、Qwen 都如此),这是大量实验收敛的结果:

  • 2 倍:中间容量太小,知识存储受限,模型表现下降明显
  • 4 倍:性价比甜点——再往上加容量,收益递减
  • 8 倍:GPT-3 用过(d_model=12288, d_ff=49152),效果高但训练成本飙升

SwiGLU 模型因为有三个矩阵,中间维度算的是 倍,但等效"知识容量"仍然约等于传统 4 倍膨胀。

一个完整层的全景图

子层结构

上图:一个完整 Transformer 层的内部结构(原论文 Encoder 层视角,Decoder 层也完全一样,只是 Attention 里多了一层 Causal Mask)。先 Attention + 残差 + LayerNorm,再 FFN + 残差 + LayerNorm。图片来源:dvgodoy / CC BY

把这一层画成流程图(现代 Pre-LN 风格):

Transformer 层:Pre-LN + 残差连接

这就是一层。总结三个关键洞见:

  1. 残差连接是保底的 让梯度不至于消失,也允许每层"保守地"只做微小改动
  2. LayerNorm/RMSNorm 是稳定的:把激活值始终控制在合理范围,防止逐层漂移
  3. FFN 是知识的容器:Attention 负责"问"(从上下文聚合信息),FFN 负责"答"(从自己的参数中提取知识)

把这一层重复堆叠 N 次,就是一台越来越"懂"语言的机器。接下来第六站就讲:怎么从单层搭出一个完整的模型。


第六站:从层到模型

堆叠:层数越多越"聪明"?

GPT-3 有 96 层,每层 12,288 维,12,288/64 = 192 个注意力头。各模型的层数和维度:

模型层数隐藏维度头数KV 头数总参数激活参数
GPT-2 Small (2019)12768121.24 亿1.24 亿
GPT-3 (2020)9612,288961,750 亿1,750 亿
LLaMA-7B (2023)324,0963270 亿70 亿
Llama 4 Scout (2026)485,1204081,090 亿170 亿
DeepSeek V4 Flash (2026)434,0966412,840 亿~130 亿
DeepSeek V4 Pro (2026)617,16812811.6 万亿~490 亿
Kimi K3 (2026)未公开未公开未公开未公开2.8 万亿~500 亿

注:Kimi K3 完整架构参数将于 2026/07/27 技术报告中公开。所有 2026 年模型均为 MoE(混合专家)架构,激活参数远小于总参数。

堆叠结构

上图:Encoder 和 Decoder 各自由 N 个相同结构(但参数不同)的层堆叠而成。GPT 系只用了右边的 Decoder 部分。图片来源:dvgodoy / CC BY

可以这样理解层的作用:底层学的是表面的语法特征("形容词后面常跟名词"),中层学的是句法和语义("谁对谁做了什么事"),高层学的是抽象的推理和规划("这段话的论点是……")。

参数是怎么算出来的

这也是第一阶段提过的问题——参数到底长什么样?以 GPT-2 Small 为例,手算一遍:

组件计算公式数量
词嵌入矩阵词表大小 × 隐藏维度50,257 × 768 = 3860 万
位置嵌入矩阵最大长度 × 隐藏维度1024 × 768 = 79 万
每层的 Attention4 × (d × d)(W^Q, W^K, W^V, W^O)4 × (768 × 768) = 236 万
每层的 FFNd × 4d + 4d × d(两层全连接)2 × (768 × 3072) = 472 万
每层 LayerNorm2 × (2 × d)(两个 LN,各有 γ 和 β)约 1.2 万

一层 ≈ 236 万(Attention) + 472 万(FFN) + 1.2 万(LN) ≈ 709 万。12 层 ≈ 8,508 万。加上嵌入矩阵 ≈ 3,939 万,总计约 1.24 亿

可以看到:参数的大头在 FFN(约占 2/3),其次是 Attention(约占 1/3),Embedding 占比不大但对于大词表也不可忽视。

GPT-2 Small 是一个稠密模型(dense model)——每次推理所有参数都参与计算。现代模型普遍采用 MoE(混合专家)架构:FFN 层被拆成多个"专家",每次只激活一小部分。

以 DeepSeek V4 Flash 为例:总参数 2,840 亿,256 个专家,每次只激活 6 个 + 1 个共享专家 ≈ 仅激活 ~130 亿参数。Kimi K3 更极端:总参数 2.8 万亿,896 个专家,每次激活 16 个 ≈ 仅激活 ~500 亿参数(不到总参数的 2%)。

这就是 MoE 的核心优势——用"总参数"换"知识容量",但推理成本只和"激活参数"挂钩,不随总参数线性增长。


第七站:自回归生成

理解了模型内部结构后,回到它的"日常工作"——怎么输出文字。

逐 token 循环

输入 "中国的首都是"
  → 模型内部全部算一遍 → 输出概率分布 → 抽样得到 "北京"
  
输入 "中国的首都是北京"
  → 模型内部全部算一遍 → 输出概率分布 → 抽样得到 "。" 或 EOS
  
输入 "中国的首都是北京。"
  → 模型内部全部算一遍 → 输出 EOS(停止) → 生成结束

每次生成一个 token,就把它拼回输入末尾,整个序列重新过一遍模型。这就是自回归(autoregressive)——自己生成的东西成为自己下一步的输入。

自回归采样流程

上图:自回归采样的完整循环。虚线箭头表示生成的 token 被追加回上下文。图片来源:PLOS One (CC BY 4.0)

KV Cache:为什么不用每次都重算?

上面的循环描述了一个朴素的做法——每生成一个新 token,就把整个序列重新喂给模型,重新算一遍 Attention。这意味着:

第 1 步: 算 1 个 token 的 Attention
第 2 步: 算 2 个 token 的 Attention(重复了第 1 步的计算)
第 3 步: 算 3 个 token 的 Attention(重复了 1、2 步的计算)
...
第 n 步: 算 n 个 token 的 Attention(前 n-1 个全是重复劳动)

生成 100 个 token,实际做了 1+2+3+...+100 ≈ 5000 个 token 的 Attention 计算,其中 99% 是重复的。

KV Cache 的思路:Attention 中每个 token 的 K(Key)和 V(Value)只依赖自己,不依赖其他 token。一旦算出来,后续步骤里它们不会再变。所以:

第一步: 输入 "中国的首都是"
  → 算出所有 token 的 K₀, K₁, K₂, K₃, V₀, ..., V₃
  → 用最后一个位置的 Q₃ 去 attend → 生成 "北京"
  → 把 K 和 V 存进缓存

第二步: 新 token 只有 "北京"
  → 只算 "北京" 的 Q_new, K_new, V_new
  → Q_new 去 attend 缓存里的 K₀...K₃ + 新算的 K_new
  → 用 V₀...V₃ + V_new 加权融合
  → 把 K_new, V_new 追加到缓存

第三步及以后: 每步只算新 token 的 Q、K、V,历史 K、V 全部从缓存读

生成 100 个 token:第一步算 4 个 token 的 K/V,之后每一步只算 1 个新 token——总计约 4 + 99 ≈ 103 次 K/V 计算,而不是没有缓存时的 ~5050 次。

图解对比:

没有 KV Cache(每步重算全部):     有 KV Cache(只算新 token):
                                                                
Step 1: [中][国][的][首都]           Step 1: [中][国][的][首都]    
        算 K,V × 4 个                       算 K,V × 4 → 存入缓存
                                                                
Step 2: [中][国][的][首都][北京]     Step 2: 缓存[K₀K₁K₂K₃] + [北京]
        算 K,V × 5 个                       只算 K,V × 1 → 追加缓存
        前 4 个白算了…                                              
                                                                
Step 3: [中][国][的][首都][北京][是]  Step 3: 缓存[K₀K₁K₂K₃K₄] + [是] 
        算 K,V × 6 个                       只算 K,V × 1

显存代价

KV Cache 省了计算,但产生了新的显存开销。对于生成中的每一个 token,缓存需要存储:

每层 KV Cache 大小:

是因为 K 和 V 各存一份)

以一个 2026 年中档模型为例来算(近似 Llama 4 Scout 配置):

层数: 48
KV 头数: 8
头维度: 128
上下文长度: 10,000 token(已生成这么多)
batch_size: 1

单层 KV Cache = 2 × 1 × 10000 × 8 × 128 × 2 bytes (FP16) = 40.96 MB
48 层总计 ≈ 1.97 GB

当上下文到 100K token 时,仅 KV Cache 就接近 20 GB。如果 batch_size > 1(并发服务多个用户),还要翻倍。

这就是为什么长上下文模型尽管"窗口"很大,实际推理却很吃显存——显存瓶颈不在模型参数,而在 KV Cache

应对方案

2026 年主流模型的针对性优化:

技术原理效果代表模型
GQA(分组查询注意力)多个 Q 头共享一组 K/V 头,减少 K/V 头数KV Cache 缩减 4-8×Llama 4 Scout (8 KV 头 / 40 Q 头)
MQA(多查询注意力)所有 Q 头共享唯一的 K/V 头KV Cache 缩减到 1/头数DeepSeek V4 全系 (1 个 KV 头)
滑动窗口注意力每个 token 只能看到附近固定范围的 token,远程 K/V 不缓存KV Cache 有固定上限DeepSeek V4 混合注意力中的滑动窗口部分
CSA / HCA 稀疏注意力按固定间隔压缩远距离 token 的 K/V,大幅减少存储亚线性 KV Cache 增长DeepSeek V4 Pro (压缩率 4× 和 128×)

DeepSeek V4 Pro 同时用了 MQA + 稀疏注意力——KV 头只有 1 个,而且远程 token 还被压缩了。这解释了为什么它能跑 1M 上下文但显存不爆炸。

Causal Mask(因果遮罩)

在 Self-Attention 那步,当前 token 不能看到未来的 token,否则就是"作弊"——提前知道了答案。解决方式是给 score 矩阵加一个上三角遮罩(把未来的 score 设成 -∞,Softmax 后变 0):

不加 Mask 的 Attention:              加了 Causal Mask 的 Attention:
      K₀  K₁  K₂                          K₀  K₁  K₂
Q₀ [  2   3   3  ]                 Q₀ [  2   -∞  -∞  ]
Q₁ [  2   4   4  ]                 Q₁ [  2   4   -∞  ]
Q₂ [  1   2   3  ]                 Q₂ [  1   2   3   ]

Softmax([2, -∞, -∞]) → [1.0, 0.0, 0.0]  ← Q₀ 只能看自己
Softmax([2, 4, -∞])  → [0.12, 0.88, 0.0] ← Q₁ 只能看 Q₀ 和自己
Softmax([1, 2, 3])   → [0.09, 0.24, 0.67] ← Q₂ 能看所有人

采样策略:Temperature、Top-p、Top-k

模型输出的是一组概率(logits → softmax → probabilities),但"怎么从这个概率里挑一个 token"大有讲究:

Greedy(贪心):永远选概率最高的那个。问题:容易陷入重复循环("我今天去了超市,买了东西,超市里有东西,东西很多……")。

Temperature(温度):在 Softmax 之前把 logits 除以 T。

python
adjusted_logits = logits / temperature
probs = softmax(adjusted_logits)
T 值效果适用场景
T = 0等价于 Greedy(永远选最高)代码生成、数学计算
T ≈ 0.3-0.5保守,极少"冒险"事实性问答、翻译
T ≈ 0.7-1.0平衡,有一定创意日常对话
T ≈ 1.2-1.5创意丰富,偶现胡言乱语头脑风暴、写诗
T > 2.0近乎随机基本没用

原理:T < 1 时,高概率 token 被进一步放大("马太效应");T > 1 时,概率分布被"抹平",低概率 token 也有机会被选中。

Top-k 采样:只保留概率最高的 k 个 token,其余直接砍掉(概率设 0),再按概率采样。比如 k=50,就从"最有希望的 50 个候选"里抽。

Top-p 采样(Nucleus Sampling / 核采样):从高到低累加概率,累加到 p 为止(比如 p=0.9),超出部分的 token 砍掉。和 Top-k 相比,Top-p 能动态调整候选数——在确定性强的位置可能只保留 3 个候选,在模糊的位置可能保留 100 个。

实践中,大多数 API 默认使用 Top-p + Temperature 的组合。


本阶段小结

来回顾一下一个 token 的完整旅程,以及每个站点学到的核心直觉:

一个 Token 的完整旅程

下一阶段(第三阶段:训练过程)会讲这些参数是怎么"学"出来的——预训练、SFT 监督微调、RLHF 对齐——也就是模型从"什么都不会"到"能跟你聊天"的完整生产工艺。


延伸阅读与可视化资源

必看交互工具

资源说明
The Illustrated TransformerJay Alammar 手绘图解,Transformer 入门圣经
The Illustrated GPT-2同上作者的 GPT-2 深度拆解
bbycroft.net/llm3D 交互式 LLM 推理全过程可视化
Cornell BPE VisualizerBPE 分词一步步演示
Tokenizer Playground对比 GPT-2/BERT/T5 三种分词器

深入阅读

资源说明
"Attention Is All You Need"Transformer 原始论文(2017),值得通读
Andrej Karpathy - Let's build GPT from scratch从零手写 GPT,代码逐行讲解
Lilian Weng - Attention? Attention!OpenAI 研究员的 Attention 机制技术综述
Sebastian Raschka - Understanding LLMsLLM 论文阅读清单,按主题分类

图片附录

以下是本文涉及的 Transformer 子结构详解图,供仔细研究:

Self-Attention 完整计算(简化版)

Self-Attention 简化

图片来源:dvgodoy / CC BY

Self-Attention 完整计算(详细版)

Self-Attention 详细

图片来源:dvgodoy / CC BY

Decoder Self-Attention(带 Causal Mask)

Decoder Self-Attention

图片来源:dvgodoy / CC BY

完整的 Transformer 层内部展开(Encoder 视角)

Encoder 单层完整展开

图片来源:dvgodoy / CC BY


参考文献与图片来源

论文

标题作者年份链接
Attention Is All You NeedVaswani et al.2017arXiv:1706.03762 — Transformer 架构原始论文

核心参考

标题作者/来源类型链接
The Illustrated TransformerJay Alammar博客jalammar.github.io — Transformer 手绘图解,入门圣经
The Illustrated GPT-2Jay Alammar博客jalammar.github.io — GPT-2 深度拆解
Let's build GPT from scratchAndrej Karpathy视频YouTube — 代码逐行讲解
Attention? Attention!Lilian Weng博客lilianweng.github.io — Attention 机制技术综述
Understanding LLMs (Reading List)Sebastian Raschka博客sebastianraschka.com — 论文阅读清单
LLM VisualizationBrendan Bycroft交互工具bbycroft.net/llm — 3D 推理全过程可视化
BPE VisualizerCornell CS4782 (2026)交互工具cs.cornell.edu — BPE 分词一步步演示
Tokenizer PlaygroundContext Lab在线工具context-lab.com — 多分词器对比

图片来源

图片来源许可
Self-Attention QKV、矩阵计算、多头注意力、残差连接、Decoder 架构、推理动图Jay Alammar, The Illustrated Transformer / GPT-2版权保留,教学引用
完整 Transformer 架构、Encoder/Decoder 堆叠、Self-Attention 简化与详细版、多头注意力、位置编码、子层结构、K-Q 匹配dvgodoy / dl-visualsCC BY 4.0
Transformer 原始架构图 (Fig.1)Vaswani et al., "Attention Is All You Need" (2017)arXiv 论文
自回归采样流程图PLOS OneCC BY 4.0
词嵌入 3D/2D 可视化Embedding Plot (Wikipedia)CC BY-SA

Released under the MIT License.