Lecture10 循环神经网络(RNN)

1. RNN介绍

1.1 模型分类

        循环神经网络 Recurrent Neural Network (RNN)的一大优点是为网络结构的搭建提供了很大的灵活性。通常情况下,我们提及的神经网络一般有一个固定的输入,然后经过一些隐藏层的处理,得到一个固定大小的输出向量(如下图左所示,其中红色表示输入,绿色表示隐藏层,蓝色表示输出,下同)。这种“原始”的神经网络接受一个输入,并产生一个输出,但是有些任务需要产生多个输出,即一对多的模型(如下图 one-to-many标签所示)。循环神经网络使得我们可以输入一个序列,或者输出一个序列,或者同时输入和输出一个序列。下面按照输入输出是否为一个序列对RNN进行划分,并给出每种模型的一个应用场景:

  • 一对一模型 one-to-one,最原始的模型,略过。
  • 一对多模型 one-to-many,比如说给图片添加字幕,即给出一张固定大小的图片,然后生成一个单词序列描述图片的内容。
  • 多对一模型 many-to-one,比如说动作预测任务。输入一串视频帧的序列,然后生成一个标签代表这个视频中发生了什么动作。另外一个多对一任务的例子是NLP领域的情感分类任务,给出一个句子的单词串,然后判断这个句子的情感类别。
  • 多对多模型 many-tomany,比如说给视频添加字幕,输入的是一串视频帧的序列,生成的是描述视频内容的字幕。另外一个例子是NLP领域的机器翻译任务,比如说将一串英文单词组成的序列翻译成法语单词组成的序列。
  • 此外,还有一种多对多模型的变种,这种变种模型会在每个时间节点都生成一个输出,一个例子是视频帧级别的视频分类任务,即对视频的每一帧都进行分类,并且模型预测的标准并不只依靠当前帧的内容,而是在这个视频中此帧之前的所有内容。

 

        相较于那些从一开始连计算步骤的都定下的固定网络,序列体制的操作要强大得多。并且对于那些和我们一样希望构建一个更加智能的系统的人来说,这样的网络也更有吸引力。我们后面还会看到,RNN将其输入向量、状态向量和一个固定(可学习的)函数结合起来生成一个新的状态向量。在程序的语境中,这可以理解为运行一个具有某些输入和内部变量的固定程序。从这个角度看,RNN本质上就是在描述程序。实际上RNN是具备图灵完备性的,只要有合适的权重,它们可以模拟任意的程序。然而就像神经网络的通用近似理论一样,你不用过于关注其中细节。实际上,我建议你忘了我刚才说过的话。

        如果训练普通神经网络是对函数做最优化,那么训练循环网络就是针对程序做最优化。

        无序列也能进行序列化处理

        你可能会想,将序列作为输入或输出的情况是相对少见的,但是需要认识到的重要一点是:即使输入或输出是固定尺寸的向量,依然可以使用这个强大的形式体系以序列化的方式对它们进行处理。

        即使数据不是序列的形式,仍然可以构建并训练出能够进行序列化处理数据的强大模型。换句话说,你是要让模型学习到一个处理固定尺寸数据的分阶段程序。

1.2 卷积网络的不足

        卷积神经网络不能很好地处理输入和输出是可变序列的任务。比如说给视频添加字幕,输入是一个可变的视频帧(比如说可能是10分钟的也可能是10小时的视频),而输出也是不定长度的字幕。而卷积神经网络只能接收固定长宽大小的输入,并且不能很好地泛化到不同大小的输入。为了解决这一问题,我们需要学习循环神经网络RNN。

