算法的故事 / 第一季第二季目录0 / 30 章已读

第二季 · 第 58 章

让机器记住昨天

《算法的故事》第二季 · 第六篇:会学习的算法——从循环神经网络、随时间反向传播,到1997年长短期记忆的诞生1990年,美国加利福尼亚州圣迭戈。一位研究语言与认知的科学家,正在追问一个看起来非常普通的问题。如果我们想让计算机理解一句话,它究竟需要记住多少过去的信息?例如:
小明把一本书放在桌上,然后走出了房间。过了一会儿,他回来了,拿起了……
如果计算机正在预测下一个词,它可能需要知道:前面出现过一本书。这本书曾经被放在桌上。句子中的“他”与前面的人物有关。而这些线索,不一定紧挨着当前需要预测的位置。我们也许可以把整句话直接交给一套巨大的规则系统。为每一种句型编写一条规则。为每一种词语组合指定一种处理方法。但是,如果语言不断变化,句子越来越长,这种方式又该如何扩展?有没有可能,让一个神经网络在连续读取字符或词语时,自己形成一种内部状态?这个状态会随着新输入不断变化。但同时,还保留此前输入留下的某些信息。这位研究者是:杰弗里·埃尔曼(Jeffrey L. Elman)。1990年,他在《Cognitive Science》第14卷第2期发表:Finding Structure in Time《在时间中发现结构》。论文第179—211页。这篇研究讨论了怎样通过循环连接,让神经网络拥有依赖过去内部状态的动态记忆,并展示了从简单时序任务到语言结构学习的一系列实验。 [Wiley Online Library][Jeffrey L. Elman]论文中,一种非常关键的设计是:把上一时刻隐藏层的活动,作为下一时刻隐藏层的输入之一。这听起来并不复杂。但它使网络的计算方式发生了重要改变。普通前馈网络可以将一个输入映射成一个输出。循环网络则能够让当前输出,受到此前计算状态的影响。于是,哪怕当前输入完全相同,因为过去不同,网络也可能给出不同结果。这是一种简单却十分重要的能力。它让计算具有了历史。不过,当神经网络开始保存过去的信息,研究者又遇到了新的困难。如果模型需要记住十步之前的输入,也许还能成功。如果需要记住一百步之前的信息呢?如果是一千步?即使网络内部能够在前向计算中暂时保存某些信息,我们还需要回答:怎样训练网络,让它知道一千步以前究竟应该记住什么?这将再次把我们带回上一章的核心算法。反向传播。但是,这一次,误差不仅需要穿过神经网络的不同层。它还需要穿过一段很长的时间。1990年,Paul Werbos系统讨论了这项方法。1994年,Yoshua Bengio、Patrice Simard和Paolo Frasconi发表了一篇标题非常直接的研究:Learning Long-Term Dependencies with Gradient Descent Is Difficult《使用梯度下降学习长期依赖是困难的》。他们从理论与实验角度分析,为什么传统循环网络在长时间间隔的学习中会遭遇严重困难。 [PubMed]随后,1997年11月15日。两位研究者在《Neural Computation》发表了一篇新的论文。他们是:塞普·霍赫赖特(Sepp Hochreiter)。于尔根·施密德胡伯(Jürgen Schmidhuber)。论文标题只有三个英文单词:Long Short-Term Memory长短期记忆。简称:LSTM。他们试图解决的不是:“怎样让神经网络随便保存一个过去的数字?”而是一个更困难的问题。怎样使重要信息在经过很长时间以后,仍然能够参与计算,并让相应的学习信号有效地返回过去?在原始论文中,研究者提出了一种特殊的内部记忆机制,让误差信号能够沿着特定路径保持,而不必在每一步都经过不断缩小的普通非线性导数。论文报告,在人工构造的长时滞任务中,方法能够学习跨越超过一千个离散时间步的依赖关系。 [MIT Press][ML Anthology]从这一刻开始,“记忆”不再只是神经网络在某个隐藏状态里留下的一点数字。它逐渐成为一种可以通过特殊结构与门控机制管理的信息。但这里还有一项常被现代教材忽略的历史细节。1997年最初的LSTM,并没有今天标准结构中那个独立的遗忘门。遗忘门的引入,是后来2000年发表的一项重要研究成果。这意味着,LSTM的故事并不是1997年就一次性完成了全部设计。它经历了真正的演化。研究者先思考如何保留。又进一步认识到:一台能够长期记忆的机器,还必须学会什么时候忘记。

第一幕:一句话里的一个词,为什么不能只看它自己?

我们先用一个没有任何复杂数学的例子,理解为什么需要循环网络。假设计算机正在逐字读取:
text
今天下雨,所以出门时要带一把____
人类通常会判断:
text
伞
很可能出现在最后。但如果计算机只看到最后一个字符附近的信息:
text
一把____
那么可能的答案就太多了。例如:伞。椅子。刀。尺子。它需要结合更早出现的:
text
下雨
才能明显改善判断。

普通前馈网络可以处理上下文吗?

可以。我们完全可以把多个词同时输入一个前馈网络。例如,将最近五个词连接成一个较大的输入向量。那么,网络也能利用一定范围内的上下文。因此不能说:“前馈网络永远无法理解时间或序列。”真正的区别是:如果模型的输入窗口固定,它能够直接接收的历史范围也可能受到窗口大小限制。而循环网络提供了另一种处理方式。每次读入一个新输入,同时保留一个随时间更新的隐藏状态。这样就不必为所有可能的序列长度,分别设计不同尺寸的输入层。

循环网络的基本形式

令:xtx_t表示第t个时间步的新输入。hth_t表示当前隐藏状态。那么:ht=f(ht−1,xt)\boxed{ h_t=f(h_{t-1},x_t) }其中:ht−1h_{t-1}是上一个时间步保留下来的状态。例如,使用一层最简单的循环神经网络:ht=tanh⁡(Whht−1+Wxxt+b)\boxed{ h_t=\tanh(W_hh_{t-1}+W_xx_t+b) }然后生成输出:yt=Wyht+c\boxed{ y_t=W_yh_t+c }其中:WxW_x 负责处理当前输入。WhW_h 负责处理上一时刻的隐藏状态。而:b, cb,\ c是偏置参数。

最重要的特征:参数共享

一个循环网络在第1个时间步使用:WhW_h第2个时间步仍然使用:WhW_h第100个时间步依然如此。也就是说:同一组参数在不同时刻反复使用。这与为每个时间步准备一套独立权重不同。参数共享使网络能够将相同的状态更新规则,应用于不同长度的序列。但它也带来了一个新的训练要求。因为同一个参数出现在很多时间步的计算中,所以最终损失对该参数的梯度,必须累加它在全部相关时间步产生的贡献。我们在第57章已经见过:同一个变量被计算图的多个分支使用时,需要累加梯度。现在,这个规则将沿时间重复出现。

循环网络真的拥有“记忆”吗?

它拥有一种有限维的动态内部状态。这一状态可以携带过去输入的信息。但这不等于:它保存了此前每个字符的完整副本。如果隐藏状态只有10个数,却已经处理了一万字文本,它不可能无损保存任意一万字符序列的全部细节,同时还保持固定有限精度表示。实际网络需要学习:哪些历史信息对于当前任务重要?哪些信息可以压缩?哪些已经不再需要?因此,循环网络里的记忆,更接近于:随任务不断更新的状态摘要。而不是一份无限容量的历史档案。

第二幕:1986年,Michael Jordan已经在研究具有内部状态的序列机器

在进一步讨论Elman之前,我们需要补上一项重要的历史背景。1990年,并不是循环神经网络第一次出现。1986年,研究者:迈克尔·乔丹(Michael I. Jordan)。发表了研究报告:Attractor Dynamics and Parallelism in a Connectionist Sequential Machine《连接主义序列机器中的吸引子动力学与并行性》。这项研究关注:怎样利用连接主义系统,表示和产生具有时间结构的行为。例如,连续的语音输出。相关模型已经使用内部上下文信息,使网络能够处理序列。[电子学术库]

为什么它与Elman的研究有关?

Elman在1990年《Finding Structure in Time》的摘要中,明确提到:他的研究路线与Jordan在1986年提出的循环连接思想有关。Elman进一步将:隐藏层自身的前一时刻活动反馈给下一时刻的隐藏层计算。这样的结构,后来常被称作:Elman Network。或:Simple Recurrent Network(SRN,简单循环网络)。

Elman网络中的Context Unit

考虑一个网络:当前输入:xtx_t上一时刻隐藏状态:ht−1h_{t-1}当前隐藏层:hth_t它会把:ht−1h_{t-1}作为上下文信息,与:xtx_t一起参与当前计算。
图58-1:Elman式简单循环网络xₜ当前输入hₜ隐藏状态yₜ输出保留上一时刻状态同一套权重在每个时间步重复使用橙色回路表示时间上的反馈连接,不是指网络在同一次前向计算中无限循环。展开时间后,这条反馈可以表示为前一时间步到后一时间步的有向连接。

1990年的论文到底训练了什么?

Elman研究了多项序列任务。包括:时序版本的XOR问题。字符序列中的结构发现。以及与词语类别和语言模式有关的实验。其论文特别强调:内部状态能够结合当前任务与过去输入,逐渐形成依赖上下文的表示。其中一个经典实验,让网络在没有直接标注词语边界的情况下,根据连续字母流预测后续字符。网络内部的预测误差变化和隐藏状态结构,反映出某些词语和序列规律。[Gwern][Wiley Online Library]但要避免一个夸张的说法。这并不意味着:1990年的网络已经能够像现代大型语言模型一样,阅读任何文章、理解全部含义并进行自由对话。Elman使用的是特定任务、特定规模的实验序列。其历史贡献在于:证明具有循环上下文的网络,能够通过序列预测学习某些时间结构。

为什么“预测下一个字符”有价值?

因为预测任务会迫使网络利用过去的信息。如果当前输入是:
text
A
但此前上下文不同,那么下一步出现的字符分布也可能不同。只有当内部状态携带足够的历史信息时,模型才能改善预测。因此:时间结构不一定需要被研究者全部手工标注,也可能通过预测任务成为学习的对象。这为后来的语言建模研究提供了重要思想基础。

第三幕:一张有回路的网络,怎样变成可以反向传播的计算图?

现在,我们已经知道:循环神经网络的隐藏状态取决于过去。但上一章的反向传播,有一个基本要求。计算过程需要能够被表示成有向无环的依赖图。可是,RNN看起来拥有一条回路:
text
隐藏层 → 隐藏层
那么,反向传播该怎么办?答案是:沿时间展开循环。

什么叫作Unrolling?

