3.3 三层神经网络

在研究Exp类时,我们以标量作为数据的类型进行考虑,而TF的基本数据类型是矩阵。标量和向量分别被认为是0维和1维矩阵。与标量一样,矩阵的运算主要有算术运算、关系运算、逻辑运算、条件运算和函数等。这一节,我们将首先介绍矩阵乘法运算,然后给出三层神经网络的定义,最后说明三层神经网络可以拟合任意函数。

3.3.1 神经元网络训练算法

前面章节中我们已经学会了用迭代法、GD法和BP算法计算平方根,但是这个方法有两个问题。第一,每计算一个平方根都要进行很多次迭代;第二,由于每一次迭代都是基于上一次迭代的结果,所以计算无法并行化。有没有办法用少量的有限次数的并行计算解决问题呢?有的,这就是目前深度学习最常用的方法之一——基于样本的神经元网络训练算法(简称为样本训练算法)。

首先,明确几个概念。依赖关系图是一个数学概念,计算图是TF对这个概念的实现。神经元网络(简称为网络,以下同)就是依赖关系图在深度学习中的等价概念。依赖关系图中的结点或者说函数就是神经元。所以我们有时用图表示神经元网络,有时用复合函数表示神经元网络,因为它们是等价的。下面的算法就是一个例子。

算法3-1 基于样本的神经元网络训练算法

1)搭建神经元网络yfxθ),其中xy分别是网络的输入集合和输出集合,θ是要训练的参数的集合。

2)准备样本集合S={(xiyi)|i=1,2,3,…,m},其中m是样本个数。

3)构建一个以求最小值为目的的目标函数,例如L=∑iyifxi))2

4)选择S的一个子集S′,按照算法2-1求L的最小值,并优化θ中的每个参数。

5)重复4),直到满足结束条件。

样本训练算法与迭代求解算法(算法2-1)的最大区别在于训练的目的不同。前者训练的目的是优化参数集合θ。训练完毕之后,使用网络时不必再更新θ中的参数,从而达到减少计算量的目的。后者并不存在训练过程,而是直接通过迭代计算确定目标函数的最优解。两者的联系在于,在训练阶段,前者利用后者对参数进行了优化。

下面我们还是通过求解平方根这个例子来说明样本训练算法的使用。

3.3.2 线性变换和激活函数

根据算法3-1,我们首先要建立求平方根的网络。输入是非负数x,输出是x的平方根y。在xy之间建立100个结点,分别与xy相连,构成一个简单的神经元网络,如图3-2所示。

图3-2 简单的神经元网络

接着,该如何定义函数zii=1,2,3,…,100)以及ziy之间的关系呢?最简单的办法就是使用线性变换和激活函数:

zixai+bii=1,2,3,…,100)

y=relu(zici+di=1,2,3,…,100)

其中aibicid都是需要我们利用GD法进行优化的参数。relu()是一个激活函数(Activation Function),全称为线性矫正单元(Rectified Linear Unit)。relu激活函数定义如下:

relu()函数的图像如图3-3所示。根据定义,我们发现relu(x)≡max(0,x)。如果不使用激活函数,那么,从xy不过是两个线性变换的组合;而任意有限次线性变换的组合仍然是线性变换。由于我们要求的平方根是非线性的,也就是说,不存在mn使得(mx+n2x对任意x都成立,所以有必要使用激活函数以实现非线性变换。除了relu()之外,还有很多其他激活函数,可参见后面章节。

图3-3 relu()函数图像

下面是用样本训练算法求解平方根的主程序代码:

接下来,我们要实现Tensors。在绘制计算图时(例如图3-2),我们只考虑一个样本的输入x和输出y;但在TF和几乎所有的深度学习框架中,都按照批次(Batch)来考虑样本。一批中含有多个样本,假设样本输入和输出的形状分别是[mn]和[pqr],则模型的实际输入和输出形状分别是[smn]和[spqr],其中第一个维度s表示样本的个数。这是因为深度学习框架几乎都是基于矩阵运算的,一批多个样本有利于并行计算,提高训练速度。

既然采用矩阵运算,图3-2所示的计算图可以简化为:

ZXA+B

Y=relu(ZC+D

其中X、Y分别是输入向量和输出向量,Z是中间变量,A、B、C、D是模型的参数。所谓训练模型,就是优化这些参数。式中的乘法(包括∗)和加法分别是矩阵乘法和矩阵加法。relu()函数是可广播的,所以relu(Z)表示对矩阵Z中的每一个元素执行relu()后得到的结果。

综上所述,我们对Tensors类的实现如下:

代码3-12第4行tf.placeholder()的第二个参数是[None],表示这个占位符只有一个维度(也就是说它是一个向量),且这个维度的长度不确定。形状[None,3,None]表示第一个和第三个维度的长度不确定。TF中绝大部分运算都允许矩阵有不确定的维度,只需在运行时把数据的真正维度代入即可。但有一个例外,变量的维度必须都是确定的。这是因为变量需要在内存和GPU中占据空间,所以TF必须准确地知道它到底有多大。

如果张量的某个维度是不确定的,则在运算后对应位置处的维度要用-1表示。例如代码3-12第5行tf.reshape(self.x,[-1,1])的第一个维度是-1,这是因为self.x的形状是[None],说明它是一个长度不确定的向量。当把它整形为二维矩阵时,如果确定它的第二个维度是1,则它的第一个维度就无法确定,于是就用-1代替。同样的例子在后面所写的大部分代码中都会出现。

在SqrtModel类中,方法train()被用来对模型进行训练,predict()被用来使用模型。它们的实现如下:

运行p03_ 12号程序得到的结果是:

sqrt(0.0555)=0.2316

sqrt(0.1297)=0.3098

sqrt(0.1994)=0.3836

sqrt(0.4350)=0.6314

sqrt(0.6073)=0.7866

sqrt(0.7146)=0.8607

sqrt(0.9207)=0.9762

sqrt(1.1832)=1.1022

sqrt(1.6513)=1.2870

sqrt(1.6923)=1.3017

sqrt(1.8790)=1.3690

sqrt(1.8974)=1.3756

sqrt(2.0739)=1.4386

sqrt(2.1878)=1.4781

sqrt(2.2408)=1.4957

sqrt(2.3656)=1.5356

sqrt(2.6307)=1.6157

sqrt(2.6467)=1.6205

sqrt(2.8969)=1.6961

sqrt(2.9273)=1.7053

由于样本仅包含了区间[0,3)里的数据,所以测试时,也只能计算这个区间上数据的平方根。当试着求解其他区间的平方根时,会发现结果误差比较大。出现这个现象的原因我们后面讲三层神经网络时会解释。

3.3.3 矩阵乘法和全连接

代码3-12的第6、7、8三行是:

这三行代码的实质含义是实现线性变换ZXA+B。这种线性变换在深度学习中被称为全连接(Full Connection,FC)。由于全连接操作比较常见,TF用一个函数tf.layers.dense()帮我们实现了全连接。例如上述三行可以用一行代替:

z=tf.layers.dense(x,100)

假设X的形状是[n,-1],经过上述操作后,Z的形状是[n,100]。由于n通常表示样本的个数,所以全连接操作的作用是改变样本向量的长度。

如果在全连接之后要执行激活操作,例如:

z=tf.nn.relu(tf.layers.dense(x,100))

可以简化为:

z=tf.layers.dense(x,100,tf.nn.relu)

或者

z=tf.layers.dense(x,100,′relu′)

如果不需要考虑偏置B,则把参数use_ bias置为False即可:

z=tf.nn.relu(tf.layers.dense(x,100,use_ bias=False))

即Z=relu(XA)。

3.3.4 激活函数

TF中的relu函数族中一共有6个激活函数,分别是:

1)tf.nn.relu()。这是最传统的relu激活函数,公式如下:

2)tf.nn.leaky_ relu()。公式如下:

leaky_ relu(x)=a>0是一个超参数

3)tf.nn.elu()。公式如下:

4)tf.nn.selu()。公式如下:

selu(x)=是一个可以训练的变量

5)tf.nn.relu6。限定最大值为6的relu激活函数,公式如下:

6)tf.nn.crelu()。公式如下:

除了crelu()外,其他5个函数的图像如图3-4所示。elu()、relu()是selu()分别在a=1和a=0的特殊情形,所以凡是使用elu()、relu()的情形一般都可以用selu()代替。crelu()实际上是把输入的x值按照正负两种情况并列处理,使得负值也有机会参与前向传播和梯度下降。所以,relu()、leaky_relu()、relu6()都是crelu()的特殊情形。注意,除了crelu()外,其他5个函数都不会改变输入数据的形状,而crelu()会增加一个长度为2的维度。

除了relu函数族以外,还有两个激活函数tf.sigmoid()和tf.tanh(),后者又叫双曲正切函数。sigmoid()和tanh()函数公式如下:

它们的图像都是S型,如图3-5所示,不同的是值域。sigmoid把(-∞,+∞)上的数映射到(0,1)区间,tanh把(-∞,+∞)上的数映射到(-1,1)区间。这两个函数的性质如下:

图3-4 relu函数族的图像

图3-5 sigmoid函数和tanh函数

1)tanh(x)=2 sigmoid(2x)-1

2)sigmoid(x)=

3)sigmoid(0)=0.5

4)tanh(0)=0

5)sigmoid′(x)=sigmoid(x)(1-sigmoid(x))

6)0<sigmoid′(x)≤,且x=0时取最大值

7)tanh′(x)=1-tanh2x

8)0<tanh′(x)≤1,且x=0时取最大值

这些性质使得sigmoid()适合把数据映射为一个概率,tanh()适合把数据映射到(-1,1)区间从而有利于模型的收敛;但两者都不太适合做激活函数,尤其是sigmoid()。这是因为sigmoid()的导数的最大值是0.25,所以有:

x≤0.25▽y,其中y=sigmoid(x

这意味着梯度每经过一次sigmoid()就会缩小3/4。经过的次数越多,缩小的程度就越呈指数增长。这就有可能导致梯度过小而消失,从而无法对模型可训练参数进行优化。tanh()也有类似情况。所以现在一般流行用relu函数族中的函数做激活函数。

3.3.5 全连接和Relu的梯度

根据relu()函数的定义,我们可知relu()的导数为:

上式中,我们约定x=0时的导数为1。这是因为在GD法中,偏导数代表了因变量相对于自变量的变化趋势,GD法根据变化趋势决定增加或者减少当前变量的值,并用学习率限制了步长。这就使得我们完全可以自行定义函数在不可求导点的导数,从而使GD法可以正常运行下去。

由于relu()的导数是分段确定的,所以,如果一个神经网络中含有relu()这样的导数分段的函数,则网络上relu()所依赖的结点的梯度也是分段确定的。例如yx3z=relu(y),由于relu依赖于x,所以zx的导数也是分段的:

把根据x的不同计算出来的不同的y分别代入上式,就可以计算不同情况下x的梯度。

对于全连接ZXA+B,有以下公式成立:

设有:

ZXA+B

Y =relu(Z

求:▽X、▽A和▽B

解:由计算得

所以即正数对应的导数为1,负数对应的导数为0。

在GD法中,有了XAB的梯度,我们就可以根据学习步长分别调整XAB的值。GD法就是根据这样的方法优化参数的。

3.3.6 求正弦

如果仔细研究代码3-12和代码3-13,会发现无论Tensors类还是SqrtModel类,它们与平方根这个概念都没有什么关系。这意味着什么?意味着Tensors和SqrtModel其实是通用的。基于这个考虑,我们先把Tensors类中的线性变换改为全连接,把中间层的长度从常数100改为变量middle_ units,再把SqrtModel改名为Model,最后把主程序稍作调整,即可得到求解正弦的程序:

在上面的程序中,我们还引入了math包和pyplot包。math被用来获取π的值,pyplot被用来作图。

得到的结果如图3-6a所示。正弦曲线和预测得到的曲线的重合度“看起来”还是比较高的。但是如果把局部放大,就会看到两者还是有区别的,如图3-6b所示。

增大epoches参数或者缩小学习步长lr,可以进一步加大两者的重合度,但是由于步长的限制,我们不可能无限地加大重合度。另外,过小的学习步长将导致网络收敛缓慢。所以,选取合适的epoches和lr就显得很重要。读者可以通过用不同的参数组合多做练习,以便摸清这些超参的最优组合。除此之外,还可以使用其他优化器,如AdamOptimizer。

图3-6 正弦曲线和预测值对比

3.3.7 BGD、SGD和MBGD

样本参与训练的方法有3种:

1)BGD(Batch Gradient Descent),批量梯度下降法,即一次训练所有样本。前面求正弦和平方根的程序就是这样的。

2)SGD(Stochastic Gradient Descent),随机梯度下降法,即一次仅训练一个样本。

