ML初学者的MNIST

本指导是写给初学机器学习和Tensorflow的朋友们。如果你已经知道MNIST和softmax(多元无序多分类)回归,你可以浏览快速教程。确保在开始本指导前安装了Tensorflow

人们学习编程,总是从“Hello World”程序开始的。机器学习也有自己的“Hello World”——MNIST。

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

它还给每张图标记,告诉我们这张图是什么。比如,上图的标签是5,0,4,1。

本指导中,我们将训练一个模型来看图片以预测标签。我们的目标是训练一个精密的模型来达到最优秀的表现——之后我们会给出相应代码!现在我们只是初步了解下Tensorflow。我们来看一个简单的模型,Softmax回归算法。

本指导代码十分简短,所有有趣的事都发生在短短三行里。但首先必须明白:Tensorflow是如何工作的以及机器学习的核心理念。我们将细细的讲解。

关于本指导

本指导详细解释了mnist_softmax.py中的代码。

你可以随意进行本指导:

在阅读本指导同时,运行每段代码。
在阅读本指导之前或之后,运行mnist_softmax.py,并阅读本指导来弄清代码的意义。

最终我们将完成:

学习MNIST数据和softmax回归算法。
创建一个能看图识别数字的算法模型
使用Tensorflow来训练模型通过看成百上千的例子来识别数字(以及运行我们首个Tensorflow session)
使用测试数据来测试模型准确度

MNIST数据

Yann LeCun的网站存放了MNIST数据。如果你是直接运行本指导中的代码的话,试着运行这两句来下载和读取数据:


In [2]:
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)


Successfully downloaded train-images-idx3-ubyte.gz 9912422 bytes.
Extracting MNIST_data/train-images-idx3-ubyte.gz
Successfully downloaded train-labels-idx1-ubyte.gz 28881 bytes.
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Successfully downloaded t10k-images-idx3-ubyte.gz 1648877 bytes.
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Successfully downloaded t10k-labels-idx1-ubyte.gz 4542 bytes.
Extracting MNIST_data/t10k-labels-idx1-ubyte.gz

MNIST数据分成3部分:55,000 个样本用作训练 (mnist.train),10,000张样本用作测试(mnist.test),以及5,000个样本用作验证 (mnist.validation)。这种分割十分重要:在机器学习中我们会挑选一部分数据不用做学习,以确保我们的学习是普适的。

就像之前所说的,每条MNIST样本有两个部分:一张手写数字的图片以及相应的标签。我们可以称之图片“x”和标“y”。训练组和测试组都包含图片及标签;举例来说,训练图片是mnist.train.images,训练标签是mnist.train.labels

每张图片都是28像素乘28像素。我们可以转换成一大个数组:

我们可以将这个数组扁平化为一个28x28 = 784的向量。如何扁平化并不重要,只要是一致的就行。本例中,图像是一系列784维结构丰富的向量空间。

扁平化处理扔掉了2D图像的结构信息。这有什么坏处呢?最好的机器视觉的确会利用这些信息,这些之后会说。本例中的简单的方法,softmax回归不会。

mnist.train.images的结果是一个[55000, 784]张量(n维数组)。第一维是所有图片的索引,第二维是每张图片所有像素的索引。张量中每条记录是一个像素的强度(0到1之间),下例为某张图片的像素:

MNIST中每张图片都有一个标签,0到9的数字,对应图片中的数字。

以本指导的目的来说,我们想要标签是独热码向量。独热码,直观来说就是有多少个状态就有多少位,而且只有一个位是1,其他全为0的一种码制。本例中,第n个数位可以表示为一个第n维置1的n维向量,举例来说,3可以是[0,0,0,1,0,0,0,0,0,0]。因此mnist.train.labels是一个浮点型的[55000, 10]数组。

我们现在准备好来建立模型了!

Softmax回归算法

我们知道每个MNIST的图片都是0到9的手写数字。所以每个图片只有十种可能性。我们可以看一张图片并给出其是某个数位的可能性。例如,我们的模型可能看了一张9的图片,并给出其可能是9的可能性为80%,还给出5%的可能性其是8(因为上半部分都是一个圆圈)以及一些其他数位的可能性,因为它无法100%确定。

这是个经典的softmax回归的例子,自然,简单。如果你想辨别一个目标是某些目标中的一个的可能性,softmax就是这种算法。softmax给我们一系列0到1之间的数字,加起来正好是1。即使以后,我们训练更复杂的模型时,最后一层也会是一层softmax。

softmax回归有两个步骤:首先我们将输入为某个类别的证据相加,然后我们将证据转换为可能性。

