(译文)面向机器学习新手的MNIST教程

原文地址(2017年8月7日时):https://www.tensorflow.org/get_started/mnist/beginners

本教程面向的读者群是既不熟悉机器学习又不熟悉TensorFlow的人群。如果你已经知道什么是MNIST、softmax(多项logistic)回归,你可能比较适合这篇教程faster paced tutorial。在读任何一篇教程之前,先确定你安装了TensorFlow

这里是我翻译的基础教程。另外,文内给了挺多重要的概念,包括交叉熵、后向传播、随机梯度下降之类,给的都是英文网址,到时候也考虑翻一下或者写点相关的东西吧……方便很烦英文的同学)。

学编程的时候,传统上第一件事就是print一个“Hello World”,在机器学习领域,这件事情就是MNIST。

MNIST是一个简单的计算机视觉数据集,它包含了如下的一些手写数字图像:

其中也包含了每个图像的label,指示了每个图像是什么数字。比如说,上图中的数字是5, 0, 4, 1。

本教程中,我们将会训练一个通过图片来预测数字的模型。我们的目的不是用复杂的方法构建一个学术上表现最优的模型,而只是初尝一下TensorFlow——以后的章节里我们会给出效果非常好的代码的。这样来讲的话,我们先来用一个很简单的模型,Softmax回归。

这份教程的实际代码是非常短的,有意思的事情三行就写完了。不过,理解其后的原理是很重要的:TensorFlow的工作方式和机器学习的核心原理。因此,我们将会步步为营地来解析这些代码。

关于此教程

本教程是mnist_softmax.py的代码解释,一行一行地说明了发生了什么事情。

你可以用不同的方法来使用本教程,包括:

  • 边看代码解释,边一行一行地把代码片段粘贴到Python环境里面去
  • 读整个解释之前先运行整个mnist_softmax.py,然后通过读教程来理解你不太明白的部分代码
    在本教程中我们将会:

  • 学习MNIST数据和softmax回归

  • 写一个通过一张图片的每一个像素点来识别它是哪个数字的函数

  • 用TensorFlow来通过让这个模型“看”数以千计的样本来训练它,以教会它识别数字(通过跑我们的第一个TensorFlow session来做这件事)

  • 用测试数据集来检查模型的准确度

    MNIST数据集

    MNIST数据集维护于Yann LeCun的网站。复制以下两行代码并运行,它将会在当前目录下新建一个MNIST_data目录,下载数据,并自动读入。

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

MNIST数据分为三部分:55000份训练数据(mnist.train), 10000份测试数据(mnist.test), 5000份验证数据(mnist.validation)。这个分割是很重要的:在机器学习中我们需要一些没有用来学习的数据,来验证是不是真的学到了一些结果。

上文提到,每一个MNIST数据都有部分:一张手写的数字图片,和一个标签数字(0-9)。我们把图片叫做"x",把标签叫做"y"。训练集和测试集都包含了图片和标签,比方说训练集的图片是mnist.train.images,标签是mnist.train.labels.

每一张图片都宽高28像素,可以看作一个大数组:

我们可以把这个二维数组变成一个28 x 28 = 784维度的向量。怎么展开它无关紧要,只要保持每个图片都是用一致的方法展开的就行。这样来看,MNIST图片就是一堆784维的向量,而且结构丰富(注意,这篇文章里面的结果是通过计算加强可视性了的)。

把图片扁平化成一维向量抛弃了图片的二维结构,这可能不是个好主意。最佳的计算机视觉模型是会利用这种结构的,我们在后面的教程中会提到。不过这里我们只用了简单的softmax回归,没有利用这种结构。

上面的结果就是mnist.train.images是一个shape为[55000, 784]的tensor。第一个维度就是一系列图片的下标,第二个维度就是这张图片的每一个像素点。这个tensor最里面的成员就是特定图片的特定像素点的灰度值,在0-1之间。

每个MNIST图片都有一个对应的标签, 是一个0-9的数字,代表了手写图片写的是什么。

本教程中,标签应该是"one-hot"向量。one-hot向量是一个大部分都是0,但是有一个1的向量。这个例子中,1在第n个位置的话就代表这是数字几。比方说,3的one-hot向量形式就是[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]。结果mnist.train.labels就是一个[55000, 10]的向量。

然后就可以正式来构造模型了!

softmax回归

