自学内容网 自学内容网

【NLP】第二章:RNN 循环神经网络

二、循环神经网络RNN(Recurrent Neural Network)

正常情况是先讲数据、再讲模型,但是NLP的数据和模型都是比较复杂的,而且需要二者结合到一起讲才比较好理解和说清楚,单独讲数据或者单独讲模型都讲不明白。所以这里就直接开讲RNN循环神经网络,让大家对NLP是干什么的有一个基础的认知。

循环神经网络RNN是NLP领域的入门级深度学习算法,是长短期记忆网络(LSTM)、门控循环单元(GRU)、双向RNN(Bi-RNN)以及其他经典算法的基础,也是理解transformer、大语言模型的基础。RNN在NLP中的地位,就好比逻辑回归之于机器学习。虽然现在大家单独使用它来解决某个特定任务已经很少了,但是它是进入NLP领域的入门架构,是你入门NLP的必备基础,因为后面的架构都是基于它的思想,所以对RNN的清晰了解有助于理解后面更加复杂的架构。

本篇就从输入数据、模型架构、参数矩阵、数据流、以及在pytorch中实现等这些方面展开讲解,来一次性说清楚RNN。

(一)RNN的输入数据

1、NLP领域的数据:序列数据
至于NLP领域的数据,很多人第一时间自然会想到,那自然就是文本数据了。其实你只答对了一部分。只要是序列数据,基本上NLP算法族中的所有架构都可以处理。
序列数据是指,样本和样本之间存在特定顺序、并且这种特定顺序是不能改变的数据。一旦样本顺序调换了或者样本缺失了,数据的含义就发生了巨大的变化。或者说序列数据是隐含有时间顺序、或者是隐含有逻辑损害、或者是有位置顺序的特性的数据。

典型的序列数据一般有以下几种类型:
(1)文本数据(Text Data):文本数据中的样本就是特定顺序的,因为它是有语义顺序的,就是词与词、句子与句子、段落与段落之间是有顺序的。而且在语义环境中,词语之间的训练或缺失,是会彻底改变语义的。例如屡败屡战和屡战屡败的语义就大相径庭。这种数据就是典型的隐含位置顺序的数据。
(2)时间序列数据(Time Series Data):比如股票价格、气温记录、心电图等数据,数据的背后是有时间语义的,就是一个样本代表一个特定时间点,每个特定时间点上有自己的标签。当你改变了样本顺序,你就把在时间维度上的标签趋势给破坏了,而我们要预测的就是这个标签,那你把标签的趋势都破坏,还能准确预测嘛?!这种数据就是典型的隐含时间顺序的数据。
(3)音频数据(Audio Data):音频是文本数据的声音信号,前面说过文本数据是有语义顺序的,所以音频也必须是位置有序的。
(4)符号序列数据。比如密码学、自动编码学、DNA测序等,都对数据的顺序有严格要求的,都属于序列数据。

2、序列数据的特点
(1)对于一些非序列数据,我们在训练过程中,还故意shuffle样本,故意利用随机性,让模型看到更多的情况,增加模型的泛化能力。但是序列数据是万万不能shuffle的

(2)我们在处理序列数据时,不仅要让算法学习每一个样本,还得让算法学习样本和样本之间的联系
而能学习到样本之间联系的算法,就构成了今天的自然语言处理架构群:从隐马尔可夫模型HMM、条件随机场CRF,到RNN、LSTM长短期记忆网络,再到基于transformer的BERT、GPT等算法,都是NLP算法族。

(3)从数据维度看,序列数据的维度是可以变化的。就是时序数据可以有各种维度。比如类似股票价格的数据是二维的,而文本数据可以是二维的也可以是三维的,而音频数据肯定是三维以上的,视频数据是五维。所以,对于任何维度的原始数据,只要它有时间顺序或者位置顺序,它都是一个序列数据

3、可以喂入RNN的时序数据
只要是序列数据,RNN都可以处理,比如下面的数据结构:

所以RNN可以处理一维的单变量序列数据,也可以处理二维的多变量序列数据,还可以处理三维的多特征序列数据。但是这里重点要强调的是这几个点:

一是,你一定要知道什么是样本,因为后面我们会经常提到样本这个概念。从上图看,一个时间点对应的一行特征就是一条样本。可见样本和样本之间的前后时间顺序是不能打乱和缺失的
二是,在NLP里面,我们一般给时间点叫做“时间步”(time_step),也叫“序列长度”(sequence_lenght)。所以一个时间步对应的一条数据就是一条样本。一条样本包含一个时间点。
三是,在NLP里面,我们一般
将构成样本的变量叫特征
。所以比如一个样本有5个特征,那么它的input_dimensions就是5。

