NSDT工具推荐Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - Three.js虚拟轴心开发包

在这个教程中,我们将学习如何利用UNET深度学习网络实现地震图像的语义分割,除了UNET,本文还介绍了图像处理的几种常见任务,以及卷积网络常用的操作和术语,例如卷积、最大池、接受域、上采样、转置卷积、跳过连接等。

1. 介绍

计算机视觉是一个跨学科的科学领域,涉及如何使计算机从数字图像或视频中获得高级别的理解。从工程学的角度看,它寻求实现人类视觉系统能够完成的任务自动化。 (维基百科)

在过去几年中,深度学习使计算机视觉领域取得了飞速的发展。在这篇文章中,我想讨论一个具体的任务,在计算机视觉称为语义分割。尽管研究人员已经想出了许多方法来解决这个问题,但我要谈谈一个特殊的架构,即UNET,它使用完全卷积网络模型来完成这项任务。

此外,我写博客的目的是也提供一些直观的见解

2. 先决条件

我假设读者已经熟悉机器学习和卷积网络的基本概念。此外,你需要对ConvNets与Python和Keras库有一些经验。

3. 什么是语义分割?

计算机可以获得对图像的理解,这有不同程度的粒度。对于每个级别,计算机视觉域中都定义了一个问题。从粗粒到更精细的粒度理解,让我们在下面描述这些问题:

a.图像分类

计算机视觉中最根本的模块是图像分类问题,如果给出图像,我们期望计算机输出一个离散的标签,这是图像中的主要对象。在图像分类中,我们假设图像中只有一个(而不是多个)对象。

b.分类并定位

在这种任务中,除了对图像进行分类,我们还期望计算对象的确切位置。定位通常使用边界框表示,该框可以通过图像边界的某些数字参数进行识别。即使在这种情况下,假设每个图像也只有一个对象。

c. 对象检测

对象检测将定位扩展到下一个级别,即现在图像不限于只有一个对象,但可以包含多个对象。对象检测任务是对图像中的所有对象进行分类和定位。这里再次使用边界框的概念完成定位。

d. 语义分割

语义图像分割的目标是将图像的每个像素标记为与所表示的相应类别。因为我们预测图像中的每一个像素,此任务通常称为密集预测

请注意,与以前的任务不同,语义细分的预期输出不仅仅是标签和边界框参数。输出本身是一个高分辨率图像(通常与输入图像大小相同),其中每个像素都归类为特定类。因此,它是一个像素级别的图像分类。

e.实例分割

实例分割比语义分割更进一步,其中与像素级别分类一起,我们期望计算机将每个实例单独分类。例如,在上图中有 3 人,从技术上来说这是"人"的 3 个实例。所有 3 个都单独标记(颜色不同)。语义细分则不会区分特定类的实例。

如果你仍然对对象检测、语义分割和实例分割之间的差异感到困惑,下图将有助于澄清问题:

在这篇文章中,我们将学习使用称为UNET的全卷积网络(FCN)解决语义分割问题。

4、语义分割的应用

可能你想知道语义分割是否真正有效,这种质疑是合理的。不过事实证明,视觉中的许多复杂任务需要对图像进行精细的细粒度理解。例如:

a.自动驾驶车辆

自动驾驶是一项复杂的机器人任务,需要在不断发展的环境中进行感知、规划和执行。这项任务还需要以最大的精度执行,因为安全至关重要。语义分割提供有关道路上自由空间的信息,以及检测车道标记和交通标志的信息。

b. 生物医学图像诊断

机器可以增强放射科医生的分析,大大缩短运行诊断测试所需的时间。

c. 地理感应

语义分割问题也可以被视为分类问题,其中每个像素被归类为一个从一系列对象类。因此,卫星图像的土地使用测绘存在使用案例。土地覆盖信息对于各种应用非常重要,例如森林砍伐和城市化的监测领域。

为了识别卫星图像上每个像素的土地覆盖类型(如城市、农业、水域等),土地覆盖分类可视为多类语义分割任务。道路和建筑物检测也是交通管理、城市规划和道路监控的重要研究课题。

可用的大型数据集(例如:SpaceNet)很少,数据标签始终是分割任务的瓶颈。

d. 精密农业

精密的农业机器人可以减少需要喷洒在田间的除草剂,对农作物的语义分割可以实时触发除草行动。这种先进的农业图像视觉技术可以减少对农业的人工监测。

我们还将考虑一个实用的现实世界案例研究,以了解语义分割的重要性。问题陈述和数据集在以下部分中进行描述。

5. 业务问题