1.3 RNN概念

        RNN基本上就是一个黑盒(如下图左所示),当逐一处理一个输入的序列时,黑盒中的“内部状态”也会随之更新。在每个时间段,我们向RNN输入一个向量,RNN将这个内部状态(上一时间段的状态)和输入的向量一起作为接收到的输入。当我们调整RNN的权重时,RNN在接受同样的输入后,会对内部状态产生不一样的改变。同时,我们也需要模型基于这个内部状态产生一个输出。因此,我们可以在RNN的顶端产生这些输出(如下图顶部所示)。

        如果我们将RNN模型展开(如下图右所示),那么每一个时间段的输入对应的是输入序列的每个元素(比如视频序列的每一帧),记作x1,x2,...,xt 。RNN在每个时间段会得到两个输入,一个来自输入序列的元素 xi ,另外一个就是对之前信息的一种表示形式(即对历史信息的记录,也就是上一段提到的内部信息)。然后,RNN会生成一个输出 yi ,同时更新这个历史信息,RNN会进行很多次这样的前向传播。下图右边所示的每个RNN块都是同一个RNN网络,他们拥有同样的参数,但是在每个时间段会接受不同的输入和历史信息。

        更加精确地说,RNN可以表示成一个循环的公式,公式由一些包含参数 W 的函数 fw 组成:

        即,每个时间段它接收一些先前的状态 ht-1,这里我们用向量 ht-1 表示第 t-1 个时间段的信息。同时接收此时的输入 xt,用两个输入共同生成当前状态 ht 。一个固定的函数 fw 被用于每一个时间段,这也允许我们使用在序列上使用RNN模型,并且不用预先给出输入序列的大小。因为,我们在每个单独的时间段都使用一个相同的函数,所以输入和输出的长度也就无关紧要了。

        最简单的RNN形式,也称为原始RNN(Vanilla RNN), 网络中只有一个单一的隐藏状态 h ,在网络中我们使用一个循环的公式来更新隐藏状态 h ,并作为一个函数接受先前的隐藏状态 ht-1 和当前时间段的输入 xt 。特别的,我们使用权重矩阵 Whh 和 Wxh对 ht-1 和 xt 进行投影,同时将结果相加后使用 tanh 进行放缩。也就是说,这样一个循环的公式表示,在 t 时刻,状态 h 是由一个包含 ht-1 和 xt 作为输入的函数进行更新的,公式如下:

         我们可以依据状态 ht 来进行预测,这是构建一个神经网络最简单的形式:

        也就是说,RNN可以抽象成一个简单的API:它接受输入,然后返回输出,不过输出不仅受输入影响,还会受到历史输入的影响,这个历史输入以隐藏状态 h 的形式存储。并且,如果将RNN抽象成一个类,那么每次输出都只经历一个 step:

                class RNN:
                    def step(self, x):
                            # 更新隐藏状态h
                            self.h = np.tanh(np.dot(self.W_hh, self.h) + np.dot(self.W_xh, x))
                            # 计算输出向量y
                            y = np.dot(self.W_hy, self.h)
                            return y
 
                    rnn = RNN()
                    y = rnn.step(x) # x是输入向量,y是输出向量

