文章目录

前言

循环神经网络(Recurrent Neural Network, RNN)同样是一类重要的神经网络模型。RNN多用来处理时序数据,时序数据指的是按照时间循序排列起来的数据,其主要特点在于,每个时间点上的元素都不是孤立的,而是与前后项相互联系的,元素的顺序本身就含有信息。下面简单地介绍RNN的通俗解释和适用的任务类型。

(1)通俗解释:RNN是一种能够进行“循环”操作的网络结构,这里的“循环”指的是对于一个时序数据中的每个元素,都要进入网络进行重复操作。每一次的输出结果都与上一次的输出有关,并且还要作为下一次循环的输入,以此类推,就构成了一个是时序上连贯的、可以表示时序数据中不同元素的前后联系的模型。形象一点来说,就是RNN可以学习到在某个元素出现后,接下来应该出现什么。

(2)任务类型:相比CNN而言,由于RNN可以提取出前后元素之间的关联信息,因此更适合处理带有上下文的时序数据。列举几个常见的时序数据的场景和任务:首先,自然语言处理(Natural Laguage Processing, NLP)的相关问题,如机器翻译、阅读理解等。在日常生活中,为了明白一句话的意思,我们自然会联系上下文、结合语境。这里的联系上下文就是利用前后项之间的时序信息,可以利用RNN来实现。另外,由于语音信号也是一种时序信号,前后之间关联密切,因此语音处理、语音识别等相关任务也可利用RNN实现。除此之外,诸如股票走势等生活中常见的时序信号,都可以利用RNN进行处理。

RNN的原理和结构

RNN是一种经典的处理时序数据相关任务的网络形式,其基本结构如下图所示。在该图中,输入和输出都标记了时刻$t$,即时序数据中的每个时刻的取值是逐个输入网络的。和本时刻$t$的输入一起被输入网络的还有上一个时刻$t-1$的结果,两者合并后,通过激活函数激活产生本时刻$t$的输出,而本时刻$t$的计算结果同样要和下一个时刻$t+1$时的输入数据共同进入网络,以此类推,形成循环。这也是RNN中“循环”的含义和由来。

将RNN的操作写成数学形式,即:

