第 11 章 RNN:把过去带到当前
11.1 线索离开窗口后,还能留下什么?
Section titled “11.1 线索离开窗口后,还能留下什么?”读到 blue key is,我们要回答钥匙的颜色。只接收最后两个 token 的模型看到的是 key is。把开头的 blue 换成 red,正确答案变了,模型收到的输入却没变。第 10 章 已经说明:区别没有进入计算,再怎么改权重也找不回来。扩大窗口可以把这条边界推远,但窗口究竟开多大,仍要预先决定。
换一种办法:读到 blue 时,先算出一小组数值并留下来。读到 key 时,把当前输入和留下的数值一起用于计算,再保存新的结果。读到 is 时,重复同样的操作。当前输入的大小仍然固定,但较早的输入多了一条影响当前预测的路径。有了这条路径,就一定能答对吗?
我们仍用红、蓝钥匙任务检验这个办法,先算三个 token,再用第 10 章那对更长的输入。判定标准是首个 token 给出的颜色,候选顺序固定为 [red, blue]。每次选概率较高的候选;相同时选排在前面的候选。下面的权重由作者给定,用于诊断机制。这不是训练好的语言模型,也不代表普通 RNN 的准确率。
计算用到 第 5 章的行向量、第 6 章的 Softmax,以及 第 7 章的 ReLU 和训练过程。用到这些操作时,我们会简短唤回它们的含义。代码可以跳过;选择编程路径的读者应先完成 Python 入门。
11.2 在三个时刻,重复使用同一个操作
Section titled “11.2 在三个时刻,重复使用同一个操作”例 11.1 — 把红色线索传过三次读取。 输入严格规定为 red key is,空格分开的每个词各算一个 token。本例的完整词表、ID 和给定的嵌入向量如下。
表 11.1 — 小模型的全部输入向量。
| Token | ID | 输入向量 |
|---|---|---|
| red | 0 | [1, 0] |
| blue | 1 | [0, 1] |
| filler | 2 | [0, 0] |
| key | 3 | [0, 0] |
| is | 4 | [0, 0] |
ID 用来选行,不是参与乘法的数量。这里的零向量是为已知 token 特意设定的,既不表示词缺失,也不表示填充位置。这两个坐标便于我们看清颜色算例;普通模型学出的坐标并不天然带有这些含义。
把最初留下的数值设为 [0, 0]。每读一个 token,先把旧数值的两个坐标各减半,再逐项加上当前输入,最后把负数改成零。最后一步就是 ReLU:正数保留,负数截成零。例如,[-1, 0.5] 经过 ReLU 得到 [0, 0.5]。本例三步中的数值都不小于零,因此截断不会改变结果。
先沿着时间表算一遍,再写公式。表中换了时刻,参数设置并没有跟着换。
表 11.2 — 一套参数,用三次。
| 读取时刻 | 当前输入 | 旧数值 | 使用的参数 | 新数值 |
|---|---|---|---|---|
| 1:red | [1, 0] | [0, 0] | 输入原样加入;旧数值乘 0.5 | [1, 0] |
| 2:key | [0, 0] | [1, 0] | 同一套设置 | [0.5, 0] |
| 3:is | [0, 0] | [0.5, 0] | 同一套设置 | [0.25, 0] |
第二次读取时,key 自己贡献零,但计算并非从零开始:第一次留下的 [1, 0] 减半后参与了计算。第三次能接到的旧数值已经是 [0.5, 0]。我们把它减半得到 [0.25, 0],并没有回头重新取出 red 的原始嵌入。
这组传给下一步的内部数值叫作 隐藏状态(hidden state)。“隐藏”表示它是内部激活值,而不是直接观察到的答案,并非说它不能被查看。用当前输入和上一步状态计算下一步状态的网络,叫作 循环神经网络(recurrent neural network,RNN)。这里的循环或递归,指状态更新反复进行,不是句子里某个英文词出现了多次。Elman 的简单循环网络较早展示了把先前隐藏激活送入后续计算的办法;本章的小权重是另外构造的教学例子。Elman,1990
现在给这些量起名。用 数第几次读取, 表示本次输入行向量, 表示本次读取完成后的隐藏状态。它们在这里都只有两个坐标,初始状态为 。全部权重和偏置如下:
三个矩阵都是两行两列。行向量乘单位矩阵后保持原样;乘 后,各坐标减半,两个坐标不会相互混合。隐藏层偏置 不额外加入任何数值。输出偏置 则给红色一个小优势,蓝色线索必须足够强,才能超过它。加上表 11.1 的五行嵌入,这就列全了模型的数值参数。
第一式写的正是表 11.2 的操作。接着, 按 [red, blue] 的顺序保存两个输出分数,也就是 logits; 保存它们的概率。隐藏坐标和概率不是一回事:隐藏坐标之和不必为一,两个候选的概率之和则必须为一。本例中的数值都没有物理单位,不是秒数、计数或金额; 数的是位置,也不是实际经过的时长。
把第三次读取的数值代回去,得到 和 。按第 6 章的规则,先对分数取指数,再除以 两个输出候选 的指数值之和:
分母不是输入 token 的个数,也不是隐藏坐标的个数,而是两个候选得到的正权重总和。剩下约 0.407333 的概率分给蓝色,因此本例回答红色。
表 11.3 — 三步输出的完整日志。
| 读取时刻 | 新隐藏状态 | Logits:红、蓝 | 概率:红、蓝 |
|---|---|---|---|
| 1 | [1, 0] | [1.125, 0] | [0.754915, 0.245085] |
| 2 | [0.5, 0] | [0.625, 0] | [0.651355, 0.348645] |
| 3 | [0.25, 0] | [0.375, 0] | [0.592667, 0.407333] |
合看表 11.1—11.3,就能找到每一步的输入、旧状态、新状态和输出。中间两步的颜色预测也列出来,方便检查;本任务只在读完 is 后判分。预测下一个 token 的语言模型也可以采用这种递归结构,只是输出词表不同,并且可以在每次读取后都有一个预测目标。
- 文本片段输入:red
- 一组数字旧状态 h:[0, 0]
- 模型保存的数值同一组 Wₓ、Wₕ、bₕ
- 一组数字新状态 h:[1, 0]
输入 + 旧状态 → 更新 → 新状态 → 下一次读取。
- 文本片段输入:key
- 一组数字旧状态 h:[1, 0]
- 模型保存的数值同一组 Wₓ、Wₕ、bₕ
- 一组数字新状态 h:[0.5, 0]
输入 + 旧状态 → 更新 → 新状态 → 下一次读取。
- 文本片段输入:is
- 一组数字旧状态 h:[0.5, 0]
- 模型保存的数值同一组 Wₓ、Wₕ、bₕ
- 一组数字新状态 h:[0.25, 0]
输入 + 旧状态 → 更新 → 新状态 → 下一次读取。
图 11.1 把时间展开,使每次更新收到什么一目了然。重复画出的参数框指向同一组已保存矩阵,并不是三个可以各学各的模型。传递状态的箭头表示计算依赖:第三步必须等第二步给出结果。
11.3 换一条线索,再把它放远一些
Section titled “11.3 换一条线索,再把它放远一些”例 11.2 — 用蓝色检查线索是否起作用。 只把首个 token 换成 blue,三步状态就变成 [0,1]、[0,0.5]、[0,0.25]。最后的 logits 是 [0.125,0.25],蓝色以 0.531209 的概率胜出。规则没有改变;输入的区别沿着状态传下来,改变了最终选择。
为了公平比较,固定窗口模型也使用表 11.1 的同一套嵌入和同一个输出头。具体办法是:拼接末尾两个输入向量,把它们的对应坐标相加,经过 ReLU,再加 [0.125,0]。这个两词前馈基线的参数已完全确定:四维到二维的权重矩阵依次有 [1,0]、[0,1]、[1,0]、[0,1] 四行,隐藏层偏置为零。它不是第 10 章训练过的 NNLM。由于末尾两个词总是 key is,这个基线送给输出头的始终是 [0,0],红色概率始终为 0.531209。
短序列中,RNN 对两种颜色都答对了,窗口基线只答对红色。现在恢复第 10 章的长输入,判定标准和两种模型的参数都保持不变:
red filler filler filler filler filler filler key isblue filler filler filler filler filler filler key is每行有九个 token。读入颜色后,还要经过八次状态更新,每次都把留下的颜色数值减半。最后剩下的坐标是 。
表 11.4 — 同一个长距离任务,暴露两种不同的失败。
| 输入颜色 | 两词窗口的 logits | RNN 最终状态 | RNN 的 logits | RNN 的红色概率 | RNN 的选择 |
|---|---|---|---|---|---|
| red | [0.125, 0] | [1/256, 0] | [0.12890625, 0] | 0.532182 | 红色:正确 |
| blue | [0.125, 0] | [0, 1/256] | [0.125, 0.00390625] | 0.530236 | 红色:错误 |
两种方法如今都只答对这两个特意挑选的样例中的一个,原因却不一样。固定窗口收到的 输入和给出的分布完全相同。RNN 的状态和分布仍有区别,只是蓝色的微小贡献不足以超过红色偏置。传递信息的路径存在,但这套设置没能为最终决策保留足够的优势。若把两种失败都说成“没有记忆”,就会掩盖决定怎样修复的关键区别。
蓝色坐标并没有被舍入成零。即使用精确分数计算,1/256 也小于 1/8,所以这次失败不需要浮点数下溢就会发生。改变保留权重或输出偏置,可能改变答案;这仍不能证明普通语言任务中的所有远处线索都能被保留下来。
11.4 运行改变状态,学习改变参数
Section titled “11.4 运行改变状态,学习改变参数”多读一个 token,RNN 不需要多保存一套参数。这种 参数共享(parameter sharing) 让同一条更新规则可以处理不同长度的序列。不过,能处理不同长度,不等于在所有长度上表现一样;表 11.4 就是一个反例。
普通 模型运行(inference) 期间,给定的矩阵保持不变,每个输入却会算出新的隐藏状态。每开始一个独立的红、蓝测试,我们都把状态清零。否则,第二个测试可能继承第一个测试留下的数值,就不再是独立检验所声明的输入了。应用可以有意在同一序列的不同片段之间接续状态,但必须说明片段边界怎样处理。
训练多了一类操作:网络先算状态和预测,再把需要评价的预测与目标比较,得到损失。梯度衡量参数变化会怎样影响损失,优化器据此更新参数。本章考虑的是整段序列训练:一次前向计算期间,共享权重不变;优化器更新之后,再用新权重重新计算下一轮状态。仅仅更新状态,并不等于学习。
表 11.5 — 什么发生变化,由什么触发?
| 对象 | 普通前向运行 | 一个训练步骤 |
|---|---|---|
| 输入 token | 按序列提供 | 由训练样本提供 |
| 隐藏状态 | 每次读取都重新计算 | 前向计算时更新;为求梯度保留或重算激活值 |
| 共享参数 | 固定 | 求出梯度后,由优化器修改 |
| 正确颜色目标 | 不能拿来直接选答案 | 用来评价损失 |
计算安排上仍有代价。不能同时启动三个相互独立的状态更新来求 ,因为 依赖 ,后者又依赖 。一个时间步内部的矩阵运算可以并行,一个批次中的不同序列也可以并行,但同一序列各次读取之间的依赖并未消失。这是在分析计算关系,不是在报告硬件耗时。
隐藏状态也不是聊天记录、应用的长期记忆,或 智能体状态(Agent State)。智能体状态可能包含明确的订单 ID、工具结果、是否批准取消等记录。隐藏向量由网络权重计算出来,既不能补上一条没有发生的订单查询,也不能授予取消权限。同样,如果应用只提供旧的 600 元住宿限额文档,递归本身不会把更新后的 750 元政策塞进输入。第 1 章建立的系统边界仍然适用。
11.5 不训练大模型,也能复现这次失败
Section titled “11.5 不训练大模型,也能复现这次失败”共享数据 data/part-iii/sequence-v1.json 固定了嵌入、权重、候选顺序,以及后面会用到的翻译数据。长输入直接读取既有的第 10 章数据文件,并记录路径和哈希,没有换成更容易的样例。expected.json 保存独立按分数推导的预期结果。上文数值来自这些教学规则;配套的 run.json 是程序实际复算的记录,不是训练日志。
完成 Python 入门后,可以在书籍仓库根目录运行下面的命令。程序只用 Python 标准库,在 CPU 上执行,不下载任何内容,并打印完整的状态和失败日志。
python3 code/part-iii/run.py第一个 Notebook 位于 notebooks/part-iii/01-state-and-gradient.ipynb,会把同一计算延续到第 12 章。本章的实践目标是:找出从哪次更新开始,蓝色不再胜出,并用当时的状态和红色偏置解释原因。不运行代码,直接分析表 11.2—11.4 并完成习题 1,也能达到核心学习目标。
11.6 本章小结
Section titled “11.6 本章小结”固定窗口限制哪些 token 能进入当前预测。递归增加了一份算出来的状态,让较早的 token 可以影响较晚的预测。展开后的三步使用同一组参数,变化的是输入和状态。信息因此多了一条传递路径,计算也多了一层顺序依赖,但可靠保留线索并没有得到保证。
本例中,远处的蓝色线索仍传到了输出端,只是影响太小,没能胜出。训练也许能调整造成衰减的权重。第 12 章 要追问:末端的一次错误,怎样沿着同一串状态更新,把有用的训练信号传回去?
11.7 习题
Section titled “11.7 习题”- 找出边界。 在
blue和key is之间插入filler。沿用原权重和平局规则,最少插入几个才会让 RNN 选择红色?写出最终状态和两个 logits。 - 只改一个参数。 把递归矩阵的对角元素都改成 1,其他数值不变。计算长蓝色样例的最终状态与选择。如果一条输入流含有许多个
blue,又可能出现什么问题? - 诊断日志。 程序员说,读完 red、key、is 后,隐藏向量从
[1,0]变成[0.25,0],说明模型被修改了。什么证据才能证明参数确实更新过?为什么每读一个 token 前都清零状态,会破坏例 11.1? - 选择证据。 某系统答对了两个长距离颜色问题。这能证明它用了 RNN、权重发生变化,或能找到当前差旅政策吗?选择其中一个说法,说明还需要怎样的受控观察。
- 追踪责任。 订单助手生成
approved后,隐藏向量发生变化;应用记录却仍显示取消操作尚未获批。该依据哪份记录决定能否操作?为什么变化的隐藏向量不能替代它?
11.8 参考解答
Section titled “11.8 参考解答”- 没有 filler 时,蓝色坐标为 1/4。一个 filler 多带来一次减半,最后剩 1/8;logits 变成
[1/8,1/8],按平局规则选择红色。因此一个就够,两个则会让蓝色分数严格小于红色。数清线索之后的状态转换次数,不要把全部 token 数当成减半次数。 - 后续每个零输入都让旧状态保持原样。长蓝色样例最后得到
[0,1],logits 为[0.125,1],因此选择蓝色。但不断读入 blue 会让对应坐标每次加一,这里没有选择性清除或覆盖的机制。能保住这条孤立线索,不等于解决了哪些信息该留下的问题。 - 应比较更新前后保存的参数,并查看对应的优化器操作。权重固定时激活值变化,本来就是正常现象。如果每次读取前都清零,就移除了旧状态的贡献,最后的 is 只能产生
[0,0],输出头便会不顾原线索而偏向红色。 - 两个正确字符串不能证明其中任何一项。例如,要调查是否发生学习,可以固定输入和选择设置,比较参数快照及更新记录。答对颜色问题,也不能证明另一个应用环节提供了当前政策文档。
- 应以应用中明确的审批记录和执行规则为准。生成的 token 及其隐藏激活都是数值计算的结果,不是审批事件,也不能证明取消操作已经获得授权。
11.9 参考文献
Section titled “11.9 参考文献”- Elman,Finding Structure in Time(1990)。第 181—185 页说明先前的隐藏激活怎样提供时间上下文;本章的 ReLU 权重由本书另行构造。
- Cho 等,Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation(2014)。第 2.1 节给出递归更新与输出分布的基本形式,后续各节为接下来的章节提供背景。
- Zhang 等,Dive into Deep Learning,第 9.7 节。开头把时间展开图与共享参数联系起来,为第 12 章的训练问题作准备。