2. RNN作为字符级的语言模型

        使用RNN的最简单的方式之一是字符级语言模型,因为它是很直观的。在这种方式中,RNN的工作方式是接收一串字母,在每一个时间段,我们都会要求RNN去预测序列中可能出现的下一个字母是什么。RNN的预测结果会以一个得分分布的形式给出,代表了RNN认为在字母表中的每个字母在接下来出现的可能性。

        所以假设有一个非常简单的例子(如下图所示),我们有字母表 v ∈ {"h","e","l","o"},然后利用训练序列“hello”训练RNN。该训练序列实际上是由4个训练样本组成:1.当h为上文时,下文字母选择的概率应该是e最高。2.l应该是he的下文。3.l应该是hel文本的下文。4.o应该是hell文本的下文。

        具体来说,我们会用独热编码的方法给每个字母编码,然后利用step方法一次一个地将其输入给RNN。随后将观察到4维向量的序列(一个字母一个维度)。我们将这些输出向量理解为RNN关于序列下一个字母预测的信心程度。下面是流程图:

         具体来说,我们每次向RNN中输入一个字母,首先是 "h" ,然后是 "e",然后 "l",最后是 "l"。每个字母使用独热编码(one-hot)来表示 ,如下图所示:

 然后,我们在每一时间段中使用上文提到的迭代的公式,并假设我们初始的状态 h 是一个全0的3维向量。通过这一个不变的迭代公式,最终我们也会得到一个三维的向量表示下一个隐藏状态 h ,如下图所示:

        当我们在每一时间段都使用这样一个迭代,我们就可以在每一时间段都预测下一个可能出现的字母。由于在字母表中有4个字母,所以我们预测的结果也是一个4维的向量。在最开始,我们输入字母 "h" ,RNN依据当时的权值计算出一个向量,表示每个字母出现的可能性:

        即,RNN认为下一个字母时 "h" 的可能性为 1.0 ,认为是 "e" 的可能性为 2.2 ,认为是 "l" 的可能性为 -3.0 ,认为是 "o" 的可能性为 4.1,因此RNN预测下一个字母应该是 "o",当然我们知道 "h" 的下一个字母应该是 "e" ,所以实际上,2.2才是正确答案对应的得分。所以我们希望正确答案对应的得分尽可能的高,而其他的得分尽可能的低。类似的,在每一步都有一个目标字母,我们希望算法分配给该字母的置信度应该更大。因为RNN包含的整个操作都是可微分的,所以我们可以通过对算法进行反向传播(微积分中链式法则的递归使用)来求得权重调整的正确方向,在正确方向上可以提升正确目标字母的得分。然后进行参数更新,即在该方向上轻微移动权重。如果我们将同样的数据输入给RNN,在参数更新后将会发现正确字母的得分(比如第一步中的e)将会变高(例如从2.2变成2.3),不正确字母的得分将会降低。重复进行一个过程很多次直到网络收敛,其预测与训练数据连贯一致,总是能正确预测下一个字母。

        跟技术一点的解释是,我们选用比如softmax分类器(交叉熵损失)等一类的损失函数,使用小批量的随机梯度下降来训练RNN,将损失值从最后一个时刻往前反向传播回去,以此来计算在参数矩阵上的梯度,并使用RMSProp或Adam来让参数稳定更新,使得我们可以改变矩阵让RNN预测的正确答案的概率最大。

        注意当字母l第一次输入时,目标字母是l,但第二次的目标是o。因此RNN不能只靠当前的输入数据,必须使用它的循环连接来保持对上下文的跟踪,以此来完成任务。在测试时,我们向RNN输入一个字母,得到其预测下一个字母的得分分布。我们根据这个分布取出得分最大的字母,然后将其输入给RNN以得到下一个字母。重复这个过程,我们就得到了文本。

3. 多层RNN

        目前为止,我们只展现了一层的RNN,实际上我们并没有被限制只能使用单层的架构。今天,RNN的使用方式之一是以更复杂的方式。RNN可以被堆叠在一起组成多层的结构,以此达到更深的深度,而根据经验,更深的架构往往效果更好。如下图,就是使用了三个不同的RNN,每个RNN使用一组独立的权重。三个RNN依次堆叠在另一个的上面,所以,第二个RNN的输入就是第一个RNN的隐藏状态,所有堆叠的RNN被这样联合在一起训练。

         同样,我们可以把这个更深的网络抽象成上文提到的RNN类,那么每次输出就变成了(以两层网络为例):

                y1 = rnn1.step(x)
                y = rnn2.step(y1)

        换句话说,我们分别有两个RNN:一个RNN接受输入向量,第二个RNN以第一个RNN的输出作为其输入。其实就RNN本身来说,它们并不在乎谁是谁的输入:都是向量的进进出出,都是在反向传播时梯度通过每个模型。

4. 长短期记忆LSTM

        到此为止,我们已经介绍了一个用于原始RNN的简单递推公式。实际运用中,我们实际上很少使用这种原始RNN的公式,作为替换,我们会使用长短期记忆RNN,即 Long-Short Term Memory (LSTM) RNN。