四是,大家都说RNN可以学习样本和样本之间的联系,说的是让算法去学习每个时间步之间的关系,也就是让算法去学习每个序列长度内部样本之间的关系。也就是上图中我红色标出来的样本和样本之间的关系。上图每张表单都是10个样本,所以RNN学的就是这个10个样本之间的关系。

五是,RNN还接受右图的三维数据,也就是多张表单数据,有几张表单batch_size就是几,所以batch_size表示一共有多少张二维时序表单。比如上右图的batch_size就是3,因为有3张表单。但是RNN学的也是每张表单中的样本和样本之间的关系不是表单和表单的关系,具体到上右图,不学股票1、股票2、股票3之间的关系!!!这点一定一定要清楚。

六是,对于单张表单时序数据,RNN正向传播一次,每次喂入RNN的是一个一维变量。对于上中图就是1行5列的数据。
对于多张表单时序数据,RNN正向传播一次,每次喂入RNN的是一个二维变量。对于上右图就是3行5列的数据。
这里不太明白的同学先不着急,回头讲RNN架构,讲数据流时,你就会有真实的体会。

4、可以喂入RNN的文本数据
RNN只认识数字、向量、矩阵之类的,不认识人类的文字。所以当我们拿到文本数据后,比如当我们拿到的是一篇小说文本,我们是先将整篇文章分成一个个句子,然后再对每个句子进行分词,最后对词进行编码,形成一个个表单序列数据,如下图的绿色栏那样:

编码是将文本数据用单一数字或者一串数字来表示某个字或者某个词。所以相同的字或者相同的词会被编码成相同的数字或者相同的数字序列。当所有的字或词都被编码后,就构成了你的词向量。至于如何分词,如何编码,又是一个big topic,以后会有一个单独的章节来讲。如上图所示,我们把一句话编码成1、2、3种方式。多句话我们可以编码成4方式。不管哪种方式,效果好就好,效果为王。当然也有一些业内经验之谈,就是大家都公认怎么处理文本效果最好(炼丹嘛当然是经验了),这我们以后细聊。这里再强调几点:
一是,一个词是一条样本。一定一定要牢记什么是"样本",不然到后面你会迷糊。
二是,和序列数据中的时间相对应的序列数据中的词序列,我们在叫法上有一点点的不同。大家给时间序列的时间点叫做“时间步”(time_step),也叫“序列长度”(sequence_lenght);给文本数据的词序列叫"sequence或者vocab_size",它代表了一个句子中的词数个数;给多个句子叫做batch_size。
三是,样本和样本之间的顺序不能打乱,也不能缺失样本。但batch之间是可以打乱的!!。因为RNN学的是样本和样本之间的关系,不是表单和表单之间的关系!

四是,sequence_lenght是我们人为设置的一个数,一般都是设置成“所有句子中的词个数的平均数”,比如上图的6吧,然后对短句进行0填充或均值填充,对长句进行裁剪,让所有的vocab_size保持在同样的维度。最后形成比如上右图的样子:一个句子一张表单,每张表单的vocab_size是相同的,不够的0补,长的截断。这就是RNN的输入数据。

所以循环神经网络RNN是可以接受二维(一句话)、三维(多句话)数据的网络。

(二)RNN的基本网络架构
在讲RNN架构之前,我们需要先弄清普通DNN的架构。因为RNN和DNN一样,也都是由全连接层、激活函数等组件构成的,只是数据流有些不一样。所以我们先详细回顾一下DNN的架构和数据流。

1、DNN的代码实现、抽象架构图、pytorch中的输入数据、DNN架构的参数矩阵、以及数据流动过程

(1)DNN架构的代码实现
上面左上角的架构代码只是写架构的其中一种写法,也是比较传统的写法。具体代码讲解可以参考【深度视觉】第八章:复现经典架构:LeNet5、AlexNet、VGGNet、NiN_nin代码复现-CSDN博客 中的一些架构中的介绍,而且你想用其他写法,这篇博文中也有示例。至于架构如何设计参考【深度学习】第三章:搭建架构-正向传播-计算损失_dnn网络架构-CSDN博客

(2)抽象架构图
上图左上角的代码架构,有时理解起来比较抽象,那我们把它画出来就非常形象了,有助于我们理解架构。所以我们一般在脑海中都会有一个上图中间白背景的架构图,辅助我们理解。当然上图画的神经元和神经元之间的连接只画了一个示例,应该都是全连接的。我们看资料看论文时,基本都有这种架构图,所以一定得能看懂这个图传递的意思。

