跳到主要内容

数据截至 (上游 commit eb980a5c9eea)

Transformer 整机 — 光有注意力还开不动

这一章讲三件事: 除了注意力,一台能跑的机器还需要哪几个零件、每个零件在补什么洞; 这些零件按两种拼法装起来,为什么会得到「编码器」和「解码器」两种东西; 以及原论文的图和它自己的代码为什么对不上。 读完这一章,你手里就有了一张完整的装配图——第 04 章开始,所有模型都是在这张图上做减法。

1. 先看现象:只有注意力的机器会出什么问题

上一章结束时,你手里有一个能算「谁和谁相关」的零件。假设我们只拿它堆二十层,会发生三件事:

会出什么事为什么
「我喜欢你」和「你喜欢我」输出一模一样注意力眼里所有位置是平等的,它根本不知道语序
堆到十几层就训不动了训练信号要从最上面一层传回最下面一层,层数一多就传没了
每一层的数值越来越大或越来越小,最后算出一堆无意义的数上一层的输出就是下一层的输入,偏差会一层层放大

这一章讲的五个零件,分别就是来堵这几个洞的。

书里把这台机器叫 Transformer——2017 年那篇论文把循环网络和卷积网络全部扔掉, 只留注意力,搭出来的架构1它是后面所有大模型的祖宗。

2. 顶层全景:一张装配图

你打的一句话

├─ ① 分词器 tokenizer 切成一个个「标记」,每个标记换成一个编号
├─ ② 查表层 Embedding 编号 → 一串数(这个词的向量)
├─ ③ 位置编码 再叠一串数上去,告诉它「这是第几个字」

├─ ④ 编码器 Encoder ×N ─┐ 每层 = 注意力 + 前馈层,外加残差与归一化
│ │
└─ ⑤ 解码器 Decoder ×N ←┘ 每层 = 掩码注意力 + 交叉注意力 + 前馈层

└─ ⑥ 线性层 + Softmax → 一张「下一个标记是什么」的可能性清单

图说:①②③ 是入口,④⑤ 是主体,⑥ 是出口。
原论文里 N 取 6,也就是编码器六层、解码器六层。

原论文的配置就是这样:六层编码器 + 六层解码器2。 最后过一个线性层和一个 Softmax,就得到输出3

这一章按 ③ → ② → ④ 的顺序讲:先讲最容易被跳过的零件,再讲怎么拼。

主走查:全章跟着「我喜欢你」这三个字走

五个零件各讲各的,很容易读完只记住五句功能描述。所以这一章挑一句话,从头跟到尾:

输入就一句:「我喜欢你」。 隐藏层维度取 768(第 02 章定义过:每个标记从头到尾用多少个数表示), 前馈层中间拉宽到 3072

下面每个零件那一节的末尾,都有一小段**「我喜欢你」走到这一步**,写出它此刻的形状和几个具体的数。 全章走完,这三个字会经历下面这七步:

① 切开编号 「我喜欢你」→ 3 个编号
② 查表 3 个编号 → 3 行 × 768 个数
③ 加位置编码 同样是 3 × 768,但每一行被叠上了「你是第几个」的信息
④ 过注意力 3 × 768 → 3 × 768(形状不变,内容互相掺过了)
⑤ 残差加回 把 ④ 的输出和 ③ 的输入逐位相加
⑥ 过前馈层 每一行 768 → 3072 → 768
⑦ 再残差、再归一化 → 这一层结束,原样送进下一层

图说:形状从头到尾都是 3 × 768,只有第 ⑥ 步中间那一瞬拉宽到 3072。
这就是「一个标记从头穿到尾,始终被表示成同样多的一串数」的意思。

先声明:下面这些小段里的数值,凡不是书里给的,全是为演示编的,不是真实数值。 编的是数值,不是步骤——形状、维度和每一步做的事,和真实模型一模一样。

3. 这台机器是为什么任务造的:序列到序列

这一节回答:为什么它长成「两半」的样子。

什么是序列到序列

书里给的定义:输入是一个自然语言序列,输出是另一个可能不等长的自然语言序列4。 行话叫 Seq2Seq(sequence to sequence 的缩写,读作「序列到序列」)。

翻译是最典型的例子:输入「今天天气真好」,输出 Today is a good day.—— 六个汉字进去,五个英文词出来,长度不一样。

书里有一个很值得记住的推广:几乎所有的自然语言处理任务都可以看成序列到序列5:

任务怎么看成序列到序列
文本分类输出序列长度为 1(就一个类别)
词性标注输出序列和输入等长(每个词配一个词性)
机器翻译输入输出都不定长

这个视角在第 04 章会变成一个模型的核心主张(T5 的「大一统」),记着它。

编码和解码分别是什么

书里的说法很干净6:

编码 = 把输入的自然语言序列压成一组能表示意义的向量; 解码 = 拿这组向量,再一步步写出目标序列。

  • 编码器(Encoder) 干第一件事;
  • 解码器(Decoder) 干第二件事。

Transformer 一开始正是用在机器翻译上的7——这解释了它为什么天生是两半: 一半读源语言,一半写目标语言。

这一点后面会变成分岔口: 只想「读懂」的任务,解码器是多余的; 只想「往下写」的任务,编码器是多余的。第 04 章那三条路线,分岔就在这里。

4. 零件一:前馈神经网络——负责加工

这一节回答:注意力已经把信息混好了,为什么还要再加一层。

先看它在干什么

注意力做的是混合:把别的位置的信息按相关度搬过来。 但它自始至终没有对单个位置的内容做过任何加工——搬过来的还是搬过来的。

前馈神经网络就是干加工这件事的。

「前馈神经网络」(书里也写作 FFN,是 Feed Forward Neural Network 的缩写) 指的就是第 01 章那种全连接结构:每个单元和上一层的所有单元都连上, 而且信息只往前走、不回头8

具体怎么做

在 Transformer 里,它就三步9:

一个位置的向量(768 个数 —— 这就是第 02 章说的隐藏层维度)
└ 第一层线性变换 → 临时拉宽到 3072 维(这个数叫前馈层维度)
└ 过一个 ReLU → 把负数全部压成 0
└ 第二层线性变换 → 缩回 768 维
└ 过一个 dropout → 训练时随机丢掉一部分

