TensorFlow 实战(三)(3)https://developer.aliyun.com/article/1522744
在列表 8.5 中,我们执行以下翻译:
- 随机水平翻转图像
- 随机改变图像的色调(最多 10%)
- 随机改变图像的亮度(最多 10%)
- 随机改变图像的对比度(最多 20%)
通过使用tf.data.Dataset.map()
函数,我们可以在管道中轻松执行指定的随机增强步骤,如果用户在管道中启用了增强(即通过将augmentation
变量设置为True
)。请注意,我们仅对输入图像执行一些增强(例如,随机色调、亮度和对比度调整)。我们还将给用户提供具有不同尺寸的输入和目标(即输出)的选项。这通过将输出调整为由output_size
参数定义的所需大小来实现。我们用于此任务的模型具有不同尺寸的输入和输出维度:
if output_size: image_ds = image_ds.map( lambda x, y: ( x, tf.image.resize(y, output_size, method='nearest') ) )
再次,这里我们使用最近邻插值来调整目标的大小。接下来,我们将对数据进行洗牌(如果用户将shuffle
参数设置为True
):
if shuffle: image_ds = image_ds.shuffle(buffer_size=batch_size*5)
混洗函数有一个重要参数称为buffer_size
,它确定了加载到内存中以随机选择样本的样本数量。buffer_size
越高,引入的随机性就越多。另一方面,较高的buffer_size
意味着更高的内存消耗。现在是批处理数据的时候了,所以在迭代时不是单个数据点,而是当我们迭代时获得一批数据:
image_ds = image_ds.batch(batch_size).repeat(epochs)
这是使用tf.data.Dataset.batch()
函数完成的,将所需的批次大小作为参数传递。在使用tf.data
管道时,如果要运行多个周期,还需要使用tf.data.Dataset.repeat()
函数重复管道给定次数的周期。
我们为什么需要tf.data.Dataset.repeat()
?
tf.data.Dataset
是一个生成器。生成器的一个独特特点是您只能迭代一次。当生成器到达正在迭代的序列的末尾时,它将通过抛出异常退出。因此,如果您需要多次迭代生成器,您需要根据需要重新定义生成器。通过添加tf.data.Dataset.repeat(epochs)
,生成器将根据需要重新定义(在此示例中为 epochs 次)。
在我们的tf.data
管道完成之前,还需要一步。如果查看目标(y)输出的形状,您将看到它具有通道维度为 1。但是,对于我们将要使用的损失函数,我们需要摆脱该维度:
image_ds = image_ds.map(lambda x, y: (x, tf.squeeze(y)))
对此,我们将使用tf.squeeze()
操作,该操作会删除尺寸为 1 的任何维度并返回一个张量。例如,如果您压缩一个尺寸为[1,3,2,1,5]
的张量,您将得到一个尺寸为[3,2,5]
的张量。最终的代码在清单 8.6 中提供。您可能会注意到两个突出显示的步骤。这是两个流行的优化步骤:缓存和预提取。
清单 8.6 最终的tf.data
管道
def get_subset_tf_dataset( subset_filename_gen_func, batch_size, epochs, input_size=(256, 256), output_size=None, resize_to_before_crop=None, augmentation=False, shuffle=False ): if augmentation and not resize_to_before_crop: raise RuntimeError( ❶ "You must define resize_to_before_crop when augmentation is enabled." ) filename_ds = tf.data.Dataset.from_generator( subset_filename_gen_func, output_types=(tf.string, tf.string) ❷ ) image_ds = filename_ds.map(lambda x,y: ( tf.image.decode_jpeg(tf.io.read_file(x)), ❸ tf.numpy_function(load_image_func, [y], [tf.uint8]) )).cache() image_ds = image_ds.map(lambda x, y: (tf.cast(x, 'float32')/255.0, y)) ❹ def randomly_crop_or_resize(x,y): ❺ """ Randomly crops or resizes the images """ ... def rand_crop(x, y): """ Randomly crop images after enlarging them """ ... def resize(x, y): """ Resize images to a desired size """ ... image_ds = image_ds.map(lambda x,y: randomly_crop_or_resize(x,y)) ❻ image_ds = image_ds.map(lambda x,y: fix_shape(x,y, target_size=input_size)) ❼ if augmentation: image_ds = image_ds.map(lambda x, y: randomly_flip_horizontal(x,y)) ❽ image_ds = image_ds.map(lambda x, y: (tf.image.random_hue(x, 0.1), y)) ❽ image_ds = image_ds.map(lambda x, y: (tf.image.random_brightness(x, 0.1), y))❽ image_ds = image_ds.map( lambda x, y: (tf.image.random_contrast(x, 0.8, 1.2), y) ❽ ) if output_size: image_ds = image_ds.map( lambda x, y: (x, tf.image.resize(y, output_size, method='nearest')) ❾ ) if shuffle: image_ds = image_ds.shuffle(buffer_size=batch_size*5) ❿ image_ds = image_ds.batch(batch_size).repeat(epochs) ⓫ image_ds = image_ds.prefetch(tf.data.experimental.AUTOTUNE) ⓬ image_ds = image_ds.map(lambda x, y: (x, tf.squeeze(y))) ⓭ return image_ds ⓮
❶ 如果启用了增强,则需要定义resize_to_before_crop
。
❷ 根据所请求的数据子集返回文件名列表。
❸ 将图像加载到内存中。cache()
是一个优化步骤,将在文本中讨论。
❹ 规范化输入图像。
❺ 随机裁剪或调整图像大小的函数
❻ 在图像上执行随机裁剪或调整大小。
❼ 设置结果图像的形状。
❽ 在数据上随机执行各种增强。
❾ 根据需要调整输出图像的大小。
❿ 使用缓冲区对数据进行随机混洗。
⓫ 批处理数据并为所需的周期重复该过程。
⓬ 这是文本中详细讨论的优化步骤。
⓭ 从目标图像中删除不必要的维度。
⓮ 获取最终的tf.data
管道。
这不是一次轻松的旅程,但却是一次有益的旅程。我们学到了一些定义数据管道的重要技能:
- 定义一个生成器,返回要获取的数据的文件名
- 在
tf.data
管道中加载图像 - 操作图像(调整大小、裁剪、亮度调整等)
- 数据批处理和重复
- 为不同数据集定义多个流水线,这些数据集具有不同的要求
接下来,我们将查看一些优化技术,将我们平庸的数据流水线转变为令人印象深刻的数据高速公路。
8.2.1 优化 tf.data 流水线
TensorFlow 是一个用于消耗大型数据集的框架,高效地消耗数据是一个关键优先事项。我们对 tf.data 流水线的讨论中仍然缺少一件事,即 tf.data 流水线可用的优化步骤。在列表 8.6 中,缓存和预取两个步骤被加粗设置。如果您对其他优化技术感兴趣,可以在 www.tensorflow.org/guide/data_performance
上阅读更多。
缓存将在数据通过流水线时将其存储在内存中。这意味着当缓存时,该步骤(例如,从磁盘加载数据)仅在第一个时期发生。随后的时期将从内存中保存的缓存数据中读取。在这里,您可以看到我们将图像加载到内存后进行缓存。这样,TensorFlow 仅在第一个时期加载图像:
image_ds = filename_ds.map(lambda x,y: ( tf.image.decode_jpeg(tf.io.read_file(x)), tf.numpy_function(load_image_func, [y], [tf.uint8]) )).cache()
Prefetching 是你可以使用的另一个强大武器,它允许你利用设备的多进程能力:
image_ds = image_ds.prefetch(tf.data.experimental.AUTOTUNE)
提供给函数的参数决定了预取多少数据。通过将其设置为 AUTOTUNE,TensorFlow 将根据可用资源决定要获取的最佳数据量。假设一个简单的数据流水线从磁盘加载图像并训练模型。然后,数据读取和模型训练将交替进行。这导致了显着的空闲时间,因为模型在数据加载时空闲,反之亦然。
然而,多亏了预取,情况就不一样了。预取利用后台线程和内部缓冲区,在模型训练时提前加载数据。当下一次迭代到来时,模型可以无缝地继续训练,因为数据已经被提前加载到内存中。图 8.7 显示了顺序执行和预取之间的差异。
图 8.7 模型训练中的顺序执行与基于预取的执行的区别
接下来,我们将查看图像分割问题的完整 tf.data 流水线。
8.2.2 最终的 tf.data 流水线
最后,您可以使用我们迄今为止定义的函数来定义数据流水线(们)。在这里,我们为三个不同的目的定义了三种不同的数据流水线:训练、验证和测试(见下面的列表)。
列表 8.7 创建训练/验证/测试数据流水线实例
orig_dir = os.path.join( 'data', 'VOCtrainval_11-May-2012', 'VOCdevkit', 'VOC2012', 'JPEGImages' ❶ ) seg_dir = os.path.join( 'data', 'VOCtrainval_11-May-2012', 'VOCdevkit', 'VOC2012', 'SegmentationClass' ❷ ) subset_dir = os.path.join( 'data', 'VOCtrainval_11-May-2012', 'VOCdevkit', 'VOC2012', 'ImageSets', ❸ 'Segmentation' ) partial_subset_fn = partial( get_subset_filenames, orig_dir=orig_dir, seg_dir=seg_dir, subset_dir=subset_dir ❹ ) train_subset_fn = partial(partial_subset_fn, subset='train') ❺ val_subset_fn = partial(partial_subset_fn, subset='val') ❺ test_subset_fn = partial(partial_subset_fn, subset='test') ❺ input_size = (384, 384) ❻ tr_image_ds = get_subset_tf_dataset( ❼ train_subset_fn, batch_size, epochs, input_size=input_size, resize_to_before_crop=(444,444), augmentation=True, shuffle=True ) val_image_ds = get_subset_tf_dataset( ❽ val_subset_fn, batch_size, epochs, input_size=input_size, shuffle=False ) test_image_ds = get_subset_tf_dataset( ❾ test_subset_fn, batch_size, 1, input_size=input_size, shuffle=False )
❶ 包含输入图像的目录
❷ 包含注释图像(目标)的目录
❸ 包含训练/验证/测试文件名的文本文件所在的目录
❹ 从 get_subset_filenames 定义一个可重用的部分函数。
❺ 为训练/验证/测试数据定义三个生成器。
❻ 定义输入图像尺寸。
❼ 定义一个使用数据增强和洗牌的训练数据流水线。
❽ 定义一个不使用数据增强或洗牌的验证数据流水线。
❾ 定义一个测试数据流水线。
首先,我们定义了几个重要的路径:
- orig_dir—包含输入图像的目录
- seg_dir—包含目标图像的目录
- subset_dir—包含列出训练和验证实例的文本文件(train.txt、val.txt)的目录
然后我们将从我们之前定义的 get_subset_filenames() 函数定义一个偏函数,以便我们可以通过设置函数的 subset 参数来获取一个生成器。利用这种技术,我们将定义三个生成器:train_subset_fn、val_subset_fn 和 test_subset_fn。最后,我们将使用 get_subset_tf_dataset() 函数定义三个 tf.data.Datasets。我们的流水线将具有以下特征:
- 训练流水线—在每个 epoch 上执行数据增强和数据洗牌
- 验证流水线和测试流水线—无增强或洗牌
我们将定义的模型期望一个 384 × 384 大小的输入和一个输出。在训练数据流水线中,我们将图像调整大小为 444 × 444,然后随机裁剪一个 384 × 384 大小的图像。接下来,我们将看一下解决方案的核心部分:定义图像分割模型。
练习 2
您已经获得了一个包含两个张量的小数据集:张量 a 包含 100 个大小为 64 × 64 × 3 的图像(即,100 × 64 × 64 × 3 的形状),张量 b 包含 100 个大小为 32 × 32 × 1 的分割蒙版(即,100 × 32 × 32 × 1 的形状)。您被要求使用讨论过的函数定义一个 tf.data.Dataset,它将
- 将分割蒙版调整大小以匹配输入图像大小(使用最近的插值)
- 使用转换(x - 128)/255 对输入图像进行标准化,其中单个图像是 x
- 将数据批处理为大小为 32 的批次,并重复五个 epochs
- 使用自动调优功能预取数据
8.3 DeepLabv3:使用预训练网络对图像进行分割
现在是创建流水线的核心部分的时候了:深度学习模型。根据一位在类似问题上工作的自动驾驶汽车公司同事的反馈,您将实现一个 DeepLab v3 模型。这是一个建立在预训练的 ResNet 50 模型(在图像分类上训练)的基础上的模型,但最后几层被改为执行 空洞卷积 而不是标准卷积。它使用金字塔聚合模块,在不同尺度上使用空洞卷积来生成不同尺度上的图像特征,以产生最终输出。最后,它使用双线性插值层将最终输出调整大小为所需大小。您相信 DeepLab v3 能够提供良好的初始结果。
基于深度神经网络的分割模型可以广泛分为两类:
- 编码器解码器模型(例如,U-Net 模型)
- 完全卷积网络(FCN)后跟金字塔聚合模块(例如,DeepLab v3 模型)
编码器-解码器模型的一个著名例子是 U-Net 模型。换句话说,U-Net 具有逐渐创建输入的较小、更粗略表示的编码器。然后,解码器接收编码器生成的表示,并逐渐上采样(即增加输出的大小)直到达到输入图像的大小为止。上采样是通过一种称为转置卷积的操作实现的。最后,你以端到端的方式训练整个结构,其中输入是输入图像,目标是相应图像的分割掩码。我们不会在本章讨论这种类型的模型。然而,我在附录 B 中包含了一个详细的步骤说明(以及模型的实现)。
另一种分割模型引入了一个特殊的模块来替换解码器。我们称之为金字塔聚合模块。它的目的是在不同尺度上收集空间信息(例如来自各种中间卷积层的不同大小的输出),以提供关于图像中存在的对象的细粒度上下文信息。DeepLab v3 是这种方法的一个典型示例。我们将对 DeepLab v3 模型进行详细分析,并借此在分割任务上取得卓越成果。
研究人员和工程师更倾向于使用金字塔聚合模块的方法。可能有很多原因。一个有利可图的原因是,使用金字塔聚合的网络参数较少,比采用基于编码器-解码器的对应网络更少。另一个原因可能是,通常引入新模块(与编码器-解码器相比)提供更多灵活性,可以在多个尺度上设计高效准确的特征提取方法。
金字塔聚合模块有多重要?为了了解这一点,我们首先必须了解完全卷积网络的结构是什么样的。图 8.8 说明了这种分割模型的通用结构。
图 8.8 全卷积网络使用金字塔聚合模块的一般结构和组织方式
了解金字塔聚合模块的重要性的最佳方式是看看如果没有它会发生什么。如果是这种情况,那么最后一个卷积层将承担建立最终分割掩码(通常是最后一层输出的 16-32 倍大)的巨大且不切实际的责任。毫不奇怪,在最终卷积层和最终分割掩码之间存在巨大的表征瓶颈,从而导致性能不佳。在卷积神经网络中通常强制执行的金字塔结构导致最后一层的输出宽度和高度非常小。
金字塔聚合模块弥合了这一差距。 它通过组合几个不同的中间输出来做到这一点。 这样,网络就有了充足的细粒度(来自较早层)和粗糙的(来自更深层)细节,以构建所需的分割掩模。 细粒度的表示提供了关于图像的空间/上下文信息,而粗糙的表示提供了关于图像的高级信息(例如,存在哪些对象)。 通过融合这两种类型的表示,生成最终输出的任务变得更加可行。
为什么不是金字塔而是摩天大楼呢?
你可能会想,如果随着时间的推移使输出变小会导致信息的丢失,“为什么不保持相同的大小呢?”(因此有了摩天大楼这个术语)。 这是一个不切实际的解决方案,主要有两个原因。
首先,通过池化或步幅减小输出大小是一种重要的正则化方法,它迫使网络学习平移不变特征(正如我们在第六章讨论的那样)。 如果去掉这个过程,我们就会阻碍网络的泛化能力。
其次,不减小输出大小将显著增加模型的内存占用。 这反过来会极大地限制网络的深度,使得创建更深层次的网络更加困难。
DeepLab v3 是一系列模型的金童,这些模型起源于并且是由来自谷歌的几位研究人员在论文“重新思考用于语义图像分割的空洞卷积”(arxiv.org/pdf/1706.05587.pdf
)中提出的。
大多数分割模型都面临着由常见且有益的设计原则引起的不利副作用。 视觉模型将步幅/池化结合起来,使网络平移不变。 但这个设计思想的一个不受欢迎的结果是输出大小的不断减小。 这通常导致最终输出比输入小 16-32 倍。 作为密集预测任务,图像分割任务严重受到这种设计思想的影响。 因此,大多数涌现出来的具有突破性的网络都是为了解决这个问题。 DeepLab 模型就是为了解决这个问题而诞生的。 现在让我们看看 DeepLab v3 是如何解决这个问题的。
DeepLab v3 使用在 ImageNet 图像分类数据集上预训练的 ResNet-50(arxiv.org/pdf/1512.03385.pdf
)作为提取图像特征的主干。 几年前,它是计算机视觉社区中引起轰动的开创性残差网络之一。 DeepLab v3 对模型进行了几个架构上的改变,以缓解这个问题。 此外,DeepLab v3 引入了一个全新的组件,称为空洞空间金字塔池化(ASPP)。 我们将在接下来的章节中更详细地讨论每个组件。
8.3.1 ResNet-50 模型的快速概述
ResNet-50 模型由多个卷积块组成,后跟一个全局平均池化层和一个具有 softmax 激活的完全连接的最终预测层。卷积块是模型的创新部分。原始模型有 16 个卷积块,组织成五个组。一个单独的块由三个卷积层组成(1 × 1 卷积层,步长为 2,3 × 3 卷积层,1 × 1 卷积层),批量归一化和残差连接。我们在第七章深入讨论了残差连接。接下来,我们将讨论模型中始终使用的核心计算,称为孔卷积。
8.3.2 孔卷积:用孔扩大卷积层的感受野
与标准 ResNet-50 相比,DeepLab v3 骄傲地使用孔卷积的主要变化。孔(法语意为“孔”)卷积,也称为扩张卷积,是标准卷积的变体。孔卷积通过在卷积参数之间插入“孔”来工作。感受野的增加由一个称为 扩张率 的参数控制。更高的扩张率意味着卷积中实际参数之间有更多的孔。孔卷积的一个主要好处是能够增加感受野的大小,而不会损害卷积层的参数效率。
图 8.9 孔卷积与标准卷积的比较。标准卷积是孔卷积的特例,其中速率为 1。随着扩张率的增加,层的感受野也会增加。
图 8.9 显示了较大的扩张率导致更大的感受野。阴影灰色框的数量表示参数数量,而虚线,轻微阴影的框表示感受野的大小。正如你所见,参数数量保持不变,而感受野增加。从计算上讲,将标准卷积扩展到孔卷积非常简单。你所需要做的就是在孔卷积操作中插入零。
等等!孔卷积如何帮助分割模型?
正如我们讨论的那样,CNN 的金字塔结构提出的主要问题是输出逐渐变小。最简单的解决方案,不改变学习的参数,是减小层的步幅。尽管技术上会增加输出大小,但在概念上存在问题。
要理解这一点,假设 CNN 的第 i 层的步长为 2,并且获得了 h × w 大小的输入。然后,第 i+1 层获得了 h/2 × w/2 大小的输入。通过移除第 i 层的步长,它获得了 h × w 大小的输出。然而,第 i+1 层的核已经被训练成看到一个更小的输出,所以通过增加输入的大小,我们破坏了(或减少了)层的感受野。通过引入空洞卷积,我们补偿了该感受野的减小。
现在让我们看看 ResNet-50 如何被重新用于图像分割。首先,我们从tf.keras.applications
模块下载它。ResNet-50 模型的架构如下所示。首先,它有一个步幅为 2 的卷积层和一个步幅为 2 的池化层。之后,它有一系列卷积块,最后是一个平均池化层和完全连接的输出层。这些卷积块具有卷积层的分层组织。每个卷积块由几个子块组成,每个子块由三个卷积层组成(即 1 × 1 卷积、3 × 3 卷积和 1 × 1 卷积),以及批量归一化。
使用 Keras 函数 API 实现 DeepLab v3
从输入开始直到conv4
块的网络保持不变。根据原始 ResNet 论文的符号,这些块被标识为conv2
、conv3
和conv4
块组。我们的第一个任务是创建一个包含输入层到原始 ResNet-50 模型的conv4
块的模型。之后,我们将专注于根据 DeepLab v3 论文重新创建最终卷积块(即conv5
):
# Pretrained model and the input inp = layers.Input(shape=target_size+(3,)) resnet50 = tf.keras.applications.ResNet50( include_top=False, input_tensor=inp,pooling=None ) for layer *in* resnet50.layers: if layer.name == "conv5_block1_1_conv": break out = layer.output resnet50_upto_conv4 = models.Model(resnet50.input, out)
如图所示,我们找到了 ResNet-50 模型中位于“conv5_block1_1_conv”之前的最后一层,这将是conv4
块组的最后一层。有了这个,我们可以定义一个临时模型,该模型包含从输入到conv4
块组的最终输出的层。后来,我们将专注于通过引入论文中的修改和新组件来增强这个模型。我们将使用扩张卷积重新定义conv5
块。为此,我们需要了解 ResNet 块的构成(图 8.10)。我们可以假设它有三个不同的级别。
图 8.10 ResNet-50 中卷积块的解剖。对于这个示例,我们展示了 ResNet-50 的第一个卷积块。卷积块组的组织包括三个不同的级别。
现在让我们实现一个函数来表示使用扩张卷积时的每个级别。为了将标准卷积层转换为扩张卷积,我们只需将所需的速率传递给tf.keras.layers.Conv2D
层的dilation_rate
参数即可。首先,我们将实现一个表示级别 3 块的函数,如下清单所示。
清单 8.8 ResNet-50 中的级别 3 卷积块
def block_level3( inp, filters, kernel_size, rate, block_id, convlayer_id, activation=True ❶ ): """ A single convolution layer with atrous convolution and batch normalization inp: 4-D tensor having shape [batch_size, height, width, channels] filters: number of output filters kernel_size: The size of the convolution kernel rate: dilation rate for atrous convolution block_id, convlayer_id - IDs to distinguish different convolution blocks and layers activation: If true ReLU is applied, if False no activation is applied """ conv5_block_conv_out = layers.Conv2D( filters, kernel_size, dilation_rate=rate, padding='same', ❷ name='conv5_block{}_{}_conv'.format(block_id, convlayer_id) )(inp) conv5_block_bn_out = layers.BatchNormalization( name='conv5_block{}_{}_bn'.format(block_id, convlayer_id) ❸ )(conv5_block_conv_out) if activation: conv5_block_relu_out = layers.Activation( 'relu', name='conv5_block{}_{}_relu'.format(block_id, convlayer_id) ❹ )(conv5_block_bn_out) return conv5_block_relu_out else: return conv5_block_bn_out ❺
❶ 在这里,inp 接受具有形状 [批量大小,高度,宽度,通道] 的 4D 输入。
❷ 对输入执行二维卷积,使用给定数量的滤波器、内核大小和扩张率。
❸ 对卷积层的输出执行批量归一化。
❹ 如果激活设置为 True,则应用 ReLU 激活。
❺ 如果激活设置为 False,则返回输出而不进行激活。
级别 3 块具有一个单独的卷积层,具有所需的扩张率和批量归一化层,随后是非线性 ReLU 激活层。接下来,我们将为级别 2 块编写一个函数(见下一个清单)。
清单 8.9 在 ResNet-50 中的 2 级卷积块
def block_level2(inp, rate, block_id): """ A level 2 resnet block that consists of three level 3 blocks """ block_1_out = block_level3(inp, 512, (1,1), rate, block_id, 1) block_2_out = block_level3(block_1_out, 512, (3,3), rate, block_id, 2) block_3_out = block_level3( block_2_out, 2048, (1,1), rate, block_id, 3, activation=False ) return block_3_out
一个 2 级块由具有给定扩张率的三个级别 3 块组成,这些块具有以下规格的卷积层:
- 1 × 1 卷积层,具有 512 个滤波器和所需的扩张率
- 3 × 3 卷积层,具有 512 个滤波器和所需的扩张率
- 1 × 1 卷积层,具有 2048 个滤波器和所需的扩张率
除了使用空洞卷积外,这与 ResNet-50 模型中原始 conv5 块的 2 级块完全相同。所有构建块准备就绪后,我们可以使用空洞卷积实现完整的 conv5 块(见下一个清单)。
清单 8.10 实现最终的 ResNet-50 卷积块组(级别 1)
def resnet_block(inp, rate): """ Redefining a resnet block with atrous convolution """ block0_out = block_level3( inp, 2048, (1,1), 1, block_id=1, convlayer_id=0, activation=False ❶ ) block1_out = block_level2(inp, 2, block_id=1) ❷ block1_add = layers.Add( name='conv5_block{}_add'.format(1))([block0_out, block1_out] ❸ ) block1_relu = layers.Activation( 'relu', name='conv5_block{}_relu'.format(1) ❹ )(block1_add) block2_out = block_level2 (block1_relu, 2, block_id=2) # no relu ❺ block2_add = layers.Add( name='conv5_block{}_add'.format(2) ❻ )([block1_add, block2_out]) block2_relu = layers.Activation( 'relu', name='conv5_block{}_relu'.format(2) ❼ )(block2_add) block3_out = block_level2 (block2_relu, 2, block_id=3) ❽ block3_add = layers.Add( name='conv5_block{}_add'.format(3) ❽ )([block2_add, block3_out]) block3_relu = layers.Activation( 'relu', name='conv5_block{}_relu'.format(3) ❽ )(block3_add) return block3_relu
❶ 创建一个级别 3 块(block0),为第一个块创建残差连接。
❷ 定义第一个具有扩张率为 2 的 2 级块(block1)。
❸ 从 block0 到 block1 创建一个残差连接。
❹ 对结果应用 ReLU 激活。
❺ 具有扩张率为 2 的第二级 2 块(block2)
❻ 从 block1 到 block2 创建一个残差连接。
❼ 应用 ReLU 激活。
❽ 对 block1 和 block2 应用类似的过程以创建 block3。
这里没有黑魔法。函数 resnet_block 将我们已经讨论的函数的输出放置在一起以组装最终的卷积块。特别地,它具有三个级别 2 块,其残差连接从前一个块到下一个块。最后,我们可以通过使用我们定义的中间模型的输出(resnet50_ upto_conv4)作为输入并使用扩张率为 2 调用 resnet_block 函数来获得 conv5 块的最终输出:
resnet_block4_out = resnet_block(resnet50_upto_conv4.output, 2)
8.3.4 实现空洞空间金字塔池化模块
在这里,我们将讨论 DeepLab v3 模型最令人兴奋的创新。空洞空间金字塔池化(ASPP)模块有两个目的:
- 聚合通过使用不同扩张率产生的输出获得的图像的多尺度信息
- 结合通过全局平均池化获得的高度摘要的信息
ASPP 模块通过在最后一个 ResNet-50 输出上执行不同的卷积来收集多尺度信息。具体来说,ASPP 模块执行 1 × 1 卷积、3 × 3 卷积(r = 6)、3 × 3 卷积(r = 12)和 3 × 3 卷积(r = 18),其中 r 是 dilation 率。所有这些卷积都有 256 个输出通道,并实现为级别 3 的块(由函数 block_level3() 提供)。
ASRP 通过执行全局平均池化来捕获高级信息,然后进行 1 × 1 卷积,输出通道为 256,以匹配多尺度输出的输出大小,最后是一个双线性上采样层,用于上采样全局平均池化所缩小的高度和宽度维度。记住,双线性插值通过计算相邻像素的平均值来上采样图像。图 8.11 说明了 ASPP 模块。
图 8.11 DeepLab v3 模型中使用的 ASPP 模块
ASPP 模块的任务可以概括为一个简明的函数。我们从之前完成的工作中已经拥有了实现此函数所需的所有工具(请参阅下面的清单)。
清单 8.11 实现 ASPP
def atrous_spatial_pyramid_pooling(inp): """ Defining the ASPP (Atrous spatial pyramid pooling) module """ # Part A: 1x1 and atrous convolutions outa_1_conv = block_level3( inp, 256, (1,1), 1, '_aspp_a', 1, activation='relu' ) ❶ outa_2_conv = block_level3( inp, 256, (3,3), 6, '_aspp_a', 2, activation='relu' ) ❷ outa_3_conv = block_level3( inp, 256, (3,3), 12, '_aspp_a', 3, activation='relu' ) ❸ outa_4_conv = block_level3( inp, 256, (3,3), 18, '_aspp_a', 4, activation='relu' ) ❹ # Part B: global pooling outb_1_avg = layers.Lambda( lambda x: K.mean(x, axis=[1,2], keepdims=True) )(inp) ❺ outb_1_conv = block_level3( outb_1_avg, 256, (1,1), 1, '_aspp_b', 1, activation='relu' ❻ ) outb_1_up = layers.UpSampling2D((24,24), interpolation='bilinear')(outb_1_avg) ❼ out_aspp = layers.Concatenate()( [outa_1_conv, outa_2_conv, outa_3_conv, outa_4_conv, outb_1_up] ❽ ) return out_aspp out_aspp = atrous_spatial_pyramid_pooling(resnet_block4_out) ❾
❶ 定义一个 1 × 1 卷积。
❷ 定义一个带有 256 个滤波器和 dilation 率为 6 的 3 × 3 卷积。
❸ 定义一个带有 256 个滤波器和 dilation 率为 12 的 3 × 3 卷积。
❹ 定义一个带有 256 个滤波器和 dilation 率为 18 的 3 × 3 卷积。
❺ 定义一个全局平均池化层。
❻ 定义一个带有 256 个滤波器的 1 × 1 卷积。
❼ 使用双线性插值上采样输出。
❽ 连接所有的输出。
❾ 创建一个 ASPP 的实例。
ASPP 模块由四个级别 3 的块组成,如代码所示。第一个块包括一个 1 × 1 卷积,带有 256 个无 dilation 的滤波器(这产生了 outa_1_conv)。后三个块包括 3 × 3 卷积,带有 256 个滤波器,但具有不同的 dilation 率(即 6、12、18;它们分别产生 outa_2_conv、outa_3_conv 和 outa_4_conv)。这涵盖了从图像中聚合多个尺度的特征。然而,我们还需要保留关于图像的全局信息,类似于全局平均池化层(outb_1_avg)。这通过一个 lambda 层实现,该层将输入在高度和宽度维度上进行平均:
outb_1_avg = layers.Lambda(lambda x: K.mean(x, axis=[1,2], keepdims=True))(inp)
平均值的输出接着是一个带有 256 个滤波器的 1 × 1 卷积滤波器。然后,为了将输出大小与以前的输出相同,使用双线性插值的上采样层(这产生 outb_1_up):
outb_1_up = layers.UpSampling2D((24,24), interpolation='bilinear')(outb_1_avg)
最后,所有这些输出都通过 Concatenate 层连接到单个输出中,以产生最终输出 out_aspp。
8.3.5 将所有内容放在一起
现在是时候整合所有不同的组件,创建一个宏伟的分割模型了。接下来的清单概述了构建最终模型所需的步骤。
清单 8.12 最终的 DeepLab v3 模型
inp = layers.Input(shape=target_size+(3,)) ❶ resnet50= tf.keras.applications.ResNet50( include_top=False, input_tensor=inp,pooling=None ❷ ) for layer *in* resnet50.layers: if layer.name == "conv5_block1_1_conv": break out = layer.output ❸ resnet50_upto_conv4 = models.Model(resnet50.input, out) ❹ resnet_block4_out = resnet_block(resnet50_upto_conv4.output, 2) ❺ out_aspp = atrous_spatial_pyramid_pooling(resnet_block4_out) ❻ out = layers.Conv2D(21, (1,1), padding='same')(out_aspp) ❼ final_out = layers.UpSampling2D((16,16), interpolation='bilinear')(out) ❼ deeplabv3 = models.Model(resnet50_upto_conv4.input, final_out) ❽
❶ 定义 RGB 输入层。
❷ 下载并定义 ResNet50。
❸ 获取我们感兴趣的最后一层的输出。
❹ 从输入定义一个中间模型,到 conv4 块的最后一层。
❺ 定义删除的 conv5 ResNet 块。
❻ 定义 ASPP 模块。
❼ 定义最终输出。
❽ 定义最终模型。
注意观察模型中的线性层,它没有任何激活函数(例如 sigmoid 或 softmax)。这是因为我们计划使用一种特殊的损失函数,该函数使用 logits(在应用 softmax 之前从最后一层获得的未归一化分数)而不是归一化的概率分数。因此,我们将保持最后一层为线性输出,没有激活函数。
我们还需要执行最后一步操作:将原始 conv5 块的权重复制到我们的模型中新创建的 conv5 块。为此,首先需要将原始模型的权重存储如下:
w_dict = {} for l *in* ["conv5_block1_0_conv", "conv5_block1_0_bn", "conv5_block1_1_conv", "conv5_block1_1_bn", "conv5_block1_2_conv", "conv5_block1_2_bn", "conv5_block1_3_conv", "conv5_block1_3_bn"]: w_dict[l] = resnet50.get_layer(l).get_weights()
在编译模型之前,我们无法将权重复制到新模型中,因为在编译模型之前权重不会被初始化。在这之前,我们需要学习在分割任务中使用的损失函数和评估指标。为此,我们需要实现自定义损失函数和指标,并使用它们编译模型。这将在下一节中讨论。
练习 3
您想要创建一个新的金字塔聚合模块称为 aug-ASPP。这个想法与我们之前实现的 ASPP 模块类似,但有一些区别。假设您已经从模型中获得了两个中间输出:out_1 和 out_2(大小相同)。您必须编写一个函数,aug_aspp,它将获取这两个输出并执行以下操作:
- 对 out_1 进行 atrous 卷积,r=16,128 个过滤器,3×3 卷积,步幅为 1,并应用 ReLU 激活函数(输出将被称为 atrous_out_1)
- 对 out_1 和 out_2 进行 atrous 卷积,r=8,128 个过滤器,3×3 卷积,步幅为 1,并对两者应用 ReLU 激活函数(输出将被称为 atrous_out_2_1 和 atrous_out_2_2)
- 拼接 atrous_out_2_1 和 atrous_out_2_2(输出将被称为 atrous_out_2)
- 对 atrous_out_1 和 atrous_out_2 进行 1×1 卷积,使用 64 个过滤器并进行拼接(输出将被称为 conv_out)
- 使用双线性上采样将 conv_out 的大小加倍(在高度和宽度尺寸上),并应用 sigmoid 激活函数
TensorFlow 实战(三)(5)https://developer.aliyun.com/article/1522747