Lecture 3 · 模型架构与训练目标:从 BERT、GPT 到 T5 与 UL2 · Model Architectures & Training Objectives

Advances in Foundation Models · Lecture 03。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应 Maps to:Stanford CS324 Winter 2023 Advances in Foundation Models,Syllabus 主题 “Model Architectures and Training Objectives for FMs” 的阅读:CS324 Winter 2022 课程笔记 Modeling 与 Training、Vaswani et al. Attention Is All You Need(2017)、Clark et al. ELECTRA(2020)、Tay et al. Unifying Language Learning Paradigms(UL2,2022)、Raffel et al. Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer(T5,2019)。补充阅读:W22 笔记 Selective Architectures(混合专家与检索)、Devlin et al. BERT(2018)、Lewis et al. BART(2019)、Wang et al. What Language Model Architecture and Pretraining Objective Work Best for Zero-Shot Generalization?(2022)。 依据说明:Winter 2023 的日历上没有专门讲这个主题的讲座,它是阅读清单里的一个主题;课程网站也没有 slides 或录像。本讲依据上面这些阅读材料来写:分词、上下文嵌入和 Transformer 的部分沿着 W22 笔记 Modeling 展开,三种架构的训练目标沿着 W22 笔记 Training 展开,具体的数字引自各篇论文(文中注明出处);§3 把第 1 讲的 tiny_gpt.py 逐块拆开,是本讲义自己的内容;最后一节”2023 年之后”是我补充的。 配套代码 Code:code/lecture03/(PyTorch + numpy;5 个脚本 + 1 个共享模块 arch_tools.py。代码 01 只用标准库、约 2 秒;02、03 几秒;04、05 第一次运行各约 80 秒(训练并缓存模型),之后几秒) 预计阅读时间:3.5 小时(含运行代码)


0. 本讲导航 · How to read this lecture

0.1 学习目标 Learning goals

学完这一讲,你应该能够:

  1. 说清楚分词器(tokenizer)在做什么、为什么不能按空格切;手算一个 BPE 的例子;解释 unigram 分词为什么是一个”概率模型”;说出第 1 讲用字符级分词的好处和代价。
  2. 用”上下文嵌入”(contextual embedding)这个抽象概括 encoder、decoder、encoder-decoder 三种架构各自做什么。
  3. 不看代码地画出第 1 讲 base model 的数据流:每一步的张量形状、注意力里的 q / k / v、为什么除以 d\sqrt{d}、因果掩码、多头、残差流、pre-LayerNorm、MLP、weight tying;并能按公式数出它的 109,056 个参数、用 12Ld212Ld^2 估算 GPT-3 的参数量。
  4. 写出三种架构家族的训练目标:GPT 的因果语言建模、BERT 的 masked LM(15%、80/10/10、NSP)、T5 的 span corruption、BART 的去噪;以及介于中间的 prefix LM。
  5. 解释 ELECTRA 的替换 token 检测为什么被认为更省样本,以及它成立需要什么前提(代码 05 给了一个反例)。
  6. 说出 UL2 的混合去噪器(R / S / X)和模式 token,以及它”把架构和目标分开”的观点。
  7. 用代码 04 的实验和文献结论解释:为什么 decoder-only 最终成了默认选择,而 encoder 在哪些地方仍然不可替代。
  8. 简要说明混合专家(MoE)和检索增强这两种”选择性”架构的动机,以及 2023 年之后的架构演进(RoPE、SwiGLU、RMSNorm、GQA、MoE 的回归)。

0.2 先修与本讲范围 Prerequisites & scope

默认你已经会 Assumed本讲只给直觉,后面几讲再展开 Previewed only
第 1 讲:语言模型 = 序列上的分布、链式法则(§2.2)、bit/字符(§2.4)、预训练与探针(§3.1、§3.4)规模定律:参数、数据、算力怎样决定损失(第 4 讲)
矩阵乘法、softmax、交叉熵微调、LoRA 等适配方法的细节(第 6 讲)
PyTorch 基础(第 1 讲 tiny_gpt.py 开头的”PyTorch 小知识”)注意力的 GPU 实现与 FlashAttention(第 8 讲);MoE 的分布式训练(第 9 讲)
注意力头的机制可解释性(第 12 讲)

本讲是”往里看一层”:第 1 讲从外面看基础模型能做什么,本讲打开它,看它由什么组成、用什么目标训练,以及不同的选择带来什么不同。如果你学过本目录下的 CS336,它的第 3–4 讲把现代架构的细节(归一化、位置编码、注意力变体、MoE)讲得更深;本讲更侧重”为什么”和 2018–2022 年这段架构之争。

0.3 本讲地图 Map

§1  分词:按空格切的问题;BPE;unigram;字符级的代价                           ← 代码 01
§2  上下文嵌入:三种架构的共同抽象;为什么是 Transformer
§3  打开第 1 讲的黑盒:嵌入、注意力、√d、因果掩码、多头、残差流、              ← 代码 02
    LayerNorm、MLP、输出层;数参数;手工前向传播;消融
§4  三种架构家族与训练目标:GPT、BERT、T5 / BART、prefix LM;训练信号的密度     ← 代码 03
§5  同样的算力,谁学得快、谁能做什么;ELECTRA(以及它在迷你世界里为什么没奏效)  ← 代码 04、05
§6  UL2:混合去噪器与模式切换
§7  为什么 decoder-only 成了默认选择
§8  选择性架构:混合专家与检索
§9  2023 年之后
§10–14  小结、常见误区、练习、延伸阅读、下一讲预告

0.4 记号 Notation

全课统一的记号见第 1 讲 §0.4。本讲新增:

符号含义本讲代码里的值
dd(dmodeld_{\text{model}})每个位置的向量维度(模型宽度)64
LL层数(Transformer 块的个数);第 1 讲里 LL 也表示序列长度,本讲序列长度写成 TT2
TT序列长度 / 上下文长度(block size)base model 96 个字符;代码 04、05 是 32 个词
HH,dh=d/Hd_h = d/H注意力头数;每个头的维度4 个头,每头 16 维
WQ,WK,WV,WOW_Q, W_K, W_V, W_O注意力里的查询、键、值、输出投影矩阵代码里 qkv、proj
MM注意力掩码:Mij=0M_{ij} = 0 表示位置 ii 可以看位置 jj,−∞-\infty 表示看不到代码里用布尔值,True = 可以看
ϕ(x1:T)\phi(x_{1:T})上下文嵌入:把 token 序列映成同样长度的向量序列
[M]、[S]、<0>本讲词级分词器的特殊 token:mask、分隔 / 开始符、T5 式哨兵
μ\mu,rrUL2 / T5 里被挖掉的片段的平均长度、被挖掉的比例T5 基线:rr = 15%
λ\lambdaELECTRA 判别器损失的权重50

0.5 关于数字 · About the numbers

讲义里的实验数字都来自配套代码的实际输出(固定种子、PyTorch 单线程,重复运行逐字相同;删掉缓存重训,得到的模型和输出也完全相同,只有计时不同)。代码 02 读取第 1 讲缓存的 base model(不修改它);代码 04、05 用迷你互联网里的评论训练自己的小模型,缓存在 code/lecture03/checkpoints/。这些模型只有 10 万量级的参数,训练只有几十秒,所以它们演示的是机制:哪个目标给的训练信号多、哪种掩码能做什么。真实模型的量级(BERT、T5、ELECTRA、UL2 在基准上的分数)引自原论文,会注明出处。

本讲有好几处实验结果和我写代码前的预想不一样——最明显的是 T5 在这个算力下一直停在”按先验猜”、ELECTRA 没有学起来。讲义按实际结果来写,并解释原因;这些情况都列在代码 README 的”关于实验结果”里。其中一条是我自己的评测错误:最初的版本让 T5 自由生成、要求整段完全匹配,而别的模型只在几个候选里挑一个,于是 T5 被记成了全 0;改成同一种协议后,T5 的分数是”永远猜 ok”的水平(§5.2)。


1. 分词 · Tokenization

语言模型是 token 序列上的概率分布(第 1 讲 §2.1)。可是拿到手的数据是一个字符串。谁来决定”token”是什么?这就是分词器(tokenizer)的工作。它看起来像预处理的小事,其实影响很大:它决定了序列有多长(于是决定了计算量和上下文能装多少内容)、词表有多大、模型能不能处理没见过的词,甚至影响模型会不会算术。

1.1 为什么不能按空格切

最朴素的办法是按空格切。CS324 笔记列了它的几个问题,代码 01 演示 1 把它们打印了出来:

  • 中文不用空格:“我今天去了商店。” 按空格切还是一整块。
  • 德语有很长的复合词;英语有 father-in-law(和 mother-in-law 没有任何共享的部分)、don’t。
  • 标点会粘在词上。

在迷你互联网上,三种粒度的对比是这样的(代码 01 演示 1):

粒度词表大小token 总数每行 token验证集里没见过的 token
字符(第 1 讲)44262,98637.50.00%
按空格切1,87454,7057.83.05%
词 + 标点35076,52410.90.00%

按空格切时,“varno.” 和 “varno” 是两个不同的 token,一整道加法题 “11+21=32.” 是一个 token——验证集里没见过的 78 个 token 全都是这样的加法题,模型对它们一无所知。这就是开放词表(open vocabulary)问题:任何”一个词一个 token”的方案,都会遇到训练时没见过的词(新名字、新数字、拼写错误),只能把它们变成 [UNK]。字符级永远不会遇到没见过的 token,代价是序列长了好几倍。

好的分词在两个极端之间找平衡(W22 笔记的说法):token 不能太多(序列太长,难以建模、计算贵),也不能太少(否则词与词之间没有参数共享——father-in-law 和 mother-in-law 应该共享 “-in-law”),而且每个 token 最好是一个有意义的单位。

1.2 BPE:反复合并最常见的一对

字节对编码(byte pair encoding, BPE)原本是一种压缩算法,Sennrich 等人(2016)把它用到了机器翻译的分词上。算法非常简单:

1. 每个词先拆成单个字符
2. 统计所有相邻 token 对的出现次数,把最常见的一对合并成一个新 token,记下这条"合并规则"
3. 重复第 2 步,直到合并了预定的次数

学到的是一张有序的合并规则表;词表 = 初始字符 + 每条规则产生的新 token。给新文本分词时,按规则学到的先后顺序依次合并。代码 01 演示 2 (a) 逐步复现了 W22 笔记的例子——语料是 the car、the cat、the rat:

步合并次数合并后
1t + h → th3th e · c a r · c a t · r a t
2th + e → the3the · c a r · c a t · r a t
3c + a → ca2the · ca r · ca t · r a t

词表是 [a, c, e, h, r, t, th, the, ca]。给新文本 “the ox” 分词,得到 [the, ␣, o, x]:the 是一个 token,ox 没见过,就退回到单个字符——BPE 没有 OOV,最坏情况就是字符级。(这个例子里空格单独成块、不参与合并;并列时取最早出现的一对,和笔记的选择一致。)

在迷你互联网上训练(演示 2 (b))。先按 GPT-2 的方式做”预分词”:空格贴在后面那个词的开头(“␣lives”),字母、数字、标点各成一块;BPE 只在块的内部合并。最先学到的合并是 he → the → ␣w → ␣the → ␣was……正是评论和故事里最高频的片段。合并越多,词表越大,序列越短:

合并次数 K词表大小字符 / token每行 token96 个 token 装几行
0(字符级)441.0037.52.6
1001442.1117.85.4
3003443.1411.98.0
649(合并到底)6933.4410.98.8

迷你互联网只有 410 种不同的块,合并 649 次以后每块都成了一个 token。真实文本的块是长尾的,GPT-2 / GPT-3 的词表停在 50,257 个 token,罕见的词仍然被拆成几块。K = 300 时可以清楚地看到 BPE 的几个性格:

zukun lives in quila.     →  |zu|ku|n|␣lives|␣in|␣quila|.|      高频词是一个 token,名字是碎片
95+42=137.                →  |95|+|4|2|=|13|7|.|               数字被切得不一致
kari livs in varno.       →  |k|ar|i|␣li|v|s|␣in|␣varno|.|      拼错一个字母,token 序列就完全不同

数字被切得不一致这一点值得多说一句。K = 300 时,10–198 这些数里 29 个是一个 token、150 个被切成两块、10 个被切成三块:95 是一个 token,45 是 4|5,107 是 10|7,137 是 13|7。同一个数位在不同的数里落在不同的 token 位置上,模型做加法时要”对齐数位”就难了;合并到底以后,训练语料里没出现过的 195、196、198 也被切成两块(1|95 等)。这被认为是 GPT-3 这类模型算术不稳定的原因之一;PaLM、LLaMA 等后来的模型干脆把数字拆成单个数位。

GPT-2 还做了一个重要的改进:字节级 BPE——先把文本变成 UTF-8 字节,再在字节上做 BPE。W22 笔记提到,Unicode 有 144,697 个字符,直接在字符上做会很稀疏;字节只有 256 种,任何语言、任何符号都能表示,永远不会遇到”字母表之外”的字符。

1.3 unigram 分词:分词也是一个概率模型

T5 用的 SentencePiece 默认用另一种算法:unigram 语言模型分词(Kudo 2018)。它把”怎么切”本身看成一个概率模型:每个 token 有一个概率,一种切法的概率是其中各 token 概率的乘积,分词时取概率最大的切法(用动态规划,即 Viterbi 算法)。

W22 笔记的例子:训练数据 “ababc”,切成 ab | ab | c,按这次切分里的频率,p(ab)=2/3p(\text{ab}) = 2/3,p(c)=1/3p(\text{c}) = 1/3,似然是

23×23×13=427≈0.148.\tfrac{2}{3} \times \tfrac{2}{3} \times \tfrac{1}{3} = \tfrac{4}{27} \approx 0.148 .

(笔记里把结果写成了 4/9,是一个算术笔误;代码 01 演示 3 用精确分数核对过。)如果词表是 {a, b, c, ab}、概率 1/8、1/8、1/4、1/2,“ababc” 有 4 种切法,概率最大的 ab | ab | c 是 1/16,其次两种都是 1/512。训练时从一个很大的候选词表出发,用 EM 算法估计概率,然后删掉”删了以后似然下降最少”的一部分 token(笔记里是保留 80%),反复进行。

和 BPE 相比,unigram 的特别之处是它给出了其他切法的概率。代码 01 在迷你互联网上用 BPE(K = 300)分词后的 token 频率当 unigram 概率:␣zukun 的头两种切法 ␣|zu|ku|n 和 ␣|zu|k|un,概率约 0.66 : 0.33。训练时可以按概率随机采样切法(subword regularization),让模型见识同一个词的不同切法,对分词的偶然性更鲁棒。多数词两种算法切得一样;拼错的 livs 不一样(BPE 按规则先合出 ␣li,而 ␣li 不在 unigram 的词表里)——对没见过的字符串,两种算法可以给出不同的切法。

1.4 第 1 讲为什么用字符,代价是什么

代码 01 演示 4 把字符级的代价摆在了一起:

分词方式词表 V每行 token n注意力矩阵 n²/行96 个 token 装几行
字符(第 1 讲)4437.51,4072.6
BPE K = 30034411.91428.0
  1. 序列长:同一行文本,字符级的 token 数是 BPE 的 3.1 倍,注意力矩阵的元素个数是 9.9 倍(自注意力的计算量随 n2n^2 增长,第 8 讲);生成一行也要多走 3 倍的步数。
  2. 上下文装得少:base model 的 96 个字符只装得下约 2.6 行,换成 BPE 能装约 8 行。上下文学习要在上下文里放示例(第 5 讲),这一点很关键。
  3. 大部分预测很琐碎:字符级模型的多数位置是在”把单词拼完”,真正有信息量的预测(住在哪个城市、评分是什么)只占一小部分位置。这也是为什么第 1 讲用 bit/字符而不是 token 级困惑度——不同分词方式的困惑度不能直接比较(第 1 讲练习 3)。

好处同样明显:词表只有 44、不用训练分词器、没有 OOV、模型能看到每个字母(包括数字的每一位)。对一个 26 万字符的迷你世界,序列长的代价也承受得起。

真实系统的分词器差别也很大。W22 笔记比较了 GPT-3(BPE,5 万词表)和 Jurassic(SentencePiece,25.6 万词表):同样的文本,Jurassic 的 token 数少 28%,所以快约 1.4 倍,同样 2048 的上下文能多装 39% 的文字——“Abraham Lincoln” 在 Jurassic 里是一个 token。词表大的代价是嵌入表更大,但在大模型里这不算什么:GPT-3 的嵌入表 50,257 × 12,288 只占参数的约 0.35%(代码 01 演示 4)。

1.5 本讲后半部分为什么换成”词级”token

§4、§5 要比较 BERT、T5、ELECTRA 这些”遮住一部分、让模型猜回来”的目标。BERT 的 WordPiece 词表里,常见词都是一个 token:遮住一个 token 就是遮住一个词,模型用一次 softmax 在 3 万个词里挑。字符级模型上照搬会出两个问题(代码 03 演示 4):

  • 每个字符独立地以 15% 的概率被遮住时,93% 的被遮字符只看同一个词剩下的字母、查一下词表就能唯一确定(“va_no” 只能是 varno)——模型学会拼写就够了,用不着理解上下文。
  • 改成遮住整个词,又碰上 masked LM “各位置独立预测”的问题(§4.3):就算模型把每个位置的字母分布估计得完全正确,逐位置选最可能的字母也会拼出 “crest” 这样的非词。

所以代码 04、05 改用”词 + 标点”的词级分词器:迷你互联网的词表是封闭的(349 个词和标点,验证集没有 OOV),加上换行和 12 个特殊 token,共 362 个。这样我们的 BERT / T5 / ELECTRA 和原版在”一个 token ≈ 一个词”这一点上一致。


2. 上下文嵌入:三种架构的共同抽象 · Contextual embeddings

W22 笔记用一个抽象把所有架构统一起来:上下文嵌入(contextual embedding)

ϕ:VT→Rd×T,x1:T↦(h1,…,hT),\phi : \mathcal{V}^T \to \mathbb{R}^{d \times T}, \qquad x_{1:T} \mapsto (h_1, \dots, h_T),

把一个 token 序列映成同样长度的向量序列,其中第 ii 个向量 hih_i 不只取决于 xix_i 本身,还取决于它的上下文。“the mouse ate the cheese” 里两个 the 会得到不同的向量。这和 word2vec 那种”每个词一个固定向量”(第 1 讲 §2.6)的根本区别就在这里。

有了 ϕ\phi,三种架构的区别就是怎样用它:

架构代表做什么优点缺点
encoder-onlyBERT、RoBERTax1:T⇒ϕ(x1:T)x_{1:T} \Rightarrow \phi(x_{1:T}),在表示上做分类等每个位置都看到左右两边(双向)不能自然地生成文本;需要专门设计的训练目标(masked LM)
decoder-onlyGPT-2、GPT-3x1:i⇒ϕ(x1:i)⇒p(xi+1∣x1:i)x_{1:i} \Rightarrow \phi(x_{1:i}) \Rightarrow p(x_{i+1} \mid x_{1:i})生成很自然;训练目标就是最大似然,简单每个位置只看到左边(单向)
encoder-decoderBART、T5x1:T⇒ϕ(x1:T)⇒p(y1:L∣ϕ(x1:T))x_{1:T} \Rightarrow \phi(x_{1:T}) \Rightarrow p(y_{1:L} \mid \phi(x_{1:T}))输入双向地读,输出一个个地写两套参数;同样需要专门设计的训练目标

ϕ\phi 用什么实现? 早期是 RNN / LSTM:从左到右逐个读,hi=RNN(hi−1,xi)h_i = \text{RNN}(h_{i-1}, x_i);ELMo 把一个从左到右、一个从右到左的 LSTM 拼起来得到双向的表示。2017 年的 Transformer 用注意力取代了循环。原论文的表 1 说明了它的优势:

层的类型每层计算量必须顺序执行的步数任意两个位置之间的最长路径
自注意力O(n2d)O(n^2 d)O(1)O(1)O(1)O(1)
循环(RNN)O(nd2)O(n d^2)O(n)O(n)O(n)O(n)

RNN 处理一段文本必须一步一步来(第 ii 步要等第 i−1i-1 步算完),无法在序列方向上并行;任意两个位置之间的信息要经过 O(n)O(n) 步传递,长程依赖很难学(W22 笔记的说法:LSTM 很少能”干净地”捕捉长程依赖)。自注意力让每个位置一步就能看到所有位置,整段序列可以并行计算——这正是 GPU 擅长的。代价是计算量随序列长度平方增长(第 8 讲的 FlashAttention 就是在这个代价上做文章)。原论文的 big 模型在 8 张 P100 上训练 3.5 天,就在 WMT 2014 英德翻译上达到 28.4 BLEU,比之前包括集成模型在内的最好结果高出 2 分以上。

此后,三种架构用的都是 Transformer,差别只在注意力”谁能看谁”和训练目标——这正是 §4 的主题。先把 Transformer 本身拆开。


3. 打开第 1 讲的黑盒 · Inside tiny_gpt

第 1 讲把 base model 当黑盒用:输入一串字符,输出下一个字符的分布。代码 02 把它拆开。先看全貌:

idx: (T,) 字符编号                       例如 "\nbase lives in " 的 15 个编号
  │  字符嵌入 E[idx] + 位置嵌入 P[0:T]
  ▼
x:  (T, d) = (15, 64)                    "残差流":之后每一层都往它上面加东西
  │  ┌──────────────── Transformer 块 × 2 ────────────────┐
  │  │ h = LayerNorm(x)                                   │
  │  │ q, k, v = h W_Q, h W_K, h W_V   拆成 4 个头 (4, T, 16) │
  │  │ A = softmax(q kᵀ / √16 + M)     (4, T, T)  M = 因果掩码 │
  │  │ x = x + concat(A v) W_O          残差                │
  │  │ x = x + W₂ GELU(W₁ LayerNorm(x)) 残差,MLP: 64→256→64 │
  │  └────────────────────────────────────────────────────┘
  ▼
LayerNorm → 乘以 Eᵀ(和字符嵌入共享)→ logits: (T, V) = (15, 44)

代码 02 演示 2 只用这些权重矩阵手工做了一遍这个流程(不调用任何模块的 forward),结果和 TinyGPT 的输出最大只差 8.9×10−68.9 \times 10^{-6}(logits 本身是十几的量级,差别只是浮点舍入):base model 里没有任何别的东西。GPT-3 的结构和它基本相同,只是 96 层、12,288 维、96 个头、上下文 2048(W22 笔记;另外 GPT-3 交替使用稠密注意力和局部的稀疏注意力)。下面逐块看每一部分为什么存在。

3.1 嵌入:字符嵌入 + 位置嵌入

第一步把每个字符编号变成一个 64 维向量:查表 E∈RV×dE \in \mathbb{R}^{V \times d} 的第 idx 行(nn.Embedding 就是一个可训练的查找表)。这张表是学出来的,意思相近的 token 会学到相近的向量。

只有字符嵌入是不够的。自注意力本身对顺序不敏感:它只是”每个位置对所有位置按相似度加权平均”,如果把输入打乱,每个位置的输出只会跟着换位置,不会改变。“mouse ate cheese” 和 “cheese ate mouse” 在它眼里是同一组东西。所以要把位置信息加进去:第 ii 个位置加上位置向量 PiP_i。原论文用固定的正余弦函数

Pi,2j=sin⁡ ⁣(i/100002j/d),Pi,2j+1=cos⁡ ⁣(i/100002j/d),P_{i,2j} = \sin\!\big(i / 10000^{2j/d}\big), \qquad P_{i,2j+1} = \cos\!\big(i / 10000^{2j/d}\big),

