跳转到内容

第 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.25−1=−0.750.25-1=-0.75。把误差平方,可以同时惩罚正、负两个方向的偏差;再乘二分之一,只是为了简化变化率,不会改变哪个状态使损失最小:

L=12(h3−1)2=12(−0.75)2=0.28125.L=\tfrac12(h_3-1)^2=\tfrac12(-0.75)^2=0.28125.

这里的 LL 是一个标量损失,h3h_3 现在专指这个诊断中的单个坐标。把最后的状态稍微增大,损失会下降,局部变化率为 −0.75。这个数从哪里来?把状态增加一个很小的量 δ\delta,展开平方后,损失的变化就是 −0.75δ+12δ2-0.75\delta+\tfrac12\delta^2。除以状态的改变量,再让改变量越来越小,剩下的就是 −0.75。我们记作 ∂L/∂h3=−0.75\partial L/\partial h_3=-0.75。

第二步状态增加 0.01,第三步只会增加 0.005,因为两步之间的连接要乘 0.5。所以损失对第二步状态的敏感程度只有对第三步的一半:−0.75×0.5=−0.375-0.75\times0.5=-0.375。再跨过前一条连接,还要乘一次 0.5,得到损失对第一步状态的变化率 −0.375×0.5=−0.1875-0.375\times0.5=-0.1875。负号表示哪个变化方向会降低损失,并不是说保存了“负的信息量”。

表 12.1 — 反传信号,以及共享权重每次使用的直接贡献。

读取时刻旧状态新状态损失对新状态的变化率本次使用权重的梯度贡献
101−0.1875−0.1875 × 0 = 0
210.5−0.375−0.375 × 1 = −0.375
30.50.25−0.75−0.75 × 0.5 = −0.375

最后一列为什么还要乘旧状态?单看某次使用,递归乘法就是“权重乘旧状态”。先把旧状态固定住:权重增加 0.01,局部结果就增加 0.01 乘旧状态。把这个局部变化率再乘上损失的变化率,便得到本次使用的贡献。旧状态本身也依赖这个权重,但那部分影响已由较早几次使用来计算,不能在最后一次使用里再算一遍。

三次使用对应 同一个参数,所以贡献要相加:0−0.375−0.375=−0.750-0.375-0.375=-0.75。既不能各自更新三个独立权重,也不能因为读了三次,就擅自除以三。只有损失本来定义为多个目标的平均值时,才需要相应的平均;这里的目标只有末端这一个。

把共享递归权重记作 ww,这个标量计算的一般形式为:

ht=xt+wht−1,∂L∂w=∑t=1T∂L∂ht ht−1.h_t=x_t+wh_{t-1},\qquad \frac{\partial L}{\partial w} =\sum_{t=1}^{T}\frac{\partial L}{\partial h_t}\,h_{t-1}.

xtx_t 是给定的标量输入,TT 是读取次数。每个状态对应的损失变化率,都已包含依赖该状态的后续计算。这条公式适用于初始状态固定的线性标量递归;如果有非线性激活,还要乘上激活的局部变化率。把表 12.1 代回去,又得到 −0.75。

还可以换一条路径独立核对。对这组输入和零初始状态,最后的状态就是 w2w^2,因此 L(w)=12(w2−1)2L(w)=\tfrac12(w^2-1)^2,变化率为 2w(w2−1)2w(w^2-1)。代入 w=0.5w=0.5,结果同样是 −0.75。学习率取 0.1,一次梯度更新得到 wnew=0.5−0.1(−0.75)=0.575w_{\text{new}}=0.5-0.1(-0.75)=0.575。用新权重重算前向过程,末端状态为 0.330625,损失降到 0.2240314453125。不能继续沿用旧隐藏状态,假装它们也是由新权重算出来的。

前向
  1. 一组数字h₁ = 1
  2. 一组数字h₂ = 0.5
  3. 一组数字h₃ = 0.25
  4. 处理步骤损失 = 0.28125

