标准的RNN模块结构
如果说从DNN到CNN的技术演进是为了面向图像数据解决提取空间依赖特征的问题,那么RNN的出现则是为了应对序列数据建模,提取时间依赖特征(这里的"时间"不一定要求具有确切的时间信息,仅用于强调数据的先后性)。
延续前文的行文思路,本文仍然从以下四个方面加以介绍:
- 什么是RNN
- RNN为何有效
- RNN的适用场景
- 在PyTorch中的使用
01 什么是RNN
循环神经网络,英文Recurrent Neural Network,简写RNN。显然,这里的"循环"是最具特色的关键词。那么,如何理解"循环"二字呢?这首先要从RNN适用的任务——序列数据建模说起。
RNN适用于序列数据建模,典型的序列数据可以是时间序列数据,例如股票价格、天气预报等;也可以是文本序列数据,比如文本情感分析,语言翻译等。这些数据都有一个共同的特点,那就是输入数据除了具有特征维度外,还有一个先后顺序的维度。以股票数据为例,假设一支股票包含[open, high, low, close]四个维度特征,那么其数据结构的示意图为:
实际上,这就是一个二维的数据矩阵[T, 4],其中T为时序长度,4为特征个数。在机器学习中,单支股票数据只能算作一个样本,进一步考虑多支股票则可构成标准的序列数据集[N, T, 4],其中N为股票数量。
进一步地,针对这样的序列数据集,RNN是怎样进行特征提取的呢?这里,我们有必要先追溯RNN之前的模型——DNN,并给出一个简单的DNN网络架构如下:
一个具有4个输入特征、单隐藏层的DNN架构
如果我们不考虑股票的时间特性(消除前述数据集的时间维度),则每支股票特征仅有4个,便可以直接利用上述的3层DNN架构完成特征处理,并得到最终的预测结果。上述这一过程可以抽象为:
这其实恰好就是前文提到的内容:神经网络本质上是在拟合一个复杂的复合函数,其中这个复合函数的输入是X,网络参数是W和b。
那么,当引入了时间维度,输入数据不再是4个特征,而是T×4个特征,且这T组特征具有确切的先后顺序,那么RNN要如何处理呢?一个简单的思路是将上述DNN结构堆叠起来,并循环执行,例如网络结构可能长这样:
RNN处理序列数据示意图
如上述示意图所示,纵向上仍然是一个单纯的DNN网络进行数据处理的流程,而横向上则代表了新增的时间维度。也正因为这个时间维度的出现,所以时刻t对应DNN输入数据将来源于两部分:当前时刻t对应的4个输入特征,以及t-1时刻的输出信息,即图中粉色横向宽箭头表示的部分。
是否好奇:为啥要将t-1时刻的的输出作为t时刻的输入呢?当然是因为要序列建模!如果不把相邻时刻的输入输出联系起来,那序列先后顺序又该如何体现?
用数学公式加以抽象表示,就是:
上式中,Wi表达当前输入信息的权重矩阵,Wh表达对前一时刻输入的权重矩阵,且二者在各个时刻是相同的,可理解为面向时间维度的权值共享。
对比该公式和前面DNN中的公式,主要有两点区别:
- 映射函数的输入数据部分不止是X,还有前一时刻提供的信息ht-1;
- 模型的直接输出变为中间状态ht,而ht与最终输出y的区别在于:y可以是直接给出最终需要的信息,例如股票预测中的收盘价,但ht为了兼顾相邻时刻之间的信息交互,往往不一定符合最终的输出结果,所以可能需要对ht进一步使用一个DNN网络进行映射得到想要的输出
至此,我们再来看一下开篇中给出的标准RNN模块结构:
上图中的右侧(unfold部分),横向代表了沿时间维度传播的流程,纵向代表了单个时刻的信息处理流程(各时刻都是一个DNN),其中X代表各时刻的输入特征,ht代表各时刻对应的状态信息,U和V分别为当前输入和前一时刻状态的网络权重,W为由当前状态ht拟合最终需要结果的网络权重。而左侧呢,其实就是把这个循环处理的流程抽象为一个循环结构,也就是那个指向自己的箭头。
这个用指向自己的箭头来表示神经网络的循环,乍一看还挺唬人的!
至此,其实就已经完成了标准RNN的结构介绍。用一个更为广泛使用且抽象的RNN单元结构示意图,表达如下:
标准RNN模块的内部结构
标准RNN结构非常简单,通常来说,在神经网络中过于简单的结构也意味着其表达能力有限。比如说,由于每经过一个时间节点的信息传递,都会将之前的历史信息和当前信息进行线性组合,并通过一个tanh激活函数。tanh激活函数的输出值在(-1,1)之间,这也就意味着,如果时间链路较长时历史信息很可能会被淹没!这也是标准RNN结构最大的问题——不容易表达长期记忆——换句话说,就是时间链路较长的历史信息会变得很小。
于是,一个相对更为复杂、可以提供相对长期记忆的循环神经网络——LSTM应运而生。LSTM:Long-Shot Term Memory network,中文即为长短时记忆网络。顾名思义,这是一个能够兼顾长期和短期记忆的网络。内部是如何实现的呢?LSTM单元结构如下
当然,除了上述这一单元结构示意图,LSTM还往往需要这样一组标准计算公式(这个等到后续择机再讲吧。。):
宏观对照标准RNN和LSTM单元结构,可以概括二者间的主要异同点如下:
- 相同点:各单元结构的输入信息均包含两部分,即当前时刻的输入和前一时刻的输入;输出均为ht
- 不同点:
- RNN中接收前一时刻的输入信息只有一种(这部分叫做ht),体现为相邻单元间的单箭头;而在LSTM中接收前一时刻的输入则包含两部分(两部分分别是ht和ct,ct是新引入的部分),体现为相邻单元间的双箭头
- RNN中的内部结构非常简单,就是两部分向量相加的结果;而LSTM的内部结构相对非常复杂
深入理解LSTM单元的内部结构,建议参考某国外作者的博客:https://colah.github.io/posts/2015-08-Understanding-LSTMs/。本文中的部分网络模块示意图素材也摘自于此(想必也是国内各种介绍循环神经网络时广泛传播的一组内容)。
这里不再班门弄斧,仅简单补充个人理解:
- 与标准RNN中简单地将前一状态信息与当前信息线性相加不同,LSTM中设计了三个门结构(所谓的门结构就是经过sigmoid处理后的权重矩阵,这个矩阵的取值在(0, 1)之间,越接近于1表示通过的信息越大,越接近于0表示对信息的消减越严重),即遗忘门、输入门和输出门,其中:
- 遗忘门作用于历史数据输入上,用于控制历史信息对当前输出影响的大小;
- 输入门作用于当前输入上,用于控制当前输入信息对当前输出影响的大小;
- 输出门则进一步控制当前输出的大小;
- LSTM中之所以相较于标准RNN能提供更为长期的记忆,根本原因在于引入了从历史信息直接到达输出的通路(LSTM结构中的上侧贯通线),由于该通路可以不与当前时刻输入同步接受相同的门控作用,所以允许网络学习更为久远的记忆(只需将遗忘门的结果学习得大一些);
- LSTM除了可以提供长期记忆,还可以提供短期记忆,原因在于专门提供了对当前输入信息的通路(LSTM结构中的下侧通路),但同时该信息又与部分历史信息会和,一并经过输入门的控制,这一部分子结构大体定位相当于标准RNN中的简单处理流程
进一步复盘由RNN到LSTM的改进:虽然LSTM设计的非常精妙,通过三个门结构很好的权衡了历史信息和当前信息对输出结果的影响,但也有一个细节问题——为了控制两部分信息的相对大小,我们是不是只需要1个参数就可以了呢?比如,计算x和y的加权平均时,我们无需为其分别提供两个系数α和β,计算z=αx+βy,而只需z=αx+y就可以,或者写成其归一化形式z=αx+(1-α)y。正是基于这一朴素思想,LSTM的精简版——GRU单元结构顺利诞生!
具体来说,GRU就是将遗忘门和输入门整合为一个更新门,其单元结构如下:
对比下LSTM与GRU的异同点
所以概括一下:从RNN到LSTM的改进是为了增加网络容量,权衡长短期记忆;而从LSTM到GRU的演进则是为了精简模型,去除冗余结构。
上述大体介绍了循环神经网络的起源,并简要介绍了三种最常用的循环神经网络单元结构:RNN、LSTM和GRU。如果说卷积和池化是卷积神经网络中的标志性模块,那么这三个模块无疑就是循环神经网络中的典型代表。
02 RNN为何有效
DNN可以用通用近似定理论证其有效性(更准确地说,通用近似定理适用于所有神经网络,而不止是DNN),CNN也可以抽取若干个特征图直观的表达其卷积的操作结果,但RNN似乎并不容易直接说明其为何会有效。
总体而言,我个人从以下几个方面加以感性理解:
- 循环神经网络适用于序列数据建模场景,而相较于普通的DNN(包括CNN,其实也是不带有时间依赖信息的单时刻输入特征)而言,其最大的特点在于如何按顺序提取各时刻的新增信息,所以形式上必然是要将当前信息与历史信息做融合
- 为了保持对所有时刻信息处理流程的一致性,RNN中也有权值共享机制,即网络参数在随时间维度的传播过程中使用同一套网络权重(Wi和Wh),这保证了处理时序信息的公平性
- 适当的门机制。实际上,标准的RNN单元其记忆能力是有限的,所以谈不上有效;但为何LSTM却非常有效?其核心就在于门的设计上,即允许神经网络通过反向传播算法去自主学习:什么情况下应给历史信息较大的权重——记忆历史;什么情况下又该给当前输入信息较大的权重——更新现在,而这一切都交由网络通过训练集自己去训练
循环神经网络是一个精妙的设计,对于序列建模而言是非常有效的,其历史地位不亚于卷积神经网络。但也值得指出,目前循环神经网络似乎已经有了替代结构——注意力机制——这也是对序列建模非常有效的网络结构,而且无需按时间顺序执行,可以方便的实现并行化,从而提高执行效率——当然,这是后来的故事!
03 RNN适用的场景
论及循环神经网络适用的场景,其实答案是相对明确的,即序列数据建模。进一步地,这里的序列数据既可以是带有时间属性的时序数据,也可以是仅含有先后顺序关系的其他序列数据,例如文本序列等。
与此同时,根据输入数据和预测结果各自序列长度的不同,又衍生了几种不同的任务场景,包括:
- N to 1:前述的例子都是N个时刻的输入对应1个时刻的输出,例如股票预测,天气预报、文本情感分类等
- 1 to N:典型应用是NLP中的作诗、写文章等,即仅提供标题或起始单词,交由模型输出一篇文章
- N to N:即给定N个历史时刻的输入数据,同步给出相应的N个输出(注意,这里的输入和输出是同步的),典型的应用场景是词性分析,即逐一输入一段文本中的各个单词,要求输出各单词的词性判断结果
- N to M:也就是给定N个历史的输入数据,完全处理后得到M个输出,其中M个输出与N个输入具有先后顺序,输入和输出是异步的(也叫Seq2Seq)。典型的场景是机器翻译:给定N个英文单词,翻译结果是M个中文词语,多步的股票预测也符合这种场景
04 在PyTorch中的使用
对于标准RNN、LSTM和GRU三种典型的循环神经网络单元,PyTorch中均有相应的实现。对使用者来说,无需过度关心各单元内在的结构,因为三者几乎是具有相近的封装形式,无论是类的初始化参数,还是对输入和输出数据的形式上。
所以,这里以LSTM为例加以阐述。首先来看其类的签名文档:
上述文档仅给出了LSTM的理论介绍,主要是陈列了其内部结构的几个相关理论计算公式。而对于类的初始化参数方面,LSTM并未直接给出形参列表,而是使用*arg和**kwargs来接受自定义的传入参数,其中,常用的参数包括
具体来说:
- input_size:输入数据的特征维度,例如在前面举的股票例子中包括open、high、low和close共4个特征,即input_size=4
- hidden_size:前面提到,在每个时间截面循环神经单元其实都是一个DNN结构,默认情况下该DNN只有单个隐藏层,hidden_size即为该隐藏层神经元的个数,在前述的股票例子中隐藏层神经元数量为3,即hidden_size=3
- num_layers:虽然RNN、LSTM和GRU这些循环单元的的重点是构建时间维度的序列依赖信息,但在单个事件截面的特征处理也可以支持含有更多隐藏层的DNN结构,默认状态下为1
- bias:类似于nn.Linear中的bias参数,用于控制是否拟合偏置项,默认为bias=True,即拟合偏置项
- batch_first:用于控制输入数据各维度所对应的含义,前面举例中一直用的示例维度是(N, T, 4),即分别对应样本数量、时序长度和特征数量,这种可能比较符合部分人的思维习惯(包括我自己也是如此),但实际上LSTM更喜欢的方式是将序列维度放于第一个维度,此时即为(T, N, 4)。batch_first默认为False,即样本数量为第二个维度,序列长度为第一个维度,(seq_len, batch, input_size)
- dropout:用于控制全连接层后面是否设置dropout单元,增加dropout有时是为了增强模型的泛化能力
- bidirectional:上述所介绍LSTM等都是沿着序列的正向进行处理和传播,正向传播更容易记住靠后的序列信息,而忘记前面的信息;所以LSTM的一种改进就是双向循环单元结构,即首先沿正向处理一遍,再逆向处理一遍。bidirectional参数即用于控制是单向还是双向,默认为bidirectional=False,即仅正向处理
接下来是输入和输出信息:
大体来看,输入和输出具有相近的形式(这也是为啥可以循环处理的原因),对于LSTM来说包含三部分,即:
- input/output:(L, N, H_in/H_out),其中L为序列长度,N为样本数量,H_in和H_out分别为输入数据和输出结果的特征维度,即前面初始化中用到的input_size和hidden_size
- h_n和c_n,分别对应最后时刻循环单元对应的隐藏状态和细胞状态(LSTM的相邻单元之间有两条连接线,上面的代表细胞状态c_n,下面代表隐藏状态h_n),如果是RNN或者GRU则只有隐藏状态h_n
- 进一步地,output与h_n的联系和区别是什么呢?output是区分时间维度的输出序列,记录了各时刻所对应DNN的最终输出结果,L个序列长度对应了L个时刻的输出;而h_n则只记录最后一个序列所对应的隐藏层输出,所以只有一个时刻的结果,但如果num_layers>1或者bidirectional设置为True时,则也会有多个输出结果。当在默认情况下,即num_layers=1,bidirectional=False时,output[-1]=h_n
关于循环神经网络的介绍就到这里,后续将基于股票数据集提供一个实际案例。