九、高级计算机视觉深度学习
本章涵盖
- 计算机视觉的不同分支:图像分类、图像分割、目标检测
- 现代卷积神经网络架构模式:残差连接、批量归一化、深度可分离卷积
- 可视化和解释卷积神经网络学习的技术
上一章通过简单模型(一堆Conv2D和MaxPooling2D层)和一个简单的用例(二进制图像分类)为您介绍了计算机视觉的深度学习。但是,计算机视觉不仅仅是图像分类!本章将深入探讨更多不同应用和高级最佳实践。
9.1 三个基本的计算机视觉任务
到目前为止,我们专注于图像分类模型:输入一幅图像,输出一个标签。“这幅图像可能包含一只猫;另一幅可能包含一只狗。”但是图像分类只是深度学习在计算机视觉中的几种可能应用之一。一般来说,有三个您需要了解的基本计算机视觉任务:
- 图像分类——目标是为图像分配一个或多个标签。它可以是单标签分类(一幅图像只能属于一个类别,排除其他类别),也可以是多标签分类(标记图像所属的所有类别,如图 9.1 所示)。例如,当您在 Google Photos 应用上搜索关键字时,背后实际上是在查询一个非常庞大的多标签分类模型——一个包含超过 20,000 个不同类别的模型,经过数百万图像训练。
- 图像分割——目标是将图像“分割”或“划分”为不同区域,每个区域通常代表一个类别(如图 9.1 所示)。例如,当 Zoom 或 Google Meet 在视频通话中在您身后显示自定义背景时,它使用图像分割模型来精确区分您的面部和背景。
- 目标检测——目标是在图像中绘制矩形(称为边界框)围绕感兴趣的对象,并将每个矩形与一个类别关联起来。例如,自动驾驶汽车可以使用目标检测模型监视其摄像头视野中的汽车、行人和标志。
图 9.1 三个主要的计算机视觉任务:分类、分割、检测
计算机视觉的深度学习还涵盖了除这三个任务之外的一些更专业的任务,例如图像相似性评分(估计两幅图像在视觉上的相似程度)、关键点检测(在图像中定位感兴趣的属性,如面部特征)、姿势估计、3D 网格估计等。但是,开始时,图像分类、图像分割和目标检测构成了每位机器学习工程师都应熟悉的基础。大多数计算机视觉应用都可以归结为这三种任务之一。
在上一章中,您已经看到了图像分类的实际应用。接下来,让我们深入了解图像分割。这是一种非常有用且多功能的技术,您可以直接使用到目前为止学到的知识来处理它。
请注意,我们不会涵盖目标检测,因为这对于入门书籍来说太专业且太复杂。但是,您可以查看 keras.io 上的 RetinaNet 示例,该示例展示了如何在 Keras 中使用大约 450 行代码从头构建和训练目标检测模型(keras.io/examples/vision/retinanet/)。
9.2 图像分割示例
使用深度学习进行图像分割是指使用模型为图像中的每个像素分配一个类别,从而将图像分割为不同区域(如“背景”和“前景”,或“道路”、“汽车”和“人行道”)。这一类技术可以用于图像和视频编辑、自动驾驶、机器人技术、医学成像等各种有价值的应用。
有两种不同的图像分割类型,你应该了解:
- 语义分割,其中每个像素独立地分类为语义类别,如“猫”。如果图像中有两只猫,相应像素都映射到相同的通用“猫”类别(见图 9.2)。
- 实例分割,不仅试图按类别对图像像素进行分类,还要解析出各个对象实例。在一幅图像中有两只猫,实例分割会将“猫 1”和“猫 2”视为两个不同的像素类别(见图 9.2)。
图 9.2 语义分割 vs. 实例分割
在这个示例中,我们将专注于语义分割:我们将再次查看猫和狗的图像,并学习如何区分主题和背景。
我们将使用牛津-IIIT 宠物数据集(www.robots.ox.ac.uk/~vgg/data/pets/),其中包含 7,390 张各种品种的猫和狗的图片,以及每张图片的前景-背景分割掩模。分割掩模是图像分割中的标签等效物:它是与输入图像大小相同的图像,具有单个颜色通道,其中每个整数值对应于输入图像中相应像素的类别。在我们的情况下,我们的分割掩模像素可以取三个整数值中的一个:
- 1 (前景)
- 2 (背景)
- 3 (轮廓)
让我们开始下载并解压我们的数据集,使用wget和tar shell 工具:
!wget http:/ /www.robots.ox.ac.uk/~vgg/data/pets/data/images.tar.gz !wget http:/ /www.robots.ox.ac.uk/~vgg/data/pets/data/annotations.tar.gz !tar -xf images.tar.gz !tar -xf annotations.tar.gz
输入图片以 JPG 文件的形式存储在 images/文件夹中(例如 images/Abyssinian_1.jpg),相应的分割掩模以 PNG 文件的形式存储在 annotations/trimaps/文件夹中(例如 annotations/trimaps/Abyssinian_1.png)。
让我们准备输入文件路径列表,以及相应掩模文件路径列表:
import os input_dir = "images/" target_dir = "annotations/trimaps/" input_img_paths = sorted( [os.path.join(input_dir, fname) for fname in os.listdir(input_dir) if fname.endswith(".jpg")]) target_paths = sorted( [os.path.join(target_dir, fname) for fname in os.listdir(target_dir) if fname.endswith(".png") and not fname.startswith(".")])
现在,其中一个输入及其掩模是什么样子?让我们快速看一下。这是一个示例图像(见图 9.3):
import matplotlib.pyplot as plt from tensorflow.keras.utils import load_img, img_to_array plt.axis("off") plt.imshow(load_img(input_img_paths[9])) # ❶
❶ 显示第 9 个输入图像。
图 9.3 一个示例图像
这是它对应的目标(见图 9.4):
def display_target(target_array): normalized_array = (target_array.astype("uint8") - 1) * 127 # ❶ plt.axis("off") plt.imshow(normalized_array[:, :, 0]) img = img_to_array(load_img(target_paths[9], color_mode="grayscale")) # ❷ display_target(img)
❶ 原始标签为 1、2 和 3。我们减去 1,使标签范围从 0 到 2,然后乘以 127,使标签变为 0(黑色)、127(灰色)、254(接近白色)。
❷ 我们使用 color_mode=“grayscale”,以便加载的图像被视为具有单个颜色通道。
图 9.4 对应的目标掩模
接下来,让我们将输入和目标加载到两个 NumPy 数组中,并将数组分割为训练集和验证集。由于数据集非常小,我们可以将所有内容加载到内存中:
import numpy as np import random img_size = (200, 200) # ❶ num_imgs = len(input_img_paths) # ❷ random.Random(1337).shuffle(input_img_paths) # ❸ random.Random(1337).shuffle(target_paths) # ❸ def path_to_input_image(path): return img_to_array(load_img(path, target_size=img_size)) def path_to_target(path): img = img_to_array( load_img(path, target_size=img_size, color_mode="grayscale")) img = img.astype("uint8") - 1 # ❹ return img input_imgs = np.zeros((num_imgs,) + img_size + (3,), dtype="float32") # ❺ targets = np.zeros((num_imgs,) + img_size + (1,), dtype="uint8") # ❺ for i in range(num_imgs): # ❺ input_imgs[i] = path_to_input_image(input_img_paths[i]) # ❺ targets[i] = path_to_target(target_paths[i]) # ❺ num_val_samples = 1000 # ❻ train_input_imgs = input_imgs[:-num_val_samples] # ❼ train_targets = targets[:-num_val_samples] # ❼ val_input_imgs = input_imgs[-num_val_samples:] # ❼ val_targets = targets[-num_val_samples:] # ❼
❶ 我们将所有内容调整为 200 × 200。
❷ 数据中的样本总数
❸ 对文件路径进行洗牌(它们最初是按品种排序的)。我们在两个语句中使用相同的种子(1337),以确保输入路径和目标路径保持相同顺序。
❹ 减去 1,使我们的标签变为 0、1 和 2。
❺ 将所有图像加载到 input_imgs 的 float32 数组中,将它们的掩模加载到 targets 的 uint8 数组中(顺序相同)。输入有三个通道(RGB 值),目标有一个单通道(包含整数标签)。
❻ 保留 1,000 个样本用于验证。
❼ 将数据分割为训练集和验证集。
现在是定义我们的模型的时候了:
from tensorflow import keras from tensorflow.keras import layers def get_model(img_size, num_classes): inputs = keras.Input(shape=img_size + (3,)) x = layers.Rescaling(1./255)(inputs) # ❶ x = layers.Conv2D(64, 3, strides=2, activation="relu", padding="same")(x)# ❷ x = layers.Conv2D(64, 3, activation="relu", padding="same")(x) x = layers.Conv2D(128, 3, strides=2, activation="relu", padding="same")(x) x = layers.Conv2D(128, 3, activation="relu", padding="same")(x) x = layers.Conv2D(256, 3, strides=2, padding="same", activation="relu")(x) x = layers.Conv2D(256, 3, activation="relu", padding="same")(x) x = layers.Conv2DTranspose(256, 3, activation="relu", padding="same")(x) x = layers.Conv2DTranspose( 256, 3, activation="relu", padding="same", strides=2)(x) x = layers.Conv2DTranspose(128, 3, activation="relu", padding="same")(x) x = layers.Conv2DTranspose( 128, 3, activation="relu", padding="same", strides=2)(x) x = layers.Conv2DTranspose(64, 3, activation="relu", padding="same")(x) x = layers.Conv2DTranspose( 64, 3, activation="relu", padding="same", strides=2)(x) outputs = layers.Conv2D(num_classes, 3, activation="softmax", # ❸ padding="same")(x) # ❸ model = keras.Model(inputs, outputs) return model model = get_model(img_size=img_size, num_classes=3) model.summary()
❶ 不要忘记将输入图像重新缩放到[0-1]范围。
❷ 请注意我们在所有地方都使用 padding=“same”,以避免边界填充对特征图大小的影响。
❸ 我们以每像素三路 softmax 结束模型,将每个输出像素分类为我们的三个类别之一。
这是model.summary()调用的输出:
Model: "model" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) [(None, 200, 200, 3)] 0 _________________________________________________________________ rescaling (Rescaling) (None, 200, 200, 3) 0 _________________________________________________________________ conv2d (Conv2D) (None, 100, 100, 64) 1792 _________________________________________________________________ conv2d_1 (Conv2D) (None, 100, 100, 64) 36928 _________________________________________________________________ conv2d_2 (Conv2D) (None, 50, 50, 128) 73856 _________________________________________________________________ conv2d_3 (Conv2D) (None, 50, 50, 128) 147584 _________________________________________________________________ conv2d_4 (Conv2D) (None, 25, 25, 256) 295168 _________________________________________________________________ conv2d_5 (Conv2D) (None, 25, 25, 256) 590080 _________________________________________________________________ conv2d_transpose (Conv2DTran (None, 25, 25, 256) 590080 _________________________________________________________________ conv2d_transpose_1 (Conv2DTr (None, 50, 50, 256) 590080 _________________________________________________________________ conv2d_transpose_2 (Conv2DTr (None, 50, 50, 128) 295040 _________________________________________________________________ conv2d_transpose_3 (Conv2DTr (None, 100, 100, 128) 147584 _________________________________________________________________ conv2d_transpose_4 (Conv2DTr (None, 100, 100, 64) 73792 _________________________________________________________________ conv2d_transpose_5 (Conv2DTr (None, 200, 200, 64) 36928 _________________________________________________________________ conv2d_6 (Conv2D) (None, 200, 200, 3) 1731 ================================================================= Total params: 2,880,643 Trainable params: 2,880,643 Non-trainable params: 0 _________________________________________________________________
模型的前半部分与你用于图像分类的卷积网络非常相似:一堆Conv2D层,逐渐增加滤波器大小。我们通过每次减少两倍的因子三次对图像进行下采样,最终得到大小为(25, 25, 256)的激活。这前半部分的目的是将图像编码为较小的特征图,其中每个空间位置(或像素)包含有关原始图像大空间块的信息。你可以将其理解为一种压缩。
这个模型的前半部分与你之前看到的分类模型之间的一个重要区别是我们进行下采样的方式:在上一章的分类卷积网络中,我们使用MaxPooling2D层来对特征图进行下采样。在这里,我们通过向每个卷积层添加步幅来进行下采样(如果你不记得卷积步幅的详细信息,请参阅第 8.1.1 节中的“理解卷积步幅”)。我们这样做是因为在图像分割的情况下,我们非常关心图像中信息的空间位置,因为我们需要将每个像素的目标掩模作为模型的输出。当你进行 2×2 最大池化时,你完全破坏了每个池化窗口内的位置信息:你返回每个窗口一个标量值,对于窗口中的四个位置中的哪一个位置的值来自于零了解。因此,虽然最大池化层在分类任务中表现良好,但对于分割任务,它会对我们造成相当大的伤害。与此同时,步幅卷积在下采样特征图的同时保留位置信息做得更好。在本书中,你会注意到我们倾向于在任何关心特征位置的模型中使用步幅而不是最大池化,比如第十二章中的生成模型。
模型的后半部分是一堆Conv2DTranspose层。那些是什么?嗯,模型的前半部分的输出是形状为(25, 25, 256)的特征图,但我们希望最终输出与目标掩模的形状相同,即(200, 200, 3)。因此,我们需要应用一种逆转换,而不是迄今为止应用的转换的一种—一种上采样特征图而不是下采样的方法。这就是Conv2DTranspose层的目的:你可以将其视为一种学习上采样的卷积层。如果你有形状为(100, 100, 64)的输入,并将其通过层Conv2D(128, 3, strides=2, padding="same"),你将得到形状为(50, 50, 128)的输出。如果你将此输出通过层Conv2DTranspose(64, 3, strides=2, padding="same"),你将得到形状为(100, 100, 64)的输出,与原始相同。因此,通过一堆Conv2D层将我们的输入压缩成形状为(25, 25, 256)的特征图后,我们只需应用相应的Conv2DTranspose层序列即可恢复到形状为(200, 200, 3)的图像。
现在我们可以编译和拟合我们的模型:
model.compile(optimizer="rmsprop", loss="sparse_categorical_crossentropy") callbacks = [ keras.callbacks.ModelCheckpoint("oxford_segmentation.keras", save_best_only=True) ] history = model.fit(train_input_imgs, train_targets, epochs=50, callbacks=callbacks, batch_size=64, validation_data=(val_input_imgs, val_targets))
让我们显示我们的训练和验证损失(见图 9.5):
epochs = range(1, len(history.history["loss"]) + 1) loss = history.history["loss"] val_loss = history.history["val_loss"] plt.figure() plt.plot(epochs, loss, "bo", label="Training loss") plt.plot(epochs, val_loss, "b", label="Validation loss") plt.title("Training and validation loss") plt.legend()
图 9.5 显示训练和验证损失曲线
你可以看到我们在中途开始过拟合,大约在第 25 个时期。让我们重新加载根据验证损失表现最佳的模型,并演示如何使用它来预测分割掩模(见图 9.6):
from tensorflow.keras.utils import array_to_img model = keras.models.load_model("oxford_segmentation.keras") i = 4 test_image = val_input_imgs[i] plt.axis("off") plt.imshow(array_to_img(test_image)) mask = model.predict(np.expand_dims(test_image, 0))[0] def display_mask(pred): # ❶ mask = np.argmax(pred, axis=-1) mask *= 127 plt.axis("off") plt.imshow(mask) display_mask(mask)
❶ 显示模型预测的实用程序
图 9.6 一个测试图像及其预测的分割掩模
我们预测掩模中有一些小的人为瑕疵,这是由前景和背景中的几何形状引起的。尽管如此,我们的模型似乎运行良好。
到目前为止,在第八章和第九章的开头,你已经学会了如何执行图像分类和图像分割的基础知识:你已经可以用你所知道的知识做很多事情了。然而,有经验的工程师开发的用于解决现实世界问题的卷积神经网络并不像我们迄今在演示中使用的那么简单。你仍然缺乏使专家能够快速准确地决定如何组合最先进模型的基本思维模型和思维过程。为了弥合这一差距,你需要了解架构模式。让我们深入探讨。
9.3 现代卷积神经网络架构模式
一个模型的“架构”是创建它所做选择的总和:使用哪些层,如何配置它们,以及如何连接它们。这些选择定义了你的模型的假设空间:梯度下降可以搜索的可能函数空间,由模型的权重参数化。像特征工程一样,一个好的假设空间编码了你对手头问题及其解决方案的先验知识。例如,使用卷积层意味着你事先知道你的输入图像中存在的相关模式是平移不变的。为了有效地从数据中学习,你需要对你正在寻找的内容做出假设。
模型架构往往是成功与失败之间的区别。如果你做出不恰当的架构选择,你的模型可能会陷入次优指标,无论训练数据量多大都无法拯救它。相反,一个好的模型架构将加速学习,并使你的模型能够有效利用可用的训练数据,减少对大型数据集的需求。一个好的模型架构是减少搜索空间的大小或使其更容易收敛到搜索空间的良好点。就像特征工程和数据整理一样,模型架构的目标是简化问题,以便梯度下降解决。记住,梯度下降是一个相当愚蠢的搜索过程,所以它需要尽可能多的帮助。
模型架构更像是一门艺术而不是一门科学。有经验的机器学习工程师能够直观地拼凑出高性能模型,而初学者常常难以创建一个能够训练的模型。关键词在于直觉:没有人能给你清晰的解释什么有效什么无效。专家依赖于模式匹配,这是他们通过广泛实践经验获得的能力。你将在本书中培养自己的直觉。然而,这也不完全是关于直觉的——实际上并没有太多的科学,但就像任何工程学科一样,有最佳实践。
在接下来的章节中,我们将回顾一些关键的卷积神经网络架构最佳实践:特别是残差连接、批量归一化和可分离卷积。一旦你掌握了如何使用它们,你将能够构建高效的图像模型。我们将把它们应用到我们的猫狗分类问题中。
让我们从鸟瞰图开始:系统架构的模块化-层次结构-重用(MHR)公式。
Python 深度学习第二版(GPT 重译)(四)(2)https://developer.aliyun.com/article/1485278