假设隐藏状态更新为:ht=tanh⁡(Wht−1+Uxt+b)h_t=\tanh(Wh_{t-1}+Ux_t+b)对于三个时间步,可以分别写成:h1=tanh⁡(Wh0+Ux1+b)h_1=\tanh(Wh_0+Ux_1+b)h2=tanh⁡(Wh1+Ux2+b)h_2=\tanh(Wh_1+Ux_2+b)h3=tanh⁡(Wh2+Ux3+b)h_3=\tanh(Wh_2+Ux_3+b)现在,我们将原来的回路画成一条时间链。
图58-2:RNN沿时间展开h0h1x1L1h2x2L2h3x3L3每个时间步使用相同的参数W、U和b将循环网络沿有限的时间区间展开后,可以得到一张普通的有向无环计算图。图中每一步都有自己的状态值,但各步共享模型参数。

展开以后,回路真的消失了吗?

从单次前向计算的依赖关系看,是的。因为:h1h_1依赖:h0h_0h2h_2依赖:h1h_1而:h3h_3依赖:h2h_2只要按照时间先后执行,就没有同一计算步内部的循环依赖。原来的循环连接,本质上表示:跨时间的状态反馈。所以,展开以后,我们可以直接使用上一章的反向模式自动微分。

为什么反向传播需要穿过时间?

假设只有最后一个时间步存在损失:L=L(h3)L=L(h_3)我们希望调整:WW而W同时出现在:h1, h2, h3h_1,\ h_2,\ h_3的计算中。因此:∂L∂W\frac{\partial L}{\partial W}需要包含W在多个时间步出现时的全部贡献。例如,W在第1步改变:h1h_1随后影响:h2h_2再影响:h3h_3最终影响:LL而W在第3步还会通过:h3h_3直接影响:LL因此,求导必须把多个时间路径连接起来。这项方法叫作:Backpropagation Through Time(BPTT)。随时间反向传播。

BPTT最重要的思想

它并不需要发明一套完全不同的微积分。我们只需要:将循环网络展开成计算图。在展开图中进行普通反向传播。然后把不同时间步共享参数的梯度贡献累加起来。这样就能获得:整个序列损失对共享权重的梯度。这便是BPTT的核心。但这里又埋下了另一项困难。时间步越多。反向计算链就可能越长。于是,梯度需要通过更多次矩阵乘法和激活函数导数。有些情况下,它会越来越小。另一些情况下,却可能越来越大。这便是我们接下来要研究的:梯度消失与梯度爆炸。

第四幕:1990年,Werbos把“沿时间反向传播”写成了系统的计算方法

1990年10月,《Proceedings of the IEEE》发表了一篇论文。作者是我们上一章已经认识的:保罗·韦博斯(Paul J. Werbos)。论文标题:Backpropagation Through Time: What It Does and How to Do It《随时间反向传播:它能做什么,以及怎样实现》。论文刊登于第78卷第10期,第1550—1560页。[OA Monitor Ireland][ResearchGate]这篇论文不仅解释普通反向传播,还讨论了怎样将它用于动态系统、循环网络、系统辨识和控制等问题。Werbos并不是在1990年才第一次研究反向传播。我们在第57章讲过,他的相关工作可以追溯到1974年的博士研究。1990年论文的重要性,在于将随时间求导的方法进一步系统化,使其与动态学习系统的实际计算联系起来。

一个最小的循环计算

为了把BPTT讲清楚,我们暂时不用Sigmoid或tanh。考虑一个线性循环系统:ht=aht−1+xt\boxed{ h_t=ah_{t-1}+x_t }其中:aa是需要学习的参数。设初始隐藏状态为:h0=0h_0=0输入序列只有三个数:x1=1,x2=0,x3=0x_1=1,\qquad x_2=0,\qquad x_3=0那么:h1=a×0+1=1h_1=a\times0+1=1h2=a×1+0=ah_2=a\times1+0=ah3=a×a+0=a2h_3=a\times a+0=a^2假设,我们希望最终隐藏状态等于0。因此定义损失:L=12h32\boxed{ L=\frac12h_3^2 }代入:h3=a2h_3=a^2得到:L=12a4L=\frac12a^4所以,如果直接求导:dLda=2a3\boxed{ \frac{dL}{da}=2a^3 }当:a=0.5a=0.5时:dLda=0.25\boxed{ \frac{dL}{da}=0.25 }现在,我们不使用直接展开的方法,而是沿时间执行反向传播。

第一步:从最后的损失开始

因为:L=12h32L=\frac12h_3^2所以:∂L∂h3=h3\frac{\partial L}{\partial h_3}=h_3在:a=0.5a=0.5时:h3=0.25h_3=0.25因此:hˉ3=0.25\boxed{ \bar h_3=0.25 }这里:hˉt\bar h_t表示:最终损失对第t个隐藏状态的导数。

第二步:计算a在第三个时间步的梯度贡献

第三步的计算是:h3=ah2+x3h_3=ah_2+x_3如果暂时将:h2h_2视为这一运算的输入变量,那么:∂h3∂a=h2\frac{\partial h_3}{\partial a}=h_2此时:h2=0.5h_2=0.5因此,第三步对共享参数a的局部梯度贡献:hˉ3h2=0.25×0.5\bar h_3h_2=0.25\times0.5得到:0.125\boxed{0.125}但这还不是a的全部梯度。因为a在第二步也被使用了。

第三步:把梯度传回第二个隐藏状态

由于:h3=ah2+x3h_3=ah_2+x_3所以:∂h3∂h2=a\frac{\partial h_3}{\partial h_2}=a于是:hˉ2=hˉ3a\bar h_2=\bar h_3a得到:hˉ2=0.125\boxed{\bar h_2=0.125}第二步的状态计算是:h2=ah1+x2h_2=ah_1+x_2这里,a的梯度贡献为:hˉ2h1\bar h_2h_1因为:h1=1h_1=1得到:0.125\boxed{0.125}

第四步:继续传回第一步

第一步:h1=ah0+x1h_1=ah_0+x_1而:h0=0h_0=0所以第一步对a的贡献为:hˉ1h0=0\bar h_1h_0=0最后,将三个时间步的贡献相加:dLda=0.125+0.125+0\frac{dL}{da} = 0.125+0.125+0得到:dLda=0.25\boxed{ \frac{dL}{da}=0.25 }与直接求导完全一致。
图58-3:同一个参数在多个时间步中贡献梯度
时间步前向状态对a的梯度贡献
t=1h1=1h_1=10
t=2h2=0.5h_2=0.50.125
t=3h3=0.25h_3=0.250.125
合计BPTT结果0.25

这项算法真正解决了什么?

它让我们不必为每个时间步,设计一套单独的求导公式。也不必把整个序列的递推公式展开成庞大的代数式。我们只需要:保存或重算必要的前向状态。从最后的损失开始。沿着时间反向传递梯度。并把共享参数在不同时间步的贡献相加。这样就可以训练时间跨度更长的循环系统。但此时,人们仍然遇到一项难题。我们已经知道怎样让误差返回过去,却还不能保证它在返回的路上不会逐渐消失。

第五幕:1991—1994年,研究者发现一个悖论——网络越擅长保持稳定,越可能学不到遥远的过去

1991年,Sepp Hochreiter完成了研究动态神经网络的学位论文。这项研究后来被1997年的LSTM原始论文明确引用,用于说明传统循环网络的误差信号为什么容易在长时间跨度中衰减或放大。[ML Anthology]到了1994年3月31日,《IEEE Transactions on Neural Networks》发表:Learning Long-Term Dependencies with Gradient Descent Is Difficult作者是:约书亚·本吉奥(Yoshua Bengio)。Patrice Simard。Paolo Frasconi。论文第5卷第2期,第157—166页。研究指出,传统梯度训练方法在长时间依赖问题上具有深刻的困难,并讨论了长期保存信息、抗噪声能力以及学习效率之间的张力。[数字对象标识符][PubMed]

我们可以用一个最简单的例子理解它

考虑:ht=aht−1h_t=ah_{t-1}为了研究一项早期信息能否被保留,设:h0=1h_0=1于是:h1=ah_1=ah2=a2h_2=a^2h3=a3h_3=a^3一般地:hT=aT\boxed{h_T=a^T}现在,我们问一个问题。第T步的状态,对最初的隐藏状态有多敏感?由于:hT=aTh0h_T=a^Th_0所以:∂hT∂h0=aT\boxed{ \frac{\partial h_T}{\partial h_0}=a^T }这便是一项梯度沿时间传播的最简模型。

如果a等于0.7

那么:∂h10∂h0=0.710\frac{\partial h_{10}}{\partial h_0}=0.7^{10}约为:0.028250.02825如果经过20步:0.720≈0.0007980.7^{20}\approx0.000798如果经过40步:0.740≈6.37×10−70.7^{40}\approx6.37\times10^{-7}最早的状态对最终结果的影响,变得非常微弱。如果最终损失希望让模型记住最初的信息,那么,它反向传回最初状态的梯度也会受到同样的乘积衰减。

如果a大于1

假设:a=1.1a=1.1那么:1.110≈2.591.1^{10}\approx2.591.120≈6.731.1^{20}\approx6.731.140≈45.261.1^{40}\approx45.26梯度可能迅速放大。这就是梯度爆炸的一种简单例子。

但是,真实RNN拥有非线性激活

考虑:ht=tanh⁡(aht−1+xt)h_t=\tanh(ah_{t-1}+x_t)那么:∂ht∂ht−1=a(1−tanh⁡2(aht−1+xt))\frac{\partial h_t}{\partial h_{t-1}} = a\left(1-\tanh^2(ah_{t-1}+x_t)\right)因此:∂hT∂h0=∏t=1Ta(1−ht2)\boxed{ \frac{\partial h_T}{\partial h_0} = \prod_{t=1}^{T} a\left(1-h_t^2\right) }这是一条很重要的公式。如果每一步局部导数的绝对值,都持续小于1,梯度就可能指数级衰减。如果沿某些方向的局部增益持续较大,梯度则可能增长。对于多维RNN,还需要分析一系列雅可比矩阵的乘积。所以真实情况比单个数字:aTa^T更复杂。但基本问题没有改变。反向传播在跨越很多时间步时,必须经历很多次局部导数的连乘。

为什么这被称作“长期依赖”问题?

例如,考虑一段人工序列:
text
1 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0
训练任务是:在最后一个时间步,输出最开始出现的数字。也就是:
text
1
中间的0都不携带新的目标信息。模型必须记住第一步。但训练时,最后输出的误差必须穿过大量中间状态,才能告诉第一步的参数:你应该怎样修改,才能在最后保留那条信息?如果梯度持续衰减,那么,即使最初输入非常重要,早期状态相关的学习信号也可能非常弱。

1994年论文讨论的悖论

