MNIST For ML Beginners

本教程面向 machine learning 与 TensorFlow 的初学者。

机器学习中的 “Hello World” 是 MNIST。

MNIST 是简单的计算机视觉数据集,包括手写数字例如:

同时带有标签 label

目标:训练一个模型来识别手写数字,采用 Softmax Regression 模型

关于此教程

源代码 mnist_softmax.py

本教程将要完成:

  • 学习 MNIST 数据和 softmax regressions
  • 基于对图像中每个像素的处理,建立一个识别手写数字的模型(函数)
  • 使用 TensorFlow 和数千个样本训练模型
  • 在测试集上检验

MNIST 数据

下载并载入数据

1
2
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data/', one_hot=True)
/root/anaconda3/envs/tensorflow/lib/python3.6/importlib/_bootstrap.py:219: RuntimeWarning: compiletime version 3.5 of module 'tensorflow.python.framework.fast_tensor_util' does not match runtime version 3.6
  return f(*args, **kwds)


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个部分:

  • 55000个训练集 mnist.train
  • 10000个测试集 mnist.test
  • 5000个验证集 mnist.validation

在机器学习中,将数据集这样划分是为了确保学习器泛化(generalization)能力的必要条件

每一幅图,可以称 image x 有 label y,例如

  • mnist.train.images
  • mnist.train.labels

每一幅图有 $28 \times 28$ 个像素,可以视为一个矩阵,可拉直成 784 长度的向量。

拉直后的数据会丢失图片的 2D 信息,最优秀的计算机视觉方法通常需要利用这些信息。但是在这里简单的 softmax regression 不需要用到。

因此 mnist.train.images 是一个 [55000, 784] 的张量(n 维数组)。

每张图片有一个数字0到9的标签

one-hot vectors:

one_hot_encoding (独热编码)。考虑多类情况。非 onehot,标签类似0,1,2,3…n 而 onehot 标签则是一个长度为 n 的数组,只有一个元素是1.0,其他元素是0.0。使用 onehot 的直接原因是现在多分类 cnn 网络的输出通常是 softmax 层,而它的输出是一个概率分布,从而要求输入的标签也以概率分布的形式出现,进而算交叉熵等。
例如 3 表示为[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]

mnist.train.labels 是 [55000, 10] 的张量

Softmax Regressions

Softmax Regressions 经常处理多分类问题。softmax 能够输出一列0到1的值,即使在更复杂的模型里,最后一步也往往采用softmax 层。

Softmax Regression 分为两步

  1. 对输入被分为具体的类的 evidence 求和
  2. 将 evidence 转换为 probabilities

我们计算对每一个像素的加权和。如果这个像素具有很强的证据说明这张图片不属于该类,那么相应的权值为负数,相反如果这个像素拥有有利的证据支持这张图片属于这个类,那么权值是正数。下面的图片显示了一个模型学习到的图片上每个像素对于特定数字类的权值。红色代表负数权值,蓝色代表正数权值。

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

$W_i$代表权重,$b_i$代表数字 i 类的偏置量,j 代表给定图片 x 的像素索引

softmax 函数可以将证据转换成概率 y:

$y = softmax(evidence)$

softmax 可以看成一个 activation 函数或 link 函数,它把线性函数输出转换为概率分布

softmax 函数定义为:
$softmax(x) = normalize(exp(x))$

展开可以写成
$softmax(x)_i = \frac{exp(x_i)}{\sum _j exp(x_j)}$

更多的时候定义为前一种形式

用图解释如下

写成等式

紧凑形式
$y = softmax(Wx + b)$

实施回归函数

为了用python实现高效的数值计算,我们通常会使用函数库,比如NumPy,会把类似矩阵乘法这样的复杂运算使用其他外部语言实现。不幸的是,从外部计算切换回Python的每一个操作,仍然是一个很大的开销。如果你用GPU来进行外部计算,这样的开销会更大。用分布式的计算方式,也会花费更多的资源用来传输数据。

TensorFlow也把复杂的计算放在python之外完成,但是为了避免前面说的那些开销,它做了进一步完善。Tensorflow不单独地运行单一的复杂计算,而是让我们可以先用图描述一系列可交互的计算操作,然后全部一起在Python之外运行。(这样类似的运行方式,可以在不少的机器学习库中看到。)

导入 TensorFlow

1
import tensorflow as tf

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

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

x 不是一个特定的值,而是一个占位符 placeholder,我们在 TensorFlow 运行计算时输入这个值。我们希望能够输入任意数量的MNIST图像,每一张图展平成784维的向量。我们用2维的浮点数张量来表示这些图,这个张量的形状是 [None,784]。(这里的None表示此张量的第一个维度可以是任何长度的。)

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

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

我们赋予tf.Variable不同的初值来创建不同的Variable:在这里,我们都用全为零的张量来初始化W和b。因为我们要学习W和b的值,它们的初值可以随意设置。

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

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

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

首先,我们用tf.matmul(X,W)表示x乘以W,对应之前等式里面的,这里x是一个2维张量拥有多个输入。然后再加上b,把和输入到tf.nn.softmax函数里面。

至此,我们先用了几行简短的代码来设置变量,然后只用了一行代码来定义我们的模型。TensorFlow不仅仅可以使softmax回归模型计算变得特别简单,它也用这种非常灵活的方式来描述其他各种数值计算,从机器学习模型对物理学模拟仿真模型。一旦被定义好之后,我们的模型就可以在不同的设备上运行:计算机的CPU,GPU,甚至是手机!

训练模型

minimize the cost or the loss

交叉熵:cross-entropy,交叉熵产生于信息论里面的信息压缩编码技术,但是它后来演变成为从博弈论到机器学习等其他领域里的重要技术手段。它的定义如下:

y 是我们预测的概率分布, y’ 是实际的分布(我们输入的one-hot vector)。比较粗糙的理解是,交叉熵是用来衡量我们的预测用于描述真相的低效性。

为计算交叉熵,需要新的 placeholder 来输入正确的 answer:

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

用 tf.log 计算 y 的每个元素的对数。接下来,我们把 y_ 的每一个元素和 tf.log(y) 的对应元素相乘。最后,用 tf.reduce_sum 计算 y 的第二个维度元素的和(按列求和),due to 参数 reduction_indices=[1]。最后 tf.reduce_mean 计算在一批(batch)的样本中的均值

但是在源代码中我们没有这么写,因为这是数值不稳定的。事实上,我们采用
tf.nn.softmax_cross_entropy_with_logits on the unnormalized logits (e.g., we call softmax_cross_entropy_with_logits on tf.matmul(x, W) + b)
因为这个数值更稳定

1
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y))

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

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

在这里,我们要求TensorFlow用梯度下降算法(gradient descent algorithm)以0.5的学习速率最小化交叉熵。梯度下降算法(gradient descent algorithm)是一个简单的学习过程,TensorFlow只需将每个变量一点点地往使成本不断降低的方向移动。当然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对象在某一维上的其数据最大值所在的索引值。由于标签向量是由0,1组成,因此最大值1所在的索引位置就是类别标签,比如tf.argmax(y,1)返回的是模型对于任一输入x预测到的标签值,而 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}))
0.9072

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

这个结果好吗?嗯,并不太好。事实上,这个结果是很差的。这是因为我们仅仅使用了一个非常简单的模型。不过,做一些小小的改进,我们就可以得到97%的正确率。最好的模型甚至可以获得超过99.7%的准确率!

分享到