文本生成的全部内容,就是「看前几个字,猜下一个字」。猜完之后把它接到后面,再猜下一个。 这一节用二十句话训一个 n 元模型,量它的困惑度,再让它写句子。 顺便看清楚:为什么 n 不能一直加大,为什么生成必须靠采样。
上一节是「拆」,这一节是「造」。 拆的时候我们相信文法;造的时候连文法都不要了,只数频率。 它和 6.4 的隐马尔可夫模型是一路的:都是「给定前面的,预测后面的」, 区别在马尔可夫链只记前一个状态,n 元模型记前 n−1 个字。 这一节也是全书离今天最近的一节。 现在那些几十亿参数的大模型,做的还是这件事,只是把「数频率」换成了一层层网络。
一、二十句话也能训出一个模型
语料是手工写的二十个短句,十六句训练、四句留作测试, 一共 51 个不同的字(含句首标记和句号)。
| 训练集(十六句) | 测试集(四句) |
|---|---|
| 今天天气很好。 | 他去上班。 |
| 明天天气也很好。 | 她去上班。 |
| 我喜欢吃苹果。 | 今天我去学校。 |
| 小猫喜欢吃鱼。 | 明天他去上班。 |
| 这个语料小得可怜,只够演示原理。 | |
| 真实的语言模型,语料是几十亿到几万亿个词。 | |
| 但小语料有个好处:每一处失败都能追到具体是哪句没覆盖到。 | |
| 大语料里你只能看到困惑度差一点,看不到原因。 |
二、一元模型:只会数频率
最省事的办法是完全不看前文,只按每个字出现的次数抽。这就是一元模型。
| 字 | 次数 | 字 | 次数 |
|---|---|---|---|
| 。 | 16 | 气 | 5 |
| 天 | 8 | 好 | 5 |
| 欢 | 6 | 很 | 4 |
| 喜 | 6 | 吃 | 4 |
| Demo 让一元模型写了四句,一句都不通:「香。天天就甜果茶欢白就今玩天茶好」、 | |||
| 「欢色很色。我出太吃天小欢。狗。喜」。 | |||
| 字全是这二十句话里的字,一个生字都没有。 | |||
| 可句子不通,因为抽每个字的时候,它根本不知道前面写了什么。 | |||
| 「。 」出现在第一位不是偶然——它是最高频的字(16 次),随手一抽就抽到。 | |||
| 这也是 n 元模型要解决的第一件事。 |
三、n 元模型:记住见过的搭配,也困在里面
二元模型至少看一眼前一个字。给定「喜欢」,它能给的答案其实很少:
| 前文 | 下一个字 | 训练集里的次数 | 平滑后概率 |
|---|---|---|---|
| 喜欢 | 吃 | 4 | 0.4678 |
| 喜欢 | 喝 | 2 | 0.2339 |
| 天气 | 不 | 2 | 0.2649 |
| 天气 | 很 | 1 | 0.1325 |
| 天气 | 好 | 1 | 0.1325 |
| 天气 | 也 | 1 | 0.1325 |
| 「喜欢」后面,训练集里只出现过「吃」和「喝」。 | |||
| 所以模型只能在这两个里挑,跑不出第三种可能。 | |||
| 这就是 n 元模型的全部本事:记住见过的搭配,没见过的一个都编不出来。 | |||
| 它不会类比,不会推理,也不会犯「有创造性的错」。 | |||
| 注意「天气」那一行:四个候选的概率加起来正好是 1, | |||
| 而「不」拿了四分之一——因为「天气不好」在训练集里出现两次。 | |||
| 语料里多写一句,概率就往那边偏一点。 |
四、困惑度:n 越大不一定越好
要比较模型好坏,得有个数。困惑度可以读成「每个字上还有几个候选在犹豫」,越小越好。
| 模型 | 训练集 | 测试集不平滑 | 测试集强平滑 | 测试集加一平滑 |
|---|---|---|---|---|
| 1 元 | 32.86 | 无穷大 | 158.32 | 54.01 |
| 2 元 | 2.49 | 无穷大 | 32.64 | 41.06 |
| 3 元 | 2.36 | 无穷大 | 24.23 | 41.46 |
| 4 元 | 2.24 | 无穷大 | 28.07 | 42.64 |
| 训练集那一列随 n 一路下降,这是应该的——记得越多,训练集上当然越准。 | ||||
| 可测试集不跟着降。 | ||||
| 强平滑那一列最好是 3 元(24.23),加一平滑那一列最好是 2 元(41.06)。 | ||||
| 再往上反而变差。 | ||||
| 为什么不平滑那一列全是无穷大?因为只要有一个位置的概率是 0,整句概率就是 0, | ||||
| 取对数就是负无穷,困惑度算不出来。 |
| 模型 | 没见过的位置 | 占全部位置的比例 |
|---|---|---|
| 1 元 | 6 个 | 25.00% |
| 2 元 | 14 个 | 58.33% |
| 3 元 | 15 个 | 62.50% |
| 4 元 | 16 个 | 66.67% |
| n 越大,没见过的组合越多——四元模型有三分之二的位置是新的。 | ||
| 平滑只能拿越来越多的假概率去填,填得越多,越偏离真实分布。 | ||
| 这就是「维度灾难」在语言上的样子。 |
五、贪心和采样:生成的两条路
模型给出概率之后,怎么选下一个字?有两条路。 第一条是贪心:每一步都挑概率最高的那一个。Demo 让它写四条,四条一模一样: 「我喜欢吃苹果很甜。」 这句话本身是通的——它是训练集里两句的拼接(「我喜欢吃苹果」加「苹果很甜」)。 可它每次都走同一条路,因为最高概率那条路只有一条。 第二条是按概率抽。同一个模型,四条全不一样,而且大多不通: 「明不很狗里色苹好香狗我就热香奶家」。 贪心稳定但呆板,采样有变化但容易跑偏。 今天那些大模型都有一个「温度」参数,调的就是这两者之间的位置: 温度低接近贪心,温度高更随机。 这五节连起来是一句话: 语言模型只会算「下一个字是什么的概率」,别的都不会。 可就是这么一件事,做到了极大规模之后,看起来就像会说话了。
六、动手 Demo:让它一个字一个字往下猜
这个 Demo 从零搭一个 n 元语言模型:建词表、数条件频次、加平滑、算困惑度、生成。 全部用字典和循环,没有用任何现成库。 这也是全课最后一段「纯手工」的代码。
def build(seqs, n): # n 元表:键是(前 n-1 个字, 当前字)
C = {}
for s in seqs:
t = BOS * (n - 1) + s # 句首补齐,这样第一句也有前文
for i in range(len(t)):
ctx = t[max(0, i - n + 1):i]
if len(ctx) < n - 1:
continue
C[(ctx, t[i])] = C.get((ctx, t[i]), 0) + 1
return C
def perplex(C, seqs, n, alpha): # 加 alpha 平滑的困惑度
for s in seqs:
t = BOS * (n - 1) + s
for i in range(len(t)):
ctx = t[max(0, i - n + 1):i]
den = ctx_total(C, ctx)
c = C.get((ctx, t[i]), 0)
if c == 0 and alpha == 0.0:
return float(u"inf"), 0 # 概率 0,整句就废了
p = (c + alpha) / (den + alpha * NV)
tot += math.log(p)
return math.exp(-tot / cnt)
实跑输出:
--------------------------------------------------------------------
让它一个字一个字往下猜
--------------------------------------------------------------------
==============================================================
[1] 语料只有二十句话
前十六句训练,后四句留作测试:
训练 今天天气很好。
训练 明天天气也很好。
训练 昨天天气不太好。
训练 我喜欢吃苹果。
训练 他喜欢吃香蕉。
训练 我喜欢喝牛奶。
训练 她喜欢喝茶。
训练 小猫喜欢吃鱼。
训练 小狗喜欢吃骨头。
训练 天气好就出去玩。
训练 天气不好就待在家里。
训练 苹果很甜。
训练 香蕉也很甜。
训练 牛奶是白色的。
训练 茶是热的。
训练 我去学校。
测试 他去上班。
测试 她去上班。
测试 今天我去学校。
测试 明天他去上班。
一共 51 个不同的字(含句首标记 ^ 和句号)。
**这个语料小得可怜,只够演示原理。**
**真实的语言模型,语料是几十亿到几万亿个词。**
[2] 一元模型:只看每个字出现多少次
「下一个字是谁」完全不看前文,只按频率:
。(16) 天(8) 欢(6) 喜(6) 气(5) 好(5) 很(4) 吃(4) 我(3) 香(2) 蕉(2) 茶(2)
生成四条(从句首开始,按概率抽,最多 16 个字):
香。天天就甜果茶欢白就今玩天茶好
欢色很色。我出太吃天小欢。狗。喜
。。就气果茶好天。蕉吃很我喝我香
气。头苹气我喜很茶气不欢我苹茶的
**字都是这些话里的字,可句子不通。**
**因为抽每个字的时候,它根本不知道前面写了什么。**
[3] 二元、三元模型:至少看一眼前文
给定前两个字,模型能给出的下一个字其实很少:
前文 下一个字 训练集里的次数 平滑后概率
喜欢 吃 4 0.4678
喜欢 喝 2 0.2339
天气 不 2 0.2649
天气 很 1 0.1325
天气 好 1 0.1325
天气 也 1 0.1325
**「喜欢」后面,训练集里只出现过「吃」和「喝」。**
**所以模型只能在这两个里挑,跑不出第三种可能。**
**这就是 n 元模型的全部本事:记住见过的搭配,没见过的一个都编不出来。**
同样两个模型,各生成三条:
2 元模型:
出欢喝喜欢吃狗家狗学校。鱼就待热
天气不天天去。。校奶喜欢喝牛我去
昨学喜欢吃苹蕉。的在$
3 元模型:
出不是明茶。去白喝校是苹鱼在是学
天很骨今明学气。校头他茶出喜。里
昨学昨也果太出好的白$
[4] 用困惑度打分:n 越大越好吗
困惑度可以读成「每个字上还有几个候选在犹豫」,越小越好。
模型 训练集 测试集不平滑 测试集强平滑 测试集加一平滑
1 元 32.86 无穷大 158.32 54.01
2 元 2.49 无穷大 32.64 41.06
3 元 2.36 无穷大 24.23 41.46
4 元 2.24 无穷大 28.07 42.64
**训练集那一列随 n 一路下降,这是应该的——记得越多,训练集上越准。**
**可测试集不跟着降:强平滑列最好是 3 元(24.23),加一平滑列最好是 2 元(41.06)。**
**再往上反而变差——n 越大,没见过的组合越多,平滑只能拿越来越多的假概率去填。**
不平滑那一列全是无穷大,原因很具体:
模型 没见过的位置 占全部位置的比例
1 元 6 个 25.00%
2 元 14 个 58.33%
3 元 15 个 62.50%
4 元 16 个 66.67%
**只要有一个位置的概率是 0,整句的概率就是 0,困惑度算不出来。**
**加一平滑就是给每个没见过的组合留一点点概率。**
**代价是从见过的组合那里匀走一部分,所以强平滑那一列的数明显更差。**
**用一点偏差换不炸,这就是全部技巧的核心。**
[5] 贪心每次一样,采样才有变化
每一步都挑概率最高的那个字(贪心),四条:
我喜欢吃苹果很甜。
我喜欢吃苹果很甜。
我喜欢吃苹果很甜。
我喜欢吃苹果很甜。
同一个模型,改成按概率抽,四条:
明不很狗里色苹好香狗我就热香奶家
小。出奶出今天天校吃狗班玩不喝好
天太他苹去上上出是牛吃猫骨热蕉猫
我牛热我狗果在猫不茶我天好果$
**四条一模一样——最高概率那条路只有一条,它每次都走同一条。**
**采样每条路都走一点,看起来才有变化。**
**今天那些大模型都有一个「温度」参数,做的就是这件事:温度调低就接近贪心,调高就更随机。**
五段连起来:**一元模型只会数频率,句子不通**;**看上前一个字就通顺一些**;
**可 n 一大就撞上没见过的组合**;
**平滑拿一点偏差换不炸**;**而生成终究得靠采样。**
第 2 段那四句要读出声。字全是熟悉的字, 串起来却不是话——这就是「只数频率、不看上下文」到底缺了什么。 第 3 段那张表是 n 元模型的全部家底:六行就是「喜欢」和「天气」两个前缀的所有可能。 注意前三行加起来不到 1,剩下的概率被平滑分给了没出现过的字。 第 4 段那张困惑度表要横着读: 训练集越读越顺(32.86 到 2.24),测试集却在 3 元那里触底反弹。 这中间的差就是过拟合,量得清清楚楚。 第 5 段那八句是这一节的落点: 同样一个模型,贪心给出的是通顺但重复的句子,采样给出的是有变化但荒唐的句子。 「怎么选」和「模型多好」是两件独立的事。
30 秒实验
- 把第 1 段训练集加三句「我喜欢吃香蕉」,重跑第 5 段的贪心。 生成结果不变——加句子只改概率,不改第一名是谁。
- 把第 4 段的强平滑系数从 0.1 改成 1,重跑。 测试困惑度会整体抬高——平滑越强,模型越保守,越不敢给见过的组合高概率。
- 把第 5 段的采样温度调成 0.2(概率取 1/T 次方再归一),重跑。 四条会开始趋同——温度趋 0 就是贪心,这条曲线能自己走出来。
常见误区
- 以为「生成就是模型在思考」。它只是在算下一个字的概率分布, 然后抽一个。所谓「灵感」,就是抽样里的随机性。
- 以为「n 越大模型越好」。Demo 第 4 段:四元模型的测试困惑度比三元还差。 参数变多不等于泛化变好,数据不够时正好相反。
- 以为「困惑度低就一定写得好」。贪心生成的困惑度很低(每步都挑最高概率), 可它写出来的句子重复、呆板。困惑度量的是「像不像语料」,不是「好不好」。
- 以为「平滑是可有可无的补丁」。不加平滑,测试集困惑度直接是无穷大。 它在数学上是必需的,不是工程上的讲究。
自测三问(点开对答案)
1. 为什么一元模型写出来的句子不通? —— 因为**它抽每个字的时候完全不知道前面写了什么**。 **「天气」后面本该接「好」「不」这类字,一元模型只按全局频率抽,** **所以「。」这种最高频的字会随时冒出来。** 2. 困惑度到底在量什么? —— 量的是**「模型平均在每个位置上有多少个候选在犹豫」**。 **困惑度 32.86,相当于每次都在三十多个字里挑;降到 2.49,相当于只剩两个半候选。** **它等价于「每个字的平均负对数似然的指数」,所以越小越好。** 3. 为什么不能只靠贪心生成? —— 因为**最高概率那条路只有一条,贪心每次都走同一条**——**Demo 第 5 段四条输出一字不差。** **要多样性就必须引入随机,而随机一多又容易跑偏,温度参数调的就是这个平衡。**第 11 章到这里结束。 下一章换个话题:这些分析出来的东西,最后存在哪里、怎么找回来。 数据库、检索、语义网,三节把「知识」落到具体的存储结构上。
- ☐ (本章完,下一节 12.1 数据库)