Deep MNIST for Experts

关于本教程

本教程第一部分解释了mnist_softmax.py的原理。这是 Tensorflow 模型的基本实现。第二部分展示了一些改进精度的方法。

完整的 DNN 实现代码mnist_deep.py

本教程我们将实现:

  • 创建一个 softmax regression 函数建模来识别 MNIST 数字,基于图像中的每一个像素
  • 使用 Tensorflow 训练模型来识别数字通过训练几千个样本
  • 在测试集上测试模型精度
  • 建立,训练并测试多层 CNN 来改善精度

安装

下载 MNIST 数据

以下代码自动下载并读取 MNIST 数据集

1
2
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
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 是存储 training,validation 和 testing 数据集的 NumPy 数组。它同时提供一个函数来通过 data minibatches 来迭代,接下来会介绍。

启动 TensorFlow InteractiveSession

TensorFlow 依赖高效率的 C++ 后台(backend)来计算。对后台的连接称为一个 session。一个 TensorFlow 程序的运行首先要创建一个计算图,然后在 session 中运行。

这里,我们使用更加方便的 InteractiveSession 类。通过它,你可以更加灵活地构建你的代码。它能让你在运行图的时候,插入一些计算图,这些计算图是由某些操作 (operations) 构成的。这对于工作在交互式环境中的人们来说非常便利,比如使用 IPython。如果你没有使用 InteractiveSession,那么你需要在启动 session 之前构建整个计算图,然后启动该计算图。

1
2
import tensorflow as tf
sess = tf.InteractiveSession()

计算图

为了在Python中进行高效的数值计算,我们通常会使用像NumPy一类的库,将一些诸如矩阵乘法的耗时操作在Python环境的外部来计算,这些计算通常会通过其它语言并用更为高效的代码来实现。但遗憾的是,每一个操作切换回Python环境时仍需要不小的开销。如果你想在GPU或者分布式环境中计算时,这一开销更加可怖,这一开销主要可能是用来进行数据迁移。

TensorFlow也是在Python外部完成其主要工作,但是进行了改进以避免这种开销。其并没有采用在Python外部独立运行某个耗时操作的方式,而是先让我们描述一个交互操作图,然后完全将其运行在Python外部。这与Theano或Torch的做法类似。

因此Python代码的目的是用来构建这个可以在外部运行的计算图,以及安排计算图的哪一部分应该被运行。

建立 Softmax Regression Model

在这一节我们建立一个 softmax regression model 带有单一线性层。下一节,我们会扩展成有多层 CNN 的 softmax regression。

Placeholders

我们通过为输入图像和目标输出类别创建节点,来开始构建计算图。

1
2
x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])

这里 x 与 y_ 不是具体的数字,它们是一个占位符 placeholder ———— 在运行计算时我们会给它赋值。

输入图片x是一个2维的浮点数张量。这里,分配给它的shape为[None, 784],其中784是一张展平的MNIST图片的维度。None表示其值大小不定,在这里作为第一个维度值,用以指代batch的大小,意即x的数量不定。输出类别值y_也是一个2维张量,其中每一行为一个10维的one-hot向量,用于代表对应某一MNIST图片的类别。

虽然placeholder的shape参数是可选的,但有了它,TensorFlow能够自动捕捉因数据维度不一致导致的错误。

Variables

我们现在定义模型权重 W 和 biases b。可以将它们当作额外的输入量,但是TensorFlow有一个更好的处理方式:Variable。一个 Variable 代表着TensorFlow计算图中的一个值,能够在计算过程中使用,甚至进行修改。在机器学习的应用过程中,模型参数一般用 Variable 来表示。

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

我们在调用 tf.Variabl e的时候传入初始值。在这个例子里,我们把 W 和 b 都初始化为零向量。W 是一个784x10的矩阵(因为我们有784个特征和10个输出值)。b是一个10维的向量(因为我们有10个分类)。

Variable 需要通过seesion初始化后,才能在session中使用。这一初始化步骤为,为初始值指定具体值(本例当中是全为零),并将其分配给每个 Variable,可以一次性为所有 Variable 完成此操作。