(3)输入数据
上图右上角就是我造的一个有两条样本、每个样本有三个特征的玩具数据。这里要强调两点:
一是,输入数据是要和架构匹配的。而输入数据又是我们在现实中采集而来的数据,所以一般都是输入数据是一定的,我们写的架构要去匹配数据。所以你现在手头的数据是什么,你就得写和数据匹配的架构。这也是为什么深度学习中,一种架构只解决一种数据。数据结构不一样就得更换模型架构。
二是,在pytorch框架下,如果输入数据只有一条样本,那输入数据就是一个一维的、float向量。如果输入数据大于一条样本,就是多条样本时,那输入数据就是一个二维的、float矩阵(例如上图右上角的data)。矩阵的行表示样本、列表示每个样本中的特征。所以一个有多条样本的输入矩阵的行数就是样本个数,列数就是样本中的特征个数

(4)DNN架构的参数矩阵
DNN架构中,输入层没有参数(当然喽),中间的隐藏层和输出层是有参数的(当然喽,呵呵)。隐藏层和输出层的参数都是以矩阵的形式存在的,而且都是参数矩阵+偏置向量构成的(当然了,要不怎么叫它线性层呢),比如上图的右下角list(net1.parameters())的返回结果,就是net1架构的所有参数。其中:
参数矩阵是一个二维float类型数据。参数矩阵的行数是本层神经元的个数、列数是输入层的神经元个数
偏置是一个一维向量、float类型数据。偏置矩阵的行数都是1,列数是本层神经元的个数
当我们实例化架构后,就自动生成一套整个架构的参数矩阵和偏置,你可以简单的认为这套参数都是随机生成的。想了解详情的可参考【深度学习】第六章:模型效果评估与优化_模型评估与优化-CSDN博客 中的第5小标题。

(5)DNN网络中的数据流
知道了输入矩阵、参数矩阵,DNN每个隐藏层就是一个带偏置的线性变换层,线性变换就非常简单了,就是用该层的参数矩阵*输入矩阵的转置+偏置向量的转置
那我们梳理一下上图的的数据流动过程:

总结:数据是以,以行为样本,以列为特征的二维数据喂入DNN,第一层网络也就是隐藏层1,隐藏层1的参数矩阵是(本层神经元个数,输入层的神经元个数),数据data一进入隐藏层1就先做一个转置,然后隐藏层1的参数矩阵乘以转置的数据加偏置,就是本层隐藏层的输出。
然后数据进入linear2,也就是上图的输出层, 同理用linear2层的参数和输入数据矩阵相乘加偏置,得到的结果再转置输出,输出的结果的行就是一条条样本,列是每个样本被转化成的特征个数。

以上就是pytorch框架中一个DNN全连接神经网络的最基本内容。下面开始讲RNN的架构和数据流。

2、RNN的基本架构、以及数据流
如下图所示,右边就是一个最简单的循环网络架构:

(1)输入层的神经元个数由词向量的长度决定。因为我们是这样喂入RNN数据的:假如我们只有一张表单数据,是先把"我"向量转置送入输入层,让"我"字正向传播完毕,然后再把"有"向量转置送入输入层,让"有"字正向传播完毕,,依次如此正向传播6次。
如果我们是上图的三张表单数据,是先把矩阵("我"向量, "我"向量, "很多"向量)转置送入输入层,让("我"向量, "我"向量, "很多"向量)正向传播完毕,然后再把矩阵("有"向量, "有"向量, "人"向量)转置送入输入层,让("有"向量, "有"向量, "人"向量)向传播完毕,,依次如此正向传播6次。
这里不明白的,先有个印象即可,因为后面讲数据流时还得展开讲呢,这里只要知道RNN的输入层神经元个数是词向量的长度。

(2)隐藏层你可以自己规定层数、以及每层有几个神经元。只有一个隐藏层的架构叫单层循环神经网络;有两个或者两个以上隐藏层的架构叫深层循环神经网络

(3)输出层的神经元个数根据你的任务目标来设置,比如这里我要进行情感分类,分积极\中性\消极三分类,所以我的输出神经元就要设置3个,而且一般最后都会再跟一个softmax层来整理输出结果,让它变成类概率的样子。如果我想做回归类任务,那输出层就设置1个神经元。如果我想做翻译,那输出层就设置成要求的输出个数个神经元。

  • 补充:根据输入输出的个数,RNN架构经常被人们这样分类:

    是不是瞬间觉得RNN这么厉害。呵呵。。。

