文章

Tensorflow - Deep MNIST for Experts

原文地址: 这里

原文首先回顾了一下 MNIST For ML Beginners 中的代码,这里我就不重复了。

卷积网络

关于卷积神经网络,本文不打算详细介绍,不然篇幅太长也非入门教程本意。详情看这里

以下是简单的介绍期望给大家一个概念上的理解

卷积和池化

卷积和池化用非常通俗的语言讲,卷积就是如何在一个二维图像上选择一小块儿并进行数值变换,池化就是在选择的这一小块儿经过变换的图像上如何进行运算。

以下举例一个2 * 2的区域,stride = 1,也就是每次移动一列,完成之后移动一行。通俗的模拟一下:

对于一个4 * 4大小的图像,处理的矩形范围序列(左上角为1, 1)

  • (1, 1) (1, 2) (2, 1) (2, 2)
  • (1, 2) (1, 3) (2, 2) (2, 3)
  • (1, 3) (1, 4) (2, 3) (2, 4)

完成了一行,移动一行(因为stride = 1嘛)

  • (2, 1) (2, 2) (3, 1) (3, 2)

由此可见一个4 * 4的图像经过这样的处理之后变成一个3 * 3的矩阵,tutorial中为了让输入和输出的Shape一样,在最后一列和最后一行都补上了0。

池化函数选择的是最大池化函数,也就是用这个2 * 2的矩阵中最大的元素来表示。

Tutorial中对应的实现:

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

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

这里说明几点:

  • conv2d函数的strides[0] = strides[3] = 1是必须的,strides[1]strides[2]是水平和垂直的偏移量,一般也一样了。
  • padding算法有两个SAME和VALID:
    • SAME

    means that the output feature map has the same spatial dimensions as the input feature map. Zero padding is introduced to make the shapes match as needed, equally on every side of the input map.

    表示输出和输入的Shape是一样的,用0充填。

    • VALID

    means no padding.

    表示不进行Padding。

  • max_pool函数复杂一些:
    • ksize表示输入的x矩阵各个维度的窗口大小,先只看中间的两个,也就是ksize[1]ksize[2],分别表示高度和宽度,因为2 * 2所以就是(2, 2)了。
    • strides表示每次移动的窗口的大小,这里没有重叠的部分 (注意和conv2d的strides区分开啊,卷积的过程中已经使用了strides = 1来移动窗口了,本例中pooling的窗口是木有重叠的),所以和ksize的值是一致的。
    • x是一个4维的Tensor[batch, height, width, channels]

卷积层

以上卷积和池化了解之后,我们开始构建卷积层,Tutorial这里有一些设定:

  • 卷积采用了一个5 * 5的矩阵作为Kernel,输出32个特征。
  • 这一层的输入channels = 1,因为输入的是灰度图像,只有一个的颜色特征(如果是RGB的话,那就是3了)

所以卷积的参数是一个Shape = (5, 5, 1, 32)的Tensor

1
2
3
4
5
6
7
8
9
10
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)

W_conv1 = weight_variable([5, 5, 1, 32])    # 卷积参数
b_conv1 = bias_variable([32])               # 每个输出特征的bias参数
  • weight_variable函数中使用随机数来初始化这个Tensor,防止梯度为0之类的情况。
  • bias_variable函数中用0.1初始化了bias参数

接下来我们需要对输入的x进行处理(还记得x是什么形状吗?Shape = (None, 784)),使得它可以符合第一层网络的输入:

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

理解这个函数有一点小绕,shape = [-1, 28, 28, 1],第一维-1表示该维度的长度不限(动态计算出来的),第二和第三维分别是高和宽( 不是宽和高,注意顺序 ),最后一维是是特征数,因为channels = 1嘛(上文解释过了),所以这里也是一。

形象的说,这个操作把图像从一个一维向量还原为了一个28 * 28的图像,那么为什么不是直接28 * 28?假设一下这是一个RGB图像,那么每个点是3个浮点数表示的,这样就很容易理解为什么需要Rank = 3的Tensor来表示图像了。