h₁ → h₂ → h₃:每次状态传递都使用同一个 w = 0.5。

反向
  1. 一组数字对 h₃:−0.75
  2. 一组数字对 h₂:−0.375
  3. 一组数字对 h₁:−0.1875

h₃ → h₂ → h₁:损失变化率每跨过一条边,就乘一次 0.5。

共享参数
  1. 处理步骤各次贡献:0 − 0.375 − 0.375
  2. 一组数字w 的总梯度:−0.75
  3. 模型保存的数值更新后 w = 0.575

汇总各次贡献 → 用学习率 0.1 更新一次共享权重 w。

图 12.1 — 沿路径向后乘,按同一权重的各次使用相加。 本书原创教学图,数值为给定值或按规则推导值,CC BY-SA 4.0;未复制论文图示。

图 12.1 沿正向追踪状态,沿反向追踪损失的变化率。把反向传播用到这个按时间展开的计算图上,就叫作 随时间反向传播(backpropagation through time,BPTT)。具体做法是保存或重算所需的前向激活,反向遍历依赖关系,再汇总共享参数的各项贡献。它不是把信息送回真实世界的过去。Pascanu 等,2013,第 2 节

12.3 距离越长,要相乘的因子越多

Section titled “12.3 距离越长,要相乘的因子越多”

例 12.1 中,信号跨过两条边,就乘了两次二分之一。跨过八条同样的边,乘数是 0.58=1/2560.5^8=1/256;跨过十六条,则为 0.516=1/655360.5^{16}=1/65536。这描述的是一个微小变化怎样传递,不是状态本身有多大。

例 12.2 — 距离不变,只改局部因子。 假设每条边的局部变化率换成 1.5。跨过两条边,信号便乘上 1.5×1.5=2.251.5\times1.5=2.25。同样是反复相乘,既可能使信号衰减,也可能使它放大。

表 12.2 — 跨越的边数与信号乘数。

距离:跨越几条边每条边乘 0.5每条边乘 1.5
011
10.51.5
20.252.25
40.06255.0625
80.0039062525.62890625
160.0000152587890625656.8408355712891

距离为零时,乘数是一,因为还没有任何转换改变信号。后两列是微小变化之间的比值,没有物理单位。它们按乘方规则算出,没有拟合训练测量。如果起始的损失变化率为 −0.75,还要把它乘上表中相应数值;表里本身不含这个起始变化率。

信号反复缩小,就是 梯度消失(vanishing gradient) 的核心直觉:与较近的参数使用相比,末端损失传给较早那次使用的信号太小。反复放大则可能造成 梯度爆炸(exploding gradient),使普通大小的学习率也带来很大的参数更新。这两个名称都不要求数值真的变成零或无穷大。早期关于长期依赖的研究分析了这些现象出现的条件,并没有断言所有递归序列都会有同样表现。Bengio 等,1994,第 4 节

真实隐藏状态有多个坐标,许多路径会相互混合、汇总。局部变化率还取决于激活函数。ReLU 在正数处的变化率为 1,在负数处为 0;零点的折角处由实现约定如何处理。另一个常见激活 双曲正切(tanh) 会把任意标量平滑地压到 −1 与 1 之间,定义为 tanh⁡(a)=(ea−e−a)/(ea+e−a)\tanh(a)=(e^a-e^{-a})/(e^a+e^{-a})。例如,输入 0 得到 0,输入 2 得到约 0.964。当输入很大或很小时,输出几乎不再变化:局部变化率为 1−tanh⁡2(a)1-\tanh^2(a),输入为 2 时只有约 0.071。因此,不能只凭权重大小判断梯度,还要看激活的变化率和网络中的传递路径。Hochreiter 和 Schmidhuber,1997,第 3 节

