Ubuntu16.04+Tensorflow1.5.0-GPU+CUDN9.0+CUDNN7.0
如果是Debian系列的系统,请参考这篇博客进行安装。
所有完整代码的github地址为:https://github.com/StudentErick/tensorflow_cnn_mnist/tree/master
关于卷积神经网络的一般架构的理解,参考这篇博客
在这里,给出本次实验需要用到的神经网络的架构:
输入的是一个28*28的二维灰度图片,其中每一个小单元的取值 n∈[0,255] n ∈ [ 0 , 255 ] ,表示这个像素的灰度值。不过,MNIST数据集合把这个图片压缩成了一个[1,784]的矩阵。因此,在读取数据的时候,需要进行一次图片恢复成28*28的操作。
本次实验用的模型连同输入和输出层共有8层(图上把卷积与池化视为一层,我们拆分来看),分别是:
1. 输入层,28*28的灰度图片
2. 第一个卷积层,把1卷积为28*28*32的数据集,并把该数据集传入Relu
激活函数。卷积过滤器的尺寸是5*5,每次移动的步长是1.
3. 第一个最大池化层,把2进行最大池化操作,输出14*14*32的数据集。采取的池化的过滤器的尺寸是2*2,每次移动的步长是2 。
4. 第二个卷积层,把3再次进行卷积操作,输出14*14*64的数据集,并把数据集传入Relu
激活函数。卷积过滤器的尺寸是5*5,移动的步长是1
5. 第二个最大池化层,把4进行最大池化操作,输出7*7*64的数据集。池化的过滤器的尺寸是2*2,每次移动的步长是2.
6. 该层是把5中所有的数据集展开成一个7*7*64=3136的一维向量(图片中的数据是错的!!!),并输入Relu
激活函数
7. 该层新建了1000个神经元,与6经过激活后输出的所有神经元进行全连接。该层不进行 Softmax处理!!!
8. 10个最终预测结果,与9所有的神经元进行全连接,并把该层输入 进Softmax函数,作为最终的预测概率分布。
要把一维向量恢复成28*28的操作。[-1, 28, 28, 1]
是让计算机自动调整维数,在这篇博客中提到,调整合适的数据格式,这种方式经常用到。
# 导入MNIST数据集
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
# 输入的数据,None可以根据batch-size的结果自动调整
x_input = tf.placeholder(dtype=tf.float32, shape=[None, 784], name="x_input")
# 预测结果
y_true = tf.placeholder(dtype=tf.float32, shape=[None, 1])
# 把输入的每张图片的数据进行恢复
X_input = tf.reshape(x_input, shape=[-1, 28, 28, 1])
这里的权重相当于卷积层或者池化层的过滤器。抽象成函数,那么在每次设置的过滤器时,仅需调整函数的参数即可。
def create_weights(shape):
'''
随机初始化权重
:param shape: 权重张量的形状
:return: 初始化够的张量
'''
return tf.Variable(tf.truncated_normal(shape=shape, stddev=0.05))
def create_biases(shape):
'''
随机初始化偏置项
:param shape:
:return:
'''
return tf.Variable(tf.constant(0.5, shape=shape))
关于卷积层的各个参数的意义,可以参照这篇关于卷积神经网络架构的博客
def create_conv_layer(input_layer,
in_channels,
filter_size,
stride_size,
out_channels,
use_relu=True):
'''
创建卷积层
:param input_layer: 前一层的输入
:param in_channels: 输入的channel个数
:param filter_size: filter的边长
:param stride_size: stride的步长
:param out_channels: 输出的channel个数
:param use_relu: 是否使用relu
:return: 当前层卷积后的结果
'''
weights = create_weights([filter_size, filter_size,
in_channels, out_channels])
biases = create_weights([out_channels])
layer = tf.nn.conv2d(input=input_layer,
filter=weights,
strides=[1, stride_size, stride_size, 1],
padding='SAME')
layer += biases
if use_relu is True:
layer = tf.nn.relu(layer)
return layerpython
池化层一般情况下,紧跟着卷积层,池化是减少神经元数量的关键操作。关于各个参数的意义,同样参照卷积层提到的那篇博客。
def create_max_pooling_layer(input_layer, #
filter_size, #
stride_size): #
'''
:param input_layer: 输出层
:param filter_size: filter的边长
:param stride_size: stride的步长
:return: 卷积层最大池化后的结果
'''
input_layer = tf.nn.max_pool(input_layer,
[1, filter_size, filter_size, 1],
[1, stride_size, stride_size, 1],
padding='SAME')
return input_layer
全连接一般在最后一个池化操作后处理,它把池化层所有的元素展开成一维向量,这样方便后面的全连接操作。关于张量变形的操作,参照这篇博客。另外说明一点,get_shape()
函数不包括第一维,因为第一维是batch-size,如果增加了这一维,就成了一次展开所有输入的训练图片了,没有意义。
def create_flatten_layer(layer):
'''
把输入的layer进行flatten操作
:param layer: 输入的layer
:return: flatten操作后的结果
'''
layer_num = layer.get_shape()[1:4].num_elements()
layer = tf.reshape(layer, [-1, layer_num])
return layer
全连接层就是通过普通的矩阵乘法实现,注意权重的维数等于[输入的维数,输出的维数]
,乘法时是输入矩阵乘以权重,并添加上偏置项
def create_fc_layer(layer, in_num, out_num, use_relu):
'''
创建全连接层
:param layer: 输入的层
:return: 全连接后的输出层
'''
weight = create_weights(shape=[in_num, out_num])
biases = create_biases([out_num])
layer = tf.matmul(layer, weight) + biases
if use_relu is True:
return tf.nn.relu(layer)
return layer
把全连接的最后一层的维数设置成需要分类的实体的个数,然后把全连接的最后一层通过Softmax函数,输出的向量的各个维就是属于各类的概率,并选择概率最大的作为最终的预测结果。
经过前边的分析,网络各层的架构可以表示为:
# 输入的数据,None可以根据batch-size的结果自动调整
x_input = tf.placeholder(dtype=tf.float32, shape=[None, 784], name="x_input")
# 预测结果
y_true = tf.placeholder(dtype=tf.float32, shape=[None, 1])
# 把输入的每张图片的数据进行恢复
X_input = tf.reshape(x_input, shape=[-1, 28, 28, 1])
# 第一个卷积层
layer_conv1 = create_conv_layer(input_layer=X_input,
in_channels=1,
filter_size=5,
stride_size=1,
out_channels=32,
use_relu=True)
# 第一个卷积层的池化层
layer_pool1 = create_max_pooling_layer(input_layer=layer_conv1,
filter_size=2,
stride_size=2)
# 第二个卷积层
layer_conv2 = create_conv_layer(input_layer=layer_pool1,
in_channels=32,
filter_size=2,
stride_size=1,
out_channels=64)
# 第二个卷积层池化后的结果
layer_pool2 = create_conv_layer(input_layer=layer_conv2,
in_channels=64,
filter_size=2,
stride_size=2,
out_channels=64,
use_relu=True)
# layer_pool2进行flatten展开,得到3136维的列向量
fc_layer1 = create_flatten_layer(layer=layer_pool2)
# 全连接,输出1000维的向量,并经过relu函数激活
fc_layer2 = create_fc_layer(layer=fc_layer1,
in_num=3136,
out_num=1000,
use_relu=True)
# 全连接,输出10维向量,作为最终预测的结果
fc_layer3 = create_fc_layer(layer=fc_layer2,
in_num=1000,
out_num=10,
use_relu=False)
# 预测结果和真实值
y_pred = tf.nn.softmax(fc_layer3)
y_true = tf.placeholder(dtype=tf.int64, shape=[None, 10], name="y_true")
损失函数使用交叉熵表示两个向量的相似程度。注意,Tensorflow中的tf.nn.softmax_cross_entropy_with_logits
函数计算完交叉熵后,返回的是一个张量,需对张量进行reduce_mean
操作。还有,该函数本身就自带有Softmax操作了,因此不能把经过Softmax操作后的数据传入该函数,否则会导致结果不准确!!!!由此可见,我们在这里传入的数据是未经过Softmax的输出层fc_layer3
,而经过Softmax操作输出的y_pred
用于做精确度计算的。
# 计算交叉熵,注意这里的logits是fc_layer3,因为这个函数本身自带有Softmax的操作
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=fc_layer3,
labels=y_true)
# 损失函数
cost = tf.reduce_mean(cross_entropy)
# 随机梯度下降优化
optimizer = tf.train.AdamOptimizer(learning_rate=1e-3).minimize(cost)
精度计算就是预测正确的数目占预测总数的百分比。首先,我们要明确y_pred
的结构:
reduce_mean
的操作
# 多批次计算的时候是二维的,在这里提取出第列的,作为比较
y_pred_cls = tf.argmax(y_pred, axis=1) # 预测结果
y_true_cls = tf.argmax(y_true, axis=1) # MNIST标准结果
# 计算精准度
correction_prediction = tf.equal(y_pred_cls, y_true_cls)
accuracy = tf.reduce_mean(tf.cast(correction_prediction, tf.float32), name="accuracy")
关于存储模型的基本操作,请参考这篇博客
首先,需要定义一个类,用于存储模型,在这里,我们存储这个计算图所有的数据,所以不添加参数。
# 存储模型的对象
saver = tf.train.Saver()
在训练迭代完成之后,保存整个模型:
# 把当前计算图存储为my_test_model
saver.save(sess, './my_test_model')
保存结束后,会在同级目录下多出几个文件:
在这之前所有的操作都是在train.py
这个文件中的。之后,我们新建一个predict.py
文件,用于加载之前保存的数据:
saver = tf.train.import_meta_graph('my_test_model.meta')
saver.restore(sess, tf.train.latest_checkpoint('./'))
加载完成之后,在加载之前的占位符、操作等。注意一点,占位符需要重新feed数据!
graph = tf.get_default_graph()
x_input = graph.get_tensor_by_name("x_input:0")
y_true = graph.get_tensor_by_name("y_true:0")
accuracy = graph.get_tensor_by_name("accuracy:0")
xs, ys = mnist.train.next_batch(5000)
print(sess.run(accuracy, feed_dict={x_input: xs, y_true: ys}))
注意上述代码中,没有使用官方推荐的mnist.test.image
喂取x_input
和minist.test.lables
喂取y_true
,因为使用的GPU版本会耗尽内存。因此仅仅使用了5000组训练数据。真正合理的做法可以使用一个输入输出的队列来处理,本文暂时不做这种介绍。上述模型的识别正确率可以到97.92%