机器学习初学者的MNIST

本教程适用于机器学习和TensorFlow都不熟悉的读者。 如果你已经知道MNIST是什么,以及什么是softmax(多项式逻辑)回归,你可能更喜欢这个更快的教程 确保在开始任何教程之前安装TensorFlow

当学习如何编程时,首先要做的事情就是打印“Hello World”。就像编程有Hello World一样,机器学习有MNIST。

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

它还包含每张图片的标签,告诉我们它是哪个数字。 例如,上述图像的标签是5,0,4和1。

在本教程中,我们将训练一个模型来查看图像并预测它们的位数。 我们的目标不是培养一个真正精心设计的模型来实现最先进的性能 - 尽管我们会在稍后给您提供代码! - 而是倾向于使用TensorFlow。 因此,我们将从一个非常简单的模型开始,称为Softmax回归。

本教程的实际代码非常短,所有有趣的内容都只发生在三行中。 然而,理解其背后的理念非常重要:TensorFlow的工作原理和核心机器学习概念。 正因为如此,我们将非常仔细地研究代码。

关于本教程

本教程是逐行解释mnist_softmax.py代码中发生的事情的解释。

您可以通过几种不同的方式使用本教程,其中包括:

我们将在本教程中完成的任务:

MNIST数据

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

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

The MNIST data is split into three parts: 55,000 data points of training data (mnist.train), 10,000 points of test data (mnist.test), and 5,000 points of validation data (mnist.validation). 这种分裂是非常重要的:机器学习中必不可少的是我们有单独的数据,我们没有从中学习,这样我们就可以确保我们所学到的知识实际上是泛化的!

如前所述,每个MNIST数据点都有两部分:手写数字图像和相应标签。 我们将调用图像“x”和标签“y”。 Both the training set and test set contain images and their corresponding labels; for example the training images are mnist.train.images and the training labels are mnist.train.labels.

每个图像是28像素×28像素。 我们可以把它解释为一大堆数字:

我们可以将这个数组变成28x28 = 784数字的向量。 只要我们在图像之间保持一致,无论如何将阵列弄平。 From this perspective, the MNIST images are just a bunch of points in a 784-dimensional vector space, with a very rich structure (warning: computationally intensive visualizations).

展平数据将丢弃有关图像二维结构的信息。 那不好吗? 那么,最好的计算机视觉方法会利用这种结构,我们将在后面的教程中讨论。 但是我们将在这里使用的简单方法,softmax回归(下面定义)不会。

The result is that mnist.train.images is a tensor (an n-dimensional array) with a shape of [55000, 784]. 第一维是图像列表中的索引,第二维是每个图像中每个像素的索引。 对于特定图像中的特定像素,张量中的每个条目都是介于0和1之间的像素强度。

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

对于本教程的目的,我们将要我们的标签作为“一个热点向量”。 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中的每个图像都是一个介于零和九之间的手写数字。 所以一个给定的图像可能只有十种可能的东西。 我们希望能够查看图像并给出每个数字的概率。 例如,我们的模型可能会查看一张九张图片,并且有80%确定这是一张九张图片,但给它一个5%的机会,因为它是一个八(因为顶部循环),并且对所有其他人有一定的概率,因为它不是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

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然后归一化这些权重,以便它们加起来为1,形成有效的概率分布。 (为了更直观地了解softmax函数,请查看Michael Nielsen书中的部分,其中包含交互式可视化。)

您可以将我们的softmax回归看成如下所示,尽管有更多x s。对于每个输出,我们计算x的加权和,添加一个偏差,然后应用softmax。

如果我们把它写成等式,我们得到:

[y1, y2, y3] = softmax(W11*x1 + W12*x2 + W13*x3 + b1,  W21*x1 + W22*x2 + W23*x3 + b2,  W31*x1 + W32*x2 + W33*x3 + b3)

我们可以将这个过程“矢量化”,将其转化为矩阵乘法和矢量加法。 这对计算效率很有帮助。 (这也是一种有用的思考方式。)

[y1, y2, y3] = softmax([[W11, W12, W13], [W21, W22, W23], [W31, W32, W33]]*[x1, x2, x3] + [b1, b2, b3])

更简洁,我们可以写出:

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

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

实现回归

为了在Python中进行有效的数值计算,我们通常使用像NumPy这样的库,这些库执行昂贵的操作,例如Python之外的矩阵乘法,使用以另一种语言实现的高效代码。 不幸的是,每次操作都会返回Python,但仍然会有很多开销。 如果您想要在GPU上运行计算或以分布式方式运行计算,则这种开销尤其糟糕,因为在这种情况下,传输数据的成本很高。

TensorFlow也在Python之外进行繁重的工作,但为了避免这种开销,它需要更进一步。 TensorFlow不是独立于Python运行一个昂贵的操作,而是让我们描述一个完全在Python之外运行的交互操作图。 (像这样的方法可以在几个机器学习库中看到。)

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

import tensorflow as tf

我们通过操纵符号变量来描述这些交互操作。 我们来创建一个:

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

