神经网络

神经网络

参考资料:

  • TENSORFLOW系列专题
  • TensorFlow 从入门到精通

神经网络_第1张图片

深度学习的概念是从人工神经网络的研究中发展而来的,早期的感知器模型只能解决简单的线性分类问题,后来发现通过增加网络的层数可以解决类似于“异或问题”的线性不可分问题,这种多层的神经网络又被称为多层感知器。对于多层感知器,我们使用BP算法进行模型的训练,但是我们发现BP算法有着收敛速度慢,以及容易陷入局部最优等缺点,导致BP算法无法很好的训练多层感知器。另外,当时使用的激活函数也存在着梯度消失的问题,这使得人工神经网络的发展几乎陷入了停滞状态。为了让多层神经网络能够训练,学者们探索了很多的改进方案,直到2006年Hinton等人基于深度置信网络(DBN)提出了非监督贪心逐层训练算法,才让这一问题的解决有了希望,而深度学习的浪潮也由此掀起。

前馈神经网络综述

本节内容主要包括四个部分,第一部分我们介绍一下神经网络的基本结构,从基本的感知器模型到多层的神经网络结构;第二部分介绍神经网络中常用的激活函数;第三部分介绍损失函数和输出单元的选择;第四部分介绍神经网络模型中的一个重要的基础知识——反向传播算法。

(1)神经网络的基本结构

1. 感知机模型

感知器(Perceptron)是一种最简单的人工神经网络,也可以称之为单层神经网络,如图1所示。感知器是由Frank Rosenblatt在1957年提出来的,它的结构很简单,输入是一个实数值的向量,输出只有两个值:1或-1,是一种两类线性分类模型。

神经网络_第2张图片

图1 感知器模型

如上图所示,感知器对于输入的向量先进行了一个加权求和的操作,得到一个中间值,假设该值为Z,则有:

接着再经过一个激活函数得到最终的输出,该激活函数是一个符号函数:

公式1中的b可以看做是一个阈值(我们通常称之为偏置项),当输入向量的加权和大于该阈值时(两者之和),感知器的输出为1,否则输出为-1。

2. 多层神经网络

感知器只能解决线性可分的问题,以逻辑运算为例:

神经网络_第3张图片

图2 逻辑运算

感知器可以解决逻辑“与”和逻辑“或”的问题,但是无法解决“异或”问题,因为“异或”运算的结果无法使用一条直线来划分。为了解决线性不可分的问题,我们需要引入多层神经网络,理论上,多层神经网络可以拟合任意的函数。

与单层神经网络相比,多层神经网络除了有输入层和输出层以外,还至少需要有一个隐藏层,如图3所示是含有一个隐藏层的两层神经网络:

神经网络_第4张图片

图3 两层神经网络

(2)激活函数

线性模型在处理非线性问题时往往手足无措,这时我们需要引入激活函数来解决线性不可分问题。激活函数(Activation function),又名激励函数,往往存在于神经网络的输入层和输出层之间,作用是给神经网络中增加一些非线性因素,使得神经网络能够解决更加复杂的问题,同时也增强了神经网络的表达能力和学习能力。另外我们使用基于梯度的方式来训练模型,因此激活函数也必须是连续可导的。

常用的激活函数有Sigmoid函数、双曲正切激活函数(tanh)、修正线性单元(ReLU)等。

1. Logistic函数

Logistic函数(又称为sigmoid函数)的数学表达式和函数图像如图4所示:

神经网络_第5张图片

图4 Logistic函数表达式及函数图像

Logistic函数在定义域上单调递增,值域为[0,1],越靠近两端,函数值的变化越平缓。因为Logistic函数简单易用,以前的神经网络经常使用它作为激活函数,但是Sigmoid函数也有两个很大的缺点:首先是Sigmoid函数会造成梯度消失问题,从图像中我们也可以得知,当输入特别大或是特别小时,神经元的梯度几乎接近于0,这就导致神经网络不收敛,模型的参数不会更新,训练过程将变得非常困难。另一方面,Sigmoid函数的输出不是以0为均值的,导致传入下一层神经网络的输入是非0的。这就导致一个后果:若Sigmoid函数的输出全部为正数,那么传入下一层神经网络的值永远大于0,这时参数无论怎么更新梯度都为正。正是基于上述的缺点,Sigmoid函数近年来的使用频率也在渐渐减弱。

2. Tanh函数

Tanh函数(双曲正切激活函数)的数学表达式和函数图像如图5所示:

神经网络_第6张图片

图5 Tanh函数表达式及函数图像

Tanh函数是Sigmoid函数的变形,其值域为[-1,1],且Tanh函数的输出是以为0均值的,这就一定程度上解决了上述Sigmoid函数的第二个缺点,所以在实际应用上,Tanh函数要优于Logistic函数,但是Tanh函数也同样面临着在其大部分定义域内都饱和的问题。

3. ReLu函数

ReLU函数(又称修正线性单元或整流线性单元)是目前最受欢迎,也是使用最多的激活函数,其数学表达式和函数图像如图6所示:

神经网络_第7张图片

图6 ReLU函数表达式及函数图像

ReLU激活函数的收敛速度相较于Logistic函数和Tanh函数要快很多,ReLU函数在轴左侧的值恒为零,这使得网络具有一定的稀疏性,从而减小参数之间的依存关系,缓解过拟合的问题,并且ReLU函数在轴右侧的部分导数是一个常数值1,因此其不存在梯度消失的问题。

