7.2 多层神经网络建模与模型的还原保存

> 两层神经网络

这是一个两层神经网络的模型,在第一个隐藏层保持256个节点不变,第二个隐藏层放64个神经元。

> 构建模型

首先定义了两个变量H1_NN和H2_NN,分别代表第一隐藏层和第二隐藏层神经元的个数。

输入层到第一隐藏层,我们在单隐藏层的时候已经构建过了。它的权值W1,形为784和H1_NN。因为输入层的节点有784个,它自己有256个节点,所以总共有784×256个权值。

这里生成随机数的方式做了改变,用了tf.truncated_normal()函数,从截断的正态分布中输出随机数。stddev是标准差,如果随机数的标准差大于两倍的0.1就会重新生成,使得W1的值相对比较均匀。在多层神经网络中最好还是采用这种随机的方式。

偏置项b1的值就是第一隐藏层的节点个数,也就是256。

第一隐藏层到第二隐藏层的定义,跟上一层的定义类似,权值W2只要修改形就可以了,分别是H1_NN和H2_NN。因为第一隐藏层的节点数为256,第二隐藏层的节点数为64。偏置项b2的值就是第二隐藏层的节点个数,也就是64。

第二隐藏层到输出层也是同理,权值W3的形为H2_NN和10。因为第二隐藏层的节点数为64,输出层的节点数为10。偏置项b3的值就是输出层的节点个数,也就是10。

接下来定义计算结果:

对于Y1,也就是第一隐藏层,是用特征值x和相应权值W1的叉乘后的结果加上偏置项b1,最后用ReLU激活函数后得到的。

对于Y2,也就是第二隐藏层,是用第一隐藏层的结果Y1和相应权值W2的叉乘后的结果加上偏置项b2,最后用ReLU激活函数后得到的。

对于输出层,前向计算是由第二隐藏层的结果Y2和相应权值W3的叉乘后的结果加上偏置项b3得到的,预测结果还需要对前向计算的结果用softmax激活函数。

除了模型定义,其他方面跟单层神经网络没有区别。

> 训练结果

可以看到,第一次训练就达到了95.86%,但是它的运算速率会比刚刚慢一些,因为它比刚才的模型多了一层,里面参数的数量又增加了许多。

后面迭代的结果,基本在96%-97%左右徘徊,说明模型已经收敛了。

> 评估模型

测试后的准确率只有96.9%,这个效果反而不如单层神经网络。所以大家要知道,多层效果不一定就比单层网络效果好,还是要通过对超参数的调整来优化模型。

> 三层神经网络

刚刚两层的效果并没有特别好,那么三层会怎么样呢?

这是一个多层神经网络的示意图,中间代表可以有很多隐藏层。

我们再来演示一下,如何构建三层的神经网络。

> 构建模型

在刚刚两层神经网络的基础上,我们增加了一个变量H3_NN,并把神经元的节点数设为32。

每两层之间的定义关系跟刚才一致,根据权值所连接的两层,确定输入的节点数和输出的节点数,设置相应的参数。

接下来定义计算结果:

定义的方式也跟刚才一致,最后的输出要分成两步:先得到前向计算,再进行softmax。

> 训练结果

最后训练模型的准确率达到了97.4%。

> 评估模型

这一次的测试结果达到了97.44%,算是目前最好的一次结果。

> 重构建模过程

在之前的建模过程中,首先需要根据隐藏层的数量构建相应的模型,层数越多,定义的W、b、Y就越多。既然构建每层隐藏层的时候,都需要定义相关的W、b以及计算结果Y,我们能否重新优化构造一下呢?

> 定义全连接层函数

有了这个函数,以后定义全连接层的神经网络时,不管有多少层,对于每一层的定义都可以通过这个函数来解决,使得整个建模过程看起来更加直观,条理也更加清晰。

下面来具体看一下这个函数:

首先定义了一个函数fcn_layer,fcn表示全连接神经网络,layer表示一层。它有四个参数:第一个参数inputs表示这一层网络的输入数据;第二个参数input_dim表示输入的神经元数量;第三个参数output_dim表示输出的神经元数量,即这一层的神经元数量;第四个参数activation表示激活函数,它的缺省值为None,即可以不带激活函数,如果带了激活函数,在后面就会用到。