$$\left \{
\begin{array}{c}
s_t=W_xx_t+W_hh_{t-1} \\
h_t=f(s_t) \\
z_t=W_yh_t \\
y_t=g(z_t)
\end{array}
\right.$$

其中,$W_t$、$W_h$、$W_y$为网络的参数;$x_t$是$t$时刻的输入;$x_t$、$h_t$和$z_t$为中间变量;$f$和$g$分别为输入到隐层和隐层到输出的激活函数。

不同于MLP和CNN,RNN还包含时间维度,因此上如所示的基本结构相较于之前的网络显得不太直观。可以将时间维度展开,每个时刻单独画出来,并标注各个时刻的参数共享,即可将RNN化成类似多层网络结构的直观形式,如下图所示。

 

由上图可以看到,RNN具有如下特点:首先,RNN是一个时序的过程,它并不是一次性将一系列数据输入网络进行训练,而是按照顺序逐个将元素输入网络,并且在输入下一个元素时,把上一个元素经过网络的输出也一并输入进去。通过这样的方式建立起上下文之间的联系,从而使网络捕捉到时序关联的信息。

其次,RNN模型参数也是共享的,这一点和CNN类似。但是,CNN是在空间维度上共享权重,一边提取空间特征,这一特点使其天然适合处理具有空间特征模式,并且这种空间特征还具有重复性的自然图像数据。对于RNN而言,模型参数的共享是针对不同时刻的输入/输出而言的,即RNN是在时间维度上共享了参数,从而可以将时序特征学习到权重当中。

RNN中权重$W_t$、$W_h$、$W_y$在各个时刻是相同的。序列中的元素一般以向量来表示,比如对于NLP处理的问题,序列中每个元素就是单词,而单词往往用词向量来表示。对于序列元素是标量的,可以看作长度为1的向量来处理。

RNN的应用场景

下图所示为从向量序列到向量的RNN模型。在该情况下,RNN的输入是一组时序数据,只输出单一的结果,不具有时序特征。这样的情况一般是等整个序列都进入网络计算后,将得到的输出序列的最后一项作为最终的对应于输入序列的结果。

 

该模型的一个应用是文本的情感分析(Sentiment Analysis)。文本的情感分析指的是对于一个带有书写着主观情感倾向和判断的文本(如对某事的评论),判断和分析其所表达的情感色彩的类型。情感分析的应用非常广泛,在很多场合都需要进行情感分析。例如,对于一场新上映的电影,出品方可能想要了解观众对这部电影的评价,那么就可以通过抓取影评网站上的观众评论文本,对这些评论是正面的还是负面的进行分类,这就是一种情感分析。另外,在网络平台的评论区里有时会出现侮辱谩骂、人身攻击等内容,利用该方法可以对此进行甄别,从而减少这些评论的出现。除此之外,情感分析还可以对公众意见进行分析、对市场前景和趋势进行预测等,应用十分广泛。

情感分析的场景实际上是一个时序数据的分类问题,因此和分类问题相同,网络的输出是以向量形式表示的各类别的归属概率。因此,并不需要输出一个序列,只需要得到最终的概率向量即可。

另一种情况如下图所示,该图表示从向量序列到向量序列(两者同步)的RNN模型。在这种情况下,循环过程中每个时刻的输出都被作为输出序列的一个元素,最终组成了整个输出序列。

 

上图所示的RNN结构中,输入与输出是具有同步对应关系的序列。这样的结构可以应用于输入一个数据流,然后输出另一种形式的数据流的场合。例如,对于一部电影,需要对每个时刻的内容进行分类,如识别出是人物还是场景等。这样最终就可以得到一个类别序列,分别对应于电影中每一帧的分类结果。

从序列到序列还有另一种实现方法,如下如所示,该结构也是从向量序列到向量序列,但是是通过先编码、后解码的方式实现的。这样的结构实际上可以看作两个RNN的组合:前面的一个是输入为一个序列,输出为一个向量;后面的RNN则将前面输出得到的向量作为输入,然后输出另一个向量。

 

实际上,在自然语言处理任务中,上图所示的模型框架通常被称为编解码(Encoder-Decoder)模型。该模型框架是一种Seq2Seq(Sequence to Sequence)方法,即给定一个序列,根据某种要求生成另一个对应的序列。

该模型的应用很广,应为很多问题都可以被归结为由序列生成序列。例如,最常见的机器翻译就属于这类问题,给定一句以A语种表达的句子,通过Seq2Seq的方法,得到同一内容用B语种的表达。以中译英为例,如果输入为中文“真相只有一个”,那么输出则为英文“One Truth Prevails”。利用编解码模型,其过程如下图所示。

 

如上图所示,在输入和输出的末尾都有一个EOS(End Of Sentens,句末标识),表示这一句话到此结束。之所以需要EOS,是因为在机器翻译中输入语句的长度和输出语句的长度不一定相同,因此需要EOS判断是否已经将输入读取完毕,以及判断翻译结果是否已经完全输出。中文句子进入编码器后,编码器将其编码为一个用来表征这句话所含语义的向量,然后通过解码器将该向量翻译成另一种语言的语句。

出了机器翻译意外,问答聊天系统也是一个该模型的应用场景,及针对某个问题,训练RNN进行回答。例如,输入“法国的首都在哪里?”,网络输出“巴黎”,这就是一个Seq2Seq的例子。类似的还有对对联、对诗等模型,也可以用该方法来处理。

还有一个应用是文本摘要。对于一个较长的文本,其将整理成字数较少、言简意赅的摘要形式,这也是一个Seq2Seq的例子,因此也可以用编解码模型来完成。

RNN的实际应用中还有一种结构,即从向量到向量序列,如下图所示的RNN中只有一个向量作为输入,然后通过循环操作,生成一个输出序列。

 

这种结构的一个有趣的应用叫做图像标注(Image Caption),通俗来说就是看图说话。此时编解码模型的后半部分(解码器)可以看作一个上图所示的向量到向量序列的结构,只不过与图像标注以图像特征向量为输入不同,解码器的输入向量是由编码器RNN部分学习并编码的语义向量。例如,下图图像中的内容是一只猫坐在草地上,那么,可以利用CNN对该图像提取特征向量作为RNN的输入,循环输出语句序列:“一”“只”“猫”“在”“草”“地”“上”“EOS”。

 

RNN的训练方法

RNN的训练方法与普通的神经网络在形式上有所不同,但是其原理是一样的,即通过误差对参数求梯度,然后反向传播,对参数进行更新。RNN的训练方法通常被称为BPTT(Back Propagation Through Time),表示反向传播是在时间维度上进行的。

下面计算RNN对参数的梯度,首先回顾RNN的数学形式,为了简便计算,用标量来进行计算,如下式:

$$\left \{
\begin{array}{c}
s_t=W_xx_t+W_hh_{t-1} \\
h_t=f(s_t) \\
z_t=W_yh_t \\
y_t=g(z_t)
\end{array}
\right.$$

记$t$时刻的标签为$d_t$,$t$时刻的误差记为:$$e_t=loss(y_t-d_t)$$

这里不具体写出损失函数的形式,loss可以实分类问题中的交叉熵函数,也可以是回归问题中的$l_1$范数或是$l_2$范数等。由此可以将时刻$t$的误差对参数$w_x$、$w_h$及$w_y$进行求梯度计算,以便完成误差的反向传播。首先计算$t$时刻的误差$e_t$对$w_y$的梯度:$$\frac{\partial e_t}{\partial w_y} = \frac{\text{d}e_t}{\text{d}y_t}\frac{\text{d}y_t}{\text{d}z_t}\frac{\text{d}z_t}{\text{d}w_y}$$

由于$h_t$与$w_y$无关,因此最后一项可以直接用$h_t$表示,而前两项分别是损失函数的倒数和激活函数$g$的导数。以上的到了$w_y$的更新方法,与之前的普通神经网络没有区别。

然后计算$e_t$对$w_x$的导数,用类似的方法,可以得到:$$\frac{\partial e_t}{\partial w_x} = \frac{\text{d}e_t}{\text{d}y_t}\frac{\text{d}y_t}{\text{d}z_t}\frac{\text{d}z_t}{\text{d}h_t}\frac{\text{d}h_t}{\text{d}s_t}\frac{\text{d}s_t}{\text{d}w_x}$$

前面两项仍然是损失函数及激活函数$g$的导数,$z_t$对$h_t$的导数为$w_y$,前面已经更新过了,因此可以作为已知数。最后两项是激活函数$f$的导数和输入$x_t$。对于$w_x$的梯度计算和更新,仍与普通神经网络一样。

最后要计算对$w_h$的梯度。注意,该参数的梯度计算和之前的两个有所不同,如下式所示:$$\frac{\partial e_t}{\partial w_x} = \frac{\text{d}e_t}{\text{d}y_t}\frac{\text{d}y_t}{\text{d}z_t}\frac{\text{d}z_t}{\text{d}h_t}\frac{\text{d}h_t}{\text{d}w_h}$$

前面几项与更新$w_x$时一样,但是这里要注意,在最后一项中,$\frac{\partial h_t}{\partial w_h}$不能直接写成$h_{t-1}$,应为在上一个时刻$t-1$时,$h_{t-1}$的计算也与$w_h$有关,即:$$h_{t-1}=f(w_xx_{t-1}+w_hh_{t-2})$$

同理,$t-2$时的$h_{t-2}$也和$w_h$有关,以此类推,将变量之间的依赖关系画出来,如下图所示(途中忽略了$w_x$和$x_t$)。

 

可以看到,每个时刻的$h_t$不仅直接与$w_h$有关,而且还因为与上一时刻的$h_{t-1}$有关,导致间接与$w_h$有关。根据上图所示,利用链式法则,可以得到:

\begin{aligned}
\frac{\partial h_t}{\partial w_h}& = \frac{\partial h_t}{\partial s_t}h_{t-1}+\frac{\partial h_t}{\partial h_{t-1}}\frac{\partial h_{t-1}}{\partial w_h} \\
& = \frac{\partial h_t}{\partial s_t}h_{t-1}+\frac{\partial h_t}{\partial h_{t-1}}(\frac{\partial h_{t-1}}{\partial s_{t-1}}h_{t-2}+\frac{\partial h_{t-1}}{\partial h_{t-2}}\frac{\partial h_{t-2}}{\partial w_h})  \\
& = \cdots
\end{aligned}

括号内的$\frac{\partial h_{t-2}}{\partial w_h}$还可以继续回溯,知道最开始的时刻为止。可以将上式化简为:$$\frac{\partial h_t}{\partial w_h}=\sum_{k=1}^t\frac{\partial h_t}{\partial h_k}\frac{\text{d}h_k}{\text{d}s_k}h_{k-1}$$

由此可以发现,对于某个时刻$t$的参数更新,需要之前每一个时刻的梯度都参与运算,这就是训练RNN时的BPTT算法。如果观察展开后的RNN结构,并且考虑导参数共享,可以发现BPTT算法实际上就是BP算法在这种特殊网络结构中的应用形式。