总结一张图片是某类的证据,我们加权求和像素的强度。如果某像素不是该类图像的证据,则该权重是负的。反之亦然。

以下是一个模型的权重。红色表示负权重,蓝色表示正权重。

我们还要加一些额外的证据称作偏置。基本上,我们希望能够说一些东西可能是独立于输入的。结果是,给定输入$x$的是类$i$的证据: $$\text{evidence}_i = \sum_j W_{i,~ j} x_j + b_i$$

其中 $W_i$ 是权重, $b_i$ 是类 $i$的偏置,$j$ 是是对输入图片$x$像素进行求和的索引。然后我们用“softmax”方法将证据转换为与我们预测相吻合的可能性:$$y = \text{softmax}(\text{evidence})$$

这里softmax作为“激活”或“连接”方法,修整我们线性方法的输出为我们期望的形式 —— 以本例来说,10种可能性的分布结果。您可以将其视为将吻合的证据转化为输入可能是某类的概率: $$\text{softmax}(x) = \text{normalize}(\exp(x))$$

展开后得到: $$\text{softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)}$$

但将softmax按第一种方式理解更有帮助:将其输入乘幂,然后使其归一化。乘幂意味着每多一个证据是以倍数增加权重的。反过来,少一个证据意味着权重是之前权重的分数。没有假设是零或者负的权重。softmax然后归一这些权重,是他们加起来正好等有一,组成有效的可能性分布。(要了解更多关于softmax方法的直觉,请参阅Michael Nielsen的书中的这部分内容,并附有交互式视觉辅助)

你可以将我们的softmax回归看作以下的东西,但有更多的$X$。每个输出,我们计算$x$的加权求和,加上偏置,然后使用softmax。

写作等式为:

我们可以“矢量化”这一过程,转换成矩阵乘法和向量加法。这有助于计算效率。(这也是一个有用的思考方式)

更紧凑地,我们可以写为: $$y = \text{softmax}(Wx + b)$$

现在让我将这些写成tensorflow程序。

实现回归

在Python里做高效的数值计算,我们通常使用像NumPy之类的库,将庞大的计算量放到Python之外,更有效率的语言中去运行。不幸的是,切换回Python仍有许多开销。这样的开销对于想运行GPU上或分布式计算尤为显著,大量开销浪费在传输数据中。

Tensorflow也将庞大的计算放在Python之外,但它更进一步阻止的这些开销。不像运行单个庞大的计算,Tensorflow让我们描述一个交互操作的图,并整个运行在Python外。(少数机器学习框架使用这种方法)

要使用Tensorflow,首先导入它。


In [3]:
import tensorflow as tf

我们操作符号变量以描述这些交互操作,让我们创建一个:


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

x不是一个特定的值。它是个占位符,当我们让Tensorflow运行时我们会输入数值。我们想要输入任意数量的MNIST图像,每个扁平化为784维向量。将其表示为2维浮点型数值的张量,维度维[None, 784]。(这里None意味着该维可以为任意长度。)

我们的模型还需要权重和偏置。我们可以把这些当作是额外的输入,但Tensorflow有个更好的主意:变量。一个变量是一个存在Tensorflow的交互计算图中的可变的张量。其可被使用甚至修改。对于机器学习程序,一般来说,模型参数是变量


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

我们通过给予tf.Variable初始值以创建这些变量:本例中,我们初始化Wb为全是0的张量。既然我们要学习Wb,它们的初始值并不重要。

注意W的维度是[784, 10],因为我们想要将784维图像向量乘以它以得到10维的证据向量。b的维度是[10],我们可以加到输出中去。

我们现在可以实现我们的模型了,这只需要一行代码!


In [6]:
y = tf.nn.softmax(tf.matmul(x, W) + b)

首先,我们使用tf.matmul(x, W)表达式来将x乘以W。这是从我们的方程中乘以它们的方法翻转出来的,我们有了$W_x$,这是处理具有多个输入的2维张量x的小技巧。然后加b,最后使用tf.nn.softmax

这就是了。几行设置之后,只用了一行代码来定义我们的模型。这并不是因为Tensorflow被设计为可以特别简单的使用softmax回归:只是描述从机器学习模型到物理模拟的多种数值计算只是一种非常灵活的方式。一旦定义,我们的模型可以在不同的设备上运行:您的计算机的CPU,GPU,甚至手机!

训练

为了训练我们的模型,我们首先需要定义一个指标来评估这个模型是好的。其实,在机器学习,我们通常定义指标来表示一个模型是坏的,这个指标称为成本(cost)或损失(loss),然后尽量最小化这个指标。但是,这两种方式是相同的。