(4)输入数据
以上上图的中的数据为例,就是将每个句子编码成长度为6(也就是vocab_size=6,也是sequence length=6)、每个词编码成4维的词向量,的3个句子表单,也就是一个batch的数据,batch_size=3

(5)每个线性层的参数矩阵
RNN的线性层的参数也DNN线性层的参数是一模一样的。都是由参数矩阵+偏置向量构成的。
参数矩阵:(本线性层的神经元个数,输入本层的神经元个数)
偏置向量:(1, 本层线性层的神经元个数)
说明:后面讲数据流的时候,为了简化问题,说得清楚,我就把偏置给省略了。

(6)RNN网络中的数据流
当我们喂入上上图中的数据时:
如果是普通的DNN架构,那DNN是把这个sequence当作6条样本数据,这6条样本数据作为一个输入矩阵,喂入网络,网络的正向传播也是网络每层神经元的参数矩阵和这个6个样本矩阵的转置相乘,然后输出到下一层,下一层的所有神经元参数矩阵再和这个输出相乘,继续输出到它的下一层,直到最后的输出层,输出的数据矩阵再转置一下显示出来。这就是DNN的一次正向传播。DNN正向传播一次完毕后,就开始计算损失,迭代参数了。也所以DNN是的一次正向传播是一次计算6条样本。

从上面我画的RNN架构看,RNN与DNN是完全一致的!是的,还真没错,所以如果你看到有人的架构用线性层写,你不要怀疑,人家写的架构也是循环神经网络,你要看人家写的forward数据流。
那RNN它之所以叫RNN,是因为它的数据流和DNN的设计是完全不同的,RNN除了像DNN那样从输入层-隐藏层-输出层这样的一层层横向传递外,RNN还有一条数据流叫循环数据流,这个数据流就是“记忆”样本和样本之间关系的数据流。就是每个样本在正向传播的时候,RNN会将这个样本所在的每层神经元的信息向下传递给下一个样本,也就是纵向传播,让网络记得已经见过的样本,也就是所谓的学习样本和样本之间关系的机制,也就是RNN的记忆功能。

那么如何实现这种数据流呢?那就得样本一条一条逐个的进行正向传播,或者说其实上上图的RNN架构,本质上应该是6个一模一样的右图架构,竖着排列起来的一个架构,然后每条样本在自己所对应的那层架构从输入传到输出,而且从垂直角度看,上面一层的数据流还会往下面的对应层流动,直到第6层输出:

  • RNN的数据正向传播过程的“记忆”机制

以上图中的第一张表单为例,也就是一张表单数据。RNN是先被喂入"我"这个字的词向量(x1,x2,x3,x4)(记作S1),转置(S1T),喂入大层1的输入层对应的4个神经元,然后传播到大层1第一个隐藏层。第一个隐藏层的参数矩阵(W1)乘以"我"字的转置词向量(S1T),得到“我”字的2个特征的中间特征(S12),然后传递给输出层。输出层用自己的参数矩阵(W2)乘以这2个特征的中间特征(S12),得到“我”字的3个特征值(S13),然后转置输出显示。
这是“我”字样本的一次正向传播,和前面的DNN数据流是一样的。

下面开始第二个词"有"字正向传播:"有"字对应的向量(记作S2),转置(S2T),喂入大层2的输入层,传播到大层2的第一个隐藏层。第一个隐藏层的参数矩阵(W1)乘以"有"字的转置词向量(S2T),得到“有”字的2个特征的中间特征(S22)。与此同时,“我”字正向传播时的第一个隐藏层的神经元和此时“有”字正向传播的第一个隐藏层的神经元也是全连接的!有连接就有参数(wh1)。所以,此时“我”字正向传播的中间数据(S12)就左乘wh1,假如结果是H1,然后H1+S22的和,再往输出层传播。“有”字输出层的输出也它的隐藏层的输出一样,都是横向传播+纵向传播。这就是“有”字样本的一次正向传播。

同理开始“一个”样本进入大层3开始正向传播,那自然和“有”样本的正向传播一样,不仅要横向传播,还得接受“有”字纵向传播过来的中间变量。
如此一直到第6个样本padding正向传播完毕,最后的输出就是RNN的输出。也是记忆了前面5个样本的结果。

但是我上图标注的数据流都是矩阵啊,不是一张表单,是的,没错,一张表单一张表单的处理岂不慢死了,所以RNN还有个权值共享机制:

  • RNN的权值共享机制,让计算更高效