在任何机器学习任务中,总是建议花相当长的时间恰当地理解我们旨在解决的业务问题。这不仅有助于有效地应用技术工具,而且还激励开发人员在解决现实世界中使用其技能。

TGS是领先的地球科学和数据公司之一,该公司使用地震图像和 3D 渲染来了解地球表面下哪些区域含有大量的石油和天然气。

有趣的是,含有石油和天然气的表面也含有大量的盐。因此,在地震技术的帮助下,他们试图预测地球表面哪些区域含有大量的

不幸的是,专业的地震成像需要专家的人类视觉来精确识别盐体。这会导致高度主观和可变的渲染。此外,如果人类预测不正确,可能会给石油和天然气公司的钻探人员造成巨大损失。

因此,TGS 主办了Kaggle竞赛,采用机器视觉以更高的效率和准确性完成此任务。

要了解有关挑战的更多信息,请单击此处

要了解更多有关地震技术的信息,请单击此处

6、了解数据

这里下载数据文件。

为了简单起见,我们只会使用train.zip文件,其中包含图像及其相应的掩码。

在图像目录中,有4000张地震图像被人类专家用来预测该地区是否可能有盐矿床。

在掩码目录中,有4000个灰度图像,这些图像是相应图像的实际地面真实值,表示地震图像是否含有盐矿床。这些数据将被用于建立一个有监督学习网络。

让我们可视化给定数据以更好地了解:

左边的图像是地震图像。绘制黑色边界只是为了理解哪个部分含有盐, 哪些部分不含有盐。(当然,此边界不是原始图像的一部分)

右边的图像被称为掩码(mask),这是实际结果标签。掩码是我们的模型必须预测的给定地震图像。白色区域表示盐矿床,黑色区域表示无盐。

让我们再看几幅图片:

请注意,如果掩码是全黑的,这意味着给定的地震图像中没有盐矿床。

显然,从上述几幅图像可以推断,人类专家很难对地震图像进行准确的掩码预测。

7、卷积、最大池化和转置卷积

在我们深入到 UNET 模型之前,了解卷积网络中通常使用的不同操作非常重要。请记下所使用的术语。

i. 卷积操作

卷积操作有两个输入

i) 3维体(输入图像)(Nin x Nin x 通道数)

ii) 一组"k"过滤器(也称为核或特征提取器),每个大小(f x f x 通道数),其中 f 通常是 3 或 5。

卷积操作的输出也是大小(Nout x Nout x k)的 3维体(也称为输出图像或功能映射)。

Nin和Nout之间的关系如下:

卷积操作可视化如下:

在上面的 GIF动画 中,我们的输入为 7x7x3 大小。两个过滤器每个大小3x3x3。填充=0,步幅=2。因此输出为 3x3x2。如果你对此计算感到不自在,那么建议你重温卷积网络的概念,然后再继续。

经常使用的一个重要术语称为接受域(Receptive Field。这只不过是特定功能提取器(过滤器)正在查看的输入中的区域。在上述 GIF 中,过滤器在任何给定实例中覆盖的输入量中的 3x3 蓝色区域是接受域。这有时也被称为上下文

简单地说,接受域(上下文)是过滤器在任何给定时间点覆盖的输入图像的区域

ii) 最大池操作

简单地说,池的功能是缩小特征映射的大小,以便我们在网络中的参数更少。

例如:


基本上,从输入特征图的每 2x2 块中,我们选择最大像素值,从而获得池化特征图。请注意,滤波器和步幅的大小是最大池化操作中的两个重要的超参数。

其理念是只保留每个区域的重要功能(最大值像素),并扔掉不重要的信息。重要的意思是这些像素最能描述图像上下文的信息。

这里需要注意的非常重要的一点是,卷积操作特别是池化操作都会缩小图像。这称为下采样(down sampling。在上述示例中,池化前图像的大小为 4x4,池化后为 2x2。事实上,下采样基本意味着将高分辨率图像转换为低分辨率图像。

因此,在池化之前,4x4 图像中的信息在汇集后(几乎)在 2x2 图像中存在相同的信息。

现在,当我们再次应用卷积操作时,下一层中的滤波器将能够看到更大的上下文,即当我们深入到网络时,图像的大小会减少,但接受域会增加。

例如,以下是 LeNet 5 架构:

请注意,在典型的卷积网络中,图像的高度和宽度会逐渐降低(由于池化和下采样),这有助于更深层的滤波器聚焦于更大的接受域(上下文)。但是,通道/深度(使用的过滤器数量)逐渐增加,这有助于从图像中提取更复杂的特征。

直觉上,我们可以对池化操作做出以下结论。通过下取样,模型更好地了解图像中存在的"内容",但它丢失了"所在位置"的信息。

iii) 需要向上取样