图说:先拉宽再缩回。宽的那一段是它「思考」的空间。
进去和出来都是隐藏层维度,只有中间那一段是前馈层维度——这两个数不是一回事。

上面那张图里有三个新词,一个一个讲清。先讲前两个:

意思
线性变换就是乘一个矩阵再加一个数——把一组数按固定的比例重新混合成另一组数
ReLU一个极简的规则:正数原样留下,负数一律变成 0。它的作用是给模型引入「拐弯」的能力,否则不管堆多少层线性变换,合起来仍然等价于一层。像 ReLU 这样夹在两层线性变换中间、专门用来引入这种「拐弯」的规则,统称激活函数——后面遇到这个词,指的就是这一类零件。这正是第 01 章第 2 节那句「再做一次简单的掰弯」欠下的账

第三个是 dropout:训练时随机把一部分中间结果扔掉,逼模型不要死记硬背某几条路径。 书里说它的用途是「防止过拟合」——过拟合就是模型把训练数据背下来了,换一组没见过的数据就不会做10

为什么这样有效

关键在「每个位置各算各的」。 注意力负责跨位置搬运,前馈层负责单位置加工, 两件事分开,所以前馈层可以对所有位置同时算——并行度一点没减。

一个容易记的分工: 注意力回答「该看谁」,前馈层回答「看完之后怎么想」。

「我喜欢你」走到这一步(主走查第 ⑥ 步)

前面注意力已经把三行数掺完了,现在轮到前馈层。它对三行各算各的,互不干扰:

「我」那一行: 768 个数 → 拉宽成 3072 个 → 负数归零 → 缩回 768 个
「喜欢」那一行: 768 个数 → 3072 → 缩回 768 ← 和上一行同时算,不用等
「你」那一行: 768 个数 → 3072 → 缩回 768 ← 同上

拿「我」那一行的头三个数看得更具体:
进来 [ 0.55, 1.31, 0.15, … ]
拉宽后前三个 [ 0.92, -0.40, 1.77, … ] ← 一共 3072 个
过 ReLU [ 0.92, 0.00, 1.77, … ] ← 中间那个负数被压成了 0
缩回后 [ 0.34, 0.88, -0.06, … ] ← 又是 768 个

图说:三行同时算完,总耗时和算一行差不多——这就是「不损失并行度」的意思。
(这几个数是为演示编的,不是真实数值;768 和 3072 是原论文的配置。)

5. 零件二:层归一化——不让数值漂掉

这一节回答:为什么每一层前后都要多加一道看起来什么也没干的手续。

先看会出什么问题

深层网络里,每一层的输入都是上一层的输出。 训练过程中所有层的参数都在变,于是11:

越靠上的层,它拿到的输入分布(这些数大致落在什么范围、怎么散开)变化越大;而且这个变化会随着深度越积越大。

结果就是模型一直在追一个动来动去的目标,训练变得又慢又不稳。

归一化在做什么

归一化的核心目的:让不同层输入的取值范围和分布保持一致12

做法朴素得像小学算术:一组数减去它们的平均值,再除以它们的标准差—— 这样这组数的平均值就变成 0、离散程度变成 1,落回一个标准的形状。

「标准差」是什么: 一组数偏离平均值的平均幅度。 标准差大表示这组数散得开,小表示挤在一起。

除法的分母上还要加一个极小的数,只是为了避免除以 013

两种归一化,差别只在「跟谁比」

批就是:训练时不是一条一条喂数据,而是一次喂一小撮,这一小撮就叫一个批(英文 batch)。 批大了算得快,但占显存。

书里给了两种14:

批归一化(Batch Norm)层归一化(Layer Norm)
跟谁比同一批样本的同一个位置同一个样本自己的所有维度
需要什么一批里要有足够多的样本只要这一个样本
算式完全一样完全一样,只是统计的维度不同15

为什么文本这一行选了层归一化

书里列了批归一化的四条毛病,每一条都正好卡在文本任务上16:

毛病为什么对文本致命
显存不够时批很小,算出来的平均值和标准差不能代表全局长文本本来就占显存,批只能开小
不同句子的同一个位置,分布大概率不同第 3 个字在不同句子里可以是任何东西,拿它们互相比毫无意义
测试时需要用训练时存下来的统计量,但测试句子可能更长后面那些位置在训练时压根没统计过,没数可用
每一步都要保存和计算这批的统计量,耗时耗力纯粹的额外开销

层归一化一条都不占:它只看这一个样本自己,不关心批多大、不关心别的句子、不需要存任何东西。

这是一个值得记的模式:一个技术从图像搬到文本, 卡住它的往往不是「效果不好」,而是「文本的形状和图像不一样」。 批归一化在图像上非常成功——图像的尺寸是固定的,同一位置的像素之间真的可比。

一个容易漏掉的细节

书里点了一句:归一化之后还有两个可训练的参数,把这组数重新缩放和平移17

为什么要多此一举: 强行把每一层都压成标准形状,可能压掉了有用的信息。 留两个参数让模型自己决定「要不要压回去、压多少」——归一化是手段,不是目的。

「我喜欢你」走到这一步(主走查第 ⑦ 步)

归一化是对每一行自己的 768 个数做的。768 个数手算不动,用 8 个数演示同一套算术:

某一行(取 8 个数): [ 2, 4, 4, 4, 5, 5, 7, 9 ]
平均值 = (2+4+4+4+5+5+7+9) ÷ 8 = 40 ÷ 8 = 5
每个数减去 5: [-3, -1, -1, -1, 0, 0, 2, 4 ]
标准差 = 这 8 个差各自平方求平均再开方
= √((9+1+1+1+0+0+4+16) ÷ 8) = √4 = 2
每个数再除以 2: [-1.5, -0.5, -0.5, -0.5, 0, 0, 1, 2 ]

验一下:新的这 8 个数,平均值正好是 0,标准差正好是 1。