3)MBGD(Mini Batch Gradient Descent),小批量梯度下降法。这是介于BGD和SGD之间的方法,一次训练多个样本。由于在实际项目中样本数量众多,我们很难使用BGD,但一次仅训练一个样本效率又实在太低,且容易出现过拟合,所以MBGD就成为比较常用的方法。

BGD和MBGD是不是等价于SGD?换句话说,如果BGD或MBGD一次训练10个样本,那么在同样的情况下,是不是等价于用SGD训练10次?答案是否定的。BGD或MBGD一次训练10个样本,模型的参数就用这10个样本的梯度的平均值进行优化。而用SGD训练10次,即对每个参数优化10次,且每次优化都是在上一次优化的基础上进行的,结果当然不一样。

3.3.8 三层神经网络模型

本节最核心的内容是三层神经网络。前面求平方根和正弦的网络都是三层的,包括输入层、中间层和输出层。我们还发现,这样的网络既可以用来训练求平方根,也可以用来求正弦。那么这种通用性是不是放之四海皆准呢?更进一步,我们可不可以用一个模型同时求正弦和平方根呢?

三层神经网络可以表达任意函数,这个定理在1989年就已经被G.Cybenko证明(参见http://www.dartmouth.edu/~gvc/Cybenko_MCSS.pdf);但是那是一个非常数学化的证明,一般人阅读起来比较吃力。从本节开始,我们将给出一个形象的、容易理解的说明(而非证明)。我们会发现,神经网络的表达能力是有限的,超过极限,增加深度和神经元个数并不能提高网络的表达能力。我们还将学习样本数量与神经元数量之间的联系,理解过拟合的实质以及根本解决办法,发现最小样本区域的概念,发现神经元网络的内在不稳定性,理解网络在样本空间之外表现不佳的原因。

所谓三层神经网络,就是除了输入层和输出层,仅有一个中间层(又称为隐藏层)的网络,结构如图3-7所示。其中各层之间的连接都是全连接,中间层带一个激活函数(通常是relu())。这样的网络就可以被用来模拟任意一个一元或者多元函数,甚至可以同时模拟多个多元函数。

图3-7 三层神经网络

但是请注意,第一,这里所说的函数是连续函数。对于非连续函数,只要它是分段连续的,我们的证明在每一段上仍然是有效的。第二,函数的定义域是有界的。也就是说,我们不考虑下界是负无穷,或者上界是正无穷的情况。这个限制对实际应用几乎没有影响。因为一般来说,我们不太可能在一个上界或者下界完全不受限的范围内使用网络。第三,中间层的激活函数是relu()。如果使用其他激活函数,如leaky_relu()、sigmoid(),结论是一样的。其证明是可以类推的,本文不再赘述。

我们将首先从一元函数讲起,然后再扩展到多元函数,最后扩展到多个多元函数。