如前所述,语义分割的输出不仅仅是一个类标签或某些边界框参数。事实上,输出是一个完整的高分辨率图像,其中所有像素都分类。

因此,如果我们使用具有池化层和密集层的常规卷积网络,我们将丢失"WHERE"信息,只保留"WHAT"信息,而这不是我们想要的。在分割的情况下,我们需要"WHAT"以及"WHERE"的信息。

因此,需要对图像进行上采样(up sampling),即将低分辨率图像转换为高分辨率图像以恢复"WHERE"信息。

在文献中有许多技术可以对图像进行上采样。其中一些是双线性插值、立方插值、最近邻插值、去池化、转置卷积等。然而,在大多数最先进的网络中,转置卷积是对图像进行上采样的首选。

iv) 转置卷积

转置卷积(有时也称为去卷积)是一种使用可学习参数对图像进行上采样的技术。

我不会描述转置卷积是如何工作的,因为Naoki Shibuya已经在他的博客上做了出色的介绍:用转置卷积进行上采样。我强烈建议你浏览此博客以了解转置卷积的过程。

然而,从更高层面看,转置卷积与正常卷积过程完全相反,即输入量为低分辨率图像,输出为高分辨率图像。

在Nakoki的博客中很好地解释了如何将正常卷积表示为输入图像和滤波器的矩阵乘法以生成输出图像。只需转置滤波器矩阵,我们就可以反转卷积过程,因此这个过程被称为转置卷积。

v) 本节摘要

阅读本节后,你需要熟悉以下概念:

  • 接受域或上下文
  • 卷积和池化操作下取样图像,即将高分辨率图像转换为低分辨率图像
  • 最大池化操作通过增加接受域,有助于理解图像中的"WHAT"。然而,它往往会丢失对象所在的"位置"的信息。
  • 在语义分割中,不仅要知道图像中存在"什么",还要知道"WHERE"它的存在。因此,我们需要一种方法来将图像从低分辨率到高分辨率进行上采样,以帮助我们恢复"WHERE"信息。
  • 转置卷积是执行向上采样的最首选,它基本上通过反向传播来学习参数,将低分辨率图像转换为高分辨率图像。

如果对本节中解释的任何术语或概念感到困惑,请随时再次阅读,直到感到舒适。

8. UNET架构和训练

UNET由Olaf Ronneberger等人开发,用于生物医学图像分割。该架构包含两条路径。第一个路径是用于捕获图像中上下文的收缩路径(也称为编码器)。编码器只是一个传统的卷积和最大池层堆栈。第二个路径是对称扩展路径(也称为解码器),用于使用转置卷积实现精确定位。因此,它是一个端到端的完全卷积网络(FCN),即它只包含卷积层,不包含任何密集层,因为它可以接受任何大小的图像。

在原始论文中,对UNET的描述如下:

如果你不明白,没关系。我会尝试更直观地描述这个架构。请注意,在原始纸张中,输入图像的大小为 572x572x3,但是,我们将使用大小为 128x128x3 的输入图像。因此,不同位置的大小将不同于原始论文,但核心组件保持不变。

以下是对UNET架构的详细解释:

要注意的要点:

  • 2@Conv层意味着连续应用两个卷积层
  • c1, c2, ...c9是卷积层的输出张量
  • p1、p2、p3和p4是最大池层的输出张量
  • u6、u7、u8和u9是上采样(转置卷积)层的输出张量
  • 左侧是收缩路径(编码器),我们应用常规卷积和最大汇集层。
  • 在编码器中,图像的大小逐渐缩小,而深度逐渐增加。从128x128x3到8x8x256
  • 这意味着网络学习图像中的"WHAT"信息,但它已经丢失了"WHERE"信息
  • 右测是扩展路径(解码器),我们应用转置卷积和常规卷积
  • 在解码器中,图像的大小逐渐增加,深度逐渐减少。从8x8x256到128x128x1
  • 直观地,解码器通过逐步应用向上采样来恢复"WHERE"信息(精确定位)
  • 为了获得更精确的位置信息, 在解码器的每一步,我们使用跳过连接,将转置卷积层的输出与来自同一级别的编码器的功能映射连接在一起:
u6 = u6 + c4
u7 = u7 + c3
u8 = u8 + c2
u9 = u9 + c1
  • 每次连接后,我们再次应用两个连续的常规卷积,以便模型可以学会组装更精确的输出
  • 这形成U 形对称架构,因此命名为 UNET
  • 从更高层面看,我们有以下关系:
    输入 (128x128x1) =>编码器 => (8x8x256) = >解码器 =>Ouput (128x128x1)

