语言模型就是「给前文,猜下一个」。字符级版本把词表缩到 21 个字母加标点,一个 RNN 就够。 实测 2421 个参数、1200 轮:损失从随机猜的 3.0445 降到 0.1230。困惑度从 21.00 降到 1.13。 困惑度就是「平均在几个候选之间犹豫」。1.13 意味着几乎不用犹豫。 它写不出新句子,但「learning 后面跟 is」「layers 后面跟 of」这类局部规律它学到了。
6.1 的 RNN 在这里第一次派上正经用场。结构一个字没改,只是每个时间步都要吐一个分布。 6.5 的 Seq2Seq 是「读一整句、写一整句」,比这里多了编码器和解码器的分工。 第 4 章算过交叉熵,这里的损失就是每个位置交叉熵的平均值。
一、任务:滑动窗口
语料 3160 个字符,去重后 21 个。窗口长 20,输入 20 个字符,标签是第 21 个。 在整段上滑动,一篇语料能造出几千条训练样本。这是自监督:标签不用人标,从前文里自己切出来。 所有位置共用同一组权重,这就是 6.1 说的权重共享。也是为什么 2421 个参数就够。 如果换成「每个位置一组独立权重」,20 个位置就要 20 份参数, 而且第 21 位的权重永远学不到。共享是必需的,不是优化。
二、困惑度:把损失翻译成人话
困惑度等于 e 的损失次方。损失是交叉熵,单位是 nat(自然对数)。 随机猜:词表 21,损失是 ln21 等于 3.0445,困惑度 21.00。正好等于「在 21 个字符里闭眼挑」。 训练后损失 0.1230,困惑度 1.13。意思是平均只在 1.13 个字符之间犹豫。 这个指标的妙处是它的单位和词表挂钩。同样损失 0.12,词表 21 算很强,词表 50000 就一般。 所以跨模型比的时候,要么比困惑度,要么把词表大小一起报出来。只报损失容易误导。
三、实测曲线
| 轮数 | 损失 | 困惑度 |
|---|---|---|
| 200 | 1.7368 | 5.68 |
| 400 | 0.5517 | 1.74 |
| 600 | 0.2832 | 1.33 |
| 800 | 0.2025 | 1.22 |
| 1000 | 0.1342 | 1.14 |
| 1200 | 0.1230 | 1.13 |
前 400 轮掉得最猛,5.68 到 1.74。 因为先学会的是粗规律:字母后面一般跟字母,句号后面一般跟空格。 后面掉得慢,剩下的是「哪个字母跟哪个字母」这种细活。每一项都要多花几百轮。 语料只有两句话重复 40 遍, 所以 1200 轮就快背下来了。1.13 已经接近这套语料的上限—— 再训下去只会更低,但那是记忆不是泛化。
四、采样看看
拿「deep 」当开头,一次一个字符往外吐,共 60 个:
deep kanh laayking. it stacks manh layers of neuro 换行 ns. deep learni
kanh、laayking、manh 是它自己拼出来的词,语料里没有。
但它拼得像英文——因为它学到了字母的组合规律:辅音后面跟元音、词尾常是 ing。
layers of 后面接 neuro(该是 neurons)——
方向对了,长度没算准。这类「差一点点」是字符级模型最典型的表现。
局部像、全局散。它在 20 个字符的窗口里很稳,超过窗口就散了。
五、从字符到词
教材 6-4 节讲的就是这个结构,只是词表从 21 个字符换成几万个词,隐层从 32 换成几百。 词表一大,输出层就变贵:几万乘几百,光这一层就上千万参数。而它占的算力比整个循环部分还多。 所以真正的语言模型要处理的大问题是「输出层太贵」, 采样技巧、softmax 近似、层次 softmax,全是为这个。 结构没变,工程全在规模上。这句话在深度学习里几乎处处成立。
六、动手 Demo:一个字符一个字符往下猜
语料是两句英文重复 40 遍。 建一个 21 个字符词表、32 维隐层的字符级模型,训练 1200 轮,每 200 轮打一次损失和困惑度。 最后拿「deep 」当开头让它写 60 个字符。
import numpy as np
corpus = ("deep learning is a kind of machine learning. "
"it stacks many layers of neurons. ") * 40
chars = sorted(set(corpus)) # 去重后 21 个
idx = dict((c, i) for i, c in enumerate(chars))
ids = np.array([idx[c] for c in corpus])
T = 20 # 看前 20 个
m = CharLM(len(chars), 32, np.random.default_rng(4)) # 2421 个参数
print("随机猜的损失", "%.4f" % np.log(len(chars))) # 3.0445
for k in range(1200):
st = rg.integers(0, len(ids) - T - 1, 16)
X = np.stack([ids[s:s + T] for s in st])
Y = np.stack([ids[s + 1:s + T + 1] for s in st])
loss, g = m.loss_grads(X, Y)
m.apply(g, 0.2)
if (k + 1) % 200 == 0:
print(k + 1, "%.4f" % loss, "困惑度 %.2f" % np.exp(loss))
gen = m.sample(idx[" "], 60, rg) # 接着往下写 60 个
print("deep " + "".join(chars[i] for i in gen))
实跑输出:
--------------------------------------------------------------------
RNN 语言模型:一个字符一个字符往下猜
--------------------------------------------------------------------
语料是两句英文重复 40 遍,共 3160 个字符,去重后词表 21 个字符。
**任务是「看一眼前 20 个字符,猜第 21 个」,然后在整段上滑动窗口。**
量 值
----------------------------
词表大小 21
隐层宽度 32
参数个数 2421
随机猜的损失 3.0445
训练 1200 轮,每轮 16 条窗口。损失与困惑度:
轮数 损失 困惑度
--------------------------------
200 1.7368 5.68
400 0.5517 1.74
600 0.2832 1.33
800 0.2025 1.22
1000 0.1342 1.14
1200 0.1230 1.13
**困惑度就是「平均在几个字符之间犹豫」。**
词表 21 个字符,闭着眼睛猜是 21.00;训练后降到 1.13,意思是它几乎不再犹豫,
下一个字符基本能定下来。
拿「deep 」当开头,让它自己往下写 60 个字符:
deep kanh laayking. it stacks manh layers of neuro
ns. deep learni
**它写不出新句子,但它把「learning 后面该跟 is」「layers 后面该跟 of」这些局部规律学到了。**
这正是语言模型的全部工作:猜下一个。
教材 6-4 节讲的 RNN 语言模型就是这个东西,只是词表从字符换成了词,隐层从 32 换成了几百。
**结构一个字没变。**
先看那张小表:词表 21、隐层 32、参数 2421、随机猜的损失 3.0445。
2421 这个数可以手算:21 乘 32 加 32 乘 32 加 32,再加 32 乘 21 加 21。
随机猜的损失就是 ln21。
注意它和参数多少无关,只和词表大小有关。这是判断「模型有没有开始学」的第一条基准线。
六行训练记录:损失 1.7368、0.5517、0.2832、0.2025、0.1342、0.1230。
前两行掉了 3 倍多,后四行只掉了 4 倍多但每步越来越小。
困惑度那一列更直观:5.68、1.74、1.33、1.22、1.14、1.13。
从「在 5 个多字符里犹豫」到「几乎不用犹豫」。
采样出来的两行要念出声:deep kanh laayking. it stacks manh layers of neuro / ns. deep learni。
词是假的,拼法是真的。
neuro 后面接 ns 而不是直接接 ns.,是因为它记住了「neurons」这个词的结尾。
它不知道 neurons 是什么意思,但它知道这串字母后面该跟什么。
这就是语言模型的全部工作:猜下一个。理解语义是猜得足够准之后才出现的副产品,
不是它一开始的目标。
30 秒实验
- 词表要是 1000 个词,随机猜的损失和困惑度各是多少?
- 语料只重复 10 遍(而不是 40 遍),1200 轮后困惑度会更高还是更低?
- 为什么字符级模型拼得出像英文的假词,却写不出通顺的长句?
常见误区
- 以为困惑度可以跨语料直接比。它和词表大小挂钩。 词表 21 的 1.13 和词表 50000 的 30,后者反而更强。
- 以为它「理解了意思」。它只学了字符的局部共现。换一个没见过的词,它照样往下拼。
- 以为损失 0.12 说明模型很强。这套语料只有两句话, 1.13 的困惑度本质上是背下来了,不是学会了英语。
自测三问(点开对答案)
1. 困惑度的定义是什么? —— 损失的指数,也就是 e 的损失次方。 它等于「模型平均在几个候选之间犹豫」,随机猜时正好等于词表大小。 2. 为什么字符级语言模型的参数可以很少? —— 所有位置共用同一组权重,参数不随序列长度增长。 这套 21 个字符、32 维隐层的模型只有 2421 个参数。 3. 为什么它写不出通顺的长句? —— 它的隐状态只有 32 维,只装得下前 20 个字符的摘要。 局部规律学得会,长距离的一致性和语义装不下。一次吐一个字符,只能「续写」。 6.5 要做的是「读一整句,再写一整句」——那就得把编码和解码分成两个网络。
- ☐ (本节完,下一节 6.5 Sequence-To-Sequence 模型)