GPT 系列和我们的 base model 用学出来的位置嵌入(一张 T×dT \times d 的表)。原论文报告两者效果几乎一样(base 模型 BLEU 25.7 vs 25.8),选正余弦是希望能外推到比训练时更长的序列。

代码 02 演示 5 把位置嵌入置零:验证集 bit/字符从 0.409 变成 5.022,接近什么都没学的 log⁡244=5.459\log_2 44 = 5.459,事实补全从 0.93 掉到 0。顺序信息几乎全靠它。

3.2 自注意力:q、k、v

注意力可以理解成一张”软”的查找表(W22 笔记的说法)。每个位置 ii 由它当前的向量算出三样东西:

  • 查询 qi=hiWQq_i = h_i W_Q:“我在找什么?”
  • 键 kj=hjWKk_j = h_j W_K:“我这里有什么?“(每个位置都提供一个)
  • 值 vj=hjWVv_j = h_j W_V:“如果你找的是我,就拿走这个。”

位置 ii 用自己的查询和每个位置的键比相似度(点积),softmax 变成权重,再对值加权平均:

Attention(Q,K,V)=softmax ⁣(QK⊤dh+M)V.\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^\top}{\sqrt{d_h}} + M\right) V .

为什么要分成 q、k、v 三个投影,而不是直接用 hi⋅hjh_i \cdot h_j?因为”我要找什么”和”我能提供什么”、“我被找到时给出什么”是三件不同的事。例如回答 “q: where does base live? a:” 时,最后一个位置要找的是”问的是谁”,名字所在的位置要能被这样的查询匹配上,而被匹配上以后传过去的应该是”这个名字的身份信息”,不是”我是一个字母 s”。分开的投影让模型可以分别学这三种关系。

在 base model 里,WQ,WK,WVW_Q, W_K, W_V 合成一个 64 × 192 的矩阵 qkv 一次算出来,再切开;注意力的输出再乘一个 64 × 64 的 WOW_O(proj)放回残差流。

3.3 为什么除以 dh\sqrt{d_h}

如果 q 和 k 的每个分量独立、均值 0、方差 1,它们的点积 q⋅k=∑t=1dhqtktq \cdot k = \sum_{t=1}^{d_h} q_t k_t 就是 dhd_h 个方差为 1 的数之和,方差是 dhd_h、标准差是 dh\sqrt{d_h}(原论文 §3.2.1 的脚注)。维度越大,点积越大,softmax 越”尖”。代码 02 演示 4 用随机向量量了一下(一行 96 个键):

每头维度 dhd_h点积的标准差不缩放:平均最大权重不缩放:最大权重 > 0.99 的行除以 dh\sqrt{d_h}:平均最大权重
42.00.2390%0.081
164.00.5522%0.083
648.00.77618%0.083
25616.00.89346%0.082
102432.00.94269%0.082

softmax 的梯度是 diag(p)−pp⊤\text{diag}(p) - pp^\top;当某个权重接近 1 时,这个矩阵接近 0,梯度几乎传不回去,注意力”卡死”在随机选中的那个位置上。除以 dh\sqrt{d_h} 后,分数的尺度和维度无关。注意它的作用是让训练开始时的分布不要太尖——训练好的模型完全可以自己把分数调大:base model 训练后,两层注意力分布的平均熵是 2.42 和 1.92 nat,明显比同长度均匀分布的 3.58 集中。GPT-3 的每头维度是 128,不缩放的话初始分数的标准差约 11,softmax 一开始几乎就是 one-hot。

3.4 因果掩码

decoder-only 模型要”根据前文预测下一个 token”,训练时位置 ii 绝不能看到位置 i+1i+1 及以后的内容——否则答案就摆在眼前。做法是在 softmax 之前给”未来”的分数加上 −∞-\infty(掩码 MM 的上三角),e−∞=0e^{-\infty} = 0,这些位置的权重严格为 0。代码 02 演示 3 检查了两层、八个头所有”看向未来”的权重之和:0.0。

这个掩码还带来一个巨大的好处:一次前向传播就得到所有位置的预测。长 TT 的一段文本同时提供了 TT 道”预测下一个 token”的题(第 1 讲 §3.1),每道题都只看到了它该看到的前文。§4.6 会看到,这种”每个位置都是一道题”的密度,是 decoder-only 在训练效率上的一大优势。

3.5 多头

base model 每层有 4 个头,每个头只有 16 维,各自有自己的 WQ,WK,WVW_Q, W_K, W_V,各算各的注意力,结果拼回 64 维。原论文的解释:多头”让模型能在不同的表示子空间里、同时关注不同位置的信息”;只有一个头时,所有信息被平均到一起,这种能力就受限了。

代码 02 演示 3 看了 “q: where does base live? a:” 最后一个位置(接下来要说出城市)的 8 个头:第 0 层的 4 个头都很”散”,最大的权重不到 0.2;第 1 层的头 1.0 把约 0.39 的权重放在名字 base 上(主要落在最后一个字母),头 1.1 则把 0.58 的权重放在自己身上。不同的头确实在做不同的事——有的负责”把问的是谁搬过来”。演示 5 也印证了这一点:关掉第 1 层的注意力,事实补全从 0.93 掉到 0.05。第 12 讲会更系统地读注意力模式。

3.6 残差流与 pre-LayerNorm

每个块里有两处 x←x+f(x)x \leftarrow x + f(x):这是残差连接(residual connection,来自 ResNet)。它的作用有两层:

  • 梯度的高速公路:反向传播时,梯度可以沿着”+“直接流回前面的层,深层网络才训练得动。
  • 残差流(residual stream)的视角:每个位置有一个 64 维的向量贯穿整个网络,嵌入先写下”这是哪个字符、在哪个位置”,每个注意力层把别的位置的信息搬过来”加一笔”,每个 MLP 在原地加工再”加一笔”,输出层读的是所有笔画的总和。代码 02 演示 2 量了最后一个位置上每一笔的长度:嵌入 1.3,第 0 层注意力 1.5、MLP 1.4,第 1 层注意力 1.5、MLP 4.8——“说出哪个城市”的决定主要在最后一层 MLP 里成形。第 12 讲的机制可解释性就是从这个视角出发的。

LayerNorm 把每个位置的向量减去自己的均值、除以自己的标准差,再乘以可学的 γ\gamma、加上 β\beta:

LN(x)=γ⊙x−mean(x)var(x)+ϵ+β.\text{LN}(x) = \gamma \odot \frac{x - \text{mean}(x)}{\sqrt{\text{var}(x) + \epsilon}} + \beta .

它只在”一个位置的 dd 个数”之间归一化,不管残差流里的数值涨到多大,送进注意力和 MLP 的输入都是”标准大小”。放在哪里有讲究:原始 Transformer 是 post-LN,LN(x+f(x))\text{LN}(x + f(x));GPT-2 起改成 pre-LN,x+f(LN(x))x + f(\text{LN}(x))——归一化放在分支里面,残差这条主干上没有任何东西挡着,深层模型训练更稳定,对 warmup 也没那么敏感(W22 笔记提到,Transformer 需要学习率 warmup 的一个解释正是 LayerNorm 带来的梯度问题)。我们的 base model 是 pre-LN,最后再加一个 ln_f。

3.7 MLP

注意力是不同位置之间交换信息的唯一地方;MLP 则在每个位置内部独立计算:64 维升到 256 维(4 倍,GPT-3 也是 dff=4dd_{ff} = 4d)、过一个 GELU 非线性、再降回 64 维。如果只有注意力,每层只是”对值做加权平均”再线性变换,表达能力很有限;MLP 提供了逐位置的非线性加工。它也是参数的大头:代码 02 演示 1 算出,每个块里 MLP 占 66% 的参数,注意力占 33%。第 12 讲会看到,很多”事实知识”被认为储存在 MLP 里。

3.8 输出层与 weight tying

最后一层表示经过 ln_f 以后,乘以一个 d×Vd \times V 的矩阵得到每个字符的打分(logits)。base model 让这个矩阵就是字符嵌入矩阵的转置(weight tying;GPT-2 也这样做):“哪个字符的嵌入向量和当前表示最像,下一个就最可能是它”。好处是省参数(这里省 2,816 个,占 2.6%)、并让输入和输出的”词义空间”一致。

3.9 数参数:109,056 从哪里来

代码 02 演示 1 按公式逐项数(V=44V = 44,T=96T = 96,d=64d = 64,L=2L = 2):

部分公式参数个数
字符嵌入VdVd2,816
位置嵌入TdTd6,144
每块:两个 LayerNorm2×2d2 \times 2d256
每块:WqkvW_{qkv} 和偏置d⋅3d+3dd \cdot 3d + 3d12,480
每块:WOW_O 和偏置d2+dd^2 + d4,160
每块:MLP 两层d⋅4d+4d+4d⋅d+dd \cdot 4d + 4d + 4d \cdot d + d33,088
每块合计12d2+13d12d^2 + 13d49,984
最后的 LayerNorm2d2d128
输出层与字符嵌入共享0
总计Vd+Td+L(12d2+13d)+2dVd + Td + L(12d^2 + 13d) + 2d109,056

和 PyTorch 数出来的一模一样。每个块的参数几乎都在 4 个矩阵里(3d2+d2+4d2+4d2=12d23d^2 + d^2 + 4d^2 + 4d^2 = 12d^2),所以参数量 ≈ 12Ld212Ld^2 是估算 Transformer 大小的标准公式。用它算 GPT-3(dd = 12,288,LL = 96):12×96×122882≈173912 \times 96 \times 12288^2 \approx 1739 亿,加上嵌入表 (50257+2048)×12288≈6.4(50257 + 2048) \times 12288 \approx 6.4 亿,合计约 1746 亿,和论文报告的 1750 亿只差一点。第 4 讲的”训练算力 C≈6NDC \approx 6ND“就建立在这个参数量上。

一个”机制 vs 量级”的小例子:我们的模型里两张嵌入表占 8.2% 的参数,GPT-3 里只占约 0.37%。规模越大,几乎所有参数都在那些 d×dd \times d 的矩阵里。

3.10 拆掉一块会怎样

代码 02 演示 5 把 base model 装进本讲的 MaskedTransformer(参数名和 TinyGPT 完全相同,只是掩码由调用者给;用因果掩码时输出逐位相同),然后关掉某个组件(把它的输出投影置零,这一层就只剩残差):

模型验证集 bit/字符事实补全
(参照:均匀分布)5.4590.00
完整的 base model0.4090.93
位置嵌入置 05.0220.00
关掉第 0 层注意力6.7990.00
关掉第 1 层注意力1.3480.05
关掉第 0 层 MLP5.4990.00
关掉第 1 层 MLP4.2050.00
因果掩码换成双向掩码7.528—

三点观察:

  1. 在 2 层的小模型里,每一块都不可缺少。关掉第 0 层的注意力或 MLP,结果比什么都没学还差——后面的层是在”第 0 层加工过的残差流”上学的,输入分布一变就失灵。几十层的大模型里,单独去掉中间某一层的影响常常小得多,层与层之间有冗余;小模型没有这种余量。
  2. 影响最小的是第 1 层注意力:bit/字符只升到 1.348,“拼单词、接模板”的能力大体还在,事实补全却几乎归零——演示 3 里把名字搬到当前位置的头就在这一层。能力和组件之间有相当具体的对应关系。
  3. 最有意思的是最后一行。把因果掩码换成双向掩码,每个位置都能”看到”它要预测的下一个字符,按理说可以作弊——可是 bit/字符变成了 7.528,比均匀分布还差得多。模型从没学过怎样利用右边的信息,多出来的注意力目标把它学到的模式全打乱了。掩码不是一个可以随便拨动的开关:架构(掩码)和训练目标是一起学进权重里的。 要把一个训练好的因果模型改成双向模型,需要继续训练来适应(§7 的 Wang et al. 2022)。

