第 12 章 为什么 RNN 会忘:BPTT、LSTM/GRU 与上下文化表示
12.1 最后一次错误,能改变前面的计算吗?
Section titled “12.1 最后一次错误,能改变前面的计算吗?”第 11 章 的长蓝色钥匙样例失败了,虽然最后的隐藏状态里还留着非零的蓝色痕迹。那是在讨论 权重固定时的前向计算。训练要回答另一个问题:如果最后预测错了,较早的一次计算发生变化,会让损失改变多少?前向图中有一条通路,并不意味着它能向后传回足够强的训练信号。
第 7 章 已经给出了所需规则:沿一串依赖关系计算时,把局部变化率相乘;多条路径通向同一个参数时,把各自的贡献相加。我们先把这条规则用到三次读取上,再看更长的序列。不需要额外学过微积分。下面的梯度表是按给定因子算出的结果,不是实际训练测得的曲线。
12.2 先把误差传回去,再汇总同一参数的贡献
Section titled “12.2 先把误差传回去,再汇总同一参数的贡献”例 12.1 — 同一个权重,每次读取都用到。 只保留第 11 章里为正的颜色坐标。三个标量输入依次为 1、0、0,初始状态为零,输入权重为 1,递归权重为 0.5。因此前向状态依次为 1、0.5、0.25。现在另设一个诊断目标:让最后的状态接近标量 1。这不是第 11 章的输出 Softmax,也不是说隐藏坐标通常都以颜色标签作为监督目标。
最后的误差是 。把误差平方,可以同时惩罚正、负两个方向的偏差;再乘二分之一,只是为了简化变化率,不会改变哪个状态使损失最小:
这里的 是一个标量损失, 现在专指这个诊断中的单个坐标。把最后的状态稍微增大,损失会下降,局部变化率为 −0.75。这个数从哪里来?把状态增加一个很小的量 ,展开平方后,损失的变化就是 。除以状态的改变量,再让改变量越来越小,剩下的就是 −0.75。我们记作 。
第二步状态增加 0.01,第三步只会增加 0.005,因为两步之间的连接要乘 0.5。所以损失对第二步状态的敏感程度只有对第三步的一半:。再跨过前一条连接,还要乘一次 0.5,得到损失对第一步状态的变化率 。负号表示哪个变化方向会降低损失,并不是说保存了“负的信息量”。
表 12.1 — 反传信号,以及共享权重每次使用的直接贡献。
| 读取时刻 | 旧状态 | 新状态 | 损失对新状态的变化率 | 本次使用权重的梯度贡献 |
|---|---|---|---|---|
| 1 | 0 | 1 | −0.1875 | −0.1875 × 0 = 0 |
| 2 | 1 | 0.5 | −0.375 | −0.375 × 1 = −0.375 |
| 3 | 0.5 | 0.25 | −0.75 | −0.75 × 0.5 = −0.375 |
最后一列为什么还要乘旧状态?单看某次使用,递归乘法就是“权重乘旧状态”。先把旧状态固定住:权重增加 0.01,局部结果就增加 0.01 乘旧状态。把这个局部变化率再乘上损失的变化率,便得到本次使用的贡献。旧状态本身也依赖这个权重,但那部分影响已由较早几次使用来计算,不能在最后一次使用里再算一遍。
三次使用对应 同一个参数,所以贡献要相加:。既不能各自更新三个独立权重,也不能因为读了三次,就擅自除以三。只有损失本来定义为多个目标的平均值时,才需要相应的平均;这里的目标只有末端这一个。
把共享递归权重记作 ,这个标量计算的一般形式为:
是给定的标量输入, 是读取次数。每个状态对应的损失变化率,都已包含依赖该状态的后续计算。这条公式适用于初始状态固定的线性标量递归;如果有非线性激活,还要乘上激活的局部变化率。把表 12.1 代回去,又得到 −0.75。
还可以换一条路径独立核对。对这组输入和零初始状态,最后的状态就是 ,因此 ,变化率为 。代入 ,结果同样是 −0.75。学习率取 0.1,一次梯度更新得到 。用新权重重算前向过程,末端状态为 0.330625,损失降到 0.2240314453125。不能继续沿用旧隐藏状态,假装它们也是由新权重算出来的。
- 一组数字h₁ = 1
- 一组数字h₂ = 0.5
- 一组数字h₃ = 0.25
- 处理步骤损失 = 0.28125
h₁ → h₂ → h₃:每次状态传递都使用同一个 w = 0.5。
- 一组数字对 h₃:−0.75
- 一组数字对 h₂:−0.375
- 一组数字对 h₁:−0.1875
h₃ → h₂ → h₁:损失变化率每跨过一条边,就乘一次 0.5。
- 处理步骤各次贡献:0 − 0.375 − 0.375
- 一组数字w 的总梯度:−0.75
- 模型保存的数值更新后 w = 0.575
汇总各次贡献 → 用学习率 0.1 更新一次共享权重 w。
图 12.1 沿正向追踪状态,沿反向追踪损失的变化率。把反向传播用到这个按时间展开的计算图上,就叫作 随时间反向传播(backpropagation through time,BPTT)。具体做法是保存或重算所需的前向激活,反向遍历依赖关系,再汇总共享参数的各项贡献。它不是把信息送回真实世界的过去。Pascanu 等,2013,第 2 节
12.3 距离越长,要相乘的因子越多
Section titled “12.3 距离越长,要相乘的因子越多”例 12.1 中,信号跨过两条边,就乘了两次二分之一。跨过八条同样的边,乘数是 ;跨过十六条,则为 。这描述的是一个微小变化怎样传递,不是状态本身有多大。
例 12.2 — 距离不变,只改局部因子。 假设每条边的局部变化率换成 1.5。跨过两条边,信号便乘上 。同样是反复相乘,既可能使信号衰减,也可能使它放大。
表 12.2 — 跨越的边数与信号乘数。
| 距离:跨越几条边 | 每条边乘 0.5 | 每条边乘 1.5 |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 0.5 | 1.5 |
| 2 | 0.25 | 2.25 |
| 4 | 0.0625 | 5.0625 |
| 8 | 0.00390625 | 25.62890625 |
| 16 | 0.0000152587890625 | 656.8408355712891 |
距离为零时,乘数是一,因为还没有任何转换改变信号。后两列是微小变化之间的比值,没有物理单位。它们按乘方规则算出,没有拟合训练测量。如果起始的损失变化率为 −0.75,还要把它乘上表中相应数值;表里本身不含这个起始变化率。
信号反复缩小,就是 梯度消失(vanishing gradient) 的核心直觉:与较近的参数使用相比,末端损失传给较早那次使用的信号太小。反复放大则可能造成 梯度爆炸(exploding gradient),使普通大小的学习率也带来很大的参数更新。这两个名称都不要求数值真的变成零或无穷大。早期关于长期依赖的研究分析了这些现象出现的条件,并没有断言所有递归序列都会有同样表现。Bengio 等,1994,第 4 节
真实隐藏状态有多个坐标,许多路径会相互混合、汇总。局部变化率还取决于激活函数。ReLU 在正数处的变化率为 1,在负数处为 0;零点的折角处由实现约定如何处理。另一个常见激活 双曲正切(tanh) 会把任意标量平滑地压到 −1 与 1 之间,定义为 。例如,输入 0 得到 0,输入 2 得到约 0.964。当输入很大或很小时,输出几乎不再变化:局部变化率为 ,输入为 2 时只有约 0.071。因此,不能只凭权重大小判断梯度,还要看激活的变化率和网络中的传递路径。Hochreiter 和 Schmidhuber,1997,第 3 节
实践中有两种针对不同代价的办法。梯度裁剪(gradient clipping) 在优化器使用梯度前限制过大的梯度,但不能重建长路径上已经变得微弱的信号。截断式 BPTT(truncated BPTT) 则限制一次训练计算沿时间向后追溯的长度。程序可以把数值状态传给下一个片段,同时在片段边界停止梯度传播:值还在,却没有沿整个前序序列分配训练贡献。这样可以节省训练内存和计算量,代价是得到近似梯度。《Dive into Deep Learning》,第 9.7 节
12.4 旧值留多少,新值写多少?
Section titled “12.4 旧值留多少,新值写多少?”每次读取都把状态减半,太不灵活。无关的填充词和新的决定性线索,都要套同一条规则。我们希望一条路径在某一步尽量保留某个坐标,在另一步又能替换它。于是引入 门(gate):用一个算出来的乘数,控制某条分支上能通过多少数值。它不是智能体作出的“是或否”判断。
回想第 7 章的 sigmoid:。有限输入得到的输出严格位于 0 和 1 之间。门值为 0.9,就让原数值的 90% 通过;门值为 0.2,就让 20% 通过。我们也会把恰好为 0 或 1 当作便于分析的极限设置。实际训练的网络会用当前输入、旧隐藏状态、可学习权重和偏置来计算门的分数。即使权重不变,不同时间步的门激活值仍可以不同。
12.4.1 LSTM:为单元状态留一条独立路径
Section titled “12.4.1 LSTM:为单元状态留一条独立路径”例 12.3 — 留下一部分旧值,再加入选中的更新。 假设一个标量单元里存着 0.8。给定保留比例 0.9、输入比例 0.2,以及候选更新 0.5。旧值保留 ,候选值加入 ,相加得到新单元值 0.82。再用输出比例 0.75,把 作为新的隐藏值传给网络其他部分。
这里的门值和候选值都是 给定的激活值,并非从训练好的门控网络中测得;新的单元值和隐藏值才是按它们计算出来的。这个例子把三件事分开了:保留内部旧值、写入一份新贡献,以及把值提供给其他计算。
这就是一种常见的、带遗忘门的 长短期记忆网络(long short-term memory,LSTM) 的核心状态计算:
单元状态行向量 与隐藏状态行向量 分开保存,二者的坐标数相同。符号 表示对应坐标逐项相乘,不是点积,也不把结果求和。遗忘门(forget gate) 控制保留多少旧单元值,接近 1 表示忘得少;输入门(input gate) 控制加入多少候选值 ;输出门(output gate) 控制向外提供多少隐藏状态,并不直接选择输出 token。输入门和遗忘门彼此独立,门值之和不必为一,因此单元值可以不断累加,超出 −1 到 1 的范围。
这些不断变化的乘数从哪里来?先把当前输入和旧隐藏向量拼接成 。若输入宽度为 ,隐藏宽度为 ,拼接后就有 个坐标。一种不含单元到门的直接连接的常见形式是:
每个 都有 行、 列,每个偏置有 个坐标。每条分支各有一套可学习参数,并沿时间复用。Sigmoid 把门坐标变成比例,tanh 则允许候选值为正或为负。刚才的标量例子,是从这些激活值已经给定的位置开始计算的。
独立单元路径来自最初的 LSTM,能自适应调整的遗忘门则在后续处理连续输入流的工作中加入。上面的公式采用加入遗忘门后的形式,并不是逐项复刻 1997 年网络的全部细节。Hochreiter 和 Schmidhuber,1997;Gers 等,2000,第 3 节
12.4.2 GRU:在一份状态里混合旧值与候选值
Section titled “12.4.2 GRU:在一份状态里混合旧值与候选值”不单独保存单元状态,也可以把旧隐藏值和候选值混合。门控循环单元(gated recurrent unit,GRU) 用更新门和重置门控制两件不同的事。
例 12.4 — 重置候选分支看到的旧值,不重置直接保留的分支。 给定旧隐藏值 0.8、更新门 0.9、重置门 0.25。候选分支的当前输入贡献设为零,递归权重设为 1。重置门先留下 来构造候选值,因此候选值为 。再混合旧值的 90% 和候选值的 10%,得到 。
重置门并没有抹掉另一条分支直接保留的 0.72。它改变的是 构造候选值时 能用到多少旧信息。沿用 Cho 等人原论文的约定,公式为:
是重置门, 是更新门。在这组 GRU 公式里, 指门,不是第 11 章的输出 logits。两个 矩阵沿用上文拼接行向量的维度; 把 维输入映射成 维候选贡献, 把 维重置后的隐藏坐标映射成 维候选贡献。各个门、候选值和隐藏状态的宽度都是 。
按这个约定,更新门越大,保留的旧状态越多。有些资料采用互补约定,有些实现则把重置操作放在不同位置。因此,解释门值之前,要先看公式。本章正文和代码始终使用上面这一种形式。Cho 等,2014,第 2.3 节,式(5)—(8)
表 12.3 — 面对同一个保留与更新问题,三种结构怎样处理。
| 机制 | 什么沿时间传递? | 怎样保留旧信息? | 怎样接纳新信息? |
|---|---|---|---|
| 简单 RNN | 隐藏状态 | 经过递归权重和激活 | 输入与旧状态的贡献一起接受变换 |
| 上述 LSTM | 单元状态和隐藏状态 | 遗忘门缩放直接传递的单元分支 | 输入门缩放候选值;输出门控制经 tanh 变换后向外提供的值 |
| 上述约定的 GRU | 隐藏状态 | 更新门缩放旧状态分支 | 更新门的互补比例接纳候选值;重置门控制候选分支能用多少旧状态 |
门控增加了可学习计算和参数。与这里的 LSTM 相比,GRU 没有独立单元和输出门,但这只是结构比较,并不是跨硬件、跨任务的速度或质量测试。
门控也没有解决无限保留的问题。把遗忘门固定为 0.9,经过八条边后,旧单元沿直接路径剩下 ;把门固定为 0.99,经过一百条边后,也只剩约 0.366032。接近一的门值比反复减半温和得多,但仍然在连乘。而且,这里为了单独检查直接路径,特意固定了门值。完整网络中的门也依赖先前状态,会引入其他梯度路径。有限的状态大小、覆盖、饱和,以及没有学会合适门值等问题,依然可能出现。
12.5 同一个词,在不同句子里可以有不同表示
Section titled “12.5 同一个词,在不同句子里可以有不同表示”变化的隐藏状态不只可以用来预测下一个 token。看同一个英文 token train:在 the train arrived 中,它表示火车;在 we train models 中,它表示训练。静态嵌入查表在两个句子里返回同一行。递归计算却能把这个词与周围的词结合,为两次出现算出不同的 上下文化表示(contextual representation)。这些向量并不自带“交通工具”“动词”等可靠标签,是否有用仍要通过任务评价。
ELMo 是 Embeddings from Language Models 的缩写,它把这个思路发展成一种可复用的预训练方法。前向语言模型根据可见的左侧上下文预测下一个 token;反向语言模型倒着读,根据右侧上下文预测前一个 token。每个方向的语言模型目标都不把自己的预测目标放进输入。当下游任务已经提供完整句子时,ELMo 会在每个词的位置组合正、反两个方向的状态,并使用堆叠的多个递归层。“双向”并不是说生成器能看见自己还没生成的词。
所谓堆叠,是把某一层在当前位置的输出,送给下一层作为当前位置的输入;每个递归层又各自沿序列传递状态。ELMo 最底层的 token 表示由这个词的字符算出,尚未使用句子中周围的词,上面的递归层再加入句子上下文。因此,图 12.2 中的静态查表是一条比较基线,并不是说 ELMo 的底层表示也来自普通的词嵌入查表。这里需要分清的是不随句子变化的输入表示与上下文化的递归层,不必实现其中的字符网络。Peters 等,2018,第 3.1 节
- 文本片段the train arrived / we train models
- 一组数字train → 同一嵌入行
仅凭查表,不能区分这两次出现。
- 处理步骤前向:左侧上下文 → 下一个 token
- 处理步骤反向:右侧上下文 → 前一个 token
- 模型保存的数值学习递归语言模型权重
两个方向各有预测目标,都不把自己的目标放进输入。
- 模型保存的数值冻结预训练权重
- 一组数字在每层组合正反向状态
- 模型保存的数值学习层权重、缩放量和任务预测器
完整输入句子改变特征;任务训练改变混合方式与预测器。
图 12.2 把学习通用表示与使用表示分开。预训练(pretraining) 是先用文本学习语言模型参数,再为具体下游任务训练。在 ELMo 原论文的任务使用方式中,预训练参数保持冻结。下游任务学习怎样加权混合输入 token 层和各递归层,并学习一个缩放混合结果的标量;各层权重经过 Softmax 归一化。任务自身的预测器也要训练。混合权重是任务参数,被混合的向量则随句子而变。这些是层与层之间的权重,不是对源序列位置分配的注意力权重。Peters 等,2018,第 3.1—3.3 节
因此,可以把 ELMo 放在“按词查一行固定向量”和“广泛使用深层预训练上下文编码器”之间理解。ELMo 不等于 BERT:前者的递归语言模型分别处理两个方向,再组合特征;后者用双向 Transformer,并通过被遮住的 token 进行预训练。这里先认清架构与目标的区别,Transformer 和遮盖训练的机制留给后文。不需要亲自训练这两个模型,才能读懂本章。Devlin 等,2019,第 3.1 节
12.6 本章小结
Section titled “12.6 本章小结”RNN 的前向状态传递与反向训练信号面临相关但不同的问题。BPTT 沿展开图计算,并把同一参数各次使用的贡献相加。局部变化率连乘,可以让远处的贡献变得极小,也可以把它放得很大。门控让保留和更新更加灵活,同时增加计算,并保留了自身的限制。
同样的递归状态还可以用来提供随上下文变化的特征,ELMo 就展示了这种用法。到目前为止,我们的主要预测任务仍是读一个输入流,给出局部答案。第 13 章 将改问:一个序列怎样生成另一个长度可能不同的序列?负责读取的部分,必须向负责写出的部分传递什么?
可选的 CPU 计算位于 code/part-iii/core.py;notebooks/part-iii/01-state-and-gradient.ipynb 会复算表 12.1—12.2 以及两个门控算例。反向循环同时用例 12.1 的独立多项式和小改变量的差分计算核对。这个循环和距离表都不是大模型训练实验。
12.7 习题
Section titled “12.7 习题”- 传递一个新信号。 末端损失变化率为 2,向前跨越四条边,每条边的局部变化率都是 0.5。到达较早状态时,变化率是多少?把因子换成 1.5 再算一次,并说明为什么距离四意味着乘四次。
- 找出漏项。 例 12.1 中,一个程序只看最后一次乘法,报告权重梯度为 −0.375。它漏掉了哪项贡献?学习率为 0.1 时,正确的新参数是多少?
- 区分数值与训练贡献。 程序在两个片段之间传递隐藏状态,却在边界截断梯度。第二个片段的目标依赖第一个片段里的线索。什么仍然能传过去?哪条学习路径被切断了?
- 操作门值。 旧单元值为 0.8,遗忘门为 0.5,输入门为 0.25,候选值为 −0.4,输出门为 1。计算新单元值和隐藏值。如果输出门改为 0,会抹掉新单元值吗?把恰好为 0 或 1 的门值按极限设置理解。
- 读懂约定。 在上述 GRU 形式中,把更新门设为 1,重置门设为 0。隐藏状态会变成零吗?两个门各影响哪条分支?
- 判断表示的使用是否合理。 某方案先从完整参考译文计算反向 ELMo 状态,再用它从头预测同一译文。为什么这不能作为有效的生成评价?举一个完整输入句子确实可用的任务。
12.8 参考解答
Section titled “12.8 参考解答”- 结果分别为 和 。距离数的是两个状态之间的依赖边,每条边把微小变化变换一次,起始变化率 2 本身不算一条边。这些数值衡量敏感程度,不是状态大小。
- 第二次读取还贡献了 −0.375;第一次的旧状态为零,所以贡献为零。总梯度为 −0.75,新权重为 0.575。相加源于参数共享;若除以读取次数,就改变了这个损失目标下的更新。
- 数值状态仍可能携带线索。第二个片段的损失却不能跨过截断边界,反传到第一个片段中形成该数值的计算。传过去的状态非零,不能证明训练追踪了完整序列。
- 保留的旧贡献为 0.4,新贡献为 −0.1,因此单元值为 0.3,隐藏值为 。若输出门为零,隐藏值为零,新单元值仍是 0.3。输出门控制向外提供多少,而不是本步怎样更新单元。
- 更新门为 1 时,旧隐藏状态原样保留,候选分支的权重为零。重置门为 0,只移除候选计算中的旧状态贡献,不移除单独保留的旧状态分支。如果资料采用互补的更新门约定,结论就会不同。
- 反向特征包含尚未生成的后续参考 token,生成器在那个时刻没有这些信息,因此泄露了评价目标。例如,对已提供的完整句子做情感分类,就可以合理使用左右上下文。这种用法是否合理,取决于信息何时可用,不能只看“双向”这个名称。
12.9 参考文献
Section titled “12.9 参考文献”- Bengio、Simard 和 Frasconi,Learning Long-Term Dependencies with Gradient Descent Is Difficult(1994)。第 4 节在明确的动力系统假设下分析长距离学习。
- Hochreiter 和 Schmidhuber,Long Short-Term Memory(1997)。第 3—4 节把梯度传递问题与单元路径设计联系起来。
- Gers、Schmidhuber 和 Cummins,Learning to Forget(2000)。第 3 节针对连续输入流引入可自适应调整的遗忘门。
- Cho 等,Learning Phrase Representations using RNN Encoder–Decoder(2014)。第 2.3 节给出重置门和更新门公式,本章沿用其中保留旧状态的约定。
- Pascanu、Mikolov 和 Bengio,On the Difficulty of Training Recurrent Neural Networks(2013)。第 2 节把梯度写成各时间贡献之和,并展示局部变换的连乘。
- Zhang 等,Dive into Deep Learning,第 9.7 节 和 第 10.1 节。分别提供 BPTT 的教材推导与现代 LSTM 的门控计算。
- Peters 等,Deep Contextualized Word Representations(2018)。第 3.1—3.3 节解释 ELMo 的双向目标、层混合与下游任务用法。
- Devlin 等,BERT(2019)。第 3.1 节和附录图 3 帮助区分它与 ELMo;其中用到的 Transformer 机制将在后文补齐。