我们知道MNIST里面每一个图像都是手写的0~9,所以所有的结果也就是10种可能。我们想要这个模型可以看一眼这个图像,然后对每一个可能结果给出一个可能性。比如说,我们的模型看一下这张图片然后说它80%是9,5%它可能是8(因为8和9两个数字上部都有个圈),然后因为不完全确定,给其它的数字一个很低很低的可能性。

这就是一个经典而又简单自然的softmax回归场景。如果你要给一个东西属于不同的几个类别的可能性打个分,那就应该用softmax。因为softmax给出的就是一系列加和为1,自身值在0-1的”可能性“。以后就算我们训练了更复杂的模型,最后一层网络也还就是softmax。

softmax回归有两步:一是搜集输入的数据是某种类的”证据“,二是把这些”证据“转换成一个可能性。

怎么来收集这些证据呢?对每一个类,我们可以对每一个点的灰度计算一个带权求和,把这些权加起来当作搜集到的证据。正的权也就是说这个图像可能是本类,负权就是说它不是本类。

下面的图标展示了一个训练好的模型里面这些权矩阵的样子,红色代表负权,蓝色代表正权。

(译注:我认为这里有必要详细说一下这个方法。

首先可以这样想象,建立十个二维矩阵,分别代表0-9。它的大小和图片的大小是一样的,它的子元素下标(x, y)对应的就是图片在(x, y)位置的那个点,这个子元素的值就是说,“现在我只考虑这个点,它使得这张图更像本类呢还是更不像本类呢?”,更像为正,不像为负。把训练好了的这十个矩阵画出来,就是下面这张图。

可是我们的图片是扁平化的,成了一个784维向量,对应的,这些权值矩阵也要同理压缩,所以我们得到10个784维的权重向量。所以就得到一个shape是[784, 10]的tensor。接下来一张图进来,shape是[1, 784],一共有784个点,它的每个点先依次和第1行指示这张图是不是0的784个权相乘,求和,得到一个值,然后依次和第2行指示这张图是不是1的784个权相乘,求和……以此类推。最后得到十个值,也就是这张图分属十个类的“证据”值。熟悉线性代数的同学可能发现这就是一个典型的矩阵相乘,这就是为什么后面用矩阵乘法代替了看上去很可怕的求和写法。)

我们还可以加上一些叫做偏置(bias)的附加证据,这是为了体现跟输入本身关系不大的一些事实。总的来说,一个类i,给定一个输入x,证据evidence就是:

其中$W_i$是第i类的权,$b_i$是第i类的偏置,j的作用是遍历用的下标,用这个来遍历输入图片x的每一个像素。接下来我们用softmax方程,把得到的这些证据的总和变成我们的预测可能性y。

这里softmax的作用是我们叫做“激活函数”或者”连接函数“的一种东西,为的是把上面那个线性方程的输出调整成我们想要的样子——在这个例子的语境下面就是十个类的可能性分布。形象来说就是把我们收集到的”证据“变成我们的输入图片是某个类的”可能“。softmax函数定义如下:

如果展开的话是这个样子:

第一种写法好理解一点:就是先对输入x求一下$e^x$然后归一化。第二种写法展示了具体怎么归一化:就看这个类i的证据x求$e^x$之后占所有类的$e^x$之和的多少。

这里用了求幂也就意味着多一个单位“证据”会表现为乘一个e,反之除上一个e。没有任何一个类的可能性会是0或者负数。然后再用softmax把他们归一化得到一个最后的可能性分布。(Michael Nielsen的这本书有一个带可交互还可视化的例子,可以帮助更直观的理解softmax。)

你可以把softmax回归理解成下面这张图的样子,不过真实情况下还有很多很多的x输入罢了。(而且$W_{i,~ j}$因为有10个类,所以i要到10)。对于每个输出我们都是用对应类的权矩阵和输入x乘起来,加上偏置,然后softmax归一化。

如果我们写成方程的样子的话:

我们也可以所谓”向量化“这个过程,把它变成一个矩阵和输入向量做乘法。这对计算和理解都有好处。

要写得更加紧凑的话:

好了,现在让我们来把这些个理论分析变成TensorFlow能跑的代码吧。

实现回归