一个非常常见的,非常漂亮的成本函数是“交叉熵”(cross-entropy)。交叉熵产生于信息论里面的信息压缩编码技术,但是它后来演变成为从博弈论到机器学习等其他领域里的重要技术手段。它的定义如下: $$H_{y'}(y) = -\sum_i y'_i \log(y_i)$$ $y$是我们预测的概率分布, $y'$是实际的分布(我们输入的one-hot vector)。比较粗糙的理解是,交叉熵是用来衡量我们的预测用于描述真相的低效性。更详细的关于交叉熵的解释超出本教程的范畴,但是你很有必要好好理解它

为了计算交叉熵,我们首先需要添加一个新的占位符用于输入正确值:


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

然后我们可以用 $-\sum y'\log(y)$ 计算交叉熵:


In [ ]:
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_sum函数将y的第二维中所有元素相加,最后tf.reduce_mean计算批次中所有示例的平均值。 注意在源码中,我们没有使用这个方程,因为其为数值不稳定的。我们替代为对非标准化逻辑使用tf.nn.softmax_cross_entropy_with_logits(例:我们对tf.matmul(x, W) + b)调用softmax_cross_entropy_with_logits),因为它计算softmax激活函数更为数值稳定。在你的代码中,考虑使用tf.nn.softmax_cross_entropy_with_logits

现在我们知道我们需要我们的模型做什么啦,用TensorFlow来训练它是非常容易的。因为TensorFlow拥有一张描述你各个计算单元的图,它可以自动地使用反向传播算法(backpropagation algorithm)来有效地确定你的变量是如何影响你想要最小化的那个成本值的。然后,TensorFlow会用你选择的优化算法来不断地修改变量以降低成本。


In [ ]:
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)

在这里,我们要求TensorFlow用梯度下降算法(gradient descent algorithm)以0.5的学习速率最小化交叉熵。梯度下降算法(gradient descent algorithm)是一个简单的学习过程,TensorFlow只需将每个变量一点点地往使成本不断降低的方向移动。当然TensorFlow也提供了许多其他优化算法:只要简单地调整一行代码就可以使用其他的算法。

TensorFlow在这里实际上所做的是,它会在后台给描述你的计算的那张图里面增加一系列新的计算操作单元用于实现反向传播算法和梯度下降算法。然后,它返回给你的只是一个单一的操作,当运行这个操作时,它用梯度下降算法训练你的模型,微调你的变量,不断减少成本。

我们可以在InteractiveSession中运行我们的模型了:


In [ ]:
sess = tf.InteractiveSession()

我们首先要添加一个操作来初始化我们创建的变量:


In [ ]:
tf.global_variables_initializer().run()

然后开始训练模型,这里我们让模型循环训练1000次!


In [ ]:
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个批处理数据点,然后我们用这些数据点作为参数替换之前的占位符来运行train_step

使用一小部分的随机数据来进行训练被称为随机训练(stochastic training)- 在这里更确切的说是随机梯度下降训练。在理想情况下,我们希望用我们所有的数据来进行每一步的训练,因为这能给我们更好的训练结果,但显然这需要很大的计算开销。所以,每一次训练我们可以使用不同的数据子集,这样做既可以减少计算开销,又可以最大化地学习到数据集的总体特性。

评估我们的模型

那么我们的模型性能如何呢?

首先让我们找出那些预测正确的标签。tf.argmax是一个非常有用的函数,它能给出某个tensor对象在某一维上的其数据最大值所在的索引值。由于标签向量是由0,1组成,因此最大值1所在的索引位置就是类别标签,比如tf.argmax(y,1)返回的是模型对于任一输入x预测到的标签值,而tf.argmax(y_,1)代表正确的标签,我们可以用tf.equal来检测我们的预测是否真实标签匹配(索引位置一样表示匹配)。


In [ ]:
correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))

这行代码会给我们一组布尔值。为了确定正确预测项的比例,我们可以把布尔值转换成浮点数,然后取平均值。例如,[True, False, True, True]会变成 [1,0,1,1],取平均值后得到0.75


In [ ]:
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

最后,我们计算所学习到的模型在测试数据集上面的正确率。


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

这个最终结果值应该大约是92%。

这个结果好吗?嗯,并不太好。事实上,这个结果是很差的。这是因为我们仅仅使用了一个非常简单的模型。不过,做一些小小的改进,我们就可以得到97%的正确率。最好的模型甚至可以获得超过99.7%的准确率!(想了解更多信息,可以看看这个关于各种模型的性能对比列表。)

比结果更重要的是,我们从这个模型中学习到的设计思想。不过,如果你仍然对这里的结果有点失望,可以查看下一个教程,在那里你可以学习如何用TensorFlow构建更加复杂的模型以获得更好的性能!