查看原文
其他

​TensorFlow 2.0 模型:卷积神经网络

李锡涵 TensorFlow 2021-08-05

文 /  李锡涵,Google Developers Expert

本文节选自《简单粗暴 TensorFlow 2.0》


在 上一篇文章 里,我们以多层感知机(Multilayer Perceptron)为例,总体介绍了 TensorFlow 2.0 的模型构建、训练、评估全流程。


本篇文章则以在图像领域常用的卷积神经网络为主题,介绍以下内容:

  • 如何使用 tf.keras 构建卷积神经网络模型;

  • 如何在自己的项目中快速载入并使用经典的卷积神经网络模型;

  • 为深度学习的入门者简介 卷积层 和 池化层 的原理。



使用 tf.keras 构建卷积神经网络模型

卷积神经网络 (Convolutional Neural Network, CNN) 是一种结构类似于人类或动物的视觉系统的人工神经网络,包含一个或多个卷积层 (Convolutional Layer)、池化层 (Pooling Layer) 和全连接层 (Fully-connected Layer)。

基础知识和原理

  • 台湾大学李宏毅教授的《机器学习》课程的 Convolutional Neural Network 一章
    https://www.bilibili.com/video/av10590361/?p=21

  • UFLDL 教程 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=[55],     # 感受野大小
7            padding='same',         # padding策略(vaild 或 same)
8            activation=tf.nn.relu   # 激活函数
9        )
10        self.pool1 = tf.keras.layers.MaxPool2D(pool_size=[22], strides=2)
11        self.conv2 = tf.keras.layers.Conv2D(
12            filters=64,
13            kernel_size=[55],
14            padding='same',
15            activation=tf.nn.relu
16        )
17        self.pool2 = tf.keras.layers.MaxPool2D(pool_size=[22], 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
示例代码中的 CNN 结构图示
上一篇文章中 我们介绍了 TensorFlow 2.0 的模型构建、训练、评估全流程。此处,我们只需要将上一篇文章实例化模型时的代码 model = MLP() 更换成我们上面实现的 CNN 模型类,即 model = CNN() ,即可使用新的模型在 MNIST 数据集上进行训练,输出如下:

1test accuracy: 0.988100

可以发现准确率相较于上篇文章的 多层感知机 有非常显著的提高。事实上,通过改变模型的网络结构(比如加入 Dropout 层防止过拟合),准确率还有进一步提升的空间。

快速载入并使用经典的 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#TensorFlow 的图像表示为 [图像数目,长,宽,色彩通道数] 的四维张量
2#这里我们的输入图像 image 的张量形状为 [1, 7, 7, 1]
3image = np.array([[
4    [0, 0, 0, 0, 0, 0, 0],
5    [0, 1, 0, 1, 2, 1, 0],
6    [0, 0, 2, 2, 0, 1, 0],
7    [0, 1, 1, 0, 2, 1, 0],
8    [0, 0, 2, 1, 1, 0, 0],
9    [0, 2, 1, 1, 2, 0, 0],
10    [0, 0, 0, 0, 0, 0, 0]
11]], dtype=np.float32)
12image = np.expand_dims(image, axis=-1)  
13W = np.array([[
14    [ 0, 0, -1], 
15    [ 0, 1, 0 ], 
16    [-2, 0, 2 ]
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[[ 6.  5. -2.  1.  2.]
3 [ 3.  0.  3.  2. -2.]
4 [ 4.  2. -1.  0.  0.]
5 [ 2.  1.  2. -1. -3.]
6 [ 1.  1.  1.  3.  1.]], shape=(55), 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 


Q5:最近在研究 object detection,想用 tensorflow-datasets 导入 voc2007 数据集,但不知道怎么导入。请问官方有比较好的演示代码吗?
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 。
TensorFlow Datasets 的使用简介及在 TFDS 中载入 voc2007 数据集的详细信息可参考:
  • https://tf.wiki/zh/appendix/tfds.html 

  • https://tensorflow.google.cn/datasets/catalog/voc2007


Q6:数据集大,怎么处理?
A:在后面的连载中,我们会专题介绍 TensorFlow Datasets(TFDS) 和 tf.data,这两个工具能够帮助你高效、灵活地载入和处理训练大规模数据。


Q7:可不可以出点系统性的教程,入门好难
A:本系列教程《简单粗暴 TensorFlow 2.0》
(https://tf.wiki) 即希望为 TensorFlow 初学者提供易于上手的系统指导。另外,TensorFlow的官方教程也经过了大量的更新以改善易读性,可参考 https://tensorflow.google.cn/overview



《简单粗暴 TensorFlow 2.0 》目录




    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存