实践中有两种针对不同代价的办法。梯度裁剪(gradient clipping) 在优化器使用梯度前限制过大的梯度,但不能重建长路径上已经变得微弱的信号。截断式 BPTT(truncated BPTT) 则限制一次训练计算沿时间向后追溯的长度。程序可以把数值状态传给下一个片段,同时在片段边界停止梯度传播:值还在,却没有沿整个前序序列分配训练贡献。这样可以节省训练内存和计算量,代价是得到近似梯度。《Dive into Deep Learning》,第 9.7 节

每次读取都把状态减半,太不灵活。无关的填充词和新的决定性线索,都要套同一条规则。我们希望一条路径在某一步尽量保留某个坐标,在另一步又能替换它。于是引入 门(gate):用一个算出来的乘数,控制某条分支上能通过多少数值。它不是智能体作出的“是或否”判断。

回想第 7 章的 sigmoid:σ(a)=1/(1+e−a)\sigma(a)=1/(1+e^{-a})。有限输入得到的输出严格位于 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.9×0.8=0.720.9\times0.8=0.72,候选值加入 0.2×0.5=0.100.2\times0.5=0.10,相加得到新单元值 0.82。再用输出比例 0.75,把 0.75tanh⁡(0.82)≈0.5063020.75\tanh(0.82)\approx0.506302 作为新的隐藏值传给网络其他部分。

这里的门值和候选值都是 给定的激活值,并非从训练好的门控网络中测得;新的单元值和隐藏值才是按它们计算出来的。这个例子把三件事分开了:保留内部旧值、写入一份新贡献,以及把值提供给其他计算。

这就是一种常见的、带遗忘门的 长短期记忆网络(long short-term memory,LSTM) 的核心状态计算:

ct=ft⊙ct−1+it⊙gt,ht=ot⊙tanh⁡(ct).c_t=f_t\odot c_{t-1}+i_t\odot g_t,\qquad h_t=o_t\odot\tanh(c_t).

单元状态行向量 ctc_t 与隐藏状态行向量 hth_t 分开保存,二者的坐标数相同。符号 ⊙\odot 表示对应坐标逐项相乘,不是点积,也不把结果求和。遗忘门(forget gate) ftf_t 控制保留多少旧单元值,接近 1 表示忘得少;输入门(input gate) iti_t 控制加入多少候选值 gtg_t;输出门(output gate) oto_t 控制向外提供多少隐藏状态,并不直接选择输出 token。输入门和遗忘门彼此独立,门值之和不必为一,因此单元值可以不断累加,超出 −1 到 1 的范围。

这些不断变化的乘数从哪里来?先把当前输入和旧隐藏向量拼接成 ut=[xt,ht−1]u_t=[x_t,h_{t-1}]。若输入宽度为 mm,隐藏宽度为 dd,拼接后就有 m+dm+d 个坐标。一种不含单元到门的直接连接的常见形式是:

ft=σ(utAf+bf),it=σ(utAi+bi),ot=σ(utAo+bo),gt=tanh⁡(utAg+bg).\begin{aligned} f_t&=\sigma(u_tA_f+b_f),& i_t&=\sigma(u_tA_i+b_i),\\ o_t&=\sigma(u_tA_o+b_o),& g_t&=\tanh(u_tA_g+b_g). \end{aligned}

每个 AA 都有 m+dm+d 行、dd 列,每个偏置有 dd 个坐标。每条分支各有一套可学习参数,并沿时间复用。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。重置门先留下 0.25×0.8=0.20.25\times0.8=0.2 来构造候选值,因此候选值为 tanh⁡(0.2)≈0.197375\tanh(0.2)\approx0.197375。再混合旧值的 90% 和候选值的 10%,得到 0.9×0.8+0.1×0.197375≈0.7397380.9\times0.8+0.1\times0.197375\approx0.739738。

重置门并没有抹掉另一条分支直接保留的 0.72。它改变的是 构造候选值时 能用到多少旧信息。沿用 Cho 等人原论文的约定,公式为:

rt=σ(utAr+br),zt=σ(utAz+bz),gt=tanh⁡(xtWg+(rt⊙ht−1)Ug+bg),ht=zt⊙ht−1+(1−zt)⊙gt.\begin{aligned} r_t&=\sigma(u_tA_r+b_r),\qquad z_t=\sigma(u_tA_z+b_z),\\ g_t&=\tanh(x_tW_g+(r_t\odot h_{t-1})U_g+b_g),\\ h_t&=z_t\odot h_{t-1}+(1-z_t)\odot g_t. \end{aligned}

rtr_t 是重置门,ztz_t 是更新门。在这组 GRU 公式里,ztz_t 指门,不是第 11 章的输出 logits。两个 AA 矩阵沿用上文拼接行向量的维度;WgW_g 把 mm 维输入映射成 dd 维候选贡献,UgU_g 把 dd 维重置后的隐藏坐标映射成 dd 维候选贡献。各个门、候选值和隐藏状态的宽度都是 dd。

按这个约定,更新门越大,保留的旧状态越多。有些资料采用互补约定,有些实现则把重置操作放在不同位置。因此,解释门值之前,要先看公式。本章正文和代码始终使用上面这一种形式。Cho 等,2014,第 2.3 节,式(5)—(8)

表 12.3 — 面对同一个保留与更新问题,三种结构怎样处理。

机制什么沿时间传递?怎样保留旧信息?怎样接纳新信息?
简单 RNN隐藏状态经过递归权重和激活输入与旧状态的贡献一起接受变换
上述 LSTM单元状态和隐藏状态遗忘门缩放直接传递的单元分支输入门缩放候选值;输出门控制经 tanh 变换后向外提供的值
上述约定的 GRU隐藏状态更新门缩放旧状态分支更新门的互补比例接纳候选值;重置门控制候选分支能用多少旧状态

门控增加了可学习计算和参数。与这里的 LSTM 相比,GRU 没有独立单元和输出门,但这只是结构比较,并不是跨硬件、跨任务的速度或质量测试。

门控也没有解决无限保留的问题。把遗忘门固定为 0.9,经过八条边后,旧单元沿直接路径剩下 0.98=0.430467210.9^8=0.43046721;把门固定为 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 节

静态查表基线
  1. 文本片段the train arrived / we train models
  2. 一组数字train → 同一嵌入行

仅凭查表,不能区分这两次出现。

预训练
  1. 处理步骤前向:左侧上下文 → 下一个 token
  2. 处理步骤反向:右侧上下文 → 前一个 token
  3. 模型保存的数值学习递归语言模型权重

两个方向各有预测目标,都不把自己的目标放进输入。

任务使用
  1. 模型保存的数值冻结预训练权重
  2. 一组数字在每层组合正反向状态
  3. 模型保存的数值学习层权重、缩放量和任务预测器

完整输入句子改变特征;任务训练改变混合方式与预测器。

图 12.2 — 同一个词的嵌入行相同,结合句子后表示可以不同;任务参数另行训练。 本书原创教学图,数值为给定值或按规则推导值,CC BY-SA 4.0;未复制论文图示。

图 12.2 把学习通用表示与使用表示分开。预训练(pretraining) 是先用文本学习语言模型参数,再为具体下游任务训练。在 ELMo 原论文的任务使用方式中,预训练参数保持冻结。下游任务学习怎样加权混合输入 token 层和各递归层,并学习一个缩放混合结果的标量;各层权重经过 Softmax 归一化。任务自身的预测器也要训练。混合权重是任务参数,被混合的向量则随句子而变。这些是层与层之间的权重,不是对源序列位置分配的注意力权重。Peters 等,2018,第 3.1—3.3 节

因此,可以把 ELMo 放在“按词查一行固定向量”和“广泛使用深层预训练上下文编码器”之间理解。ELMo 不等于 BERT:前者的递归语言模型分别处理两个方向,再组合特征;后者用双向 Transformer,并通过被遮住的 token 进行预训练。这里先认清架构与目标的区别,Transformer 和遮盖训练的机制留给后文。不需要亲自训练这两个模型,才能读懂本章。Devlin 等,2019,第 3.1 节