图说:「我」「喜欢」「你」三行各做一遍这套算术,各算各的、互不参照——
这就是「层归一化只看这一个样本自己」的全部含义。
换成批归一化,就要拿「我」这一位去和别的句子的第一个字比。
(这 8 个数是为演示编的,不是真实数值。)

6. 零件三:残差连接——给训练信号留一条直路

这一节回答:为什么几十层能堆得起来。

先看现象

书里给的动机是一句话:模型结构复杂、层数深,为了避免模型退化18

「模型退化」是什么: 层数加深之后,效果不升反降—— 不是过拟合,是训练本身就没做好,深的那个连训练集都学不好。

做法:把输入原样抄一份加到输出上

普通的一层: 输入 x → [ 这一层 ] → 输出 F(x)
带残差的一层: 输入 x → [ 这一层 ] → F(x)
└────────── 原样抄一份 ────────┴→ 输出 = x + F(x)

图说:多出来的那条斜线就是残差连接。它让信息有一条完全不经过这一层的通路。

书里的定义:下一层的输入不仅是上一层的输出,还包括上一层的输入19

为什么有效:两个角度

角度一(书里给的): 残差连接允许最底层信息直接传到最高层,让高层专注于残差的学习19

「残差」这个词就是从这来的: 这一层不必学「输出应该是什么」, 只要学「在输入的基础上要改动什么」——改动量就叫残差。 如果这一层什么也不该做,它只要输出 0 就行,比学一个恒等变换容易得多。

角度二(书里没讲,我们补): 训练时,调整参数的信号要从最上层一路传回最下层。 没有残差,这个信号每经过一层就要乘一次这层的参数;几十层连乘之后,它要么趋近 0、要么爆掉。 残差那条斜线给了信号一条「乘 1」的通路,几十层之后仍然传得回去。

在 Transformer 里的具体写法

编码器的每一个子层都套着这个结构20:

第一个子层: h = x + 注意力(归一化(x))
第二个子层: out = h + 前馈层(归一化(h))

图说:两句话就是一个编码器层的全部。
「归一化 → 子层 → 加回原输入」这三步,后面每个模型都在重复。

「我喜欢你」走到这一步(主走查第 ⑤ 步)

残差就是一次逐位加法,没有别的。 拿「我」那一行的头三个数看:

进这个子层时(x) [ 0.21, 0.97, 0.15, … ]
子层算出来的改动量 [ 0.34, 0.34, 0.00, … ]
加回去 = x + 改动量 [ 0.55, 1.31, 0.15, … ] ← 这就是第 ⑥ 步的输入

图说:第三位的改动量是 0.00 —— 这一层认为这一位不用改,原样放过去。
「学改动量」比「学输出该是什么」容易,就容易在这里:
什么都不做,只要输出 0 就行。
(这几个数是为演示编的,不是真实数值。)

7. 零件四:查表层——把编号变成向量

这一节回答:文字进模型的第一道门。

它是什么

一个可训练的查询表。 嵌入就是 Embedding 的中文直译,说的是「把一个词变成一串数」这件事。 书里的说法:Embedding 层是一个存储固定大小词典的嵌入向量查找表21

「Embedding」这个英文词在中文材料里有三种译法:嵌入、词向量、查表层—— 说的是同一样东西。本文一律叫「查表层」,它查出来的那串数叫「词向量」。

完整流程(主走查第 ①② 步)

「我喜欢你」
└ 分词器 tokenizer 切开并编号: 我→0 喜欢→1 你→2 ← 第 ① 步
└ 查表层拿编号去查表: ← 第 ② 步
0 → [ 0.21, -0.03, 0.15, 0.08, … ] (768 个数)
1 → [-0.11, 0.42, 0.07, -0.05, … ] (768 个数)
2 → [ 0.30, 0.05, -0.22, 0.11, … ] (768 个数)
└ 拼起来: 3 行 × 768 个数的矩阵

图说:分词器负责「切开并编号」,查表层负责「编号换成一串数」。两件事常被混为一谈。
(这几个数是为演示编的,不是真实数值;书里只给了形状,没给数。)

书里对分词器的定义:把自然语言输入切分成标记并转化成一个固定的编号22。 它可以切成词、切成子词、切成字符,而词表大小(词表里一共装了多少个片段)往往高达数万到数十万23—— 具体怎么切、怎么训出这张表,是第 07 章第 3 节的内容。

这张表本身是学出来的

表内部是一个 (词表大小 × 向量维度) 的可训练矩阵: 词表里每一个编号,对应一行24

注意这句话的分量: 第 01 章里,Word2Vec 那一代把词向量当成一个独立产出—— 先单独训好,再拿去用。到了这里,它只是模型的第一层,和整个模型一起训练。 书里没有明说这是范式的变化,但它确实是。

输入形状

书里给的形状是 (batch_size, seq_len, 1)25——一次处理几条、每条几个标记、每个标记一个编号。 过完查表层就变成 (batch_size, seq_len, 向量维度)

「seq_len」就是序列长度,也就是这句话有几个标记。 这个词后面每章都会用到。

8. 零件五:位置编码——把丢掉的语序补回来

这一节讲全章最容易被跳过、也是最有巧思的一个零件。

先看问题

上一章讲过,注意力的计算方式导致序列中相对位置的丢失26。 书里的例子最直白:

对序列中的每一个标记来说,其他各个位置对它都是平等的—— 「我喜欢你」和「你喜欢我」在注意力机制看来是完全相同的。27

循环网络没有这个问题,因为它本来就是一个词一个词按顺序读的,顺序天然写在计算过程里。 注意力用并行换掉了顺序,于是必须把顺序补回来。

做法:给每个位置算一串数,直接加到词向量上

Transformer 用的是三角函数里的正弦和余弦——它们把一个角度换算成 -1 到 1 之间的一个数; 书里管这套做法叫绝对位置编码 Sinusoidal28

规则: 位置编号进去,向量的偶数位用正弦算,奇数位用余弦算; 每一维用的「转速」不同——排在前面的那些维转得快、排在后面的转得慢29

