跳转到内容

第 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 — 小模型的全部输入向量。

TokenID输入向量
red0[1, 0]
blue1[0, 1]
filler2[0, 0]
key3[0, 0]
is4[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

现在给这些量起名。用 tt 数第几次读取,xtx_t 表示本次输入行向量,hth_t 表示本次读取完成后的隐藏状态。它们在这里都只有两个坐标,初始状态为 h0=[0,0]h_0=[0,0]。全部权重和偏置如下:

Wx=Wy=[1001],Wh=[0.5000.5],bh=[0,0],by=[0.125,0].W_x=W_y=\begin{bmatrix}1&0\\0&1\end{bmatrix},\qquad W_h=\begin{bmatrix}0.5&0\\0&0.5\end{bmatrix},\qquad b_h=[0,0],\quad b_y=[0.125,0].

三个矩阵都是两行两列。行向量乘单位矩阵后保持原样;乘 WhW_h 后,各坐标减半,两个坐标不会相互混合。隐藏层偏置 bhb_h 不额外加入任何数值。输出偏置 byb_y 则给红色一个小优势,蓝色线索必须足够强,才能超过它。加上表 11.1 的五行嵌入,这就列全了模型的数值参数。

ht=ReLU⁡(xtWx+ht−1Wh+bh),zt=htWy+by,pt=softmax⁡(zt).h_t=\operatorname{ReLU}(x_tW_x+h_{t-1}W_h+b_h),\qquad z_t=h_tW_y+b_y,\qquad p_t=\operatorname{softmax}(z_t).

第一式写的正是表 11.2 的操作。接着,ztz_t 按 [red, blue] 的顺序保存两个输出分数,也就是 logits;ptp_t 保存它们的概率。隐藏坐标和概率不是一回事:隐藏坐标之和不必为一,两个候选的概率之和则必须为一。本例中的数值都没有物理单位,不是秒数、计数或金额;tt 数的是位置,也不是实际经过的时长。

把第三次读取的数值代回去,得到 h3=[0.25,0]h_3=[0.25,0] 和 z3=[0.375,0]z_3=[0.375,0]。按第 6 章的规则,先对分数取指数,再除以 两个输出候选 的指数值之和:

p3(red)=e0.375e0.375+e0≈1.4549911.454991+1≈0.592667.p_3(\text{red})=\frac{e^{0.375}}{e^{0.375}+e^0} \approx\frac{1.454991}{1.454991+1}\approx0.592667.

分母不是输入 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 的语言模型也可以采用这种递归结构,只是输出词表不同,并且可以在每次读取后都有一个预测目标。

第 1 次读取
  1. 文本片段输入:red
  2. 一组数字旧状态 h:[0, 0]
  3. 模型保存的数值同一组 Wₓ、Wₕ、bₕ
  4. 一组数字新状态 h:[1, 0]

输入 + 旧状态 → 更新 → 新状态 → 下一次读取。

第 2 次读取
  1. 文本片段输入:key
  2. 一组数字旧状态 h:[1, 0]
  3. 模型保存的数值同一组 Wₓ、Wₕ、bₕ
  4. 一组数字新状态 h:[0.5, 0]

输入 + 旧状态 → 更新 → 新状态 → 下一次读取。

第 3 次读取
  1. 文本片段输入:is
  2. 一组数字旧状态 h:[0.5, 0]
  3. 模型保存的数值同一组 Wₓ、Wₕ、bₕ
  4. 一组数字新状态 h:[0.25, 0]

输入 + 旧状态 → 更新 → 新状态 → 下一次读取。

图 11.1 — 同一个递归更新用三次,数值状态沿时间传递。 本书原创教学算例,CC BY-SA 4.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 is
blue filler filler filler filler filler filler key is

每行有九个 token。读入颜色后,还要经过八次状态更新,每次都把留下的颜色数值减半。最后剩下的坐标是 0.58=1/256=0.003906250.5^8=1/256=0.00390625。

表 11.4 — 同一个长距离任务,暴露两种不同的失败。

输入颜色两词窗口的 logitsRNN 最终状态RNN 的 logitsRNN 的红色概率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按序列提供由训练样本提供
隐藏状态每次读取都重新计算前向计算时更新;为求梯度保留或重算激活值
共享参数固定求出梯度后,由优化器修改
正确颜色目标不能拿来直接选答案用来评价损失

计算安排上仍有代价。不能同时启动三个相互独立的状态更新来求 h3h_3,因为 h3h_3 依赖 h2h_2,后者又依赖 h1h_1。一个时间步内部的矩阵运算可以并行,一个批次中的不同序列也可以并行,但同一序列各次读取之间的依赖并未消失。这是在分析计算关系,不是在报告硬件耗时。

隐藏状态也不是聊天记录、应用的长期记忆,或 智能体状态(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,也能达到核心学习目标。

固定窗口限制哪些 token 能进入当前预测。递归增加了一份算出来的状态,让较早的 token 可以影响较晚的预测。展开后的三步使用同一组参数,变化的是输入和状态。信息因此多了一条传递路径,计算也多了一层顺序依赖,但可靠保留线索并没有得到保证。

本例中,远处的蓝色线索仍传到了输出端,只是影响太小,没能胜出。训练也许能调整造成衰减的权重。第 12 章 要追问:末端的一次错误,怎样沿着同一串状态更新,把有用的训练信号传回去?

  1. 找出边界。 在 blue 和 key is 之间插入 filler。沿用原权重和平局规则,最少插入几个才会让 RNN 选择红色?写出最终状态和两个 logits。
  2. 只改一个参数。 把递归矩阵的对角元素都改成 1,其他数值不变。计算长蓝色样例的最终状态与选择。如果一条输入流含有许多个 blue,又可能出现什么问题?
  3. 诊断日志。 程序员说,读完 red、key、is 后,隐藏向量从 [1,0] 变成 [0.25,0],说明模型被修改了。什么证据才能证明参数确实更新过?为什么每读一个 token 前都清零状态,会破坏例 11.1?
  4. 选择证据。 某系统答对了两个长距离颜色问题。这能证明它用了 RNN、权重发生变化,或能找到当前差旅政策吗?选择其中一个说法,说明还需要怎样的受控观察。
  5. 追踪责任。 订单助手生成 approved 后,隐藏向量发生变化;应用记录却仍显示取消操作尚未获批。该依据哪份记录决定能否操作?为什么变化的隐藏向量不能替代它?
  1. 没有 filler 时,蓝色坐标为 1/4。一个 filler 多带来一次减半,最后剩 1/8;logits 变成 [1/8,1/8],按平局规则选择红色。因此一个就够,两个则会让蓝色分数严格小于红色。数清线索之后的状态转换次数,不要把全部 token 数当成减半次数。
  2. 后续每个零输入都让旧状态保持原样。长蓝色样例最后得到 [0,1],logits 为 [0.125,1],因此选择蓝色。但不断读入 blue 会让对应坐标每次加一,这里没有选择性清除或覆盖的机制。能保住这条孤立线索,不等于解决了哪些信息该留下的问题。
  3. 应比较更新前后保存的参数,并查看对应的优化器操作。权重固定时激活值变化,本来就是正常现象。如果每次读取前都清零,就移除了旧状态的贡献,最后的 is 只能产生 [0,0],输出头便会不顾原线索而偏向红色。
  4. 两个正确字符串不能证明其中任何一项。例如,要调查是否发生学习,可以固定输入和选择设置,比较参数快照及更新记录。答对颜色问题,也不能证明另一个应用环节提供了当前政策文档。
  5. 应以应用中明确的审批记录和执行规则为准。生成的 token 及其隐藏激活都是数值计算的结果,不是审批事件,也不能证明取消操作已经获得授权。