使用深度学习实现简单语义分割(FCN)

原文地址:https://medium.com/nanonets/how-to-do-image-segmentation-using-deep-learning-c673cc5862ef

image

语义分割是计算机视觉领域的一个关键问题,观察上图可以发现语义分割是实现完全场景理解的高层次任务之一。场景理解作为核心计算机视觉问题,其重要性在于越来越多的应用需要利用图像进行理解推断,包括自动驾驶、人机交互、虚拟现实等等。随着深度学习逐渐流行,许多语义分割问题可以使用深度网络架构解决,通常使用的是卷积神经网络,这种网络在准确率和效率上都大大超越了其他方法。


什么是语义分割?

语义分割是由粗糙到精细推断的一系列过程:

  • 首先进行分类,包括对整个输入进行一个预测。
  • 然后是定位/检测,不仅仅预测类别,还有关于类别空间位置的额外信息。
  • 最后,语义分割通过对每个像素进行密集预测实现细粒度推断,使得封闭区域的同一目标都被标注为同一类。

一些标准深度神经网络为计算机视觉领域做出了极大贡献,它们也经常作为语义分割的基础网络:

  • AlexNet:多伦多大学提出的具有开拓性的深度卷积神经网络,以84.6%的测试准确率赢得了 2012 年 ImageNet 竞赛冠军。AlexNet 包括 5 个卷积层、3 个最大池化层、3 个全连接层,ReLU作为非线性激活函数,引入Dropout技术。
  • VGG-16:牛津大学提出的 VGGNet 以92.7%的准确率赢得了 2013 年 ImageNet 竞赛冠军。该模型使用具有小感受野的堆叠的卷积层替代大感受野的卷积层。
  • GoogLeNet:谷歌提出的 GoogLeNet 以93.3%的准确率赢得了 2014 年 ImageNet 竞赛冠军。该网络有22层,其中使用了新提出的 Inception 模块。这个模块包含 Network-in-Network 层、池化操作、大尺寸卷积层和小尺寸卷积层。
  • ResNet:微软提出的 ResNet 以96.4%的准确率赢得了 2016 年 ImageNet 竞赛冠军,该网络以它的深度(152层)和残差块的提出而为众人所熟知。残差块通过引入恒等跳远连接,将前面层的输入复制至后续层,以解决深度架构的训练问题。

image

深度神经网络分析 图片来源


现有的语义分割方法有哪些?

一个通用的语义分割架构是编码器-解码器网络架构:

  • 编码器网络通常是一个预训练分类网络,例如 VGG/ResNet。
  • 解码器网络的任务是将编码器学到的可判别特征(低分辨率)在语义上将其投影到像素空间(更高分辨率),来得到一个密集分类。

不同于分类任务中深度网络的最终结果(即类存在的概率)是唯一重要的事,语义分割不仅需要在像素级有判别能力,还需要有能将编码器在不同阶段学到的可判别特征投影到像素空间的机制。不同的架构采用不同的机制作为解码机制的一部分。让我们探索三个主要的方法:

1-基于区域的语义分割(Region-based Semantic Segmentation)

基于区域的方法通常遵循“使用识别进行分割”流程,首先从一张图像中提取任意形状的区域并且标识出来,然后进行基于区域的分类。在测试时,基于区域的预测被转化成像素级的预测,通常根据包含像素的最高分数区域来标注像素。

扫描二维码关注公众号,回复: 1123346 查看本文章



R-CNN架构

R-CNN 是一个具有代表性的基于区域的方法,它基于目标检测结果执行语义分割。具体地,R-CNN 首先利用选择性搜索提取大量目标建议,然后对每个建议计算 CNN 特征,最后使用特定类线性支持向量机对每个区域进行分类。与传统用于图像分类的 CNN 结构相比,R-CNN 可以解决更复杂的问题,例如目标检测和图像分割,并且已经成为了这两个领域一个重要的基础网络。除此之外, R-CNN 可以建立在任意 CNN 基准架构之上,例如 AlexNet、VGG、GoogLeNet 和 ResNet。

对于图像分割任务,R-CNN 对每个区域提取两类特征:全区域特征和前景特征。将这两类特征合并在一起作为区域特征可以使网络获得更好的性能。由于使用了高度可判别 CNN 特征,R-CNN 获得了显著的性能提升。然而,R-CNN 在处理分割任务时还有一些缺点:

  • 特征不兼容分割任务。
  • 特征没有包含足够的空间信息来生成精确的边界。
  • 生成基于分割的建议需要花费大量时间并且会大大影响最终的性能。

由于上述瓶颈,一些研究着力于解决这些问题,包括 SDSHypercolumnsMask R-CNN

2-基于全卷积网络的语义分割

原始的 全卷积网络(FCN) 学习像素到像素的映射,没有提取区域建议。FCN 网络是传统 CNN 的扩展,主要思想是使传统 CNN 可以输入任意大小的图像。传统 CNN 只能接受特定大小的输入的原因在于全连接层是固定的。相反,FCN 仅使用卷积和池化层,使得网络可以对任意大小的输入进行预测。