但是ReLU函数也有一些缺点,例如ReLU的强制稀疏处理虽然可以缓解过拟合问题,但是也可能产生特征屏蔽过多,导致模型无法学习到有效特征的问题。还有训练的时候不适合大梯度的输入数据,因为在参数更新之后,ReLU的神经元不会再任何数据节点被激活,这就会导致梯度永远为0。比如:输入的数据小于0时,梯度就会为0,这就导致了负的梯度被置0,而且今后也可能不会被任何数据所激活,也就是说ReLU的神经元“坏死”了。所以针对ReLU函数上述的缺点,又出现了带泄露的ReLU(Leaky ReLU)和带参数的ReLU(Parametric ReLU)。

Leaky ReLU是ReLU激活函数的变形,主要是针对ReLU函数中神经元容易坏死的缺陷,将原点左侧小于0的部分,赋予一个很小的斜率。其效果通常要好于ReLU激活函数,但实践中使用的频率并没有那么高。数据公式为:f(x) = max(0, x) + γmin(0, x)。通常,γ是一个很小的常数,如:0.01。

Parametric ReLU是ReLU激活函数的另一种变形,和Leaky ReLU函数一样是非饱和函数,解决了坏死难题。不同之处在于其在函数中引入一个可学习的参数,往往不同的神经元有不同的参数,所以第i个神经元的数学表达式为:f(x) = max(0, x) + γi min(0, x)。当γi 取0时,便可以视为ReLU函数,取很小的常数时,可视为Leaky ReLU函数。相对而言,Parametric ReLU激活函数的使用频率也不是很高。

上述两种ReLU激活函数的变形Leaky ReLU、Parametric ReLU函数图如图7所示:

图7 Leaky ReLU/Parametric ReLU函数图像

(3)损失函数和输出单元

损失函数(Loss Function)又称为代价函数(Cost Function),它是神经网络设计中的一个重要部分。损失函数用来表征模型的预测值与真实类标之间的误差,深度学习模型的训练就是使用基于梯度的方法最小化损失函数的过程。损失函数值越小,说明模型的预测值就越接近真实值,模型的健壮性也就越好。损失函数的选择与输出单元的选择也有着密切的关系。

1. 损失函数的选择

1.1 均方误差损失函数

均方误差(Mean Squared Error,MSE)是一个较为常用的损失函数,我们用预测值和实际值之间的距离(即误差)来衡量模型的好坏,为了保证一致性,我们通常使用距离的平方。在深度学习算法中,我们使用基于梯度的方式来训练参数,每次将一个批次的数据输入到模型中,并得到这批数据的预测结果,再利用这批预测结果和实际值之间的距离更新网络的参数。均方误差损失函数将这一批数据的误差的期望作为最终的误差值,均方误差的公式如下:

上式中yk为样本数据的实际值,^yk为模型的预测值。为了简化计算,我们一般会在均方误差的基础上乘以1/2,作为最终的损失函数:

1.2 交叉熵损失函数

交叉熵(Cross Entropy)损失函数使用训练数据的真实类标与模型预测值之间的交叉熵作为损失函数,相较于均方误差损失函数其更受欢迎。假设我们使用均方误差这类二次函数作为代价函数,更新神经网络参数的时候,误差项中会包含激活函数的偏导。在前面介绍激活函数的时候我们有介绍,Logistic等激活函数很容易饱和,这会使得参数的更新缓慢,甚至无法更新。交叉熵损失函数求导不会引入激活函数的导数,因此可以很好地避免这一问题,交叉熵的定义如下:

上式中为p样本数据的真实分布,q为模型预测结果的分布。以二分类问题为例,交叉熵损失函数的形式如下:

上式中为y真实值,^y为预测值。对于多分类问题,我们对每一个类别的预测结果计算交叉熵后求和即可。

1.3 Hinge损失函数(hinge loss function)

Hinge损失函数通常适用于二分类的场景中,可以用来解决间隔最大化的问题,常应用于著名的SVM算法中。其数学公式为:

其中在上式中,t是目标值{-1,+1},y为预测值的输出,取值范围在(-1,1)之间。

1.4 对数损失函数(Log Loss)

对数损失函数也是常见的一种损失函数,常用于逻辑回归问题中,其标准形式为:

上式中,y为已知分类的类别,x为样本值,我们需要让概率p(y|x)达到最大值,也就是说我们要求一个参数值,使得输出的目前这组数据的概率值最大。因为概率P(Y|X)的取值范围为[0,1],log(x)函数在区间[0,1]的取值为负数,所以为了保证损失值为正数要在log函数前加负号。

2. 输出单元的选择

2.1 线性单元

线性输出单元常用于回归问题,当输出层采用线性单元时,收到上一层的输出后,输出层输出一个向量。线性单元的一个优势是其不存在饱和的问题,因此很适合采用基于梯度的优化算法。

2.2 Sigmoid单元

Sigmoid输出单元常用于二分类问题,Sigmoid单元是在线性单元的基础上,增加了一个阈值来限制其有效概率,使其被约束在区间之中,线性输出单元的定义为:

上式中是Sigmoid函数的符号表示,其数学表达式在感知机模型小节中有介绍。

2.3 Softmax****单元