4. 三种架构家族与训练目标 · Architectures & objectives

4.1 同一个矩阵,三种填法

代码 03 演示 1 把四种注意力模式画成了网格(行 = 当前位置,列 = 被看的位置,# = 可以看)。以 “37 + 45 = 82 .” 为例:

(a) encoder-only(BERT)   (b) decoder-only(GPT)   (c) prefix LM(前缀 37+45=)
      37 +  45 =  82 .          37 +  45 =  82 .          37 +  45 =  82 .
  37   #  #  #  #  #  #     37   #  .  .  .  .  .     37   #  #  #  #  .  .
   +   #  #  #  #  #  #      +   #  #  .  .  .  .      +   #  #  #  #  .  .
  45   #  #  #  #  #  #     45   #  #  #  .  .  .     45   #  #  #  #  .  .
   =   #  #  #  #  #  #      =   #  #  #  #  .  .      =   #  #  #  #  .  .
  82   #  #  #  #  #  #     82   #  #  #  #  #  .     82   #  #  #  #  #  .
   .   #  #  #  #  #  #      .   #  #  #  #  #  #      .   #  #  #  #  #  #

(d) encoder-decoder(T5、BART)有三块注意力:编码器对输入 “37 + 45 =” 双向自注意力;解码器对输出 “[S] 82 .” 因果自注意力;再加上交叉注意力(cross-attention)——解码器的每个位置用自己的查询去”查”编码器最后一层的所有输出(原论文称之为 encoder-decoder attention;第 14 讲的 Flamingo 用同样的机制让语言模型”看”图像)。

prefix LM 正好介于 (a)(b) 之间:前缀长度为 1 时就是 GPT,前缀等于整段时就是 BERT。它和 (d) 的信息流也几乎一样——输出能看到全部输入、只能看到自己左边的输出——区别在于参数是否共享,以及输出看的是输入的哪一层表示。

掩码决定了模型能做什么:(b)(c)(d) 都能一个一个地生成输出;(a) 的每个位置都看到了整段输入,没有”下一个”的概念,只能给已有的位置打分或填空。

4.2 decoder-only + 因果语言建模(GPT)

目标就是第 1 讲的最大似然:

LCLM(θ)=−∑x1:T∈D∑i=1Tlog⁡pθ(xi∣x1:i−1).\mathcal{L}_{\text{CLM}}(\theta) = -\sum_{x_{1:T} \in \mathcal{D}} \sum_{i=1}^{T} \log p_\theta(x_i \mid x_{1:i-1}).

简单、和生成完全一致(训练时做的事就是使用时做的事),而且因为因果掩码,每个位置都是一道题。缺点是表示是单向的:第 ii 个位置的表示只包含左边的信息。BERT 正是为了解决这一点而提出的。

4.3 encoder-only + masked LM(BERT)

如果不需要生成,就可以让每个位置看到左右两边。但”预测下一个词”在双向注意力下就成了作弊(§3.10 最后一行)。BERT(Devlin et al. 2018)的办法是遮住一部分输入,让模型猜回来——把语言建模变成一个去噪问题:从被破坏的 x~\tilde{x} 恢复原来的 xx。

masked LM 的具体规则(BERT 论文 §3.1):随机选 15% 的 token 位置;被选中的位置

  • 80% 换成 [MASK];
  • 10% 换成一个随机 token;
  • 10% 保持原样(但仍然要预测)。

损失只在被选中的位置上算,每个位置独立地预测。为什么不总是换成 [MASK]?因为下游微调时输入里根本没有 [MASK]:如果模型只在 [MASK] 处认真”思考”,预训练和使用之间就有分布差异。随机替换和保持原样让模型不知道哪些位置被动过,必须对每个位置都保持好的表示。论文附录 C.2 的消融显示,对微调来说这个选择不太敏感(MNLI 上 80/10/10 是 84.2,全用 [MASK] 是 84.3),但对”直接拿表示当特征”的用法,全用 [MASK] 明显更差(NER 特征法 94.9 vs 94.0)。代码 03 演示 2 把一行评论按这个规则变成了训练样本:

原文:review : the soup was tasty and the host was kind . rating : good .
输入:review : the [M]  was tasty pilot the host was kind . rating : good .
要预测:位置 3 → soup(换成了 [M]),位置 6 → and(换成了随机词 pilot),位置 13 → :(保持原样)

下一句预测(next sentence prediction, NSP):BERT 还有第二个目标。输入是两段话 A 和 B,50% 的情况 B 真的是 A 的下一句,50% 是随便找的一句;用开头的 [CLS] 位置的表示做二分类。论文认为它对问答和自然语言推理有帮助,但后来的 RoBERTa 发现去掉 NSP 并不变差,再把训练数据从 16GB 增加到 160GB、训练得更久,SQuAD 从 81.8 提高到 89.4(W22 笔记)。

规模与影响:BERT-Base 12 层、768 维、12 个头、1.1 亿参数;BERT-Large 24 层、1024 维、16 个头、3.4 亿参数;训练数据是 BooksCorpus(8 亿词)和英文维基百科(25 亿词)。它把 GLUE 基准推到 80.5,绝对提升 7.7 个点,几乎刷新了当时所有 NLP 任务的最好成绩(第 1 讲 §2.6)。双向有多重要?论文表 5 把 BERT-Base 换成”从左到右”(LTR,等于一个 GPT 式的编码器)后,MRPC 从 86.7 掉到 77.5,SQuAD 从 88.5 掉到 77.8。

masked LM 的两个弱点。第一,只有 15% 的位置提供训练信号(§4.6)。第二,被遮住的多个位置是彼此独立预测的:模型给每个 [MASK] 一个分布,但不建模它们之间的联合关系。当一个”意思单位”被拆成几个 token 时,这会出问题——代码 03 演示 4 的例子:右边的上下文说明空格里是一个 5 字母的正面形容词(tasty、fresh、sweet、crisp、great),即使模型把每个位置的字母分布估计得完全正确,逐位置选最可能的字母也会拼出 “crest”。XLNet 等工作批评的正是这种独立性假设;BERT 后来发布的 whole word masking 版本、SpanBERT(整段遮住)都和它有关;而 T5 的做法是干脆让解码器把被挖掉的片段一个一个地写出来。

4.4 encoder-decoder:T5 与 BART

T5(Raffel et al. 2019)的核心主张是”把所有 NLP 任务都写成文本到文本”(text-to-text):翻译是 “translate English to German: …” → 德文,分类也是输出类别的名字(“positive”),而不是在 [CLS] 上接一个分类器。这样一个模型、一个目标、一套解码方式就能做所有任务。它的预训练目标是 span corruption:随机挖掉 15% 的 token,连续被挖掉的 token 合成一个片段,每个片段在输入里换成一个哨兵(sentinel);目标是”哨兵 + 被挖掉的内容”。论文表 3 的例子:

原文:Thank you for inviting me to your party last week .
输入:Thank you <X> me to your party <Y> week .
目标:<X> for inviting <Y> last <Z>

和 BERT 相比有两个好处:目标很短(只包含被挖掉的片段,省计算);解码器自回归地写出片段,片段里的 token 不再独立。T5 论文对这个设计做了极其系统的比较(全部是先预训练、再在各任务上微调):

  • 损坏比例(表 6):10%、15%、25%、50% 差别不大,只有 50% 在 GLUE、SQuAD 上明显变差;保留 15%。
  • 片段长度(表 7):平均长度 2、3、5 差别不大,3 在多数非翻译任务上略好;最终模型用平均长度 3。
  • 三类目标(表 4):BERT 式去噪 GLUE 82.96,prefix LM 80.69,打乱顺序再还原(deshuffling)73.17。
  • 架构(表 2,GLUE 分数;PP 是一个 12 层栈的参数量,MM 是计算量):
架构参数计算去噪目标语言建模目标
encoder-decoder2P2PMM83.2879.56
encoder-decoder,编解码器共享参数PPMM82.8179.60
encoder-decoder,各 6 层PPM/2M/280.8878.67
decoder-only 语言模型PPMM74.7073.78
prefix LMPPMM81.8279.68

在”预训练 + 微调”的设定下,encoder-decoder + 去噪最好;去噪目标在每种架构上都好于语言建模目标;纯 decoder-only 最差。注意 encoder-decoder 的参数是 decoder-only 的 2 倍,计算量却相同:输入 token 只走编码器、输出 token 只走解码器,每个 token 经过的层数一样。T5 最终在约 750GB 的 C4 语料上训练,最大的模型 110 亿参数。(T5 的作者之一 Colin Raffel 是第 13 讲的客座讲者。)

BART(Lewis et al. 2019)是另一个 encoder-decoder:编码器读被破坏的文本,解码器写出完整的原文。它比较了五种噪声——token masking、token deletion、text infilling(片段长度服从 Poisson(3),每个片段只换成一个 [MASK],模型还得自己判断缺了几个词)、句子打乱、文档旋转。结论是 text infilling 最稳定;最终的大模型遮住 30% 的 token 并打乱所有句子,编码器、解码器各 12 层、1024 维。BART 在分类上和 RoBERTa 相当,在摘要等生成任务上提升明显(论文摘要说最多提高 6 个 ROUGE 点)。

4.5 prefix LM:两者之间

prefix LM 用一个 decoder 的参数,但前缀部分双向可见、之后因果(§4.1 (c)),损失只算在前缀之后。它天然适合”给定输入、生成输出”的任务:输入是前缀,双向地读;输出照常一个个地写。T5 表 2 里,用去噪目标训练的 prefix LM(81.82)远好于纯 decoder-only(74.70),接近 encoder-decoder。UL2 的 S-denoiser 就是它(§6)。代码 02 演示 5 和代码 04 都说明了一点:prefix LM 和 GPT 用的是同一个网络结构,区别只在掩码。

4.6 训练信号有多密

代码 03 演示 3 用代码 04、05 训练时真正使用的 batch 函数,数了每种目标”每处理 1000 个位置,有多少个位置在算损失”:

目标每 1000 个位置的损失项其中是哨兵真正的”内容”预测
因果 LM(GPT)100001000
prefix LM5330533
masked LM(BERT,15%)1370137
span corruption(单个 Transformer,15%)18710384
span corruption(T5 式编码器 + 解码器)235131105
ELECTRA 判别器100001000(二分类)

同样的计算量,GPT 得到的训练信号是 BERT 的 7 倍多。T5 的目标里一大半是哨兵——它们按 <0>、<1>、<2> 的顺序出现,几乎不用猜——真正”写出被挖掉的词”的信号只有一百左右。这张表是 §5 所有实验结果的钥匙。


5. 同样的算力,谁学得快、谁能做什么 · A controlled comparison

5.1 实验设置

代码 04 把四种”架构 + 目标”放在同一条起跑线上:

  • 数据:迷你互联网训练集里的 1,897 条评论(32,249 个词级 token)。只用评论,是为了让四个模型都能在 2 分钟内训练完;评论里有我们要测的全部能力(评分规则、形容词的正负、生成)。
  • 模型:宽度 64、2 层、4 个头;GPT、prefix LM、BERT 都是 125,312 个参数;T5 是编码器 2 层 + 解码器 2 层,260,992 个参数(参数约 2 倍、每个 token 的计算量相同,和 T5 论文表 2 的设定一致)。
  • 训练:每段 32 个位置、batch 32、2500 步,同样的学习率调度和种子。每种模型每一步处理的位置数相同,所以算力相同;不同的只是目标。
  • 目标:GPT(因果 LM)、prefix LM(随机分界)、BERT(15%、80/10/10)、T5(15% 的 i.i.d. span corruption,即 T5 的基线目标)。

5.2 学习曲线:谁先学会

训练过程中每 500 步,让每个模型用它自己的接口预测 100 条评论的评分:GPT 接着 “rating:” 往下写,prefix LM 把整段当前缀,BERT 填评分位置上的 [M],T5 在评分位置放 <0>、看解码器写到 “[S] <0>” 时的下一个 token。打分协议对四个模型完全相同:取那个空位上的 logits,只在 good / ok / bad 三个候选里取最大的。

模型0 步500 步1000 步1500 步2000 步2500 步
GPT(因果 LM)0.471.001.001.001.001.00
prefix LM0.471.001.001.001.001.00
BERT(masked LM)0.260.470.731.001.001.00
T5(span corruption)0.390.470.470.470.470.47
(T5 自由生成,整段完全匹配)0.000.000.000.000.000.00

(三种评分大约各占 1/4、1/2、1/4;0.47 恰好是这 100 条里 ok 的比例,也就是”永远猜 ok”的水平。最后一行是另一种更苛刻的协议,见下文。)

  • GPT 和 prefix LM 在第 500 步就全对。“评分由两个形容词决定”恰好是从左往右读就能学到的规律,而 GPT 的每个位置都是一道题。
  • BERT 先在”按先验猜”的水平停了一段(500 步时和”永远猜 ok”一样),1000 步 0.73,1500 步才全对。它每 1000 个位置只有约 140 个训练信号,而且大多数被遮住的词靠附近的语法就能猜对(“the ___ was” 后面是食物名),真正需要”看远处内容”的信号更少。这种”先平台、后突变”的学习曲线在小模型上很常见(第 5 讲还会讨论”涌现”和平台期)。
  • T5 从第 500 步起一直停在 0.47,也就是”永远猜 ok”——和 BERT 在第 500 步的状态一样,但它在 2500 步内没有跨过这个平台。在三个评分词里它总是选最常见的 ok,没学会”评分由两个形容词决定”。它在自己的目标上并非什么都没学:训练结束时哨兵预测对 99%,被挖掉的词猜对 66%(BERT 猜被遮住的词是 77%);但”在输入里找到 <0>、再根据 <0> 周围的内容决定写什么”这件事,在这个算力下还没学会。它不只训练信号少,任务也更绕。
  • 表里最后一行是一个教训。如果让 T5 自由生成——解码器一直写到下一个哨兵,整段必须正好是评分词——它在每个检查点都是 0.00:最常写出的片段是 “ok .”,把后面本来就在的句号也写了进去(§5.3)。本讲最初的版本就是只用这种方式给 T5 打分、却让别的模型在三个候选里挑,于是把”还没学会规则”错记成了”连评分词都写不出”。比较不同架构时,评测协议必须相同:同样是”在空位上打分”,或者同样是”自由生成”,否则比较的是协议而不是模型。

这些都是机制层面的观察:在同样的、很小的算力下,训练信号越多、任务越直接的目标学得越快。它们不说明 BERT 或 T5 最终学不会,也不说明它们在大规模上更差——T5 论文在大得多的算力下发现 encoder-decoder + 去噪在微调后最好(§4.4)。(写代码时我在副本里把 T5 的训练延长到 6000 步,它学会了往形容词的空里填形容词,但评分仍是”永远猜 ok”,见练习 9。)

5.3 能做什么、不能做什么

训练结束后,用 200 条新评论测三件事(代码 04 演示 2)。“填形容词”:把食物形容词挖掉,右边的 “and/but + 服务 + 评分” 决定了它该是正面还是负面(“填对” = 填出一个食物形容词且正负和右边一致,瞎猜约 0.5)。“生成”:从零采样 200 行,格式合法率 / 内容为真率(第 1 讲的 check_line)。

模型预测评分填形容词:做法 → 填对从零生成:合法 / 为真
GPT1.00从左往右写 → 0.55;给 16 个候选打分 → 1.000.97 / 0.97
prefix LM1.00从左往右写 → 0.530.97 / 0.97
BERT1.00填 [M](一次前向传播)→ 1.000.00 / 0.00
T50.45填 <0>、第一步取最大 → 0.00;给 16 个候选打分 → 0.46;自由生成 → 0.00—

这张表是三种架构”性格”的缩影:

  1. GPT 从左往右写,用不上右边的信息。写到 “the soup was” 时,它还没看到后面的 “and the host was kind. rating: good”,只能按先验挑一个形容词,正负对的比例和瞎猜差不多(0.55)。prefix LM 一样——生成的部分是因果的。
  2. 但 GPT 并不是”不知道”:把 16 个候选逐个填进去、比较整行的概率,它全对。整行的概率里包含了”这个形容词和后面的评分配不配”——右边的信息通过打分间接用上了,代价是 16 次前向传播。GPT-3 论文做多项选择题正是这样(第 1 讲的 completion_logprob)。
  3. BERT 一次前向传播就能填对:[M] 同时看到左右两边。分类、抽取、填空这类”输入已经完整给出”的任务,正是 BERT 时代的主战场。
  4. BERT 不会写:没有”下一个词”的概念,用”末尾放一个 [M]、猜出来再放一个”的办法硬凑,200 行里没有一行合法。它写出的是 ”: the was. soggy and the staff was bad. rating: good..” 这样词序混乱、缺词少字的东西——它的训练目标里根本没有”从左往右接着写”这回事:[M] 只是”这里少了一个词”,左边和右边对它来说是对称的。
  5. T5 在这个算力下还在按先验猜。三种做法都按和别人相同的协议打分:评分 0.45(这 200 条里 ok 的比例,即”永远猜 ok”);形容词空位上第一步最可能的词 200 次全是 the(连词性都不对);给 16 个候选打分 0.46,和瞎猜一样。
  6. T5 的自由生成暴露了另一个问题:片段的边界。在评分的空位上,它最常写出的片段是 “ok .”(105 次)——把后面本来就在的句号也当成了被挖掉的内容,偶尔还接着写 “rating”(11 次);其次是从 “review : the” 开始重新写一条评论(84 次)。span corruption 训练的是”把挖掉的片段写回来”,“片段从哪里开始、到哪里结束”本身也要学。T5 在大规模上的长处恰恰是两者兼得:编码器双向地读,解码器写出任意长度的片段;但要它从零写一整段(“生成”一栏的”—”),需要换一种输入格式或继续训练——Lester et al.(2021)做 prompt tuning 时专门用了”LM-adapted” 版的 T5,就是这个原因(第 6 讲)。

5.4 线性探针:表示里有什么

不看各自的接口,只看表示:冻结模型,用很少的标签训练一个线性探针读出”食物好不好”,测试评论里的食物形容词在标注数据里从没出现过(和第 1 讲代码 03 演示 2 的设定 B 相同)。要做对,表示本身就得编码”这个形容词是正面还是负面”——这只能来自预训练。

模型读哪里k = 8k = 32k = 128
GPT各位置平均0.8440.8380.851
GPT最后一个位置0.8530.9360.948
prefix LM各位置平均0.8940.9931.000
BERT各位置平均0.7310.8020.943
T5编码器:各位置平均0.5410.5060.503
T5编码器:形容词处0.5330.5220.559
T5解码器:[S] <0> 处0.4850.4990.451
  • 学会了”形容词决定评分”的三个模型,表示里都带着形容词的正负;还没学会的 T5 换了三种读法(编码器各位置平均;编码器在形容词那个位置;解码器写到 “[S] <0>” 时的状态,输入是”评论 + rating: <0>.”,相当于 GPT 读 “rating:” 处)都在 0.45–0.56 之间。它的输入里没有补齐位置,每个位置都看得到整条评论,所以这不是”读错了地方”——它的表示里确实还没有形容词的正负。能不能把知识带到标签之外,取决于预训练时学到了什么。
  • 怎么读也很重要。GPT 的因果表示取平均时,评论前半段的位置还没看到形容词,平均会把信息冲淡(0.838);读最后一个位置(它看到了整条评论)就好得多(0.936)——和第 1 讲代码 03 的发现一致。双向的模型每个位置都看到了整条评论,取平均就很自然。
  • prefix LM 的表示最好读:它的前缀部分是双向的,训练信号又比 BERT 多得多——在这个小实验里,它兼得了两边的好处。

5.5 ELECTRA:让每个位置都有信号

既然 BERT 的问题是”只有 15% 的位置有训练信号”,能不能让每个位置都有信号?ELECTRA(Clark et al. 2020)的办法叫替换 token 检测(replaced token detection):

  1. 一个小的生成器(generator,本身就是一个小 masked LM)在被遮住的 15% 位置上预测原词,并从它的预测分布里采样一个词填回去;
  2. 一个判别器(discriminator,和 BERT 一样大的双向编码器)对每一个位置判断:这个词是原来的,还是被换过的?生成器碰巧采到原词时,标签算”原词”;
  3. 总损失 = 生成器的 MLM 损失 + λ\lambda × 判别器的二分类损失(λ\lambda = 50);预训练完扔掉生成器,只用判别器。

代码 03 演示 2 的例子:

原文:review : the soup was tasty and the host was kind . rating : good .
输入:review : the soup was bland and the host was kind . rating : good .   (另有 host → staff;good 被采回了 good)
标签:每个位置一个"原 / 换"——16 个位置全部有标签

要发现 bland 被换过,判别器得懂”评分由形容词决定”。几个设计细节:

  • 不是 GAN:生成器用最大似然训练,判别器的损失不传回生成器(采样不可导)。论文试过用强化学习训练对抗式生成器,效果更差(生成器的 MLM 准确率只有 58%,最大似然的是 65%)。
  • 生成器要小:论文发现生成器是判别器的 1/4 到 1/2 大小最好。生成器太强,替换出来的词太像真的,判别器的任务过难。
  • 收益从哪来:论文表 5 的消融(GLUE):ELECTRA 85.0;把判别器的损失只算在被遮住的 15% 位置上(ELECTRA 15%)82.4;BERT 82.2。也就是说,大部分收益来自”所有位置都有信号”。
  • 效果:同样的算力(1.4×10181.4 \times 10^{18} FLOPs、1400 万参数),ELECTRA-Small 在 GLUE 上 79.9,BERT-Small 75.1;只用一张 GPU 训练 4 天,就超过了用约 30 倍算力训练的 GPT(78.8)。在大规模上,它用不到四分之一的算力就和 RoBERTa、XLNet 相当。论文强调,模型越小,ELECTRA 的优势越大。

5.6 在迷你世界里,ELECTRA 没有奏效

代码 05 在代码 04 的同一份评论上比较三个同样大小的双向编码器(判别器 = BERT 的大小,125,312 个参数;生成器 1 层、32 维、25,376 个参数,每步多约 20% 的计算),每 500 步用探针(k = 32,测试形容词没见过)检查一次表示:

模型0 步500 步1000 步1500 步2000 步2500 步
BERT(masked LM 15%)0.4450.5020.5320.6650.8980.802
ELECTRA(100% 位置)0.4450.5180.5160.5190.5030.512
ELECTRA-15%(只在遮住处)0.4450.5270.5250.5140.5270.513

我原本预期 ELECTRA 比 BERT 更早学会,而且 ELECTRA-15% 介于两者之间。实际上 ELECTRA 和 ELECTRA-15% 从头到尾都停在瞎猜的水平,BERT 反而在 2000 步达到约 0.9(2500 步回落到 0.8:探针只用 32 个标签,表示也还在变化;两份代码里的 BERT 是同一个模型)。

问题出在哪里?代码 05 演示 2 打开了训练好的 ELECTRA:

  • 生成器学得太快。被选中的位置里真被换掉的比例:第 500 步 0.92,第 1500 步 0.45,第 2500 步 0.29。在模板化的评论里,很多被遮住的词几乎是确定的(“rating” 后面是 ”:”),训练后期只有 4.1% 的位置被换掉,判别器看到的标签几乎全是”原词”。
  • 被换掉的词里,约一半根本无法分辨:waiter → cook、bread → pie、两个同为正面的形容词互换——意思没变(47%)。另一半改变了意思(48%),例如句子里是 “sour and the waiter was polite. rating: bad”,polite 显然是被换过的;要发现它们,判别器得做”形容词正负 + and/but + 评分”的推理。只有 6% 是词性都不对的明显替换。
  • 判别器只学会了抓明显的替换(抓出 79%),对改变意思的替换只抓出 3%,对原词从不误报。它停在”语法检查器”的阶段,没学到探针需要的正负知识。真正有信息量的标签——改变了意思的替换——只占全部位置的约 2%,比 BERT 的约 14% 还少。

教训:ELECTRA 的”每个位置都有信号”,只有当”判断换没换”需要我们想让编码器学的知识时才有用;它的效果取决于生成器犯什么样的错,而这又取决于数据。真实文本比我们的模板丰富得多,生成器的错误五花八门(语法、搭配、事实、指代……),判别器能从中学到大量语言知识。所以这个玩具实验既不能证明、也不能推翻论文的结论;它说明的是一个机制上的前提条件。这也是本讲反复出现的”机制 vs 量级”:小实验适合检验”为什么”,不适合检验”有多好”。

5.7 哪些结论能推广

结论在迷你实验里在真实大模型上
掩码决定了”能做什么”(生成 vs 双向填空)清楚地看到(§5.3);比较时评测协议要相同成立:这是架构的定义性差别
同样算力下,训练信号越多学得越快GPT、prefix LM ≫ BERT ≫ T5(T5 停在”永远猜 ok”,§5.2)方向成立(ELECTRA、decoder-only 的效率论证),但量级取决于规模、数据和训练时长
双向表示更适合”读”prefix LM 的探针最好;GPT 要选对读出位置(§5.4)BERT 论文的 LTR 消融;encoder 至今仍用于检索和分类(§9)
encoder-decoder + 去噪最好没能复现:T5 在这个算力下停在先验水平T5 论文:在”预训练 + 微调”设定下最好;但 zero-shot 时因果 decoder 最好(§7)
ELECTRA 更省样本没能复现,原因清楚(§5.6)论文:同算力下 GLUE 明显更好,小模型优势最大

6. UL2:把目标混在一起 · Unifying Language Learning Paradigms

到 2022 年,局面有点混乱:GPT 式的因果 LM 适合生成和 few-shot,T5 式的去噪适合微调,不同论文各说各的。UL2(Tay et al. 2022)提出了两个主张。

第一,把架构和目标分开。 论文的原话是”把架构和自监督方案解耦”(disentangle the architecture from the self-supervision scheme):decoder-only 和 encoder-decoder 都可以用任何目标训练,而”选择什么去噪器的影响,比选择什么骨干架构大得多”;骨干的选择主要是各种效率指标之间的取舍。这和 §3.10、§5 的观察一致:prefix LM 和 GPT 是同一个网络,只差一个掩码。

第二,混合去噪器(mixture of denoisers, MoD)。用片段平均长度 μ\mu、损坏比例 rr 描述一个去噪任务,UL2 混合三类:

去噪器含义最终配置
R(regular)常规的 span corruption,和 T5 相同(μ\mu = 3, rr = 0.15)、(μ\mu = 8, rr = 0.15)
S(sequential)顺序去噪,就是 prefix LM:目标在序列末尾,“不依赖未来的信息”目标约占 25%
X(extreme)极端去噪:片段很长(≥ 12 个 token)或损坏比例很高(≥ 30%)(μ\mu = 3, rr = 0.5)、(μ\mu = 8, rr = 0.5)、(μ\mu = 64, rr = 0.15)、(μ\mu = 64, rr = 0.5)

7 个去噪器大致等比例混合。这个框架把各种目标看成同一族的特例:当被挖掉的”片段”就是整个序列(μ=T\mu = T、r=1r = 1)时,去噪就成了语言建模;prefix LM 是 μ=T−P\mu = T - P、r=1−P/Tr = 1 - P/T(PP 是前缀长度)。论文因此认为没必要再单独加一个因果 LM 任务——它是 prefix LM 的特例。代码 03 演示 2 (7) 把一段文本按 R、S、X 三种方式变成了训练样本:R 挖掉几个短片段,S 给前半段写后半段,X 一口气挖掉一半,要求模型写出大段内容(更接近生成)。

模式切换(mode switching):预训练时在输入开头加一个模式 token,告诉模型现在做的是哪一类去噪(论文写作 [R]、[S]、[X];公开发布的 UL2 checkpoint 的说明里对应的是 [NLU]、[S2S]、[NLG]);下游微调或 few-shot 时也加上与任务相称的模式 token。论文发现这对 one-shot 摘要这类任务影响很大,对 SuperGLUE 影响较小。

结果:UL2 20B(一个和 T5 很像的 encoder-decoder,在 C4 上训练了 1 万亿 token)在 50 个有监督任务上达到当时最好,zero-shot SuperGLUE 超过了 1750 亿参数的 GPT-3,one-shot 摘要是 T5-XXL 的三倍。之后的 Flan-UL2 是它的指令微调版(第 7 讲)。UL2 的混合目标思路也被用到 decoder-only 模型上(例如对 PaLM 继续训练的 U-PaLM),但到 2023 年以后,大规模预训练基本收敛到了纯因果 LM(§9)。


7. 为什么 decoder-only 成了默认选择 · Why decoder-only won

回头看 2018 年,BERT 风头最盛:几乎所有 NLP 排行榜都被 BERT 及其变体占据,GPT-1 反而不太被注意。几年后,GPT-3、PaLM、LLaMA、以及几乎所有公开了架构细节的大模型都是 decoder-only。为什么?没有单一的原因,而是几股力量叠加:

  1. 生成是最通用的接口。只要能写字,任何任务都能写成”给定提示、续写答案”:分类是写出类别名,问答是写出答案,翻译是写出译文。GPT-3 的 in-context learning(第 1 讲 §4.2)把这一点推到了极致——不用微调,换个提示就换个任务。encoder-only 模型做不到这一点,每个任务都要接一个专门的”头”再微调。
  2. 训练效率:每个 token 都是一道题(§4.6)。代码 04 的学习曲线(§5.2)是这一点在小尺度上的直观体现。在”算力就是一切”的规模化时代,这很重要。
  3. 简单:一个栈、一个目标、一个掩码,没有 [MASK]、哨兵、去噪比例、片段长度这些要调的东西;训练时做的事和使用时做的事完全一样;推理时可以缓存前面位置的键和值(KV cache),每生成一个 token 只算新位置。简单的东西更容易扩展、优化和调试。
  4. zero-shot 能力。Wang et al.(2022)做了一组很系统的对照:在约 50 亿参数(decoder 4.8B、encoder-decoder 11.0B)、1680 亿 token 的规模上,比较因果 decoder(CD)、非因果 decoder(ND,即 prefix LM 架构)、encoder-decoder(ED)三种架构与全量语言建模、prefix LM、masked LM 三种目标。结论分两半:
    • 只做预训练、不做多任务微调时,因果 decoder + 全量语言建模的 zero-shot 表现最好(EAI-Eval:CD 44.2、ND 43.5、ED 39.9,随机水平 32.9);
    • 做了多任务提示微调之后,encoder-decoder + masked LM 最好(T0-Eval:ED:MLM 60.6、ND:MLM 55.2、CD:FLM 51.4)。
    • 他们还发现两者可以兼得:先训练一个因果 decoder,再把它改成非因果、用 masked LM 继续训练(non-causal MLM adaptation),收敛速度是从头训练 ND:MLM 的 3.3 倍。论文的建议正是”先训练因果 decoder,需要时再适应”——这也是 §3.10 最后一行”掩码要靠训练来适应”的大规模版本。
  5. 规模化的经验与工程积累。规模定律(第 4 讲)、训练稳定性的技巧、分布式训练(第 9 讲)、推理优化,几乎都是在 decoder-only 上发展起来的。一旦一个方向积累了足够多的经验,换方向的成本就很高。

但这不等于另外两种架构”输了”:

  • 在”预训练 + 微调”的固定任务上,encoder-decoder 往往更好(T5 表 2、Wang et al. 的多任务微调结果);T0、Flan-T5 这些早期的指令微调模型都是 encoder-decoder(第 7 讲)。
  • 双向编码器在”读”的任务上仍然是首选:句子嵌入、检索、重排序、分类(§9)。
  • 架构之争在很大程度上是目标之争(UL2 的观点):同一个网络,因果 LM 训练出的是生成器,masked LM 训练出的是编码器。

8. 选择性架构:混合专家与检索 · Selective architectures

到目前为止,模型对每个输入都动用全部参数:GPT-3 处理每个 token 都要用到全部 1750 亿个参数。W22 笔记 Selective Architectures 介绍了两种打破这一点的思路。

8.1 混合专家(Mixture of Experts, MoE)

把一个 MLP 换成 EE 个”专家”(各自是一个 MLP),再加一个门控(gating)网络决定每个 token 交给哪些专家:

ge(x)=exp⁡(we⋅x)∑e′exp⁡(we′⋅x),f(x)=∑ege(x) he(x).g_e(x) = \frac{\exp(w_e \cdot x)}{\sum_{e'} \exp(w_{e'} \cdot x)}, \qquad f(x) = \sum_{e} g_e(x)\, h_e(x).

