【Tensorflow r1.0 文档翻译】机器学习的HelloWorld -- MNIST手写数字识别

本教程面向那些不熟悉机器学习TensorFlow的读者。如果你已经知道MNIST是什么,softmax(多项Logistic)回归是什么,你可能更喜欢这个更快节奏的教程。在开始教程之前,请确认安装TensorFlow

当一个人开始学习如何编程时,有一个传统,就是编写的第一个程序是能够打印”Hello World.”的程序。正如编程中的”Hello World”一样,机器学习中有MNIST。

MNIST是一个简单的计算机视觉数据集。它由像以下这样的手写数字的图像组成:

它还包括每个图像的标签,用于标识是哪个数字。例如,上述图像的标签是5,0,41

在本教程中,我们将训练一个模型,用来查看图像并预测它们是什么数字。我们的目标不是训练一个真正精准的,拥有高性能的模型,而是浅尝辄止的来体验一下TensorFlow的使用。 - 尽管我们稍后会给出实现这种效果的代码。因此,我们将从一个非常简单的,称为Softmax回归的模型开始。

这个教程的实际代码非常短,其中真正有趣的东西只有三行代码。然而,了解背后的想法是非常重要的:TensorFlow如何工作和核心机器学习概念。因此,我们将非常仔细地完成这部分代码。

关于本教程

本教程是对mnist_softmax.py中的代码进行逐行解释。

您可以通过以下几种不同的方式使用本教程:

  • 在阅读每行的解释时,将每个代码段逐行复制并粘贴到Python环境中。
  • 在阅读教程期间,运行整个mnist_softmax.py,并使用本教程来了解您不清楚的代码行。

我们将在本教程中完成:

  • 了解MNIST数据和softmax回归。
  • 创建一个函数,它是一个用于识别数字的模型,其识别原理是基于查看图像中的每个像素的值来实现的。
  • 使用TensorFlow来训练模型以识别数字,其训练方式是“查看”数千个示例(运行我们的第一个TensorFlow会话来执行此逻辑)。
  • 使用我们的测试数据检查模型的精度。

MNIST数据集

MNIST数据集托管在Yann LeCun的站点。如果您要复制粘贴本教程中的代码,请从这两行代码开始,这两行代码将自动下载并读入数据:

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

MNIST数据被分为三部分: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的向量。如何平铺数组其实并不重要,重要的是要保证图像和数组之间的一致性。从这个角度来看,MNIST图像只是784维向量空间中的一堆点,具有非常丰富的结构(警告:计算密集的可视化)。

展平数据丢弃了关于图像的2D结构的信息。这样做是不是并不够好?没错,最好的计算机视觉方法确实可以利用这种2D结构信息,我们将在后面的教程进行介绍。但是我们在这里所使用的一种简单方法:softmax回归(下面会给出定义),不会利用到这种信息。

mnist.train.images是形状为[55000,784]的张量(n维数组)。第一个维度是在列表中图像的索引,第二个维度是每个图像中的每个像素点的索引。对于特定图像中的特定像素,张量中的每个条目是介于0和1之间的像素强度。

MNIST中的每个图像都有相应的标签,标签用介于0到9之间的数字表示图像中绘制的数字。

为了达到本教程的目的,我们需要要将我们的标签作为“one-hot 向量”。one-hot向量是指在大多数维度上数值为0,仅在其中一个维度上数值为1的向量。在这种情况下,第n个数字将被表示为在第n维中为1的向量。例如,3将表示为$[0,0,0,1,0,0,0,0,0,0]$。因此,mnist.train.labels是一个形状为[55000, 10]的数字矩阵。

现在,我们可以开始构建我们的模型啦!

Softmax回归

我们知道MNIST中的每个图像都是一个在0和9之间的手写数字。因此,对于给定的图像,只有10种可能的结果。我们想要能够看到一个图像,并给出它的每个数字的概率。例如,用我们的模型来查看一个9的图片,80%的可能性确认是9,但有5%的可能是8(因为8和9顶部都有一个圈),剩余的可能性分布在其他数值上。

这是一个softmax回归的典型案例。如果你想给一个对象赋予其表示不同数字的概率,可以使用softmax,因为softmax可以得出一组介于0到1之间的值,并且这组值加起来结果为1。即使在以后,当我们训练其他更复杂的模型时,最后一步也是一层softmax。

softmax回归有两个步骤:首先我们将图片中属于某个特定数字的证据(evidence)相加,然后将该证据转换为概率。

为了计算给定图像在特定类中的证据,我们对像素强度进行加权求和。如果像素点有很高的强度表示和对应的标签数字不匹配,那么这一点的权值是负数,相反,权值是正数。

下面的图片显示了一个模型学习到的图片上每个像素对于特定数字类的权值。红色表示负权重,蓝色表示正权重。

我们还需要增加一个偏置量(bias),因为输入往往会带有一些无关的干扰量。因此对于给定的输入图片x它代表的是数字i的证据可以表示为:

$$
\text{evidence}_i = \sum_j W_{i,~ j} x_j + b_i
$$