Softmax输出单元适用于多分类问题,可以将其看作是Sigmoid函数的扩展。对于Sigmoid输出单元的输出,我们可以认为其值为模型预测样本为某一类的概率,而Softmax则需要输出多个值,输出值的个数对应分类问题的类别数。Softmax函数的形式如下:

我们以一个简单的图示来解释Softmax函数的作用,如图8所示。原始输出层的输出为,,,增加了Softmax层后,最终的输出为:

上式中、和的值可以看做是分类器预测的结果,值的大小代表分类器认为该样本属于该类别的概率,、和的和为1。

神经网络_第8张图片

图8 Softmax输出单元

需要注意的是,Softmax层的输入和输出的维度是一样的,如果不一致,可以通过在Softmax层的前面添加一层全连接层来解决问题。

(4)BP算法原理

反向传播算法(Backpropagation Algorithm,简称BP算法)是深度学习的重要思想基础,对于初学者来说也是必须要掌握的基础知识。

我们使用一个如图9所示的神经网络,该图所示是一个三层神经网络,两层隐藏层和一层输出层,输入层有两个神经元,接收输入样本 , ^y为网络的输出。假设我们使用这个神经网络来解决二分类问题,我们给这个网络一个输入样本 ,通过前向运算得到输出 。输出值^y的值域为[0, 1] ,例如^y 的值越接近0,代表该样本是“0”类的可能性越大,反之是“1”类的可能性越大。

神经网络_第9张图片

图9 一个三层神经网络

1.前馈计算的过程

为了理解神经网络的运算过程,我们需要先搞清楚前馈计算,即数据沿着神经网络前向传播的计算过程,以图9所示的网络为例:

输入的样本为:

神经网络_第10张图片

第一层网络的参数为:

神经网络_第11张图片

第二层网络的参数为:

第三层网络的参数为:

  • 第一层隐藏层的计算

神经网络_第12张图片

图10 计算第一层隐藏层

第一层隐藏层有三个神经元:neu1 、neu2 和neu3 。该层的输入为:

以 neu1神经元为例,则其输入为:

同理有:

假设我们选择函数 f(x) 作为该层的激活函数(图9中的激活函数都标了一个下标,一般情况下,同一层的激活函数都是一样的,不同层可以选择不同的激活函数),那么该层的输出为: f1(z1)、f2(z2) 和f3(z3) 。

  • 第二层隐藏层的计算

神经网络_第13张图片

图11 计算第二层隐藏层

第二层隐藏层有两个神经元:neu4 和neu5 。该层的输入为:

即第二层的输入是第一层的输出乘以第二层的权重,再加上第二层的偏置。因此得到neu4 和neu5 的输入分别为:

该层的输出分别为: f4(z4)和f5(z5) 。

  • 输出层的计算

神经网络_第14张图片

图12 计算输出层

输出层只有一个神经元:neu6 。该层的输入为:

即:

因为该网络要解决的是一个二分类问题,所以输出层的激活函数也可以使用一个Sigmoid型函数,神经网络最后的输出为: f6(z6)。

2. 反向传播的计算

上一小节里我们已经了解了数据沿着神经网络前向传播的过程,这一节我们来介绍更重要的反向传播的计算过程。假设我们使用随机梯度下降的方式来学习神经网络的参数,损失函数定义为L(y,^y) ,其中 y是该样本的真实类标。使用梯度下降进行参数的学习,我们必须计算出损失函数关于神经网络中各层参数(权重w和偏置b)的偏导数。

3. BP算法流程

下面是基于随机梯度下降更新参数的反向传播算法:

输入: 训练集:D={(xi,yi)}, i=1,2,….,N

学习率:γ

训练回合数(epoch):T

初始化网络各层参数w(t) 和b(t)

for t=1 …T do

打乱训练集中样本的顺序

for i=1… N do

(1)获取一个训练样本,前馈计算每一层的输入 和输出

(2)利用公式*反向传播计算每一层的误差项

(3)利用公式和公式*计算每一层参数的导数

(4)更新参数:

4. 图解BP算法

我们依然使用如图9所示的简单的神经网络,其中所有参数的初始值如下:

输入的样本为(假设其真实类标为“1”):

第一层网络的参数为:

第二层网络的参数为:

第三层网络的参数为:

4.1 前向传播

我们首先初始化神经网络的参数,计算第一层神经元:

4.2 误差反向传播

接着计算第二层隐藏层的误差项,根据误差项的计算公式有:

最后是计算第一层隐藏层的误差项:

4.3 更新参数

上一小节中我们已经计算出了每一层的误差项,现在我们要利用每一层的误差项和梯度来更新每一层的参数,权重W和偏置b的更新公式如下:

通常权重W的更新会加上一个正则化项来避免过拟合,这里为了简化计算,我们省去了正则化项。上式中的 a 是学习率,我们设其值为0.1。参数更新的计算相对简单,每一层的计算方式都相同,因此本文仅演示第一层隐藏层的参数更新:

卷积神经网络综述

卷积介绍

我们尝试用一个简单的神经网络,来探讨如何解决这个问题。假设有4个输入节点和4个隐藏层节点的神经网络,如图所示:

神经网络_第15张图片

图1 全连接神经网络

每一个输入节点都要和隐藏层的 4 个节点连接,每一个连接需要一个权重参数 w:

神经网络_第16张图片

图2 一个输入节点向下一层传播

一共有 4 个输入节点,,所以一共需要 4*4=16个参数。

相应的每一个隐藏层节点,都会接收所有输入层节点:

