21个项目玩转深度学习:基于TensorFlow的实践详解
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.2 利用TensorFlow识别MNIST

在第1.1节中,我们已经对MNIST数据集和TensorFlow中MNIST数据集的载入有了基本的了解。本节将真正以TensorFlow为工具,写一个手写体数字识别程序,使用的机器学习方法是Softmax回归。

1.2.1 Softmax回归

1.Softmax回归的原理

Softmax回归是一个线性的多类分类模型,实际上它是直接从Logistic回归模型转化而来的。区别在于Logistic回归模型为两类分类模型,而Softmax模型为多类分类模型。

在手写体识别问题中,一共有10个类别(0~9),我们希望对输入的图像计算它属于每个类别的概率。如属于9的概率为70%,属于1的概率为10%等。最后模型预测的结果就是概率最大的那个类别。

先来了解什么是Softmax函数。Softmax函数的主要功能是将各个类别的“打分”转化成合理的概率值。例如,一个样本可能属于三个类别:第一个类别的打分为a,第二个类别的打分为b,第三个类别的打分为c。打分越高代表属于这个类别的概率越高,但是打分本身不代表概率,因为打分的值可以是负数,也可以很大,但概率要求值必须在0~1,并且三类的概率加起来应该等于1。那么,如何将(a, b, c)转换成合理的概率值呢?方法就是使用Softmax函数。例如,对(a, b, c)使用Softmax函数后,相应的值会变成,也就是说,第一类的概率可以用表示,第二类的概率可以用表示,第三类的概率可以用表示。显然,这三个数值都在0~1之间,并且加起来正好等于1,是合理的概率表示。

假设x是单个样本的特征,Wb是Softmax模型的参数。在MNIST数据集中,x就代表输入图片,它是一个784维的向量,而W是一个矩阵,它的形状为(784, 10), b是一个10维的向量,10代表的是类别数。Softmax模型的第一步是通过下面的公式计算各个类别的Logit:

Logit=WTx+b

Logit同样是一个10维的向量,它实际上可以看成样本对应于各个类别的“打分”。接下来使用Softmax函数将它转换成各个类别的概率值:

y=Softmax(Logit)

Softmax模型输出的y代表各个类别的概率,还可以直接用下面的式子来表示整个Softmax模型:

y=Softmax(WTx+b)

2.Softmax回归在TensorFlow中的实现

本节对应的程序为softmax_regression.py,在该程序中,使用TensorFlow定义了一个Softmax模型,实现了MNIST数据集的分类。首先导入TensorFlow模块:

    # 导入TensorFlow
    # 这句import tensorflow as tf是导入TensorFlow约定俗成的做法,请大家记住
    import tensorflow as tf

导入TensorFlow的语句一般写作:import tensorflow as tf。这是一种约定俗成的写法。请记住这条语句,它将在后面的每一章中重复出现。

接下来和之前一样,导入MNIST数据库:

    # 导入MNIST教学的模块
    from tensorflow.examples.tutorials.mnist import input_data
    # 与之前一样,读入MNIST数据
    mnist=input_data.read_data_sets("MNIST_data/", one_hot=True)

下面的步骤是非常关键的几步,先来看代码:

    # 创建x, x是一个占位符(placeholder),代表待识别的图片
    x=tf.placeholder(tf.float32, [None, 784])

    # W是Softmax模型的参数,将一个784维的输入转换为一个10维的输出
    # 在TensorFlow中,变量的参数用tf.Variable表示
    W=tf.Variable(tf.zeros([784, 10]))
    # b是又一个Softmax模型的参数,一般叫作“偏置项”(bias)
    b=tf.Variable(tf.zeros([10]))

    # y表示模型的输出
    y=tf.nn.softmax(tf.matmul(x, W)+b)

    # y_是实际的图像标签,同样以占位符表示
    y_=tf.placeholder(tf.float32, [None, 10])