1
sess.run(tf.global_variables_initializer())

Predicted Class 和 Loss Function

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

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

可以很容易的为训练过程指定最小化误差用的损失函数,我们的损失函数是目标类别和预测类别之间的交叉熵。

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

注意到 tf.nn.softmax_cross_entropy_with_logits internally applies the softmax on the model’s unnormalized model prediction and sums across all classes。tf.reduce_mean 对和取平均。

训练模型

TensorFlow 知道完整的计算图,可以使用自动的微分运算来寻找损失函数关于每个变量的梯度。TensorFlow 有大量内置的优化算法。例如,我们可以使用最速下降法,步长为0.5,来优化交叉熵。

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

上一代码中,TensorFlow 实际做的是将 new operations add 到计算图中。这些操作包括计算梯度,计算每个参数的步长变化,并且计算出新的参数值。

返回的 operation 是 train_step,运行的时候,它会应用梯度下降算法更新参数。因此,整个模型的训练可以通过反复地运行 train_step 来完成。

1
2
3
for _ in range(1000):
batch = mnist.train.next_batch(100)
train_step.run(feed_dict={x: batch[0], y_: batch[1]})

每一步迭代,我们都会加载100个训练样本,然后执行一次train_step,并通过feed_dict将x 和 y_ 张量占位符用训练训练数据替代。

注意,在计算图中,你可以用feed_dict来替代任何张量,并不仅限于替换占位符。

评估模型

首先找出预测正确的标签。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))

这会返回一列 bool 变量。为了获得正确率,我们将布尔值转换为浮点数来代表对、错,然后取平均值。例如:[True, False, True, True]变为[1,0,1,1],计算出平均值为0.75。

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

最后,我们可以评估模型在 test 数据上的精度,大约是 92% 的正确率。

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

建立多层 CNN

在 MNIST 上只有 92% 的精度非常差。在这一部分,我们会修正,把这个简单的模型转换为一些更复杂的模型:一个小型的 CNN。这会是精度达到 99.2%。

这是我们将要建立的模型,用 TensorBoard 来创建的。

初始化 Weight

为了创建这个模型,我们首先需要创建大量 weights 和 biases。这个模型中的权重在初始化时应该加入少量的噪声来打破对称性以及避免0梯度。由于我们使用的是ReLU神经元,因此比较好的做法是用一个较小的正数来初始化偏置项,以避免神经元节点输出恒为0的问题(dead neurons)。为了不在建立模型的时候反复做初始化操作,我们定义两个函数用于初始化。

1
2
3
4
5
6
7
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial)

def bias_variable(shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial)

tf.truncated_normal(shape, mean=0.0, stddev=1.0, dtype=tf.float32, seed=None, name=None)

用于生成随机数tensor的。尺寸是shape

truncated_normal:截断正态分布随机数,均值mean,标准差stddev,不过只保留[mean-2stddev,mean+2stddev]范围内的随机数

tf.constant

tf.constant(value,dtype=None,shape=None,name=’Const’)
创建一个常量tensor,按照给出value来赋值,可以用shape来指定其形状。value可以是一个数,也可以是一个list。
如果是一个数,那么这个常量中所有值的按该数来赋值。
如果是list,那么len(value)一定要小于等于shape展开后的长度。赋值时,先将value中的值逐个存入。不够的部分,则全部存入value的最后一个值。

卷积(Convolution)和池化(Pooling)

TensorFlow在卷积和池化上有很强的灵活性。我们怎么处理边界?步长应该设多大?在这个实例里,我们会一直使用vanilla版本(original version)。

步长 stride 为1,pad 为 0,保证输出和输入是同一个大小。我们的池化用简单传统的2x2大小的模板做max pooling。为了代码更简洁,我们把这部分抽象成一个函数。

1
2
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

原文来自这里

tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, name=None)

除去name参数用以指定该操作的name,与方法有关的一共五个参数:

第一个参数input:指需要做卷积的输入图像,它要求是一个Tensor,具有[batch, in_height, in_width, in_channels]这样的shape,具体含义是[训练时一个batch的图片数量, 图片高度, 图片宽度, 图像通道数],注意这是一个4维的Tensor,要求类型为float32和float64其中之一

