手写数字mnist识别---手撸网络层入门
本文使用Tensorflow1.14.0搭建超简单的网络进行mnist手写体识别。第一节代码片段讲解,第二节附完整代码。尊重原文----原文连接。
代码片段讲解
首先,导入依赖的两个模块,一个是tensorflow,另一个是tensorflow.keras.datasets,我们要的数据集MNIST就是由这个datasets管理下载的。该网址中列出了datasets管理的所有数据集。
1 | import tensorflow as tf |
导入数据集,用(x,y)存储训练图片和标签,用(val_x,val_y)存储测试图片和标签。
1 | (x, y), (val_x, val_y) = datasets.mnist.load_data() |
在导入后,需要对数据形式进行初步查看,对于图像识别来说,图片的数量、大小、通道数量和数据范围、类型是必须了解的。以一下程序打印出这些信息,注释为输出结果。datasets导出的数据是Numpy数组,类型为uint8,训练图片共60k张,大小为28*28,为灰度图像,灰度范围0~255;测试图片共10k张。
1 | 数据集信息 |
在训练前必须先将数据转为Tensor。用tf.convert_to_tensor(value,dtype)函数可将value转为Tensor,并可指定数据类型(dtype)。将x,val_x转成浮点类型,而训练数据的标签y需要先转为Tensor整型再转化为独热码形式,测试数据val_y的标签转化为Tensor整型,独热码转换可用tf.one_hot(indices,depth,dtype),indices必须是整型,这也就是为什么先转成Tensor整型的原因,depth决定独热码位数,dtype默认是tf.float32。以下代码完成数据类型转换。
1 | 需要将数据转成Tensor |
数据集一次性加载到内存计算是不现实的,所以采用批处理将数据集分批喂进网络,在分批前先对数据shuffle一下,以防网络发现顺序规律。
- 把整个数据集一次喂给网络来更新参数的过程称为批梯度下降;
- 而每次只喂一张图片,则称为随机梯度下降;
- 每次将一小批图片喂给网络,称为小批量梯度下降。
简单来说,小批量梯度下降是最合适的,一般Batch设的较大,则达到最大准确率的速度变慢,但更容易收敛;Batch设小了,在一开始,准确率提高得非常快,但是最终收敛可能不太好。
1 | 生成批处理 |
train_db是可以直接迭代的,下面进行一次迭代,观察迭代结果,可以知道每一次迭代图片数量就是Batch大小。
1 | 批处理数据信息 |
下面就可以开始构建全连接网络了。网络节点数为784(input)->256->128->10(output),加上输入输出一共4层,其中输入层是打平后的图片,共28*28=784个像素;输出层由类别数决定,这里手写数字0-9共10类,故输出有10个节点,这些节点表示属于该类的概率。构建网络需要有初始化的参数,可以利用高斯分布进行参数的初始化,即函数tf.random.normal(shape,mean=0.0,stddev=1.0),但是为了避免参数初始化过大,常采用截断型正态分布,即函数tf.random.truncated_normal(shape,mean=0.0,stddev=1.0),该函数将丢弃幅度大于平均值的2个标准偏差的值并重新选择,这里也就是说随机的值范围在-2~2之间。在初始化参数时也要注意参数的shape,例如784->256的参数shape应为(784,256),偏置shape应为(256,),这样还方便之后的矩阵运算。偏置一般都初始化为0。此外,所有参数都必须转为tf.Variable类型,才可以记录下梯度信息。
1 | 参数初始化 |
初始化参数后,可以统计一下网络的参数量:784256+256128+128*10+256+128+10=235146。大约20万个参数,相比一些经典卷积网络,全连接网络的参数量还是比较少的。
对train_db进行迭代,套上enumerate()以便获取迭代批次。每一批数据,都要进行前向传播。首先,将shape为[256,28,28]图片打平为[256,784],这个可以借助tf.reshape(tensor,shape),在不改变元素个数的前提下,对维度进行分解或者合并。这样,h_1=x@theta1+bias1就可以得到下一层网络的节点值。h_1的shape为[256,256];同理,h_2的shape为[256,128],h_3的shape为[256,10]。每一层计算之后都应该加上一个激活函数,最常用的就是ReLu,通过激活函数可以增加网络的非线性表达能力,这里使用函数tf.nn.relu(features)。
由于更新参数需要得到各参数的梯度信息,因此前向传播要用with tf.GradientTape() as tape:包裹起来。此外,还得计算代价函数,就是Loss,一般采用差平方的均值来计算,差平方使用tf.math.square(x),均值采用tf.math.reduce_mean(input_tensor,axis=None),如果不指定axis就对所有元素求均值,返回值是标量,而如果指定axis,就仅对该axis做均值,结果的shape中该axis消失。
1 | 确定学习率 |
上一部分对梯度信息进行了记录,我们要更新参数,必须先执行loss对各参数求导,之后根据学习率进行参数更新:
1 | 获取梯度信息,grads为一个列表,顺序依据给定的参数列表 |
到此为止,整个训练网络就完成了。为了测试网络的效果,我们需要对测试数据集进行预测,并且计算出准确率。关于测试的前向传播同之前的一样,但测试时并不需要对参数进行更新。网络的输出层有10个类别的概率,我们要取概率最大的作为预测的类别,这可以通过tf.math.argmax(input,axis=None)来实现,该函数可以返回数组中最大数的位置,axis的作用类似与reduce_mean。预测结果的正确与否可用tf.math.equal(x,y)来判别,它返回Bool型列表。由于一批次有256个图片,那么预测结果也有256个,可以用tf.math.reduce_sum(input_tensor,axis=None)进行求和,求和前通过tf.cast(x,dtype)将Bool类型转为整型。
1 | 测试数据预测 |
自此所有的代码片段都已分析完毕。下一节将展示综合的代码和运行结果。
完整代码
1 | """ |
小结
本文采用2070训练,10min左右训练结束,最终达到0.95测试集准确度。作者手动加了一层网络,最终达到0.97的准确度。大家可以各自尝试手写。