FCN 架构

FCN 存在一个问题,输入经过几个卷积和池化后,输出特征图的分辨率下降,因此,FCN 的直接预测分辨率低,导致了相对模糊的物体边界。为了解决这个问题,一些更加高级的基于全卷积网络的方法被提出,包括 SegNetDeepLab-CRF空洞卷积

3-弱监督语义分割

语义分割中大多数方法都依赖于大量带有像素级标注的图像,然而,手工标注相当费时费力。因此,一些弱监督方法被提出。

例如,Boxsup 利用边界框标注作为监督信号来训练网络,迭代地提升预测掩码。Simple Does It 将弱监督限制作为一个输入标签噪声,并将递归训练作为去噪策略。Pixel-level Labeling 解释了多实例学习框架内的分割任务,通过添加一个额外的层来限制模型将更多的权重分配给图像级分类中重要的像素。




Boxsup 训练结果


使用全卷积网络实现语义分割

在这一部分,让我们一步一步地实现最基础的语义分割架构——全卷积网络(FCN)。我们使用 Python 3 实现,用到的库有 TensorFlow,numpy 和 Scipy。

我们使用用于道路检测的 Kitti Road数据集,程序来自 Udacity无人驾驶纳米学位项目,这里 可以了解更多关于程序的设置。




Kitti Road 数据集训练样本 来源

FCN 架构的关键特点:

  • FCN 使用 VGG16 进行迁移学习来实现语义分割。
  • VGG16 的全连接层转换为全卷积层,使用 1x1 卷积。该处理使得网络可以生成低分辨率的类存在热图。
  • 低分辨率语义特征图的上采样通过反卷积完成(通过双线性插值滤波器进行初始化)。
  • 在每一步,通过添加来自低层的粗糙的但是分辨率高的特征图来对上采样过程进行进一步的细化。
  • 卷积块后的跳远连接使得后续块能够从先前的已池化特征中提取更加抽象、突出类别的特征。

FCN 总共有3个版本(FCN-32,FCN-16,FCN-8),我们实现 FCN-8,详细步骤如下:

  • 编码器:使用预训练的 VGG16 作为编码器,解码器从 VGG16 的第七层开始。
  • FCN Layer-8:VGG16 的最后一个全连接层替换为 1x1 卷积。
  • FCN Layer-9:将 FCN Layer-8 上采样 2 倍大小以匹配 VGG16 的 Layer 4 的维度,反卷积参数如下:(kernel=(4, 4), stride=(2, 2), padding=’same’),然后将 Layer 4 和 FCN Layer-9 相加。
  • FCN Layer-10:将 FCN Layer-9 上采样 2 倍大小以匹配 VGG16 的 Layer 3 的维度,反卷积参数如下:(kernel=(4, 4), stride=(2, 2), padding=’same’),然后将 Layer 3 和 FCN Layer-10 相加。
  • FCN Layer-11:将 FCN Layer-10 上采样 4 倍大小以匹配 输入图像的尺寸,确保我们获得的图像深度为类的数量,反卷积参数如下:(kernel=(16, 16), stride=(8, 8), padding=’same’)



    FCN-8 架构 来源

第一步

首先载入预训练的 VGG16 模型,若电脑中没有该模型,TensorFlow 将会自动下载,也可以手动 下载。函数接受 TensorFlow session 和 VGG 模型的路径作为参数,返回 VGG 模型中的 tensor 的元组。包括 image_input,keep_prob(控制dropout),layer3,layer4 和 layer7。

def load_vgg(sess, vgg_path):

  # 加载模型和权重
  model = tf.saved_model.loader.load(sess, ['vgg16'], vgg_path)

  # 获取需要返回的 Tensor
  graph = tf.get_default_graph()
  image_input = graph.get_tensor_by_name('image_input:0')
  keep_prob = graph.get_tensor_by_name('keep_prob:0')
  layer3 = graph.get_tensor_by_name('layer3_out:0')
  layer4 = graph.get_tensor_by_name('layer4_out:0')
  layer7 = graph.get_tensor_by_name('layer7_out:0')

  return image_input, keep_prob, layer3, layer4, layer7