如果一个动态系统非常稳定,能够迅速消除扰动,那么,它通常也会逐渐消除过去输入留下的影响。这对于抗噪声可能有好处。但如果我们希望某个特殊输入被长期保存,就需要它对后续状态持续产生影响。因此:我们既希望系统能够忘掉无关扰动,又希望它能够长期保留重要信息。这两种要求之间存在复杂的权衡。Bengio等人的研究,正是从这一矛盾出发,分析传统梯度下降学习长期依赖的困难。而接下来,LSTM将尝试通过新的网络结构,改善这项矛盾。

第六幕:1997年,Hochreiter与Schmidhuber没有先修补梯度,而是重新设计了记忆通道

1997年11月15日。《Neural Computation》第9卷第8期,第1735—1780页。发表:Long Short-Term Memory作者:Sepp Hochreiter。Jürgen Schmidhuber。这篇论文于1995年8月28日收到稿件,1997年2月24日被接受。论文明确说明,传统循环网络训练时,误差信号往往在跨越很长时间间隔后衰减或放大。作者提出了一种特殊的记忆单元,并使用:Constant Error Carousel(CEC,恒定误差循环通路)帮助在较长时间跨度中保存误差信号。[MIT Press][ML Anthology]

为什么普通RNN容易让状态不断改变?

考虑:ht=tanh⁡(Wht−1+Uxt+b)h_t=\tanh(Wh_{t-1}+Ux_t+b)每个时间步,旧状态都需要经过:矩阵变换。非线性激活。以及当前输入的影响。因此,旧信息可能在每一次更新中不断被压缩、变形甚至覆盖。并且,反向梯度也必须沿着这些连续的变换返回过去。

LSTM的想法:额外设置一条记忆状态通道

LSTM引入:Cell State(细胞状态或记忆状态)。通常记作:ctc_t它与用于产生当前输出的隐藏状态:hth_t相关,但并非完全相同。核心思想是:让记忆状态具有一条更直接的时间传递路径。例如,在最理想的保持情形:ct=ct−1\boxed{ c_t=c_{t-1} }于是:∂ct∂ct−1=1\frac{\partial c_t}{\partial c_{t-1}}=1跨越多个时间步:∂cT∂c0=1×1×⋯×1\frac{\partial c_T}{\partial c_0} = 1\times1\times\cdots\times1得到:∂cT∂c0=1\boxed{ \frac{\partial c_T}{\partial c_0}=1 }这意味着:沿着这条理想化的记忆状态直通路径,梯度不会仅仅因为时间步变长,就自动出现指数衰减。

这就是CEC的核心

在1997年的原始LSTM结构中,研究者通过具有单位自连接权重的内部状态机制,构造了一条能够保持误差信号的特殊路径。它被称作:Constant Error Carousel。这个名称并不是说:整张神经网络里的所有梯度都永远保持常数。它特指记忆单元内部的一项特殊误差信号传播机制。经过其他输入、输出和门控路径时,梯度仍然可能发生变化。因此,不能把LSTM简单描述成:“所有梯度都等于1。”更准确的说法是:LSTM通过专门的记忆状态通路,减少了长期依赖训练必须经过一连串普通非线性导数的情况。

可是,如果状态永远保持不变,新的信息怎么写进去?

这就需要:Input Gate(输入门)。输入门决定:什么时候允许新的信息进入记忆单元?例如,假设当前输入产生一个候选写入值:gtg_t输入门:iti_t则可以控制写入强度。在一种便于理解的简化形式中:ct=ct−1+itgtc_t=c_{t-1}+i_tg_t如果:it≈0i_t\approx0则当前新信息几乎不会写入。如果:it≈1i_t\approx1则候选新信息可以较充分地进入状态。

如果我们不希望每次都公开内部记忆呢?

还需要:Output Gate(输出门)。输出门决定:当前记忆状态的多少信息,应该进入外部可见的隐藏输出?例如:ht=ottanh⁡(ct)h_t=o_t\tanh(c_t)其中:oto_t控制输出强度。这样,模型就可能:内部保存某条信息。却暂时不将它全部暴露到外部输出。
图58-4:1997年LSTM的核心记忆思想+记忆状态的直接通路输入门输出门cₜ₋₁cₜ教学重绘:突出CEC、输入门与输出门的概念关系,不是1997年论文的完整计算拓扑。特别注意,最初的1997年LSTM并不包含后来标准结构中的独立遗忘门。

为什么输入门和输出门还不够?

如果我们只会:保存旧信息。写入新信息。决定输出多少。那么,当任务连续运行很长时间时,某些不再有用的内部状态可能长期占据记忆单元。尤其当数据流没有明确的序列结束标志,模型可能缺少清空旧信息的能力。于是,一个新的问题出现了。长期记忆不仅需要保持,还需要可控地释放。这将成为LSTM接下来一次重要改进的起点。

第七幕:2000年,“遗忘门”出现——为什么学会忘记也是一种智能?

2000年10月,《Neural Computation》第12卷第10期发表:Learning to Forget: Continual Prediction with LSTM《学习遗忘:使用LSTM进行连续预测》。作者是:费利克斯·格尔斯(Felix A. Gers)。Jürgen Schmidhuber。Fred Cummins。论文第2451—2471页。[MIT Press][PubMed]

研究者发现了什么问题?

1997年的LSTM已经能够在多种人工长时间依赖任务中有效学习。但研究者注意到:如果网络处理的是一条持续不断的数据流。并没有明确的序列终点。模型便可能缺少恰当的重置机制。某些记忆单元可能不断积累状态。最终使继续预测变得困难。这并不是说:1997年的LSTM完全不能处理序列。而是说明:长期保持信息的能力,也需要与长期连续运行中的状态清理能力相配合。

遗忘门的作用

遗忘门通常记作:ftf_t它可以在:00和:11之间取值。假设当前记忆状态为:ct−1c_{t-1}那么,遗忘门控制保留多少旧信息。例如:ct=ftct−1+itgt\boxed{ c_t=f_tc_{t-1}+i_tg_t }这便是现代常见LSTM更新公式的核心部分。当:ft≈1f_t\approx1旧记忆保留较多。当:ft≈0f_t\approx0旧记忆受到较强抑制。如果:ft=0f_t=0并且:itgt=0i_tg_t=0那么:ct=0c_t=0相当于将该状态清空。

为什么遗忘并不等于失败?

想象一个人正在连续阅读新闻。早上,新闻讨论:天气。下午,新闻讨论:股票市场。晚上,新闻讨论:足球比赛。如果系统永远把所有旧信息以完全相同的权重保存,当前状态可能逐渐被大量不相关信息占据。因此,某些任务需要:记住重要的旧内容。同时抑制已经过时的信息。这与简单的“记得越多越好”并不相同。好的记忆系统需要选择性保留,也需要选择性更新。

遗忘门是不是把过去彻底删除?

在数学上:ftf_t通常是通过Sigmoid计算得到的连续值。它更多是一种软门控机制。如果:ft=0.8f_t=0.8那不是一条绝对的“保存”或“删除”指令。而是:在这一项状态更新路径上,保留80%的旧状态数值。实际网络还会受到其他门、隐藏状态、参数以及未来输入的影响。因此,门控是一种连续的、可学习的数学控制机制。它不是硬盘文件的删除操作。

为什么这项历史区别值得认真保留?

现代教材经常直接展示:输入门。遗忘门。输出门。三个门共同组成的LSTM。然后说:“1997年,LSTM被提出。”这样讲,作为介绍当前常见结构的简化版本,比较方便。但如果要讲真实算法史,就需要明确:
  • 1997年,Hochreiter与Schmidhuber提出LSTM,核心是恒定误差通路及输入、输出门机制。
  • 2000年,Gers、Schmidhuber与Cummins正式发表了引入自适应遗忘门的研究。
  • 今天常见的三门LSTM公式,是在这一系列发展基础上形成的现代通用表达。
这并不是在吹毛求疵。因为它揭示了算法研究中非常真实的一项过程。某项创新首先解决最迫切的问题,随后又暴露出新的限制,再由后来的研究逐步完善。

第八幕:现代LSTM的完整数学结构——三个门究竟控制什么?

现在,我们已经了解:1997年LSTM的记忆通路。以及2000年加入的自适应遗忘门。终于可以完整理解现代教材最常见的一套LSTM公式。设当前输入:xtx_t上一时刻隐藏状态:ht−1h_{t-1}上一时刻细胞状态:ct−1c_{t-1}我们要计算:ctc_t与:hth_t

第一项:遗忘门

ft=σ(Wfxt+Ufht−1+bf)\boxed{ f_t=\sigma(W_fx_t+U_fh_{t-1}+b_f) }它决定:旧细胞状态应当保留多少。由于Sigmoid输出位于0与1之间,遗忘门能对不同状态分量施加连续的保留比例。

第二项:输入门

it=σ(Wixt+Uiht−1+bi)\boxed{ i_t=\sigma(W_ix_t+U_ih_{t-1}+b_i) }它决定:允许多少新信息进入当前记忆状态。

第三项:候选记忆

gt=tanh⁡(Wgxt+Ught−1+bg)\boxed{ g_t=\tanh(W_gx_t+U_gh_{t-1}+b_g) }它根据当前输入和过去隐藏状态,计算可能写入的新内容。

第四项:更新细胞状态

ct=ft⊙ct−1+it⊙gt\boxed{ c_t=f_t\odot c_{t-1}+i_t\odot g_t }这个公式是现代LSTM最重要的结构之一。第一部分:ft⊙ct−1f_t\odot c_{t-1}保留旧状态。第二部分:it⊙gti_t\odot g_t写入新状态。其中:⊙\odot表示逐元素相乘。如果:ctc_t是一个向量,那么每一个分量都可以有自己的门控系数。

第五项:输出门

ot=σ(Woxt+Uoht−1+bo)\boxed{ o_t=\sigma(W_ox_t+U_oh_{t-1}+b_o) }它决定:细胞状态应当以多大比例影响当前隐藏输出。

第六项:计算隐藏状态

ht=ot⊙tanh⁡(ct)\boxed{ h_t=o_t\odot\tanh(c_t) }于是,一次完整的LSTM更新就完成了。

用一组数字亲手计算

假设:ct−1=0.4c_{t-1}=0.4三个门与候选值分别为:ft=0.9f_t=0.9it=0.6i_t=0.6gt=0.5g_t=0.5ot=0.8o_t=0.8那么:ct=0.9×0.4+0.6×0.5c_t=0.9\times0.4+0.6\times0.5得到:ct=0.66\boxed{c_t=0.66}而隐藏输出:ht=0.8tanh⁡(0.66)h_t=0.8\tanh(0.66)约为:ht≈0.463\boxed{h_t\approx0.463}
图58-5:LSTM如何更新旧记忆并写入新内容×+×遗忘门 fₜ输入 iₜ · gₜ输出门 oₜ旧记忆更新新记忆图示仅表达门控逻辑,输出路径已简化