神经网络_第17张图片

图3 每个隐藏层节点接收所有输入层节点输入

这是一个简化版的模型,例如手写数据集 MNIST 28 * 28 的图片,输入节点有 784 个,假如也只要一个隐藏层有 784 个节点,那么参数的个数都会是:784 * 784=614656,很明显参数的个数随着输入维度指数级增长。

因为神经网络中的参数过多,会造成训练中的困难,所以降低神经网络中参数的规模,是图像处理问题中的一个重要问题。

有两个思路可以进行尝试:

1.隐藏层的节点并不需要连接所有输入层节点,而只需要连接部分输入层。

如图所示:

神经网络_第18张图片

图4 改为局部连接之后的网络结构

每个隐藏层节点,只接受两个输入层节点的输入,那么,这个网络只需要 3 * 2 =6个连接。使用局部连接之后,单个输出层节点虽然没有连接到所有的隐藏层节点,但是隐藏层汇总之后所有的输出节点的值都对网络有影响。

2.局部连接的权重参数,如果可以共享,那么网络中参数规模又会明显的下降。如果把局部连接的权重参数当做是一个特征提取器的话,可以尝试将这个特征提取器用在其他的地方。

那么这个网络最后只需要 2 个参数,就可以完成输入层节点和隐藏层节点的连接。

这两个思路就是卷积神经网络中的稀疏交互和权值共享。

稀疏交互

在神经网络中,稀疏交互就是下一层节点只和上一层中的部分节点进行连接的操作。稀疏交互可以显著的降低神经网络中参数的数量。

左边是全连接方式,隐藏节点都需要所有的输入;右边是稀疏交互,隐藏层节点只接受一个区域内的节点输入。

./images/5-5.png

稀疏交互的实现

以 MNIST 数据集为例,来实现稀疏交互,并输出结果对应的图片。

MNIST 原始图片:

./images/Figure_1.png

为了进行局部连接,有两个重要的参数需要选择:

1.局部区域的大小

局部区域的大小,首先以 5 * 5 的局部区域为例:

2.局部特征的抽取次数

针对局部区域可以进行多次特征抽取,可以选择局部特征抽取的次数,首先以抽取 5 次为例。

3.步长

在确定局部区域大小之后,可以平滑的每次移动一个像素,也可以间隔 N 个像素进行移动。
如图:

./images/5-10.jpg

也可以使用不同的特征提取器对同一片区域,进行多次特征提取,如图所示:

4.边缘填充

在进行一次局部连接的过程中,如果不进行边缘填充,图像的维度将会发生变化,如图所示:

4 * 4 的图像,进行了 3 * 3 的局部连接,维度发生了变化。

./images/5-11.png

对于边缘的两种处理方法:

权值共享

降低网络中参数的个数,还有一个方法就是共享参数。每一组参数都可以认为是一个特征提取器,即使图像有一定的偏移,还是可以将相应的特征用同一组参数提取出来。

池化

除了之前的两种方式,在数据量很大,类比现实生活中事情纷繁复杂的时候,我们总是想抓住重点,在图像中,可以在一个区域选取一个重要的点。

一般是选择值最大的点,作为这一个区域的代表:

如图所示:


这个池化选取的是 2 * 2 的区域,留下值最大点,步长为 2。原来 4 * 4 的图片矩阵池化之后变成了 2 * 2 的图片矩阵。

RNN循环神经网络综述

前馈神经网络不考虑数据之间的关联性,网络的输出只和当前时刻网络的输入相关。然而在解决很多实际问题的时候我们发现,现实问题中存在着很多序列型的数据,例如文本、语音以及视频等。这些序列型的数据往往都是具有时序上的关联性的,既某一时刻网络的输出除了与当前时刻的输入相关之外,还与之前某一时刻或某几个时刻的输出相关。而前馈神经网络并不能处理好这种关联性,因为它没有记忆能力,所以前面时刻的输出不能传递到后面的时刻。

此外,我们在做语音识别或机器翻译的时候,输入和输出的数据都是不定长的,而前馈神经网络的输入和输出的数据格式都是固定的,无法改变。因此,需要有一种能力更强的模型来解决这些问题。

在过去的几年里,循环神经网络的实力已经得到了很好的证明,在许多序列问题中,例如文本处理、语音识别以及机器翻译等,循环神经网络都取得了显著的成绩。循环神经网络也正被越来越多的应用到其它领域。

在本节中,我们将会从最简单的循环神经网络开始介绍,通过实例掌握循环神经网络是如何解决序列化数据的,以及循环神经网络前向计算和参数优化的过程及方法。在此基础上我们会介绍几种循环神经网络的常用结构,即双向循环神经网络、深度循环神经网络以及递归神经网络。

此外,我们还会学习一类结构更为复杂的循环神经网络——门控循环神经网络,包括长短期记忆网络(LSTM)和门控制循环单元(GRU),这也是目前最常使用的两种循环神经网络结构。最后我们还会介绍一种注意力模型:Attention-based model,这是近两年来的研究热点。

1. 简单循环神经网络

简单循环网络(simple recurrent networks,简称SRN)又称为Elman network,是由Jeff Elman在1990年提出来的。Elman在Jordan network(1986)的基础上进行了创新,并且简化了它的结构,最终提出了Elman network。Jordan network和Elman network的网络结构如下图所示。

神经网络_第19张图片

