基于TensorFlow Slim库实现手写数字识别

本文介绍如何基于Tensorflow的Slim库,利用CNN(卷积神经网络)实现手写数字识别。

本文GitHub源码地址

首先介绍一些基本概念:

  1. 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:0

      a_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更常用一些。

  2. slim库
    tf.contrib.slim库是一个基于tensorflow的机器学习库(除了slim库,还有tf.contrib.learn、tf.contrib.keras等其他封装了tensorflow的库,可以随意组合使用)。slim库提供了一些常用模型的实现,封装了模型底层的细节,使得开发者用起来更加简洁,代码可靠性和可读性都大大增强。
  3. numpy库
    一个数学库,集成了一些对数组和矩阵的操作。
  4. python with关键字的使用
    with封装了对某个对象的初始化工作和清理工作,类似于自动执行构造函数和析构函数,并且能够自动处理获取资源(比如打开/关闭文件)过程中的异常,适合于文件读取、资源获取等场景。
  5. 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()
预测结果展示

本文GitHub源码地址

参考:
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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容