如果每个专家都算,就没有省任何计算。关键是稀疏:只保留门控值最大的 top-kk 个专家并重新归一化,其余的专家根本不算。W22 笔记的例子:稠密的门控 [0.04, 0.8, 0.01, 0.15],取 top-2 后变成 [0, 0.84, 0, 0.16]。于是参数量和每个 token 的计算量脱钩:参数可以非常多,每个 token 只用到其中一小部分。

难点是负载均衡:如果门控总是选同一个专家,其他专家拿不到梯度、永远学不会。常见的做法是给每个专家设容量上限(例如容量因子 2:超出的 token 直接走残差连接跳过这一层),并在损失里加一项鼓励均衡的辅助损失。几个里程碑(数字引自 W22 笔记):

  • GShard(Lepikhin et al. 2021):每隔一个块把 MLP 换成 MoE,top-2。
  • Switch Transformer(Fedus et al. 2021):top-1,训练了 1.6 万亿参数的模型。
  • GLaM(Du et al. 2021):1.2 万亿参数、64 个专家、top-2,每个 token 只激活约 950 亿参数(约 8%);训练成本约为 GPT-3 的三分之一,zero-shot 和 one-shot 表现好于 GPT-3。

MoE 天然适合分布式:不同专家放在不同的机器上(专家并行,第 9 讲)。