图1 Jordan network(左)和Elman network(右)

从图1中可以很直观的看出,两种网络结构最主要的区别在于记忆单元中保存的内容不同。Jordan network的记忆单元中保存的是整个网络最终的输出,而Elman network的记忆单元只保存中间的循环层,所以如果是基于Elman network的深层循环神经网络,那么每一个循环的中间层都会有一个相应的记忆单元。

Jordan network和Elman network都可以扩展应用到深度学习中来,但由于Elman network的结构更易于扩展(Elman network的每一个循环层都是相互独立的,因此网络结构的设计可以更加灵活。另外,当Jordan network的输出层与循环层的维度不一致时还需要额外的调整,而Elman network则不存在该问题。),因此当前主流的循环神经网络都是基于Elman network的,例如我们后面会介绍的LSTM等。所以,通常我们所说的循环神经网络(RNN),默认指的就是Elman network结构的循环神经网络。本书中所提到的循环神经网络,如果没有特别注明,均指Elman network结构的循环神经网络。

2. RNN的基本结构

循环神经网络的基本结构如下图所示(注意:为了清晰,图中没有画出所有的连接线。):

神经网络_第20张图片

图2 循环神经网络的基本结构

关于循环神经网络的结构有很多种不同的图形化描述,但是其所表达的含义都与图2一致。将循环神经网络的结构与一般的全连接神经网络比较,我们会发现循环神经网络只是多了一个记忆单元,而这个记忆单元就是循环神经网络的关键所在。

从图2我们可以看到,循环神经网络的记忆单元会保存t时刻时循环层(既图2中的隐藏层)的状态,并在t时刻,将记忆单元的内容和t-1时刻的输入一起给到循环层。为了更直观的表示清楚,我们将循环神经网络按时间展开,如图3所示。

神经网络_第21张图片

图3 循环神经网络及其按时间展开后的效果图

左边部分是一个简化的循环神经网络示意图,右边部分是将整个网络按时间展开后的效果。在左边部分中,x是神经网络的输入,U是输入层到隐藏层之间的权重矩阵,W是记忆单元到隐藏层之间的权重矩阵,V是隐藏层到输出层之间的权重矩阵,s是隐藏层的输出,同时也是要保存到记忆单元中,并与下一时刻的x一起作为输入,o是神经网络的输出。

从右边的展开部分可以更清楚的看到,RNN每个时刻隐藏层的输出都会传递给下一时刻,因此每个时刻的网络都会保留一定的来自之前时刻的历史信息,并结合当前时刻的网络状态一并再传给下一时刻。

理论上来说,RNN是可以记忆任意长度序列的信息的,即RNN的记忆单元中可以保存此前很长时刻网络的状态,但是在实际的使用中我们发现,RNN的记忆能力总是很有限,它通常只能记住最近几个时刻的网络状态。

3. RNN的运算过程和参数更新

3.1 RNN的前向运算

在一个全连接的循环神经网络中,假设隐藏层只有一层。在时刻神经网络接收到一个输入,则隐藏层的输出为:

上式中,f(x)函数是隐藏层的激活函数,在TensorFlow中默认是tanh函数。参数U和W分别是输入层到隐藏层之间的权重矩阵和记忆单元到隐藏层之间的权重矩阵,参数b是偏置项。在神经网络刚开始训练的时候,记忆单元中没有上一个时刻的网络状态,这时候ht-1就是一个初始值。

在得到隐藏层的输出后,神经网络的输出为:

上式中,函数g是输出层的激活函数,当我们在做分类问题的时候,函数g通常选为Softmax函数。参数V是隐藏层到输出层的参数矩阵,参数b是偏置项。

我们先看看TensorFlow源码中关于RNN隐藏层部分的计算。这部分代码在TensorFlow源码中的位置是:https://github.com/tensorflow/tensorflow/tree/master/tensorflow/python/ops/rnn_cell_impl.py。

在rnn_cell_impl.py文件中定义了一个抽象类RNNCell,其它实现RNN的类都会继承这个类,例如BasicRNNCell、BasicLSTMCell以及GRUCell等。我们以BasicRNNCell类为例,所有继承了RNNCell的类都需要实现一个call方法,BasicRNNCell类中的call方法的实现如下:

def call(self, inputs, state):  
   """Most basic RNN: output = new_state  
                             = act(W * input + U * state + B)."""  
   gate_inputs = math_ops.matmul(  
       array_ops.concat([inputs, state], 1), self._kernel)  
   gate_inputs = nn_ops.bias_add(gate_inputs, self._bias)  
   output = self._activation(gate_inputs)  
   return output, output

从上面的TensorFlow源码里可以看到,TensorFlow隐藏层的计算结果即是该层的输出,同时也作为当前时刻的状态,作为下一时刻的输入。第2、3行的注释说明了“call”方法的功能:output = new_state = act(W * input + U * state + B),其实就是实现了我们前面给出的公式6.1。第5行代码中的“self._kernel”是权重矩阵,第6行代码中的“self._bias”是偏置项。

这里有一个地方需要注意一下,这段代码在实现 W * input + U * state + B 时,没有分别计算W * input和U * state,然后再相加,而是先用“concat”方法,将前一时刻的状态“state”和当前的输入“inputs”进行拼接,然后用拼接后的矩阵和拼接后的权重矩阵相乘。可能有些读者刚开始看到的时候不太能理解,其实效果是一样的,我们看下面这个例子:

我们有四个矩阵:a、b、c和d:

假设我们想要计算,则有:

如果我们把矩阵a和b、c和d先分别拼接到一起,得到e和f两个矩阵:

再来计算,会得到同样的结果:

下面我们用一段代码实现循环神经网络中完整的前向计算过程。

import numpy as np  

# 输入数据,总共三个time step  
dataset = np.array([[1, 2], [2, 3], [3, 4]])  

# 初始化相关参数  
state = [0.0, 0.0]            # 记忆单元  

np.random.seed(2)          # 给定随机数种子,每次产生相同的随机数  
W_h = np.random.rand(4, 2)  # 隐藏层权重矩阵  
b_h = np.random.rand(2)     # 隐藏层偏置项  

np.random.seed(3)  
W_o = np.random.rand(2)    # 输出层权重矩阵  
b_o = np.random.rand()      # 输出层偏置项  

for i in range(len(dataset)):  
  # 将前一时刻的状态和当前的输入拼接  
  value = np.append(state, dataset[i])  

  # 隐藏层  
  h_in = np.dot(value, W_h) + b_h  # 隐藏层的输入  
  h_out = np.tanh(h_in)           # 隐藏层的输出  
  state = h_out                  # 保存当前状态  

  # 输出层  
  y_in = np.dot(h_out, W_o) + b_o  # 输出层的输入  
  y_out = np.tanh(y_in)           # 输出层的输出(即最终神经网络的输出)  

  print(y_out)

上面代码里所使用的RNN结构如下:

神经网络_第22张图片

图4 代码中使用的RNN网络结构

在上面的示例代码中,我们用了一个如图4所示的简单循环神经网络。该网络结构输入层有两个单元,隐藏层有两个神经元,输出层一个神经元,所有的激活函数均为tanh函数。在第四行代码中我们定义了输入数据,总共三个time-step,每个time-step处理一个输入。我们将程序运行过程中各个参数以及输入和输出的值以表格的形式展示如下(读者可以使用下表的数据验算一遍RNN的前向运算,以加深印象):

3.2 RNN的参数更新

循环神经网络中参数的更新主要有两种方法:随时间反向传播(backpropagation through time,BPTT)和实时循环学习(real-time recurrent learning,RTRL)。这两种算法都是基于梯度下降,不同的是BPTT算法是通过反向传播的方式来更新梯度,而RTRL算法则是使用前向传播的方式来更新梯度。目前,在RNN的训练中,BPTT是最常用的参数更新的算法。

BPTT算法和我们在前馈神经网络上使用的BP算法本质上没有任何区别,只是RNN中的参数存在时间上的共享,因此RNN中的参数在求梯度的时候,存在沿着时间的反向传播。所以RNN中参数的梯度,是按时间展开后各级参数梯度的总和。

4. 多层循环神经网络(深度循环神经网络)

多层循环神经网络是最容易想到的一种变种结构,它的结构也很简单,就是在基本的循环神经网络的基础上增加了隐藏层的数量。

神经网络_第23张图片

图5 多层循环神经网络结构

多层循环神经网络按时间展开后,每一层的参数和基本的循环神经网络结构一样,参数共享,而不同层的参数则一般不会共享(可以类比CNN的网络结构)。和基本结构的循环神经网络相比,多层循环神经网络的泛化能力更强,不过训练的时间复杂度和空间复杂度也更高。

5. 双向循环神经网络

无论是简单循环神经网络还是深度循环神经网络,网络中的状态都是随着时间向后传播的,然而现实中的许多问题,并不都是这种单向的时序关系。例如在做词性标注的时候,我们需要结合这个词前后相邻的几个词才能对该词的词性做出判断,这种情况就需要双向循环神经网络来解决问题。

神经网络_第24张图片

图6 双向循环神经网络结构

双向循环神经网络可以简单的看成是两个单向循环神经网络的叠加,按时间展开后,一个是从左到右,一个是从右到左。双向循环神经网络的计算与单向的循环神经网络类似,只是每个时刻的输出由上下两个循环神经网络的输出共同决定。双向循环神经网络也可以在深度上进行叠加:

神经网络_第25张图片

图7 深度双向循环神经网络结构

6. 递归神经网络

递归神经网络(recursive neural network,RNN)是循环神经网络的又一个变种结构,看它们的名称缩写,很容易将两者混淆(通常我们说的RNN均特指recurrent neural network,同时也不建议将recurrent neural network说成是递归神经网络。一般默认递归神经网络指的是recursive neural network,而循环神经网络指的是recurrent neural network)。我们前面所介绍的循环神经网络是时间上的递归神经网络,而这里所说的递归神经网络是结构上的递归。递归神经网络相较于循环神经网络有它一定的优势,当然这个优势只适用于某些特定场合。目前递归神经网络在自然语言处理和计算机视觉领域已经有所应用,并获得了一定的成功,但是由于其对输入数据的要求相对循环神经网络更为苛刻,因此并没有被广泛的应用。

神经网络_第26张图片

图8 递归神经网络结构示意图

7. 长期依赖问题及其优化

7.1 长期依赖问题

什么是长期依赖?