第二个参数filter:相当于CNN中的卷积核,它要求是一个Tensor,具有[filter_height, filter_width, in_channels, out_channels]这样的shape,具体含义是[卷积核的高度,卷积核的宽度,图像通道数,卷积核个数],要求类型与参数input相同,有一个地方需要注意,第三维in_channels,就是参数input的第四维

第三个参数strides:卷积时在图像每一维的步长,这是一个一维的向量,长度4

第四个参数padding:string类型的量,只能是”SAME”,”VALID”其中之一,这个值决定了不同的卷积方式(后面会介绍)

第五个参数:use_cudnn_on_gpu:bool类型,是否使用cudnn加速,默认为true

结果返回一个Tensor,这个输出,就是我们常说的feature map,shape仍然是[batch, height, width, channels]这种形式。

那么TensorFlow的卷积具体是怎样实现的呢,用一些例子去解释它:

1.考虑一种最简单的情况,现在有一张3×3单通道的图像(对应的shape:[1,3,3,1]),用一个1×1的卷积核(对应的shape:[1,1,1,1])去做卷积,最后会得到一张3×3的feature map

2.增加图片的通道数,使用一张3×3五通道的图像(对应的shape:[1,3,3,5]),用一个1×1的卷积核(对应的shape:[1,1,1,1])去做卷积,仍然是一张3×3的feature map,这就相当于每一个像素点,卷积核都与该像素点的每一个通道做卷积。