8.2 检索增强(retrieval-augmented)

另一种思路:知识不必全塞进参数里,可以在需要时查。RAG(Lewis et al. 2020)用一个检索器(DPR:用两个 BERT 分别编码问题和文档,按点积检索)从维基百科里取出相关段落 zz,再让生成器(BART-large,约 4 亿参数)在 zz 和问题 xx 的条件下生成答案:

p(y∣x)=∑zp(z∣x) p(y∣z,x).p(y \mid x) = \sum_{z} p(z \mid x)\, p(y \mid z, x).

RETRO(Borgeaud et al. 2021)把检索做到了预训练里:每 32 个 token 的片段,从 2 万亿 token 的数据库里检索相近的片段,用交叉注意力读进来;70 亿参数,比 GPT-3 少 25 倍,语言建模表现很好。检索的好处是知识可以更新(换数据库即可)、来源可以追溯;第 2 讲的数据问题和第 16 讲的隐私问题都会因此有新的形态。注意这里又出现了 encoder(检索器)和交叉注意力——三种架构在系统层面是互补的。

W22 笔记的总结很中肯:这两条路都是为了”突破稠密 Transformer 的规模”;MoE 让参数可以到万亿级而计算不变,检索模型在知识密集型问答上最强;但它们能否在通用能力上和稠密模型相当,当时还没有定论。


9. 2023 年之后 · Since 2023