位置 0 的编码:[ 0.00, 1.00, 0.00, 1.00 ]
位置 1 的编码:[ 0.84, 0.54, 0.10, 1.00 ]
位置 2 的编码:[ 0.91, -0.42, 0.20, 0.98 ]
位置 3 的编码:[ 0.14, -0.99, 0.30, 0.96 ]
└ 这四行直接加到对应位置的词向量上

图说:书里的实测输出。同一列上下看,是一条不同频率的波;
每个位置拿到的是一组「不同快慢的指针指到哪儿」的读数。

为什么这么绕,不直接写 1、2、3

书里给了两条理由30:

第一,它能算出训练时没见过的位置。 训练集里最长的句子只有 20 个词,来了一个 21 个词的句子怎么办? 用公式现算第 21 位就行——查表法就没辙,表里没有第 21 行。

第二,相对位置可以被算出来。 对任意固定的间距 k,第 pos+k 位的编码可以由第 pos 位的编码线性地算出来—— 依据是三角函数的和角公式(Sin(A+B) = Sin(A)Cos(B) + Cos(A)Sin(B))。

这一条才是关键。 模型真正需要的往往不是「这是第 17 个字」, 而是「这两个字隔了 3 位」。正余弦编码让「隔多远」这件事变成了一次线性运算。

「我喜欢你」走到这一步(主走查第 ③ 步)

做法就是逐位相加,一个数都不多。 拿上一节查出来的前两行、各取前四个数:

「我」在第 0 位:
查表查出来的 [ 0.21, -0.03, 0.15, 0.08 ]
+ 第 0 位的编码 [ 0.00, 1.00, 0.00, 1.00 ] ← 上面那张表的第一行
────────────────────────────────────────────
= 送进注意力的 [ 0.21, 0.97, 0.15, 1.08 ]

「喜欢」在第 1 位:
查表查出来的 [-0.11, 0.42, 0.07, -0.05 ]
+ 第 1 位的编码 [ 0.84, 0.54, 0.10, 1.00 ] ← 上面那张表的第二行
────────────────────────────────────────────
= 送进注意力的 [ 0.73, 0.96, 0.17, 0.95 ]

图说:两行加的是不同的东西,所以加完之后,
「同一个字排在第 0 位」和「排在第 1 位」送进注意力的数就不一样了。
「我喜欢你」和「你喜欢我」从这一步开始才分得开。
(词向量那两行是为演示编的;位置编码那两行是书里的实测输出。)

书里还给了一段推导,值得知道它在证什么

书里花了大约二十段做数学推导31你不需要跟着推,但值得知道它的骨架:

① 想要一种编码,使得「加了位置之后的两个向量做点积」能反映相对位置
② 把这个式子拆成一长串越来越小的项(这种拆法叫泰勒展开),
发现这一长串里只有第 6 项同时依赖两个位置
③ 于是问题变成:找一个函数,让第 6 项只跟两个位置的「差」有关
④ 换一套记数方式来解这个方程(把「两个数一组」当成一个数来算,数学上叫复数),
解出来正好是正余弦形式
⑤ 更高维就是若干个二维解拼起来

图说:这一段说明正余弦不是拍脑袋选的,是解一个方程解出来的。

但里面有一个数是拍脑袋的: 公式里那个 10000。 书里老老实实写了——这个取值是实验结果32。 选这个形式的理由是它能让两个位置离得越远、相关性越趋近于零。

判断(我们的,不是书里的): 这一段推导的价值不在结论,在它示范了一种读法—— 一条公式里,哪些部分是被推导逼出来的、哪些是试出来的,常常混在一起。 正余弦的形状是推出来的,底数 10000 是试出来的。 引用的时候把两者分开,才不会把经验值说成定理。 如果错,会错在: 如果后来有人给出了 10000 这个取值的理论依据(比如从波长与序列长度的关系推出), 那它就该从「试出来的」挪到「推出来的」那一栏。

这个零件在第 06 章会被换掉。 今天的模型基本都改用了旋转位置编码—— 不是加上去,而是把向量旋转一个角度。为什么换,第 06 章讲。

9. 拼起来:编码器与解码器差在哪

这一节把上面五个零件装成两种东西。

编码器层:两个子层

输入 x
├─ 归一化 → 多头自注意力(不加掩码)→ 加回 x
└─ 归一化 → 前馈层 → 加回上一步

图说:编码器不需要掩码——它是来「读懂」的,读的时候当然可以看到整句话。

书里的代码里这一点写得很明白:编码器传入的参数是「不要因果掩码」33。 N 个这样的层叠起来就是编码器,最后再加一个层归一化收尾34

解码器层:三个子层

书里说得很清楚:解码器由两个注意力层和一个前馈神经网络组成35:

第几个子层是什么它在干什么
1掩码自注意力只看自己已经写出来的部分,未来的位置被盖住
2交叉注意力Query 来自上一个子层的输出,Key 和 Value 来自编码器的输出
3前馈层加工

第二个子层是整台机器的枢纽,值得停一下:

编码器读完源语言 ──→ 一组表示 ┐
├→ 交叉注意力:「我现在要写下一个词,
解码器已经写出的部分 ──→ Query ┘ 该回头看源语言的哪几个部分?」

图说:Query 问「我要什么」,Key/Value 提供「源语言里有什么」。
翻译时的对齐关系,就是在这一步被算出来的。

一张对照表

编码器解码器
注意力层数12
看不看得到未来看得到(整句一起看)看不到(掩码盖住)
信息从哪来只有输入本身输入 + 编码器的输出
擅长什么理解生成

这张表就是第 04 章的地图。 三条技术路线的差别,全在这两列里怎么取舍。

主走查收尾:「我喜欢你」在一层里的完整账

把散在各节的那几小段接起来,补上第 ④ 步,就是完整的一遍:

① 「我喜欢你」→ 编号 0, 1, 2 (第 7 节)
② 查表 → 3 行 × 768 个数,「我」那行头四位 [0.21,-0.03,0.15,0.08] (第 7 节)
③ 加位置编码 → 「我」那行变成 [0.21, 0.97, 0.15, 1.08] (第 8 节)
④ 过注意力 → 形状仍是 3 × 768。处理「你」这一位时,给三个位置的比例
比如 0.18 / 0.52 / 0.30 —— 「喜欢」那份占了一半多
(怎么算出这三个数,第 02 章第 7 节走过一遍)
⑤ 残差加回 → 「我」那行 [0.21,0.97,0.15,…] + 改动量 [0.34,0.34,0.00,…]
= [0.55, 1.31, 0.15, …] (第 6 节)
⑥ 过前馈层 → 每行 768 → 3072 → 768,「我」那行出来是 [0.34, 0.88, -0.06, …] (第 4 节)
⑦ 再残差、再归一化 → 每行自己的 768 个数被压成平均值 0、标准差 1 (第 5 节)
└ 出来还是 3 行 × 768,原样送进下一层,一共这样过 6 次

图说:五个零件在这条线上各占一步——查表 ②、位置编码 ③、残差 ⑤、前馈 ⑥、归一化 ⑦。
第 ④ 步是上一章的零件,这一章只是把它接进来。
(0.18 / 0.52 / 0.30 同样是为演示编的,不是真实数值。)

10. 一处对不上的地方:图和代码不一致

这一节讲一个书里主动坦白的细节,它比看起来重要。

归一化到底放在注意力之前还是之后?

书里说36:

原论文的配图把层归一化放在注意力层后面(Post-Norm), 但它发布的源代码里,层归一化是放在注意力层前面的(Pre-Norm)。

损失就是训练时用来衡量「模型答得离正确答案有多远」的那个数;它稳定,就是训练过程不容易突然崩掉。

书里的选择是跟着源代码走,用 Pre-Norm,理由是「目前大模型一般采用 Pre-Norm 结构,可以使损失更稳定」。

这条差别有多大

补充(不在书里):2020 年有一篇论文专门研究了这件事。先说清一个词。

训练时每一步要把参数朝「答得更准」的方向挪一点,挪多大就叫学习率。 挪得太大会一步跨过好答案,太小则半天到不了;所以它是训练里最要紧的一个设定。

而训练刚开始时参数还全是随机的,一上来就迈大步特别容易把模型带偏。 所以有一种做法是:头几百步故意把步子迈得极小,再慢慢加大。这叫预热。

那篇论文的结论是:Post-Norm 结构在初始化时梯度不稳定,所以必须配一段学习率预热才能训起来; 而 Pre-Norm 结构的梯度表现良好,可以直接去掉预热阶段而不损失效果37

预热要预热多少步、从多小开始,全靠人一次次试出来,是深度学习里很麻烦的一件事—— 能去掉它,是实打实的省事。

判断(我们的,不是书里的): 这条「图和代码不一致」值得单独记住, 因为它示范了一件在这一行反复发生的事—— 论文里画的图,和真正被跑通的代码,可以不是同一个东西;而后人抄的往往是图。 Post-Norm 之所以在很多复现里出现「训不起来」,根子就在这。 遇到一个结构上的细节争议,先去看原始实现,别只看论文配图。 如果错,会错在: 如果某个项目的图和代码是严格对齐的,这条怀疑就是多余的成本; 判据是看有没有第三方复现报告过「照图做训不起来」。

11. 作者的判断与证据

这一节把「书里给了理由的」和「书里只是这么做的」分开。

说法书里给了什么该怎么看
层归一化比批归一化更适合文本给了四条具体机制,每条都能对上文本任务的特点可以采信,这是机制论证不是经验观察
残差连接能避免模型退化只给了结论和一句解释(让高层专注于残差)结论是学界共识,但书里没给证据也没给出处
正余弦编码的两个好处给了推导(和角公式)和完整数学推演第二条(相对位置可线性算出)论证扎实
底数取 10000明说是实验结果这是书里少见的坦白,值得肯定
Pre-Norm 让损失更稳定只给了结论,一句带过结论正确,但书没给出处;见上一节我们补的
几乎所有任务都可以看成序列到序列给了两个例子(分类、词性标注)这是一个有力的视角,不是被证明的定理

判断(我们的,不是书里的): 这一章有一个隐藏的组织逻辑,书里没点破: 五个零件里,只有注意力是「为了做得更好」,其余四个都是「为了训得起来」。 前馈层管加工,是能力;残差、归一化、位置编码,全是在补注意力带来的副作用。 这个比例很说明问题——深度学习里的绝大多数结构创新,不是让模型更聪明,是让训练不崩。 如果错,会错在: 如果某个零件其实显著提升了表达能力而不只是稳定性 (有人认为前馈层承担了模型的大部分知识存储),那把它归到「加工」这一栏就低估了它。

12. 边界与局限

① 书里没讲的:这台机器有多贵

第 02 章提过,自注意力的计算量随序列长度平方增长。 这一章讲的整机,把这个代价乘上了层数:六层编码器加六层解码器,就是十二次。

书里从头到尾没有给出任何计算量或显存的估算。

后面第 06 章讲的键值共享省的是另一笔账——生成时那块存键和值的地方, 它随长度线性增长,和这条平方关系不是一回事;两笔账第 06 章第 5 节拆开算。

至于第 05 章讲的「预训练要上千张卡」,算的是参数量乘语料量,和序列长度无关。 这三处别混成一条线。

② 代码块里有三处对不上

这本书的代码是从可运行工程里摘出来的讲解片段,摘的时候丢了东西。这一章有三处:

位置对不上什么
造前馈层这个部件时,按代码的写法得分别递给它三个数(向量宽度、中间放宽到多少、丢弃比例),但编码器那一段是把一整包设置整个扔过去照书里的代码原样跑会报参数错误38
整机真正开算的那一段里,解码器拿到的输入和编码器是同一份真正的序列到序列应该给解码器喂目标序列;这样写等于让它看着源语言去解码源语言39
同一段里还散着五条调试用的打印语句说明这段是边写边调的过程稿,不是成品

判断(我们的,不是书里的): 这些不该被当成「书写错了」而否定这本书。 教程型材料的代码块,正确的读法是当伪代码读——它的任务是让你看懂结构,不是让你复制运行。 真要跑,去它的配套仓库拿完整脚本。 如果错,会错在: 如果配套仓库里的对应文件也有同样的问题,那就不是「摘漏了」而是真的有 bug—— 我们核对的是书,没有核对仓库。