RNN 的前向状态传递与反向训练信号面临相关但不同的问题。BPTT 沿展开图计算,并把同一参数各次使用的贡献相加。局部变化率连乘,可以让远处的贡献变得极小,也可以把它放得很大。门控让保留和更新更加灵活,同时增加计算,并保留了自身的限制。

同样的递归状态还可以用来提供随上下文变化的特征,ELMo 就展示了这种用法。到目前为止,我们的主要预测任务仍是读一个输入流,给出局部答案。第 13 章 将改问:一个序列怎样生成另一个长度可能不同的序列?负责读取的部分,必须向负责写出的部分传递什么?

可选的 CPU 计算位于 code/part-iii/core.py;notebooks/part-iii/01-state-and-gradient.ipynb 会复算表 12.1—12.2 以及两个门控算例。反向循环同时用例 12.1 的独立多项式和小改变量的差分计算核对。这个循环和距离表都不是大模型训练实验。

  1. 传递一个新信号。 末端损失变化率为 2,向前跨越四条边,每条边的局部变化率都是 0.5。到达较早状态时,变化率是多少?把因子换成 1.5 再算一次,并说明为什么距离四意味着乘四次。
  2. 找出漏项。 例 12.1 中,一个程序只看最后一次乘法,报告权重梯度为 −0.375。它漏掉了哪项贡献?学习率为 0.1 时,正确的新参数是多少?
  3. 区分数值与训练贡献。 程序在两个片段之间传递隐藏状态,却在边界截断梯度。第二个片段的目标依赖第一个片段里的线索。什么仍然能传过去?哪条学习路径被切断了?
  4. 操作门值。 旧单元值为 0.8,遗忘门为 0.5,输入门为 0.25,候选值为 −0.4,输出门为 1。计算新单元值和隐藏值。如果输出门改为 0,会抹掉新单元值吗?把恰好为 0 或 1 的门值按极限设置理解。
  5. 读懂约定。 在上述 GRU 形式中,把更新门设为 1,重置门设为 0。隐藏状态会变成零吗?两个门各影响哪条分支?
  6. 判断表示的使用是否合理。 某方案先从完整参考译文计算反向 ELMo 状态,再用它从头预测同一译文。为什么这不能作为有效的生成评价?举一个完整输入句子确实可用的任务。
  1. 结果分别为 2(0.5)4=0.1252(0.5)^4=0.125 和 2(1.5)4=10.1252(1.5)^4=10.125。距离数的是两个状态之间的依赖边,每条边把微小变化变换一次,起始变化率 2 本身不算一条边。这些数值衡量敏感程度,不是状态大小。
  2. 第二次读取还贡献了 −0.375;第一次的旧状态为零,所以贡献为零。总梯度为 −0.75,新权重为 0.575。相加源于参数共享;若除以读取次数,就改变了这个损失目标下的更新。
  3. 数值状态仍可能携带线索。第二个片段的损失却不能跨过截断边界,反传到第一个片段中形成该数值的计算。传过去的状态非零,不能证明训练追踪了完整序列。
  4. 保留的旧贡献为 0.4,新贡献为 −0.1,因此单元值为 0.3,隐藏值为 tanh⁡(0.3)≈0.291313\tanh(0.3)\approx0.291313。若输出门为零,隐藏值为零,新单元值仍是 0.3。输出门控制向外提供多少,而不是本步怎样更新单元。
  5. 更新门为 1 时,旧隐藏状态原样保留,候选分支的权重为零。重置门为 0,只移除候选计算中的旧状态贡献,不移除单独保留的旧状态分支。如果资料采用互补的更新门约定,结论就会不同。
  6. 反向特征包含尚未生成的后续参考 token,生成器在那个时刻没有这些信息,因此泄露了评价目标。例如,对已提供的完整句子做情感分类,就可以合理使用左右上下文。这种用法是否合理,取决于信息何时可用,不能只看“双向”这个名称。