本讲的问题在 2023 年之后有了相当明确的答案,但细节一直在演进:

  • decoder-only 全面主导。在公开了架构的大模型里,几乎都是 decoder-only + 因果 LM(LLaMA 系列、Mistral、Qwen、DeepSeek、Gemma 等;GPT-4 等闭源模型没有公开架构)。指令微调、RLHF、长上下文、推理模型都建立在这个骨架上。T5 / UL2 一系在 2023 年还很活跃(Flan-T5、Flan-UL2 是当时常用的开放模型),此后在大规模生成模型里逐渐少见。
  • “现代 decoder”的标准配方。和第 1 讲的 base model 相比,2023 年的 LLaMA 做了几处如今已成标配的改动(2022 年的 PaLM 已经用了其中的多数,第 10 讲):
    • RMSNorm(Zhang & Sennrich 2019)替代 LayerNorm:只除以均方根、不减均值,更省、效果相当;仍然是 pre-norm。
    • SwiGLU(Shazeer 2020)替代 GELU MLP:带门控的 MLP,同样参数量下效果更好。
    • RoPE(旋转位置编码,Su et al. 2021)替代学出来的位置嵌入:把位置信息以”旋转”的方式乘进 q、k,使注意力分数只依赖相对位置,便于扩展上下文长度。
    • GQA(grouped-query attention,Ainslie et al. 2023):多个查询头共享一组键 / 值头(极端情形是 Shazeer 2019 的 multi-query attention),大幅缩小推理时的 KV cache。Llama 2 的大模型就用了它。
    • 更大的词表(Llama 3 的分词器约 12.8 万个 token)、数字拆成单个数位等分词上的改进(§1.2)。
    • 本讲 §3 的结构骨架——嵌入、注意力、MLP、残差、归一化、输出层——没有变。CS336 第 3–4 讲把这些变体讲得更细。
  • MoE 的回归。Mixtral 8x7B(2023 年底)每层 8 个专家、每个 token 选 2 个,总参数约 470 亿、每个 token 激活约 130 亿;DeepSeek-V2 / V3 用更细粒度的专家加共享专家,V3 总参数 6710 亿、每个 token 激活约 370 亿。2024–2025 年,很多旗舰开放模型都是 MoE。§8.1 的问题(负载均衡、专家并行、通信)成了大规模训练的核心工程问题。
  • 注意力的替代者。状态空间模型(Mamba,2023)和线性注意力把序列长度的代价从平方降到线性;纯替代者没有取代 Transformer,但”Transformer + Mamba”的混合架构(如 Jamba)出现在一些模型里。
  • encoder 并没有消失。检索增强生成(RAG)几乎离不开双向编码器做句子嵌入和重排序;ModernBERT(2024)等工作重新训练了更现代的 encoder。2025 年也有把 decoder-only 模型改造成 encoder-decoder 的尝试(如 Google 的 T5Gemma)。
  • 本讲那些”缺点”各自有了补丁。decoder 用不上右边的上下文(§5.3)?代码模型普遍用 fill-in-the-middle(FIM,Bavarian et al. 2022)训练:把”前缀、后缀、中间”重新排列,让因果模型学会填中间。训练信号不够密?多 token 预测(multi-token prediction,DeepSeek-V3 等)让每个位置同时预测后面几个 token。masked LM 不能生成?2025 年出现了 80 亿参数规模的掩码扩散语言模型(如 LLaDA):从全是 [MASK] 开始,一轮轮地把最有把握的位置填上——BERT 的目标以一种新的方式变成了生成模型。

哪些结论仍然成立?架构和目标可以分开看(UL2);训练信号的密度决定了预训练效率;掩码决定了模型能做什么;双向表示适合”读”——都还成立。被修正的主要是”哪种组合最好”:在”一个模型、所有任务、靠提示使用”的范式下,因果 decoder 成了默认答案。


10. 小结 · Summary

  1. 分词决定了 token 是什么。按空格切有开放词表问题;BPE 反复合并最常见的相邻对,没有 OOV;unigram 把分词当作概率模型。第 1 讲用字符级:词表小、透明,代价是序列长约 3 倍、注意力开销约 10 倍、上下文装得少(2.6 行 vs 8 行)。BPE 会把数字切得不一致。
  2. 上下文嵌入 ϕ\phi 统一了三种架构:encoder 双向地产生表示,decoder 单向地产生表示并预测下一个 token,encoder-decoder 双向地读、因果地写。Transformer 胜过 RNN,因为它可以在序列方向上并行、任意两个位置一步可达。
  3. Transformer 块 = pre-LayerNorm → 多头自注意力(q、k、v,除以 dh\sqrt{d_h},因果掩码)→ 残差 → pre-LayerNorm → MLP(4 倍宽、GELU)→ 残差。位置嵌入提供顺序;注意力是位置之间交换信息的唯一地方;残差流是贯穿全网的”共享黑板”;参数 ≈ 12Ld212Ld^2(base model 精确是 109,056,GPT-3 约 1746 亿)。
  4. 三种目标:GPT 的因果 LM(每个位置一道题);BERT 的 masked LM(15%、80/10/10、独立预测;NSP 后来被证明不必要);T5 的 span corruption(哨兵 + 片段,解码器自回归地写)和 BART 的去噪;prefix LM 介于中间。掩码决定能做什么,目标决定学到什么。
  5. 同算力的小实验:GPT 和 prefix LM 第 500 步就学会评分,BERT 要到 1500 步,T5 在 2500 步内一直停在”永远猜 ok”(最初误用自由生成给它打分,记成了 0——比较不同架构时评测协议必须相同);GPT 不能用右边的上下文生成、但能靠打分做填空,BERT 一次就能填对、却完全不会写。训练信号的密度(1000 / 533 / 137 / 约 100 每千位置)是主要原因。
  6. ELECTRA 让判别器在每个位置判断”换没换”,在论文里同算力下明显好于 BERT;但在迷你世界里,生成器很快变准、一半的替换无法分辨、有信息量的标签只占约 2%,判别器停在”语法检查”阶段——方法的收益依赖数据。
  7. UL2 把架构和目标分开,用 R / S / X 三类去噪器的混合和模式 token 统一了 T5 式与 GPT 式的训练。
  8. decoder-only 成为默认,原因是:生成是通用接口、每个 token 都是训练信号、简单、zero-shot 最好(Wang et al. 2022)、规模化的工程积累。encoder-decoder 在微调后的固定任务上仍有优势,双向编码器仍是检索和分类的首选。MoE 让参数和计算脱钩,检索让知识可以外置,两者都在 2023 年之后变得更重要。

11. 常见误区 · Common pitfalls

  1. “BERT 和 GPT 的区别是网络结构不同。” 它们的块几乎一模一样(嵌入、注意力、MLP、残差、LayerNorm);区别在注意力掩码和训练目标。同一组权重换个掩码就是另一种”架构”——只是需要重新训练来适应(代码 02:直接把 base model 改成双向,bit/字符变成 7.528)。
  2. “masked LM 也是语言模型,所以也能生成。” BERT 没有定义序列上的概率分布,也没有”下一个”的概念;硬凑的生成一行合法的都没有(代码 04)。它擅长的是对已经给出的输入做表示、打分和填空。
  3. “decoder-only 看不到右边,所以做不了填空。” 生成时确实看不到;但把候选填进去比较整段的概率,右边的信息就用上了(代码 04:16 个候选打分 1.00),代价是多次前向传播;FIM 训练则让它直接学会填中间。
  4. “除以 d\sqrt{d} 是为了让注意力更准确。” 它是为了让初始的分数尺度不随维度增长、softmax 不饱和、梯度传得回来(代码 02 演示 4)。训练好的模型可以自己学出很尖的注意力。
  5. “注意力是 Transformer 里最重要、参数最多的部分。” 每个块里 MLP 占约 2/3 的参数;注意力的特殊之处是它是位置之间交换信息的唯一通道。
  6. “位置嵌入只是锦上添花。” 自注意力本身对顺序不敏感;base model 去掉位置嵌入,bit/字符从 0.409 变成 5.022,几乎回到什么都没学。
  7. “T5 证明了 encoder-decoder 最好。” T5 表 2 的结论是在”预训练 + 微调”设定下;Wang et al. 2022 发现只预训练时,因果 decoder 的 zero-shot 最好。“哪个最好”取决于怎么用。
  8. “ELECTRA 一定比 BERT 省样本。” 论文在真实文本上是这样,但它依赖”生成器犯的错有信息量”。代码 05 的迷你世界里,ELECTRA 一直停在瞎猜。
  9. “MoE 模型的参数量就是它的计算量。” MoE 每个 token 只激活一小部分专家:GLaM 1.2 万亿参数、每个 token 约 950 亿;比较模型时要分清”总参数”和”激活参数”。
  10. “比较不同架构时,各自用最顺手的方式打分就行。” 打分协议本身会造成巨大差别:同一个 T5,在空位上只比较三个候选时是 0.47(“永远猜 ok”),要求它自由生成、整段完全匹配时是 0.00(代码 04)。不同架构要用相同的协议比较——都在空位上打分,或者都自由生成。
  11. “分词只是预处理,不影响模型能力。” 分词决定了序列长度(于是决定算力和上下文)、模型能不能看到数字的每一位、拼错一个字母时 token 序列变多少;不同分词器下的 token 级困惑度不能直接比较。

12. 练习 · Exercises

练习 1(手算 BPE) 语料是 “low low low lower newest newest”(按空格切成 6 个词,空格不参与合并)。手算前 3 次 BPE 合并(并列时取最早出现的一对),写出每一步之后的词表新增项。再用学到的规则给 “lowest” 分词。

参考答案

初始:l o w(×3)、l o w e r、n e w e s t(×2)。相邻对计数:l o 4、o w 4、w e 3(lower 1 次 + newest 2 次)、e r 1、n e 2、e w 2、e s 2、s t 2。

第 1 次:最多的是 l o 和 o w(各 4 次),l o 先出现 → 合并 lo。之后:lo w(×3)、lo w e r、n e w e s t(×2)。 第 2 次:lo w 4 次最多 → low。之后:low(×3)、low e r、n e w e s t(×2)。 第 3 次:计数 low e 1、e r 1、n e 2、e w 2、w e 2、e s 2、s t 2;最早出现的 2 次的对是 n e → ne。

“lowest”:l o w e s t → 先按规则 1 合并 lo → lo w e s t → 规则 2 合并 low → low e s t → 规则 3(n e)不适用。结果 [low, e, s, t]。没见过的词被拆成学过的片段加单个字符——没有 OOV。

练习 2(unigram 的似然) 用 W22 笔记的例子:词表 {ab, c},p(ab)=2/3p(\text{ab}) = 2/3,p(c)=1/3p(\text{c}) = 1/3。(a) “ababc” 的似然是多少?(b) 如果词表是 {a, b, c, ab},概率 1/8、1/8、1/4、1/2,“abc” 有哪几种切法、各自的概率是多少?Viterbi 会选哪种?

参考答案

(a) 23⋅23⋅13=427≈0.148\frac{2}{3} \cdot \frac{2}{3} \cdot \frac{1}{3} = \frac{4}{27} \approx 0.148(笔记里写的 4/9 是笔误,代码 01 演示 3 用精确分数核对过)。

(b) 两种:a | b | c,概率 18⋅18⋅14=1256\frac{1}{8} \cdot \frac{1}{8} \cdot \frac{1}{4} = \frac{1}{256};ab | c,概率 12⋅14=18\frac{1}{2} \cdot \frac{1}{4} = \frac{1}{8}。Viterbi 选 ab | c。注意 unigram 天然偏爱”更少、更长”的 token:每多切一刀就多乘一个小于 1 的概率。

练习 3(数参数) GPT-2 small:L=12L = 12,d=768d = 768,词表 50,257,上下文 1024,输出层和嵌入共享。用代码 02 演示 1 的公式估算它的参数量。嵌入表占多少?和我们的 base model(8.2%)、GPT-3(约 0.37%)比较。

参考答案