这个变换过程我擅自用代码描述一下,可能会更容易理解一下(第一维忽略,这个很好理解,表示N个样本嘛):

1
2
3
4
5
6
7
8
9
index = 0
vector = [ 0.1, 0.2, 0.3, .., 0.1 ] # 长度是784
tensor = SomeMagicType()            # 就是表示这个意思啦

for d1 in range(0, 28):             # 这是迭代高度(也就是行),高高高!
    for d2 in range(0, 28):         # 这是迭代宽度(也就是列),宽宽宽!
        for d3 in range(0, 1):      # 这就是channels了,因为是灰度图像,所以range的第二个参数是1,重复三遍:灰度灰度灰度!
            tensor[d1][d2][d3] = vector[index]  # Tensor的(d1, d2, d3)对应的数值就是vector[index]的数值了
            index += 1

以上明白了吗?我是明白了,汗个=. =

这个过程反过来用tensorflow非常简单,就是一个把Shape = (28, 28, 1)的Tensor打平的过程:

1
vector = tf.reshape(image, [-1])

好,收回来,继续卷积网络。第一层的最后一步:

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

这里使用Relu(Rectified linear unit)作为激活函数,详情请看这里,这个函数比较好理解,原始的Relu就是非负了f(x) = max(0, x),还有其他变种等。激活函数本身也有很大的学问的,比如softplus, sigmoid等。

这里有个网站很形象的解释了卷积的过程。

隐藏层

既然是深度神经网络,就一定要有隐藏的中间层了,这一层就比较直观了:

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)
  • 依然进行二维的卷积计算
  • 池化方式不变
  • 输入的channels = 32,因为第一层中的输出特征是32
  • 输出的特征是64(所以下一层的输入channels就是64了)

全连通层

全连通层把所有神经元连接在一起,在一个整体上计算图像的特征。之前卷积的过程都可以看做是在图像的一部分上进行操作,这一步是把图像整体特征输入进行计算。

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)

这里有一个地方需要理解一下,W Shape为什么是[7 * 7 * 64, 1024]?因为原始图像经过了第一层和第二层处理之后,到这一步已经变成了7 * 7大小,每一个元素有64个特征。

为什么是7 * 7?原始图像是28 * 28,卷积之后输出仍然是28 * 28(因为zero padding嘛),池化之后变成了14 * 14(因为window = (2, 2), strides = (2, 2))。第二层再次经过卷积和池化处理,就是7 * 7咯。

reshape一下让第二层输出的Tensor符合,把一个图像的特征打平到一个向量里,然后和W_fc1进行矩阵乘法,这里和element-wise乘法完全不同哦

为了防止过拟合,再次引入dropout(这个东西怎么翻译?),dropout的基本作用原理是在训练的时候让一些中间层的输出无效,避免过拟合的情况。Dropout的实现方式这里就不展开了。

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

输出层

输出层就是最终把计算结果输出表示为某一个分类的过程,最后一个过程了,tutorial中直接用softmax做输出层,也很直观:

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

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

这段代码不解释了,很容易理解,最后输出的b_fc2就是一个Shape = (10)的Tensor

训练和测试

损失函数依然是cross_entropy,优化器不再是GradientDescentOptimizer,而是AdamOptimizer,关于Adam看这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y_conv), reduction_indices=[1]))
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))
sess.run(tf.initialize_all_variables())
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}))

这里有一点要注意,测试的时候keep_prob是1.0,表示所有中间层都生效。(因为dropout本身只在训练过程中生效)

测试一下,因为神经网络的计算量太大,考虑到我的小CPU的承受能力,把20000次迭代改成了1000次,最后准确率差不多96%,已经很不错了。

OK,本文到此结束,下一章开始从TensorFlow Linear Model Tutorial开始。

本文由作者按照 CC BY 4.0 进行授权