③ 这一章的时间坐标是 2017 年

这一章讲的是原版 Transformer。 今天真正在用的解码器,五个零件里换掉了三个:

零件2017 年原版今天
归一化层归一化只做缩放的简化版(RMSNorm)
位置编码正余弦相加旋转位置编码
前馈层两层线性 + ReLU三层线性 + 门控

这三处替换是第 06 章的全部内容。 这一章打的是地基,不是现状。

④ 书里没有训练这台机器

书里明说了:限于篇幅聚焦在大模型,这一章不再讲怎么训练 Transformer40; 真正的手把手训练留到了第五章的 LLaMA。 所以这一章你拿到的是「一台装好的机器」,还没有见过它跑起来的样子。

13. 可带走的

  1. 注意力只解决「谁和谁相关」,其余四个零件都是在堵它带来的洞;
  2. 前馈层负责加工:先拉宽再缩回,中间过一次 ReLU;每个位置各算各的,所以不损失并行度;
  3. ReLU = 正数留下、负数归零;没有它,堆多少层线性变换都等价于一层;
  4. 归一化 = 减平均值、除标准差,目的是让每一层拿到的输入形状稳定;
  5. 文本用层归一化不用批归一化,因为批归一化的四条毛病全部卡在「句子不等长、批开不大」上;
  6. 残差连接 = 把输入原样加到输出上;它让训练信号有一条「乘 1」的通路,几十层才堆得起来;
  7. 「残差」的意思是「在输入基础上的改动量」——这一层不必学输出是什么,只学要改什么;
  8. 查表层是一个可训练的 (词表大小 × 维度) 矩阵;从这一代起,词向量不再单独交付,而是模型的第一层;
  9. 位置编码是补丁不是装饰——注意力用并行换掉了顺序,顺序必须另外补回来;
  10. 正余弦编码的两个好处:能外推(就是训练时没见过这么长,用的时候照样算得出来)到更长的输入;相对位置可以线性算出来;
  11. 底数 10000 是试出来的,书里自己说是实验结果;
  12. 编码器 = 注意力 + 前馈,看得到整句;解码器多一层交叉注意力,看不到未来;
  13. 交叉注意力是两半之间的枢纽:Query 来自解码器,Key/Value 来自编码器;
  14. 原论文的图是 Post-Norm,它自己的代码是 Pre-Norm,今天大家跟的是代码那一版;
  15. 五个零件里只有一个是为了「更聪明」,四个是为了「训得起来」。

14. 原文地图

主题原书章原文位置
Transformer 由哪两个组件构成第二章 Transformer 架构text/06-ch02-transformer.txt:308(搜「Encoder 的 BERT」) · text/06-ch02-transformer.txt:329(搜「6个 Encoder」)
序列到序列与三个例子第二章 Transformer 架构text/06-ch02-transformer.txt:314(搜「Seq2Seq,即序列到序列」) · text/06-ch02-transformer.txt:316(搜「输出与输」)
编码与解码的定义第二章 Transformer 架构text/06-ch02-transformer.txt:322(搜「编码,就是将输」) · text/06-ch02-transformer.txt:320(搜「机器翻译任务上的」)
前馈神经网络第二章 Transformer 架构text/06-ch02-transformer.txt:336(搜「Feed Forward Neural Network」) · text/06-ch02-transformer.txt:356(搜「RELU 激活函数」)
归一化的目的与两种做法第二章 Transformer 架构text/06-ch02-transformer.txt:362(搜「取值范围或者分布」) · text/06-ch02-transformer.txt:366(搜「mini-batch 上进」) · text/06-ch02-transformer.txt:389(搜「每个样本上计算」)
批归一化的四条毛病第二章 Transformer 架构text/06-ch02-transformer.txt:379(搜「显存有限」) · text/06-ch02-transformer.txt:381(搜「不同句」) · text/06-ch02-transformer.txt:383(搜「测试集可能出现」) · text/06-ch02-transformer.txt:386(搜「耗时」)
残差连接第二章 Transformer 架构text/06-ch02-transformer.txt:413(搜「避免模型退化」) · text/06-ch02-transformer.txt:414(搜「残差的学习」)
编码器与解码器的结构差别第二章 Transformer 架构text/06-ch02-transformer.txt:438(搜「Encoder 不需要掩码」) · text/06-ch02-transformer.txt:452(搜「加⼊⼀个 Layer Norm」) · text/06-ch02-transformer.txt:471(搜「Decoder 由两个注意」) · text/06-ch02-transformer.txt:473(搜「Encoder 的输出作为 key」)
查表层与分词器第二章 Transformer 架构text/06-ch02-transformer.txt:530(搜「Embedding 层其实是⼀个存储」) · text/06-ch02-transformer.txt:531(搜「index」) · text/06-ch02-transformer.txt:542(搜「词表⼤⼩则往」) · text/06-ch02-transformer.txt:552(搜「可训练的」)
注意力丢掉了语序第二章 Transformer 架构text/06-ch02-transformer.txt:560(搜「相对位置的丢失」) · text/06-ch02-transformer.txt:563(搜「都是平等的」)
正余弦位置编码与两个好处第二章 Transformer 架构text/06-ch02-transformer.txt:568(搜「Sinusoidal」) · text/06-ch02-transformer.txt:601(搜「适应⽐训练集」) · text/06-ch02-transformer.txt:604(搜「PE(pos+k)」)
数学推导与底数 10000第二章 Transformer 架构text/06-ch02-transformer.txt:613(搜「第6项」) · text/06-ch02-transformer.txt:622(搜「复数的指数形式」) · text/06-ch02-transformer.txt:625(搜「base 取为」)
Post-Norm 与 Pre-Norm 的不一致第二章 Transformer 架构text/06-ch02-transformer.txt:672(搜「Post-Norm」) · text/06-ch02-transformer.txt:673(搜「Pre Norm」)
整机流程与输出层第二章 Transformer 架构text/06-ch02-transformer.txt:677(搜「N 取为6」) · text/06-ch02-transformer.txt:753(搜「交叉熵」)
代码里的三处对不上第二章 Transformer 架构text/06-ch02-transformer.txt:341(搜「hidden_dim: int, dropout: float」) · text/06-ch02-transformer.txt:441(搜「self.feed_forward = MLP(args)」) · text/06-ch02-transformer.txt:746(搜「decoder(x, enc_out)」)