x不是特定的值。 这是一个占位符,当我们要求TensorFlow运行计算时我们将输入一个值。 我们希望能够输入任意数量的MNIST图像,每个图像都被拼合成784维向量。 我们将其表示为浮点数的二维张量,其形状为[无,784] (这里表示维度可以是任意长度。)

我们还需要我们模型的权重和偏差。 我们可以想象将这些视为额外的输入,但TensorFlow有更好的方式来处理它:Variable 一个变量​​是一个可修改的张量,它存在于TensorFlow的交互操作图中。 它可以被使用,甚至被计算修改。 对于机器学习应用程序,人们通常将模型参数设置为Variable

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

We create these Variables by giving tf.Variable the initial value of the Variable: in this case, we initialize both W and b as tensors full of zeros. 既然我们要学习Wb,它们最初的内容并不重要。

Notice that W has a shape of [784, 10] because we want to multiply the 784-dimensional image vectors by it to produce 10-dimensional vectors of evidence for the difference classes. b的形状为[10],所以我们可以将它添加到输出中。

我们现在可以实现我们的模型。 它只需要一行来定义它!

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

First, we multiply x by W with the expression tf.matmul(x, W). 当我们将它们乘以我们的Wx时,我们将它翻转过来,作为处理x是具有多个输入的2D张量的小技巧。 We then add b, and finally apply tf.nn.softmax.

而已。 经过几次简单的设置之后,我们只用一条线来定义我们的模型。 这并不是因为TensorFlow的设计使得softmax回归特别容易:它只是一种非常灵活的方式来描述从机器学习模型到物理模拟的各种数值计算。 一旦定义,我们的模型可以在不同的设备上运行:计算机的CPU,GPU甚至是手机!

训练

为了训练我们的模型,我们需要定义模型的好处。 呃,实际上,在机器学习中,我们通常定义模型的坏意味着什么。 我们称之为成本或损失,它表示我们的模型与我们期望的结果有多远。 我们尽量减少误差,误差越小,我们的模型越好。

确定模型损失的一个非常常见的非常好的函数被称为“交叉熵”。跨熵来源于信息理论中对信息压缩代码的思考,但它在从赌博到机器学习等许多领域都是一个重要思想。 它被定义为:

H_{y'}(y) = -\sum_i y'_i \log(y_i)

其中y是我们预测的概率分布,y'是真实分布(带有数字标签的单热矢量)。 在一些粗略的意义上,交叉熵正在测量我们的预测对于描述真相的效率低下。 有关交叉熵的更多细节超出了本教程的范围,但非常值得理解

为了实现交叉熵,我们需要首先添加一个新的占位符来输入正确的答案:

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中每个元素的对数。 Next, we multiply each element of y_ with the corresponding element of tf.log(y). 然后,由于reduction_indices = [1]参数的原因,tf.reduce_sum将元素添加到y的第二维中。 Finally, tf.reduce_mean computes the mean over all the examples in the batch.

请注意,在源代码中,我们不使用这个公式,因为它在数值上不稳定。 相反,我们将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知道整个计算图,因此它可以自动使用反向传播算法来有效地确定变量如何影响您要求它最小化的损失。 然后它可以应用您选择的优化算法来修改变量并减少损失。

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

In this case, we ask TensorFlow to minimize cross_entropy using the gradient descent algorithm with a learning rate of 0.5. 梯度下降是一个简单的过程,其中TensorFlow只是简单地将每个变量向降低成本的方向移动一点点。 But TensorFlow also provides many other optimization algorithms: using one is as simple as tweaking one line.

TensorFlow实际上在幕后做了什么,是在图形中添加新的操作来实现反向传播和渐变下降。 然后它会给你一个单一的操作,在运行时进行梯度下降训练,略微调整你的变量以减少损失。

我们现在可以在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})

循环的每一步,我们从我们的训练集中获得一百个随机数据点的“批次”。 我们在批次数据中运行train_step以替换占位符

使用小批量随机数据称为随机训练 - 在这种情况下,随机梯度下降。 理想情况下,我们希望将所有数据用于每一步培训,因为这会让我们更好地了解我们应该做什么,但这很昂贵。 所以,相反,我们每次都使用不同的子集。 这样做既便宜又有很多相同的好处。

评估我们的模型

我们的模型有多好?

那么,首先让我们弄清楚我们在哪里预测了正确的标签。 tf.argmax是一个非常有用的函数,它为沿着某个轴的张量中的最高条目提供索引。 例如,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))

最后,我们要求我们的测试数据的准确性。

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

这应该是大约92%。

这很好吗? 那么,不是真的。 事实上,这很糟糕。 这是因为我们正在使用一个非常简单的模型。 通过一些小的变化,我们可以达到97%。 最好的模型可以达到超过99.7%的准确度! (有关更多信息,请查看结果列表。)

重要的是我们从这个模型中学到了东西。 尽管如此,如果你对这些结果感到有些失望,请查看下一个教程,在那里我们做得更好,并学习如何使用TensorFlow构建更复杂的模型!