我们知道循环神经网络具有记忆能力,凭着它的记忆能力,能够较好的解决一般的序列问题,这些序列问题的数据内部基本上都存在着一定的依赖性,例如我们在前面提到过的词性标注的问题。在有些现实问题中,数据间的依赖都是局部的、较短时间间隔的依赖。还是以词性标注为例,判断一个词是动词还是名词,或者是形容词之类,我们往往只需要看一下这个词前后的两个或多个词就可以做出判断,这种依赖关系在时间上的跨度很小。

神经网络_第27张图片

图9 时间跨度较小的依赖关系示意图

如上图所示,时刻网络的输出(上图中到是隐藏层的输出)除了与当前时刻的输入相关之外,还受到和时刻网络的状态影响。像这种依赖关系在时间上的跨度较小的情况下,RNN基本可以较好地解决,但如果出现了像下图所示的依赖情况,就会出现长期依赖问题:梯度消失和梯度爆炸

神经网络_第28张图片

图10 时间跨度较大的依赖关系示意图

什么是梯度消失和梯度爆炸?

图11是一个较为形象的描述,在深层的神经网络中,由于多个权重矩阵的相乘,会出现很多如图所示的陡峭区域,当然也有可能会出现很多非常平坦的区域。在这些陡峭的地方,Loss函数的倒数非常大,导致最终的梯度也很大,对参数进行更新后可能会导致参数的取值超出有效的取值范围,这种情况称之为梯度爆炸。而在那些非常平坦的地方,Loss的变化很小,这个时候梯度的值也会很小(可能趋近于0),导致参数的更新非常缓慢,甚至更新的方向都不明确,这种情况称之为梯度消失。长期依赖问题的存在会导致循环神经网络没有办法学习到时间跨度较长的依赖关系。

神经网络_第29张图片

图11 导致梯度爆炸的情况

正如上面所说,长期依赖问题普遍存在于层数较深的神经网络之中,不仅存在于循环神经网络中,在深层的前馈神经网络中也存在这一问题,但由于循环神经网络中循环结构的存在,导致这一问题尤为突出,而在一般的前馈神经网络中,这一问题其实并不严重。

值得注意的是,在介绍激活函数的时候我们其实已经提到过梯度消失的问题,这是由于Sigmoid型的函数在其函数图像两端的倒数趋近于0,使得在使用BP算法进行参数更新的时候会出现梯度趋近于0的情况。针对这种情况导致的梯度消失的问题,一种有效的方法是使用ReLU激活函数。但是由于本节所介绍的梯度消失的问题并不是由激活函数引起的,因此使用ReLU激活函数也无法解决问题。下面我们来看一个简单的例子。

神经网络_第30张图片

图12 参数W在循环神经网络中随时间传递的示意图

如上图所示,我们定义一个简化的循环神经网络,该网络中的所有激活函数均为线性的,除了在每个时间步上共享的参数W以外,其它的权重矩阵均设为1,偏置项均设为0。假设输入的序列中除了的值为1以外,其它输入的值均为0,则根据之前讲解RNN的原理内容,可以知道:

神经网络_第31张图片

最终可以得到,神经网络的输出是关于权重矩阵W的指数函数。当W的值大于1时,随着n的增加,神经网络最终输出的值也成指数级增长,而当W的值小于1时,随着n的值增加,神经网络最终的输出则会非常小。这两种结果分别是导致梯度爆炸和梯度消失的根本原因。

从上面的例子可以看到,循环神经网络中梯度消失和梯度爆炸问题产生的根本原因,是由于参数共享导致的。

7.2 长期依赖问题的优化

对于梯度爆炸的问题,一般来说比较容易解决。我们可以用一个比较简单的方法叫做“梯度截断”,“梯度截断”的思路是设定一个阈值,当求得的梯度大于这个阈值的时候,就使用某种方法来进行干涉,从而减小梯度的变化。还有一种方法是给相关的参数添加正则化项,使得梯度处在一个较小的变化范围内。

梯度消失是循环神经网络面临的最大问题,相较于梯度爆炸问题要更难解决。目前最有效的方法就是在模型上做一些改变,这就是我们下一节要介绍的门控循环神经网络。

8. 门控循环神经网络

门控循环神经网络在简单循环神经网络的基础上对网络的结构做了调整,加入了门控机制,用来控制神经网络中信息的传递。门控机制可以用来控制记忆单元中的信息有多少需要保留,有多少需要丢弃,新的状态信息又有多少需要保存到记忆单元中等。这使得门控循环神经网络可以学习跨度相对较长的依赖关系,而不会出现梯度消失和梯度爆炸的问题。如果从数学的角度来理解,一般结构的循环神经网络中,网络的状态和之间是非线性的关系,并且参数W在每个时间步共享,这是导致梯度爆炸和梯度消失的根本原因。门控循环神经网络解决问题的方法就是在状态和之间添加一个线性的依赖关系,从而避免梯度消失或梯度爆炸的问题。

8.1 长短期记忆网络(LSTM)

长短期记忆网络(Long Short-term Memory,简称LSTM)的结构如图13所示,LSTM的网络结构看上去很复杂,但实际上如果将每一部分拆开来看,其实也很简单。在一般的循环神经网络中,记忆单元没有衡量信息的价值量的能力,因此,记忆单元对于每个时刻的状态信息等同视之,这就导致了记忆单元中往往存储了一些无用的信息,而真正有用的信息却被这些无用的信息挤了出去。LSTM正是从这一点出发做了相应改进,和一般结构的循环神经网络只有一种网络状态不同,LSTM中将网络的状态分为内部状态和外部状态两种。LSTM的外部状态类似于一般结构的循环神经网络中的状态,即该状态既是当前时刻隐藏层的输出,也是下一时刻隐藏层的输入。这里的内部状态则是LSTM特有的。

