深入MNIST及实践

  • 问题描述

    • 背景

      • MNIST是一个包含大量经过正则化、中心化处理的手写数字及其标签的数据集,其中包括大小为60000的训练集和10000的测试集。
      • softmax函数,\(softmax(x_i)=\frac{e^{x_i}}{\sum_{j=1}^{j=n}{e^{x_j}}}\)是一个归一化指数函数,可以将神经网络输出的一维向量归一化为各分量和为1的向量。于是归一化之后的向量便可作为n个离散变量的概率分布,从而实现多分类。
      • TensorFlow是一个开源的机器学习平台。TensorFlow框架中的一核心概念是计算图,其中的每一个节点代表一个operation,简称op;数据用tensor表示;在被称为session的上下文中执行该计算图。整体的运行方式有点像搭建水管线路:通过op搭建出一个线路,打开水管开关session,使tensor流入线路中。
    • 问题

      本次实验在TensorFlow中文社区指导下,基于TensorFlow框架,用MNIST数据集训练两个机器学习模型:

      • 具有单一线性层的softmax回归模型;
      • 和具有多层卷积神经网络的softmax回归模型。

      并对以上两个模型的学习效果进行对比。

  • 方法过程

    • 模型的建立

      • 数学推导

        MNIST数据集中的手写数字图片均为\(28\times28\)的灰度图。在此模型中我们暂时不考虑各像素之间的相对位置关系,于是可以将其视为一个\([1, 748]\)的张量,则整个训练集中的手写数字图片部分可以视为一个\([6000,748]\)的张量,其中第一位为图片编号索引,第二位为像素编号索引。在映射时对于\(R^{28}\times{R^{28}}\rightarrow{R}\times{R^{748}}\)的映射方式不做限制,只需保证所有的数据均经过同样的映射即可。

        对于人类来说,我们在书写数字的过程中,要保证自己写的数字能被认出,至少需要保证结构相同。这里的“结构”是指哪一块应该出现什么形状,比如数字9就应该由上面的一个圈和下面连接圈的一竖组成;更进一步地,“结构”是指哪一部分像素应该被填充,哪一部分像素应该为空。

        对于机器学习也是同理。

        我们定义\(w_{i,j}\)代表第\(j\)块像素被认为是数字\(i\)的可能性高低,其中$0\leq{j<748},0\leq{i\leq9} $。

        \(w_{i,j}\)组成形状为形状为\([748, 10]\)的张量\(W=\left[\begin{matrix}w_{1,0}&w_{1,1}&...&w_{1,9}\\{w_{2,0}}&w_{2,1}&...&w_{2,9}\\{...}&...&...&... \\{w_{748,0}}&w_{748,1}&...&w_{748,9}\end{matrix}\right]\)

        定义\(b_{i}\)表示数字\(i\)的偏置量,即线性回归的截距,则\(b_{i}\)组成形状为\([1,10]\)的张量\(b=[b_0,b_1,...,b_9]\)

        对于数据集中的图片,定义其对应的\([1,748]\)张量为\(a\),则\(a\times{W}+B\)\([1,10]\)的张量,其中的每个分量代表该图片是数字\(i\)的可能性。

        定义\([1,10]\)张量\(y=softmax(a\times{W}+b)\),则\(y\)为经过softmax函数归一化激活之后的得到的该图片为数字\(i\)的概率分布。

        \(W\)\(b\) 的取值是我们的模型需要学习部分。学习的目标是使基于此模型计算出的\(y\)与标签值\({y\_}\)相差最小。这里的相差值我们通过交叉熵\({cross\_entroy}=-\sum_i^n y_i\_\times\log(y_i)\)衡量。即目标为,最小化\({corss\_entroy}\),其中最小化的方法为神经网络中常用的反向传播(BP)优化梯度下降(GD)。

        梯度下降求出交叉熵\(cross\_entroy\)随每个权重\(w_{i,j}\)变化的情况\(\frac{\partial{cross\_entroy}} {\partial{w_{i,j}}}\),每次更优解即\(w_{i,j}'=w_{i,j}-\alpha\times\frac{\partial{cross\_entroy}}{\partial{w_{i,j}}}\).

        \(\frac{\partial{cross\_entroy}}{\partial{w_{i,j}}}\)则通过函数求导的链式法则,从输出层开始进行反向传播。

        卷积神经网络(CNN)的推导与上述基本相同,区别在于CNN利用了图片的局部性,在增加神经网络层数的同时保证参数数量不会大幅增长。

      • 正确率计算

        对于建立好的模型,我们通过对测试集数据进行多分类,并与测试集数据标签相对照得出模型正确率。

        \(y\)中最大分量所在的位置为\(index\),说明该图片最有可能是数字\(index\),于是认为该数字被预测为\(index\)。将\(index\)与该图片的标签进行对比,可获该次预测结果的正确性判断\(correct\_predicton\),多次预测的统计结果即为模型的正确率\(accuracy\)

    • 具有单一线性层的softmax回归模型

      此模型是一个单层神经网络,只包含输入层和输出层,又称感知器。

      • 输入层

        输入层只负责输入,并不进行计算。

        首先从MNSIT读入数据

        from tensorflow.examples.tutorials.mnist import input_data
        import tensorflow as tf
        
        mnist = input_data.read_data_sets("MNIST_data", one_hot=True)
        

        其中 one_hot 选项意为独热码表示,即若图片中的数字为5,则获取到的对应标签为一个\([1, 10]\)的张量,其中只有代表数字5的分量为1,其余均为0。

        接下来定义输入层张量

        x = tf.palceholder("float", [None, 784])
        

        表示输入一个第一维长度未知,第二维长度为784,各分量均为浮点数的张量x。

        placeholder表示占位符,是一个可变常量,需在调用session的run方法时赋值。

      • 输出层

        输出层包括计算神经元之间的连接和激活。

        神经元间的连接强度即为\(W\), 计算神经元件的连接即计算\(x\times{W}+B\);激活即用非线性函数作用于计算结果,使之不再是简单的矩阵相乘,这里用非线性函数softmax作用于op,产生归一化的离散概率分布\(y\)

        W = tf.Variable(tf.zeros([784, 10]))
        b = tf.Variable(tf.zeros([1, 10]))
        y = tf.nn.softmax(tf.matmul(x, W) + b)
        y_ = tf.placeholder("float", [None, 10])
        cross_entropy = -tf.reduce_sum(y_ * tf.log(y))
        
        train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)
        # 使用幅度为0.01的梯度下降算法最小化交叉熵
        

        其中 tf.reduce_sun 是求和函数。Variable表示变量,\(W\)\(b\) 即我们的模型需要学习的内容。Variable需要在session运行前进行初始化。

      • 模型正确率计算

        correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
        

        其中 tf.argmax 函数的作用是按行寻找张量中最大分量的位置。

        tf.reduce_mean 是求平均值函数。

    • 具有多层卷积神经网络的softmax回归模型

      多层卷积神经网络的结构一般为\((卷积层\times{n}+池化层)\times{m}+全连接层\),此模型中神经网络也符合上述结构,具体为\(卷积层+池化层+卷积层+池化层+全连接层\)

      • 初始化

        def weight_variable(shape): # 连接初始化函数
          initial = tf.truncated_normal(shape, stddev=0.1)
          return tf.Variable(initial)
        
        def bias_variable(shape): # 偏置初始化函数
          initial = tf.constant(0.1, shape=shape)
          return tf.Variable(initial)
        

        在多层卷积神经网络中有大量需要初始化的权重,所以定义以上两个初始化函数,实现代码重用。

        其中, tf.truncated_normal 函数的作用是产生服从正态分布的shape形状的张量,与默认均值0的差的绝对值不超过标准差stddev的两倍。这样取值是因为此模型使用relu激发函数\(relu(x)=\max(x,0)\),需要取稍大于0的数作为初始值,若全部取0则容易出现"死神经元",即神经元输出恒为0的情况。

        tf.constant 函数的作用是产生shape形的各分量值均为0.1的张量。

      • 卷积与池化

        def conv2d(x, W): # 卷积函数
          return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], 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')
        

        卷积中有许多参数可调,此模型中使用默认参数,即步长为1,边缘为0,这样将得出一个和输入形状相同的输出。

        池化一般接在一个或多个卷积op之后,此模型中池化模板大小为\(2 \times 2\)

        函数tf.nn.cov2d 的原型是 tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, name=None)。一般都要设置前4个参数:

        • input

          指需要做卷积的输入图像,要求是一个tensor,具有形如[batch, in_height, in_width, in_channels]的形状。

          各分量的具体含义是:batch_in,训练图片数;in_height,图片高度;in_width,图片宽度;in_channels,图片通道数。

        • filter

          指卷积神经网络中的卷积核,要求是一个tensor,具有形如[filter_height, filter_width, in_channels, out_channels]的形状。

          各分量的具体含义是:filter_height,卷积核的高度;filter_width,卷积核的宽度;in_channels,图像通道数;out_channels,卷积核的个数。其中in_channels需与input中的in_channels相同。

        • strides

          指卷积时在图像每一维的步长,是一个长度为4的一维向量。

        • padding

          指卷积的边缘的填充方式,有两种选择:SAME,填充0;VALID,不填充,即忽略。

        返回结果也是一个形如input的tensor。

        函数tf.maxpool 是最大池化函数,用于在对图片包含信息量影响不大的情况下,对张量进行压缩。

        压缩方式为取ksize形状的张量中的分量的最大值作为新的分量。strides参数和padding参数与 tf.nn.cov2d 中对应参数含义相同。

      • 第一层网络

        第一层网络包括一层卷积和一层池化。

        首先初始化卷积核。

        W_conv1 = weight_variable([5, 5, 1, 32]) # 高度5,宽度5,输入通道数1,输出通道数32
        b_conv1 = bias_variable([32])
        

        在使用这一层之前首先需要对输入图片x进行形状重塑,使之符合卷积输入的4维张量。

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

        tf.reshape 函数将输入数据集x重塑成一个图片数量不变,图片高度28,宽度28,通道数为1的新集合。其中第一维的-1代表函数会自动计算图片数量,无需传入具体值;第四维取1因为图片是灰度图,只有一个通道。

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

        然后依次进行卷积、激活、池化,完成第一层神经网络。

      • 第二层网络

        第一层神经网络池化后的数据为输入进行第二层神经网络。

        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_cov2)
        
      • 全连接层

        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)
        

        全连接层是必不可少的,它在多层神经网络的最后对整张图片的信息进行整合。

        数据集中的\(28\times28\)的图片在经过两次池化过后大小变为\(7\times7\),这是建立全连接对整张图片进行处理。处理时前需要将第二层神经网络池化后的数据集形状进行重塑。

      • 减少过拟合

        我们的学习是基于训练集的,但可能由于训练集不能保证随机抽样,从而具有某些特性,使得训练的结果和训练集符合较好,但和整体的数据符合并不好,这时称模型过拟合。

        为了减少过拟合,我们采用如下操作:

        keep_prob = tf.placeholder("float")
        h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
        

        tf.dropout 函数原型def dropout(x, keep_prob, noise_shape=None, seed=None, name=None) ,其作用可以概括为,输入张量x中的各分量,有keep_prob的概率保留下来,其余变为0。相当于在同层神经元中挑选一部分,本次暂停工作。

        tf.dropout 函数只在训练阶段启用,测试时keep_prob应设为1;且一般用于大型神经网络。

      • 输出层

        W_fc2 = weight_variable([1024, 10])
        b_fc2 = bias_variable([10])
        y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
        
        cross_entropy = -tf.reduce_sum(y_ * tf.log(y_conv))
        train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
        

        此模型中使用ADAM优化的最速梯度下降算法来实现最小化交叉熵。

      • 模型正确率计算

        correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
        
        for i in range(20000):
            batch = mnist.train.next_batch(50)
            if i % 100 == 0:
                train_accuracy = accuracy.eval(feed_dict={
                    x: batch[0], y_: batch[1], keep_prob: 1.0})
                # %g print a group
                print("step %d, training accuracy %g" % (i, train_accuracy))
            train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
        
        print("test accuracy %g" % accuracy.eval(feed_dict={
            x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
        

        每次随机抽取50的训练集,每100次输出一次日志。

        accuracy.eval()相当于sess.run(accuracy, ...)

  • 结果展示

    • 具有单一线性层的softmax回归模型

      • 代码实现

        from tensorflow.examples.tutorials.mnist import input_data
        import tensorflow as tf
        
        def main():
            mnist = input_data.read_data_sets("MNIST_data", one_hot=True)
            x = tf.placeholder("float", [None, 784])
            W = tf.Variable(tf.zeros([784, 10]))
            b = tf.Variable(tf.zeros([1, 10]))
            y = tf.nn.softmax(tf.matmul(x, W) + b)
            
            y_ = tf.placeholder("float", [None, 10])
            cross_entropy = -tf.reduce_sum(y_ * tf.log(y))
            train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)
        
            correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
            accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
        
            init = tf.global_variables_initializer()
            sess = tf.Session()
            sess.run(init)
        
            for i in range(1000):
                batch_xs, batch_ys = mnist.train.next_batch(100)
                sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
        
            print("The accuracy of this model is:", sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))
        
        main()
        
      • 正确率计算结果

        单层神经网络模型处理MNIST数据集正确率

      单层神经网络模型的正确率为91.5%左右。

    • 具有多层卷积神经网络的softmax回归模型

      • 代码实现

        from tensorflow.examples.tutorials.mnist import input_data
        import tensorflow as tf
        
        def weight_variable(shape):
          initial = tf.truncated_normal(shape, stddev=0.1)
          return tf.Variable(initial)
        
        def bias_variable(shape):
          initial = tf.constant(0.1, shape=shape)
          return tf.Variable(initial)
        
        def conv2d(x, W):
          return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], 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')
        
        
        def main():
            mnist = input_data.read_data_sets("MNIST_data", one_hot=True)
            x = tf.placeholder("float", [None, 784])
            y_ = tf.placeholder("float", [None, 10])
        
            # 第一层神经网络
            x_image = tf.reshape(x, [-1, 28, 28, 1])
            W_conv1 = weight_variable([5, 5, 1, 32])
            b_conv1 = bias_variable([32])
            h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)	# 卷积层
            h_pool1 = max_pool_2x2(h_conv1)								# 池化层
        	
            # 第二层神经网络
            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_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)
        
            # 防止过拟合
            keep_prob = tf.placeholder("float")
            h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
            W_fc2 = weight_variable([1024, 10])
            b_fc2 = bias_variable([10])
            y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
        
            
            cross_entropy = -tf.reduce_sum(y_ * tf.log(y_conv))
            train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
        
            # 正确率计算
            correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
            accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
        
            sess = tf.InteractiveSession() # 交互式初始化
            sess.run(tf.global_variables_initializer())
        
            for i in range(20000):
                batch = mnist.train.next_batch(50)
                if i % 100 == 0:
                    train_accuracy = accuracy.eval(feed_dict={
                        x: batch[0], y_: batch[1], keep_prob: 1.0})
                    print("step %d, training accuracy %g" % (i, train_accuracy))
                train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
        
            print("test accuracy %g" % accuracy.eval(feed_dict={
                x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
        
        main()
        
      • 正确率计算结果

        多层卷积神经网络模型处理MNIST数据集正确率
        多层卷积神经网络模型处理MNIST数据集正确率测试集结果

    多层卷积神经网络模型的正确率为99.2%左右。

  • 讨论总结

    本次实验学习了两种机器学习的算法。

    这两种算法的主要区别在于神经网络的层数,和建立的连接数。

    • 采用多层神经网络能够更深入地表示特征,以及拥有更强的函数模拟能力

      • 更深入地表示特征

        神经网络的每一层所学习到的都是对上一层更加抽象的表示。

        所以随着神经网络层数的增加,对事物的抽象能力也得到了提升,可以对更多的抽象特征进行分类。

      • 更强的函数模拟能力

        神经网络的层数增加,网络的节点和连接也随之增加,使整个网络的参数增多。

        神经网络的本质是模拟特征与目标之间的真实函数关系,更多参数引入意味着模拟的函数可以更加复杂,因而有能力进行更深入的拟合。

    • 多层卷积神经网络能够提升模型的正确率

      从上文可知,提高模型的正确率的一种简单方法是增加神经网络的层数,但如此一来连接数也会大幅增加,对计算性能的要求较高。

      多层卷积神经网络利用图片的局部性,能够在提升正确率的同时,保证连接数不会大幅增长。

      卷积神经网络通过局部连接、权值共享和下采样池化的操作达到该目的:

      • 局部连接

        由于图片的信息具有局部性,每个神经元不再向全连接神经网络中一样同上一层所有神经元相连,之和与卷积核大小相同的部分相连,减少了连接数。

      • 权值共享

        通过卷积核的步长移动实现一组连接共享同一个权重,这样又减少了很多参数。

        但我暂时还不清楚这么做的物理意义。

      • 下采样池化

        利用图像的局部性原理,对图像进行子抽样,在减少数据处理量的同时保存有用信息。


TODO


参考网页

posted @ 2019-05-24 12:20  weekends  阅读(240)  评论(1编辑  收藏  举报