对应原课:CMU 11-667 Fall 2025 · Lecture 1 · Tue 26-Aug-2025 · Building blocks of modern LLMs(Jaromir Savelka、Yubin Kim) 原课 slides:Class Overview、Language Models Basics(课表页公开) 阅读材料:Jurafsky & Martin, Speech and Language Processing(第 3 版草稿)第 7 章;Jay Alammar, The Illustrated Transformer 配套代码:code/lecture01/(6 个模块 + 5 个演示脚本,全部在 CPU 上几秒内跑完) 本讲的写法:内容骨架来自原课的两份 slides;背景、动机、推导和实验是补充的。文中所有实验数字都来自配套代码在作者机器上的实际输出。
目录
- 0. 本讲导读
- 1. 课程概述:11-667 是一门什么样的课
- 2. 什么是语言模型
- 3. 从数数到神经网络:n-gram 与它的天花板
- 4. 有条件与无条件的语言模型
- 5. 六块积木:从文字到概率
- 6. 把积木拼起来:n-gram 与神经语言模型的正面比较
- 7. 解码:从概率回到文字
- 8. 小结、常见误区、练习
0. 本讲导读
0.1 这一讲要回答的问题
ChatGPT、Claude、LLaMA、Qwen……这些”大语言模型”(Large Language Model,LLM)看起来能写代码、能翻译、能聊天、能做数学题。它们内部到底在算什么?
这一讲给出一个可能让你意外的答案:它们都只是在反复做同一件事——给定前面的文字,算出”下一个 token 是什么”的概率分布,然后从里面选一个。整门课后面所有的内容(Transformer、预训练数据、微调、RLHF、RAG、推理加速……)都是在这一件事上做文章:怎样把这个分布算得更准、算得更快、让它更有用、更安全。
所以第 1 讲的任务是把这件”唯一的事情”拆开来看清楚。原课 slides 把它画成一条流水线:
文字 ──► 分词 ──► 嵌入 ──► 神经网络 ──► logits ──► softmax ──► 解码 ──► 文字
Tokenization Embedding Neural Net Logits Softmax Decoding
(预处理) ("魔法") (输出层)
我们会一块一块地把这六块积木拆开、用代码从零实现、再拼回去。
0.2 学完这一讲你应该能
- 说清楚”语言模型”的数学定义,并用链式法则手算一句话的概率;
- 解释 n-gram 模型为什么是”数数”,以及为什么它注定会遇到稀疏性的天花板;
- 解释有条件和无条件语言模型的区别,以及怎样用”损失掩码”互相转换;
- 画出”分词 → 嵌入 → 网络 → logits → softmax → 解码”这条流水线,说出每一步的输入输出形状;
- 推导 softmax + 交叉熵的梯度 ,解释困惑度的含义;
- 比较贪心、束搜索、温度、top-k、top-p 等解码策略的取舍,理解”长尾问题”和”太高概率反而不好”。
0.3 本讲用到的”花园世界”
为了让每个概念都能精确地验证,配套代码造了一门小语言,叫”花园世界(Garden World)“。slides 上的例句 I eat the apple from my own garden 就是它的一个合法句子:
I eat the apple from my own garden .
she picks her ripe plum .
the children feed the hungry goat every day .
这门语言有 87 个词,句子结构是”主语 + 动词 + 限定词 + [形容词] + 名词 + [尾巴短语] + 句号”,里面藏着几条规则:主谓一致(she eats)、物主代词和主语一致(she … her)、动词决定名词类别(eat 后面是水果或蔬菜,feed 后面是动物)、尾巴里的物主代词还要和句首主语一致(she picks the rose from her own garden)。
它最大的好处是:我们知道它的真实概率分布。任意一句话的真实概率、任意一个前缀之后下一个词的真实分布、一句模型生成的话合不合语法,都能精确算出来。真实的英语做不到这一点。这样,“模型学到了多少""解码策略的质量好不好”都能被准确测量,而不是凭感觉。(代码:toy_world.py)
1. 课程概述:11-667 是一门什么样的课
1.1 定位:LLM 的”全景图”
11-667(Large Language Models: Methods and Applications)是 CMU 语言技术研究所(LTI)的研究生课程,2023 年秋季首次开设(Chenyan Xiong、Daphne Ippolito 主讲),2025 年秋季由 Jaromir Savelka 和 Yubin Kim 接手。课程描述把它定位为对 LLM 现状的一个整体性介绍:前半学期讲基础(网络结构、训练、推理、评估),再讲可解释性、对齐、涌现能力和各种应用;后半学期讲怎样把预训练做大、怎样让训练和部署更高效、部署 LLM 带来哪些担忧,最后是前沿挑战。
所以这门课宽而不太深:几乎每个和 LLM 有关的主题都会讲到,但每个主题只讲核心思想、关键论文和动手实验。它的目标读者是”想做 LLM 研究或应用、需要一张完整地图的研究生”。如果你想在某个方向深挖,CMU 还有更专门的课(只讲系统的 11-868、只讲应用的 11-766),本仓库也有更深入的 CS336(从零构建语言模型)和 CMU10-423(生成式 AI)。
1.2 学习目标
原课列了 8 条学习目标,概括起来是三类能力:
- 理解:解释 LLM 论文里的常用术语(对齐、规模定律、RLHF、提示工程、指令微调……);读懂近期的 LLM 论文;观察预训练阶段的决定怎样影响下游任务。
- 动手:用 PyTorch 从零实现并训练一个神经语言模型;用开源库对预训练模型做微调和推理。
- 应用与判断:比较 LLM 生态里的不同模型,为任务选出最合适的;把 LLM 用到各种下游应用;设计利用现有大模型的新方法。
注意第一条”为任务选模型”:这要求你对模型的结构差异(第 2 讲)、训练数据(第 3 讲)、评估方法(第 5 讲)、适配方式(第 6 讲)都有概念。这正是这门课”宽”的原因。
1.3 课程结构与作业
| 组成 | 比例 | 内容 |
|---|---|---|
| 作业 | 40% | HW0(申请 AWS 账号,1%);HW1 预训练数据的探索与清洗(9%);HW2 从零实现 Transformer + 分词 + 用 HuggingFace 做推理(10%);HW3 检索增强生成 + 工具调用(10%);HW4 提高训练效率(10%) |
| Mini-project | 20% | 1–3 人一组,自选任务,最后两次课上展示 |
| 期中、期末考试 | 各 20% | 课堂上进行 |
| 课堂参与 | 最多 +3% |
原课对学生的期待:Python 必须熟练;最好会 PyTorch;每周大约 9 小时做作业;上过 NLP 或 ML 课程,或者愿意花额外时间自学。本讲义的前置讲 P1–P3 就是为后一种情况准备的。
1.4 关于”用 AI 写作业”的规定
这门课对使用 LLM 的规定很有代表性,值得单独说一下(意译):
- 用了 ChatGPT、Copilot 等工具必须明确说明,否则算学术不端;
- 如果用 AI 生成了作业的任何部分,要提交你用的提示(prompt)和输入;
- 评分同时看答案的正确性和提示的质量——把题目原样贴给模型不算”智力投入”,生成结果里的错误照样扣分。
这条规定背后的态度是:LLM 是工具,会用工具是一种能力,但”会用”意味着你理解问题、能设计好的提示、能检查输出。这和整门课的精神一致:我们要理解这些模型,而不是把它们当黑盒。
本讲义的练习都附有参考答案(默认折叠)。建议先自己做,再展开对照。
1.5 LLM 生态的一张速写
在进入技术细节之前,先给后面的内容一个”坐标系”。今天的 LLM 可以从几个维度来区分:
| 维度 | 典型选项 | 在哪一讲展开 |
|---|---|---|
| 结构 | decoder-only(GPT、LLaMA、Claude)、encoder-only(BERT)、encoder-decoder(T5) | 第 2、4 讲 |
| 训练阶段 | 预训练(海量文本上预测下一个 token)→ 指令微调 → 偏好对齐(RLHF / DPO) | 第 2、3、6、18 讲 |
| 规模 | 从几亿到上万亿参数;稠密 vs 稀疏(MoE) | 第 12、22 讲 |
| 开放程度 | 开放权重(LLaMA、Qwen、Mistral)vs 只能通过 API 使用 | 贯穿全课 |
| 使用方式 | 直接提示、少样本、微调、RAG、工具调用、智能体 | 第 6–11、20 讲 |
无论哪一种,它们最底层都是同一条流水线。下面开始拆它。
2. 什么是语言模型
2.1 定义
原课 slides 的定义是:语言模型(language model,LM)计算一个语言序列(词或 token)的似然(likelihood);在操作上,它表现为给定序列中前面的 token,输出下一个 token 的概率分布。
写成数学:设一段文字被切成 token 序列 ,语言模型给出
而它实际计算的是一串条件分布
在讲分词之前,先假设”token = 词”(slides 也是这么做的,§5.1 再回来讨论)。
为什么要给语言算概率? 初看这件事有点奇怪——我们要的是会写文章、会回答问题的系统,为什么要关心”一句话的概率是多少”?有三个原因:
- 打分:概率告诉你哪句话”更像人话”。语音识别里,“recognize speech” 和 “wreck a nice beach” 发音几乎一样,语言模型告诉系统前者更可能;机器翻译、拼写纠错也是这样。这是语言模型最早的用途。
- 生成:既然能算出”下一个词”的分布,就能从里面抽一个词、接在后面、再算下一个……一句话就”生成”出来了(§7)。
- 通用接口:几乎所有语言任务都能写成”给定前面的文字,续写后面的文字”——问答是”问题 → 答案”,翻译是”原文 → 译文”,分类是”文本 + 这段话的情感是 → 正面”。一个足够好的语言模型因此就是一个通用的任务求解器。这是 GPT-3(2020)以来整个领域的核心发现,也是为什么”预测下一个词”这样朴素的目标能训练出看起来很聪明的系统。
2.2 链式法则:从”下一个词”到”整句话”
为什么”预测下一个 token”就足以给整个序列算概率?因为概率论的链式法则(chain rule)对任何联合分布都成立:
这里 是 的简写。注意这不是近似,是恒等式——它只是把”同时发生”拆成”一件接一件发生”。
代码 01 §1 在花园世界里逐步计算 slides 上那句话的概率(真实分布,每一步的概率都是精确的):
| t | 累计 | |||
|---|---|---|---|---|
| 1 | I | 0.2200 | −1.514 | −1.514 |
| 2 | eat | 0.3500 | −1.050 | −2.564 |
| 3 | the | 0.6000 | −0.511 | −3.075 |
| 4 | apple | 0.1477 | −1.913 | −4.988 |
| 5 | from | 0.2500 | −1.386 | −6.374 |
| 6 | my | 1.0000 | 0.000 | −6.374 |
| 7 | own | 1.0000 | 0.000 | −6.374 |
| 8 | garden | 1.0000 | 0.000 | −6.374 |
| 9 | . | 1.0000 | 0.000 | −6.374 |
| 10 | <eos> | 0.3000 | −1.204 | −7.578 |
整句概率 。几个值得注意的地方:
- 第 6–9 步的概率都是 1:一旦说了 from,后面必然是 my own garden .,而且 my 由句首的主语 I 决定。语言里大量的词其实是”几乎确定的”,真正的不确定性集中在少数几个位置(选哪个主语、哪个动词、哪种水果)。
- 我们用 log 概率而不是概率:十几个小于 1 的数相乘很快就小到计算机表示不了(一篇 1000 词的文章,概率可能是 ,远小于双精度浮点数能表示的最小正数约 )。取对数把乘法变成加法,既不会下溢,也更好求导。
- 最后的 <eos>(end of sequence):语言模型还要决定”什么时候停”。没有结束符,模型就只能给”前缀”算概率,没法区分 “I eat the apple .” 是一篇完整的文档还是一篇更长文档的开头。
再看一句不合法的话 I eats the apple .:第 2 步 ,于是整句概率为 0,log 概率为 。只要有一步不可能,整句就不可能——这一点在 §3 讨论 n-gram 的”零概率问题”时会变得非常要命。
2.3 语言模型是一个”分布”
链式法则还有一个推论:只要每一步的条件分布都是合法的概率分布(非负、加起来为 1),并且有结束符,那么所有可能序列的概率加起来恰好等于 1。也就是说,语言模型定义的是”所有可能文本”上的一个概率分布。
真实语言有无穷多个句子,这件事没法直接验证。但花园世界是有限的,代码 01 §2 把它的全部句子枚举了出来:
- 合法句子一共 30336 个,概率之和 = 1.000000000000;
- 句子分布的熵 nat(13.16 bit),相当于在约 个等可能的句子里挑一个;
- 句子平均 7.21 个词,平均每个词的熵是 1.265 nat。
最可能的 5 个句子是 the children feed the cat .(0.00309)、I eat the apple .(0.00307)、he feeds the cat .、they feed the cat .、the children feed their cat .。最不可能的是 the children grow their red quince with their friends .,概率只有 。
更重要的是分布的形状:概率不小于 0.001 的句子只有 42 个,它们加起来只占 6.1% 的概率;其余 93.9% 的概率散落在 30294 个”长尾”句子上。每一个长尾句子都很罕见,但它们合起来才是语言的主体。这是自然语言的普遍特征(Zipf 定律),也是后面两个问题的根源:n-gram 的稀疏性(§3)和解码时的”长尾问题”(§7.4)。
最后,代码用蒙特卡洛做了一个检查:按真实分布采样 10 万句,最常见的三句的采样频率(0.0031、0.0031、0.0028)和精确概率一致。10 万次采样里一共出现了 17953 种不同的句子——还不到全部的 60%,长尾就是这么”长”。
2.4 怎样衡量一个语言模型的好坏:交叉熵与困惑度
如果真实分布是 ,模型是 ,一个自然的衡量方式是:在真实文本上,模型平均给每个 token 多少概率?用负对数来度量,就是交叉熵(cross-entropy):
实际中用测试集上的平均值来估计它,也就是平均每个 token 的负对数似然(negative log-likelihood,NLL)。它的指数叫困惑度(perplexity):
困惑度的直觉是”模型平均每一步在几个等可能的选项之间犹豫”:如果模型每一步都在 个词之间均匀地猜,困惑度就是 。一个对 个词完全均匀猜测的模型,困惑度恰好是 。
交叉熵有一个重要的性质:,等号当且仅当 。也就是说,真实分布自己的熵是任何模型都无法超越的下界。在花园世界里这个下界可以算出来:测试集(1000 篇文档、24832 个 token)上真实分布的困惑度是 3.655。后面所有模型都拿这个数作参照。
同一个测试集上,困惑度只能在相同分词方式的模型之间比较:把文字切得更碎,每个 token 更容易猜,但 token 数更多。第 5 讲会详细讨论。
3. 从数数到神经网络:n-gram 与它的天花板
3.1 马尔可夫假设与 n-gram
怎样估计 ?最古老的办法是:只看最近的 个词,把更早的历史忽略掉。这叫马尔可夫假设:
这样的模型叫 n-gram 语言模型。n = 2 叫 bigram,n = 3 叫 trigram。slides 的例子:在语料库 C 里,“I eat the” 后面跟 “apple” 的比例是 2%,那么 4-gram 模型就说 。
怎么得到这个数?数数:
为什么”数数”是对的? 这个比值恰好是最大似然估计(maximum likelihood estimate,MLE):在所有可能的条件分布里,它是让训练语料出现概率最大的那一个。直觉上也很好理解——如果你看到某个硬币 100 次里有 70 次正面,最”符合数据”的估计就是 。对每个上下文分别做这件事,就是 n-gram 模型。训练只需要扫一遍语料。
代码 01 §3 在 8000 篇训练文档上数出了 “I eat the” 后面的词:
| 词 | 计数 | 4-gram MLE | 真实概率 |
|---|---|---|---|
| apple | 208 | 0.1606 | 0.1477 |
| red | 122 | 0.0942 | 0.0700 |
| ripe | 97 | 0.0749 | 0.0700 |
| sweet | 94 | 0.0726 | 0.0700 |
| carrot | 90 | 0.0695 | 0.0677 |
| pear | 81 | 0.0625 | 0.0738 |
| plum | 72 | 0.0556 | 0.0492 |
| fresh | 60 | 0.0463 | 0.0450 |
“I eat the” 出现了 1295 次,估计已经相当接近真实概率了。但真实语言里它后面可以接 32 种词,训练集里只见过 31 种——缺的是 onion。这是我们故意的:为了做实验,训练语料刻意拿掉了 5 种”动词-名词”搭配(pick + plum、grow + tulip、feed + goat、eat + onion、water + daisy),模拟”训练数据恰好没覆盖到”。于是 MLE 断言 ,尽管它的真实概率是 0.0135。
3.2 稀疏性:n-gram 的根本缺陷
故意拿掉的搭配只是一个极端的例子。即使不做手脚,n 越大,没见过的 n-gram 就越多。代码 01 §4 统计了测试集的 n-gram 中”训练集里从没出现过”的比例:
| 训练文档数 | n=1 | n=2 | n=3 | n=4 | n=5 | n=6 |
|---|---|---|---|---|---|---|
| 100 | 0.18% | 6.79% | 23.44% | 43.83% | 63.04% | 75.64% |
| 1000 | 0.18% | 0.67% | 5.26% | 16.01% | 30.79% | 48.50% |
| 8000 | 0.18% | 0.36% | 1.11% | 3.42% | 8.67% | 19.66% |
两个趋势一目了然:数据越多,没见过的越少;但 n 每加 1,没见过的比例就成倍上升。原因是组合爆炸:词表有 个词,可能的 n-gram 有 种,而语料的长度只是线性增长。花园世界只有 87 个词;真实语言的词表有几十万,n = 5 时可能的组合是 量级——任何语料都覆盖不了。
后果是灾难性的:MLE 给没见过的 n-gram 的概率是 0,而测试集里只要有一个这样的 token,整个测试集的概率就是 0,困惑度是 。代码里 1 到 6 阶的 MLE 模型,测试集困惑度全部是无穷大。连 unigram 都不例外:goat 只能被 feed,而 feed + goat 被拿掉了,所以训练集里 goat 一次都没出现过。
这就是 n-gram 的两难:
- n 小:数据充足,但上下文太短。看不到句首的主语,就没法决定尾巴里该用 my 还是 her。
- n 大:上下文够长,但绝大多数上下文都没见过,估计不出来。
3.3 平滑:给没见过的事情留一点概率
几十年里,研究者发明了很多”平滑”(smoothing)技巧来缓解零概率问题。代码实现了两种最基础的:
- 加 k 平滑(add-k):每个计数都加上 ,。简单粗暴:没见过的词都分到一点点概率。
- 插值(interpolation,Jelinek–Mercer):把 0 阶(均匀分布)到 n 阶的 MLE 模型按权重 混合,。高阶模型”说得出就说”,说不出(上下文没见过)就由低阶模型兜底。权重 在验证集上用 EM 算法拟合——不能在训练集上拟合,因为训练集上最高阶的 MLE 永远是”对的”(它就是照着训练集数出来的),EM 会把全部权重给它。
代码 04 §1 的测试集困惑度(真实下界 3.655):
| 阶数 n | MLE | 加 k(k = 0.01) | 加 k(k = 1) | 插值 |
|---|---|---|---|---|
| 1 | ∞ | 44.223 | 43.856 | — |
| 2 | ∞ | 5.719 | 5.857 | 5.688 |
| 3 | ∞ | 4.198 | 4.910 | 4.115 |
| 4 | ∞ | 4.572 | 6.999 | 4.083 |
| 6 | ∞ | 8.135 | 18.421 | 4.064 |
几个值得注意的现象:
- 加 k 在 3 阶最好,阶数再高反而变差:上下文越长越稀疏,大量上下文只出现过一两次,加 k 把可观的概率均匀地撒给了 90 个词(大部分是不可能的词)。 时这个问题更严重。
- 插值随阶数单调变好:6 阶插值学到的权重是(均匀 0.003,1 阶 0.000,2 阶 0.030,3 阶 0.563,4 阶 0.209,5 阶 0.090,6 阶 0.105)——大部分权重给了”既不太稀疏又有足够上下文”的 3、4 阶,6 阶只在见过的时候帮一把。
平滑能把困惑度从 ∞ 拉回到 4.06,但它治标不治本:n-gram 把每个上下文当成一个孤立的”格子”。“my friend grows the” 和 “she grows the” 对 n-gram 来说是两个毫无关系的字符串,前者的统计无法帮助后者。
3.4 神经语言模型:用向量代替格子
2003 年,Bengio 等人的论文 A Neural Probabilistic Language Model 提出了另一种思路:
- 给每个词一个实数向量(嵌入,embedding),意思相近的词向量也相近;
- 用一个神经网络,把”最近 个词的向量”映射成”下一个词的概率分布”;
- 词向量和网络参数一起,用最大似然训练。
关键在第 1 点。如果训练数据里有 “she grows the rose”,模型学到 she 和 my friend 的向量很像(它们后面都接 grows、picks、waters 这类加 s 的动词),那么即使从没见过 “my friend grows the”,网络算出来的分布也会和 “she grows the” 相似。用论文的话说,这是用分布式表示(distributed representation)来对抗维度灾难(curse of dimensionality):模型不再需要见过每一个上下文,只需要见过”相似的”上下文。
这就是 slides 上 “N-gram versus Neural Models” 两页对比的意思:n-gram 用计数估计条件概率;神经语言模型把 “I eat the” 送进一个网络,网络输出 apple、banana 等词的概率。§6 会用实验精确地测量这个差别到底有多大——以及它的局限。
一点历史:用概率模型研究语言可以追溯到 Shannon(1948、1951),他用 n-gram 近似生成”像英语的文字”,并估计了英语的熵。n-gram 模型在 1980–2010 年代统治了语音识别和机器翻译。神经语言模型在 Bengio et al.(2003)之后沉寂了一段时间(太慢),直到 2010 年前后 RNN 语言模型(Mikolov et al., 2010)和 word2vec(2013)让它复兴;2017 年的 Transformer 和随后的 GPT(2018)、BERT(2019)、GPT-3(2020)、ChatGPT(2022)一路把它推到今天的规模。今天的 LLM 和 2003 年的模型在结构上是同一个家族:嵌入 → 网络 → 下一个词的分布。
4. 有条件与无条件的语言模型
4.1 两种设计
slides 区分了两种神经语言模型:
| 无条件(unconditioned) | 有条件(conditioned) | |
|---|---|---|
| 每一步预测 | ,多了一个”条件序列” | |
| 例子 | GPT 系列、LLaMA | T5、机器翻译模型 |
| 别名 | — | seq2seq(序列到序列)模型 |
有条件模型通常有一个编码器读入 (比如英文原文),一个解码器逐词生成 (法文译文),解码器的每一步都能”看”编码器的结果。无条件模型只有一个解码器,一切都是”续写”。
4.2 两者可以互相转换
slides 的一个重要论点是:理论上,为无条件模型设计的任务都能改写成有条件模型的任务,反之亦然。
把有条件任务交给无条件模型:把条件拼到前面,变成一段续写。比如翻译:
English: she picks her ripe plum . French: elle cueille sa prune mûre . <eos>
无条件模型对这整段文字建模。用链式法则就能看到:
代码 01 §5 用花园世界的真实分布验证了这个恒等式:前缀 the children feed,续写 the hungry goat every day .,,和”只对续写部分逐词求和”的结果完全相同。
训练时怎样让无条件模型只学 ,而不去学”怎样写英文”?用损失掩码(loss mask):
token: English: she picks her ripe plum . French: elle cueille sa prune mûre . <eos>
掩码 : 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1
只在掩码为 1 的位置计算损失。模型仍然”读”到了英文(作为上下文),但只因为法文预测得好不好而被奖惩。今天的聊天模型就是这样训练的:用户的提问是条件(掩码 0),助手的回答是目标(掩码 1)。第 6、20 讲会再见到它。
把无条件任务交给有条件模型:把序列切成两半,前半段当作编码器的输入,后半段当作解码器的目标。T5 的预训练目标(span corruption,第 2 讲)就是这种思路的变体。
4.3 为什么今天几乎都是无条件(decoder-only)模型
既然两者能力上等价,为什么 GPT、LLaMA、Claude 这些主流模型都是无条件的?简单地说:结构更简单(一套参数、一种注意力掩码);所有文本都能直接拿来训练,不需要成对的”输入-输出”;训练时每个位置都产生损失,数据利用率高;而且”一切皆续写”的接口最通用。代价和细节是第 2 讲的内容(Wang et al., 2022 的对比实验)。
slides 用一张图总结:输入序列 → 语言模型 → 目标序列,有时输入为空(纯生成),有时目标就是输入的延续(续写),有时两者是不同的东西(翻译、问答)——在语言模型眼里,这些都是同一件事。
5. 六块积木:从文字到概率
现在打开语言模型这个”黑盒”。下图是 slides 上的流水线,标出了每一步的数据形状(以批大小 、序列长度 、词表大小 、嵌入维度 为例):
"I eat the"
│ ① 分词 Tokenization 文字 → token 序列 → id 序列
▼
[I, eat, the] → [3, 13, 9] 形状 (B, T),整数
│ ② 嵌入 Embedding 每个 id 查嵌入矩阵 E 的一行
▼
3 个 d 维向量 形状 (B, T, d)
│ ③ 神经网络 Neural Network (第 1 讲把它当黑盒,第 2 讲打开)
▼
输出嵌入 o("预测出来的下一个词的向量") 形状 (B, d)
│ ④ logits o 与词表里每个词的嵌入做点积
▼
每个词一个分数 形状 (B, V)
│ ⑤ softmax 分数 → 概率
▼
p(apple)=0.148, ... 形状 (B, V),每行和为 1
│ ⑥ 解码 Decoding 从分布里选一个词
▼
"apple"
训练时,第 ⑤ 步之后不是解码,而是损失函数:拿正确答案的概率算 。下面逐块讲。
5.1 积木 ①:分词
分词器(tokenizer)把文字变成离散的 token 序列;词表(vocabulary)是所有可用 token 的列表,每个 token 有一个编号(id)。神经网络只认识 id。
slides 用 “A hippopotamus ate my homework.” 演示了三种粒度:
| 粒度 | 切分结果 | 长度 |
|---|---|---|
| 字符级 | A, , h, i, p, p, o, … , k, . | 31 |
| 子词级 | A, hip, ##pop, ##ota, ##mus, ate, my, homework, . | 9 |
| 词级 | A, hippopotamus, ate, my, homework, . | 6 |
(## 是 BERT 的 WordPiece 分词器的写法,表示”接在前一个片段后面,中间没有空格”。)
为什么不直接用词? 词级分词的序列最短,看起来最自然,但有两个致命问题:
- 词表爆炸:英语有几十万个词形(eat、eats、eating、eaten……),加上人名、地名、拼写错误、代码、其他语言,词表永远不够大。词表大,嵌入矩阵和输出层()就大。
- 没见过的词(out-of-vocabulary,OOV):词表外的词只能变成一个特殊的
<unk>,信息永久丢失。
为什么不直接用字符? 字符级(或字节级)永远不会遇到没见过的词,词表也很小;但序列太长了——同一段文字长 4~5 倍。第 2 讲会看到,Transformer 注意力的计算量随序列长度平方增长,上下文窗口也按 token 计,序列变长意味着又慢又”看得少”。而且每个字符本身几乎不携带意义,模型要花更多的层数去”拼出”词义。
子词(subword)是折中:常见的词是一个 token,罕见的词拆成几个常见片段。最常用的算法是 BPE(byte pair encoding,Sennrich et al., 2016;源自 Gage 1994 的压缩算法):
- 从最小单位出发(字符,或者 UTF-8 字节);
- 统计语料里每一对相邻 token 出现的次数;
- 把出现最多的那一对合并成一个新 token,记下这条”合并规则”;
- 重复,直到词表达到目标大小。
编码新文字时,按学到的顺序反复应用合并规则。
代码 02 从零实现了字符级、词级、字节级和字节级 BPE 四种分词器,在一段原创英文短文加 200 篇花园世界文档上训练。BPE 的前几次合并是 h+e → he(1139 次)、▁+t → ▁t、▁t+he → ▁the……(▁ 表示空格;和 GPT-2 一样,空格算在后一个词的开头)。高频片段最先被合并,the 第三步就成了一个 token。slides 上那句话的切分:
| 分词器 | token 数 | 结果 |
|---|---|---|
| 字符级 | 31 | A ▁ h i p p o … |
| 词级 | 6 | <unk> <unk> <unk> my homework . |
| 字节级 | 31 | 同字符级(全是 ASCII) |
| BPE(500 次合并) | 12 | A ▁hippo p ot am u s ▁at e ▁my ▁homework . |
词级分词器没见过 A(训练文字里只有小写的 a)、hippopotamus 和 ate,全变成了 <unk>。BPE 则把 homework 认成一个 token,把罕见的 hippopotamus 拆成 ▁hippo + 几个片段——训练文字里出现过 hippo(河马),所以这个片段被学到了。合并 0、50、200、500 次时,hippopotamus 分别被切成 13、12、10、6 个 token。模型最终看到的只是一串整数:[65, 515, 112, 498, 587, 117, 115, 575, 101, 274, 734, 46]。
在一段训练时没见过的英文(275 个字符、57 个词和标点)上,四种分词器的取舍(代码 02 §3):
| 分词器 | 词表大小 | token 数 | 每词 token 数 | <unk> 数 | 无损还原? |
|---|---|---|---|---|---|
| 字符级 | 41 | 275 | 4.82 | 0 | 是 |
| 词级(全部词) | 211 | 57 | 1.00 | 21 | 否 |
| 词级(前 50 个词) | 51 | 57 | 1.00 | 44 | 否 |
| 字节级 | 256 | 275 | 4.82 | 0 | 是 |
| BPE 50 次合并 | 306 | 216 | 3.79 | 0 | 是 |
| BPE 200 次合并 | 456 | 156 | 2.74 | 0 | 是 |
| BPE 500 次合并 | 756 | 104 | 1.82 | 0 | 是 |
这张表就是分词的全部取舍:词表越大,序列越短;词级的序列最短,但 57 个词里有 21 个变成了 <unk>;BPE 在两者之间连续可调,而且(字节级 BPE)永远无损、永远没有 <unk>。
换一种语言会怎样? “河马吃了我的作业。“有 9 个字符,UTF-8 编码后是 27 个字节(每个汉字 3 个字节)。在英文上训练的 BPE 没有学到任何中文片段,于是退化成字节级:27 个 token。同样的意思,中文要花 3 倍的 token——上下文窗口装得更少、按 token 计费的 API 更贵。这叫”分词税”,真实的多语言模型通过在多语言语料上训练分词器来缓解它(第 3 讲)。
花园世界里为什么直接用词作 token? 因为它的词表是封闭的(87 个词),不存在没见过的词,词级分词的缺点在这里不会出现,而且一个 token 就是一个词,方便阅读。真实语言是开放的,所以所有 LLM 都用子词。
【LLM 联系】 GPT-2 的字节级 BPE 词表有 50257 个 token,Llama 3 是 128256 个。分词也解释了 LLM 的一些”怪癖”:模型看到的是 ▁homework 这样的整块,而不是 h-o-m-e-w-o-r-k,所以”这个单词里有几个 r”这类问题对它来说出奇地难。
5.2 积木 ②:嵌入
为什么要把 token 变成向量? 神经网络的运算是加法和乘法,id 3 和 id 13 这两个整数之间的大小关系毫无意义(I 不比 eat “小”)。最朴素的表示是 one-hot 向量:长度为 ,第 个位置为 1,其余为 0。但 one-hot 向量两两正交,任何两个词的”相似度”都是 0——apple 和 pear 和 goat 一样不相干。这正是 n-gram 的问题。
嵌入的做法是:建一个 的矩阵 (嵌入矩阵),第 行就是第 个 token 的 维向量。这些向量是可学习的参数,训练会把它们调整到”有用”的位置——在预测下一个词时扮演相似角色的词,向量会靠近。
查表 = one-hot 乘矩阵:。数学上,嵌入层是一个”输入为 one-hot 的线性层”;实现上,没有人真的去乘,因为直接取第 行快得多。代码 03 §1:、 时,one-hot 乘法要做 360 次乘加(其中 356 次乘的是 0),查表只要复制 4 个数;PyTorch 的 nn.Embedding 做的就是查表。
嵌入矩阵的参数量是 :
| 模型 | 嵌入参数 | ||
|---|---|---|---|
| 本讲的花园世界模型 | 90 | 32 | 2,880 |
| GPT-2 small | 50,257 | 768 | 38,597,376 |
| Llama 3 8B | 128,256 | 4,096 | 525,336,576 |
光是一张嵌入表就有 5 亿参数——这是词表不能无限变大的另一个原因。
嵌入真的学到了”相似”吗?§6.4 会给出实验:训练好的模型里,同类词(水果、蔬菜、花、动物、动词……)的嵌入平均余弦相似度明显高于异类。
5.3 积木 ③:神经网络
slides 在这一步写的是 “Magic”(魔法):第 1 讲先把它当成黑盒,只关心它的接口——输入每个 token 的向量,一层层地产生新的向量(称为隐藏状态,hidden states),最后产生一个”输出嵌入”,代表要生成的下一个 token。从第 2 讲开始,这个黑盒就是 Transformer。
配套代码用最简单的一种网络——Bengio et al.(2003)式的固定窗口多层感知机(neural_lm.py):
最近 C=5 个 token 的嵌入(5 × 32)
→ 首尾拼接成一个 160 维向量
→ 线性层 + tanh,得到 128 维的隐藏状态
→ 线性层,投影回 32 维:输出嵌入 o
代码 03 §6 把 “my friend grows the” 走了一遍,打印出每一步的形状:最近 5 个 token [<bos>, my, friend, grows, the] → id [0, 11, 12, 18, 9] → 嵌入 (1, 5, 32) → 隐藏状态 (1, 128) → 输出嵌入 (1, 32) → logits (1, 90) → 概率(和为 1)。文档开头不够 5 个词时,左边用特殊符号 <bos>(beginning of sequence)补齐。
这个网络只有 27706 个参数,它的局限也很明显:只看最近 5 个 token。尾巴 from ___ own garden 里的物主代词要和句首主语一致,主语一旦滑出窗口,模型就看不见了。§6.5 会测量这个代价,第 2 讲的注意力机制就是为了解决它。
5.4 积木 ④:logits
网络给出了输出嵌入 (“预测出来的下一个词应该长什么样”)。怎样把它变成”每个词一个分数”?slides 的做法是:把 乘以词表的嵌入矩阵:
第 个词的分数,就是 和这个词的嵌入的点积(再加一个偏置)。理想情况下 应该和真正的下一个词的嵌入”方向一致”,点积就大。这些分数叫 logits:它们可正可负,加起来也不等于 1,只是”未归一化的对数概率”。
代码 03 §2 用训练好的模型看 “I eat the” 之后的 logits:分数最高的是 apple 7.84、red 7.35、ripe 7.30、sweet 7.16、pear 7.14、carrot 6.91;最低的是 his −7.09、our −6.75、day −6.61——物主代词和尾巴里的词在这里都不可能出现。手算 和模型的输出完全一致(最大差 0)。
输入和输出共用一个嵌入矩阵,叫权重共享(weight tying,Press & Wolf 2017;Inan et al. 2017)。为什么这样做?
- 省参数:不共享就要再加一个 的输出矩阵。本模型从 27706 个参数变成 30586 个;对 Llama 3 8B 的规模,就是多 5.25 亿个参数。
- 对称性:一个词”作为输入时的意思”和”作为输出时该被预测的样子”本来就相关,共享让两边互相帮助,罕见词的输出向量也能从它作为输入时的训练里获益。
不过共享不是必须的:GPT-2、原版 T5 共享,Llama 3 8B 就选择了不共享。这是一个经验性的设计选择。
5.5 积木 ⑤:softmax
logits 怎样变成概率?用 softmax:
它保证每个 、加起来等于 1,并且保持大小顺序(logit 越大概率越大)。代码 03 §3: 得到 。
为什么用指数? 至少有三个理由:
- 处理负数:logits 可以是负的,直接除以总和会得到负”概率”。指数把任何实数映射成正数。
- “对数概率”的自然解释:,即 logit 就是”差一个常数的对数概率”。神经网络输出一个无约束的实数向量,softmax 把它解释成对数概率,这是最自然的参数化。
- 梯度简洁:配合交叉熵损失,梯度恰好是 (§5.6),这让训练又稳定又好算。
平移不变性:给所有 logits 加同一个常数 ,分子分母同时乘 ,结果不变。所以 logits 的绝对大小没有意义,只有差值有意义。代码验证了 ;而 ——拉大差距会让分布变尖。
数值稳定性:按课本公式直接算, 时 溢出成 inf,inf / inf = nan——代码里朴素实现的输出就是 [nan nan nan]。利用平移不变性,先减去最大值:,最大的一项变成 ,不会溢出,结果 和 一样。同理,计算 要用 log-sum-exp 技巧:,。所有深度学习框架内部都是这么做的(lm_math.py 里有完整实现)。
温度(temperature):把 logits 除以 再 softmax,。用训练好的模型在 “I eat the” 之后的 logits(代码 03 §3):
| 最大概率 | 熵(nat) | 凑够 90% 概率需要几个词 | ||
|---|---|---|---|---|
| 0.1 | 0.987 | 0.087 | 1.09 | 1 |
| 0.5 | 0.336 | 2.279 | 9.76 | 9 |
| 1.0 | 0.148 | 3.039 | 20.88 | 20 |
| 2.0 | 0.073 | 3.549 | 34.79 | 28 |
| 10.0 | 0.021 | 4.403 | 81.68 | 73 |
时分布集中到 logit 最大的那个词(argmax); 时趋于均匀()。“温度”这个名字来自统计物理的玻尔兹曼分布:温度越高,系统越”无序”。§7.5 会看到它是控制生成”保守 vs 大胆”的主要旋钮。
5.6 损失函数:负对数似然、交叉熵、困惑度
训练时,每个位置都有一个正确答案 (训练文本里真正的下一个 token)。负对数似然(NLL)损失就是
slides 的例子:预测 “the dog chased the ___“。代码 03 §4 设 5 个候选词的 logits 为 cat +3.0、dog +1.5、mouse +1.0、ball +0.5、the −2.0,softmax 后 。如果正确答案是 cat,损失是 ;如果是 ball(),损失是 2.870。模型越”意外”,损失越大;如果正确答案的概率趋近 0,损失趋近无穷——这就是不平滑的 n-gram 困惑度无穷大的原因。
为什么是 ? 因为对整个训练语料,总损失 。最小化 NLL 就是最大化训练语料的似然——和 n-gram 的”数数”是同一个原则(最大似然),只是模型换成了神经网络,没法再用闭式解,只能用梯度下降。从信息论的角度,最小化平均 NLL 等价于最小化”数据分布”和”模型分布”之间的 KL 散度(交叉熵 = 数据的熵 + KL,前一项是常数)。
把所有位置的 NLL 取平均,就是交叉熵损失;它的指数就是困惑度。代码用自己的实现和 PyTorch 的 F.cross_entropy 对比,逐位置的结果一致(最大差 )。
一个实用的检查:初始损失应该约等于 。一个对 个词均匀猜测的模型,NLL 是 ,困惑度是 。但”没训练过”不等于”均匀”!代码 03 §6 的结果:
| 模型 | 测试集平均 NLL | 困惑度 |
|---|---|---|
| 均匀猜测(理论值) | 4.500 | 90 |
| 未训练,PyTorch 默认初始化 | 5.755 | 315.7 |
| 未训练,小初始化 | 4.499 | 90.0 |
| 训练 6000 步之后 | 1.347 | 3.846 |
默认初始化下,嵌入向量很大,logits 之间的差距也大(标准差约 1.5),模型随机地”很自信”,结果比均匀乱猜还差 3.5 倍。这就是为什么 GPT-2 等模型把嵌入初始化成标准差 0.02 的小数:训练开始时 logits 几乎都是 0,分布接近均匀,损失约等于 。反过来,如果你写的训练代码第一步的损失远离 ,通常说明初始化或代码有问题——这是调试训练代码的常用第一步。
梯度:为什么是 。对 logits 求导( 是正确答案的下标):
写成向量就是 。代码 03 §5 用公式、有限差分和 PyTorch 自动微分三种方法计算,结果完全一致。这个形式的直觉很漂亮:
- 正确答案的梯度是 ,梯度下降会把它的 logit 往上推,推的力度是 (模型越没把握,推得越狠);
- 每个错误答案的梯度是 ,会被往下压,力度正比于模型给它的概率——越是”抢走”概率的错误答案,被压得越狠;
- 所有分量之和为 0:logits 整体平移不改变 softmax,所以也不会被”推”。
训练循环:把上面的东西串起来,就是 neural_lm.py 里的训练过程——随机抽 256 个(上下文, 下一个词)样本 → 前向得到 logits → 交叉熵损失 → 反向传播 → Adam 更新,学习率按余弦曲线从 0.003 降到 0,共 6000 步,单线程 CPU 约 2.5 秒。验证集困惑度从 4.146(第 1000 步)降到 3.844(第 6000 步)。
6. 把积木拼起来:n-gram 与神经语言模型的正面比较
现在有了两类模型:n-gram(§3)和固定窗口神经网络(§5)。它们看到的上下文一样长(6 阶 n-gram 和窗口 5 的网络都看最近 5 个词),训练数据一模一样(8000 篇文档、约 20 万个 token,刻意缺 5 种搭配)。代码 04 把它们放在一起比较。
6.1 总成绩
| 模型 | 测试集困惑度 |
|---|---|
| 均匀猜测 | 90.000 |
| 最好的加 k 平滑 n-gram(3 阶,k = 0.01) | 4.198 |
| 最好的插值 n-gram(6 阶) | 4.064 |
| 固定窗口神经网络(窗口 5,27706 个参数) | 3.846 |
| 真实分布(下界) | 3.655 |
神经网络把”离下界的差距”从 0.41 缩小到 0.19。困惑度的差别看起来不大,但要注意:这是在一门非常简单、n-gram 也能学得不错的玩具语言上。差别具体来自哪里?
6.2 优势在”没见过的上下文”
把测试集的每个位置按”前面 5 个 token 组成的上下文在训练集里有没有出现过”分成两组,比较平均每个 token 的 NLL(代码 04 §2):
| 上下文 | token 数 | 真实分布 | 神经网络 | 6-gram 加 k | 6 阶插值 |
|---|---|---|---|---|---|
| 见过 | 22743 | 1.339 | 1.389 | 1.875 | 1.432 |
| 没见过 | 2089 | 0.832 | 0.887 | 4.500 | 1.080 |
- 神经网络在见过的上下文上比真实分布多 0.051 nat,在没见过的上下文上多 0.056 nat——几乎没有差别。对它来说,“没见过”根本不是问题,因为它比较的是上下文的向量,而不是上下文的字符串。
- 6-gram(加 k)在没见过的上下文上多出 3.67 nat:它退化成了在 90 个词里均匀乱猜()。插值好得多(多 0.25),因为低阶模型能兜底,但仍然明显不如神经网络。
这就是 Bengio et al.(2003)论点的直接证据。
6.3 但神经网络不会”无中生有”
那 5 种被刻意拿掉的搭配呢?上下文都是 “you ⟨动词⟩ the”(代码 04 §3):
| 上下文 → 名词 | 真实 | 4-gram MLE | 4-gram 加 k | 6 阶插值 | 神经网络 | 神经网络给 eats | 倍数 |
|---|---|---|---|---|---|---|---|
| you pick the → plum | 0.0422 | 0 | 2.1e-05 | 3.9e-04 | 4.9e-04 | 4.3e-07 | 1129× |
| you grow the → tulip | 0.0386 | 0 | 3.5e-05 | 6.0e-04 | 0.0037 | 1.0e-07 | 36458× |
| you feed the → goat | 0.0515 | 0 | 3.5e-05 | 3.1e-05 | 2.2e-05 | 3.4e-06 | 7× |
| you eat the → onion | 0.0135 | 0 | 1.7e-05 | 1.8e-04 | 8.5e-04 | 3.1e-06 | 277× |
| you water the → daisy | 0.0515 | 0 | 3.6e-05 | 3.0e-04 | 6.2e-04 | 1.1e-07 | 5677× |
我原本预期神经网络能”举一反三”地给出接近真实的概率,实验说明这个预期是错的:神经网络给的概率比真实值低 10~2300 倍。原因并不神秘——训练数据里 “you pick the” 后面出现了成百上千次,从来不是 plum;最大似然训练的目标就是拟合训练数据,它会主动把这个概率往下压(还记得梯度 吗?每次 “pick the” 后面是别的词,plum 的 logit 都会被压一下)。对照组里见过的搭配,神经网络的估计都很准(pick → apple 真实 0.127、模型 0.134;eat → carrot 真实 0.068、模型 0.068)。
神经网络的”聪明”体现在另一处:它给”合理但没见过”的名词的概率,比给不可能的词 eats 高出几百到几万倍;而 n-gram 加 k 给两者完全一样的概率。也就是说,它知道 plum 是”可以被摘的那一类东西”,只是不知道它有多常见。
例外是 goat(只高 7 倍):goat 只能被 feed,而 feed + goat 被拿掉了,所以 goat 在整个训练集里一次都没出现过。它的嵌入从来没有作为输入被训练过,作为输出也只被”往下压”过——模型对它一无所知,泛化也就无从谈起。
教训:神经网络通过相似性泛化,但泛化不能违背数据。数据里系统性的缺口,模型会忠实地学进去。这是第 3 讲(数据覆盖)、第 11 讲(RAG 补充长尾知识)和第 23 讲(合成数据)的伏笔。
6.4 嵌入学到了什么
把词按类别分组,计算两组之间嵌入的平均余弦相似度(代码 04 §4,节选):
| 水果 | 蔬菜 | 花 | 动物 | 动词原形 | 动词+s | |
|---|---|---|---|---|---|---|
| 水果 | 0.13 | 0.04 | 0.04 | 0.01 | −0.01 | 0.00 |
| 蔬菜 | 0.04 | 0.15 | 0.03 | −0.00 | −0.09 | −0.05 |
| 花 | 0.04 | 0.03 | 0.18 | 0.06 | −0.10 | −0.10 |
| 动物 | 0.01 | −0.00 | 0.06 | 0.25 | −0.04 | −0.03 |
| 动词原形 | −0.01 | −0.09 | −0.10 | −0.04 | 0.23 | 0.05 |
| 动词+s | 0.00 | −0.05 | −0.10 | −0.03 | 0.05 | 0.17 |
对角线(同类)明显高于其余位置。另一个指标:43 个名词里,有 29 个(67%)“最近的名词”和自己同类,随机猜的期望是 25%。cat 的最近邻依次是 dog、hen、duck;apple 的是 carrot、cherry、melon、pear。
但也要诚实地说:单个词的最近邻噪声不小(apple 最近的是 carrot 而不是另一种水果;morning 最近的是 day,因为两者后面都接句号)。模型很小(32 维),而且共享的嵌入要同时编码”这个词是什么”和”后面接什么”。在真实的大模型里,嵌入空间的结构要丰富得多。
6.5 固定窗口的代价
尾巴 from ___ own garden 里的物主代词必须和句首主语一致。对测试集里每一个这样的物主代词,看模型给正确答案的平均概率(真实分布永远是 1.000,代码 04 §5):
| 个数 | 神经网络 | 6 阶插值 | |
|---|---|---|---|
| 主语在窗口内 | 543 | 0.988 | 0.364 |
| 主语在窗口外 | 437 | 0.700 | 0.309 |
主语在窗口内时,神经网络几乎完美(0.988);一旦主语移出 5 个词的窗口,就降到 0.700,只能靠残留的线索(动词有没有加 s、限定词是不是物主代词)去猜。例如 the children grow the fresh pumpkin from ___:窗口只看得到 grow the fresh pumpkin from,模型给 our 0.477、my 0.374、their 0.104——而正确答案是 their。(插值 n-gram 连窗口内都只有 0.364:6 阶上下文太稀疏,它的权重主要压在 3、4 阶上。)
把窗口加大不就好了?练习 7 会告诉你:窗口加到 8,物主代词的问题几乎消失,但总困惑度反而略微变差。真正的解决方案是让模型能直接查看任意远的位置——这就是第 2 讲的注意力机制。
6.6 剩下的误差花在哪里
神经网络比真实分布总共多出 1268.4 nat 的 NLL(平均每个 token 0.051)。按位置类型分解(代码 04 §6):
| 位置类型 | token 数 | 占 token 比例 | 多出的 NLL | 占总误差 |
|---|---|---|---|---|
| 被拿掉搭配里的名词 | 195 | 0.79% | 807.5 | 63.7% |
| 尾巴里的物主代词 | 980 | 3.95% | 246.3 | 19.4% |
| 其余所有位置 | 23657 | 95.27% | 214.5 | 16.9% |
不到 1% 的 token 贡献了近三分之二的误差;4% 的 token(受窗口限制的位置)又贡献了五分之一;其余 95% 的位置模型几乎已经学到了真实分布。这个模式在真实 LLM 里同样存在:平均损失掩盖了”在哪里错”的信息,而错误往往集中在数据缺口和能力边界上。数据和上下文——这正是后面很多讲要解决的问题。
7. 解码:从概率回到文字
7.1 为什么需要解码算法
语言模型每一步只给出一个分布 。要真正”说话”,需要一个解码算法决定每一步选哪个 token,把它接到历史后面,再算下一步。这里有两种不同的目标:
- 搜索:找”整体概率最大”的序列,。适合有标准答案的任务(翻译、摘要、代码补全)。
- 采样:按分布随机抽,得到”像真实数据”的多样样本。适合开放式任务(写故事、聊天)。
slides 列出了 6 种选项。下面的实验都用 §5 训练好的窗口模型,并用花园世界的真实语法判断生成的句子合不合法(decoding.py,代码 05)。
7.2 选项 1:贪心解码
每一步都选概率最大的 token:。简单、确定、快。问题是每一步的最优不等于整体最优。
代码 05 §1 的手工例子:
p(· | 开头) = A:0.5, B:0.4, C:0.1
p(· | A) = A:0.4, B:0.3, C:0.3
p(· | B) = A:0.9, B:0.05, C:0.05
贪心第一步选 A(0.5 > 0.4),第二步最多拿到 0.4,整体 。但 B 后面几乎一定是 A, 才是最可能的序列。贪心因为”短视”错过了它。
7.3 选项 6:束搜索
slides 的说法:假设最好的序列是整体似然最高的那个;穷举所有序列不可行( 种),所以用束搜索(beam search)近似。
算法:始终保留 条得分(累计 log 概率)最高的部分序列,叫做”束”(beam)。每一步,把每条束接上词表里的每个 token,得到 个候选,再从中保留得分最高的 条;遇到结束符的束就停止扩展。 就是贪心, 越大越接近穷举。
在手工例子里, 时 B 作为”第二名”被保留下来,第二步就反超了:束搜索找到了 B A(0.36),和穷举一致。
束搜索偏爱短句:每多一个 token,累计 log 概率就再减一点(log 概率都 ≤ 0),所以原始的束搜索倾向于早早结束。常见的修正是长度归一化:按 排序, 就是”平均每个 token 的 log 概率”。
在花园世界的模型上(代码 05 §2):
| 方法 | 模型整句 log p | 每词 log p | 输出 |
|---|---|---|---|
| 贪心 | −5.783 | −1.157 | I eat the apple . |
| 束搜索 B = 2、4 | −5.783 | −1.157 | I eat the apple . |
| 束搜索 B = 8 | −5.687 | −0.948 | the children feed the cat . |
| 束搜索 B = 8,α = 1 | −6.653 | −0.832 | the children feed the cat every day . |
找到了模型认为整体概率更高的句子。加上长度归一化后,输出变长了(多了 every day),整句概率变低,但平均每词的概率更高。
7.4 选项 2:纯采样,以及”长尾问题”
直接按模型的分布随机抽:。如果模型完美(),这样生成的文字和真实文字在统计上无法区分——这是最”忠实”的做法。
问题在于模型不完美。slides 指出:词表里大多数 token 的概率都很低,但它们加起来,抽中其中某一个的可能性相当大。slides 上的例子里,抽中一个概率不超过 0.001 的 token 的机会超过 29%。而模型的错误往往就藏在这条长尾里:每个”不该出现”的词只分到一点点概率,但积少成多。
代码 05 §3 用纯采样生成 2000 句话,在每一步检查模型的分布:
- 分给”真实语法下不可能的 token”的总概率:平均每步 0.0084(不到 1%),第 99 百分位 0.056;
- 一句话平均约 8 步,至少走错一次的概率约 ;
- 实际有 120 句(6.0%)不合法,和估算吻合。
在花园世界里,“概率不超过 0.001 的 token”加起来平均只有 0.18%,比 slides 上的 29% 小得多——因为这里词表只有 90 个、语言又简单。真实 LLM 的词表有几万到几十万个 token,长尾长得多,这个问题也严重得多。
slides 给出的解决思路是:修改模型返回的分布,让长尾里的 token 更不容易被抽中。下面三个选项都是这么做的。
7.5 选项 3:带温度的采样
先用温度 改造分布(§5.5),再采样。 让分布变尖:大概率的更大、小概率的更小,长尾被压低。 时,带温度的采样就越来越像 argmax(贪心)。 则相反,让长尾”翻身”。
7.6 选项 4:top-k 采样
只保留概率最大的 个 token,把全部概率质量重新分配给它们(重新归一化),其余设为 0。slides 说通常选 在 10 到 50 之间(Fan et al., 2018 在故事生成中使用了 top-k)。
top-k 的问题是 固定:模型很确定时(“the children” 后面几乎一定是动词), 会把一堆本该为 0 的词放进候选;模型不确定时(选哪种水果), 又会把很多合理的选择砍掉。
7.7 选项 5:top-p(核采样)
按概率从大到小排序,保留最少的若干个 token,使它们的累计概率达到 ,在这些 token 里重新归一化后采样(Holtzman et al., 2020,称为 nucleus sampling)。候选集的大小随分布的形状自动调整:分布尖时只剩一两个,分布平时可能有几十个。
例:分布 ,。累计概率 0.5、0.7、0.85——前 3 个就够了,重新归一化后是 。
7.8 “太高概率”的文本反而不好
回到搜索。如果模型足够好,“最可能的序列”不就是”最好的序列”吗?slides 指出,对于对话、写故事这类开放式任务,追求整体概率最高的序列并不总是好主意:
- 束搜索生成的文本,概率远高于人写的文本(Holtzman et al., 2020 的观察:人写的文字每个 token 的概率起伏很大,经常出现”意外”的词;束搜索的文字一直处在高概率区);
- 当序列的似然太高时,人类评价者反而会给它打低分(Zhang et al., 2021 称之为”似然陷阱”,likelihood trap:质量随似然先升后降)。
代码 05 §2 在花园世界里复现了第一点。从真实分布采样 2000 句(相当于”人写的文字”),用模型给它们打分:
| 解码结果 | 整句 log p 高于多少”人写的”句子 | 每词 log p 高于多少 |
|---|---|---|
| 贪心(I eat the apple .) | 99.7% | 70.0% |
| 束搜索 B = 8 | 99.7% | 90.4% |
| 束搜索 B = 8,α = 1 | 96.5% | 97.0% |
更直观的是多样性:真实分布里最常见的句子,在 2000 句中也只出现了 9 次;而贪心和束搜索每次都输出同一句话。在花园世界里这句话是完全合法的——“太高概率”在这里的代价是没有多样性、不像真实的数据分布。在真实的大模型里,高概率区还经常是空洞、重复、套话的文字,代价就更明显了。
7.9 各种策略的正面比较
每种策略生成 2000 句(贪心只有 1 句),用真实语法和真实分布评估(代码 05 §4):
| 方法 | 合法率 | 不同句子 | 名词种类(/43) | 真实 NLL/词 |
|---|---|---|---|---|
| 真实分布采样(参照) | 100.0% | 81.5% | 43 | 1.265 |
| 贪心 | 100.0% | — | 1 | 1.157 |
| 纯采样 T = 1 | 93.5% | 80.8% | 42 | 1.286 |
| 温度 T = 0.7 | 96.5% | 69.2% | 42 | 1.264 |
| 温度 T = 1.5 | 76.6% | 90.5% | 42 | 1.309 |
| top-k,k = 5 | 94.4% | 51.5% | 31 | 1.228 |
| top-k,k = 20 | 94.4% | 79.4% | 42 | 1.282 |
| top-p,p = 0.8 | 95.4% | 61.7% | 34 | 1.252 |
| top-p,p = 0.95 | 96.3% | 78.3% | 42 | 1.274 |
(“真实 NLL/词”是合法句子在真实分布下平均每个词的负对数似然,越低表示越”常见”、越保守。)
读这张表的方法:
- 质量和多样性此消彼长。T = 0.7、top-p 0.8 把合法率提高了,但不同句子的比例从约 81% 降到 62%~69%;top-k 5 最极端,有 12 种名词再也说不出来了。T = 1.5 则反过来:多样性甚至超过了真实文字(90.5%),合法率却跌到 77%。
- top-p 0.95 是这里最好的折中:合法率从 93.5% 升到 96.3%,多样性(78.3%)和名词覆盖(42 种)几乎不变。Holtzman et al. 的主要实验用的也是 。
- 越保守,越”平庸”:top-k 5、top-p 0.8 生成的句子在真实分布下的 NLL(1.228、1.252)低于真实文字(1.265)——它们只说”常见的话”。
为什么砍了尾巴,合法率还是到不了 100%? 把不合法的句子按”第一个出错的位置”分类:
| 方法 | 不合法句数 | 错误类型 |
|---|---|---|
| 纯采样 T = 1 | 129 | 尾巴里的物主代词 91,名词类别 12,主语 7,形容词后的名词 6,其他 13 |
| top-p 0.95 | 74 | 全部是尾巴里的物主代词 |
| top-p 0.8 | 92 | 全部是尾巴里的物主代词 |
| 温度 T = 1.5 | 468 | 各种错误都有 |
这是本讲我最喜欢的一个结果:top-p 把长尾里的”垃圾”全部清除了,剩下的错误 100% 是窗口外的物主代词。这类错误不在长尾里——主语滑出窗口后,模型在 my / our / their 之间真心犹豫,错误答案拿到了 0.3、0.4 的概率,处在分布的头部。截断只能去掉”模型自己也觉得不太可能”的错误,去不掉”模型很有把握的错误”。后者只能靠更好的模型来解决(第 2 讲)。
7.10 其他生成参数:频率惩罚、存在惩罚、停止条件
slides 最后列了几个”附加旋钮”,它们出现在几乎所有 LLM API 里。
复读问题。让模型一直写下去(禁止输出 <eos>),贪心解码会陷入死循环(代码 05 §5):
I eat the apple . I eat the apple . I eat the apple . I eat the apple . ...
窗口模型只看最近 5 个 token,句号之后的上下文永远一样,自然永远选同一句话。真实的 LLM 上下文长得多,但贪心和束搜索同样容易复读(Holtzman et al., 2020 报告了大量例子),原因之一是”重复”会自我强化:前文出现过的模式让模型更倾向于再写一遍。
频率惩罚和存在惩罚(OpenAI API 文档给出的公式):
其中 是 token 在已生成文字中出现的次数。
- 频率惩罚:出现越多次,扣得越多(线性增长);
- 存在惩罚:只要出现过就扣一次固定的分,不管出现几次。
关键在于:惩罚看的是全部已生成的文字,正好补上窗口模型”看不到自己说过什么”的盲区。实验结果(60 个 token):
| 设置 | 完整句子 | 其中不同的 | 合法的 |
|---|---|---|---|
| 贪心 | 12 | 1 | 12 |
| 贪心 + 频率惩罚 0.3 | 8 | 7 | 8 |
| 贪心 + 频率惩罚 1.0 | 7 | 7 | 7 |
| 贪心 + 频率惩罚 3.0 | 5 | 5 | 3 |
| 贪心 + 存在惩罚 0.5 | 11 | 3 | 11 |
| 贪心 + 存在惩罚 2.0 | 8 | 8 | 8 |
| 贪心 + 存在惩罚 10 | 3 | 3 | 1 |
| top-p 0.9(不惩罚) | 8 | 8 | 8 |
两个有意思的现象:
- 存在惩罚 0.5 只管用了一会儿:前三句各不相同,之后又回到 “I eat the apple .” 的循环。原因是:等常用词都”出现过”之后,存在惩罚对所有候选都是同一个常数,而 softmax 只看差值(平移不变性!),于是它不再起作用。频率惩罚随次数增长,没有这个问题。
- 惩罚太大会伤害语法:the、句号、物主代词本来就该反复出现,惩罚它们会逼模型说出 … with their friends in the morning .(两个尾巴)甚至一串乱码(存在惩罚 10 时 3 句里只有 1 句合法)。惩罚项是一种很粗糙的工具:它不区分”该重复的词”和”不该重复的内容”。
停止条件:(1)生成 个 token 后停止(长度上限);(2)生成某个特定 token 时停止(句号、换行、<eos>)。代码 05 §6 用同一个随机种子生成三次:长度上限 6 时得到 she waters her carrot . the(被从中间切断);遇到句号就停得到 she waters her carrot .;遇到 <eos> 才停得到一整篇文档。API 里的 max_tokens 和 stop 参数就是这两种停止条件。
7.11 实践中怎样选
没有放之四海而皆准的设置,但有一些常见的经验:
- 有标准答案的任务(翻译、抽取、代码、数学):低温度(甚至 ,即贪心)或束搜索,追求”最可能正确”。
- 开放式生成(写作、头脑风暴、聊天):~ 配合 top-p 0.9~0.95,在质量和多样性之间折中。
- 需要多个不同答案(第 7、9 讲的”多次采样再投票”):较高的温度,保证样本之间有差异。
- 惩罚项用得较少,且宜小不宜大;更根本的办法是更好的模型和训练。
8. 小结、常见误区、练习
8.1 小结
- 语言模型 = 序列上的概率分布,操作上是”给定前文,输出下一个 token 的分布”。链式法则把两者联系起来;log 概率避免下溢;<eos> 让模型知道何时停止。花园世界的 30336 个句子概率之和精确为 1。
- n-gram = 数数 = 最大似然,但稀疏性随 n 指数恶化,没见过的 n-gram 让困惑度变成 ∞。平滑(加 k、插值)能补救,但 n-gram 把每个上下文当孤立的格子,无法泛化。
- 神经语言模型用嵌入向量表示词,相似的上下文得到相似的预测。实验中它在没见过的上下文上几乎和见过的一样好;但它不会违背数据去”发明”训练集里系统性缺失的搭配。
- 有条件和无条件模型可以互相转换:把条件拼在前面,用损失掩码只在目标部分计算损失。聊天模型就是这样训练的。
- 六块积木:分词(词表大小 vs 序列长度,子词/BPE 是折中)→ 嵌入(查表 = one-hot × E)→ 网络(隐藏状态)→ logits(输出嵌入与词嵌入的点积,常用权重共享)→ softmax(只看差值、要减最大值、温度控制尖锐程度)→ 解码。训练时用 NLL / 交叉熵,梯度是 ,初始损失应约等于 。
- 解码:贪心短视,束搜索近似找最可能的序列但缺乏多样性;纯采样受长尾拖累;温度、top-k、top-p 用”砍尾巴”换质量,代价是多样性;它们去不掉模型”很有把握的错误”。最可能的文本往往不是最好的文本。惩罚项缓解复读,但太强会伤害语法。
8.2 常见误区
- “语言模型只能生成,不能打分”:同一个模型既能逐词生成,也能用链式法则给任意文本打分。打分是它最早的用途,也是评估(第 5 讲)、RLHF(第 18 讲)、检测数据污染(第 16 讲)的基础。
- “困惑度越低的模型一定越好用”:困惑度只能在相同分词的模型之间比较;而且它衡量的是”平均预测得多准”,不直接衡量生成质量或任务能力。
- “logits 就是概率”:logits 是未归一化的分数,可正可负,只有差值有意义;必须经过 softmax 才是概率。
- “没训练的模型就是均匀分布”:取决于初始化。PyTorch 默认初始化的嵌入下,未训练模型的困惑度(315.7)比均匀猜测(90)差得多。
- “温度 0 的采样是一种采样”: 的极限是 argmax,完全确定,没有随机性。(很多 API 把
temperature=0实现成贪心。) - “top-p / 低温度能消除模型的错误”:它们只能去掉落在长尾里的错误。模型很有把握的错误(§7.9 的物主代词)不受影响。
- “束搜索越宽越好”:束越宽越接近”模型认为最可能的序列”,但对开放式生成来说,那恰恰是缺乏多样性、甚至更差的文本。
- “神经网络能泛化,所以不怕训练数据有缺口”:§6.3 表明,训练数据里系统性缺失的东西,模型会学成”很不可能”。
8.3 练习
练习 1(链式法则):用花园世界的规则手算 (不含 <eos>)。已知:主语 she 的概率 0.12;she 选 pick 的概率 0.35;限定词用物主代词的概率 0.4;名词前加形容词的概率 0.3,pick 后面名词是水果的概率 0.6,水果有 3 个形容词(均匀);plum 在 15 种水果里排第 3,Zipf 权重为 ();植物名词后直接接句号的概率 0.45。如果主语换成 he(选 pick 的概率 0.1),概率变成多少?
参考答案
各步概率:she 0.12;picks 0.35;her 0.4;ripe ;plum ;句号 0.45。乘积 。代码 GardenWorld().logprob("she picks her ripe plum .".split(), return_steps=True) 给出完全相同的各步概率,乘积 。换成 he:只有第 2 步从 0.35 变成 0.1(his 的概率仍是 0.4),结果 。注意”ripe”那一步是三个概率的乘积:先决定加形容词,再决定类别,最后选具体的形容词——这是一个”隐变量”(名词类别)被边缘化掉的例子。
练习 2(softmax 的性质):(a)证明 。(b)证明当 时, 趋向于 argmax 的 one-hot 向量(假设最大值唯一);当 时趋向均匀分布。
参考答案
(a)。
(b)设 是唯一最大值。。对 ,所有 (), 时 ,所以 ;对 ,分母里有 ,所以 。 时所有指数 ,。代码 03 §3 的温度表展示了这两个极限( 时最大概率 0.987, 时 ,接近 )。
练习 3(梯度):(a)推导 对 的梯度。(b)如果模型已经非常确定且正确(),梯度是多少?这说明了什么?(c)如果模型非常确定但错了(,)呢?
参考答案
(a)见 §5.6:。
(b),梯度 :已经学会的样本几乎不再产生学习信号。这是交叉熵训练”自动聚焦在没学会的样本上”的原因。
(c)梯度在 处约为 ,在 处约为 ,其余约为 0:把正确答案往上推、把”抢走概率”的那个错误答案往下压,力度都是最大的。注意梯度的大小有上界(每个分量的绝对值不超过 1),不会因为模型错得离谱而爆炸——这是交叉熵比例如”平方误差 + sigmoid”更适合分类的原因之一。
练习 4(困惑度):(a)一个模型在 1000 个 token 上的总 NLL 是 1386.3 nat,困惑度是多少?(b)为什么不平滑的 n-gram 只要测试集里有一个没见过的 n-gram,困惑度就是无穷大?(c)测试集困惑度能不能低于真实分布的困惑度 3.655?
参考答案
(a):平均每一步像在 4 个等可能的词之间犹豫。
(b)那个 token 的概率是 0,NLL 是 ,总 NLL 和平均 NLL 都是 ,。
(c)在期望意义下不能:交叉熵 熵(吉布斯不等式)。但具体某个有限的测试集上,模型的困惑度可能碰巧略低于用真实分布算出来的值(统计波动);测试集越大,这种情况越不可能。另外注意:训练集上的困惑度可以低于 3.655(代码里窗口模型训练集困惑度是 3.635),因为训练集的分布刻意去掉了 5 种搭配,熵本来就略低,而且模型可能记住了训练集。
练习 5(top-k 与 top-p):分布 。(a)top-k, 之后的分布是什么?(b)top-p, 呢?(c)构造一个分布,使 top-p()保留的 token 比 top-k()多;再构造一个使它保留得少。
参考答案
(a)保留前两个,重新归一化:。
(b)累计 0.5、0.7、0.85,第 3 个时首次达到 0.8,保留 3 个:。(decoding.top_p_filter 和 top_k_filter 给出相同的结果。)
(c)很平的分布,例如 100 个 token 各 0.01:top-p 0.9 要保留 90 个,比 top-k 10 多。很尖的分布,例如 :top-p 0.9 只保留 1 个,比 top-k 10 少。这正是 top-p “自适应”的含义。
练习 6(束搜索):(a)为什么束搜索在不做长度归一化时偏爱短句?(b)在 §7.2 的手工例子里,如果把 改成 ,贪心和束搜索()的结果分别是什么?
参考答案
(a)每一步的 log 概率都 ≤ 0,序列每多一个 token,累计 log 概率就只会更小(或不变)。一个早早输出结束符的短序列,往往比继续写下去的长序列得分高。
(b)贪心仍是 A A(0.20)。束搜索 的第二步候选:AA 0.20、AB 0.15、AC 0.15、BA 0.18、BB 0.18、BC 0.04,保留 AA 和 BA(或 BB),最好的是 AA 0.20——此时贪心的结果恰好就是最优解,束搜索也找到了它。束搜索不会比贪心差(在同一个打分标准下),但也不保证比贪心好。
练习 7(动手:窗口大小):把 neural_lm.py 的窗口从 5 改成 8(load_or_train(cfg={"context": 8})),重新计算测试集困惑度和 §6.5 的物主代词准确率。预测一下会发生什么,再运行验证。
参考答案(作者机器上的实际结果)
| 窗口 | 参数量 | 训练集困惑度 | 测试集困惑度 | 物主代词(主语在 5 词窗口外) |
|---|---|---|---|---|
| 5 | 27706 | 3.635 | 3.846 | 0.700 |
| 8 | 39994 | 3.544 | 3.866 | 0.984 |
窗口加到 8 后,主语几乎总在窗口内,物主代词的准确率从 0.700 升到 0.984——这部分问题基本解决了。但测试集困惑度反而从 3.846 略升到 3.866,而训练集困惑度从 3.635 降到 3.544:这是过拟合的信号。更大的输入层有更多参数,更长的上下文组合也更稀疏,模型更紧地贴合了训练集(包括它”缺 5 种搭配”的特点)。
教训:加大固定窗口不是免费的——参数随窗口线性增长,每个位置的权重都要单独学,远处位置的信号又很稀疏。我们需要一种”参数量不随上下文长度增长、又能直接查看任意位置”的机制:注意力(第 2 讲)。
练习 8(惩罚项):(a)为什么存在惩罚 0.5 在 §7.10 的实验里”只管用了一会儿”?用 softmax 的平移不变性解释。(b)设计一种比频率惩罚更”聪明”的反复读方法,使它不会惩罚 the 和句号这类必须重复的词。
参考答案
(a)存在惩罚对”出现过的 token”统一减去一个常数。生成了几句话之后,模型在某一步真正考虑的候选(比如句号之后的各个主语)全都已经出现过,它们被减去同一个常数,相当于 logits 整体平移——softmax 不变,于是贪心又选回了原来那个最可能的主语,循环恢复。频率惩罚随出现次数增长,出现得越多扣得越多,候选之间的差距会被持续改变。
(b)思路很多,例如:只惩罚内容词(名词、动词),不惩罚功能词和标点;或者惩罚重复的 n-gram 而不是单个 token(“no-repeat n-gram”:禁止生成已经出现过的 3-gram 或 4-gram,HuggingFace 的 no_repeat_ngram_size 就是这样做的);或者用对比搜索(contrastive search)惩罚”和前文表示太相似”的候选。它们都在用”更多关于语言的知识”代替一刀切的惩罚。
练习 9(思考题:分词与困惑度):同一段测试文本,分词器 A 切出 1000 个 token,模型 A 的困惑度是 20;分词器 B 切出 1500 个 token,模型 B 的困惑度是 8。哪个模型给这段文本的整体概率更高?
参考答案
整段文本的负 log 概率 = token 数 × 平均 NLL = token 数 × log(困惑度)。A:;B:。A 给整段文本的概率更高(负 log 概率更小),尽管它的困惑度更高。所以不同分词器的困惑度不能直接比较,要换算成”每个字符/每个字节的比特数”(bits per byte)这类与分词无关的指标——第 5 讲会再讨论。
8.4 延伸阅读
- 原课阅读:Jurafsky & Martin, Speech and Language Processing(第 3 版草稿)第 7 章;Alammar, The Illustrated Transformer(为第 2 讲做准备)
- 神经语言模型的起点:Bengio, Ducharme, Vincent & Jauvin (2003), A Neural Probabilistic Language Model, JMLR
- 语言的信息论:Shannon (1951), Prediction and Entropy of Printed English
- BPE:Sennrich, Haddow & Birch (2016), Neural Machine Translation of Rare Words with Subword Units
- 权重共享:Press & Wolf (2017), Using the Output Embedding to Improve Language Models
- 解码:Holtzman et al. (2020), The Curious Case of Neural Text Degeneration(核采样);Fan, Lewis & Dauphin (2018), Hierarchical Neural Story Generation(top-k);Zhang, Duckworth, Ippolito & Neelakantan (2021), Trading Off Diversity and Quality in Natural Language Generation(似然陷阱)
- 本仓库的相关内容:CMU10-423 第 1 讲(n-gram、RNN 语言模型与自动微分)、CMU10-423 第 3 讲(训练与解码,含 KV 缓存与 BPE)
8.5 下一讲预告
第 2 讲打开”神经网络”这个黑盒:Transformer 结构与预训练目标。我们会看到,§6.5 里固定窗口解决不了的物主代词一致问题,注意力机制怎样让每个位置”直接回头看”句首的主语;以及为什么同样是 Transformer,有 encoder-only(BERT)、decoder-only(GPT)和 encoder-decoder(T5)三种结构,它们分别用什么目标预训练、适合什么任务。