为什么LSTM同时拥有c和h?

可以将:ctc_t理解为:专门为跨时间保存信息而设计的内部状态。而:hth_t则是当前时刻对外提供的隐藏表示。二者相互关联。但并不完全相同。假设:ot≈0o_t\approx0即使:ctc_t内部保存了一个较大的值,当前:hth_t仍然可能非常小。这意味着:模型可以暂时保存某项内部信息,而不必每个时间步都将它充分暴露到外部输出。

三个门为什么需要训练?

因为对于真实任务,研究者通常并不知道:哪一时刻应该输入?哪一时刻应该忘记?哪一时刻应该输出?这些门本身也拥有可训练权重。它们通过反向传播与梯度下降,在训练数据中逐渐学习控制策略。所以,LSTM不是一套永远按照人工规则打开关闭的逻辑电路。它是一种具有可学习门控参数的循环神经网络结构。

一个重要的数学限制

在现代门控LSTM中:ct=ftct−1+itgtc_t=f_tc_{t-1}+i_tg_t如果我们只观察保持旧状态的那条直接路径,并暂时固定各个门控数值,那么:∂ct∂ct−1∣direct=ft\boxed{ \left. \frac{\partial c_t}{\partial c_{t-1}} \right|_{\text{direct}} =f_t }跨越多个时间步:∂cT∂cs∣direct=∏t=s+1Tft\boxed{ \left. \frac{\partial c_T}{\partial c_s} \right|_{\text{direct}} = \prod_{t=s+1}^{T}f_t }如果:ft≈1f_t\approx1这条直接路径上的梯度能够较好地保留。但是,真实LSTM的完整导数还可能包括门控网络、隐藏状态反馈和其他依赖路径的贡献。因此,直接路径的乘积,不等于全部真实网络梯度在所有条件下的完整表达。这个区别,在接下来的互动实验中必须保持清楚。

第九幕:互动实验室——三十个时间步之后,网络还记得第一个数字吗?

现在,所有历史和数学概念终于可以结合起来。我们设计一个非常简单的人工记忆任务。在第一个时间步,向模型输入:x1=1x_1=1之后连续输入很多个零:
text
1 0 0 0 0 0 0 0 0 0 ... 0
我们希望模型能够将最初的:11保留到最后。为了清楚比较,我们使用两种简化系统。第一种是线性RNN:h1=1h_1=1ht=aht−1,t≥2h_t=ah_{t-1},\quad t\ge2第二种是固定门控的LSTM细胞状态直通路径:c1=1c_1=1ct=fct−1,t≥2c_t=fc_{t-1},\quad t\ge2其中:aa是RNN的循环系数。ff是LSTM的固定遗忘门系数。这两套系统都是专门构造的教学模型。目的不是比较真实训练完成的RNN与LSTM性能,而是隔离出:跨时间状态保持倍率,以及沿相应路径传递的梯度倍率。

互动实验:记忆保持与长程梯度

图58-6:第一个数字经过多少时间步才能被保留下来?两条曲线分别模拟线性RNN的状态保持,以及固定遗忘门下LSTM记忆直通路径的保持。纵轴采用对数刻度,使极小的数值也可以被观察。

30

0.65
0.98
30-3-6-9-12-15-18log₁₀(保持倍率)130时间步 t
线性RNN
LSTM固定门直通路径
30 / 30
RNN当前状态

3.754e-6

直接梯度倍率:3.754e-6
LSTM记忆状态

0.556617

直通梯度倍率:0.556617
完整T个时间步以后
RNN状态与直接梯度倍率3.754e-6
LSTM直通状态与梯度倍率0.556617
本实验故意使用线性RNN和固定门系数,令状态倍率与直接状态导数相同。真实RNN通常有非线性激活;真实LSTM的门由可训练网络计算,梯度还通过其他路径传播。本实验不能据此推出真实LSTM的整体梯度恒为1,或任何LSTM都能完美记忆任意长度的信息。

实验一:经过30个时间步,最初的数字还剩多少?

选择:“长期记忆”。此时:a=0.65a=0.65f=0.98f=0.98从第一个时间步到第30步,共有29次保持更新。对于RNN:h30=0.6529h_{30}=0.65^{29}大约只有:3.8×10−63.8\times10^{-6}而LSTM的固定门状态:c30=0.9829c_{30}=0.98^{29}约为:0.5570.557差距非常明显。但不要误以为:0.557已经等于完美记忆。它仍然意味着旧状态比最初下降了约44.3%。

实验二:把RNN循环系数增大到1.10

现在:a=1.10a=1.10连续放大后:h30=1.1029h_{30}=1.10^{29}会达到十几倍的初始值。这说明:简单地把循环权重调大,并不能解决长期记忆问题。它可能从一个极端:梯度消失。走向另一个极端:梯度爆炸。对于具有tanh等非线性的网络,状态数值可能受到激活函数约束,但反向敏感度仍然可能存在复杂的放大与衰减问题。

实验三:将遗忘门调到0.70

选择:“遗忘门关闭”。此时:f=0.70f=0.70那么,LSTM状态也会逐步衰减:c30=0.7029c_{30}=0.70^{29}这说明:LSTM不是永远不遗忘,而是允许网络学习什么时候保持、什么时候遗忘。对于已经过时的内容,这种遗忘反而可能是正确行为。

实验四:将遗忘门调到1

此时:ct=ct−1c_t=c_{t-1}沿这条理想化的直接记忆通路:∂cT∂c1=1\frac{\partial c_T}{\partial c_1}=1不论经过多少步,状态与直接梯度都得到保留。这正好让我们理解1997年LSTM的CEC思想。但真实Sigmoid门在有限输入下通常严格介于0与1之间。所以,互动实验中的:f=1f=1是一种理想化的数学保持情形。它不代表真实网络在有限参数下必然能够得到完全精确的遗忘门输出1。

第十幕:截断BPTT——如果一条序列有十万步,我们真的要把全部历史展开吗?

现在,我们知道:RNN沿时间展开以后,可以使用BPTT训练。但是,这种方法在实际工程中,还有一项非常现实的问题。如果序列非常长,怎么办?假设我们需要处理:100个时间步。可能还比较容易。如果是:10,000个时间步。甚至:100,000个时间步。我们是否必须保存全部时间步的中间激活?然后让反向传播穿越整条历史?

完整BPTT的计算与内存成本

设序列长度:TT隐藏状态维度:HH输入维度:DD对于一个使用稠密矩阵变换的简单RNN,每步主要计算包括:Whht−1W_hh_{t-1}以及:WxxtW_xx_t主要算术量约为:O(H2+HD)O(H^2+HD)因此,完整T步的前向计算主要成本约为:O(T(H2+HD))\boxed{ O(T(H^2+HD)) }反向传播也具有类似的计算数量级。而保存隐藏状态本身,至少会涉及:O(TH)O(TH)量级的状态数据。具体内存占用,还取决于保存哪些激活、临时张量、算子信息和实现方式。因此,随着:TT增长,计算量与中间状态内存需求都会增加。

截断BPTT的想法

一种常见的工程策略是:只让反向梯度穿过有限长度的时间窗口。例如:每次允许反向传播最多20个时间步。然后切断计算图中的历史梯度连接。这种方法叫作:Truncated Backpropagation Through Time(截断随时间反向传播)。

先看一张图

图58-7:保留前向状态,但截断反向梯度h1h2h3h4h5h6反向截断点允许反向传播不再追溯这段历史前向计算仍可将旧状态数值传到新的时间窗口
这里最重要的区别是:截断梯度,并不一定意味着将隐藏状态的数值清零。例如:在第20个时间步计算出:h20h_{20}下一段仍然可以使用:h20h_{20}作为初始状态。但是,反向传播不会继续追溯计算:h20h_{20}时使用的更早历史依赖。在现代自动微分框架中,这类操作通常涉及将边界状态从旧计算图中分离。

为什么这样做能够节省资源?

因为反向传播只需要处理当前有限长度的计算窗口。因此,某些情况下,不必保留全部历史时间步的计算图。这对于连续流数据训练非常有用。但它也带来一项限制。

如果真正重要的信息位于50步以前呢?

假设:当前反向窗口只有20步。最终损失真正依赖于50步之前某项输入。那么,虽然前向隐藏状态可能携带了那项信息,但当前反向传播不能通过被截断的连接,直接将梯度传回50步之前的计算过程。这可能使学习长期依赖更加困难。因此,截断BPTT并不是无条件更好的训练方式。它是一种:降低计算与存储成本,同时牺牲部分长期梯度信息的近似策略。这与LSTM的设计目标也存在联系。LSTM通过门控记忆状态,使某些重要信息在跨越较长时间以后仍可能被有效保留。但如果训练时使用过短的截断窗口,相关参数仍可能缺少必要的远程学习信号。

1990年Werbos论文中的意义

Werbos在1990年的BPTT研究中,讨论的不只是最简单的完整时间展开,还包括动态系统中实际训练需要面对的不同计算变体与工程问题。因此,BPTT的发展始终与:计算成本。状态保存。以及反向敏感度怎样跨时间计算。密切相关。[ResearchGate]时间是循环网络能够利用的上下文,也同时是它训练时必须付出的计算成本。

第十一幕:2014年,为什么研究者又设计了一种更简洁的门控循环单元?

1997年以后,LSTM逐渐成为重要的序列建模方法。但它的结构相对复杂。对于常见的现代版本,一次更新需要计算:遗忘门。输入门。输出门。候选记忆。也就是四组主要的仿射变换。于是,研究者开始思考:能不能保留门控记忆的主要思想,同时减少结构复杂度?

2014年:GRU

2014年6月3日,研究者:曹京贤(Kyunghyun Cho)。Bart van Merriënboer。Çağlar Gülçehre。Dzmitry Bahdanau。Fethi Bougares。Holger Schwenk。Yoshua Bengio。提交论文:Learning Phrase Representations Using RNN Encoder–Decoder for Statistical Machine Translation该研究随后发表于2014年10月的EMNLP会议,第1724—1734页。[ACL Anthology]论文提出一种RNN编码器—解码器模型,并使用具有门控机制的循环单元。这种单元后来广泛称为:Gated Recurrent Unit(GRU,门控循环单元)。

GRU的核心结构