1
2
3
4
input = tf.Variable(tf.random_normal([1,3,3,5]))
filter = tf.Variable(tf.random_normal([1,1,5,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='VALID')

3.把卷积核扩大,现在用3×3的卷积核做卷积,最后的输出是一个值,相当于情况2的feature map所有像素点的值求和

1
2
3
4
input = tf.Variable(tf.random_normal([1,3,3,5]))
filter = tf.Variable(tf.random_normal([3,3,5,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='VALID')

4.使用更大的图片将情况2的图片扩大到5×5,仍然是3×3的卷积核,令步长为1,输出3×3的feature map

1
2
3
4
input = tf.Variable(tf.random_normal([1,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='VALID')

注意我们可以把这种情况看成情况2和情况3的中间状态,卷积核以步长1滑动遍历全图,以下x表示的位置,表示卷积核停留的位置,每停留一个,输出feature map的一个像素

…..

.xxx.

.xxx.

.xxx.

…..

5.上面我们一直令参数padding的值为‘VALID’,当其为‘SAME’时,表示卷积核可以停留在图像边缘,如下,输出5×5的feature map

1
2
3
4
input = tf.Variable(tf.random_normal([1,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='SAME')

xxxxx

xxxxx

xxxxx

xxxxx

xxxxx

6.如果卷积核有多个

1
2
3
4
input = tf.Variable(tf.random_normal([1,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,7]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='SAME')

此时输出7张5×5的feature map

7.步长不为1的情况,文档里说了对于图片,因为只有两维,通常strides取[1,stride,stride,1]

1
2
3
4
5
input = tf.Variable(tf.random_normal([1,5,5,5]))

filter = tf.Variable(tf.random_normal([3,3,5,7]))

op = tf.nn.conv2d(input, filter, strides=[1, 2, 2, 1], padding='SAME')

此时,输出7张3×3的feature map

x.x.x

…..

x.x.x

…..

x.x.x

8.如果batch值不为1,同时输入10张图

1
2
3
4
input = tf.Variable(tf.random_normal([10,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,7]))

op = tf.nn.conv2d(input, filter, strides=[1, 2, 2, 1], padding='SAME')

每张图,都有7张3×3的feature map,输出的shape就是[10,3,3,7]

1
2
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

tf.nn.max_pool(value, ksize, strides, padding, name=None)

参数是四个,和卷积很类似:

第一个参数value:需要池化的输入,一般池化层接在卷积层后面,所以输入通常是feature map,依然是[batch, height, width, channels]这样的shape

第二个参数ksize:池化窗口的大小,取一个四维向量,一般是[1, height, width, 1],因为我们不想在batch和channels上做池化,所以这两个维度设为了1

第三个参数strides:和卷积类似,窗口在每一个维度上滑动的步长,一般也是[1, stride,stride, 1]

第四个参数padding:和卷积类似,可以取’VALID’ 或者’SAME’

返回一个Tensor,类型不变,shape仍然是[batch, height, width, channels]这种形式

第一层卷积层

第一层卷积层包含卷积与max pooling。卷积在每个5x5的patch中算出32个特征。32个filters,每一个都有一个大小为5 * 5的窗口卷积的权重张量形状是[5, 5, 1, 32],前两个维度是patch的大小,接着是输入的通道数目,最后是输出的通道数目。 而对于每一个输出通道都有一个对应的偏置量。

1
2
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])

为了用这一层,我们把x变成一个4d向量,其第2、第3维对应图片的宽、高,最后一维代表图片的颜色通道数(因为是灰度图所以这里的通道数为1,如果是rgb彩色图,则为3)。

1
x_image = tf.reshape(x, [-1, 28, 28, 1])

我们把x_image和权值向量进行卷积,加上偏置项,然后应用ReLU激活函数,最后进行max pooling。The max_pool_2x2 method will reduce the image size to 14x14.

1
2
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

第二层卷积层

为了构建一个更深的网络,我们会把几个类似的层堆叠起来。第二层中,每个5x5的patch会得到64个特征。

1
2
3
4
5
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

密连接层

现在,图片尺寸减小到7x7,我们加入一个有1024个神经元的全连接层,用于处理整个图片。我们把池化层输出的张量reshape成一些向量,乘上权重矩阵,加上偏置,然后对其使用ReLU。

1
2
3
4
5
W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

Dropout

为了减少过拟合,我们在输出层之前加入dropout。我们用一个placeholder来代表一个神经元的输出在dropout中保持不变的概率。这样我们可以在训练过程中启用dropout,在测试过程中关闭dropout。 TensorFlow的tf.nn.dropout操作除了可以屏蔽神经元的输出外,还会自动处理神经元输出值的scale。所以用dropout的时候可以不用考虑scale。

1
2
keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

输出层

最后加上输出层,一层 softmax regression。

1
2
3
4
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2

训练并评估模型

这个模型的效果如何呢?

为了进行训练和评估,我们使用与之前简单的单层SoftMax神经网络模型几乎相同的一套代码。

区别:

  • 我们会用更加复杂的ADAM优化器代替最速下降法
  • 在feed_dict中加入额外的参数keep_prob来控制dropout比例
  • 然后每100次迭代输出一次日志。

我们也使用tf.Session 代替之前的 tf.InteractiveSession。这将更有助于区分创建计算图(model specification)与处理评估计算图(model fitting)。使得代码更简洁。这里的 tf.Session 代码写在 python 的 with 块中,使得块运行结束后自动销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
labels=y_, logits=y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for i in range(20000):
batch = mnist.train.next_batch(50)
if i % 100 == 0:
train_accuracy = accuracy.eval(feed_dict={x: batch[0],
y_: batch[1],
keep_prob: 1.0})
print('step %d, training accuracy %g' % (i, train_accuracy))
train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
print('test accuracy %g' % accuracy.eval(feed_dict={
x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
step 0, training accuracy 0.12
step 100, training accuracy 0.8
step 200, training accuracy 0.88
step 300, training accuracy 0.98
step 400, training accuracy 0.92
step 500, training accuracy 0.94
......
step 19000, training accuracy 1
step 19100, training accuracy 1
step 19200, training accuracy 1
step 19300, training accuracy 1
step 19400, training accuracy 1
step 19500, training accuracy 1
step 19600, training accuracy 1
step 19700, training accuracy 1
step 19800, training accuracy 1
step 19900, training accuracy 1
test accuracy 0.9922

以上代码,在最终测试集上的准确率大概是99.2%

对这个简单的 CNN 来说,是否有 dropout 对最后的表现几乎是一样的。Dropout 是减少过拟合非常有效的方法,它在训练更大型的 NN 时会更有效。

分享到