其中,$W_i$表示权值,$b_i$代表$i$类别的偏置量,$j$代表给定图片$x$的像素索引,用于像素求和。然后用softmax函数可以把这些证据转换成概率y

$$
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定义为第一种形式:对其输入求幂,然后将其归一化处理。这里幂运算表示,更大的证据对应更大的假设模型(hypothesis)里面的乘数权重值。反之,拥有更少的证据意味着在假设模型里面拥有更小的乘数系数。假设模型里的权值不可以是0值或者负值。Softmax然后会正则化这些权重值,使它们的总和等于1,以此构造一个有效的概率分布。(更多的关于Softmax函数的信息,可以参考Michael Nieslen的书里面的这个部分,其中有关于softmax的可交互式的可视化解释。)

softmax回归可以表示为下面这张图,不过真实情况下会有更多的$x$值。我们通过计算出$x$的权值之和加上一个偏置量,然后代入到一个softmax中,来计算出每个输出值。

如果我们把它写成方程的形式,我们将得到:

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

更紧凑的表达形式如下:

$$
y = \text{softmax}(Wx + b)
$$

现在让我们把它变成TensorFlow可以使用的形式。

回归的实现

为了在Python中进行高效的数值计算,我们通常使用像NumPy这样的库,它们会把类似矩阵乘法这样的复杂运算使用其他外部语言实现。不幸的是,从外部计算切换回Python的每一个操作,仍然是一个很大的开销。如果要在GPU上以分布式方式运行计算,那么这种开销尤其糟糕,其中传输数据的成本很高。

TensorFlow也在Python之外做了很大量的计算工作,但它做了进一步的完善以改善前面说的那种切换。TensorFlow不是独立于Python运行一个昂贵的操作,而是让我们可以先用图描述一系列可交互的计算操作,然后全部一起在Python之外运行。(这样类似的运行方式,可以在不少的机器学习库中看到。)

要使用TensorFlow,首先我们需要导入它。

1
import tensorflow as tf

我们通过操作符号变量来描述这些交互的操作单元。让我们用下面的方式创建一个:

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

x不是一个特定的值。它是一个占位符(placeholder),当我们要求TensorFlow运行一个计算时,我们将输入一个值。我们希望能够输入任意数量的MNIST图像,其中每个图像都被展开为784维向量。我们将其表示为float类型的2-D张量,形状为[None, 784]。(这里的None表示维度可以是任何长度。)

我们的模型还需要权重和偏差。当然我们可以把它们当做是另外的输入(使用占位符),但TensorFlow有一个更好的方法来表示它们:VariableVariable代表一个可修改的张量,它存在于TensorFlow中用于描述交互性操作的图中。在计算过程中,它们可以被拿来使用甚至可以修改。对于机器学习应用,一般都会有模型参数,可以用Variable表示。

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

我们通过给与tf.Variable初始值来创建Variable:在这种情况下,我们将Wb初始化为全部为0的张量。因为我们要通过学习得到Wb,因此它们的初始值具体是什么并不重要。

注意,W的形状为[784,10],因为我们想要用784维的图片向量乘以它以得到一个10维的证据值向量,其中每一位对应着不同数字类别。b的形状是[10],所以我们可以直接把它加到输出上面。

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

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

首先,我们通过表达式tf.matmul(x, W)xW相乘。这对应于前面方程中的$Wx$,x是一个拥有多个输入的2D张量。紧接着,我们加上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)。比较粗糙的理解是,交叉熵是用来衡量相对于真实值我们所给出的预测的低效性。有关交叉熵的更详细的讨论超出了本教程的范畴,但理解它的原理很有必要。

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

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

然后,我们可以实现交叉熵方法:$-\sum y’\log(y)$

1
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_sumy中的第二维中的元素相加求和。最后,通过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知道用于计算的整个图(graph),它会自动地使用反向传播算法来有效地确定你的变量是如何影响你想要最小化的那个成本值的。然后它可以应用您选择的优化算法修改变量和减少损失。

1
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)

在这里,我们通过使用学习率为0.5的梯度下降算法令TensorFlow最小化cross_entropy(交叉熵)。梯度下降是一个简单的程序,它的原理是每次向着减少损失的方向移动一小步,来最小化代价函数。但TensorFlow也提供了很多其他的优化算法,只需要简单的调整一行代码就可以随意切换。

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

我们现在可以在InteractiveSession中启动我们的模型:

1
sess = tf.InteractiveSession()

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

1
tf.global_variables_initializer().run()

让我们开始执行训练 - 我们将运行1000次训练步骤!

1
2
3
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对象在某一维上的其数据最大值所在的索引值。例如,tf.argmax(y,1)是我们的模型认为每个输入最可能的标签,而tf.argmax(y_,1)是正确的标签。我们可以用tf.equal来检查我们我预测值与真实值是否相符。

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

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

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

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

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

结果大概维持在92%左右。

这种结果很好吗?其实并不是很好。其实,它相当差。这是因为我们使用的是一个非常简单的模型。我们可以通过做一些简单的修改,可以将正确率提高到97%。事实上,最优秀的模型可以达到超过99.7%的准确率!(想了解更多信息,可以看看这个关于各种模型的性能对比列表。)

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