在LSTM中有三个称之为“门”的控制单元,分别是输入门(input gate)、输出门(output gate)和遗忘门(forget gate),其中输入门和遗忘门是LSTM能够记忆长期依赖的关键。输入门决定了当前时刻网络的状态有多少信息需要保存到内部状态中,而遗忘门则决定了过去的状态信息有多少需要丢弃。最后,由输出门决定当前时刻的内部状态有多少信息需要输出给外部状态。

神经网络_第32张图片

图13 单个时间步的LSTM网络结构示意图

从上图我们可以看到,一个LSTM单元在每个时间步都会接收三个输入,当前时刻的输入,来自上一时刻的内部状态以及上一时刻的外部状态。其中,和同时作为三个“门”的输入。为Logistic函数,。

接下来我们将分别介绍LSTM中的几个“门”结构。首先看一下输入门,如图2所示:

从上图我们可以看到,一个LSTM单元在每个时间步都会接收三个输入,当前时刻的输入,来自上一时刻的内部状态以及上一时刻的外部状态。其中,xt和ht-1同时作为三个“门”的输入。为Logistic函数,。

接下来我们将分别介绍LSTM中的几个“门”结构。首先看一下输入门,如图14所示:

神经网络_第33张图片

图14 LSTM的输入门结构示意图

LSTM中也有类似于RNN(这里特指前面介绍过的简单结构的循环神经网络)的前向计算过程,如图14,如果去掉输入门部分,剩下的部分其实就是RNN中输入层到隐藏层的结构,“tanh”可以看作是隐藏层的激活函数,从“tanh”节点输出的值为:

上式中,参数的下标“c”代表这是“tanh”节点的参数,同理,输入门参数的下标为“i”,输出门参数的下标为“o”,遗忘门参数的下标为“f”。上式与简单结构循环神经网络中隐藏层的计算公式一样。在LSTM中,我们将“tanh”节点的输出称为候选状态。

输入门是如何实现其控制功能的?输入门的计算公式如下:

由于为Logistic函数,其值域为[0,1],因此输入门的值就属于[0,1]。LSTM将“tanh”节点的输出(即候选状态)乘上输入门的值后再用来更新内部状态。如果i的值趋向于0的话,那么候选状态就只有极少量的信息会保存到内部状态中,相反的,如果i的值趋近于1,那么候选状态就会有更多的信息被保存。输入门就是通过这种方法来决定保存多少中的信息,i值的大小就代表了新信息的重要性,不重要的信息就不会被保存到内部状态中。

再来看遗忘门,如图15所示:

神经网络_第34张图片

图15 LSTM的遗忘门结构示意图

遗忘门的计算公式如下:

和输入门是同样的方法,通过f_t的值来控制上一时刻的内部状态c_(t-1)有多少信息需要“遗忘”。当f_t的值越趋近于0,被遗忘的信息越多。同样的原理,我们来看“输出门”,如图16所示。

神经网络_第35张图片

图16 LSTM的输出门结构示意图

输出门的计算公式如下:

当o_t的值月接近于1,则当前时刻的内部状态c_t就会有更多的信息输出给当前时刻的外部状态h_t。

以上就是LSTM的整个网络结构以及各个“门”的计算公式。通过选择性的记忆和遗忘状态信息,使得LSTM要比一般的循环神经网络能够学习更长时间间隔的依赖关系。根据不同的需求,LSTM还有着很多不同的变体版本,这些版本的网络结构大同小异,但都在其特定的应用中表现出色。

8.2 门控制循环单元(GRU)

门控制循环单元(gated recurrent unit,GRU)网络是另一种基于门控制的循环神经网络,GRU的网络结构相比LSTM要简单一些。GRU将LSTM中的输入门和遗忘门合并成了一个门,称为更新门(update gate)。在GRU网络中,没有LSTM网络中的内部状态和外部状态的划分,而是通过直接在当前网络的状态h_t和上一时刻网络的状态h_(t-1)之间添加一个线性的依赖关系,来解决梯度消失和梯度爆炸的问题。

神经网络_第36张图片

图17 单个时间步的GRU网络结构示意图

在GRU网络中,更新门用来控制当前时刻输出的状态h_t中要保留多少历史状态h_(t-1),以及保留多少当前时刻的候选状态h ̃_t。更新门的计算公式如下:

如图17所示,更新门的输出分别和历史状态h_(t-1)以及候选状态h ̃_t进行了乘操作,其中和h ̃_t相乘的是1-z_t。最终当前时刻网络的输出为:

重置门的作用是决定当前时刻的候选状态是否需要依赖上一时刻的网络状态以及需要依赖多少。从图17可以看到,上一时刻的网络状态h_t先和重置门的输出r_t相乘之后,再作为参数用于计算当前时刻的候选状态。重置门的计算公式如下:

r_t的值决定了候选状态h ̃_t对上一时刻的状态h_(t-1)的依赖程度,候选状态h ̃_t的计算公式如下:

其实当z_t的值为0且r_t的值为1时,GRU网络中的更新门和重置门就不再发挥作用了,而此时的GRU网络就退化成了简单循环神经网络,因为此时有:

神经网络_第37张图片

你可能感兴趣的:(大数据,神经网络)