以下是定义上述模型的 Keras 代码:

def conv2d_block(input_tensor, n_filters, kernel_size = 3, batchnorm = True):
    """Function to add 2 convolutional layers with the parameters passed to it"""
    # first layer
    x = Conv2D(filters = n_filters, kernel_size = (kernel_size, kernel_size),\
              kernel_initializer = 'he_normal', padding = 'same')(input_tensor)
    if batchnorm:
        x = BatchNormalization()(x)
    x = Activation('relu')(x)
    
    # second layer
    x = Conv2D(filters = n_filters, kernel_size = (kernel_size, kernel_size),\
              kernel_initializer = 'he_normal', padding = 'same')(input_tensor)
    if batchnorm:
        x = BatchNormalization()(x)
    x = Activation('relu')(x)
    
    return x
  
  def get_unet(input_img, n_filters = 16, dropout = 0.1, batchnorm = True):
    # Contracting Path
    c1 = conv2d_block(input_img, n_filters * 1, kernel_size = 3, batchnorm = batchnorm)
    p1 = MaxPooling2D((2, 2))(c1)
    p1 = Dropout(dropout)(p1)
    
    c2 = conv2d_block(p1, n_filters * 2, kernel_size = 3, batchnorm = batchnorm)
    p2 = MaxPooling2D((2, 2))(c2)
    p2 = Dropout(dropout)(p2)
    
    c3 = conv2d_block(p2, n_filters * 4, kernel_size = 3, batchnorm = batchnorm)
    p3 = MaxPooling2D((2, 2))(c3)
    p3 = Dropout(dropout)(p3)
    
    c4 = conv2d_block(p3, n_filters * 8, kernel_size = 3, batchnorm = batchnorm)
    p4 = MaxPooling2D((2, 2))(c4)
    p4 = Dropout(dropout)(p4)
    
    c5 = conv2d_block(p4, n_filters = n_filters * 16, kernel_size = 3, batchnorm = batchnorm)
    
    # Expansive Path
    u6 = Conv2DTranspose(n_filters * 8, (3, 3), strides = (2, 2), padding = 'same')(c5)
    u6 = concatenate([u6, c4])
    u6 = Dropout(dropout)(u6)
    c6 = conv2d_block(u6, n_filters * 8, kernel_size = 3, batchnorm = batchnorm)
    
    u7 = Conv2DTranspose(n_filters * 4, (3, 3), strides = (2, 2), padding = 'same')(c6)
    u7 = concatenate([u7, c3])
    u7 = Dropout(dropout)(u7)
    c7 = conv2d_block(u7, n_filters * 4, kernel_size = 3, batchnorm = batchnorm)
    
    u8 = Conv2DTranspose(n_filters * 2, (3, 3), strides = (2, 2), padding = 'same')(c7)
    u8 = concatenate([u8, c2])
    u8 = Dropout(dropout)(u8)
    c8 = conv2d_block(u8, n_filters * 2, kernel_size = 3, batchnorm = batchnorm)
    
    u9 = Conv2DTranspose(n_filters * 1, (3, 3), strides = (2, 2), padding = 'same')(c8)
    u9 = concatenate([u9, c1])
    u9 = Dropout(dropout)(u9)
    c9 = conv2d_block(u9, n_filters * 1, kernel_size = 3, batchnorm = batchnorm)
    
    outputs = Conv2D(1, (1, 1), activation='sigmoid')(c9)
    model = Model(inputs=[input_img], outputs=[outputs])
    return model

训练

模型由 Adam 优化器编译,我们使用二进制交叉熵损失函数,因为只有两个类(盐和无盐)。

我们使用 Keras 回调实现:

  • 如果验证损失在 5 个持续周期没有改善,学习率会下降。
  • 如果验证损失在 10 个持续周期没有改善, 则提前停止。
  • 只有在验证损失有改善的情况下,才能保存全重。

我们使用大小为32的批次尺寸。

请注意,调整这些超参数并进一步提高模型性能可能有很大的余地。

该模型在 P4000 GPU 上进行训练,用时不到 20 分钟。

9. 推理

请注意,对于每个像素,我们得到 0 到 1 之间的值。0表示无盐,1表示盐。我们以 0.5 作为阈值,以决定是否将像素分类为 0 或 1。

然而,决定阈值是棘手的事情,可以被视为另一个超参数。

让我们来看看训练集和验证集的一些结果:

训练集的结果

验证集的结果

训练集的结果相对优于验证集的结果,这意味着模型存在过度拟合。一个明显的原因可能是用于训练模型图像数量太少。


原文链接:Understanding Semantic Segmentation with UNET

BimAnt翻译整理,转载请标明出处