### 第二步 现在我们利用 VGG 模型中的 tensor 来创建 FCN 的网络。函数接受 VGG 层的输出和需要分类的数量作为参数,返回最后一层的输出 tensor。特别地,我们在编码器中使用 1x1 卷积,然后使用跳远连接和上采样将解码器层添加到网络。
def layers(vgg_layer3_out, vgg_layer4_out, vgg_layer7_out, num_classes):

    # 使用简化的变量名
    layer3, layer4, layer7 = vgg_layer3_out, vgg_layer4_out, vgg_layer7_out

    # 应用 1x1 卷积替代全连接
    fcn8 = tf.layers.conv2d(layer7, filters=num_classes, kernel_size=1, name="fcn8")

    # 对 fcn8 进行上采样以匹配 layer 4 的维度
    fcn9 = tf.layers.conv2d_transpose(fcn8, filters=layer4.get_shape().as_list()[-1],
    kernel_size=4, strides=(2, 2), padding='SAME', name="fcn9")

    # 在 fcn9 和 layer 4 之间建立跳远连接
    fcn9_skip_connected = tf.add(fcn9, layer4, name="fcn9_plus_vgg_layer4")

    # 再次执行上采样
    fcn10 = tf.layers.conv2d_transpose(fcn9_skip_connected, filters=layer3.get_shape().as_list()[-1],
    kernel_size=4, strides=(2, 2), padding='SAME', name="fcn10_conv2d")

    # 添加跳远连接
    fcn10_skip_connected = tf.add(fcn10, layer3, name="fcn10_plus_vgg_layer3")

    # 再次执行上采样
    fcn11 = tf.layers.conv2d_transpose(fcn10_skip_connected, filters=num_classes,
    kernel_size=16, strides=(8, 8), padding='SAME', name="fcn11")

    return fcn11
### 第三步 优化神经网络,即建立 TensorFlow 损失函数和优化操作。这里我们使用**交叉熵**作为损失函数,**Adam** 作为优化算法。
def optimize(nn_last_layer, correct_label, learning_rate, num_classes):

  # 将 4D tensors 转换为 2D, 每一行代表一个像素, 每一列代表一类
  logits = tf.reshape(nn_last_layer, (-1, num_classes), name="fcn_logits")
  correct_label_reshaped = tf.reshape(correct_label, (-1, num_classes))

  # 使用交叉熵计算预测与真实标签之间的差异
  cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=correct_label_reshaped[:])
  # 计算均值作为损失
  loss_op = tf.reduce_mean(cross_entropy, name="fcn_loss")

  # 模型应用这个操作来寻找权重/参数,以获得正确像素标签
  train_op = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss_op, name="fcn_train_op")

  return logits, train_op, loss_op
### 第四步 这里我们定义 **train_nn** 函数,对于训练过程,keep_probability 设为 0.5,learning_rate 设为 0.001。我们也打印出损失以记录进度。
def train_nn(sess, epochs, batch_size, get_batches_fn, train_op,
             cross_entropy_loss, input_image,
             correct_label, keep_prob, learning_rate):

  keep_prob_value = 0.5
  learning_rate_value = 0.001
  for epoch in range(epochs):
      # 创建函数获取 batches
      total_loss = 0
      for X_batch, gt_batch in get_batches_fn(batch_size):

          loss, _ = sess.run([cross_entropy_loss, train_op],
          feed_dict={input_image: X_batch, correct_label: gt_batch,
          keep_prob: keep_prob_value, learning_rate:learning_rate_value})

          total_loss += loss;

      print("EPOCH {} ...".format(epoch + 1))
      print("Loss = {:.3f}".format(total_loss))
      print()
### 第五步 最后,定义 **run** 函数训练网络。首先利用 **load_vgg**,**layers** 和 **optimize** 函数创建网络,然后使用 **train_nn** 函数训练网络,并且保存推断的数据。
def run():

  # 下载预训练 vgg 模型
  helper.maybe_download_pretrained_vgg(data_dir)

  # 获取 batch 函数
  get_batches_fn = helper.gen_batch_function(training_dir, image_shape)

  with tf.Session() as session:

    # 从 vgg 架构中返回输入、三个层和keep probability
    image_input, keep_prob, layer3, layer4, layer7 = load_vgg(session, vgg_path)

    # 完整网络架构
    model_output = layers(layer3, layer4, layer7, num_classes)

    # 返回输出预测,训练操作和损失操作
    # - logits: 每一行代表一个像素, 每一列代表一类
    # - train_op: 获取正确参数,使得模型能够正确地标记像素
    # - cross_entropy_loss: 输出需要最小化的损失,更低的损失可以获得更高的准确率
    logits, train_op, cross_entropy_loss = optimize(model_output, correct_label, learning_rate, num_classes)

    # 初始化所有变量
    session.run(tf.global_variables_initializer())
    session.run(tf.local_variables_initializer())

    print("Model build successful, starting training")

    # 训练神经网络
    train_nn(session, EPOCHS, BATCH_SIZE, get_batches_fn, 
             train_op, cross_entropy_loss, image_input,
             correct_label, keep_prob, learning_rate)

    # 在测试图像上运行模型,保存每个输出图像(道路为绿色)
    helper.save_inference_samples(runs_dir, data_dir, session, image_shape, logits, keep_prob, image_input)

    print("All done!")
**参数设置**:epochs = 40, batch_size = 16, num_classes = 2, image_shape = (160, 576)。 dropout使用了 0.5 和 0.75, 我们发现后者能获得更好的结果。

训练结果

完整代码:GitHub

猜你喜欢

转载自blog.csdn.net/qq_20084101/article/details/80501502