【Get】用深度学习识别手写数字

作者: CoorChice | 来源:发表于2018-10-19 03:37 被阅读112次
    image

    前置参考读物:

    《机器学习,看完就明白了》传送门

    获取数据源

    训练数据直接使用开源的手写数据集MNIST。

    MNIST数据集是一个开源的手写数据库。它提供了大量的数据样本作为训练集和验证集。这个数据集拥有 60000 个训练样本,和 10000 个测试样本。

    MNIST 官网(一个很 low 的网站)传送门:http://yann.lecun.com/exdb/mnist/

    image_mnist_web

    就是上面那几个红色的、带下划线的!!!

    如果你下载的 tensorflow 包括了 mnist 的例子的话,很幸运,你可以直接引用到数据加载的工具:

    from tensorflow.contrib.learn.python.learn.datasets.mnist import read_data_sets
    

    否则的话,你需要自己写一个工具,用于训练数据的下载和读取。但你可以在这个地址中找到这段代码。

    【mnist.py 传送门】

    我们实际在外面,需要调用的就是 read_data_sets 这个函数。

    关于数据下载,你可以直接到 CoorChice 给出的 MNIST 官网上直接下好数据,然后使用 read_data_sets 函数从储存路径读取就行。

    开始构建网络

    定义几个辅助函数

    首先,先抽象出几个函数,用来创建 权重、偏置量、卷积核和池化层。它们是这样的。

    # 定义一个用于创建 权重 变量的函数
    def weight_variable(shape):
        initial = tf.truncated_normal(shape, stddev=0.1)
        var = tf.Variable(initial)
        # 记录每一个权重,因为后面要使用正则化
        # 至于原因,后面具体再说
        tf.add_to_collection(tf.GraphKeys.WEIGHTS, var)
        return var
    
    
    # 定义一个用于创建 偏置量 变量的函数
    def bias_variable(shape):
        # 初始化值为0.1
        initial = tf.constant(0.1, shape=shape)
        return tf.Variable(initial)
    
    
    # 构建卷积函数
    # 该卷积核每次在长、宽上移动一个步长,padding采用"SAME"策略
    def conv2d(x, W):
        return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
    
    
    # 该池化核大小为2x2,长、宽上步长为2,padding采用"SAME"策略
    def max_pool_2x2(x):
        return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
    

    在上面这段代码中,CoorChice 有必要解释解释权重 weight 的生成。

    tf.truncated_normal(shape, mean, stddev)
    

    这个函数会从一个正太分布(该正太分布的均值为 mean,默认为0)中产生随机数,什么意思呢?

    image_truncated

    看上图,stddev 表示标准差,就是图中横坐标上的取值,也就是说,这个函数会从 [mean ± 2stddev] 的范围内产生随机数,如果产生的随机数不在这个范围内,就会再次随机产生,直到随机数落在这个范围内。

    至于为什么要用这种方法来初始化 weight 呢?这是根据各位大牛们的经验所得,使用这种方式产生的 weight 不容易出现梯度消失或者爆炸的问题。

    反正这也是一门玄学。

    构建网络结构

    先看一下完整代码,我们在逐一解释。

    import tensorflow as tf
    
    class CnnModel_MNIST:
        def __init__(self):
            # 创建占位tensor,用于装载数据
            self.x_data = tf.placeholder(tf.float32, [None, 784])
            self.y_data = tf.placeholder(tf.float32, [None, 10])
    
            # -----------------------构建第一层卷积-----------------------
            with tf.name_scope('hidden1'):
                W_conv1 = weight_variable([5, 5, 1, 32])
                b_conv1 = bias_variable([32])
                x_image = tf.reshape(self.x_data, [-1, 28, 28, 1])
                h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
                h_pool1 = max_pool_2x2(h_conv1)
    
            # -----------------------构建第二层卷积------------------------
            with tf.name_scope('hidden2'):
                W_conv2 = weight_variable([5, 5, 32, 64])
                b_conv2 = bias_variable([64])
                h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
                h_pool2 = max_pool_2x2(h_conv2)
    
            # -----------------------构建密集(全)链接层------------------------
            with tf.name_scope('FC1'):
                W_fc1 = weight_variable([7 * 7 * 64, 1024])
                b_fc1 = bias_variable([1024])
                h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])
                h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
    
            # -----------------------加入Dropout------------------------
            with tf.name_scope('dropout'):
                self.keep_prob = tf.placeholder(tf.float32)
                h_fc1_drop = tf.nn.dropout(h_fc1, self.keep_prob)
    
            # -----------------------构建输出层------------------------
            with tf.name_scope('output'):
                W_fc2 = weight_variable([1024, 10])
                b_fc2 = bias_variable([10])
    
                self.y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
    
    

    创建数据输入的占位符

    x_data = tf.placeholder(tf.float32, [None, 784])
    y_data = tf.placeholder(tf.float32, [None, 10])
    

    我们需要先创建两个占位变量来容纳待会儿输入的数据,然后把它们带着到网络中进行运算。

    • x_data 是用来容纳训练数据的,它的 shape 形状在这里被莫名其妙的定义为 [None, 784]。其实这里是有学问的,且听 CoorChice 慢慢道来。

      第一维定义为 None 表示不确定,后面会被实际的数值替代。这样做是因为我们一开始并不知道会有多少张图片数据会被输入。或者当我们采取 mini-batch 的梯度下降策略时,可以自由的设置 batch 的大小。

      第二个维度定义为 784,这完全是因为我们数据集中的图片大小被统一为了 28*28 。

    • y_data 是用来容纳训练数据的标签的,它的 shape 之所以被定义为 [None, 10] ,是因为它的第一维为 None 与 x_data 具有相同理由,而第二维为 10 是因为我们总共有 0~9 共 10 种类别的数字。

    构建第一层网络

    W_conv1 = weight_variable([5, 5, 1, 32])
    b_conv1 = bias_variable([32])
    x_image = tf.reshape(self.x_data, [-1, 28, 28, 1])
    h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
    h_pool1 = max_pool_2x2(h_conv1)
    

    权值 w 的构建,

    W_conv1 = weight_variable([5, 5, 1, 32])
    

    传入的数组表示 w 的形状,其实就定义了该层卷积核的大小和数量:

    • 每个卷积核的大小为 5x5

    • 输入通道数为 1,因为我们用的图片是灰度图。如果是用不带透明通道的 rgb 彩色图该值就设为 3,如果在带了透明通道的 rgba 彩色图该值就设为 4

    • 该层输出通道数为 32,即该层有 32 个卷积核。对于隐藏层中每个卷积层中的卷积核大小如何的确定,再次强调,这是一个玄学,凭感觉设置。最靠谱的方法是用一些公开的网络模型,照着巨人们的设置,毕竟是经过反复尝试论证出来的。

    偏置量 b 的构建

    b_conv1 = bias_variable([32])
    

    b 的大小和卷积核个数相对应就行了。什么意思呢?就是每个卷积核和输入卷积后,再加上一个偏置量就好。回顾一下卷积核的结构。

    wx + b
    

    改变输入数据的形状

    x_image = tf.reshape(self.x_data, [-1, 28, 28, 1])
    

    这行代码作用是改变我们输入数据的形状,让它可以和我们的卷积核进行卷积。因为输入的数据 x_data 是一个 Nx784 的张量,所以需要先变为 M x 28 x 28 x 1 的形状才能进行运算。

    第一个维度的 -1 表示大小待定,优先满足后面 3 个维度,最后再计算第一个维度的大小。也就是这样的:N x 784 / (28 x 28 x 1)

    第二、第三维度实际上表示每张输入图片的大小需改为 28 x 28。

    第四维度表示通道数,灰度为 1,rgb 为 3,rgba 为 4 。

    构建卷积并加上激活函数

    h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
    

    这行代码包含了 3 个操作:

    • conv2d(x_image, W_conv1),将输入与该层的所有卷积核进行卷积运算
    image

    这是一个卷积运算的动态示意图。

    每次从输入中取出一个和卷积核大小相同的张量进行卷积运算。

    image_conv_compute

    其意义就是,将卷积核旋转 180 度,然后再输入进行计算。

    image_conv_compute2

    由于涉及到旋转操作,所以卷积核的大小通常会取奇数,这样能让卷积核有一个明显的旋转中心。

    • conv2d(x_image, W_conv1) + b_conv1,每次卷积后加上一个偏置量
    • tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1),最后加上一个激活函数,增加非线性的变换

    此处使用的激活函数是比较流行的 ReLu 函数,它的的函数式很简单:

    y = max(x, 0)
    

    图像也比较直观:

    image

    ReLu激活函数的好处在于,由于它在第一象限就是 x,所以能够大量的减少计算,从而加速收敛。同时它天生就能减小梯度消失发生的可能性,不过梯度爆炸还是可能会发生。

    池化

    h_pool1 = max_pool_2x2(h_conv1)
    

    最后,再加上一个池化层,能够压缩权值数量,减小最后模型的大小,提高模型的泛化性。

    这里使用了一个 2x2 的 max_pooling,且步长取 1。可以回到上面定义的 max_pool_2x2(x) 函数回顾一下。

    max_pooling 实际就是取一个 2x2 张量中的最大值,这样能够过滤掉一些并不是很重要的噪声。

    image

    构建第二层网络

    W_conv2 = weight_variable([5, 5, 32, 64])
    b_conv2 = bias_variable([64])
    h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
    h_pool2 = max_pool_2x2(h_conv2)
    

    第二层和第一层构建的套路基本是一样的,其实我们再多加几层也都是按照这个模式走的。

    需要注意的就是第二层中,w 的输入通道数为上一层最后的输出通道数,也就是 hpool1 的输出,这里直接算出了是 32,因为上面已经定义好了啊。

    如果不确定的时候,可以通过这种方式来确定输入通道数:

    in = h_pool1.get_shape()[-1].value
    

    [-1] 表示不管 h_pool1 的形状如何,都取它最后一维的大小。

    构建全链接层

    W_fc1 = weight_variable([7 * 7 * 64, 1024])
    b_fc1 = bias_variable([1024])
    h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])
    h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
    

    全连接层则起到将学到的“分布式特征表示”映射到样本标记空间的作用

    这句话看起来很具体,然而并不知道它在说些什么。

    那么,我们试着理解一下,先看看全链接层的结构。

    定义 w 和 b

    W_fc1 = weight_variable([7 * 7 * 64, 1024])
    

    上面这行代码,将fc1 的 w 形状设定为 [7 * 7 * 64, 1024]。第一个维度大小为 7 * 7 * 64,因为经过前面一层的卷积层的池化层后,输出的就是一个 7 * 7 * 64 的张量,所以这里的输入就是上一层的输出。嗯,这一点在整个神经网络里都是这样。

    第二个维度大小为 1024。这个就有点诡异了!为什么是这个数值?

    实际上,这是我们可以自己随便设定的,它表示该全链接层的神经元个数,数量越多计算耗时越长,但是数量过少,对前面提取出来的特征的分类效果又不够好。

    因此,我们的全链接层拥有的特征数量就是 7 x 7 x 64 x 1024。数量还是比较惊人的。

    b_fc1 = bias_variable([1024])
    

    同样,b 的数量需要对应于 w 的最后一个维度,也就是一个神经元对应一个偏置量b。

    变形输入

    h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])
    

    为了能和上面定义的全链接层特征张量相乘,需要把输入的张量变形。其实对于每一个 7 x 7 x64 的输入张量而言,就是将它们展平成一个一维的向量。

    第一个维度取 -1 同上面提到过的意思一样,最后确定这个维度。实际上就是最后一个池化层输出的数量。

    构建线性函数,加上 ReLu 函数

    h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
    

    上面的代码还是按照 wx + b 的线性公式构建函数,然后加上 ReLu 函数增加非线性变化。

    image

    这一波计算,清晰的表达出了,当一个来自最后一层池化层的 [7 x 7 x 64] 的输出经过全链接层后,就被平铺成了一个 [1 x 1024] 的向量。相当于把前面分散的特征全部链接在了一起,这也就是为什么说前面的卷积核是从局部观察,而全链接层是从全局的视野去观察的,因为它这一层整合所有前面的特征。整合了所有的特征,我们就可以进行后续的分类操作了。

    至此,相信你对全链接层有了一个大概的了解。从中可以看出一些猫腻来。

    • 全链接层增加模型的复杂度,因为增加了很多神经元来扩充特征集。也因此,它有助于提升模型的准确率。

    • 但随着特征数量的爆炸式增加,训练速度必然会变慢。而且如果全链接层设置的神经元数量过多,会出现过拟合的现象。所以,需要适当的设置,不能一味的贪多。

    加入 Dropout

    self.keep_prob = tf.placeholder(tf.float32)
    h_fc1_drop = tf.nn.dropout(h_fc1, self.keep_prob)
    

    在全链接层之后,往往会跟着 Dropout 操作。由于在神经网络中,神经元的个数非常爆炸,往往会产生过拟合的问题,特别是引入了全链接层这种操作之后。所以我们需要做些什么来让过拟合发生的概率减小一些。

    Dropout 就是一种很流行的方案。

    h_fc1_drop = tf.nn.dropout(h_fc1, self.keep_prob)
    

    这行代码的第二个参数,我们可以动态的传入一个数值,表示每个神经元有多大的概率失效,其实就是不参与计算。

    形象点描述就是这样一个过程。每个神经元进行运算前,都按照设置的 keep_prob 概率决定它要不要参与计算。比如 keep_prob=0.5 的话,表示每个神经元有 50% 的概率失效。

    image

    不难看出,Dropout 操作能够一定程度上加快训练速度,同时降低过拟合的可能性。

    构建输出层

    W_fc2 = weight_variable([1024, 10])
    b_fc2 = bias_variable([10])
    
    self.y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
    

    最后一层输出层,我们就可以得到一个结果了。

    输出层的函数结构仍然是 wx+b 的线性结构。

    这里有必要解释一下输出的 w 的形状。第一个维度不用说,就是上一层输出的结果,这里上一层是从全链接层输出出来 1024 个神经元。第二个维度是我们分类的总类目数,由于识别的是 0-9 的手写数字,所以总共有 10 个类别。

    构建好线性函数后,在加入一个非线性的激活函数。在分类场景中, Softmax 是一个在输出层被广泛使用的激活函数。

    image

    这是 Softmax 的公式,很容易看出,它的值域为 [0, 1]。这就比较厉害,直接就转成一个个的概率了。就是说,每个输出对应类别的概率是多少。

    再这个看看形象的示意图理解一下。

    image

    这,就是网络!

    image_graph

    最后,这个网络就成型了。这其实是一个很简单的网络,总共 4 层,包含两个卷积层,一个全链接层和一个输出层。

    从图中可以清晰的看到数据的流向。

    就要开始训练了

    # coding=utf-8
    
    import time
    from input_data import *
    from cnn_utils import *
    from cnn_model import CnnMnistNetwork
    
    train_times = 35000
    base_path = "../mnist/"
    save_path = base_path + str(train_times) + "/"
    
    # 读取数据
    mnist = read_data_sets("MNIST_data/", one_hot=True)
    
    # 创建网络
    network = CnnMnistNetwork()
    x_data = network.x_data
    y_data = network.y_data
    y_conv = network.y_conv
    keep_prob = network.keep_prob
    # ------------------------构建损失函数---------------------
    with tf.name_scope("cross_entropy"):
        # 创建正则化对象,此处使用的是 L2 范数
        regularization = tf.contrib.layers.l2_regularizer(scale=(5.0 / 50000))
        # 应用正则化到参数集上
        reg_term = tf.contrib.layers.apply_regularization(regularization)
        # 在损失函数中加入正则化项
        cross_entropy = (-tf.reduce_sum(y_data * tf.log(y_conv)) + reg_term)
    tf.scalar_summary('loss', cross_entropy)
    with tf.name_scope("train_step"):
        # 使用 Adam 进行损失函数的梯度下降求解
        train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
    
    # ------------------------构建模型评估函数---------------------
    with tf.name_scope("accuracy"):
        with tf.name_scope("correct_prediction"):
            # 对比预测结果和标签
            correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_data, 1))
        with tf.name_scope("accuracy"):
            # 计算准确率
            accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    tf.scalar_summary('accuracy', accuracy)
    
    # 创建会话
    sess = tf.InteractiveSession()
    
    # 合并 summary
    summary_merged = tf.merge_all_summaries()
    train_writer = tf.train.SummaryWriter(save_path + "graph/train", sess.graph)
    test_writer = tf.train.SummaryWriter(save_path + "graph/test")
    
    start_time = int(round(time.time() * 1000))
    
    # 初始化参数
    sess.run(tf.initialize_all_variables())
    
    for i in range(train_times):
        # 从训练集中取出 50 个样本进行一波训练
        batch = mnist.train.next_batch(50)
        if i % 100 == 0:
            summary, train_accuracy = sess.run([summary_merged, accuracy],
                                               feed_dict={x_data: batch[0], y_data: batch[1], keep_prob: 1.0})
            test_writer.add_summary(summary, i)
            consume_time = int(round(time.time() * 1000)) - start_time
            print("当前共训练 " + str(i) + "次, 累计耗时:" + str(consume_time) + "ms,实时准确率为:%g" % (train_accuracy))
        # 记录训练时数据,每训练1000次保存一次训练信息
        if i % 1000 == 0:
            run_options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE)
            run_metadata = tf.RunMetadata()
            # 训练一次,dropout 的参数设置为 0.5
            summary, _ = sess.run([summary_merged, train_step],
                                  feed_dict={x_data: batch[0], y_data: batch[1], keep_prob: 0.5}, options=run_options,
                                  run_metadata=run_metadata)
            train_writer.add_run_metadata(run_metadata, str(i))
            train_writer.add_summary(summary, i)
        else:
            summary, _ = sess.run([summary_merged, train_step],
                                  feed_dict={x_data: batch[0], y_data: batch[1], keep_prob: 0.5})
            train_writer.add_summary(summary, i)
        # 每训练 2000 次保存一次模型
        if i != 0 and i % 2000 == 0:
            test_accuracy = int(
                accuracy.eval(feed_dict={x_data: mnist.test.images, y_data: mnist.test.labels, keep_prob: 1.0}) * 100)
            save_model(base_path + str(i) + "_" + str(test_accuracy) + "%/", sess, i)
    
    # 在测试集计算准确率
    summary, test_accuracy = sess.run([summary_merged, accuracy],
                                      feed_dict={x_data: mnist.test.images, y_data: mnist.test.labels, keep_prob: 1.0})
    train_writer.add_summary(summary)
    print("测试集准确率:%g" % (test_accuracy))
    
    print("训练完成!")
    train_writer.close()
    test_writer.close()
    # 保存模型
    save_model(save_path, sess, train_times)
    
    

    先放一波完整代码,后面再挑着重点说一说。

    其中涉及到一些深度学中的基本概念,本篇篇幅已经够长了,CoorChice 就不在这里过多解释了。如果还不清楚,可以先跳到以下这篇文章,花个几分钟了解了基本概念后再继续往下。

    《机器学习,看完就明白了》传送门

    构建损失函数

    # 创建正则化对象,此处使用的是 L2 范数
    regularization = tf.contrib.layers.l2_regularizer(scale=(5.0 / 50000))
    # 应用正则化到参数集上
    reg_term = tf.contrib.layers.apply_regularization(regularization)
    # 在损失函数中加入正则化项
    cross_entropy = (-tf.reduce_sum(y_data * tf.log(y_conv)) + reg_term)
    tf.scalar_summary('loss', cross_entropy)
    with tf.name_scope("train_step"):
    # 使用 Adam 进行损失函数的梯度下降求解
    train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
    

    实际上,构建损失函数的关键代码就两行:

    cross_entropy = (-tf.reduce_sum(y_data * tf.log(y_conv)) + reg_term)
    train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
    

    第一行构建出一个 交叉熵 损失函数,第二行对损失函数做梯度下降获得 tensor。

    这里使用了 Adam优化算法 能够为不同的参数动态的计算不同的自适应学习率,这与 SGD 恒定不变的学习率有区别。这种优化算法使得各个参数的变化比较平稳,计算消耗的内存更小,收敛也会更快一点。

    但,网上看到有说它的训练效果不如 SGD 的 ???

    本来,损失函数的构建到此就结束了,反手就可以开始训练了。但是 CoorChice 在训练过程中发现,每次当训练进行到 2w 多次的时候就会出现梯度爆炸的现象。突然的 loss 就都变为 NaN,accuracy 本来好好的 0.99 呢,骤降的趋近于 0 !

    image

    于是 CoorChice 二话不说,打开 Google 就是一通搜索,网上说的各种各样的原因都有。

    这种问题也不太好确定具体是因为哪一个原因导致的,于是就加个正则化试试。结果就好了!

    来看看正则化是怎么加。

    前面在构建 w 函数里加了一行代码:

    tf.add_to_collection(tf.GraphKeys.WEIGHTS, var)
    

    目的就是为了把每个 w 放到集合中,以便此时进行正则化使用。

    此处,CoorChice 选择使用高端一点的 L2范式,加入正则化后的 Loss 公式如下:

    image

    c0就是原本的损失函数部分,这里就是 交叉熵,这部分又被称作是 经验风险。后面的一部分就是我们的 L2正则化式了,它实际就是把每个权重平方后求和,然后除以 w 的数量,在乘以个重要度系数。正则化的部分又叫作 结构风险,因为它是基于 w 计算出的一个数值,加在 交叉熵 上,从而每次增大交叉熵的值,也就是增大梯度,达到惩罚loss的效果。它一定程度上削弱了网络中特征值的作用,从而使模型的泛化性提高,也就能进一步的避免过拟合发生的可能。

    再回过头看看上面的代码,就理解正则化是如何加入到网络中的。

    构建评估模型

    # 对比预测结果和标签
    correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_data, 1))
    # 计算准确率
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    

    这两行代码,构建了一个用于评估模型准确率的 accuracy tensor。第一行实际就是比较了一下预测值和真实值,结果是一个 bool 向量,在第二行中转化为浮点数,求个平均就是准确率了。

    开始训练啦!

    for i in range(train_times):
        # 从训练集中取出 50 个样本进行一波训练
        batch = mnist.train.next_batch(50)
        summary, _ = sess.run([summary_merged, train_step],
                                  feed_dict={x_data: batch[0], y_data: batch[1], keep_prob: 0.5})
        # 每训练 2000 次保存一次模型
        if i != 0 and i % 2000 == 0:
            test_accuracy = int(
                accuracy.eval(feed_dict={x_data: mnist.test.images, y_data: mnist.test.labels, keep_prob: 1.0}) * 100)
            save_model(base_path + str(i) + "_" + str(test_accuracy) + "%/", sess, i)
    

    首先从训练集中随机的取出 50 个样本作为一次训练的输入。为什么要这么干呢?因为训练集有赤裸裸的 60000 个样本啊!训练一次太耗时了,特别是在平时开发用的笔记本上。

    这么做理论上准确率没有全集训练高,但是也能达到 99% 的准确率,却能节省巨量的时间,这点理论上的准确率还是可以适当的舍弃的。

    接着就调用 sess.run(train_step) 开始一次训练了,注意此处由于加了 dropout,所以每次 feed_dict 中需要设定它的值。

    if i != 0 and i % 2000 == 0:
            test_accuracy = int(
                accuracy.eval(feed_dict={x_data: mnist.test.images, y_data: mnist.test.labels, keep_prob: 1.0}) * 100)
            save_model(base_path + str(i) + "_" + str(test_accuracy) + "%/", sess, i)
    

    CoorChice 每训练两千次,在测试集上测试一下,然后保存一下模型。这是良好的习惯。因为一旦训练起来,很多不可控的因素,多保存些模型,后面还可以挑最合的。

    上面这个过程 CoorChice 循环了整整 35000 次!电脑全速运转了一晚上才训练完成。

    image

    上图就是一堆存档的模型文件夹,可以看到,一开始其实准确率其实也不低,97%。随着训练次数的增加,准确率就稳定在了 99% 了。

    使用模型进行识别

    现在,模型已经训练好了,接下来就可以使用这个模型识别我们自己手写的数字了。

    # coding=utf-8
    
    import numpy as np
    from PIL import Image
    import os
    from cnn_model import CnnMnistNetwork
    import tensorflow.python as tf
    
    train_times = 20000
    num = 5
    image_path = "num_images_test/num"
    CKPT_DIR = "../mnist/" + str(train_times) + "_99%"
    # 将数字图片缩放为标准的 28*28,接着进行灰度处理
    img = Image.open(image_path + str(num) +".png").resize((28, 28), Image.ANTIALIAS).convert("L")
    # os.system("open " + image_path + str(num) + ".png")
    flatten_img = np.reshape(img, 784)
    arr = np.array([1 - flatten_img])
    print(arr)
    
    # 创建模型对应的网络
    network = CnnMnistNetwork()
    x_data = network.x_data
    y = network.y_conv
    keep_prob = network.keep_prob
    # 创建会话
    sess = tf.InteractiveSession()
    # 初始化参数
    sess.run(tf.initialize_all_variables())
    saver = tf.train.Saver()
    ckpt = tf.train.get_checkpoint_state(CKPT_DIR)
    if ckpt and ckpt.model_checkpoint_path:
        # 读取恢复模型
        saver.restore(sess, ckpt.model_checkpoint_path)
        # 载入数据,进行识别
        y = sess.run(y, feed_dict={x_data: arr, keep_prob:1.0})
        # 取最大可能
        result = str(np.argmax(y, 1))
        print("\n期望结果" + str(num) + ", 预测结果:" + result)
        os.system("open num_images_test/num" + result[1] + ".png")
    else:
        print("没有模型")
    

    使用模型比较简单,就是读取一张图片,然后创建出模型所对应的网络结构来,接着读取模型,输入图片,就能得到识别结果了。

    image

    闲扯两句

    MNIST 数据的训练相当于是机器学习的 HelloWorld 程序,我们构建了一个 4 层的简单的网络进行训练识别,最后得到的模型准确率也是不错的。

    完整的体验了如何从 0 开始构建一个神经网络,然后保存模型,再读取模型进行识别。总的来说,这个过程思路还是比较简单的,关键就在于一些参数设置,还有出现问题如何去解决。比如,CoorChice 在训练过程中就碰到了 NaN 的问题。机器学习还是比较依靠经验的一门技术,需要在不断的实战中总结出一套自己的分析、解决问题的套路来。

    • 抽出空余时间写文章分享需要信仰,还请各位看官动动小手点个赞,快给 CoorChice 充值信仰吧 😄
    • CoorChice 会不定期的分享干货,想要上车只需进到 CoorChice的【个人主页】 点个关注就好了哦。

    相关文章

      网友评论

        本文标题:【Get】用深度学习识别手写数字

        本文链接:https://www.haomeiwen.com/subject/wqbezftx.html