Footnotes

  1. 出处:「第二章 Transformer 架构」第 305 段(text/06-ch02-transformer.txt:305,搜「Attention is All You Need」)。原文的说法是:作者「通过仅使用注意力机制而抛弃传统的 RNN、CNN 架构搭建出 Transformer 模型,从而带来了 NLP 领域的大变革」。这篇论文的核对见第 01 章脚注。

  2. 出处:「第二章 Transformer 架构」第 329 段(text/06-ch02-transformer.txt:329,搜「6个 Encoder」)。原文:每一个编码器(解码器)又由 6 个编码器(解码器)层组成;输入源序列进入编码器编码,到最顶层再把结果输出给解码器的每一层。

  3. 出处:「第二章 Transformer 架构」第 677 段(text/06-ch02-transformer.txt:677,搜「N 取为6」)。原文同时点明训练时用交叉熵计算损失(text/06-ch02-transformer.txt:753,搜「交叉熵」)。补充(不在书里,来自通用知识):交叉熵是衡量「模型给出的可能性清单」和「正确答案」之间差距的一种算法,书里用了这个词但没有解释。

  4. 出处:「第二章 Transformer 架构」第 314 段(text/06-ch02-transformer.txt:314,搜「Seq2Seq,即序列到序列」)。原文强调输出序列「可能不等长」。

  5. 出处:「第二章 Transformer 架构」第 316 段(text/06-ch02-transformer.txt:316,搜「输出与输」)。原文给的两个例子:文本分类可视为输出长度为 1 的目标序列,词性标注可视为输出与输入等长的目标序列。

  6. 出处:「第二章 Transformer 架构」第 322 段(text/06-ch02-transformer.txt:322,搜「编码,就是将输」)。原文把编码结果形容成「可以简单理解为更复杂的词向量表示」。

  7. 出处:「第二章 Transformer 架构」第 320 段(text/06-ch02-transformer.txt:320,搜「机器翻译任务上的」)。

  8. 出处:「第二章 Transformer 架构」第 336 段(text/06-ch02-transformer.txt:336,搜「Feed Forward Neural Network」)。注意书里在第 8 段(text/06-ch02-transformer.txt:8,搜「全连接神经」)用的缩写是 FNN,到这一节改成了 FFN,指的是同一样东西。

  9. 出处:「第二章 Transformer 架构」第 356 段(text/06-ch02-transformer.txt:356,搜「RELU 激活函数」)。原文:Transformer 的前馈神经网络由两个线性层中间加一个 ReLU 激活函数组成,并加入了一个 dropout 层。「拉宽到 3072 维再缩回」这个具体倍数不在这一段,是原论文的配置;书里只写了「从输入维度到隐藏维度」再「从隐藏维度到输入维度」。

  10. 出处:「第二章 Transformer 架构」第 356 段(text/06-ch02-transformer.txt:356,搜「防⽌过拟合」)。补充(不在书里,来自通用知识):过拟合指模型把训练数据的细节甚至噪声都记住了,换一批没见过的数据就表现变差;dropout 通过训练时随机丢弃部分中间结果来抑制这种记忆。

  11. 出处:「第二章 Transformer 架构」第 364 段(text/06-ch02-transformer.txt:364,搜「随着⽹络深度的增⼤⽽增⼤」)。原文:随着参数更新,各层的输出分布不相同,且差异会随着网络深度增大而增大;而需要预测的条件分布始终相同,从而造成预测误差。

  12. 出处:「第二章 Transformer 架构」第 362 段(text/06-ch02-transformer.txt:362,搜「取值范围或者分布」)。

  13. 出处:「第二章 Transformer 架构」第 375 段(text/06-ch02-transformer.txt:375,搜「避免分」)。原文:加上一个极小量是为了避免分母为 0。

  14. 出处:「第二章 Transformer 架构」第 359 段(text/06-ch02-transformer.txt:359,搜「Batch Norm」)与第 366 段(text/06-ch02-transformer.txt:366,搜「mini-batch 上进」)。补充(不在书里):批归一化的原始出处是《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》,作者 Sergey Ioffe 与 Christian Szegedy,首次公开于 2015 年 2 月 11 日,摘要里报告用它可以少用约 14 倍的训练步数达到同样精度。来源:https://arxiv.org/abs/1502.03167(查阅于 2026-08-25)。

  15. 出处:「第二章 Transformer 架构」第 389 段(text/06-ch02-transformer.txt:389,搜「每个样本上计算」)。原文:层归一化的归一化方式和批归一化完全一样,只是统计量的维度不同。补充(不在书里):层归一化的原始出处是《Layer Normalization》,作者 Jimmy Lei Ba、Jamie Ryan Kiros、Geoffrey E. Hinton,首次公开于 2016 年 7 月 21 日。摘要里给出的动机和书里一致:批归一化「依赖于小批量的大小,而且不清楚该怎么用到循环神经网络上」;层归一化则「在训练和测试时执行完全相同的计算」。来源:https://arxiv.org/abs/1607.06450(查阅于 2026-08-25)。

  16. 出处:「第二章 Transformer 架构」第 379 段(text/06-ch02-transformer.txt:379,搜「显存有限」)、第 381 段(text/06-ch02-transformer.txt:381,搜「不同句」)、第 383 段(text/06-ch02-transformer.txt:383,搜「测试集可能出现」)、第 386 段(text/06-ch02-transformer.txt:386,搜「耗时」)。

  17. 出处:「第二章 Transformer 架构」第 410 段(text/06-ch02-transformer.txt:410,搜「两个线性矩阵进⾏映射」)。书里只说了「有两个线性矩阵进行映射」,没有解释为什么;「让模型自己决定压不压回去」这层解释是我们补的(补充,不在书里,来自通用知识)。

  18. 出处:「第二章 Transformer 架构」第 413 段(text/06-ch02-transformer.txt:413,搜「避免模型退化」)。补充(不在书里):残差连接的原始出处是《Deep Residual Learning for Image Recognition》,作者 Kaiming He、Xiangyu Zhang、Shaoqing Ren、Jian Sun,首次公开于 2015 年 12 月 10 日。摘要的开场就是书里那句动机的完整版:「更深的神经网络更难训练」;论文把网络堆到 152 层,在 ImageNet 上取得 3.57% 的错误率。来源:https://arxiv.org/abs/1512.03385(查阅于 2026-08-25)。

  19. 出处:「第二章 Transformer 架构」第 414 段(text/06-ch02-transformer.txt:414,搜「残差的学习」)。原文:「残差连接允许最底层信息直接传到最高层,让高层专注于残差的学习」。 2

  20. 出处:「第二章 Transformer 架构」第 416 段(text/06-ch02-transformer.txt:416,搜「多头⾃注意⼒层的同时」)与代码第 422 段(text/06-ch02-transformer.txt:422,搜「self.attention.forward」)。

  21. 出处:「第二章 Transformer 架构」第 530 段(text/06-ch02-transformer.txt:530,搜「Embedding 层其实是⼀个存储」)。

  22. 出处:「第二章 Transformer 架构」第 531 段(text/06-ch02-transformer.txt:531,搜「index」)。

  23. 出处:「第二章 Transformer 架构」第 542 段(text/06-ch02-transformer.txt:542,搜「词表⼤⼩则往」)。原文明说此处不赘述分词器细节,留到后文。

  24. 出处:「第二章 Transformer 架构」第 552 段(text/06-ch02-transformer.txt:552,搜「可训练的」)。

  25. 出处:「第二章 Transformer 架构」第 545 段(text/06-ch02-transformer.txt:545,搜「batch_size,seq_len,1」)。

  26. 出处:「第二章 Transformer 架构」第 560 段(text/06-ch02-transformer.txt:560,搜「相对位置的丢失」)。

  27. 出处:「第二章 Transformer 架构」第 563 段(text/06-ch02-transformer.txt:563,搜「都是平等的」)与第 565 段(text/06-ch02-transformer.txt:565,搜「位置编码机制」)。

  28. 出处:「第二章 Transformer 架构」第 568 段(text/06-ch02-transformer.txt:568,搜「Sinusoidal」)。

  29. 出处:「第二章 Transformer 架构」第 570 段(text/06-ch02-transformer.txt:570,搜「奇数位置」)。示意数值出自书里的实测输出(text/06-ch02-transformer.txt:595,搜「0.84147098」),那是把序列长度取 4、维度取 4、底数取 100 跑出来的结果——注意这里底数用的是 100 而不是正式配置里的 10000,只为示例好读。

  30. 出处:「第二章 Transformer 架构」第 601 段(text/06-ch02-transformer.txt:601,搜「适应⽐训练集」)与第 604 段(text/06-ch02-transformer.txt:604,搜「PE(pos+k)」)。

  31. 出处:「第二章 Transformer 架构」第 613 段(text/06-ch02-transformer.txt:613,搜「第6项」)与第 622 段(text/06-ch02-transformer.txt:622,搜「复数的指数形式」)。推导的收尾在第 629 至 633 段(text/06-ch02-transformer.txt:631,搜「积化和差」),处理的是投影矩阵不是单位矩阵的情形。

  32. 出处:「第二章 Transformer 架构」第 625 段(text/06-ch02-transformer.txt:625,搜「base 取为」)。原文:该形式「可以使得随着 |m−n| 的增大,⟨pm,pn⟩ 有着趋于零的趋势」,而「base 取为 10000 是实验结果」。

  33. 出处:「第二章 Transformer 架构」第 438 段(text/06-ch02-transformer.txt:438,搜「Encoder 不需要掩码」)。

  34. 出处:「第二章 Transformer 架构」第 452 段(text/06-ch02-transformer.txt:452,搜「加⼊⼀个 Layer Norm」)。

  35. 出处:「第二章 Transformer 架构」第 471 段(text/06-ch02-transformer.txt:471,搜「Decoder 由两个注意」)与第 473 段(text/06-ch02-transformer.txt:473,搜「Encoder 的输出作为 key」)。原文:第二个注意力层用第一个注意力层的输出作为 Query,用编码器的输出作为 Key 和 Value。

  36. 出处:「第二章 Transformer 架构」第 672 段(text/06-ch02-transformer.txt:672,搜「Post-Norm」)与第 673 段(text/06-ch02-transformer.txt:673,搜「Pre Norm」)。

  37. 补充(不在书里):《On Layer Normalization in the Transformer Architecture》,第一作者 Ruibin Xiong,首次公开于 2020 年 2 月 12 日。论文的结论是:Post-LN 结构在初始化时梯度不稳定,因此需要学习率预热;Pre-LN 结构的梯度表现良好,可以去掉预热阶段而不损失效果。来源:https://arxiv.org/abs/2002.04745(查阅于 2026-08-25)。

  38. 出处:「第二章 Transformer 架构」第 341 段(text/06-ch02-transformer.txt:341,搜「hidden_dim: int, dropout: float」)与第 441 段(text/06-ch02-transformer.txt:441,搜「self.feed_forward = MLP(args)」)。前者的构造函数要三个具体参数,后者只传了一个配置对象。

  39. 出处:「第二章 Transformer 架构」第 746 段(text/06-ch02-transformer.txt:746,搜「decoder(x, enc_out)」)。同一个 x 既进了编码器也进了解码器;调试用的打印语句见第 733 段(text/06-ch02-transformer.txt:733,搜「print(」)。

  40. 出处:「第二章 Transformer 架构」第 773 段(text/06-ch02-transformer.txt:773,搜「从零“⼿搓”」)与第 774 段(text/06-ch02-transformer.txt:774,搜「Tiny LLaMA」)。