7.1 单隐藏层神经网络构建与应用
> 单个神经元模型
可以把它简单分为几部分:首先是输入数据,输入数据是由输入样本的特征来决定的,在手写字符识别的例子中,输入是28×28像素点的灰度图,把每一个像素点的值当做是其中的一个特征,那么总共有28×28也就是784个特征点,从x1到x784,然后还需要一个偏置项b。
神经元所做的第一件事是求和,用到了上图黑框中的公式,对xi×wi求和,wi为这条边的权重,最后加上偏置项,得到神经元的计算值,然后再做激活处理,这里用f()函数来表示激活函数,最终通过激活函数运算得到的结果,就是这个神经元的输出结果。
如果把求和和激活函数看作一个整体,这个灰色的圆圈就可以表示神经元模型,接下来所要讲的多层神经网络,也就是基于多个这样的模型做扩展。
> 常见激活函数
由于上一讲中是个分类应用,所以用Softmax函数把结果进行分类的概率运算,但常见激活函数还包括Sigmoid、ReLU、tanh。
> Sigmoid
Sigmoid也称为S型函数,在上一讲中有介绍过,它的公式和图像如下所示:
它把所有的x的值映射到0-1之间,形成一条S型的曲线。
> tanh
它是一个双曲正切函数,长得跟Sigmoid很类似,但它们的区别在于tanh的取值范围从-1到1。
> ReLU
ReLU又叫修正线性单元函数,这个函数比较简单,若x≥0,它的值为x本身,若x<0,它的值为0。
> 单神经元模型效果
在上一讲中,通过单神经元建模,训练了150轮次,设置单批样本数为50,学习率为0.01等等,最终可以达到90%左右的正确率。
如果对这个准确率还不是很满意的话,没关系,我们可以做进一步的改进处理——加一些神经元。
> 全连接神经网络
这是一个全连接的单隐藏层的神经网络,可以分为三层:第一层是输入层,刚刚有提到过,是根据训练样本的特征决定有多少个节点,最后加上一个偏置项;第二层是隐藏层,隐藏层中的每一个节点就是一个神经元,最后也需要加上一个偏置项;第三层是输出层,所有隐藏层的输出都会作为下一层(也就是输出层)中每一个节点的输入。
什么是全连接呢?以上图中的a1为例,它上一层中的所有节点,从x1到b都有线跟它相连,即作为它的输入,同时,它作为输出层y1-yj的上层节点之一,它的输出会反馈给输出层上所有的节点,这样就形成了一个全连接的网络。而且在这个全连接的网络中,同一层之间没有线相连,跨层之间也没有线相连,因此这个全连接指的是相邻两层的所有节点之间相连。根据箭头的方向,前面一层所有的输出作为后面一层每一个节点的输入,形成一个全连接的网络。
在这个网络中,我们通常会说这个神经网络有多少层,这个层数是由隐藏层的数量决定的,不用考虑输入层和输出层,上图的结构中隐藏层只有一层,因此是一个单隐藏层的全连接神经网络。
更复杂一点的神经网络,比如有两个隐藏层,我们称之为两层全连接层神经网络。
> 全连接单隐藏层网络建模实现
> 载入数据
> 构建输入层
在输入层中,x1-xn都是样本的特征值,是28×28的灰度图,它总共有784个特征。这里定义了一个占位符x,它的形为784,前面的参数为None,表示行数不限定,之后根据训练时确定的一次输入多少个样本再进行赋值就可以了。占位符y表示样本的标签值,因为是one hot的,所以有10列。
> 构建隐藏层
这里我们设置了256个神经元,大家也可以根据自己的喜好去调整。
这是一个全连接的模型,跟a1相连的有784个特征以及一个偏置项b。所以这里定义了W1,也就是权值,它的形为784,表示784行(784个特征),它的列为H1_NN,也就是隐藏层的节点数,所以这里的权值总共有784×256=200704个,它的初值用了tf.randon_normal()函数,产生一个符合正态分布的随机数。这里还定义了b1,也就是偏置项,它也跟256个节点相连,因此也是H1_NN,它的初值用了tf.zeros()函数,赋值为0。
隐藏层中,神经网络的计算规则是输入的特征值乘以对应边上的权值,这里用了tf.matmul()函数,实现矩阵叉乘,然后再加上偏置项b。并对运算结果用了ReLU函数,它在TensorFlow中集成了,只要直接调用即可。最后,把得到结果赋给Y1。
> 构建输出层
输出层的每一个y的输入是前一层所有节点的输出,因此这里的权值W2的形就是H1_NN,也就是前一层的256个节点,由于输出的是one hot的十分类值,因此后面一个参数为10。偏置项b2跟10个节点相连,因此,它的形为10。
通过tf.matmul()函数实现Y1和 W2的叉乘,再加上偏置项b2,得到前向计算(forward)的值。这个前向计算并没有加上激活函数。
最后的预测值(pred)通过softmax形成一个多分类的结果。
> 定义损失函数
> 设置训练参数
训练轮数定义为40轮;一次训练的样本数为50;在一轮中batch的总数为训练集的大小除以批次大小,也就是1100次;显示粒度为1,学习率为0.01。
> 选择优化器
> 定义准确率
准确率的定义跟上一讲一样,通过argmax函数得到正确的数值(十个元素中最大值的下标)。对于标签值y来说,只有一个为1,其他全部为0,返回的就是值为1的下标。对于预测值pred来说,通过softmax后得到的是十个0-1的概率值,返回的就是概率最大的值的下标。然后通过equal函数,判断两者的值是否相等,返回true或false。再通过类型转换(tf.cast)把它变成浮点数(0或1)。最后计算均值,得到准确率。
> 模型训练
开头记录了开始时间,然后进行了变量初始化。接下来是for循环进行训练,每次通过TensorFlow中封装好函数读取下一批数据,而且在读取之前,已经做了洗牌工作。然后把得到的样本值作为输入,进行优化训练。优化器训练完一轮后,通过验证的数据集去验证数据的损失和准确率并显示出来。在整个训练完成后,显示运行的总时间。
> 训练结果
运行之后可能会出现下面的结果:
loss值出现了nan的情况,而准确率一直停留在0.0958。我们知道,一个十分类的问题,就算随机猜一个数,它的概率也是10%,所以这个数据有问题的。那么为什么会产生这样的问题呢?
主要是计算得到的loss值不对,超过了认可的范围。
前面定义损失函数的时候,用了交叉熵损失函数,它的公式没有问题,但是我们对前面获得的pred求了log,如果pred的值为0或者接近于0,会引起数据的不稳定,得到nan的结果。
如何破解这个难题呢?
在TensorFlow中,提供了一种新的损失函数定义的方法——结合Softmax的交叉熵损失函数定义方法:
它的第一个参数是前向计算的值,第二个参数是标签值y。
注意:这里所用的参数forward是没有经过softmax的前向计算的值,而之前定义的交叉熵损失函数,是经过softmax后的值,两者是不同的。
所以损失函数的定义要改成这种新的形式。
修改后再训练的结果如下:
可以看到,只是用了单隐藏层的神经网络,第一轮训练后的准确率就已经达到了93.26%,比上一讲中用一个神经元训练了150轮后的准确率还要高。
40轮结束后,得到的训练集的准确率为97.56%,总耗时为87.11秒。
> 评估模型
用测试集对训练好的模型进行评估,最后的结果为97.18%。虽然略低于训练集,但是差别不大,还是很不错的。
> 应用模型
当这个模型基本上达到要求,就可以用它进行预测了。
> 进行预测
这里的预测还是选用了测试集中的样本。
把需要测试的图像的数据集作为输入填充进去,产生pred,通过argmax取出最大值的下标,即它所预测的数字,最后赋给prediction_result,它是一个列表。
查看前十项的结果:
这里给出的是10个数字,在上一讲中也提到过,这样的显示并不是那么直观。我们所关心的是:它哪里预测出现了错误。
> 找出预测错误
prediction_result是预测得到的结果,mnist.test.labbels是正确的标签。通过对两者的比较(用两个等号),判断它们的值是否相等并把结果赋给compare_list,compare_list中包含了所有标签的正确与否。
接下来,把所有有误的预测值的下标找出来:
err_list中保存了所有预测有误的值的下标,这里用到了列表递推式,做10000次(compare_list的长度)循环,如果列表项所对应的值为False,就把它的下标加入到err_list。最后输出err_list列表中的所有内容以及列表的长度(代表有多少项没有预测正确)。由于之前测试集的准确率为97.18%,也就意味着有282条数据没有预测正确,这跟上图中的结果一致。
> 定义输出错误分类的函数
由于在err_list中,只能知道预测错误的下标,而不能知道错误的值是多少、标签值是多少,因此再定义一个函数来解决这个问题:
它有两个参数:一个是标签值的列表;另一个是预测值的列表。这里还是用到了compare_list和err_list,下一步只需要在err_list中,针对每一个错误的值显示它的下标、标签值以及预测值就可以了。全部输出完毕后,会有一个总计,统计一共有多少个预测错误。
第一个预测错误的下标为119,标签为2,预测值为8;第二个预测错误的下标为247,标签为4,预测值为2;……。这样,就以文本的形式告诉我们它到底把什么误判成了什么。我们也可以用到上一讲的知识,做一个图像可视化。
> 定义可视化函数
这里的定义跟上一讲一样,就不再赘述。构建好函数之后,就可以可视化查看预测错误的样本:
比如下标为610的值是错的,就可以看到它把4当成了2。
示例代码
Last updated