与常见LSTM不同,GRU通常不区分独立的:ctc_t与:hth_t两套状态。它使用:hth_t作为主要状态。并设置:Update Gate(更新门)。以及:Reset Gate(重置门)。一种常见的GRU公式约定是:zt=σ(Wzxt+Uzht−1+bz)\boxed{ z_t=\sigma(W_zx_t+U_zh_{t-1}+b_z) }rt=σ(Wrxt+Urht−1+br)\boxed{ r_t=\sigma(W_rx_t+U_rh_{t-1}+b_r) }候选状态:h~t=tanh⁡(Whxt+Uh(rt⊙ht−1)+bh)\boxed{ \tilde h_t= \tanh\left( W_hx_t+U_h(r_t\odot h_{t-1})+b_h \right) }最后:ht=(1−zt)⊙ht−1+zt⊙h~t\boxed{ h_t=(1-z_t)\odot h_{t-1}+z_t\odot\tilde h_t }请注意:不同论文和软件对更新门的符号、候选状态混合权重可能使用相反的约定。本章采用的定义中:zt≈0z_t\approx0意味着更多保留旧状态。zt≈1z_t\approx1意味着更多使用新候选状态。不能不看定义,就认为所有GRU公式中的:ztz_t都具有完全相同的保留方向。

GRU与LSTM的比较

图58-8:两类门控循环网络的结构比较
特征LSTMGRU
主要状态ct,htc_t,h_thth_t
常见门控遗忘、输入、输出更新、重置
候选更新有有
典型主要仿射计算组数43
通常的单步计算相对较多相对较少
实际参数量和运行速度还取决于隐藏维度、输入维度、门的具体实现、投影层、矩阵融合和硬件内核。不能仅由门数量推出所有情况下的速度结论。

GRU是不是LSTM的升级替代品?

不能这样理解。GRU结构通常更简洁。某些任务中,它可能以更少参数获得良好表现。但在另一些任务中,LSTM的独立细胞状态与门控设计,也可能更适合。哪一种效果更好,仍然依赖:数据。任务。训练设置。模型规模。以及硬件。历史上,GRU更适合被看成:门控循环网络研究中的另一条重要结构路线。而不是:“LSTM已经过时,所以必须全部换成GRU。”

2014年论文真正研究的背景

Cho等人的工作,不只是提出一种新的门控单元。他们还研究了:怎样将一个符号序列编码成向量表示。再通过另一个循环网络,将这项表示转换成另一条符号序列。这种结构叫作:Encoder–Decoder(编码器—解码器)。它会成为下一幕的重要内容。

第十二幕:2014年,一句话怎样被另一种语言重新说出来?

现在,我们重新回到本章开篇提出的语言问题。如果一个网络可以处理过去的序列。那么,它是否能够读取:一整句英文。然后生成:一整句法文?例如:
text
I love music.
对应:
text
J'aime la musique.
这里不是简单地把每个英文单词直接替换成一个法文单词。不同语言的:词序。语法。冠词。搭配关系。都可能不同。因此,模型需要学习序列之间更加复杂的对应关系。

2014年:Sequence to Sequence

2014年9月10日,研究者:伊利亚·苏茨克维(Ilya Sutskever)。奥里奥尔·维尼亚尔斯(Oriol Vinyals)。Quoc V. Le。提交:Sequence to Sequence Learning with Neural Networks《使用神经网络进行序列到序列学习》。该研究发表于2014年的NeurIPS会议。[Google Research][ML Anthology]论文采用多层LSTM,将输入序列编码成固定维度的表示,再使用另一套LSTM生成输出序列。

编码器怎样工作?

假设输入包含:x1,x2,…,xTx_1,x_2,\ldots,x_T编码器逐步更新:ht=f(ht−1,xt)h_t=f(h_{t-1},x_t)最后形成一个用于表示输入句子的状态。在一种简化描述中,可以将它记作:v=Encoder⁡(x1,…,xT)\boxed{v=\operatorname{Encoder}(x_1,\ldots,x_T)}这个:vv就像输入序列的一份压缩表示。但它通常并不等于一句话的完整、无损副本。

解码器怎样工作?

解码器利用编码得到的状态,逐个生成:y1,y2,…,yMy_1,y_2,\ldots,y_M每一步的生成,都依赖:当前解码器状态。此前已经产生的符号。以及输入编码信息。因此,输入和输出的长度不必相同。这对于机器翻译非常重要。
图58-9:LSTM编码器—解码器的基本流程E1E2E3D1D2编码输入序列生成输出序列v这是简化流程;真实模型还要处理状态初始化、结束标记等细节

2014年论文报告了什么?

在WMT 2014英语—法语翻译任务上,作者报告了LSTM序列到序列模型的实验结果,并研究了使用LSTM表示句子及改变输入词序的效果。论文还发现:在其特定实验设置里,反转输入源语言句子的顺序,可以改善优化和翻译表现。其解释之一是:这种处理使某些源词与目标词之间的依赖跨度缩短,从而使学习更加容易。[Google Research]这是一项非常有意思的发现。它说明:即使网络具有LSTM这样的长期记忆结构,输入信息怎样排列、不同符号之间的路径距离有多长,仍然会影响训练。

一个新的困难出现了

但是,将整句话压缩进一个固定维度的向量,也可能成为瓶颈。假设源语言句子非常长。如果解码器每生成一个新词,都主要依赖一份固定长度的全句表示,某些细节可能难以保留和提取。于是,研究者开始考虑另一种方法。当解码器正在生成某个词时,能否直接查看输入句子中最相关的部分?而不是要求它提前把整句话全部压缩进一个固定向量。这个新方向,就是:Attention(注意力机制)。2014年的一项研究中,Dzmitry Bahdanau、Kyunghyun Cho和Yoshua Bengio提出了在神经机器翻译中联合学习对齐与翻译的方法;相关论文随后发表于ICLR 2015。[Yoshua Bengio]至此,本章的研究历史已经为下一章铺好了道路。循环神经网络让机器可以利用过去的信息。LSTM和GRU改善了状态保留与更新。编码器—解码器让模型开始从一条序列生成另一条序列。而注意力机制将进一步提出:不必把全部重要内容都塞进单一状态;当需要某项信息时,可以有选择地直接访问它。

第十三幕:Python实战——从零实现RNN的BPTT与LSTM的门控反向传播

现在,我们不再只画结构图。我们要让全部公式变成真正可以运行的代码。本章的实战分为三部分。第一部分:手写普通RNN的前向计算与BPTT。第二部分:用有限差分验证BPTT的梯度。第三部分:手写一个包含输入门、遗忘门和输出门的标量LSTM,并实现它的完整时间反向传播。不使用PyTorch。不使用NumPy。只使用Python标准库。

第一部分:普通RNN的完整BPTT

我们的RNN只有一个隐藏状态:ht=tanh⁡(aht−1+bxt)h_t=\tanh(ah_{t-1}+bx_t)参数:a, ba,\ b都是待学习的标量。我们希望最后一个隐藏状态接近目标:yy所以:L=12(hT−y)2L=\frac12(h_T-y)^2
python

import math


def rnn_forward(xs, a, b, target):
    """
    标量RNN:
        h[t] = tanh(a*h[t-1] + b*x[t])

    最后一步的状态用于预测target。
    """

    states = [0.0]

    for x in xs:
        h_prev = states[-1]

        z = a*h_prev + b*x

        states.append(math.tanh(z))

    loss = 0.5 * (states[-1]-target)**2

    return states, loss


def rnn_bptt(xs, a, b, target):
    """
    手动实现完整BPTT。

    返回:
        loss
        dL/da
        dL/db
        各时间步隐藏状态
    """

    states, loss = rnn_forward(
        xs, a, b, target
    )

    grad_a = 0.0
    grad_b = 0.0

    # 从最终损失反向开始
    dh = states[-1] - target

    # 从T回到1
    for t in range(len(xs), 0, -1):

        h = states[t]
        h_prev = states[t-1]
        x = xs[t-1]

        # tanh的局部导数
        dz = dh * (1-h*h)

        # 共享参数a,b在当前时间步的贡献
        grad_a += dz * h_prev
        grad_b += dz * x

        # 将梯度传回上一时刻隐藏状态
        dh = dz * a

    return loss, grad_a, grad_b, states


xs = [1.0] + [0.0]*11

a = 0.8
b = 0.7
target = 0.5

loss, ga, gb, states = rnn_bptt(
    xs, a, b, target
)

print("最终隐藏状态:", states[-1])
print("损失:", loss)
print("dL/da:", ga)
print("dL/db:", gb)
这个例子的主要数值为:
text
最终隐藏状态: 0.0433335
损失:         0.1042721
dL/da:      -0.2518358
dL/db:      -0.0144073
值得注意的是:虽然输入中只有第一个时间步出现了非零值,但参数a仍然能收到来自很多时间步的梯度贡献。因为:aa控制旧状态在每一个时间步怎样影响新状态。而:bb主要通过第一次非零输入影响状态,然后间接影响后续结果。

第二部分:使用数值梯度检查BPTT

现在,我们要确认:手写BPTT的梯度,是否真的等于目标函数的数学导数?使用中心差分:∂L∂a≈L(a+ε,b)−L(a−ε,b)2ε\frac{\partial L}{\partial a} \approx \frac{L(a+\varepsilon,b)-L(a-\varepsilon,b)}{2\varepsilon}同理,也可以检查:∂L∂b\frac{\partial L}{\partial b}
python

def numerical_rnn_grad(
    xs, a, b, target, eps=1e-5
):
    def objective(aa, bb):
        _, loss = rnn_forward(
            xs, aa, bb, target
        )
        return loss

    ga = (
        objective(a+eps, b)
        - objective(a-eps, b)
    ) / (2*eps)

    gb = (
        objective(a, b+eps)
        - objective(a, b-eps)
    ) / (2*eps)

    return ga, gb


numeric_a, numeric_b = numerical_rnn_grad(
    xs, a, b, target
)

print("解析BPTT:", ga, gb)
print("有限差分:", numeric_a, numeric_b)

assert abs(ga-numeric_a) < 1e-6
assert abs(gb-numeric_b) < 1e-6

print("RNN反向传播梯度验证成功")
这项验证很重要。因为BPTT最容易出现的错误之一,就是遗漏共享参数在某些时间步产生的梯度贡献。如果你把:
python
grad_a += dz * h_prev
错误改成:
python
grad_a = dz * h_prev
就会不断覆盖此前已经累积的梯度。最终结果通常会与数值差分不一致。因此,我们再次看到了第57章的重要原则:同一个参数在计算图中多次使用时,全部相关路径的梯度贡献必须相加。

第三部分:实现一个真正的现代LSTM单元

现在,我们实现四组计算:遗忘门:ftf_t输入门:iti_t输出门:oto_t以及候选状态:gtg_t为了让全部公式能够手工检查,我们只使用:一个标量输入。一个标量隐藏状态。一个标量细胞状态。实际神经网络会使用向量和矩阵,但其逐元素门控与链式求导思想相同。
python

