基于TensorFlow Slim库实现手写数字识别
本文介绍如何基于Tensorflow的Slim库,利用CNN(卷积神经网络)实现手写数字识别。
首先介绍一些基本概念:
- tensorflow库
-
placeholder和variable的区别:
placeholder是占位符,用于定义模型的输入和输出,定义的时候不赋值,使用模型的时候才赋值。variable是变量,定义的时候就需要赋值,而且随着模型的训练过程不停的优化,比如权重和偏差。 -
tf.Variable和tf.get_variable
tf.Variable会新建一个变量,变量名相同的情况下通过添加后缀编号识别不同的变量;tf.get_variable如果发现有同名变量就复用,否则新建变量。 -
tf.nn.softmax
softmax就是将一个数组中的所有值转换成概率,每个值的概率和自己的大小成正比,所有数的概率和为1。 -
tf.name_scope和tf.variable_scope的区别:
它们总的作用都是在变量名前面添加一个scope前缀,通过这种方式来给不同的变量分组。如:
a_name_scope/var2:0
a_name_scope/var2_1:0
a_name_scope/var2_2:0a_variable_scope/var3:0
a_variable_scope/var3_1:0区别在于tf.name_scope只对tf.Variable(name='var2', ...)建立的变量有效,对tf.get_variable(name='var1')这种方式建立的变量无效;而tf.variable_scope对两者都有效。tf.variable_scope更常用一些。
-
- slim库
tf.contrib.slim库是一个基于tensorflow的机器学习库(除了slim库,还有tf.contrib.learn、tf.contrib.keras等其他封装了tensorflow的库,可以随意组合使用)。slim库提供了一些常用模型的实现,封装了模型底层的细节,使得开发者用起来更加简洁,代码可靠性和可读性都大大增强。 - numpy库
一个数学库,集成了一些对数组和矩阵的操作。 - python with关键字的使用
with封装了对某个对象的初始化工作和清理工作,类似于自动执行构造函数和析构函数,并且能够自动处理获取资源(比如打开/关闭文件)过程中的异常,适合于文件读取、资源获取等场景。 - Batch Normalization
Batch Normalization是指对一个batch的数据通过计算均值和方差,并以均值和方差为基础,进行一系列的计算,使得batch里面所有数据归一化到某个范围的处理方式。这种归一化处理可以避免数据两极化分布,提高接下来的激活函数处理的有效性。同时注意,只有批量处理打到一定的数目,Batch Normalization才有作用,如果一次训练只使用一个或很少数的样本,则无效。
接下来通过代码讲解如何实现训练和预测过程。
第一步:定义网络:
def CNN(inputs, is_training=True):
# 将1*784的输入数据reshape成28*28的ndArray
shaped_inputs = tf.reshape(inputs, [-1, height, width, 1]) # NHWC N:Sample的数量 HW:高和宽 C=1 一个通道,灰度值
batch_norm_params = {'is_training': is_training, 'decay': 0.9, 'updates_collections': None}
init_func = tf.truncated_normal_initializer(stddev=0.01) # 正太分布初始化
with slim.arg_scope([slim.conv2d],
padding='SAME',
activation_fn=lrelu,
weights_initializer=init_func,
normalizer_fn=slim.batch_norm,
normalizer_params=batch_norm_params):
# 第一个卷积层 16个卷积核
net = slim.conv2d(shaped_inputs, 16, [5, 5], scope='conv0')
# 第一个池化层
net = slim.max_pool2d(net, [2, 2], scope='pool0')
# 第二个卷积层 32个卷积核
net = slim.conv2d(net, 32, [5, 5], scope='conv1')
# 第二个池化层
net = slim.max_pool2d(net, [2, 2], scope='pool1')
# 第三个卷积层 64个卷积核
net = slim.conv2d(net, 64, [5, 5], scope='conv2')
# 第三个池化层
net = slim.max_pool2d(net, [2, 2], scope='pool2')
# 把矩阵flattern成一维的,[batch_size, k]
net = slim.flatten(net, scope='flatten3')
# 第一个全连接层
net = slim.fully_connected(net, 1024,
activation_fn=lrelu,
weights_initializer=init_func,
normalizer_fn=slim.batch_norm,
normalizer_params=batch_norm_params,
scope='fc4')
net = slim.dropout(net, keep_prob=0.7, is_training=is_training, scope='dr')
# 第二个全连接层,输出为10个类别
out = slim.fully_connected(net, n_classes, activation_fn=None, normalizer_fn=None, scope='fco')
return out
这里我们定义的神经网络包括三个卷积层和两个全连接层,每个卷积层紧跟一个池化层。输入值的维度是batch_sizex784,代表batch_size个图片,每个图片大小是28x28,转换成一维的数据就是784。输出层维度是batch_sizex10,每一项是一个1x10个数组,代表一张图片属于0-9每个数字的概率,最大的概率对应的数字就是分类的结果数字。
三个卷积层共享同样的padding、激活函数、Batch Normalization方法,所以我们提出来放到arg_scope里面。
第一个卷积层有16个卷积核,代表一张原始输入图片经过该卷积层会生成16张新的图片。第二层32个卷积核,代表在这一层每张输入的图片会生成32张新的图片,依次类推,第三个卷积层输出的图片总是是16x32x64=2^15。接下来是两个全连接层,为了将卷积层的输出接入到全连接层,需要通过slim.flatten函数将输出数据转换成[batch_size, 2^15] 维的数据。第一个全连接层共1024个节点,将2^15个数据映射到1024个节点;第二个全连接层将这1024个节点映射到最终10个节点上,代表10个类别的结果。
第二步:定义输入输出,Loss和Optimizer
# 定义模型的输入输出
x = tf.placeholder("float", shape=(None, 28 * 28), name="w1") # 输入的图像28*28
y = tf.placeholder("float", shape=(None, n_classes), name="w2") # 输出的标签 1*10
is_training = tf.placeholder(tf.bool, name="w3") # 标志位,是训练还是预测
# 网络计算
pred = CNN(x, is_training)
# 预测的时候使用这个节点的值,选10个分类中概率最大的一个作为预测结果
out_result = tf.arg_max(pred, 1, name="op_to_restore")
# 定义LOSS和OPTIMIZER
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y, logits=pred)) # 计算输出和标记结果的交叉熵作为损失函数
optm = tf.train.AdamOptimizer(learning_rate=0.001).minimize(cost)
# 定义准确率
corr = tf.equal(tf.arg_max(pred, 1), tf.argmax(y, 1)) # 按行取最大值所在的位置,比较预测结果和标注结果是否相同,计算准确率
accr = tf.reduce_mean(tf.cast(corr, "float")) # 由于一次处理一个batch,一个batch包含多条结果,求多个结果的平均值作为准确度
在这一步,我们定义了模型的输入和输出,其中输入有两个,一个是图片数据,一个是标志是训练还是预测的标志位is_training,训练和预测过程会根据这个标志位确定是否应用dropout,并且会影响Batch Normalization的计算。
我们定义了Loss的计算和优化的方式,使用AdamOptimizer进行优化,学习率设置为0.001。除了Loss值,我们还定义了一个准确率,准确率是说在1000张图片的test过程中,我们正确识别了多少。
这里有个问题,为什么不直接拿准确率来作为优化的目标,而是采用Loss值呢?这是因为准确率的计算对每一张图片的识别的结果只是简单划分为正确和错误两类,相当于离散的概率0和1,而Loss值计算出来的概率则是一个连续区间的值。比如数字4被识别成了5或者7,对于计算准确率来说都是一样的,都是不正确。但是对于计算Loss来说5比7更接近4,说明参数的调整使得模型变好了,可以继续沿这个方向调整下去,所以这两个参数各有各的用处。
同时还定义了用于预测过程的结果输出out_result,并指定了节点名称name="op_to_restore",这样可以在其他地方加载模型预测的时候知道加载哪个节点进行预测结果的计算。
第三步:训练模型,择优保存
# INITIALIZER
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
print ("FUNCTIONS READY")
# 存储模型路径
savedir = "minist_model_out/"
saver = tf.train.Saver(max_to_keep=100)
save_step = 4
if not os.path.exists(savedir):
os.makedirs(savedir)
print ("SAVER READY")
# PARAMETERS
training_epochs = 50 # 在整个训练集上过多少遍
batch_size = 10 # 每次处理训练集的一个batch包含条目的数量
val_acc = 0
val_acc_max = 0
current_best_accuracy = 0.0
# OPTIMIZE
currentTime = time.time()
total_cost = 0.
total_cnt = 0
for epoch in range(training_epochs): # 循环处理所有训练集多次
total_batch = int(minist.train.num_examples / batch_size) # 训练数据集分割成若干个输入batch,一次处理一个batch
# 循环处理所有训练集一次 start
for i in range(total_batch):
batch = minist.train.next_batch(batch_size) # 一次获取batch_size个元素
batch_xs = batch[0] # 对应一条训练数据的748个像素
batch_ys = batch[1] # 对应一条训练数据的标注结果
feeds = {x: batch_xs, y: batch_ys, is_training: True}
sess.run(optm, feed_dict=feeds) # 执行一次训练过程
one_cost = sess.run(cost, feed_dict=feeds) # 计算本次训练的cost
total_cnt += 1
total_cost += one_cost
# 100步输出一次cost结果
if total_cnt % out_frequency == 0:
print ("total_cnt:%d cost: %.9f" % (total_cnt, total_cost / out_frequency))
total_cost = 0.
# 每训练1000次,在测试集上测试一下
if total_cnt % test_frequency == 0:
# 在1000张测试集图片上计算准确度
val_acc_sum = 0.0
for j in range(test_photo_batch_cnt):
test_batch = minist.test.next_batch(test_photo_each_batch_size)
test_batch_xs = test_batch[0]
test_batch_ys = test_batch[1]
test_feeds = {x: test_batch_xs, y: test_batch_ys, is_training: False}
val_acc = sess.run(accr, feed_dict=test_feeds)
val_acc_sum = val_acc_sum + val_acc
val_acc = val_acc_sum / test_photo_batch_cnt
print (" 在验证数据集上的准确度为: %.5f" % (val_acc))
# 如果准确率高于之前最好水平,保存模型
if val_acc > current_best_accuracy:
current_best_accuracy = val_acc
savename = savedir + "best_cnt_" + str(total_cnt) + "_accuracy_" + str(
current_best_accuracy) + ".ckpt"
saver.save(sess=sess, save_path=savename)
print (" [%s] SAVED." % (savename))
# 循环处理所有训练集一次 end
这一步首先计算所有的训练数据有多大,根据一个batch有10条训练数据,划分成若干个batch,同时指定在所有训练数据上过多少遍(epochs),就可以循环训练了。
每训练100步输出一下cost值,每过1000步在测试集上跑一下准确度,如果高于之前最佳水平,保存之。跑完所有的遍数,或是提前终止训练过程,模型训练就结束了。
第四步:加载模型,预测
训练过程分两步,加载模型和预测。加载模型代码如下:
with tf.Session() as sess:
# First let's load meta graph and restore weights
saver = tf.train.import_meta_graph('./minist_model_out/best_cnt_84000_accuracy_0.993.ckpt.meta')
saver.restore(sess, tf.train.latest_checkpoint('./minist_model_out/'))
graph = tf.get_default_graph()
x = graph.get_tensor_by_name("w1:0")
y = graph.get_tensor_by_name("w2:0")
flag = graph.get_tensor_by_name("w3:0")
# Now, access the op that you want to run.
op_to_restore = graph.get_tensor_by_name("op_to_restore:0")
通过saver.restore加载最优的模型,加载输入、输出节点,然后就可以使用模型了,可以看出我这边预测的最终精度大约99.3%,还是很高的。
预测过程如下,对100张图片进行预测:
for i in range(100):
batch = mnist.train.next_batch(1)
batch_xs = batch[0]
batch_ys = batch[1]
predict(batch_xs, batch_ys)
计算op_to_restore节点,就是识别的结果,同时通过plt库画出进行预测的原始图,可以和预测结果进行比较,整个识别过程就ok了。
def predict(val_x, labels):
feed_dict = {x: val_x, flag: False}
print "labels: "
print labels
print "predicts:"
print sess.run(op_to_restore, feed_dict)
val_x.shape = 28, 28 # nparray尺寸由1*784转换成28*28
plt.imshow(val_x) # 显示图片
plt.axis('off') # 不显示坐标轴
plt.show()
预测结果展示
参考:
https://stackoverflow.com/questions/36693740/whats-the-difference-between-tf-placeholder-and-tf-variable
http://geek.csdn.net/news/detail/126133
http://blog.csdn.net/mao_xiao_feng/article/details/73409975
https://morvanzhou.github.io/tutorials/machine-learning/tensorflow/5-13-A-batch-normalization/