接下来,定义了权值W,它是TensorFlow的一个变量(tf.Variable),它的形为输入的神经元数量以及输出的神经元数量,以截断的正态分布随机数来初始化,它的标准差为0.1。然后,以0来初始化偏置项b,它的形为输出的神经元数量。

然后做了一个计算,把输入数据和权值做了矩阵叉乘,再加上偏置项b。

再接下来是是否使用激活函数的判断,如果不采用激活函数,那么函数的输出就是刚刚的计算结果;如果有激活函数,那么刚刚的计算结果还需要通过激活函数后再输出。

通过这个函数,可以对之前所构建的三个神经网络的模型进行修改:

> 单隐层模型

> 构建输入层

首先定义了一个占位符(placeholder),这也是进行训练样本的特征值。

> 构建隐藏层

构建隐藏层就比较简单了,直接调用定义好的fcn_layer的结果,填入对应的数据,激活函数是ReLU,h1就是这个隐藏层计算之后的结果。

> 构建输出层

输出层的前向计算也利用了神经网络的层定义函数,输入的数据就是前一层的数据,输入的神经元数量就是前一层的神经元数量,输出的神经元数量就是y的维度,也就是10。前向计算的值不需要激活函数,因为之后会利用集成了softmax的交叉熵函数。然后又通过softmax对forward做了激活计算,这是整个模型的预测结果,也就是pred的值。

> 双隐层模型

构建双隐藏层也是同样的三个步骤:

> 构建输入层

> 构建隐藏层

根据隐藏层的数量,这里需要构建两个隐藏层。

> 构建输出层

> 三隐层模型

构建三隐藏层也是同理:

> 构建输入层

> 构建隐藏层

> 构建输出层

> 训练模型的保存

前面大家已经学会如何去构建一个多层的全连接网络并进行训练,当得到比较满意的训练结果时,可以用这个模型进行新的数据的预测。

这里有个问题:我们花了大量的时间去训练这个模型,当我们重新开机时,由刚刚训练得到的比较好的模型参数就全部丢失了,没有办法直接拿来用,只能利用以前记录下来的比较好的那组超参数接着进行模型训练,训练完成后再应用。这样是比较麻烦的,不符合日常的应用场景。

我们需要把训练好的模型做持久化保存,哪怕关机重启也不会丢失,可以把模型重新读取出来以供使用。这么做还有一个好处:当我们在处理一个比较复杂的模型时,需要花费大量时间,有的大型模型可能需要几天甚至几十天,如果训练中发生断电或是需要关机,模型不能保存下来,是比较麻烦的。

这里会提到一个“断点续训”的概念,即不管训练到什么阶段,可以暂停训练,下一次需要的时候,可以从暂停点继续训练。这可以通过模型的保存和还原来实现。

> 初始化参数和文件目录

为了做好模型的保存,需要添加一些参数。首先用到了save_step,即模型保存的粒度(训练多少轮保存一次),如果设为1,那么每一轮都会保存。在这里设置为5,即每五轮保存一次。

模型保存在计算机上需要有一个具体的位置,我们建立了一个目录“./ckpt_dir/”,前面的点表示当前文件目录下新建一个子目录。这里会用到os库,所以需要导入进来,接着做一个判断,如果当前目录下不存在这个子目录则创建一个新的子目录。

> 训练并存储模型

在训练模型之前,若所有的变量都定义好了,就可以调用tf.train.Saver()去初始化saver,存储模型将通过saver来实现。

在训练模型中,要加入的代码也不多。橙框的上面部分输出了这一轮的训练结果(损失值以及精确率);橙框中的代码是加入的代码,它表示当这一轮结果需要保存的时候,调用saver的save方法,里面的有两个参数:第一个参数是当前运行的会话,它要把会话中的所有变量的值保存下来,第二个参数是所要保存模型的文件名,这个文件名需要包含具体目录,所以这里利用了os.path.join()函数来合成,第一个参数是刚才定义的子目录,第二个参数是模型的名字,名字中保留了一个记录轮次的格式,这样就可以知道这个文件是第几轮训练的结果。然后输出一个提示,告诉我们已经保存完成。当所有轮次训练完毕后,再通过蓝框中的代码保存最后的结果。

再完整看一下修改后的训练过程(框中为新增部分):

执行训练之后,进入对应的目录,可以看到保存的文件:

可以看到每一轮的模型文件都有三个文件。由于模型保存的机制中缺省最多保留最近5份模型,因此最初几轮的模型就没有了。因为最上面还有一个checkpoint文件,所以这里总共是16个文件。

