CS20 07. 使用TensorFlow实现卷积神经网络

原始课程讲义地址

原始课程胶片地址

无特殊说明,笔记内容均来自老师提供的讲义和胶片

在MNIST上使用TensorFlow

TensorFlow使用tf.nn.conv2d来做二维的卷积操作(此时需要输入是三维)。不同维度输入的卷积操作可以参考Runhani的StackOverflow回答。函数签名为

1
2
3
4
5
6
7
8
9
10
tf.nn.conv2d(
input,
filter,
strides,
padding,
use_cudnn_on_gpu=True,
data_format='NHWC',
dilations=[1, 1, 1, 1],
name=None
)

其中

  • input是输入,默认是四维,每个维度按顺序是batch大小(N)、高度(H)、宽度(W)、信道数(C),即shape(input) = (N, H, W, C) (对应于data_format参数值'NHWC'
  • filter对应CNN的卷积核(也就是前面理论部分所讲的滤波器),其维度也是四维,不过是高度 x 宽度 x 输入信道数 x 输出信道数
  • stride是长度为4的一维数组,代表在input四个维度上的步长。通常设为[1, 1, 1, 1][1, 2, 2, 1]。一般情况下,stride[0] == stride[-1] == 1,因为不想跳过任何样本和任何信道
  • padding是补零的方式而非个数,有两个选项。
    • 如果设置为VALID,那么当卷积核和步幅的设置使得卷积过程不能完整覆盖输入时,舍弃输入最右侧(或最下侧)的内容。根据相关StackOverflow问题的讲解,考虑最简单的1维卷积的情况,假设输入长度为13,卷积核大小为6,步幅为5,那么第一次卷积操作所“看”的范围是1-6,第二次是6-11,此时再移动卷积核不能覆盖剩下的数据,到此为止,输入的最后两个元素被丢弃
    • 如果设置为SAME,那么会对输入补0,使得输入可以被卷积核按照设定的步幅完整覆盖。补0的原则是左边和右边补的0尽量一样——如果要补的0是奇数个0,在右侧多补一个
  • dilations是理论讲义里提到的膨胀系数。1.4版本里尚未引入这个参数

其它卷积函数,例如depthwise_conv2dseparable_conv2d,可以参考文档

课程讲义中给出了一个使用预定义卷积核做计算的例子,不过这种场景在实际应用中估计很难遇到,大部分卷积核都是训练得出来的(不要忘记卷积核的本质,它就是一组权重)

因此,为了让例子更加贴近实际,这里讲解对MNIST数据集使用TF构建CNN的方法。这里使用的体系结构是两个(卷积+ReLU+最大池)的组合,然后跟两个全连接。具体的体系结构如下图所示

解决MNIST问题的示例ConvNet体系结构
解决MNIST问题的示例ConvNet体系结构

由于很多事情(例如最大池、conv)都要做多次,因此需要设计可复用的代码,而且需要使用变量域来让我们可以在不同的层使用名字相同的变量。通常是每层都创建自己的变量域

定义卷积层

具体实现时,通常把conv和随后的非线性变化ReLU实现在一起

1
2
3
4
5
6
7
8
9
def conv_relu(inputs, filters, k_size, stride, padding, scope_name):
with tf.variable_scope(scope_name, reuse=tf.AUTO_REUSE) as scope:
in_channels = inputs.shape[-1]
kernel = tf.get_variable('kernel', [k_size, k_size, in_channels, filters],
initializer=tf.truncated_normal_initializer())
biases = tf.get_variable('biases', [filters],
initializer=tf.random_normal_initializer())
conv = tf.nn.conv2d(inputs, kernel, strides=[1, stride, stride, 1], padding=padding)
return tf.nn.relu(conv + biases, name=scope.name)

个中维度计算可以参考前面的理论讲解

定义池化层

可以直接使用TF中的tf.nn.max_pool来完成

1
2
3
4
5
6
7
8

def maxpool(inputs, ksize, stride, padding='VALID', scope_name='pool'):
with tf.variable_scope(scope_name, reuse=tf.AUTO_REUSE) as scope:
pool = tf.nn.max_pool(inputs,
ksize=[1, ksize, ksize, 1],
strides=[1, stride, stride, 1],
padding=padding)
return pool

定义全连接

与前面介绍的基本前馈神经网络定义方法无异

1
2
3
4
5
6
7
8
9
def fully_connected(inputs, out_dim, scope_name='fc'):
with tf.variable_scope(scope_name, reuse=tf.AUTO_REUSE) as scope:
in_dim = inputs.shape[-1]
w = tf.get_variable('weights', [in_dim, out_dim],
initializer=tf.truncated_normal_initializer())
b = tf.get_variable('biases', [out_dim],
initializer=tf.constant_initializer(0.0))
out = tf.matmul(inputs, w) + b
return out

构成最终代码

将上面的各个函数组合起来可以得到完整的代码。讲义里给出的是预测的图,训练的图应该是类似的,只不过加了定义loss和优化器的部分(然而GitHub上的代码并没有像上面一样良好地组织)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def inference(self):
conv1 = conv_relu(inputs=self.img,
filters=32,
k_size=5,
stride=1,
padding='SAME',
scope_name='conv1')
pool1 = maxpool(conv1, 2, 2, 'VALID', 'pool1')
conv2 = conv_relu(inputs=pool1,
filters=64,
k_size=5,
stride=1,
padding='SAME',
scope_name='conv2')
pool2 = maxpool(conv2, 2, 2, 'VALID', 'pool2')
feature_dim = pool2.shape[1] * pool2.shape[2] * pool2.shape[3]
pool2 = tf.reshape(pool2, [-1, feature_dim])
fc = tf.nn.relu(fully_connected(pool2, 1024, 'fc'))
dropout = tf.layers.dropout(fc, self.keep_prob, training=self.training, name='dropout')

self.logits = fully_connected(dropout, self.n_classes, 'logits')

def eval(self):
'''
Count the number of right predictions in a batch
'''
with tf.name_scope('predict'):
preds = tf.nn.softmax(self.logits)
correct_preds = tf.equal(tf.argmax(preds, 1), tf.argmax(self.label, 1))
self.accuracy = tf.reduce_sum(tf.cast(correct_preds, tf.float32))

tf.layers

前面的各层定义还是有些麻烦,实际上,TensorFlow提供了一种更简单的手段,就是直接使用在tf.layers里定义的各种层

1
2
3
4
5
6
7
8
9
10
11
12
13
conv1 = tf.layers.conv2d(inputs=self.img,
filters=32,
kernel_size=[5, 5],
padding='SAME',
activation=tf.nn.relu,
name='conv1')

pool1 = tf.layers.max_pooling2d(inputs=conv1,
pool_size=[2, 2],
strides=2,
name='pool1')

fc = tf.layers.dense(pool2, 1024, activation=tf.nn.relu, name='fc')

注意在使用tf.layers.dropout做dropout时,需要指明现在是在训练还是预测。回忆dropout的核心思想,其仅在训练时会随机丢弃一部分神经元,而预测时不会

1
2
3
4
dropout = tf.layers.dropout(fc,
self.keep_prob,
training=self.training,
name='dropout')
坚持原创技术分享,您的支持将鼓励我继续创作!