Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(六)(1)https://developer.aliyun.com/article/1482439
使用 Keras 实现 ResNet-34 CNN
到目前为止,大多数描述的 CNN 架构可以很自然地使用 Keras 实现(尽管通常您会加载一个预训练网络,正如您将看到的)。为了说明这个过程,让我们使用 Keras 从头开始实现一个 ResNet-34。首先,我们将创建一个ResidualUnit
层:
DefaultConv2D = partial(tf.keras.layers.Conv2D, kernel_size=3, strides=1, padding="same", kernel_initializer="he_normal", use_bias=False) class ResidualUnit(tf.keras.layers.Layer): def __init__(self, filters, strides=1, activation="relu", **kwargs): super().__init__(**kwargs) self.activation = tf.keras.activations.get(activation) self.main_layers = [ DefaultConv2D(filters, strides=strides), tf.keras.layers.BatchNormalization(), self.activation, DefaultConv2D(filters), tf.keras.layers.BatchNormalization() ] self.skip_layers = [] if strides > 1: self.skip_layers = [ DefaultConv2D(filters, kernel_size=1, strides=strides), tf.keras.layers.BatchNormalization() ] def call(self, inputs): Z = inputs for layer in self.main_layers: Z = layer(Z) skip_Z = inputs for layer in self.skip_layers: skip_Z = layer(skip_Z) return self.activation(Z + skip_Z)
正如您所看到的,这段代码与图 14-19 非常接近。在构造函数中,我们创建所有需要的层:图中右侧的主要层和左侧的跳过层(仅在步幅大于 1 时需要)。然后在call()
方法中,我们让输入经过主要层和跳过层(如果有的话),然后我们添加两个输出并应用激活函数。
现在我们可以使用Sequential
模型构建一个 ResNet-34,因为它实际上只是一长串的层——现在我们有了ResidualUnit
类,可以将每个残差单元视为一个单独的层。代码与图 14-18 非常相似:
model = tf.keras.Sequential([ DefaultConv2D(64, kernel_size=7, strides=2, input_shape=[224, 224, 3]), tf.keras.layers.BatchNormalization(), tf.keras.layers.Activation("relu"), tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding="same"), ]) prev_filters = 64 for filters in [64] * 3 + [128] * 4 + [256] * 6 + [512] * 3: strides = 1 if filters == prev_filters else 2 model.add(ResidualUnit(filters, strides=strides)) prev_filters = filters model.add(tf.keras.layers.GlobalAvgPool2D()) model.add(tf.keras.layers.Flatten()) model.add(tf.keras.layers.Dense(10, activation="softmax"))
这段代码中唯一棘手的部分是将ResidualUnit
层添加到模型的循环:正如前面解释的,前 3 个 RU 有 64 个滤波器,然后接下来的 4 个 RU 有 128 个滤波器,依此类推。在每次迭代中,当滤波器的数量与前一个 RU 中的数量相同时,我们必须将步幅设置为 1;否则,我们将其设置为 2;然后我们添加ResidualUnit
,最后我们更新prev_filters
。
令人惊讶的是,我们只需大约 40 行代码,就可以构建赢得 ILSVRC 2015 挑战的模型!这既展示了 ResNet 模型的优雅之处,也展示了 Keras API 的表现力。实现其他 CNN 架构会有点长,但并不难。不过,Keras 内置了几种这些架构,为什么不直接使用呢?
使用 Keras 中的预训练模型
通常,您不必手动实现标准模型,如 GoogLeNet 或 ResNet,因为在tf.keras.applications
包中只需一行代码即可获得预训练网络。
例如,您可以使用以下代码加载在 ImageNet 上预训练的 ResNet-50 模型:
model = tf.keras.applications.ResNet50(weights="imagenet")
就这些!这将创建一个 ResNet-50 模型,并下载在 ImageNet 数据集上预训练的权重。要使用它,您首先需要确保图像的尺寸正确。ResNet-50 模型期望 224×224 像素的图像(其他模型可能期望其他尺寸,如 299×299),因此让我们使用 Keras 的Resizing
层(在第十三章中介绍)来调整两个示例图像的大小(在将它们裁剪到目标纵横比之后):
images = load_sample_images()["images"] images_resized = tf.keras.layers.Resizing(height=224, width=224, crop_to_aspect_ratio=True)(images)
预训练模型假定图像以特定方式预处理。在某些情况下,它们可能期望输入被缩放为 0 到 1,或者从-1 到 1 等等。每个模型都提供了一个preprocess_input()
函数,您可以用它来预处理您的图像。这些函数假设原始像素值的范围是 0 到 255,这在这里是正确的:
inputs = tf.keras.applications.resnet50.preprocess_input(images_resized)
现在我们可以使用预训练模型进行预测:
>>> Y_proba = model.predict(inputs) >>> Y_proba.shape (2, 1000)
像往常一样,输出Y_proba
是一个矩阵,每行代表一个图像,每列代表一个类别(在本例中有 1,000 个类别)。如果您想显示前K个预测结果,包括类别名称和每个预测类别的估计概率,请使用decode_predictions()
函数。对于每个图像,它返回一个包含前K个预测结果的数组,其中每个预测结果表示为一个包含类别标识符、其名称和相应置信度分数的数组:
top_K = tf.keras.applications.resnet50.decode_predictions(Y_proba, top=3) for image_index in range(len(images)): print(f"Image #{image_index}") for class_id, name, y_proba in top_K[image_index]: print(f" {class_id} - {name:12s}{y_proba:.2%}")
输出如下所示:
Image #0 n03877845 - palace 54.69% n03781244 - monastery 24.72% n02825657 - bell_cote 18.55% Image #1 n04522168 - vase 32.66% n11939491 - daisy 17.81% n03530642 - honeycomb 12.06%
正确的类别是 palace 和 dahlia,因此模型对第一张图像是正确的,但对第二张图像是错误的。然而,这是因为 dahlia 不是 1,000 个 ImageNet 类之一。考虑到这一点,vase 是一个合理的猜测(也许这朵花在花瓶里?),daisy 也不是一个坏选择,因为 dahlias 和 daisies 都属于同一菊科家族。
正如您所看到的,使用预训练模型创建一个相当不错的图像分类器非常容易。正如您在表 14-3 中看到的,tf.keras.applications
中提供了许多其他视觉模型,从轻量级快速模型到大型准确模型。
但是,如果您想要为不属于 ImageNet 的图像类别使用图像分类器,那么您仍然可以通过使用预训练模型来进行迁移学习获益。
用于迁移学习的预训练模型
如果您想构建一个图像分类器,但没有足够的数据来从头开始训练它,那么通常可以重用预训练模型的较低层,正如我们在第十一章中讨论的那样。例如,让我们训练一个模型来对花的图片进行分类,重用一个预训练的 Xception 模型。首先,我们将使用 TensorFlow Datasets(在第十三章中介绍)加载花卉数据集:
import tensorflow_datasets as tfds dataset, info = tfds.load("tf_flowers", as_supervised=True, with_info=True) dataset_size = info.splits["train"].num_examples # 3670 class_names = info.features["label"].names # ["dandelion", "daisy", ...] n_classes = info.features["label"].num_classes # 5
请注意,您可以通过设置with_info=True
来获取有关数据集的信息。在这里,我们获取数据集的大小和类的名称。不幸的是,只有一个"train"
数据集,没有测试集或验证集,所以我们需要拆分训练集。让我们再次调用tfds.load()
,但这次将前 10%的数据集用于测试,接下来的 15%用于验证,剩下的 75%用于训练:
test_set_raw, valid_set_raw, train_set_raw = tfds.load( "tf_flowers", split=["train[:10%]", "train[10%:25%]", "train[25%:]"], as_supervised=True)
所有三个数据集都包含单独的图像。我们需要对它们进行批处理,但首先我们需要确保它们都具有相同的大小,否则批处理将失败。我们可以使用Resizing
层来实现这一点。我们还必须调用tf.keras.applications.xception.preprocess_input()
函数,以适当地预处理图像以供 Xception 模型使用。最后,我们还将对训练集进行洗牌并使用预取:
batch_size = 32 preprocess = tf.keras.Sequential([ tf.keras.layers.Resizing(height=224, width=224, crop_to_aspect_ratio=True), tf.keras.layers.Lambda(tf.keras.applications.xception.preprocess_input) ]) train_set = train_set_raw.map(lambda X, y: (preprocess(X), y)) train_set = train_set.shuffle(1000, seed=42).batch(batch_size).prefetch(1) valid_set = valid_set_raw.map(lambda X, y: (preprocess(X), y)).batch(batch_size) test_set = test_set_raw.map(lambda X, y: (preprocess(X), y)).batch(batch_size)
现在每个批次包含 32 个图像,所有图像都是 224×224 像素,像素值范围从-1 到 1。完美!
由于数据集不是很大,一点数据增强肯定会有所帮助。让我们创建一个数据增强模型,将其嵌入到我们的最终模型中。在训练期间,它将随机水平翻转图像,稍微旋转它们,并调整对比度:
data_augmentation = tf.keras.Sequential([ tf.keras.layers.RandomFlip(mode="horizontal", seed=42), tf.keras.layers.RandomRotation(factor=0.05, seed=42), tf.keras.layers.RandomContrast(factor=0.2, seed=42) ])
提示
tf.keras.preprocessing.image.ImageDataGenerator
类使从磁盘加载图像并以各种方式增强它们变得容易:您可以移动每个图像,旋转它,重新缩放它,水平或垂直翻转它,剪切它,或者应用任何您想要的转换函数。这对于简单的项目非常方便。然而,tf.data 管道并不复杂,通常更快。此外,如果您有 GPU 并且将预处理或数据增强层包含在模型内部,它们将在训练过程中受益于 GPU 加速。
接下来让我们加载一个在 ImageNet 上预训练的 Xception 模型。通过设置include_top=False
来排除网络的顶部。这将排除全局平均池化层和密集输出层。然后我们添加自己的全局平均池化层(将其输入设置为基础模型的输出),然后是一个具有每个类别一个单元的密集输出层,使用 softmax 激活函数。最后,我们将所有这些包装在一个 Keras Model
中:
base_model = tf.keras.applications.xception.Xception(weights="imagenet", include_top=False) avg = tf.keras.layers.GlobalAveragePooling2D()(base_model.output) output = tf.keras.layers.Dense(n_classes, activation="softmax")(avg) model = tf.keras.Model(inputs=base_model.input, outputs=output)
如第十一章中解释的,通常冻结预训练层的权重是一个好主意,至少在训练开始时是这样的:
for layer in base_model.layers: layer.trainable = False
警告
由于我们的模型直接使用基础模型的层,而不是base_model
对象本身,设置base_model.trainable=False
不会产生任何效果。
最后,我们可以编译模型并开始训练:
optimizer = tf.keras.optimizers.SGD(learning_rate=0.1, momentum=0.9) model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer, metrics=["accuracy"]) history = model.fit(train_set, validation_data=valid_set, epochs=3)
警告
如果你在 Colab 上运行,请确保运行时正在使用 GPU:选择运行时→“更改运行时类型”,在“硬件加速器”下拉菜单中选择“GPU”,然后点击保存。可以在没有 GPU 的情况下训练模型,但速度会非常慢(每个时期几分钟,而不是几秒)。
在训练模型几个时期后,其验证准确率应该达到 80%以上,然后停止提高。这意味着顶层现在已经训练得相当好,我们准备解冻一些基础模型的顶层,然后继续训练。例如,让我们解冻第 56 层及以上的层(这是 14 个残差单元中第 7 个的开始,如果列出层名称,你会看到):
for layer in base_model.layers[56:]: layer.trainable = True
不要忘记在冻结或解冻层时编译模型。还要确保使用更低的学习率以避免破坏预训练权重:
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01, momentum=0.9) model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer, metrics=["accuracy"]) history = model.fit(train_set, validation_data=valid_set, epochs=10)
这个模型应该在测试集上达到大约 92%的准确率,在几分钟的训练时间内(使用 GPU)。如果调整超参数,降低学习率,并进行更长时间的训练,应该能够达到 95%至 97%的准确率。有了这个,你可以开始在自己的图像和类别上训练出色的图像分类器!但计算机视觉不仅仅是分类。例如,如果你还想知道图片中花朵的位置在哪里?让我们现在来看看。
分类和定位
在图片中定位一个对象可以被表达为一个回归任务,如第十章中讨论的:预测一个对象周围的边界框,一个常见的方法是预测对象中心的水平和垂直坐标,以及它的高度和宽度。这意味着我们有四个数字要预测。对模型不需要太多改变;我们只需要添加一个具有四个单元的第二个密集输出层(通常在全局平均池化层之上),并且可以使用 MSE 损失进行训练:
base_model = tf.keras.applications.xception.Xception(weights="imagenet", include_top=False) avg = tf.keras.layers.GlobalAveragePooling2D()(base_model.output) class_output = tf.keras.layers.Dense(n_classes, activation="softmax")(avg) loc_output = tf.keras.layers.Dense(4)(avg) model = tf.keras.Model(inputs=base_model.input, outputs=[class_output, loc_output]) model.compile(loss=["sparse_categorical_crossentropy", "mse"], loss_weights=[0.8, 0.2], # depends on what you care most about optimizer=optimizer, metrics=["accuracy"])
但是现在我们有一个问题:花卉数据集中没有围绕花朵的边界框。因此,我们需要自己添加。这通常是机器学习项目中最困难和最昂贵的部分之一:获取标签。花时间寻找合适的工具是个好主意。要用边界框注释图像,您可能想使用开源图像标注工具,如 VGG Image Annotator、LabelImg、OpenLabeler 或 ImgLab,或者商业工具如 LabelBox 或 Supervisely。您还可以考虑众包平台,如亚马逊机械土耳其,如果您有大量图像需要注释。然而,设置众包平台、准备发送给工人的表格、监督他们并确保他们产生的边界框的质量是好的,这是相当多的工作,所以确保这是值得的。Adriana Kovashka 等人撰写了一篇非常实用的论文关于计算机视觉中的众包。我建议您查看一下,即使您不打算使用众包。如果只有几百张甚至几千张图像需要标记,并且您不打算经常这样做,最好自己做:使用合适的工具,只需要几天时间,您还将更好地了解您的数据集和任务。
现在假设您已经为花卉数据集中的每个图像获得了边界框(暂时假设每个图像只有一个边界框)。然后,您需要创建一个数据集,其项目将是经过预处理的图像的批次以及它们的类标签和边界框。每个项目应该是一个形式为(images, (class_labels, bounding_boxes))
的元组。然后您就可以开始训练您的模型!
提示
边界框应该被归一化,使得水平和垂直坐标以及高度和宽度的范围都在 0 到 1 之间。此外,通常预测高度和宽度的平方根,而不是直接预测高度和宽度:这样,对于大边界框的 10 像素误差不会受到与小边界框的 10 像素误差一样多的惩罚。
均方误差通常作为训练模型的成本函数效果相当不错,但不是评估模型如何预测边界框的好指标。这方面最常见的度量是交并比(IoU):预测边界框与目标边界框之间的重叠区域除以它们的并集的面积(参见图 14-24)。在 Keras 中,它由tf.keras.metrics.MeanIoU
类实现。
对单个对象进行分类和定位是很好的,但是如果图像中包含多个对象(通常在花卉数据集中是这种情况),怎么办呢?
图 14-24。边界框的 IoU 度量
目标检测
在图像中对多个对象进行分类和定位的任务称为目标检测。直到几年前,一种常见的方法是采用一个 CNN,该 CNN 经过训练,可以对图像中大致位于中心的单个对象进行分类和定位,然后在图像上滑动这个 CNN,并在每一步进行预测。通常,CNN 被训练来预测不仅类别概率和边界框,还有一个对象性分数:这是估计的概率,即图像确实包含一个位于中心附近的对象。这是一个二元分类输出;它可以通过具有单个单元的密集输出层产生,使用 sigmoid 激活函数并使用二元交叉熵损失进行训练。
注意
有时会添加一个“无对象”类,而不是对象性分数,但总的来说,这并不起作用得很好:最好分开回答“是否存在对象?”和“对象的类型是什么?”这两个问题。
这种滑动 CNN 方法在图 14-25 中有所说明。在这个例子中,图像被切成了一个 5×7 的网格,我们看到一个 CNN——厚厚的黑色矩形——在所有 3×3 区域上滑动,并在每一步进行预测。
图 14-25。通过在图像上滑动 CNN 来检测多个对象
在这个图中,CNN 已经对这三个 3×3 区域进行了预测:
- 当查看左上角的 3×3 区域(位于第二行第二列的红色阴影网格单元中心)时,它检测到了最左边的玫瑰。请注意,预测的边界框超出了这个 3×3 区域的边界。这完全没问题:即使 CNN 看不到玫瑰的底部部分,它仍能合理猜测它可能在哪里。它还预测了类别概率,给“玫瑰”类别一个很高的概率。最后,它预测了一个相当高的物体得分,因为边界框的中心位于中心网格单元内(在这个图中,物体得分由边界框的厚度表示)。
- 当查看下一个 3×3 区域,向右移动一个网格单元(位于阴影蓝色正方形中心)时,它没有检测到任何位于该区域中心的花朵,因此预测的物体得分非常低;因此,可以安全地忽略预测的边界框和类别概率。您可以看到,预测的边界框也不好。
- 最后,当查看下一个 3×3 区域,再向右移动一个网格单元(位于阴影绿色单元中心)时,它检测到了顶部的玫瑰,尽管不完美:这朵玫瑰没有很好地位于该区域中心,因此预测的物体得分并不是很高。
您可以想象,将 CNN 滑动到整个图像上会给您总共 15 个预测的边界框,以 3×5 的网格组织,每个边界框都伴随着其估计的类别概率和物体得分。由于对象的大小可能不同,您可能希望再次在更大的 4×4 区域上滑动 CNN,以获得更多的边界框。
这种技术相当简单,但正如您所看到的,它经常会在稍微不同的位置多次检测到相同的对象。需要一些后处理来摆脱所有不必要的边界框。一个常见的方法是称为非极大值抑制。下面是它的工作原理:
- 首先,摆脱所有物体得分低于某个阈值的边界框:因为 CNN 认为该位置没有对象,所以边界框是无用的。
- 找到具有最高物体得分的剩余边界框,并摆脱所有与其重叠很多的其他剩余边界框(例如,IoU 大于 60%)。例如,在图 14-25 中,具有最大物体得分的边界框是覆盖最左边的玫瑰的厚边界框。与这朵相同玫瑰接触的另一个边界框与最大边界框重叠很多,因此我们将摆脱它(尽管在这个例子中,它在上一步中已经被移除)。
- 重复步骤 2,直到没有更多需要摆脱的边界框。
这种简单的目标检测方法效果相当不错,但需要多次运行 CNN(在这个例子中为 15 次),因此速度相当慢。幸运的是,有一种更快的方法可以在图像上滑动 CNN:使用全卷积网络(FCN)。
全卷积网络
FCN 的概念最初是由 Jonathan Long 等人在2015 年的一篇论文中提出的,用于语义分割(根据对象所属的类别对图像中的每个像素进行分类的任务)。作者指出,可以用卷积层替换 CNN 顶部的密集层。为了理解这一点,让我们看一个例子:假设一个具有 200 个神经元的密集层位于一个输出 100 个大小为 7×7 的特征图的卷积层的顶部(这是特征图的大小,而不是卷积核的大小)。每个神经元将计算来自卷积层的所有 100×7×7 激活的加权和(加上一个偏置项)。现在让我们看看如果我们用 200 个大小为 7×7 的滤波器和"valid"
填充的卷积层来替换密集层会发生什么。这一层将输出 200 个大小为 1×1 的特征图(因为卷积核恰好是输入特征图的大小,而且我们使用"valid"
填充)。换句话说,它将输出 200 个数字,就像密集层一样;如果你仔细观察卷积层执行的计算,你会注意到这些数字将与密集层产生的数字完全相同。唯一的区别是密集层的输出是一个形状为[批量大小, 200]的张量,而卷积层将输出一个形状为[批量大小, 1, 1, 200]的张量。
提示
要将密集层转换为卷积层,卷积层中的滤波器数量必须等于密集层中的单元数量,滤波器大小必须等于输入特征图的大小,并且必须使用"valid"
填充。步幅可以设置为 1 或更多,稍后您将看到。
为什么这很重要?嗯,密集层期望特定的输入大小(因为它对每个输入特征有一个权重),而卷积层将愉快地处理任何大小的图像(但是,它期望其输入具有特定数量的通道,因为每个卷积核包含每个输入通道的不同权重集)。由于 FCN 只包含卷积层(和具有相同属性的池化层),它可以在任何大小的图像上进行训练和执行!
例如,假设我们已经训练了一个用于花卉分类和定位的 CNN。它是在 224×224 的图像上训练的,并输出 10 个数字:
- 输出 0 到 4 通过 softmax 激活函数发送,这给出了类别概率(每个类别一个)。
- 输出 5 通过 sigmoid 激活函数发送,这给出了物体得分。
- 输出 6 和 7 代表边界框的中心坐标;它们也经过 sigmoid 激活函数,以确保它们的范围在 0 到 1 之间。
- 最后,输出 8 和 9 代表边界框的高度和宽度;它们不经过任何激活函数,以允许边界框延伸到图像的边界之外。
现在我们可以将 CNN 的密集层转换为卷积层。实际上,我们甚至不需要重新训练它;我们可以直接将密集层的权重复制到卷积层!或者,在训练之前,我们可以将 CNN 转换为 FCN。
现在假设在输出层之前的最后一个卷积层(也称为瓶颈层)在网络输入 224×224 图像时输出 7×7 特征图(参见图 14-26 的左侧)。如果我们将 FCN 输入 448×448 图像(参见图 14-26 的右侧),瓶颈层现在将输出 14×14 特征图。³² 由于密集输出层被使用大小为 7×7 的 10 个滤波器的卷积层替换,使用"valid"
填充和步幅 1,输出将由 10 个特征图组成,每个大小为 8×8(因为 14-7+1=8)。换句话说,FCN 将仅处理整个图像一次,并输出一个 8×8 的网格,其中每个单元包含 10 个数字(5 个类别概率,1 个物体性分数和 4 个边界框坐标)。这就像拿着原始 CNN 并在图像上每行移动 8 步,每列移动 8 步。为了可视化这一点,想象将原始图像切成一个 14×14 的网格,然后在这个网格上滑动一个 7×7 的窗口;窗口将有 8×8=64 个可能的位置,因此有 8×8 个预测。然而,FCN 方法要更有效,因为网络只看一次图像。事实上,You Only Look Once(YOLO)是一个非常流行的目标检测架构的名称,我们将在接下来看一下。
图 14-26。相同的全卷积网络处理小图像(左)和大图像(右)
只看一次
YOLO 是由 Joseph Redmon 等人在2015 年的一篇论文中提出的一种快速准确的目标检测架构。³³ 它非常快速,可以在视频上实时运行,就像在 Redmon 的演示中看到的那样。YOLO 的架构与我们刚讨论的架构非常相似,但有一些重要的区别:
- 对于每个网格单元,YOLO 只考虑边界框中心位于该单元内的对象。边界框坐标是相对于该单元的,其中(0, 0)表示单元的左上角,(1, 1)表示右下角。然而,边界框的高度和宽度可能远远超出单元。
- 它为每个网格单元输出两个边界框(而不仅仅是一个),这使得模型能够处理两个对象非常接近,以至于它们的边界框中心位于同一个单元格内的情况。每个边界框还附带自己的物体性分数。
- YOLO 还为每个网格单元输出一个类别概率分布,每个网格单元预测 20 个类别概率,因为 YOLO 是在包含 20 个类别的 PASCAL VOC 数据集上训练的。这产生了一个粗糙的类别概率图。请注意,模型为每个网格单元预测一个类别概率分布,而不是每个边界框。然而,可以在后处理期间估计每个边界框的类别概率,方法是测量每个边界框与类别概率图中的每个类别匹配的程度。例如,想象一张图片中有一个人站在一辆车前面。将会有两个边界框:一个大的水平边界框用于车,一个较小的垂直边界框用于人。这些边界框的中心可能位于同一个网格单元内。那么我们如何确定应该为每个边界框分配哪个类别呢?嗯,类别概率图将包含一个“车”类占主导地位的大区域,里面将有一个“人”类占主导地位的较小区域。希望车的边界框大致匹配“车”区域,而人的边界框大致匹配“人”区域:这将允许为每个边界框分配正确的类别。
YOLO 最初是使用 Darknet 开发的,Darknet 是由 Joseph Redmon 最初用 C 开发的开源深度学习框架,但很快就被移植到了 TensorFlow、Keras、PyTorch 等。多年来不断改进,包括 YOLOv2、YOLOv3 和 YOLO9000(再次由 Joseph Redmon 等人开发)、YOLOv4(由 Alexey Bochkovskiy 等人开发)、YOLOv5(由 Glenn Jocher 开发)和 PP-YOLO(由 Xiang Long 等人开发)。
每个版本都带来了一些令人印象深刻的速度和准确性改进,使用了各种技术;例如,YOLOv3 在一定程度上提高了准确性,部分原因在于锚先验,利用了某些边界框形状比其他形状更有可能的事实,这取决于类别(例如,人们倾向于具有垂直边界框,而汽车通常不会)。他们还增加了每个网格单元的边界框数量,他们在不同数据集上进行了训练,包含更多类别(YOLO9000 的情况下最多达到 9,000 个类别,按层次结构组织),他们添加了跳跃连接以恢复在 CNN 中丢失的一些空间分辨率(我们将很快讨论这一点,当我们看语义分割时),等等。这些模型也有许多变体,例如 YOLOv4-tiny,它经过优化,可以在性能较弱的机器上进行训练,并且可以运行得非常快(每秒超过 1,000 帧!),但平均精度均值(mAP)略低。
许多目标检测模型都可以在 TensorFlow Hub 上找到,通常具有预训练权重,例如 YOLOv5、SSD、Faster R-CNN和EfficientDet。
SSD 和 EfficientDet 是“一次查看”检测模型,类似于 YOLO。EfficientDet 基于 EfficientNet 卷积架构。Faster R-CNN 更复杂:图像首先经过 CNN,然后输出传递给区域建议网络(RPN),该网络提出最有可能包含对象的边界框;然后为每个边界框运行分类器,基于 CNN 的裁剪输出。使用这些模型的最佳起点是 TensorFlow Hub 的出色目标检测教程。
到目前为止,我们只考虑在单个图像中检测对象。但是视频呢?对象不仅必须在每一帧中被检测到,还必须随着时间进行跟踪。现在让我们快速看一下目标跟踪。
目标跟踪
目标跟踪是一项具有挑战性的任务:对象移动,它们可能随着接近或远离摄像机而变大或变小,它们的外观可能会随着转身或移动到不同的光照条件或背景而改变,它们可能会被其他对象暂时遮挡,等等。
最受欢迎的目标跟踪系统之一是DeepSORT。它基于经典算法和深度学习的组合:
- 它使用Kalman 滤波器来估计给定先前检测的对象最可能的当前位置,并假设对象倾向于以恒定速度移动。
- 它使用深度学习模型来衡量新检测和现有跟踪对象之间的相似度。
- 最后,它使用匈牙利算法将新检测映射到现有跟踪对象(或新跟踪对象):该算法有效地找到最小化检测和跟踪对象预测位置之间距离的映射组合,同时最小化外观差异。
例如,想象一个红色球刚从相反方向移动的蓝色球上弹起。根据球的先前位置,卡尔曼滤波器将预测球会相互穿过:实际上,它假设对象以恒定速度移动,因此不会预期弹跳。如果匈牙利算法只考虑位置,那么它会愉快地将新的检测结果映射到错误的球上,就好像它们刚刚相互穿过并交换了颜色。但由于相似度度量,匈牙利算法会注意到问题。假设球不太相似,算法将新的检测结果映射到正确的球上。
提示
在 GitHub 上有一些 DeepSORT 的实现,包括 YOLOv4 + DeepSORT 的 TensorFlow 实现:https://github.com/theAIGuysCode/yolov4-deepsort。
到目前为止,我们已经使用边界框定位了对象。这通常足够了,但有时您需要更精确地定位对象,例如在视频会议中去除人物背后的背景。让我们看看如何降到像素级别。
语义分割
在语义分割中,每个像素根据其所属对象的类别进行分类(例如,道路、汽车、行人、建筑等),如图 14-27 所示。请注意,同一类别的不同对象不被区分。例如,分割图像右侧的所有自行车最终会成为一个大块像素。这项任务的主要困难在于,当图像经过常规 CNN 时,由于步幅大于 1 的层,它们逐渐失去空间分辨率;因此,常规 CNN 可能只会知道图像左下角某处有一个人,但不会比这更精确。
图 14-27. 语义分割
与目标检测一样,有许多不同的方法来解决这个问题,有些方法相当复杂。然而,在 Jonathan Long 等人于 2015 年提出的一篇关于完全卷积网络的论文中提出了一个相当简单的解决方案。作者首先采用了一个预训练的 CNN,并将其转换为 FCN。CNN 对输入图像应用了总步幅为 32(即,如果将所有大于 1 的步幅相加),这意味着最后一层输出的特征图比输入图像小 32 倍。这显然太粗糙了,因此他们添加了一个单一的上采样层,将分辨率乘以 32。
有几种可用的上采样解决方案(增加图像的大小),例如双线性插值,但这只能在×4 或×8 的范围内工作得相当好。相反,他们使用转置卷积层:³⁹这相当于首先通过插入空行和列(全是零)来拉伸图像,然后执行常规卷积(参见图 14-28)。或者,有些人更喜欢将其视为使用分数步幅的常规卷积层(例如,图 14-28 中的步幅为 1/2)。转置卷积层可以初始化为执行接近线性插值的操作,但由于它是一个可训练的层,在训练期间会学习做得更好。在 Keras 中,您可以使用Conv2DTranspose
层。
注意
在转置卷积层中,步幅定义了输入将被拉伸多少,而不是滤波器步长的大小,因此步幅越大,输出就越大(与卷积层或池化层不同)。
图 14-28. 使用转置卷积层进行上采样
使用转置卷积层进行上采样是可以的,但仍然太不精确。为了做得更好,Long 等人从较低层添加了跳跃连接:例如,他们将输出图像上采样了 2 倍(而不是 32 倍),并添加了具有这种双倍分辨率的较低层的输出。然后,他们将结果上采样了 16 倍,导致总的上采样因子为 32(参见图 14-29)。这恢复了在较早的池化层中丢失的一些空间分辨率。在他们最好的架构中,他们使用了第二个类似的跳跃连接,以从更低的层中恢复更精细的细节。简而言之,原始 CNN 的输出经过以下额外步骤:上采样×2,添加较低层的输出(适当比例),上采样×2,添加更低层的输出,最后上采样×8。甚至可以将缩放超出原始图像的大小:这可以用于增加图像的分辨率,这是一种称为超分辨率的技术。
图 14-29。跳跃层从较低层恢复一些空间分辨率
实例分割类似于语义分割,但不是将同一类别的所有对象合并成一个大块,而是将每个对象与其他对象区分开来(例如,它识别每辆自行车)。例如,由 Kaiming He 等人在2017 年的一篇论文中提出的Mask R-CNN架构,通过为每个边界框额外生成一个像素掩码来扩展 Faster R-CNN 模型。因此,您不仅可以获得围绕每个对象的边界框,以及一组估计的类别概率,还可以获得一个像素掩码,该掩码定位属于对象的边界框中的像素。该模型可在 TensorFlow Hub 上获得,预训练于 COCO 2017 数据集。尽管该领域发展迅速,但如果您想尝试最新和最优秀的模型,请查看https://paperswithcode.com的最新技术部分。
正如您所看到的,深度计算机视觉领域广阔且快速发展,每年都会涌现出各种架构。几乎所有这些架构都基于卷积神经网络,但自 2020 年以来,另一种神经网络架构已进入计算机视觉领域:Transformer(我们将在第十六章中讨论)。过去十年取得的进步令人瞩目,研究人员现在正专注于越来越困难的问题,例如对抗学习(试图使网络更具抗干扰性,以防止被设计用来欺骗它的图像)、可解释性(了解网络为何做出特定分类)、现实图像生成(我们将在第十七章中回顾)、单次学习(一个系统只需看到一次对象就能识别该对象)、预测视频中的下一帧、结合文本和图像任务等等。
现在进入下一章,我们将看看如何使用递归神经网络和卷积神经网络处理序列数据,例如时间序列。
练习
- 相比于完全连接的 DNN,CNN 在图像分类方面有哪些优势?
- 考虑一个由三个卷积层组成的 CNN,每个卷积层都有 3×3 的内核,步幅为 2,且具有
"same"
填充。最底层输出 100 个特征映射,中间层输出 200 个,顶层输出 400 个。输入图像是 200×300 像素的 RGB 图像:
- CNN 中的参数总数是多少?
- 如果我们使用 32 位浮点数,那么在对单个实例进行预测时,这个网络至少需要多少 RAM?
- 当在一个包含 50 张图像的小批量上进行训练时呢?
- 如果您的 GPU 在训练 CNN 时内存不足,您可以尝试哪五种方法来解决这个问题?
- 为什么要添加最大池化层而不是具有相同步幅的卷积层?
- 何时要添加局部响应归一化层?
- 您能否列出 AlexNet 相对于 LeNet-5 的主要创新?GoogLeNet、ResNet、SENet、Xception 和 EfficientNet 的主要创新又是什么?
- 什么是全卷积网络?如何将密集层转换为卷积层?
- 语义分割的主要技术难点是什么?
- 从头开始构建自己的 CNN,并尝试在 MNIST 上实现最高可能的准确性。
- 使用大型图像分类的迁移学习,经过以下步骤:
- 创建一个包含每类至少 100 张图像的训练集。例如,您可以根据位置(海滩、山脉、城市等)对自己的图片进行分类,或者您可以使用现有数据集(例如来自 TensorFlow 数据集)。
- 将其分为训练集、验证集和测试集。
- 构建输入管道,应用适当的预处理操作,并可选择添加数据增强。
- 在这个数据集上微调一个预训练模型。
- 按照 TensorFlow 的风格转移教程进行操作。这是使用深度学习生成艺术的有趣方式。
这些练习的解决方案可在本章笔记本的末尾找到,网址为https://homl.info/colab3。
(1)David H. Hubel,“不受限制的猫条纹皮层单元活动”,《生理学杂志》147 卷(1959 年):226-238。
(2)David H. Hubel 和 Torsten N. Wiesel,“猫条纹皮层单个神经元的感受野”,《生理学杂志》148 卷(1959 年):574-591。
(3)David H. Hubel 和 Torsten N. Wiesel,“猴子条纹皮层的感受野和功能结构”,《生理学杂志》195 卷(1968 年):215-243。
(4)福岛邦彦,“Neocognitron:一种不受位置偏移影响的模式识别机制的自组织神经网络模型”,《生物控制论》36 卷(1980 年):193-202。
(5)Yann LeCun 等人,“基于梯度的学习应用于文档识别”,《IEEE 会议录》86 卷,第 11 期(1998 年):2278-2324。
(6)卷积是一种数学操作,它将一个函数滑动到另一个函数上,并测量它们逐点乘积的积分。它与傅里叶变换和拉普拉斯变换有深刻的联系,并且在信号处理中被广泛使用。卷积层实际上使用交叉相关,这与卷积非常相似(有关更多详细信息,请参见https://homl.info/76)。
(7)为了产生相同大小的输出,一个全连接层需要 200×150×100 个神经元,每个神经元连接到所有 150×100×3 个输入。它将有 200×150×100×(150×100×3+1)≈1350 亿个参数!
(8)在国际单位制(SI)中,1 MB = 1,000 KB = 1,000×1,000 字节 = 1,000×1,000×8 位。而 1 MiB = 1,024 kiB = 1,024×1,024 字节。所以 12 MB ≈ 11.44 MiB。
(9)我们迄今讨论过的其他内核具有权重,但池化内核没有:它们只是无状态的滑动窗口。
(10)Yann LeCun 等人,“基于梯度的学习应用于文档识别”,《IEEE 会议录》86 卷,第 11 期(1998 年):2278-2324。
(11)Alex Krizhevsky 等人,“使用深度卷积神经网络对 ImageNet 进行分类”,《第 25 届国际神经信息处理系统会议论文集》1 卷(2012 年):1097-1105。
¹² Matthew D. Zeiler 和 Rob Fergus,“可视化和理解卷积网络”,欧洲计算机视觉会议论文集(2014):818-833。
¹³ Christian Szegedy 等人,“使用卷积深入”,IEEE 计算机视觉和模式识别会议论文集(2015):1-9。
¹⁴ 在 2010 年的电影Inception中,角色们不断深入多层梦境;因此这些模块的名称。
¹⁵ Karen Simonyan 和 Andrew Zisserman,“用于大规模图像识别的非常深的卷积网络”,arXiv 预印本 arXiv:1409.1556(2014)。
¹⁶ Kaiming He 等人,“用于图像识别的深度残差学习”,arXiv 预印本 arXiv:1512:03385(2015)。
¹⁷ 描述神经网络时,通常只计算具有参数的层。
¹⁸ Christian Szegedy 等人,“Inception-v4,Inception-ResNet 和残差连接对学习的影响”,arXiv 预印本 arXiv:1602.07261(2016)。
¹⁹ François Chollet,“Xception:深度学习与深度可分离卷积”,arXiv 预印本 arXiv:1610.02357(2016)。
²⁰ 这个名称有时可能会有歧义,因为空间可分离卷积通常也被称为“可分离卷积”。
²¹ Jie Hu 等人,“挤压激励网络”,IEEE 计算机视觉和模式识别会议论文集(2018):7132-7141。
²² Saining Xie 等人,“聚合残差变换用于深度神经网络”,arXiv 预印本 arXiv:1611.05431(2016)。
²³ Gao Huang 等人,“密集连接卷积网络”,arXiv 预印本 arXiv:1608.06993(2016)。
²⁴ Andrew G. Howard 等人,“MobileNets:用于移动视觉应用的高效卷积神经网络”,arXiv 预印本 arxiv:1704.04861(2017)。
²⁵ Chien-Yao Wang 等人,“CSPNet:一种可以增强 CNN 学习能力的新骨干”,arXiv 预印本 arXiv:1911.11929(2019)。
²⁶ Mingxing Tan 和 Quoc V. Le,“EfficientNet:重新思考卷积神经网络的模型缩放”,arXiv 预印本 arXiv:1905.11946(2019)。
²⁷ 一款 92 核心的 AMD EPYC CPU,带有 IBPB,1.7 TB 的 RAM 和一款 Nvidia Tesla A100 GPU。
²⁸ 在 ImageNet 数据集中,每个图像都映射到WordNet 数据集中的一个单词:类别 ID 只是一个 WordNet ID。
²⁹ Adriana Kovashka 等人,“计算机视觉中的众包”,计算机图形学和视觉基础与趋势 10,第 3 期(2014):177-243。
³⁰ Jonathan Long 等人,“用于语义分割的全卷积网络”,IEEE 计算机视觉和模式识别会议论文集(2015):3431-3440。
³¹ 有一个小例外:使用"valid"
填充的卷积层会在输入大小小于核大小时报错。
³² 这假设我们在网络中只使用了"same"
填充:"valid"
填充会减小特征图的大小。此外,448 可以被 2 整除多次,直到达到 7,没有任何舍入误差。如果任何一层使用不同于 1 或 2 的步幅,那么可能会有一些舍入误差,因此特征图最终可能会变小。
³³ Joseph Redmon 等人,“You Only Look Once: Unified, Real-Time Object Detection”,《IEEE 计算机视觉与模式识别会议论文集》(2016):779–788。
³⁴ 您可以在 TensorFlow Models 项目中找到 YOLOv3、YOLOv4 及其微小变体,网址为https://homl.info/yolotf。
³⁵ Wei Liu 等人,“SSD: Single Shot Multibox Detector”,《第 14 届欧洲计算机视觉会议论文集》1(2016):21–37。
³⁶ Shaoqing Ren 等人,“Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks”,《第 28 届国际神经信息处理系统会议论文集》1(2015):91–99。
³⁷ Mingxing Tan 等人,“EfficientDet: Scalable and Efficient Object Detection”,arXiv 预印本 arXiv:1911.09070(2019)。
³⁸ Nicolai Wojke 等人,“Simple Online and Realtime Tracking with a Deep Association Metric”,arXiv 预印本 arXiv:1703.07402(2017)。
³⁹ 这种类型的层有时被称为反卷积层,但它不执行数学家所说的反卷积,因此应避免使用这个名称。
⁴⁰ Kaiming He 等人,“Mask R-CNN”,arXiv 预印本 arXiv:1703.06870(2017)。
第十五章:使用 RNNs 和 CNNs 处理序列
预测未来是你经常做的事情,无论是在结束朋友的句子还是预期早餐时咖啡的味道。在本章中,我们将讨论循环神经网络(RNNs)-一类可以预测未来的网络(嗯,至少在一定程度上)。RNNs 可以分析时间序列数据,例如您网站上每日活跃用户的数量,您所在城市的每小时温度,您家每日的用电量,附近汽车的轨迹等等。一旦 RNN 学习了数据中的过去模式,它就能利用自己的知识来预测未来,当然前提是过去的模式在未来仍然成立。
更一般地说,RNNs 可以处理任意长度的序列,而不是固定大小的输入。例如,它们可以将句子、文档或音频样本作为输入,使它们非常适用于自然语言处理应用,如自动翻译或语音转文本。
在本章中,我们将首先介绍 RNNs 的基本概念以及如何使用时间反向传播来训练它们。然后,我们将使用它们来预测时间序列。在此过程中,我们将研究常用的 ARMA 模型系列,通常用于预测时间序列,并将它们用作与我们的 RNNs 进行比较的基准。之后,我们将探讨 RNNs 面临的两个主要困难:
- 不稳定的梯度(在第十一章中讨论),可以通过各种技术来缓解,包括循环丢失和循环层归一化。
- (非常)有限的短期记忆,可以使用 LSTM 和 GRU 单元进行扩展。
RNNs 并不是处理序列数据的唯一类型的神经网络。对于小序列,常规的密集网络可以胜任,而对于非常长的序列,例如音频样本或文本,卷积神经网络也可以表现得相当不错。我们将讨论这两种可能性,并通过实现 WaveNet 来结束本章-一种能够处理数万个时间步的 CNN 架构。让我们开始吧!
Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(六)(3)https://developer.aliyun.com/article/1482442