> 训练模型的还原

之前把训练模型存盘,实际上保存的是模型里所有变量当前运行的值。这相当于是训练模型的快照,把保存的时间点的所有变量都变成存盘文件保存起来。如果要还原这个模型,我们需要从存盘的模型中把所有变量的值读取出来,赋给当前准备被还原的模型。

> 定义相同结构的模型

首先,我们还是要定义一个和以前存盘模型相同结构的模型,只有它们的结构相同,这些变量才能吻合,才能把读取出来的变量的值赋给等待着被覆盖的变量的值。

这里还是采用单层的256个神经元的神经网络的结构模型,构建相同的输入层、隐藏层、输出层。。

> 构建输入层

> 构建隐藏层

> 构建输出层

> 设置模型文件的存放目录

设置还原模型的文件所存放的位置,这个目录跟存盘文件的目录相同。因为这个存盘文件缺省就是最多保留最近的五份模型文件,所以恢复时,会找最新的那一份文件。

> 读取还原模型

这里同样需要创建一个saver,然后通过红框中的语句,得到存盘文件里所有模型的最新状态。如果它找到了这个存盘文件,就可以从目录中读取参数,恢复到当前的会话中,而原本会话中的值就被覆盖掉了。之后输出模型已从当前目录下恢复的提示。

恢复好以后,就可以利用这个模型。可以直接用来预测准确率,也可以断点续训,在当前存盘文件数据的基础上继续做优化训练。

> 输出还原模型的准确率

这里测试出来的准确率和当时存盘时的准确率是一样的。

示例代码

两层神经网络模型.py
H1_NN = 256   # 第1隐藏层神经元为 256 个
H2_NN = 64    # 第2隐藏层神经元为 64 个

# 输入层 - 第1隐藏层参数和偏置项
W1 = tf.Variable(tf.truncated_normal([784, H1_NN], stddev=0.1))
b1 = tf.Variable(tf.zeros([H1_NN]))

# 第1隐藏层 - 第2隐藏层参数和偏置项
W2 = tf.Variable(tf.truncated_normal([H1_NN,H2_NN], stddev=0.1))
b2 = tf.Variable(tf.zeros([H2_NN]))

# 第2隐藏层 - 输出层参数和偏置项
W3 = tf.Variable(tf.truncated_normal([H2_NN,10], stddev=0.1))
b3 = tf.Variable(tf.zeros([10]))

# 计算第1隐藏层结果
Y1 = tf.nn.relu(tf.matmul(x, W1) + b1)

# 计算第2隐藏层结果
Y2 = tf.nn.relu(tf.matmul(Y1, W2) + b2)

# 计算输出结果
forward = tf.matmul(Y2, W3) + b3
pred = tf.nn.softmax(forward)
三层神经网络模型.py
H1_NN = 256     # 第1隐藏层神经元为 256 个
H2_NN = 64      # 第2隐藏层神经元为 64 个
H3_NN = 32      # 第3隐藏层神经元为 32 个

# 输入层 - 第1隐藏层参数和偏置项
W1 = tf.Variable(tf.truncated_normal([784, H1_NN], stddev=0.1))
b1 = tf.Variable(tf.zeros([H1_NN]))

# 第1隐藏层 - 第2隐藏层参数和偏置项
W2 = tf.Variable(tf.truncated_normal([H1_NN,H2_NN], stddev=0.1))
b2 = tf.Variable(tf.zeros([H2_NN]))

# 第2隐藏层 - 第3隐藏层参数和偏置项
W3 = tf.Variable(tf.truncated_normal([H2_NN, H3_NN], stddev=0.1))
b3 = tf.Variable(tf.zeros([H3_NN]))

# 第3隐藏层 - 输出层参数和偏置项
W4 = tf.Variable(tf.truncated_normal([H3_NN,10], stddev=0.1))
b4 = tf.Variable(tf.zeros([10]))

# 计算第1隐藏层结果
Y1 = tf.nn.relu(tf.matmul(x, W1) + b1)

# 计算第2隐藏层结果
Y2 = tf.nn.relu(tf.matmul(Y1, W2) + b2)

# 计算第3隐藏层结果
Y3 = tf.nn.relu(tf.matmul(Y2, W3) + b3)

# 计算输出结果
forward = tf.matmul(Y3, W4) + b4
pred = tf.nn.softmax(forward)

Last updated