def sigmoid(z):
    """数值稳定的Sigmoid。"""
    if z >= 0:
        return 1/(1+math.exp(-z))

    e = math.exp(z)
    return e/(1+e)


PARAMS = {
    # 每个门都有三个参数:
    # (当前输入权重, 上一隐藏状态权重, 偏置)

    "f": (0.15, 0.05, 2.0),
    "i": (0.40, 0.10, -0.2),
    "o": (0.35, 0.08, 0.3),
    "g": (0.65, 0.12, 0.0),
}


def lstm_forward(xs, params):
    """
    标量LSTM前向计算。

    返回:
        最终隐藏状态h
        每个时间步的中间计算记录

    记录中间值是为了随后执行BPTT。
    """

    h = 0.0
    c = 0.0

    history = []

    for x in xs:

        h_prev = h
        c_prev = c

        pre = {}

        for name, (wx, uh, bias) in params.items():
            pre[name] = (
                wx*x + uh*h_prev + bias
            )

        f = sigmoid(pre["f"])
        i = sigmoid(pre["i"])
        o = sigmoid(pre["o"])
        g = math.tanh(pre["g"])

        c = f*c_prev + i*g
        h = o*math.tanh(c)

        history.append({
            "x": x,
            "h_prev": h_prev,
            "c_prev": c_prev,
            "f": f,
            "i": i,
            "o": o,
            "g": g,
            "c": c,
            "h": h,
        })

    return h, history
测试:
python

xs = [1.0] + [0.0]*9

h, history = lstm_forward(xs, PARAMS)

print("最终隐藏状态:", h)

for t, record in enumerate(history, start=1):
    print(
        f"t={t:2d}",
        f"f={record['f']:.4f}",
        f"i={record['i']:.4f}",
        f"o={record['o']:.4f}",
        f"c={record['c']:.6f}",
        f"h={record['h']:.6f}",
    )
这里的门不再是我们在互动实验里人为指定的固定常数。它们会根据:当前输入。上一时刻隐藏状态。以及自身的权重与偏置。动态计算。这更接近真实LSTM的工作方式。但这些权重目前仍然是我们人为指定的初始参数。尚未进行训练。

第四部分:完整推导LSTM的反向传播

这是本章数学含量最高的一部分。但我们已经有了全部必要基础。从最后一个时间步开始,假设:L=12(hT−y)2L=\frac12(h_T-y)^2所以:∂L∂hT=hT−y\frac{\partial L}{\partial h_T}=h_T-y为了反向计算,我们需要分别维护:隐藏状态梯度:dhdh以及:细胞状态梯度:dcdc因为LSTM有两条重要状态通路。在某个时间步t,首先计算输出门的梯度贡献。由于:ht=ottanh⁡(ct)h_t=o_t\tanh(c_t)所以:∂L∂ot=dhttanh⁡(ct)\boxed{ \frac{\partial L}{\partial o_t} = dh_t\tanh(c_t) }而细胞状态从隐藏输出得到的梯度为:dht ot(1−tanh⁡2(ct))dh_t\,o_t(1-\tanh^2(c_t))再加上来自未来时间步细胞状态通路的梯度。因此:dct=dcnext+dht ot(1−tanh⁡2(ct))\boxed{ dc_t= dc_{\mathrm{next}} + dh_t\,o_t(1-\tanh^2(c_t)) }接着,由:ct=ftct−1+itgtc_t=f_tc_{t-1}+i_tg_t得到:dft=dct ct−1\boxed{ df_t=dc_t\,c_{t-1} }dit=dct gt\boxed{ di_t=dc_t\,g_t }dgt=dct it\boxed{ dg_t=dc_t\,i_t }而传回旧细胞状态的梯度是:dct−1=dctft\boxed{ dc_{t-1}=dc_tf_t }最后,利用各个门的Sigmoid或tanh导数,得到它们对对应仿射输入的梯度。并且将所有门对:ht−1h_{t-1}的贡献加起来。这就是LSTM完整BPTT的核心计算过程。

将它写成Python

python

def lstm_bptt(xs, params, target):
    """
    标量LSTM完整BPTT。

    参数:
        params[name] = (wx, uh, bias)

    返回:
        loss
        grads[name] = [dL/dwx, dL/duh, dL/dbias]

    本例只在最后一个时间步计算损失。
    """

    h, history = lstm_forward(xs, params)

    loss = 0.5*(h-target)**2

    grads = {
        name: [0.0, 0.0, 0.0]
        for name in params
    }

    # 来自最终损失的隐藏状态梯度
    dh = h-target

    # 来自后一时间步的细胞状态梯度
    dc_next = 0.0

    for step in reversed(history):

        f = step["f"]
        i = step["i"]
        o = step["o"]
        g = step["g"]

        c = step["c"]
        c_prev = step["c_prev"]

        h_prev = step["h_prev"]
        x = step["x"]

        tc = math.tanh(c)

        # 输出门贡献
        do = dh*tc

        # 合并隐藏输出路径与未来细胞状态路径
        dc = (
            dc_next
            + dh*o*(1-tc*tc)
        )

        # 细胞状态更新中的三项贡献
        df = dc*c_prev
        di = dc*g
        dg = dc*i

        # 传给上一时刻细胞状态
        dc_next = dc*f

        # 各门的激活函数反向导数
        dz = {
            "f": df*f*(1-f),
            "i": di*i*(1-i),
            "o": do*o*(1-o),
            "g": dg*(1-g*g),
        }

        # 当前时间步传给上一隐藏状态的梯度
        dh_prev = 0.0

        for name, gate_grad in dz.items():

            wx, uh, bias = params[name]

            grads[name][0] += gate_grad*x
            grads[name][1] += gate_grad*h_prev
            grads[name][2] += gate_grad

            dh_prev += gate_grad*uh

        dh = dh_prev

    return loss, grads
这里最值得观察的是:
python
dc_next = dc*f
它对应LSTM细胞状态的直接梯度通路。而:
python
dh_prev += gate_grad*uh
则对应隐藏状态通过各个门的反馈参数,将梯度传向前一个时间步。因此,我们没有将完整LSTM简化成单独一个:fTf^T的乘积。代码显式计算了:各个门。细胞状态。隐藏状态。以及共享参数在所有时间步的梯度累加。

第五部分:验证LSTM的梯度

因为LSTM的反向传播比普通RNN复杂得多,我们必须利用数值差分检查。
python

def lstm_objective(xs, params, target):
    h, _ = lstm_forward(xs, params)
    return 0.5*(h-target)**2


def numerical_lstm_param_grad(
    xs,
    params,
    target,
    gate_name,
    parameter_index,
    eps=1e-5,
):
    plus = {
        name: list(values)
        for name, values in params.items()
    }

    minus = {
        name: list(values)
        for name, values in params.items()
    }

    plus[gate_name][parameter_index] += eps
    minus[gate_name][parameter_index] -= eps

    return (
        lstm_objective(xs, plus, target)
        - lstm_objective(xs, minus, target)
    ) / (2*eps)


xs = [1.0] + [0.0]*9
target = 0.2

loss, grads = lstm_bptt(
    xs, PARAMS, target
)

print("当前损失:", loss)

for name in ("f", "i", "o", "g"):
    print(name, grads[name])

for name, idx in (
    ("f", 2),
    ("i", 0),
    ("o", 1),
    ("g", 0),
):
    analytical = grads[name][idx]

    numerical = numerical_lstm_param_grad(
        xs, PARAMS, target,
        name, idx
    )

    print(
        name, idx,
        "BPTT:", analytical,
        "差分:", numerical
    )

    assert abs(
        analytical-numerical
    ) < 1e-6

print("LSTM反向传播梯度检查通过")
这段程序的主要输出之一是:
text
当前损失: 0.0073010
而几个待检查的梯度约为:
参数BPTT梯度
遗忘门偏置 bfb_f−0.00975716
输入门输入权重 WiW_i−0.00425989
输出门循环权重 UoU_o−0.00050411
候选状态输入权重 WgW_g−0.01114347
通过有限差分计算,它们应该与BPTT结果在合理浮点误差范围内一致。这证明:我们不仅成功实现了LSTM的前向状态更新。还完整实现了该标量LSTM在整个序列上的反向梯度计算。

第六部分:利用计算出的梯度进行训练

现在,我们可以将:∂L∂θ\frac{\partial L}{\partial\theta}用于更新所有门控参数。
python

def train_lstm(
    xs,
    initial_params,
    target,
    lr=0.05,
    steps=100,
):
    params = {
        name: list(values)
        for name, values in initial_params.items()
    }

    losses = []

    for step in range(steps):

        loss, grads = lstm_bptt(
            xs, params, target
        )

        losses.append(loss)

        for name in params:
            for j in range(3):
                params[name][j] -= (
                    lr*grads[name][j]
                )

    return params, losses


xs = [1.0] + [0.0]*9

trained_params, losses = train_lstm(
    xs,
    PARAMS,
    target=0.2,
    lr=0.05,
    steps=100,
)

print("初始损失:", losses[0])
print("最后一轮更新前损失:", losses[-1])

assert losses[-1] < losses[0]

print("LSTM参数训练完成")
现在,程序会真的修改:输入门权重。遗忘门权重。输出门权重。以及候选状态权重。它不只是使用一组人为指定的固定门。而是通过训练数据中的最终损失,反向计算这些参数的梯度,再不断更新。当然,这仍然是一个极度简化的单样本任务。它不代表LSTM已经学会了通用的语言理解能力。但从算法角度看,我们已经完整实现了:LSTM前向计算 → 时间反向传播 → 参数更新。这正是现代门控循环网络训练的基本机制。

第十四幕:1989年,还有另一种训练循环网络的方法——不必等到最后才能求梯度

到现在,我们已经详细研究BPTT。它的基本策略是:先执行一段序列的前向计算。保存或重新获得中间状态。最后沿时间反向求导。但研究者还有另一项考虑。如果系统永远不会停止,我们能不能在它持续运行的时候,一边接收新输入,一边更新梯度?例如:一台持续工作的工业控制器。一项实时传感器系统。或者一段不容易划分固定起止边界的动态数据流。如果必须等到序列完全结束,才能进行训练,就可能不适合某些在线任务。

1989年:Williams与Zipser

1989年6月,《Neural Computation》第1卷第2期发表:A Learning Algorithm for Continually Running Fully Recurrent Neural Networks《一种用于连续运行的全循环神经网络的学习算法》。作者是:Ronald J. Williams。以及:大卫·齐普瑟(David Zipser)。论文第270—280页。[OA Monitor Ireland][Khoury College of Computer Sciences]这篇研究与一种通常称作:Real-Time Recurrent Learning(RTRL,实时循环学习)的方法密切相关。

RTRL与BPTT有什么区别?