用Python的时候,为了实现高效数字计算,我们经常Numpy一类的库,把一些很复杂的计算,比如矩阵乘法之类的放到Python外面去运行,使用用其他语言实现的高效代码。不幸的是,这样每个操作都从Python切换出去/回来也还是有很多损失,如果你想要在GPU上面或者分布式构架上面跑运算的话这个损失更是巨大,因为传送数据有很高的代价。

TensorFlow也把困难的计算放在Python之外,不过为了避免传送数据的损失TensorFlow做得更进一步。它不把单个的计算运算在Python之外,而是让我们定义一个由可操作的运算符组成的运算图,然后这个运算图的运算完全跑在Python之外。(好几个机器学习库里面也有这种实现方法)

要使用TensorFlow,我们先要import。

import tensorflow as tf

我们用一些变量符号来描述这些数学上的互相操作,先来造一个变量:

x = tf.placeholder(tf.float32, [None, 784])

x不是一个确定的值,而是个占位符placeholder,一个跑计算的时候我们会传给TensorFlow的参数。我们想要可以传入任意张的MNIST图片,每一张都是一个压缩成784维向量的图。我们将其表达为一个[None, 784]的浮点数2D tensor,这里None的意思是维度任意(对应了我们可以一次输入任意张图片)。

我们还需要提供模型的weights和biases。我们可以把他们也想象成输入参数,不过TensorFlow中有一种更好的方法来处理他们:变量Variables。 变量是在TensorFlow的运算图中可以变动的一些值,它可以被运算使用,甚至可以被运算修改。对于机器学习应用场景,变量基本上就当模型参数用。

W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))

我们通过给tf.Variable一个初始值来初始化Variable的值:本例中W和b都被初始化为0。既然我们是要去学习W和b的,初始值也就没什么所谓了。

注意到W的shape是[784, 10],这是因为我们希望W乘上一个784维的向量得到这个图像分属十个类的可能性(这里的具体说明见上面的译注)。b的shape是10,这样我们就可以直接把它加到各个可能性上。

现在可以来定义我们的模型。一行就够了。

y = tf.nn.softmax(tf.matmul(x, W) + b)

首先我们把x和W相乘,也就是tf.matmul(x, W)这一句。这和我们方程里头写的Wx不太一样,就像上面括号里说的,这是为了处理x这个二维输入的小技巧,因为一个[784, 10]和一个[不确定数字, 10]的矩阵怎么相乘呢?毕竟矩阵相乘没有交换律:)。然后我们加上b,最后施加tf.nn.softmax。

(译注:matmul是矩阵相乘,这和TensorFlow中multiply的概念是不一样的,下面将有独段解释。)

好了。一行就定了模型,再加上几行的准备工作,这就完事了。这可不是说TensorFlow造来就是为了让softmax变得简单的:TensorFlow是设计来方便构造各种各样数字模型的,从机器学习模型到物理模拟都是这样。另外,一次定义,到处能跑:不管是CPU,GPU,还是你的智能手机。

训练

为了训练模型,我们需要告诉这个模型什么叫“好”。嘛,实际上,在机器学习里面我们一般教这个模型什么叫“坏”。这个度量我们叫做cost或者loss(或者error),代表了模型的输出和真正的答案之间的差距。我们把这个error尽可能缩小,它越小模型就越好。

一个很常用也很好用的loss函数就是“交叉熵”(cross-entropy)。交叉熵这个主意一开始是信息论领域里面的一帮天才想信息压缩编码的时候想出来的,不过结果它成了从赌博到机器学习内的很多领域的重要概念。交叉熵定义如下:

这里$y$是我们预测的分布,$y'$是真正的分布(也就是数据介绍那一部分说的one-hot向量)。粗略来看,交叉熵是在度量我们的预测度量现实的时候到底有多差。更详细的交叉熵介绍超出了本文范围,但是还是很值得学一下

为了实现交叉熵首先我们需要一个placeholder来输入正确答案:

y_ = tf.placeholder(tf.float32, [None, 10])

然后实现交叉熵函数,$-\sum y'\log(y)$:

cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))

首先,tf.log当然就是运算每个y的对数。然后我们把y_和tf.log(y)的结果乘起来。接下来,由于reduction_indices=[1]这个参数的指示,tf.reduce_mean会把这个结果的第二个维度加和。最后,tf.reduce_mean会计算一个batch里面所有加和的平均。

