DeepLab-V3+ 是一个很好的 segmentation 架构。→ arXiv 论文链接在此
可以用 Inception-ResNet V2 作为脊柱 backbone,搭建一个 DeepLab-V3+ 模型,在 Cityscapes 数据集上做 segmentation 任务,效果如下。
下面用 Keras/TensorFlow 2.8 来搭建这个 DeepLab-V3+ 模型。
DeepLab-V3+ 的主体架构有 3 个关键点:
为了方便训练模型和调试,需要使用 Jupyter Lab,在 cityscapes_deeplab_v3plus.ipynb 文件中进行模型训练。
而其它函数,则集中放在 Python 文件 deeplab_v3_plus.py 中。
TensorFlow: 2.8
使用时,只需要在 Jupyter Lab 中打开 cityscapes_deeplab_v3plus.ipynb 文件操作即可。下面为 Jupyter Lab 中训练模型部分的图示。
如果显存不够大,可以设置如下 2 项:
如果是单张 RTX 3090 显卡,可以使用模型默认的设置,MODEL_IMAGE_SIZE = 512 x 1024 , BATCH_SIZE = 8。如果显存不够,可以降低图片大小为 320 x 640,或降低批次大小。
我的模型参考了 Keras 官方代码。→ 官方代码链接在此 官方代码使用的是一个不同的 backbone 和一个不同的数据集,有兴趣的同学可以学习一下。
需要注意的是,官方代码中有一点不足,即在对标签 mask 进行图片缩放操作时,使用了 bilinear 插值方法。但实际上,应该使用最邻近点插值方法(在 TensorFlow 中为 tf.image.ResizeMethod.NEAREST_NEIGHBOR。这个问题属于吴恩达在深度学习课程中所说的 subtlety,即容易被忽视的要点)。经验证后发现,使用官方代码的插值方法,会使得准确度下降 2%。
举例说明为什么只能用最邻近点插值方法。假设 mask 图片中有一个桌子,桌子上面放着一个杯子。桌子的类别为 0,而杯子的类别为 2。当使用 bilinear 或 bicubic 插值方法时,可能会插入一个值 1,但是类别 1 有可能是代表了一辆火车。这时 mask 中就会出现一个错误的情形,即一辆火车被夹在一个桌子和一个杯子中间,从而导致模型学习到一些错误的信息。反之,如果使用最邻近点插值方法,就不会出现这种问题。
下面是创建 DeepLab-V3+ 模型的主函数,其它完整数据,可以在 github 下载。→ 下载链接在此
def inceptionresnetv2_deeplabv3plus(model_image_size, num_classes,
rate_dropout=0.1):
"""使用 Inception-ResNet V2 作为 backbone,创建一个 DeepLab-V3+ 模型。
Arguments:
model_image_size: 一个整数型元祖,表示输入给模型的图片大小,格式为
(height, width, depth)。注意对于 Inception-ResNet V2,要求输入图片的像
素值必须转换到 [-1, 1] 之间。
为了便于说明数组形状,这里假设模型输入图片的大小为 (512, 1024, 3)。
num_classes: 一个整数,是模型中需要区分类别的数量。
rate_dropout: 一个浮点数,范围是 [0, 1],表示 SpatialDropout2D 层的比例。
Returns:
model: 一个以 Inception-ResNet V2 为脊柱的 DeepLab-V3+ 模型。
"""
# 新建模型之前,先用 clear_session 把状态清零。
keras.backend.clear_session()
model_input = keras.Input(shape=(*model_image_size, 3))
conv_base = keras.applications.InceptionResNetV2(
include_top=False,
input_tensor=model_input)
# low_level_feature_backbone 形状为 (None, 124, 252, 192)。
low_level_feature_backbone = conv_base.get_layer(
'activation_4').output
# 因为需要 low_level_feature_backbone的特征图为 128, 256, 所以要用 2次
# Conv2DTranspose。
for _ in range(2):
low_level_feature_backbone = convolution_block(
low_level_feature_backbone)
low_level_feature_backbone = keras.layers.Conv2DTranspose(
filters=256, kernel_size=3,
kernel_initializer=keras.initializers.HeNormal())(
low_level_feature_backbone)
# low_level_feature_backbone 形状为 (None, 128, 256, 256)。
low_level_feature_backbone = convolution_block(
low_level_feature_backbone, num_filters=256, kernel_size=1)
if rate_dropout != 0:
low_level_feature_backbone = keras.layers.SpatialDropout2D(
rate=rate_dropout)(low_level_feature_backbone)
# encoder_backbone 形状为 (None, 30, 62, 1088)。
encoder_backbone = conv_base.get_layer('block17_10_ac').output
# 在特征图被放大之前,都算作 encoder 部分,因为这部分内容都是在图片进行理解,所以 DSPP
# 也算作 encoder 部分。下面进行 DSPP 操作。
# encoder_backbone 形状为 (None, 30, 62, 256)。
encoder_backbone = dilated_spatial_pyramid_pooling(encoder_backbone)
# 下面进入解码器 decoder 部分,开始放大特征图。
# encoder_backbone 形状为 (None, 32, 64, 256)。
decoder_backbone = keras.layers.Conv2DTranspose(
256, kernel_size=3, kernel_initializer=keras.initializers.HeNormal())(
encoder_backbone)
# encoder_backbone 形状为 (None, 128, 256, 256)。
decoder_backbone = keras.layers.UpSampling2D(
size=(4, 4), interpolation='bilinear')(decoder_backbone)
if rate_dropout != 0:
decoder_backbone = keras.layers.SpatialDropout2D(rate=rate_dropout)(
decoder_backbone)
# x 形状为 (None, 128, 256, 512)。
x = keras.layers.Concatenate()(
[decoder_backbone, low_level_feature_backbone])
# 下面进行2次卷积,将前面 2 个合并的分支信息进行处理,特征通道数量将变为 256。
for _ in range(2):
x = convolution_block(x)
if rate_dropout != 0:
x = keras.layers.SpatialDropout2D(rate=rate_dropout)(x)
# x 形状为 (None, 512, 1024, 256)。
x = keras.layers.UpSampling2D(size=(4, 4), interpolation='bilinear')(x)
if rate_dropout != 0:
x = keras.layers.SpatialDropout2D(rate=rate_dropout)(x)
# 尝试增加一个分支 down_sampling_1,特征图大小和输入图片大小一样。目的是使得在预测结果
# mask 中,物体的轮廓更准确。
down_sampling_1 = model_input
down_sampling_1 = convolution_block(down_sampling_1, num_filters=8,
separableconv=False)
for _ in range(2):
down_sampling_1 = convolution_block(down_sampling_1, num_filters=8)
if rate_dropout != 0:
down_sampling_1 = keras.layers.SpatialDropout2D(rate=rate_dropout)(
down_sampling_1)
# x 形状为 (None, 512, 1024, 264)。
x = keras.layers.Concatenate()([down_sampling_1, x])
for _ in range(2):
x = convolution_block(x, num_filters=64)
# model_output 形状为 (None, 512, 1024, 34)。
model_output = keras.layers.Conv2D(
num_classes, kernel_size=(1, 1), padding='same')(x)
model = keras.Model(inputs=model_input, outputs=model_output)
return model