📕 第6章 深度学习的应用
📄 一句话概括

语言模型就是「给前文,猜下一个」。字符级版本把词表缩到 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 秒实验

  1. 词表要是 1000 个词,随机猜的损失和困惑度各是多少?
  2. 语料只重复 10 遍(而不是 40 遍),1200 轮后困惑度会更高还是更低?
  3. 为什么字符级模型拼得出像英文的假词,却写不出通顺的长句?

常见误区

  1. 以为困惑度可以跨语料直接比。它和词表大小挂钩。 词表 21 的 1.13 和词表 50000 的 30,后者反而更强。
  2. 以为它「理解了意思」。它只学了字符的局部共现。换一个没见过的词,它照样往下拼。
  3. 以为损失 0.12 说明模型很强。这套语料只有两句话, 1.13 的困惑度本质上是背下来了,不是学会了英语。
自测三问(点开对答案) 1. 困惑度的定义是什么? —— 损失的指数,也就是 e 的损失次方。 它等于「模型平均在几个候选之间犹豫」,随机猜时正好等于词表大小。 2. 为什么字符级语言模型的参数可以很少? —— 所有位置共用同一组权重,参数不随序列长度增长。 这套 21 个字符、32 维隐层的模型只有 2421 个参数。 3. 为什么它写不出通顺的长句? —— 它的隐状态只有 32 维,只装得下前 20 个字符的摘要。 局部规律学得会,长距离的一致性和语义装不下。

一次吐一个字符,只能「续写」。 6.5 要做的是「读一整句,再写一整句」——那就得把编码和解码分成两个网络。

相关笔记