Deep Learning with Python for computer vision
这篇文章是我学习《Deep Learning with Python》(第二版,François Chollet 著) 时写的系列笔记之一。文章的内容是从 Jupyter notebooks 转成 Markdown 的,你可以去 GitHub 或 Gitee 找到原始的 .ipynb
笔记本。
你可以去这个网站在线阅读这本书的正版原文(英文)。这本书的作者也给出了配套的 Jupyter notebooks。
本文为 第5章 深度学习用于计算机视觉 (Chapter 5. Deep learning for computer vision) 的笔记整合。
文章目录
卷积神经网络简介
5.1 Introduction to convnets
卷积神经网络处理计算机视觉问题很厉害啦。
首先看一个最简单的卷积神经网络处理 MNIST 完爆第二章里的全连接网络的例子:
from tensorflow.keras import layers
from tensorflow.keras import models
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.Flatten())
model.add(layers.Dense(64, activation="relu"))
model.add(layers.Dense(10, activation="softmax"))
这里我们用的 Conv2D 层要的 input_shape
是 (image_height, image_width, image_channels)
这种格式的。
Conv2D 和 MaxPooling2D 层的输出都是 3D 张量 (height, width, channels)
, height 和 width 会逐层减小,channels 是由 Conv2D 的第一个参数控制的。
最后的三层里,我们是把最后一个 Conv2D 层的 (3, 3, 64)
的张量用一系列全连接层变成想要的结果向量:Flatten 层是用来把我们的 3D 张量展平(flatten,其实我想写成“压”、“降”之类的,这才是flatten的本意,但标准的中文翻译是展平)到 1D 的。 然后后面的两个 Dense 层就行我们在第二章做的那种,最后得到一个 10 路的分类。
最后,看一下模型结构:
model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_3 (Conv2D) (None, 26, 26, 32) 320
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 13, 13, 32) 0
_________________________________________________________________
conv2d_4 (Conv2D) (None, 11, 11, 64) 18496
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 5, 5, 64) 0
_________________________________________________________________
conv2d_5 (Conv2D) (None, 3, 3, 64) 36928
_________________________________________________________________
flatten_1 (Flatten) (None, 576) 0
_________________________________________________________________
dense_2 (Dense) (None, 64) 36928
_________________________________________________________________
dense_3 (Dense) (None, 10) 650
=================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0
_________________________________________________________________
好了,网络就建成这样了,还是很简单的,接下来就训练它了,大致和之前第二章里的是一样的(但注意reshape的形状不一样):
# Load the TensorBoard notebook extension
# TensorBoard 可以可视化训练过程
%load_ext tensorboard
# Clear any logs from previous runs
!rm -rf ./logs/
# 在 MNIST 图像上训练卷积神经网络
import datetime
import tensorflow as tf
from tensorflow.keras.datasets import mnist
from tensorflow.keras.utils import to_categorical
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype('float32') / 255
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
# 准备 TensorBoard
log_dir = "logs/fit/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1)
model.compile(optimizer='rmsprop',
loss="categorical_crossentropy",
metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, batch_size=64,
callbacks=[tensorboard_callback])
Train on 60000 samples
Epoch 1/5
60000/60000 [==============================] - 36s 599us/sample - loss: 0.0156 - accuracy: 0.9953
Epoch 2/5
60000/60000 [==============================] - 33s 554us/sample - loss: 0.0127 - accuracy: 0.9960
Epoch 3/5
60000/60000 [==============================] - 31s 524us/sample - loss: 0.0097 - accuracy: 0.9971
Epoch 4/5
60000/60000 [==============================] - 32s 529us/sample - loss: 0.0092 - accuracy: 0.9974
Epoch 5/5
60000/60000 [==============================] - 31s 523us/sample - loss: 0.0095 - accuracy: 0.9971
<tensorflow.python.keras.callbacks.History at 0x1441fa9d0>
%tensorboard --logdir logs/fit
(这理会输出 tensorboard 显示的可视化模型训练情况)
来在测试集看一下结果:
test_loss, test_acc = model.evaluate(test_images, test_labels, verbose=2)
print(test_loss, test_acc)
10000/1 - 1s - loss: 0.0172 - accuracy: 0.9926
0.03441549262946125 0.9926
卷积
卷积神经网络
我们之前用的密集连接层是在整个输入特征空间(在 MNIST 中就是所有的像素)中学习全局模式的;而这里的卷积层是学习局部模式的。也就是说,Dense 是学整个图像的,而 Conv 是学图像的局部,比如在我们刚才写的代码里是学了 3x3 的窗口:
这种卷积神经网络具有两个性质:
-
卷积神经网络学到的模式是平移不变的(translation invariant):卷积神经网络学习到某个模式之后,在其他地方又看到了这个一样的模式,它就会认出它已经学过这个了,不用再去学一次了。而对于 Dense 的网络,即使遇到有一样的局部部分依然要去重新学习一次。这个性质让卷积神经网络可以高效利用数据,它只需要更少的训练样本就可以学到泛化比较好的数据表示(一个个局部都记住了嘛,而不是靠整体去映射)。
-
卷积神经网络可以学到模式的空间层次结构(spatial hierarchies of patterns):卷积神经网络在第一层学完了一个一个小的局部模式之后,下一层又可以用这些小局部拼出大一些的模式。然后这样多搞几层,卷积神经网络就可以学到越来越复杂、越来越抽象的视觉概念了,就是下面图片这个意思:
卷积层
我们刚才例子中用来表示图片的那种 3D 张量,包括两个空间轴 height、width 和一个深度轴 depth(也叫 channels 轴),对于 RGB 图片,深度轴的维度就是3,分别表示3种颜色嘛;而对于 MNIST 这种灰度图片,深度就是1,只用一个数去表示灰度值。在这种3D张量和在上面做的卷积运算的结果被称作 feature map(特征图)。
卷积运算会从输入特征图中提取出一个个小分块,并对所有这些分块施加一个相同的变换,得到输出特征图。输出特征图仍是一个 3D 张量:具有宽度和高度,其深度可能是任意值,深度的大小是该层的一个参数,深度轴里的每个 channel 都代表一个 filter (过滤器)。filter 会对输入数据的某一方面进行编码,比如,某个过滤器可以编码“输入中包含一张脸”这种概念。
在刚才的 MNIST 例子中,第一个卷积层接受尺寸为 (28, 28, 1)
的输入特征图,输出一个尺寸为 (26, 26, 32)
的特征图。这个输出中包含 32 个 filter,在每个深度轴中的 channel 都包含有 26x26 的值,叫做 filter 对输入的响应图(response map),表示 filter 在输入中不同位置上的运算结果。这也就是特征图为什么叫特征图的原因了:深度轴的每个维度都是一个特征(或过滤器),而 2D 张量 output[:, :, n]
是这个过滤器在输入上的响应的二维空间图。
卷积运算
关于卷积,,emmm,复变没怎么听懂,我主要是看「知乎: 如何通俗易懂地解释卷积?」来理解的。这里我们主要用的是这种作用:
Keras 的 Conv2D 层初始化写成:
Conv2D(output_depth, (window_height, window_width))
其中包含了卷积运算有两个核心参数:
- 输出特征图的深度:在我们刚才的 MNIST 例子里用了 32 和 64;
- 从输入中提取的每个块(滑窗)的尺寸:一般是 3x3 或者 5x5;
卷积运算会像滑动窗口一样的遍历所有可能的位置,把输入中每一小块的特征 (window_height, window_width, input_depth)
通过与一个称作卷积核(convolution kernel)的要学习的权重矩阵做点乘,变化得到一个向量 (output_depth, )
。所有的这种结果向量拼在一起就得到了一个 3D 的最终输出 (height, width, output_depth)
,其中的每个值就是输入对应过来的,比如取 3x3 的滑窗,则 output[i, j, :]
来自 input[i-1:i+1, j-1:j+1, :]
。
关于卷积和 CNN 可以去看看这篇文章:Convolutional Neural Networks - Basics, An Introduction to CNNs and Deep Learning
注意,因为边界效应(border effects)和使用了步幅(strides),我们输出的宽度和高度可能与输入的宽度和高度不同。
边界效应和填充
边界效应就是你在做滑窗之后得到的矩阵大小会缩小一圈(边界没了)。例如现输入一个 5x5 的图片,取 3x3 的小块只能取出 9 块来,因此输出的结果为 3x3 的:
之前我们做的 MNIST 也是类似的,一开始输入 28x28,第一层取 3x3 的,结果就是 26x26 了。
如果不希望这种减小发生,即希望保持输出的空间维度与输入的一致,则需要做填充(padding)。填充就是在输入的图片边界上加一些行和列,3x3 加1圈,5x5 要加2圈:
Keras 的 Conv2D 层里可以用 padding
参数来设置使用填充。padding
可以设为:
"valid"
(默认值):不做填充,只取“有效”的块。例如在 5×5 的输入特征图中,可以提取 3×3 图块的有效位置;"same"
: 做填充,使输出和输入的 width、height 相等。
卷积步幅
卷积的步幅就是一次滑窗移多少,之前我们一直做的都是步幅为1的。我们把步幅大于 1 的卷积叫做步进卷积(strided convolution),比如下面这个是步幅为 2 的:
然而步进卷积在实际里面用的并不多😂,要做这种对特征图的下采样(downsampled)我们一般用最大池化。
注:
下采样:对于一个样值序列间隔几个样值取样一次,这样得到新序列就是原序列的下采样。
From 百度百科
最大池化
与步进卷积类似,最大池化是用来对特征图进行下采样的。在一开始的 MNIST 例子中,我们用了 MaxPooling2D 层之后,特征图的尺寸就减半了。
最大池化是在一个窗口里,从输入特征图取出每个 channel 的最大值,然后输出出来。这个运算和卷积很类似,不过施加的函数是一个 max。
最大池化我们一般都是用 2x2 的窗口,步幅为 2,这样取可以将特征图下采样2倍。(卷积是一般取3x3窗口和步幅1)
如果不用最大池化,直接把一大堆卷积层堆起来,会有两个问题:
- 特征图尺寸下降的慢,搞到后面参数太多了,会加重过拟合;
- 不利于空间层级结构的学习:一直一小点一小点的用卷积层去学,不利于看到更抽象的整体。
除了最大池化,下采样的方式还有很多:比如步进卷积、平均池化之类的。但一般用最大池化效果比较好,我们要的是知道有没有某个特征嘛,如果用平均去就把这个特征“减淡”了,如果用步进卷积又可能把这个信息错过了。
总而言之,使用最大池化/其他下采样的原因,一是减少需要处理的特征图的元素个数,二是通过让一系列的卷积层观察到越来越大的窗口(看到的覆盖越来越多比例的原始输入),从而学到空间层级结构。
在小型数据集上从头训练一个卷积神经网络
5.2 Training a convnet from scratch on a small dataset
我们搞计算机视觉的时候,经常要处理的问题就是在很小的数据集上训练一个图像分类的模型。emmm,这里的“很小”可以是几百到几万。
从这一节开始到后面几节,我们要搞的就是从头开始训练一个小型模型、使用预训练的网络做特征提取、对预训练的网络进行微调,这些步骤合起来就可以用于解决小型数据集的图像分类问题了。
我们这一节要做的是从头开始训练一个小型模型来分类猫狗的图片。不做正则化,先不管过拟合的问题。
下载数据
我们将使用 Dogs vs. Cats dataset 来训练模型,这个数据集里面是一大堆猫、狗的照片。这个数据集就不是 Keras 内置的了,我们可以从 Kaggle 下载:https://www.kaggle.com/c/dogs-vs-cats/data
下载下来解压缩,,,(emmmm有点大我的 MBP 都装不下了,放移动硬盘上才解出来的😂,emmmm,又想起来该买个新固态了)。
然后来创建我们要用的数据集:训练集猫狗各1000个样本,验证集各500个,测试集各500个。编程来完成这个工作:
# 将图像复制到训练、验证和测试的目录
import os, shutil
original_dataset_dir = '/Volumes/WD/Files/dataset/dogs-vs-cats/dogs-vs-cats/train' # 原始数据集
base_dir = '/Volumes/WD/Files/dataset/dogs-vs-cats/cats_and_dogs_small' # 将要保存的较小数据集的位置
os.mkdir(base_dir)
# 创几个目录放划分后的训练、验证和测试集
train_dir = os.path.join(base_dir, 'train')
os.mkdir(train_dir)
validation_dir = os.path.join(base_dir, 'validation')
os.mkdir(validation_dir)
test_dir = os.path.join(base_dir, 'test')
os.mkdir(test_dir)
# 分开放猫狗
train_cats_dir = os.path.join(train_dir, 'cats')
os.mkdir(train_cats_dir)
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir)
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)
test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_dir)
# 复制猫的图片
fnames = [f'cat.{i}.jpg' for i in range(1000)] # 这里用了 f-String,要求 Python >= 3.6,老版本可以用 'cat.{}.jpg'.format(i)
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_cats_dir, fname)
shutil.copyfile(src, dst)
fnames = [f'cat.{i}.jpg' for i in range(1000, 1500)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_cats_dir, fname)
shutil.copyfile(src, dst)
fnames = [f'cat.{i}.jpg' for i in range(1500, 2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_cats_dir, fname)
shutil.copyfile(src, dst)
# 复制狗的图片
fnames = [f'dog.{i}.jpg' for i in range(1000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_dogs_dir, fname)
shutil.copyfile(src, dst)
fnames = [f'dog.{i}.jpg' for i in range(1000, 1500)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_dogs_dir, fname)
shutil.copyfile(src, dst)
fnames = [f'dog.{i}.jpg' for i in range(1500, 2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_dogs_dir, fname)
shutil.copyfile(src, dst)
# 检查
print('total training cat images:', len(os.listdir(train_cats_dir)))
print('total validation cat images:', len(os.listdir(validation_cats_dir)))
print('total test cat images:', len(os.listdir(test_cats_dir)))
print('total training dog images:', len(os.listdir(train_dogs_dir)))
print('total validation dog images:', len(os.listdir(validation_dogs_dir)))
print('total test dog images:'