语言模型这件事,从头到尾只干一件事:给一句话打分,看它像不像人话。 这一节把这条路走一遍——从数词频的 n-gram,到把词变成向量的词嵌入, 再到 Transformer 与预训练范式。 走完你会明白两件事:为什么参数共享是分水岭, 以及为什么最后赢的是「一个大模型加一堆数据」而不是「一堆规则」。
这是全书第一章,前面没有本书的内容可接。 往下挖原理的话,41 课 图解深度学习 讲的是网络内部怎么算, 42 课 图解人工智能 讲的是这门学科的来路。 这一节跟它们的区别:那两本讲模型里面发生了什么, 这一节讲模型外面这条技术路线是怎么一步步走到今天的。
一、语言模型到底在算什么
把一句话拆成词,语言模型给这串词一个概率。概率高,说明这串词像人话;概率低,说明别扭。 这句话听着简单,但它一下子把「懂语言」变成了一个可以算的数。 后面七十年所有的进展,都是在这同一个目标上换更好的算法。 最早的算法是数数。给定前 n-1 个词,看下一个词在语料里出现过几次,除以总次数,就是概率。 这就是 n-gram。
n 取 1 叫一元模型,只看当前词;取 2 叫二元,看前一个;取 3 叫三元。n 越大,考虑的历史越长。 它的毛病也很直接:语料里没出现过的组合,概率就是零。 而零概率会顺着乘法传下去,一句话里只要有一处没见过,整句概率就是零。取对数,就是负无穷。 工程上的补丁叫平滑:给每个没见过的格子留一点概率,代价是把见过的也压一点。 加一平滑是最土的,后来的 Kneser-Ney 之类精细得多,但道理一样。
第二个毛病更根本。n 阶模型要为每一个「前 n-1 个词的组合」单独记一本账,账本大小随 n 指数增长。 词表三万二、三元模型,全表就是三万二的立方——存不下。
二、把词变成向量,账本就不用一人一本了
2013 年前后,词嵌入把这条路掰了一下。 它不再为每个词记一本账,而是给每个词一个几十到几百维的向量,用向量之间的距离表示意思相近。 这一下的意义在于参数共享:「今天的天」和「明天的天」不再各记一本账, 它们共用同一个「天」的向量。没见过的组合,也能靠向量算出非零概率。 2017 年的 Transformer 换了另一件事:怎么让一句话里的词互相看。 它用注意力机制,让每个词直接去「问」其他所有词,而不是像循环网络那样一个接一个往后传。 这一改,训练可以并行,层数可以堆到几十上百。
2018 年分出两条路。一条是 BERT,用编码器,把句子挖空让人填,擅长理解; 一条是 GPT,用解码器,接着往下写,擅长生成。这一本书讲的 Llama 2,走的是后一条。 2019 年 GPT-2 十五亿参数,2020 年 GPT-3 一千七百五十亿参数。 规模上去之后出现了新东西:不用专门训练,只要在提示里给几个例子,它就会做。 这叫上下文学习,它把「预训练加微调」的老范式改成了「预训练加提示」。
三、为什么最后是「大」赢了
2020 年有一批论文把规律量了出来:损失随参数量、数据量、算力的增长, 大致是一条幂律曲线——画在双对数坐标上是一条斜向下的直线。 这意味着想更好,就往上堆,而且堆多少能提前估出来。 2022 年的 Chinchilla 又补了一刀:多数模型参数堆得太多、数据喂得太少。 在算力固定的前提下,参数和数据应该同比例长,大约每个参数配二十个 token。 这个结论后来被反复修正,但它把「数据也是要花钱的」这件事摆到了台面上。 2023 年 2 月 Meta 放出 LLaMA,思路反过来:不追求最大,追求「在能跑得动的规模上把数据喂足」。 七十亿参数配上万亿级 token,在很多任务上追上了十几倍大的模型。 这一下把开源社区点着了——原来一台机器也能碰大模型。
同年 7 月 Llama 2 发布,补齐了两件上一代缺的东西:允许商用,以及配套的对话版本。 这本书后面所有实验,用的都是它。
四、Llama 2 的规格表
书里没给完整规格,这里按 Meta 的论文补上。 三档模型除了大小,还差在层数、隐藏维度和注意力头的数量上:
| 项 | 7B | 13B | 70B |
|---|---|---|---|
| 参数量 | 67 亿 | 130 亿 | 689 亿 |
| 层数 | 32 | 40 | 80 |
| 隐藏维度 | 4096 | 5120 | 8192 |
| 注意力头 | 32 | 40 | 64(KV 头 8 个) |
| 上下文长度 | 4096 | 4096 | 4096 |
| 词表大小 | 32000 | 32000 | 32000 |
| 预训练数据 | 2 万亿 token | 2 万亿 token | 2 万亿 token |
这张表里有三个数后面会反复用到:上下文 4096(第 4 章多轮对话的天花板就是它)、 词表 32000(第 7 章讲多语言要拿它算账)、67 亿参数(第 2、3 章算显存的起点)。 结构上有四处跟原始 Transformer 不一样,记住名字就够,细节在第 1.2 节和后面各章展开: 一是归一化放在子层之前,用的是 RMSNorm,只算均方根,不减均值,快一点; 二是前馈层用 SwiGLU,带一个门控,同等参数量下效果好一些; 三是位置编码用旋转式(RoPE),把位置信息转进向量角度里; 四是 70B 那一档用了分组查询注意力,多个查询头共用一组键值,把缓存压下来。
还有一处不在结构里,但更要紧:它的预训练语料里 89.7% 是英文。中文只占千分之一出头。 这就是为什么它对中文的感觉总差一口气,也是第 7 章要解决的问题。
五、动手 Demo:两个模型跑同一句话,看零概率与参数量
这一节拿一段十句话的小语料,跑两件事。 第一件是 1、2、3 阶的 n-gram,看「不加平滑」那一列怎么从有限变成无穷; 第二件是一个十六维隐层的最小神经语言模型,看它为什么不会出现零概率。 语料只有十句,词表六十多个字,所以这批数字只能用来比自己跟自己,不能当行业基准。
# 1 阶到 3 阶的 n-gram:数格子,算困惑度
tab = _train_ngram(CORPUS, n) # 上下文 -> {下一个字: 次数}
ppl, N = _ppl_ngram(TEST, n, tab, len(chars)) # 不加平滑
ppl1, _ = _ppl_ngram(TEST, n, tab, len(chars), smooth=1.0)
# 一个最小神经语言模型:字 -> 16 维向量 -> 下一个字的分数
class Bigram(object):
def __init__(self, V, d=16, seed=1):
rs = np.random.RandomState(seed)
self.E = rs.randn(V, d) * 0.1 # 每个字一个向量
self.W = rs.randn(d, V) * 0.1 # 向量 -> 词表打分
def fit(self, pairs, steps=400, lr=0.6):
xs = np.array([a for a, _b in pairs])
ys = np.array([b for _a, b in pairs])
for _t in range(steps):
h = self.E[xs] # (n, d)
p = softmax(h @ self.W) # (n, V)
g = p.copy()
g[np.arange(len(xs)), ys] -= 1.0 # 交叉熵的梯度
g /= len(xs)
np.add.at(self.E, xs, -lr * (g @ self.W.T))
self.W -= lr * (h.T @ g)
# 参数量:n-gram 全表 vs 这个模型
print(len(chars) ** 3, m.E.size + m.W.size)
实跑输出:
--------------------------------------------------------------------
从数词频到预测下一个词:两种模型跑同一句话
--------------------------------------------------------------------
① 先看语料与字表
--------------------------------------------------------------------
训练语料 10 句,去重后 67 个字(含两个句子边界符),平均每句 12.9 字。
字表:不丢为书买了今他们但便公到匙去只吧吹商回因园在
场坏天她好如家就山待很心情我才收散明是晚来果步
气水点热爬物看真着睡礼空腿说调酸里钥顺[]
测试句 2 句,其中「明天天气真好我们去公园」里的三元组在训练语料里一次也没出现过。
! 这就是语言模型最原始的那个难题:没见过的组合,概率该给多少。
② 数词频:1/2/3 阶 n-gram
--------------------------------------------------------------------
| 阶数 | 上下文个数 | 表里格子数 | 不加平滑 | 加一平滑 |
|:---:|---:|---:|---:|---:|
| 1 阶 | 1 | 66 | 41.36 | 44.83 |
| 2 阶 | 66 | 102 | 无穷 | 25.78 |
| 3 阶 | 93 | 116 | 无穷 | 27.97 |
阶数一高,上下文的种类立刻从个位数跳到几十上百,而「不加平滑」那一列直接从有限变成无穷。
! 零概率不是小事:一句话里只要有一个格子是空的,整句的概率就是 0,取对数变负无穷,
困惑度就没法算了。
③ 平滑管什么用
--------------------------------------------------------------------
同一份三元组表,加一平滑给 27.97,加 0.1 平滑给 6.24,差 21.73。
平滑把「没出现过」当成「很少出现」,代价是把「出现过」的概率也压低了。
! 困惑度这个数,跟平滑系数绑在一起。所以两条不同来源的困惑度不能直接比大小。
④ 换成参数共享的模型
--------------------------------------------------------------------
139 个训练对,16 维隐层,参数量 2144 个——比三元组表的 116 个格子多 18.5 倍。
但这笔账不能这么算:三元组表的 116 个格子只覆盖了这 10 句话里真出现过的组合,
换一份语料就得重数;模型的 2144 个参数对着任何语料都是这么多。
训练 400 步,损失从 4.207 降到 1.124。
同一批测试句,它给 3.60,而且是有限值——因为它把「明天的天」和「今天的天」共用了同一个字向量。
! 这就是从「数词频」走到「学表示」的那一步:不再为每个上下文单独记一本账。
⑤ 参数量差在哪
--------------------------------------------------------------------
| 模型 | 参数 | 共享方式 |
|---|---:|---|
| 三元组表 | 300763 | 一个格子只服务一个上下文 |
| 这个玩具模型 | 2144 | 一个向量服务所有上下文 |
| Llama 2 7B | 67 亿 | 一层就有两亿,靠 32 层堆出来 |
表的大小随阶数指数涨,模型的参数随宽度线性涨——这就是两条路的根本差别。
先说 n-gram 那张表。 1 阶只有一个上下文,2 阶就变成 66 个,3 阶 93 个。这不是线性增长, 是把「前面出现过什么」的所有组合都当成一个格子。 「不加平滑」那一列,1 阶给了 41.36,2 阶和 3 阶直接是无穷。 原因就一个:测试句里有训练语料没见过的三元组,概率是零,取对数就是负无穷。 加上平滑之后,三个阶都变成有限值了。 但注意加一平滑给的是 27.97,把平滑系数从 1 降到 0.1,同一个模型给出 6.24。 同一张表、同一批测试句,困惑度差了四倍多——所以两条不同来源的困惑度不能直接比大小,得先问口径。 再看那个十六维的小模型。它一共 2144 个参数,训练 400 步,损失从 4.207 降到 1.124。 在同一批测试句上给 3.60,是有限值, 理由是它把「今天的天气」和「明天的天气」里的那个「天」共用成了同一个向量, 所以没见过的上下文也能凑出一个概率。 这里的账要算清楚:三元组表是 116 个格子,模型是 2144 个参数,模型多 18.5 倍。 但三元组表的 116 个格子只覆盖了这十句话里真出现过的组合,换一份语料就得重数; 模型的 2144 个参数对着任何语料都是这么多。前者是查表,后者是拟合。 最后一个数值得盯着:如果把词表换成真实的 32000,三元组的全表是三万二的立方, 大约是 3.3 后面跟十四个零——存不下。而模型那边,参数只随「词表乘维度」线性长。 这就是为什么这条路走到最后只剩一条。
30 秒实验
- 把语料里的「今天」全部改成「今儿」,重跑一遍。 看 3 阶的上下文个数变了多少——你会发现它变了,因为所有带「今天」的三元组都换了个名字。 这就是查表法的脆弱处。
- 把
smooth=1.0改成0.01,看困惑度怎么变。 然后想一想:能不能靠调这个数,让任意一个模型在任意一份测试集上「表现更好」? - 把
Bigram的隐层从 16 改成 4,再改成 64,看参数量与最终损失各是什么关系。 参数变多损失一定更低吗?
常见误区
- 把困惑度当成「模型好不好」的唯一标准。困惑度只说明它对你这份语料有多熟,不说明答得对不对。 第 1.5 节会用两套题把这件事拆开。
- 以为 n-gram 已经完全淘汰了。 它没死——很多检索、输入法、语音识别的语言模型至今还在用,因为便宜、可解释、改起来不用重训。 它输在规模上,不是输在道理上。
- 以为「参数越多越好」。 2022 年之后的主流结论是参数与数据要匹配,一个喂不足数据的大模型,打不过一个喂饱了的小模型。
自测三问(点开对答案)
1. 三元语言模型在测试句上给出零概率,是模型错了还是数据不够? —— 两个都不是,是方法本身的性质。n-gram 的概率来自计数,没数过就是零。 补的办法是平滑,代价是见过的那部分概率被压低。这也说明它没法「举一反三」。 2. 参数共享具体省掉了什么? —— 省掉了「为每个上下文单独记一本账」。三元组表要求「今天的天气」和「明天的天气」各占一行; 共享向量的模型只有一个「天」的向量,两个上下文都用它。 代价是表达变粗,收益是没见过的组合也有概率。 3. Llama 2 的 4096 上下文、32000 词表,分别会在后面的哪一章变成限制? —— 4096 在第 4 章卡住多轮对话——聊到第五轮,前面的原话就被挤出去了。 32000 的词表在第 7 章卡住多语言——中文、泰文这类语言被切得太碎,同样的意思要花更多 token。下一节往里走一层,看那个「让词互相看」的注意力到底怎么算,以及位置编码为什么不能省。
- ☐ (本篇完,下一篇 1.2 大模型的核心框架)
相关笔记
- 41 课 图解深度学习:注意力机制的图解法
- 42 课 图解人工智能:从符号主义到连接主义的来路
- 🦙 Llama 大模型实践 · 索引