BPTT可以先完成前向计算,再反向传播。而RTRL试图在系统持续前向运行时,维护隐藏状态对参数的敏感度。例如:ht=f(ht−1,xt;θ)h_t=f(h_{t-1},x_t;\theta)我们希望知道:∂ht∂θ\frac{\partial h_t}{\partial\theta}利用链式法则:∂ht∂θ=∂f∂ht−1∂ht−1∂θ+∂f∂θ\boxed{ \frac{\partial h_t}{\partial\theta} = \frac{\partial f}{\partial h_{t-1}} \frac{\partial h_{t-1}}{\partial\theta} + \frac{\partial f}{\partial\theta} }这项公式告诉我们:当前状态对参数的敏感度,由两部分组成。第一:过去状态对参数的影响,经过当前状态更新函数继续传递。第二:当前时间步直接使用参数产生的新影响。

一个单参数例子

设:ht=tanh⁡(aht−1+bxt)h_t=\tanh(ah_{t-1}+bx_t)我们关心:Pt=∂ht∂aP_t=\frac{\partial h_t}{\partial a}那么:Pt=(1−ht2)(ht−1+aPt−1)\boxed{ P_t= (1-h_t^2) \left( h_{t-1}+aP_{t-1} \right) }只要知道:当前状态。上一状态。以及此前维护的:Pt−1P_{t-1}就能够计算新的:PtP_t不需要为这个单参数例子反向遍历整条历史。

既然RTRL可以在线计算,为什么不全部使用它?

因为对于大型循环网络,敏感度的数量可能非常庞大。假设:隐藏状态向量包含:HH个分量。循环权重矩阵包含约:H2H^2个参数。如果我们显式保存每一个隐藏状态分量对每一个参数的敏感度,就需要约:H×H2=H3H\times H^2=H^3个敏感度数值。对于一般稠密RNN,经典精确RTRL还可能具有非常昂贵的每时间步计算成本。这在较大的网络中会成为实际限制。而BPTT通常更适合利用现代张量运算,对一段有限长度的序列批量求梯度。因此,1989年论文也强调了:RTRL能够用于连续运行系统。但代价是:计算量昂贵,并需要复杂的网络内部信息传播。[Gwern]

两种方法并不是完全对立

图58-10:BPTT与RTRL的主要区别
项目BPTTRTRL
主要计算方向反向穿过时间展开图前向传播参数敏感度
时间依赖需要相应历史计算关系可在线递推
典型优势有限序列上高效计算梯度适合持续在线的敏感度计算
典型限制历史激活与反向窗口成本高维敏感度计算成本
这项比较也让我们发现:BPTT与RTRL之间的区别,与第57章讲过的:反向模式自动微分。前向模式自动微分。具有深刻联系。BPTT沿展开图反向求导。RTRL则沿时间正向维护参数敏感度。两者都来自链式法则。但由于传播的信息不同,计算成本也不同。这再次说明:同一项数学定律,经过不同的计算组织方式,可以产生适用于不同场景的算法。

第十五幕:1997年,为什么有些序列任务还需要同时查看“未来”?

循环网络的设计最初非常自然。在时间:tt计算当前状态时,只使用过去信息:ht−1h_{t-1}和当前输入:xtx_t这样可以满足某些实时预测任务。例如:根据此前的传感器读数,预测下一秒温度。或者:在输入字符不断到来时,预测下一字符。但是,所有序列任务都只需要过去吗?当然不是。

一个语言中的例子

假设我们正在阅读:
text
他没有把那本书放到桌上,而是把它交给了朋友。
如果要分析中间某个词的语法作用,后面出现的词语可能帮助消除歧义。同样,在语音识别中,某个当前音素的判断,也可能受到后面相邻声音的影响。如果整段语音已经录制完成,为什么一定只能从左到右处理?研究者可以同时使用:前面的上下文。以及:后面的上下文。

1997年:双向循环神经网络

1997年11月,《IEEE Transactions on Signal Processing》第45卷第11期发表:Bidirectional Recurrent Neural Networks《双向循环神经网络》。作者是:迈克·舒斯特(Mike Schuster)。库尔迪普·帕利瓦尔(Kuldip K. Paliwal)。论文第2673—2681页。[国立国会図書館][IEEE Xplore]他们研究的结构,在一个方向上从左到右处理序列。在另一个方向上从右到左处理。然后,将两种方向的内部表示共同用于输出预测。

双向RNN的计算

正向状态:h→t=f(h→t−1,xt)\boxed{ \overrightarrow h_t= f(\overrightarrow h_{t-1},x_t) }反向状态:h←t=g(h←t+1,xt)\boxed{ \overleftarrow h_t= g(\overleftarrow h_{t+1},x_t) }最后:yt=ϕ(h→t,h←t)\boxed{ y_t=\phi( \overrightarrow h_t, \overleftarrow h_t ) }输出函数:ϕ\phi可以是拼接后经过线性层,也可以采用其他形式。
图58-11:同一段序列的两个方向x1x2x3x4x5正向:利用过去的信息反向:利用后面的信息每个位置可综合两个方向得到的状态表示

双向RNN与LSTM是什么关系?

双向是一种:序列处理方向的结构设计。LSTM是一种:循环状态更新单元。因此,它们可以组合。例如:使用LSTM分别从两个方向处理序列,就得到:Bidirectional LSTM(BiLSTM)。这并不是两者互相替代。而是:在每个方向上,使用更适合长期状态传播的门控单元。

为什么不是所有任务都可以使用双向模型?

因为双向模型在计算某个时间步的结果时,通常还需要使用其后面的输入。如果整段序列已经完整存在,就可以这样做。例如:离线语音分析。已录制文本的序列标注。以及对已经完整获得的数据进行分类。但是,如果我们必须实时预测下一秒尚未发生的事件,那么:未来的观测并不存在。此时,我们不能假装已经知道未来信息。因此,双向模型是否适用,取决于任务是否允许访问完整序列。这是由实际信息可用性决定的限制。不是简单的网络性能高低问题。

第十六幕:2017年以后,Transformer兴起了,循环神经网络就不重要了吗?

到现在,我们已经经历了从1980年代到2014年的循环网络发展。我们看到:普通RNN通过状态反馈保存历史。BPTT让最终误差能够穿过时间。LSTM改善了长程状态与误差信号的传播。遗忘门让网络能够主动抑制过时信息。GRU提供了另一种相对简洁的门控结构。编码器—解码器又将循环模型应用于序列转换任务。但是,新的问题并没有结束。

循环网络有一项天然的计算依赖

考虑:ht=f(ht−1,xt)h_t=f(h_{t-1},x_t)如果想得到:h100h_{100}通常必须先计算:h99h_{99}而:h99h_{99}又依赖:h98h_{98}所以,对于同一条序列里的相邻时间步,普通循环计算存在顺序依赖。这意味着:即使我们拥有很多GPU计算核心,也不能简单地将同一层全部时间步的递推状态更新完全独立地同时计算。当然,不同样本之间仍然可以进行批量并行。不同矩阵运算也可以利用硬件加速。但跨时间的递推依赖,确实可能限制序列维度上的并行效率。

2017年:Attention Is All You Need

2017年,一组研究者发表:Attention Is All You Need《注意力就是你所需要的一切》。作者包括:Ashish Vaswani。Noam Shazeer。Niki Parmar。Jakob Uszkoreit。Llion Jones。Aidan N. Gomez。Łukasz Kaiser。Illia Polosukhin。论文提出:Transformer。它使用注意力机制构建序列转换网络,在所提出的架构中,不再依赖传统循环或卷积结构作为序列处理的核心。[Google Research]

为什么Attention能够改变序列处理方式?

在循环网络中,某个早期词语对后面词语的影响,可能需要经过很多次状态更新。而在自注意力结构中,某个位置可以通过注意力权重,直接与序列中其他允许访问的位置建立信息交互。例如:
text
小明把书放在桌上。过了一会儿,他回来取书。
如果我们正在处理:
text
他
注意力机制可以学习将较多权重分配给:
text
小明
这样的相关位置。这里的例子只用于说明注意力权重可以与远距离词语建立直接联系,并不是声称任意训练后的模型在该句中一定得到某个特定权重。

RNN与Transformer的一项结构比较

图58-12:两种不同的序列信息交互方式
循环网络1234历史沿着状态链不断传递与更新。
自注意力1234位置之间可以通过注意力机制建立直接交互。

那么,Transformer在所有方面都更好吗?

不能这样说。Transformer在很多语言、视觉与多模态任务中发展得非常成功。其计算结构也为大量并行训练提供了重要优势。但注意力机制仍然有自己的成本。例如,对于标准稠密自注意力,在序列长度为:TT时,成对位置的注意力计算可能涉及:O(T2)O(T^2)规模的注意力关系。实际内存需求还可以通过不同的实现和优化方式改变。同时,循环网络在某些场景下仍然具有吸引力。例如:实时流式信号。资源有限的边缘设备。需要固定大小状态更新的系统。以及一些特定的时间序列任务。所以,现代算法研究并不是:Transformer出现以后,所有RNN立即失去意义。更准确的理解是:不同序列模型对信息保持、计算并行性、状态规模与访问历史的方式,作出了不同的设计选择。

从这一章可以得到什么更深的认识?

普通RNN把历史压缩进不断更新的隐藏状态。LSTM通过门控机制,尝试更好地管理长期状态。GRU提供了另一种状态更新结构。而注意力机制则允许模型在处理当前位置时,更直接地访问相关历史位置的表示。这代表了一次重要的思想转变。从:历史信息必须经过多少次状态传递,才能抵达当前?转向:当前计算究竟应该直接关注历史中的哪些位置?这正是下一章将要讲述的故事。

尾声:真正困难的,不是记住一个数字,而是知道什么时候应该记住它

1986年。Jordan研究能够处理连续行为的连接主义机器。他希望网络不只计算孤立的输入,还能够让内部状态参与后续行为的产生。1989年。Williams与Zipser研究连续运行循环网络的在线学习。他们试图在系统不断接收输入的同时,持续维护训练所需要的敏感度信息。1990年。Elman发表《Finding Structure in Time》。他将隐藏状态反馈到下一时刻的计算,让网络能够通过序列预测学习某些时间结构。同一年,Werbos发表系统性的BPTT论文。循环连接可以沿时间展开。然后,误差就能够根据链式法则,一步步返回过去。但新的困难很快暴露出来。如果误差需要跨越几十步、几百步,甚至更多时间步,它可能越来越小。或者越来越大。梯度消失与梯度爆炸,使普通循环网络很难可靠地学习某些长期依赖。1991年,Hochreiter研究这些长期梯度问题。1994年,Bengio、Simard与Frasconi进一步揭示了稳定存储、抗噪声与梯度学习之间的矛盾。1997年,Hochreiter与Schmidhuber提出LSTM。他们没有仅仅要求传统RNN使用更复杂的优化器。而是重新设计了内部记忆状态,使重要信息能够拥有更加直接的传播路径。2000年,Gers、Schmidhuber与Cummins又加入了自适应遗忘门。因为一项可以长期运行的记忆系统,不能只知道如何保存。它还需要学习如何释放不再有用的信息。到了2014年,GRU和序列到序列学习研究,进一步将门控循环网络应用到语言表示和机器翻译中。此后,注意力机制与Transformer,又提供了不同于传统循环状态传递的信息访问方式。这段历史并不是一群科学家反复改造同一个公式的简单故事。它是一系列不同研究领域共同面对的问题。怎样让过去的信息,对未来仍然有意义?