为什么会有权值共享这件事呢?前面讲了,RNN处理一张表单,也就是处理6条样本(因为一张表单的sequence_lenght就是6),它不是像DNN那样,把这6条样本放到一个矩阵里面,这个矩阵和网络的各个层参数进行矩阵乘,一层层网络往后传递,完成一次正向传播,然后计算损失、迭代网络参数。RNN它是一条一条样本正向传播的。就是RNN处理一张表单,就需要正向传播6次,虽然这6次都不用计算损失也不用更新参数,但是6次传播才处理6条样本的效率也实在是太低了。为了解决这个问题才有了权值共享机制。但是这个权值共享机制从名字上看,好像很高端,其实非常容易理解:
你想,何为正向传播?正向传播从数学角度理解就是做矩阵乘积啊。当一条样本从RNN网络中正向传播一次,其实就是每层的参数矩阵乘以一个向量。而pytorch框架的底层矩阵乘是用了一种非常快速的计算方式计算的,不是我们数学上是左边矩阵的行乘以右边矩阵的列再相加,才求出结果矩阵的一个元素。pytorch的矩阵乘是经过优化了的一个算法,所以对应pytorch来讲,一个向量和矩阵的乘积,和一个矩阵和矩阵的乘积,对它来说计算量是一样的。所以如果一条条样本计算太低效,那就把多张表单一起正向传播,不就是矩阵乘矩阵了嘛,这效率就一下子上来了。

所以,RNN的输入一般是(batch_size, vocab_size, input_dimentions)这样的一个三维数据。我正向传播的时候,因为要学习样本和样本之间的关系,所以我还是得一条条样本正向传播。但是我可以一次正向传播batch_size条每条样本啊。因为表单和表单之间的关系是不需要学习的
你可以这样理解,上图数据batch_size=3, vocab_size=6, input_dimentions=4,那我现在喂入RNN的数据就是3张表单,也就是3句话;每张表单的sequence_length=6,就是每句话我都分成了6个词;每个词我都编码成4个数字。这时RNN正向传播一次处理的就是3句话中的每句的第一个词。RNN是不需要考虑每句话之间的前后上下文关系的,它只需考虑每句话中的每个词之间的前后上下文关系。所以每张表单中每条样本,一次正向传播,就可以喂入(batch_size,1,input_dimentions)这样一个矩阵,就是喂入3张表单的第一个词的数据,那就是(3,4),就是3行4列的矩阵,进行正向传播。此时就是矩阵乘矩阵,就可以更好的享受pytorch底层矩阵乘的优化。而RNN要处理完毕这3张表,就只需要正向传播6次,而且这6次中,次与次之间不需要计算损失和更新参数,就是这6次都是同一套参数。然后6次正向传播完毕,也就是3张表单处理完毕,也就是3句话处理完毕,再求一次损失迭代一次参数。下一轮继续另外3句话:3张表单、6次正向传播、一次计算损失和一次迭代网络参数。。。如此反复。

3、五花八门的RNN架构图
当我们知道RNN的数据流后,我们也就可以理解为啥网上的RNN架构图真实五花八门、各式各样、眼花缭乱:

但是不管什么形式的RNN架构图,我们都要明白它是想表达什么的。
上图左图的每个神经元H被称为“隐藏状态”(Hidden Representation 或者 Hidden State),特指RNN隐藏层上的中间变量,一般在代码中也表示为小写的字母h。X看作是每个time_step上的词向量。对于小白上来就看这种架构图就比较吃力。但它本质上其实就是我画的上上图的一个垂直截面图。

我画的图就形象太多了,你就立马理解了我前面说的是把6个一模一样的架构竖着排,然后第一个样本从垂直方向的第一层进入,第二个样本从垂直方向的第二层进入,,,依次类推,开始正向传播。传播过程中第一个样本流出第一个隐藏层后,其结果一是继续向后传播,二是向下传播(因为这个隐藏层和下面对应位置的隐藏层之间也是全连接),就是所谓的“记忆”了。同理后面。如此这样流动直到这9个样本全部都正向传播完毕一次,才是叫“正向传播了一次,然后才是求损失求梯度更新网络参数”。

小结:
RNN网络架构和DNN的区别就是它的隐藏层在sequence维度上有纵向的一个全连接的!所有纵向的对应位置的隐藏层之间都是互相全连接的!一般不会有交叉,都是对应位置之间全连接。就是有几个隐藏层,纵向就有几个全连接。纵向的长度就是sequence_length长度。所以,RNN网络架构可以是sequence_length个DNN。网上五花八门的RNN架构你要明白它的重点是什么。

4、在pytorch中实现RNN网络

待续。。。。。


原文地址:https://blog.csdn.net/friday1203/article/details/143810918

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!