TensorFlow 2.0 模型:卷积神经网络
文 / 李锡涵,Google Developers Expert
本文节选自《简单粗暴 TensorFlow 2.0》
在 上一篇文章 里,我们以多层感知机(Multilayer Perceptron)为例,总体介绍了 TensorFlow 2.0 的模型构建、训练、评估全流程。
本篇文章则以在图像领域常用的卷积神经网络为主题,介绍以下内容:
如何使用 tf.keras 构建卷积神经网络模型;
如何在自己的项目中快速载入并使用经典的卷积神经网络模型;
为深度学习的入门者简介 卷积层 和 池化层 的原理。
使用 tf.keras 构建卷积神经网络模型
基础知识和原理
台湾大学李宏毅教授的《机器学习》课程的 Convolutional Neural Network 一章
https://www.bilibili.com/video/av10590361/?p=21UFLDL 教程 Convolutional Neural Network 一节
http://ufldl.stanford.edu/tutorial/supervised/ConvolutionalNeuralNetwork/斯坦福课程 CS231n: Convolutional Neural Networks for Visual Recognition 中的 “Module 2: Convolutional Neural Networks” 部分
https://cs231n.github.io/
卷积神经网络的一个示例实现如下所示,和上节中的 多层感知机 在代码结构上很类似,只是新加入了一些卷积层和池化层。这里的网络结构并不是唯一的,可以增加、删除或调整 CNN 的网络结构和参数,以达到更好的性能。
1class CNN(tf.keras.Model):
2 def __init__(self):
3 super().__init__()
4 self.conv1 = tf.keras.layers.Conv2D(
5 filters=32, # 卷积层神经元(卷积核)数目
6 kernel_size=[5, 5], # 感受野大小
7 padding='same', # padding策略(vaild 或 same)
8 activation=tf.nn.relu # 激活函数
9 )
10 self.pool1 = tf.keras.layers.MaxPool2D(pool_size=[2, 2], strides=2)
11 self.conv2 = tf.keras.layers.Conv2D(
12 filters=64,
13 kernel_size=[5, 5],
14 padding='same',
15 activation=tf.nn.relu
16 )
17 self.pool2 = tf.keras.layers.MaxPool2D(pool_size=[2, 2], strides=2)
18 self.flatten = tf.keras.layers.Reshape(target_shape=(7 * 7 * 64,))
19 self.dense1 = tf.keras.layers.Dense(units=1024, activation=tf.nn.relu)
20 self.dense2 = tf.keras.layers.Dense(units=10)
21
22 def call(self, inputs):
23 x = self.conv1(inputs) # [batch_size, 28, 28, 32]
24 x = self.pool1(x) # [batch_size, 14, 14, 32]
25 x = self.conv2(x) # [batch_size, 14, 14, 64]
26 x = self.pool2(x) # [batch_size, 7, 7, 64]
27 x = self.flatten(x) # [batch_size, 7 * 7 * 64]
28 x = self.dense1(x) # [batch_size, 1024]
29 x = self.dense2(x) # [batch_size, 10]
30 output = tf.nn.softmax(x)
31 return output
model = MLP()
更换成我们上面实现的 CNN 模型类,即 model = CNN()
,即可使用新的模型在 MNIST 数据集上进行训练,输出如下:1test accuracy: 0.988100
快速载入并使用经典的 CNN 模型
tf.keras.applications
中有一些预定义好的经典卷积神经网络结构,如 VGG16
、 VGG19
、 ResNet
、 MobileNet
等。我们可以直接调用这些经典的卷积神经网络结构,而无需手动定义网络结构。例如,我们可以使用以下代码来实例化一个 MobileNetV2
网络结构:
1model = tf.keras.applications.MobileNetV2()
当执行以上代码时,TensorFlow 会自动从网络上下载 MobileNetV2
网络结构,因此在第一次执行代码时需要具备网络连接。每个网络结构具有自己特定的详细参数设置,常用参数如下:
input_shape
:输入张量的形状 (不含第一维的 Batch),大多默认为224 × 224 × 3
。一般而言,模型对输入张量的大小有下限限制,长和宽至少为32 × 32
或75 × 75
;include_top
:在网络的最后是否包含全连接层,默认为True
;weights
:预训练权值,默认为'imagenet'
,即为当前模型载入在 ImageNet 数据集上预训练的权值。如需随机初始化变量可设为None
;classes
:分类数,默认为 1000。修改该参数需要include_top
参数为True
且weights
参数为None
。
各网络模型参数的详细介绍可参考 Keras 文档 (https://keras.io/applications/) 。
以下展示一个例子,使用 MobileNetV2
网络在 tf_flowers
五分类数据集上进行训练(为了代码的简短高效,在该示例中我们使用了 TensorFlow Datasets 和 tf.data 载入和预处理数据,在后面的连载中会专题介绍,或可 参考手册。通过将 weights
设置为 None
,我们随机初始化变量而不使用预训练权值。同时将 classes
设置为 5,对应于 5 分类的数据集。
注:参考手册 链接
https://tf.wiki/zh/appendix/tfds.html
https://tf.wiki/zh/basic/tools.html#tf-data
1import tensorflow as tf
2import tensorflow_datasets as tfds
3
4num_batches = 1000
5batch_size = 50
6learning_rate = 0.001
7
8dataset = tfds.load("tf_flowers", split=tfds.Split.TRAIN, as_supervised=True)
9dataset = dataset.map(lambda img, label: (tf.image.resize(img, [224, 224]) / 255.0, label)).shuffle(1024).batch(32)
10model = tf.keras.applications.DenseNet121(weights=None, classes=5)
11optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
12for images, labels in dataset:
13 with tf.GradientTape() as tape:
14 labels_pred = model(images)
15 loss = tf.keras.losses.sparse_categorical_crossentropy(y_true=labels, y_pred=labels_pred)
16 loss = tf.reduce_mean(loss)
17 print("loss %f" % loss.numpy())
18 grads = tape.gradient(loss, model.trainable_variables)
19 optimizer.apply_gradients(grads_and_vars=zip(grads, model.trainable_variables))
后文连载文章中,我们也会直接调用这些经典的网络结构来进行训练。
卷积层和池化层的工作原理
卷积层(Convolutional Layer,以
tf.keras.layers.Conv2D
为代表)是 CNN 的核心组件,其结构与大脑的视觉皮层有类似之处。
回忆我们之前建立的 神经细胞的计算模型 以及全连接层,我们默认每个神经元与上一层的所有神经元相连。不过,在视觉皮层的神经元中,情况并不是这样。你或许在生物课上学习过 感受野 (Receptive Field)这一概念,即视觉皮层中的神经元并非与前一层的所有神经元相连,而只是感受一片区域内的视觉信号,并只对局部区域的视觉刺激进行反应。CNN 中的卷积层正体现了这一特性。
例如,下图是一个 7×7 的单通道图片信号输入:
如果使用之前基于全连接层的模型,我们需要让每个输入信号对应一个权值,即建模一个神经元需要 7×7=49 个权值(加上偏置项是 50 个),并得到一个输出信号。如果一层有 N 个神经元,我们就需要 49N 个权值,并得到 N 个输出信号。
而在 CNN 的卷积层中,我们这样建模一个卷积层的神经元:图中 3×3 的红框代表该神经元的感受野。由此,我们只需 3×3=9 个权值
,外加 1 个偏置项 ,即可得到一个输出信号。例如,对于红框所示的位置,输出信号即为对矩阵 的所有元素求和并加上偏置项 ,记作 。
不过,3×3 的范围显然不足以处理整个图像,因此我们使用滑动窗口的方法。使用相同的参数,但将红框在图像中从左到右滑动,进行逐行扫描,每滑动到一个位置就计算一个值。例如,当红框向右移动一个单位时,我们计算矩阵 的所有元素的和并加上偏置项 ,记作 。由此,和一般的神经元只能输出 1 个值不同,这里的卷积层神经元可以输出一个 5×5 的矩阵 。
卷积示意图,一个单通道的 7×7 图像在通过一个感受野为 3×3 ,参数为 10 个的卷积层神经元后,得到 5×5 的矩阵作为卷积结果
下面,我们使用 TensorFlow 来验证一下上图的计算结果。
将上图中的输入图像、权值矩阵和偏置项 表示为 NumPy 数组 image
,W
,b
如下:
1
2
3image = np.array([[
4 [ ],
5 [ ],
6 [ ],
7 [ ],
8 [ ],
9 [ ],
10 [ ]
11]], dtype=np.float32)
12image = np.expand_dims(image, axis=-1)
13W = np.array([[
14 [ ],
15 [ ],
16 [ ]
17]], dtype=np.float32)
18b = np.array([1], dtype=np.float32)
然后建立一个仅有一个卷积层的模型,用
W
和b
初始化 [4] :
1model = tf.keras.models.Sequential([
2 tf.keras.layers.Conv2D(
3 filters=1, # 卷积层神经元(卷积核)数目
4 kernel_size=[3, 3], # 感受野大小
5 kernel_initializer=tf.constant_initializer(W),
6 bias_initializer=tf.constant_initializer(b)
7 )]
8)
最后将图像数据
image
输入模型,打印输出:
1output = model(image)
2print(tf.squeeze(output))
程序运行结果为:
1tf.Tensor(
2[ ]
3 [ ]
4 [ ]
5 [ ]
6 [ ]], shape=(5, 5), dtype=float32)
可见与上图中矩阵 的值一致。
还有一个问题,以上假设图片都只有一个通道(例如灰度图片),但如果图像是彩色的(例如有 RGB 三个通道)该怎么办呢?此时,我们可以为每个通道准备一个 3×3 的权值矩阵,即一共有 3×3×3=27 个权值。对于每个通道,均使用自己的权值矩阵进行处理,输出时将多个通道所输出的值进行加和即可。
可能有读者会注意到,按照上述介绍的方法,每次卷积后的结果相比于原始图像而言,四周都会 “少一圈”。比如上面 7×7 的图像,卷积后变成了 5×5 ,这有时会为后面的工作带来麻烦。因此,我们可以设定 padding 策略。在tf.keras.layers.Conv2D
中,当我们将padding
参数设为same
时,会将周围缺少的部分使用 0 补齐,使得输出的矩阵大小和输入一致。最后,既然我们可以使用滑动窗口的方法进行卷积,那么每次滑动的步长是不是可以设置呢?答案是肯定的。通过 tf.keras.layers.Conv2D
的strides
参数即可设置步长(默认为 1)。比如,在上面的例子中,如果我们将步长设定为 2,输出的卷积结果即会是一个 3×3 的矩阵。
事实上,卷积的形式多种多样,以上的介绍只是其中最简单和基础的一种。更多卷积方式的示例可见 Convolution arithmetic (https://github.com/vdumoulin/conv_arithmetic) 。
池化层(Pooling Layer)的理解则简单得多,其可以理解为对图像进行降采样的过程,对于每一次滑动窗口中的所有值,输出其中的最大值(MaxPooling)、均值或其他方法产生的值。例如,对于一个三通道的 16×16 图像(即一个 16163 的张量),经过感受野为 2×2,滑动步长为 2 的池化层,则得到一个 883 的张量。
[4]
这里使用了较为简易的 Sequential 模式建立模型,具体介绍可参考手册 (https://tf.wiki/zh/basic/models.html#keras-sequential-functional-api) 。
福利 | 问答环节
我们知道在入门一项新的技术时有许多挑战与困难需要克服。如果您有关于 TensorFlow 的相关问题,可在本文后留言,我们的工程师和 GDE 将挑选其中具有代表性的问题在下一期进行回答~
在上一篇文章《TensorFlow 2.0 模型:多层感知机》中,我们对于部分具有代表性的问题回答如下:
Q1:有 labelimg 标注过了的 datasets 下载库推荐吗?自己标几万张是不可能完成的任务……
A:对于已有的经典数据集,可以参考 TensorFlow Datasets (https://tensorflow.google.cn/datasets/)。TensorFlow Datasets 是一个开箱即用的数据集集合,包含数十种常用的机器学习数据集。通过简单的几行代码即可将数据以 tf.data.Datasets 的格式载入。
Q2:Release版本啥时候出来呀?
A:目前 TensorFlow 2.0 已发布了 RC1 (Release Candidate 1)版本,可使用 pip install tensorflow==2.0.0-rc1 安装。相信距离正式发布已经不会太远了。
Q3:TensorFlow 的 keras 和 keras 库本身性能有区别么?
A:TensorFlow 的 Keras(tf.keras)可以理解为与 TensorFlow 紧密整合的 Keras,支持 TensorFlow 的更多独有特性(如 Eager Execution、TPU、基于 tf.distribution 的多 GPU 和多机训练等),而这些特性能够帮助你更高效地训练模型。同时建议参考知乎问题:tf.keras 和 keras有什么区别?(https://www.zhihu.com/question/313111229)。
Q4:每次都是 mnist ,能不能在数据 pipeline 上面多一些叙述?
A:其实,在本文的“快速载入并使用经典的卷积神经网络模型”部分,我们已经开始使用 TensorFlow Datasets(TFDS) 和 tf.data ,仅用短短数行就载入了 tf_flowers 花朵五分类数据集,并进行了图像大小转换、打散及分批次等预处理。在后面的连载中,我们会专题介绍 TensorFlow Datasets(TFDS) 和 tf.data,这两个工具能够帮助你高效、灵活地载入和处理大规模训练数据。可参考 :
https://tf.wiki/zh/appendix/tfds.html
https://tf.wiki/zh/basic/tools.html#tf-data
A:您可以尝试使用 TensorFlow Datasets 载入 voc2007 数据集,演示代码如下:
1import tensorflow_datasets as tfds
2dataset = tfds.load("voc2007", split=tfds.Split.TRAIN)
tf.data
方便高效地预处理和迭代读取数据。以上是载入训练集,载入测试集和验证集可将split=tfds.Split.TRAIN
换为split=tfds.Split.TEST
和split=tfds.Split.VALIDATION
。https://tf.wiki/zh/appendix/tfds.html
https://tensorflow.google.cn/datasets/catalog/voc2007
Q6:数据集大,怎么处理?
A:在后面的连载中,我们会专题介绍 TensorFlow Datasets(TFDS) 和 tf.data,这两个工具能够帮助你高效、灵活地载入和处理训练大规模数据。
A:本系列教程《简单粗暴 TensorFlow 2.0》(https://tf.wiki) 即希望为 TensorFlow 初学者提供易于上手的系统指导。另外,TensorFlow的官方教程也经过了大量的更新以改善易读性,可参考 https://tensorflow.google.cn/overview 。
《简单粗暴 TensorFlow 2.0 》目录
TensorFlow 2.0 模型:卷积神经网络(本文)