4.1 原始RNN的梯度溢出和梯度消失

        一个RNN区块接收输入 xt 以及先前的隐藏信息 ht-1 并学习如何翻译,通过 tanh 来生成当前的隐藏信息 ht 和输出 yt ,公式如下:

        对于反向传播,我们来检查最后一个时间段的输出如何影响最早时间段时的权重,ht对于ht-1  的偏导数可以写作: ,然后我们获取 t 时刻的损失值对于权重 Whh 的偏导数:

  • 梯度消失: 我们可以从  中看出,这一项的值始终小于1,因为 tanh的倒数值域小于1。因此,随着 t 的变大(即经过更多的时间段),梯度值也会随之变小直至接近0。这就导致了梯度消失(梯度弥散)问题,即处于后面时间段的梯度几乎对前面时间段的梯度无法造成影响(反向传播会将本地梯度和上游梯度相乘),当我们对长序列的输入进行建模时,这是有问题的,因为更新会非常慢。 

  • 移除非线性(tanh):上面的梯度消失问题貌似是由于tanh导致的,那么如果我们将tanh移除,是否就能解决这个问题呢?首先原先计算的梯度公式会变成:,这就会引来以下问题:

    • 梯度爆炸:如果有一个 Whh 的值是大于1的,那么梯度就会越来越大,直到出现NaNs错误。
    • 梯度消失:是的,移除非线性无法避免梯度消失。当所有的 Whh 值小于1,那么梯度就会越来越小,越来越接近0。

        实践中,我们可以通过梯度裁剪的方式避免梯度爆炸问题,即限制大的梯度小于一个最大的门限值。但是梯度消失的问题仍然无法解决,所以LSTM就被设计出来用以解决这个问题。

4.2 LSTM表达式

        接下来是对LSTM的精确表示。在第 t 步时,此时有隐藏状态 ht ,细胞状态(cell state)ct  ,这两者都是大小为  的向量。一个LSTM和RNN的区别是,它含有一个额外的细胞状态 ct ,直觉上来说,ct 可以看成存储了长期的信息。LSTM可以读取、擦除、写入信息到细胞状态 ct 中。LSTM修改 ct 的方法是通过三个特殊的门(gate):i,f,o ,分别对应着输入门、遗忘门、输出门。这三个门的取值在0到1之间,其中0表示关闭,1表示开启。三个门都是 n 维的向量。

        在每一个时间段,我们都拥有一个输入向量 xt ,一个先前的隐藏状态ht-1  ,一个先前的细胞状态 ct-1 ,LSTM通过下述式子计算下一个隐藏状态 ht ,下一个细胞状态ct :

 

 其中 ⊙ 表示哈达玛积 Hadamard Product,gt 是一个中间变量用于暂存计算结果。

        哈达玛积举例:

 

        f,i,o的值都是在0到1之间,因为它们都被使用sigmoid函数 σ 进行放缩,当进行逐元素的乘法运算时,我们可以了解到:

  • 遗忘门: 控制有多少来自 ct-1 的信息该被移除。遗忘门用于学习删除来自早先时间节点的隐藏信息,这也是为什么LSTM有两个隐藏信息 ht 和 ct ,因为 ct 将会被一直传播下去,并且学习是否忘记部分来自先前细胞的状态。
  • 输入门: 控制有多少来自 ht-1 和 xt 的信息应该被添加到 ct 中。 输入门使用的是sigmoid函数,可以将输入的值缩放到0到1之间。如果其值要么几乎总是0,或者要么总是1时,输入门就可以看成一个开关。它决定了是否将RNN的输出结果gt 添加到 ct 中。
  • 输出门: 控制有多少来自 ht 的信息需要被作为输出值展示。

        LSTM的关键思想是细胞状态(cell state),即贯穿于循环时间段之间的水平线。你可以把细胞状态想象成某种信息的高速公路,沿着整个链条在时间上笔直地流过,期间只有一些微小的线性互动。有了上面的公式表达,信息很容易就沿着这条高速公路流动(如下图加粗的横线)。因此,即使有一堆LSTM堆积在一起,我们也可以得到一个不间断的梯度流,梯度通过细胞状态而不是隐藏状态 h 回流,因此在时间段中梯度不会消失。

        这极大地解决了我们上面概述的梯度消失/爆炸的问题。下图还显示,梯度包含一个 "遗忘 "门的激活矢量。这允许通过使用 "遗忘 "门的适当参数更新来更好地控制梯度值。

 

4.3 LSTM是否解决了梯度消失问题

        LSTM结构使RNN更容易在许多循环时间段中保存信息。例如,如果遗忘门被设置为1,而输入门被设置为0,那么细胞状态的信息将在许多循环时间步骤中始终被保留。相比之下,对于原始的RNN来说,仅仅利用单一的权重矩阵,就很难在循环时间步骤中保留隐藏状态的信息。不过LSTM不能保证没有消失/爆炸的梯度问题,但它确实为模型学习序列中长距离的依赖关系提供了一种更容易的方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值