每块 12d2+13d=12×589,824+9,984=7,087,87212d^2 + 13d = 12 \times 589{,}824 + 9{,}984 = 7{,}087{,}872;12 块共 85,054,464。嵌入:50257×768=38,597,37650257 \times 768 = 38{,}597{,}376;位置:1024×768=786,4321024 \times 768 = 786{,}432;最后的 LayerNorm 2d=1,5362d = 1{,}536。合计约 1.244 亿——正是常说的 “124M”。

嵌入表(含位置)约 3,938 万,占约 32%。规模越小,嵌入表的占比越大:base model 8.2%(词表很小但模型更小),GPT-2 small 约 32%(词表大),GPT-3 约 0.37%(模型极大)。这也是为什么小模型上”大词表”的代价更明显。

练习 4(掩码的形状) 一个 encoder-decoder 模型,输入 5 个 token、输出 3 个 token(解码器输入以 [S] 开头)。分别写出编码器自注意力、解码器自注意力、交叉注意力的掩码矩阵形状和”可以看”的元素个数。如果把它改写成一个共享参数的 prefix LM(序列 = 输入 + [S] + 输出去掉最后一个),掩码是多大、有多少个”可以看”?

参考答案

编码器:5 × 5,全部可以看,25 个。解码器自注意力:3 × 3 的下三角,6 个。交叉注意力:3 × 5,全部可以看,15 个。

prefix LM:序列长 5 + 1 + 2 = 8([S] 加输出的前两个),前缀长 6(输入 + [S])。前缀内部 6 × 6 = 36 个;后面 2 个位置各自看前缀 6 个加上自己和左边的输出位置:第 7 个位置看 7 个、第 8 个看 8 个。共 36 + 7 + 8 = 51 个。两者的信息流几乎一样(输出能看到全部输入和自己左边的输出),区别是参数是否共享、输出看的是输入的哪一层表示。

练习 5(√d 的直觉) 用代码 02 演示 4 的设定:两个 dhd_h 维随机向量,各分量独立 N(0, 1)。(a) 证明点积的方差是 dhd_h。(b) 如果分量的方差是 σ2\sigma^2 而不是 1,应该除以什么才能让分数的方差变成 1?(c) 为什么”训练好的 base model 的注意力比均匀分布集中得多”和”要除以 dh\sqrt{d_h}“不矛盾?

参考答案

(a) q⋅k=∑tqtktq \cdot k = \sum_t q_t k_t。每一项 qtktq_t k_t 的均值是 0,方差是 E[qt2]E[kt2]=1E[q_t^2] E[k_t^2] = 1;各项独立,方差相加得 dhd_h。

(b) 每项方差变成 σ4\sigma^4,总方差 dhσ4d_h \sigma^4,要除以 σ2dh\sigma^2 \sqrt{d_h}。实践里初始化和 LayerNorm 让分量大致是单位方差,所以除以 dh\sqrt{d_h} 就够了。

(c) 缩放管的是训练开始时分数的尺度:避免一开始 softmax 就饱和、梯度传不回去。训练过程中,WQW_Q、WKW_K 可以学得很大,把需要的分数拉开——base model 训练后两层的注意力熵是 2.42 和 1.92 nat,远低于均匀分布的 3.58。缩放是”给优化一个好的起点”,不是限制模型最终能多尖。

练习 6(80/10/10 的算术) BERT 选 15% 的位置,按 80/10/10 处理。(a) 输入里被换成 [MASK] 的 token 占全部 token 的比例是多少?被换成随机词的呢?(b) 一个没有被换成 [MASK] 的 token,它其实是”被选中、需要预测”的概率是多少?这个数说明了什么?

参考答案

(a) [MASK]:15% × 80% = 12%;随机词:15% × 10% = 1.5%;被选中但保持原样:1.5%。

(b) 没被换成 [MASK] 的 token 占 88%,其中”被选中”的是 1.5% + 1.5% = 3%(随机替换 + 保持原样),条件概率 3% / 88% ≈ 3.4%。模型对任何一个看起来正常的位置,都不能百分之百确定”它是原词、不用管”——有约 1.7% 的概率它其实是被随机替换的错词。这迫使模型在每个位置上都检查”这个词放在这里合不合理”,从而对每个位置都保持有用的表示。ELECTRA 可以看成把这一点推到极致:让模型在每个位置都明确地做这个判断。

练习 7(训练信号的密度) 代码 03 演示 3 里,span corruption 每 1000 个位置有 187 个损失项,其中 103 个是哨兵。(a) 为什么哨兵这么多?(b) 如果把片段平均长度从约 1 改成 3(mean_span=3,T5 最终的设定),损坏比例仍是 15%,“内容”预测的数量和哨兵的数量会怎样变?

参考答案

(a) i.i.d. 地选 15% 的 token,大多数片段只有 1 个 token;每个片段在目标里要配一个哨兵,最后还有一个结束哨兵,所以目标里哨兵和内容差不多一样多,甚至更多。

(b) 预测:被挖掉的内容 token 总数大致不变(还是约 15%),片段数大约变成原来的 1/3,哨兵随之减少;目标变短,“内容 / 哨兵”的比例提高。

我用 span_batch(..., mean_span=3) 在副本里数了一下(同样每 1000 个位置):损失项 135,其中哨兵 61、内容 74;而 mean_span=1 时(换了一个随机数序列,所以和演示 3 差一两个)是 188、104、83。哨兵没有降到 1/3,因为每段末尾的结束哨兵总在、片段太长时会被窗口截断;内容也略少,因为随机放下的片段会互相重叠、换行符不挖,实际挖掉的比例低于 15%。T5 论文表 7 里平均长度 3 略好于 i.i.d.;同时目标更短,训练也更省计算(T5 论文在讨论 span corruption 时也提到了”目标序列更短”这个好处)。

练习 8(改一行:BERT 遮更多) 把 arch_tools.mlm_batch 的默认 rate=0.15 改成 0.40,删掉 checkpoints/l3_bert.pt,重跑代码 04。预测 BERT 的学习曲线(评分准确率)和探针结果会怎样变。

参考答案

预测:训练信号多了约 2.7 倍,平台期应该更短;但每个被遮住的词周围的上下文也更少了,任务更难。

我在一份代码副本里跑了(没有改动课程目录里的文件):BERT 的评分准确率 500 步 0.75、1000 步就是 1.00(原来是 0.47、0.73,1500 步才全对);探针 k = 32 从 0.802 升到 0.914,k = 128 从 0.943 升到 0.960;在自己目标上的准确率 0.79(原来 0.77)。在这个小实验里,多遮一些明显更好。这和 Wettig et al.(2022)“Should You Mask 15% in Masked Language Modeling?” 的发现方向一致:在他们的设定下,遮 40% 比 15% 更好。15% 是 BERT 的历史选择,不是什么最优值。(做完记得改回来、删掉缓存。)

练习 9(改一行:T5 训练更久) 代码 04 里 T5 在 2500 步内一直停在”永远猜 ok”。把 STEPS 改成 6000,只训练 T5,你预测会怎样?这说明了什么?

参考答案

我在代码副本里只训练了 T5(6000 步,学习率调度按 6000 步拉长,每 1000 步记一次,评测协议和正文相同):

  • 评分准确率从第 1000 步到第 6000 步的每个检查点都是 0.47(200 条评论上 0.45),始终是”永远猜 ok”,没有出现 BERT 那样的突变;
  • 它学会了往形容词的空里填形容词:第一步取最大,填出形容词的比例从 2500 步时的 0.00 升到 0.89;但正负对的只有 0.41,给 16 个候选打分是 0.48——仍是瞎猜;
  • 在自己的目标上继续进步:准确率 0.88(哨兵 1.00,被挖掉的词 0.73;2500 步时是 0.84、0.99、0.66);探针(k = 32)三种读法在 0.50–0.59 之间,解码器读法在 k = 128 时 0.623,略高于瞎猜;
  • 自由生成的习惯变了:在评分空位上 200 次全写成 “review : ok”。

这说明它学得非常慢,而且学会的顺序是先”这个空里该填哪类词”(6000 步时做到了),后”该填哪个意思”(还没做到)。在这个 2 + 2 层、64 维的小模型上,“按哨兵定位、再根据定位处周围的内容决定写什么”这条路径特别难学。可能的原因(没有一一验证):哨兵 <0> 几乎总是输入里第一个被挖掉的位置,模型可以靠”位置的统计规律”糊弄过去,而不真正去找 <0>;目标里一半是哨兵,有效信号更少;评论模板里很多词靠局部语法就能猜对,进一步降低了”必须看远处”的压力。真实的 T5 最小也有编码器、解码器各 6 层(T5-Small),在海量多样的文本上训练,这些捷径基本不存在。

练习 10(MoE 的账) GLaM 有 1.2 万亿参数、64 个专家、top-2 路由,每个 token 激活约 950 亿参数。(a) 激活参数占总参数的比例?(b) 如果每个专家是一个 MLP、且 MoE 层每隔一层出现一次,粗略地说,为什么激活参数远多于”总参数 × 2/64”?(c) MoE 省的是什么,不省的是什么?

参考答案

(a) 950 / 12000 ≈ 7.9%,即 W22 笔记说的约 8%。

(b) 2/64 ≈ 3.1% 只适用于专家(MLP)部分;注意力、嵌入、以及不是 MoE 的那些层的 MLP 是每个 token 都要用的”共享”参数,它们全部计入激活参数。所以激活比例高于 3.1%。

(c) 省的是每个 token 的计算量(FLOPs):参数可以加到万亿级,计算只按激活参数算,GLaM 的训练成本约为 GPT-3 的三分之一。不省的是内存和通信:所有专家的参数都要存在某处,token 要在机器之间路由到对应的专家(第 9 讲);推理时如果 batch 小,很多专家的权重都要读进来,硬件利用率可能不高。

练习 11(ELECTRA 的前提) 代码 05 里 ELECTRA 失败了。提出一种修改迷你世界或训练设置的办法,使”判断换没换”更需要正负知识,并预测结果。(提示:看演示 2 的三类替换。)

参考答案

几种思路(都没有在本讲的代码里实际运行,结果需要你自己验证):(1) 让生成器更弱或更早停止训练,使替换更多、更随机——但替换太随机时,判别器只需要学语法;(2) 直接构造”难负例”:把被遮住的形容词换成正负相反的形容词(这样每个替换都”改变了意思”),相当于把 ELECTRA 变成一个专门的对比任务;(3) 降低模板的确定性,让更多位置真正有多种可能。预测:方案 (2) 应该能让判别器学会正负,因为有信息量的标签比例从约 2% 提高到所有被替换的位置;但这也说明了问题——好的替换分布要靠设计,而真实文本里生成器的错误”免费”提供了丰富的难负例。


13. 延伸阅读 · Further reading

原课阅读(按建议顺序)

补充

2023 年之后

  • Touvron et al., LLaMA(2023)——“现代 decoder”配方的代表;Ainslie et al., GQA(2023);Jiang et al., Mixtral of Experts(2024);DeepSeek-AI, DeepSeek-V3 Technical Report(2024);Bavarian et al., Efficient Training of Language Models to Fill in the Middle(2022)。
  • 本目录下 CS336 的第 3–4 讲(架构细节与 MoE),以及 CS224N 的预训练一讲。

14. 下一讲预告 · Next lecture

本讲比较的是”同样的算力下,哪种架构和目标学得更好”。但真正让基础模型变强的,是把算力本身加上去:更多参数、更多数据、更长训练。损失怎样随这三者变化?给定预算,参数和数据应该怎么分?为什么 Chinchilla 推翻了 GPT-3 时代的分配?

第 4 讲:规模定律——损失怎样随参数、数据和算力变化。 本讲的参数公式 12Ld212Ld^2 正是估算训练算力 C≈6NDC \approx 6ND 的起点。那一讲会在迷你互联网上训练一系列不同大小的 tiny GPT,拟合幂律,画出 IsoFLOP 曲线,并看看外推能有多准。

← 返回课程目录