这里定义了一些占位符和变量(Variable)。在TensorFlow中,无论是占位符还是变量,它们实际上都是“Tensor”。从TensorFlow的名字中,就可以看出Tensor在整个系统中处于核心地位。TensorFlow中的Tensor并不是具体的数值,它只是一些我们“希望”TensorFlow系统计算的“节点”。

这里的占位符和变量是不同类型的Tensor。先来讲解占位符。占位符不依赖于其他的Tensor,它的值由用户自行传递给TensorFlow,通常用来存储样本数据和标签。如在这里定义了x=tf.placeholder(tf.float32, [None, 784]),它是用来存储训练图片数据的占位符。它的形状为[None, 784], None表示这一维的大小可以是任意的,也就是说可以传递任意张训练图片给这个占位符,每张图片用一个784维的向量表示。同样的,y_=tf.placeholder(tf.float32, [None, 10])也是一个占位符,它存储训练图片的实际标签。

再来看什么是变量。变量是指在计算过程中可以改变的值,每次计算后变量的值会被保存下来,通常用变量来存储模型的参数。如这里创建了两个变量:W=tf.Variable(tf.zeros([784, 10]))、b=tf.Variable(tf.zeros([10]))。它们都是Softmax模型的参数。创建变量时通常需要指定某些初始值。这里W的初始值是一个784×10的全零矩阵,b的初始值是一个10维的0向量。

除了变量和占位符之外,还创建了一个y=tf.nn.softmax(tf.matmul(x, W)+b)。这个y就是一个依赖x、W、b的Tensor。如果要求TensorFlow计算y的值,那么系统首先会获取x、W、b的值,再去计算y的值。

y实际上定义了一个Softmax回归模型,在此可以尝试写出y的形状。假设输入x的形状为(N, 784),其中N表示输入的训练图像的数目。W的形状为(784,10),b的形状为(10,)形状(784.10)表示一个784行10列的矩阵,形状(10,)表示一个10维的列向量,下同。。那么,Wx+b的形状是(N,10)。Softmax函数不改变结果的形状,所以得到y的形状为(N, 10)。也就是说,y的每一行是一个10维的向量,表示模型预测的样本对应到各个类别的概率。

模型的输出是y,而实际的标签为y_,它们应当越相似越好。在Softmax回归模型中,通常使用“交叉熵”损失来衡量这种相似性。损失越小,模型的输出就和实际标签越接近,模型的预测也就越准确。

在TensorFlow中,这样定义交叉熵损失:

    # 至此,得到了两个重要的Tensor:y和y_
    # y是模型的输出,y_是实际的图像标签,注意y_是独热表示的
    # 下面会根据y和y_构造损失

    # 根据y和y_构造交叉熵损失
    cross_entropy=\
      tf.reduce_mean(-tf.reduce_sum(y_* tf.log(y)))

构造完损失之后,下面一步是如何优化损失,让损失减小。这里使用梯度下降法优化损失,定义为

    # 有了损失,就可以用梯度下降法针对模型的参数(W和b)进行优化
    train_step=tf.train.GradientDescentOptimizer(0.01).minimize
    (cross_entropy)

TensorFlow默认会对所有变量计算梯度。在这里只定义了两个变量W和b,因此程序将使用梯度下降法对W、b计算梯度并更新它们的值。tf.train.GradientDescentOptimizer(0.01)中的0.01是梯度下降优化器使用的学习率(Learning Rate)。

在优化前,必须要创建一个会话(Session),并在会话中对变量进行初始化操作:

    # 创建一个Session。只有在Session中才能运行优化步骤train_step
    sess=tf.InteractiveSession()
    # 运行之前必须要初始化所有变量,分配内存
    tf.global_variables_initializer().run()

会话是TensorFlow的又一个核心概念。前面提到Tensor是“希望”TensorFlow进行计算的结点。而会话就可以看成对这些结点进行计算的上下文。之前还提到过,变量是在计算过程中可以改变值的Tensor,同时变量的值会被保存下来。事实上,变量的值就是被保存在会话中的。在对变量进行操作前必须对变量进行初始化,实际上是在会话中保存变量的初始值。初始化所有变量的语句是tf.global_variables_initializer().run()。