如果只能记住本章的五个公式

第一,普通RNN的状态更新ht=tanh⁡(Whht−1+Wxxt+b)\boxed{ h_t=\tanh(W_hh_{t-1}+W_xx_t+b) }让当前状态同时依赖新输入与过去状态。
第二,BPTT沿时间累加共享参数梯度∂L∂θ=∑t=1T∂L∂θ∣第t步直接参数贡献\boxed{ \frac{\partial L}{\partial\theta} = \sum_{t=1}^{T} \left. \frac{\partial L}{\partial\theta} \right|_{\text{第t步直接参数贡献}} }这里的每一步贡献,已经包含通过相应后续时间状态传回该步的损失梯度。
第三,长时间梯度的连乘结构∂hT∂hs=∏t=s+1T∂ht∂ht−1\boxed{ \frac{\partial h_T}{\partial h_s} = \prod_{t=s+1}^{T} \frac{\partial h_t}{\partial h_{t-1}} }标量状态时可写成普通乘积;向量状态时需要使用按正确顺序排列的雅可比矩阵乘积。
第四,现代LSTM的记忆状态更新ct=ft⊙ct−1+it⊙gt\boxed{ c_t=f_t\odot c_{t-1}+i_t\odot g_t }遗忘门控制旧信息保留,输入门控制新信息写入。
第五,现代LSTM的隐藏输出ht=ot⊙tanh⁡(ct)\boxed{ h_t=o_t\odot\tanh(c_t) }输出门控制当前记忆怎样成为对外使用的隐藏表示。
这五个公式共同揭示了:普通RNN为什么能够表示历史。BPTT为什么能够训练时间依赖。梯度连乘为什么造成长期训练困难。以及LSTM怎样通过门控记忆状态,改善一些长期依赖问题。但我们同样看到:即使拥有LSTM,也不意味着机器能够无限记住过去。因为它仍然拥有有限的状态维度。门控也需要学习。过长的序列仍然可能带来训练困难。不同任务还可能需要不同的信息访问方式。因此,本章真正的结论不是:LSTM终于让机器拥有了永不遗忘的完美记忆。而是:LSTM让神经网络获得了一种更加有效、可以训练的长期信息保存与选择性遗忘机制。

第六篇的前三章,形成了怎样的算法体系?

《会学习的算法》· 第56—58章
第56章
沿着山坡寻找最低点:梯度下降当我们知道目标函数的梯度,怎样选择参数更新方向和步长?

第57章
错误怎样穿越一层层神经元:反向传播当计算由大量复合运算组成,怎样高效计算全部参数的梯度?

第58章
让机器记住昨天:循环网络与长短期记忆当当前判断依赖过去输入,怎样保存状态、跨时间训练,并减少长期梯度衰减?
第56章研究:参数怎样改。第57章研究:梯度怎样算。第58章研究:当计算具有时间依赖时,信息与梯度怎样跨越过去。三章结合起来,我们已经能够理解现代很多序列学习系统背后的数学基础。但是,我们也逐渐发现了一项新的限制。如果某个重要信息位于一千个词以前。我们一定要把它不断传过一千个隐藏状态,才能在当前使用吗?有没有更直接的办法?这就成为下一章的起点。

历史档案:循环网络、BPTT与LSTM的发展

年份研究者与成果历史意义
1986年Michael I. Jordan,序列连接主义机器研究使用内部状态处理具有时间结构的行为
1989年Ronald Williams与David Zipser,连续运行RNN学习实时循环学习及在线参数敏感度计算
1990年3月Jeffrey Elman,Finding Structure in Time简单循环网络学习时间与语言序列结构
1990年10月Paul Werbos,BPTT方法论文系统讨论沿时间反向传播的计算方法
1991年Sepp Hochreiter,动态神经网络学位论文长期梯度衰减问题的重要早期分析
1994年3月Bengio、Simard、Frasconi,长期依赖论文分析梯度训练长期依赖的数学困难
1997年11月15日Hochreiter与Schmidhuber,LSTM提出CEC、输入门及输出门等关键机制
1997年11月Schuster与Paliwal,双向RNN同时利用序列过去与后面的上下文
2000年10月Gers、Schmidhuber、Cummins,遗忘门改善LSTM对持续数据流的状态管理
2014年6月Cho等,RNN Encoder–Decoder发展包括GRU在内的门控序列建模
2014年9月Sutskever、Vinyals、Le,Seq2Seq使用LSTM研究端到端序列转换
2014年9月Bahdanau、Cho、Bengio,注意力翻译预印本为编码器—解码器增加可学习的软对齐与信息选择
2017年Vaswani等,Transformer以注意力为核心构建不依赖循环的序列模型

主要参考文献与历史依据

1986年 · Jordan的序列网络研究Attractor Dynamics and Parallelism in a Connectionist Sequential Machine加州大学电子学术资料库收录的1986年研究报告,用于核查Elman之前的循环序列模型研究背景。

1989年 · Williams–Zipser实时循环学习A Learning Algorithm for Continually Running Fully Recurrent Neural Networks《Neural Computation》第1卷第2期,第270—280页。在线敏感度传播与RTRL历史的重要原始论文。

1990年 · Elman的时间结构研究Finding Structure in TimeJeffrey Elman本人的论文目录《Cognitive Science》第14卷第2期,第179—211页。研究隐藏状态反馈、序列预测及语言结构学习。

1990年 · Werbos的BPTT论文Backpropagation Through Time: What It Does and How to Do It《Proceedings of the IEEE》第78卷第10期,第1550—1560页。重点解释BPTT如何作用于动态系统与循环网络。

1994年 · 长期依赖训练的困难Bengio、Simard、Frasconi — Learning Long-Term Dependencies with Gradient Descent Is Difficult《IEEE Transactions on Neural Networks》第5卷第2期,第157—166页。用于核查长期状态保存、稳定性与梯度训练之间的矛盾。

1997年 · LSTM原始论文Hochreiter & Schmidhuber — Long Short-Term Memory《Neural Computation》第9卷第8期,第1735—1780页。重点查阅CEC、输入门、输出门及原始人工长时滞实验。此时尚未加入现代常见的独立遗忘门。

1997年 · 双向循环网络Schuster & Paliwal — Bidirectional Recurrent Neural Networks《IEEE Transactions on Signal Processing》第45卷第11期,第2673—2681页。解释利用两个时间方向的信息进行序列处理。

2000年 · LSTM自适应遗忘门Gers、Schmidhuber、Cummins — Learning to Forget: Continual Prediction with LSTM《Neural Computation》第12卷第10期,第2451—2471页。解释为什么持续运行的LSTM需要学习重置和遗忘。

2014年 · GRU与RNN编码器—解码器Cho等 — Learning Phrase Representations Using RNN Encoder–Decoder for Statistical Machine TranslationEMNLP 2014,第1724—1734页。正式会议档案,包含论文作者、时间和原始研究内容。

2014年 · 使用LSTM进行序列转换Sutskever、Vinyals、Le — Sequence to Sequence Learning with Neural NetworksNeurIPS 2014。研究以LSTM编码器与解码器进行端到端序列学习及机器翻译。

2017年 · TransformerVaswani等 — Attention Is All You NeedNeurIPS 2017。为理解下一章的注意力机制及之后的Transformer研究提供历史衔接。
**史实与教学重构说明:**本章涉及1986年Jordan研究、1989年Williams–Zipser、1990年Elman与Werbos、1994年Bengio等人的长期依赖研究、1997年LSTM、2000年遗忘门,以及2014年门控循环网络和序列转换的历史描述,均依据可核查的研究文献。开篇语言预测场景属于教学重构,并非Elman论文中某次被逐字记录的实验过程。互动实验采用线性RNN与固定门控状态路径,用于精确展示指数衰减或放大的数学机制,不代表训练完成的真实模型表现。Python实战则进一步实现了普通RNN与标量现代LSTM的完整BPTT,并通过有限差分验证梯度。相关代码属于现代教学重建,不应与1997年原始LSTM网络结构或实验参数混为一谈。
下一章预告 · 第六篇 第59章

《不必记住全部,只需找到重要的部分:注意力机制的诞生》

2014年9月1日。一篇新的研究预印本出现在arXiv上。作者是:德兹米特里·巴赫达瑙(Dzmitry Bahdanau)。Kyunghyun Cho。Yoshua Bengio。论文题为:Neural Machine Translation by Jointly Learning to Align and Translate《通过联合学习对齐与翻译实现神经机器翻译》。该研究随后发表于2015年的ICLR会议。 [ScienceOpen][APICe]它提出了一项关键问题。如果编码器已经读取了一整句话,解码器为什么一定要只依赖一个固定长度的全句表示?当模型正在翻译某个特定词语时,能否直接查看源语言中最相关的几个位置?例如:
text
The little cat is sleeping.
如果模型正在生成:
text
猫
那么,它是否可以将更多计算注意力分配给:
text
cat
而在生成其他词语时,再改变关注的位置?这项思想将循环网络的序列建模,引向另一种信息选择方式。Attention Mechanism(注意力机制)。与其要求模型把全部历史压缩成一个随时间不断更新的状态,不如让它在当前需要的时候,有选择地访问相关位置的表示。后来,2017年的Transformer又进一步使注意力成为序列网络的核心计算结构。下一章,我们将从早期神经注意力与机器翻译研究讲起,严格区分2014年的软对齐注意力与2017年的Transformer,不把整个注意力思想误写成某一篇论文突然独立发明的成果。我们将亲手计算:softmax⁡(QK⊤/dk)V\boxed{ \operatorname{softmax}(QK^\top/\sqrt{d_k})V }并制作可操作的词语对齐热力图,让你修改Query、Key和Value,观察不同词语怎样获得不同的注意力权重。第58章,我们学习怎样让机器保存过去的信息;第59章,我们将研究,机器怎样从已经存在的信息中,主动找到当前最重要的部分。
——《算法的故事》第二季 · 第六篇《会学习的算法》 · 第58章 完