(译注:说明一下batch这个概念,就是说一小批量的数据,因为有的时候数据量太大你想一次喂小批量的数据训练,或者你想先喂100个做下预测看看现在训练的效果如何,这时候一个batch=100的设计就会比较舒服。上面的例子里当写下输入维度为None的时候就相当于平常写函数留了个参量batch等待传入,不过要注意在后面设计模型的时候做矩阵乘法数目的对应。

说到这个问题有同学可能就会觉得y_ * tf.log(y)这一句是一个[None, 10]的矩阵和一个[None, 10]的矩阵相乘,根本乘不了。这里要说明一下,官方api解释了,tf中乘号是tf.multiply的一个shortcut,multiply是所谓的element-wise的,或者了解的同学会知道这个就是Hadamard 乘积,这里借用Nielsen书中的一张图:

因为是元素互乘,最后还是得到一个[None, 10],正好实现了交叉熵。

好了,我们继续。)

注意,在总的文首给出的源代码里我们没有用这个写法, 因为这个写法数值上不太稳定。为了取代它,我们用了tf.nn.softmax_cross_entropy_with_logits这个函数,传给它没有归一化的可能性输出。(本例是softmax_cross_entropy_with_logits on tf.matmul(x, W) + b))。这是因为直接用内部的softmax_cross_entropy数值运算上稳定些。在你自己的代码里也可以考虑用tf.nn.softmax_cross_entropy_with_logits来代替外部写法。

现在已经定义好了我们想让模型做什么,很容易就可以用TensorFlow来训练它做这个事儿。因为TensorFlow知道你的整个运算图是什么是样子的,它可以自动用后向传播算法来高效地算出你的变量会如何影响你要求最小化的那个loss变量。然后它就会运用你选择的优化算法来改变对应的变量以减少loss。

train_step = tf.train.GradientDescentOptimizer(0.05).minimize(cross_entropy)

这里,我们要求TensorFlow用梯度下降法来最小化cross_entropy这个值,学习率Leaning Rate定为0.05。梯度下降是一个很简单的过程,TensorFlow就是先对每个变量求一下其对于cost的导数,然后把它往导致cost下降的方向(负导数方向)降一点点(大小和学习率有关)。TensorFlow还提供了很多其他的优化器:要换优化器也就是改一行代码的事情。

TensorFlow在幕后做的工作其实就是,在计算图里面添加一些额外的运算来实现后向传播和梯度下降,然后return给你一个能够按减少cost方向改动各变量一点点的操作。

现在,我们建立一个InteractiveSession,启动我们的模型了。

sess = tf.InteractiveSession()

首先,别忘了初始化变量。

tf.global_variables_initializer().run()

然后来训练——这里我们训练1000次。

for _ in range(1000):
  batch_xs, batch_ys = mnist.train.next_batch(100)
  sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

每个循环中我们从训练集中随机获得100个数据,作为一个batch,提供batch数据给placeholder,让Session来跑train_step。

用小批量的随机数据来训练叫做随机训练——在这个场景下我们叫它随机梯度下降。理想来说我们会想要一次喂进所有的数据,因为这样明显更合逻辑,不过这样做需求是很高的。所以我们取而代之使用每次使用一个不同的子集来训练,这样实现起来简单一点儿,而且基本能达到要求。

评估模型

我们的模型效果如何?

嗯,我们先来看看预测得怎么样。tf.argmax是一个很有用的函数,它找到一个tensor中某一个维度上最大的值,返回这个值的下标。比如说tf.argmax(y, 1)是我们的模型认为的,每个输入所属的类,而tf.argmax(y_, 1)就代表了正确的数字。我们可以用tf.equal来检查我们的预测是否和真实数据相对应。

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))

这会得到很多布尔值,为了检查正确的部分有多少,我们把这些布尔值转换成浮点数然后求个平均。比如说[True, False, True, True] 会变成 [1,0,1,1]然后变成 0.75.

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

最后我们让sess给出测试数据上的正确率:

print(sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))

这个数字大略在92%。

结果如何呢?不怎么样。实际上,还挺差的。这是因为我们的模型很简单。做些小改动就能让这个结果变成97%,最好的模型可以做到99.7%的准确率!(这里是一系列模型的结果

重要的是我们从这个模型中学到的概念。不过,如果你对这个结果还是不太满意,可以看看下一个章节的内容,我们可以做得比这节好得多,并且学会如何使用TensorFlow来构建更加精巧的模型。

2017-05-30 08:4153