有了会话,就可以对变量W、b进行优化了,优化的程序如下:

    # 进行1000步梯度下降
    for_in range(1000):
      # 在mnist.train中取100个训练数据
      # batch_xs是形状为(100, 784)的图像数据,batch_ys是形如(100, 10)的实际标签
      # batch_xs, batch_ys对应着两个占位符x和y_
      batch_xs, batch_ys=mnist.train.next_batch(100)
      # 在Session中运行train_step,运行时要传入占位符的值
      sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

每次不使用全部训练数据,而是每次提取100个数据进行训练,共训练1000次。batch_xs, batch_ys分别是100个训练图像及其对应的标签。在训练时,需要把它们放入对应的占位符x, y_中,对应的语句是feed_dict={x:batch_xs, y_: batch_ys}。

在会话中,不需要系统计算占位符的值,而是直接把占位符的值传递给会话。与变量不同的是,占位符的值不会被保存,每次可以给占位符传递不同的值。

运行完梯度下降后,可以检测模型训练的结果,对应的代码如下:

    # 正确的预测结果
    correct_prediction=tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
    # 计算预测准确率,它们都是Tensor
    accuracy=tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    # 在Session中运行Tensor可以得到Tensor的值
    # 这里是获取最终模型的准确率
    print(sess.run(accuracy, feed_dict={x:mnist.test.images, y_:mnist.test.lab
    els}))# 0.9185

模型预测y的形状是(N, 10),而实际标签y_的形状是(N, 10),其中N为输入模型的样本个数。tf.argmax(y, 1)、tf.argmax(y_, 1)的功能是取出数组中最大值的下标,可以用来将独热表示以及模型输出转换为数字标签。假设传入四个样本,它们的独热表示y_为(需要通过sess.run(y_)才能获取此Tensor的值,下同):

    [[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

tf.argmax(y_, 1)就是:

    [0, 2, 9, 0]

也就是说,取出每一行最大值对应的下标位置,它们是输入样本的实际标签。假设此时模型的预测输出y为:

    [[0.91, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
    [0.91, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
    [0.91, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
    [0.91, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01]]

tf.argmax(y_, 1)就是:

    [0, 0, 0, 0]

得到了预测的标签和实际标签,接下来通过tf.equal函数来比较它们是否相等,并将结果保存到correct_prediction中。在上述例子中,correct_prediction就是:

    [True, False, False, True]

即第一个样本和最后一个样本预测是正确的,另外两个样本预测错误。可以用tf.cast(correct_prediction, tf.float32)将比较值转换成float32型的变量,此时True会被转换成1, False会被转换成0。在上述例子中,tf.cast(correct_prediction, tf.float32)的结果为:

    [1., 0., 0., 1.]

最后,用tf.reduce_mean可以计算数组中的所有元素的平均值,相当于得到了模型的预测准确率,如[1., 0., 0., 1.]的平均值为0.5,即50%的分类准确率。

在程序softmax_regression.py中,传入占位符的值是feed_dict={x:mnist.test.images, y_: mnist.test.labels}。也就是说,使用全体测试样本进行测试。测试图片一共有10000张,运行的结果为0.9185,即91.85%的准确率。因为Softmax回归是一个比较简单的模型,这里预测的准确率并不高,在下一节将学习如何使用卷积神经网络将预测的准确率提高到99%。

1.2.2 两层卷积网络分类

本节对应的程序文件是convolutional.py,将建立一个卷积神经网络,它可以把MNIST手写字符的识别准确率提高到99%,读者可能需要一些卷积神经网络的基础知识才能更好地理解本节的内容。

程序的开头依旧是导入TensorFlow:

    # coding: utf-8
    import tensorflow as tf
    from tensorflow.examples.tutorials.mnist import input_data

接下来载入MNIST数据,并建立占位符。占位符x的含义为训练图像,y_为对应训练图像的标签,这与上文是一样的。

    # 读入数据
    mnist=input_data.read_data_sets("MNIST_data/", one_hot=True)
    # x为训练图像的占位符、y_为训练图像标签的占位符
    x=tf.placeholder(tf.float32, [None, 784])
    y_=tf.placeholder(tf.float32, [None, 10])

由于使用的是卷积网络对图像进行分类,所以不能再使用784维的向量表示输入的x,而是将其还原为28×28的图片形式。[-1, 28, 28, 1]中的-1表示形状第一维的大小是根据x自动确定的。

    # 将单张图片从784维向量重新还原为28×28的矩阵图片
    x_image=tf.reshape(x, [-1, 28, 28, 1])

x_image就是输入的训练图像,接下来,我们对训练图像进行卷积计算,第一层卷积的代码如下:

    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')
    # 第一层卷积层
    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)

先定义了四个函数,函数weight_variable可以返回一个给定形状的变量并自动以截断正态分布初始化,bias_variabale同样返回一个给定形状的变量,初始化时所有值是0.1,可分别用这两个函数创建卷积的核(kernel)与偏置(bias)。h_conv1=tf.nn.relu(conv2d(x_image, W_conv1)+b_conv1)是真正进行卷积计算,卷积计算后选用ReLU作为激活函数。h_pool1=max_pool_2x2(h_conv1)是调用函数max_pool_2x2进行一次池化操作。卷积、激活函数、池化,可以说是一个卷积层的“标配”,通常一个卷积层都会包含这三个步骤,有时也会去掉最后的池化操作。

对第一次卷积操作后产生的h_pool1再做一次卷积计算,使用的代码与上面类似。

    # 第二层卷积层
    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)

两层卷积层之后是全连接层:

    # 全连接层,输出为1024维的向量
    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, keep_prob是一个占位符,训练时为0.5,测试时为1
    keep_prob=tf.placeholder(tf.float32)
    h_fc1_drop=tf.nn.dropout(h_fc1, keep_prob)

在全连接层中加入了Dropout,它是防止神经网络过拟合的一种手段。在每一步训练时,以一定概率“去掉”网络中的某些连接,但这种去除不是永久性的,只是在当前步骤中去除,并且每一步去除的连接都是随机选择的。在这个程序中,选择的Dropout概率是0.5,也就是说训练时每一个连接都有50%的概率被去除。在测试时保留所有连接。

最后,再加入一层全连接,把上一步得到的h_fc1_drop转换为10个类别的打分。

    # 把1024维的向量转换成10维,对应10个类别
    W_fc2=weight_variable([1024, 10])
    b_fc2=bias_variable([10])
    y_conv=tf.matmul(h_fc1_drop, W_fc2)+b_fc2

y_conv相当于Softmax模型中的Logit,当然可以使用Softmax函数将其转换为10个类别的概率,再定义交叉熵损失。但其实TensorFlow提供了一个更直接的tf.nn.softmax_cross_entropy_with_logits函数,它可以直接对Logit定义交叉熵损失,写法为

    # 不采用先Softmax再计算交叉熵的方法
    # 而是用tf.nn.softmax_cross_entropy_with_logits直接计算
    cross_entropy=tf.reduce_mean(
          tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv))
    # 同样定义train_step
    train_step=tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

定义测试的准确率(和第1.2.1节类似):

    # 定义测试的准确率
    correct_prediction=tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
    accuracy=tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

训练过程同样与第1.2.1节类似,不同点在于这次会额外在验证集上计算模型的准确度并输出,方便监控训练的进度,也可以据此来调整模型的参数。

    # 创建Session,对变量初始化
    sess=tf.InteractiveSession()
    sess.run(tf.global_variables_initializer())

    # 训练20000步
    for i in range(20000):
      batch=mnist.train.next_batch(50)
      # 每100步报告一次在验证集上的准确率
      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}))

得到的准确率结果应该在99%左右。与Softmax回归模型相比,使用两层卷积的神经网络模型借助了卷积的威力,准确率有非常大的提升。本节的程序同第1.2.1节在流程上非常相似,都是先读入MNIST数据集,再定义训练数据的占位符(x和y_),以x为输入定义模型,最后定义损失,进行训练。