U-Net应用:从空中图像绘制建筑物足迹

2024年11月20日 由 alex 发表 88 0

什么是U-Net?

U-Net是一种为图像分割任务设计的卷积神经网络(CNN)。它最初是为生物医学图像分割而开发的,其架构由编码器和解码器组成,这使得它呈现出有趣的U形外观。

  • 编码器(收缩路径):编码器由一系列卷积层(用于发现边缘和模式等特征)和最大池化层(用于减小图像尺寸,帮助模型更加聚焦于最重要的特征)组成。
  • 解码器(扩展路径):解码器由上采样层(用于恢复原始图像尺寸)和更多的卷积层组成。解码器基本上会对输出进行精炼和重建,从而生成一个漂亮的分割图。
  • 跳跃连接:U-Net使用跳跃连接,将编码器和解码器中对应的层连接起来,这有助于保留精细细节并提高分割准确性。你可以把跳跃连接想象成直达列车,它直接从U-Net的一侧飞驰到另一侧,避开所有“交通拥堵”,确保重要的“乘客”(细节)能够安全到达另一端。


8


数据准备

对于这个项目,我使用了以下数据集:

  • 图像:3,347张大小为256×256×3的彩色光栅图像,每张图像代表马萨诸塞州的一个300平方米的区域。
  • 标签:根据OpenStreetMap的建筑轮廓生成的二进制掩码,指示哪些像素对应建筑物。


数据集被划分为70%的训练集、15%的验证集和15%的测试集。在将图像输入模型之前,我将像素值归一化到[0, 1]的范围内。


以下是输入-输出对的快速可视化:

  • 左图:航拍图像。
  • 右图:二进制掩码,其中白色区域表示建筑轮廓。


9


类别不平衡

数据集中存在相当显著的类别不平衡问题,即非建筑像素的数量远远超过建筑像素——这是分割任务中非常常见的一个挑战。


10


处理类别不平衡

有很多方法可以解决类别不平衡问题,比如过采样、欠采样和数据增强。我尝试了一些这些方法,但对我的模型帮助最大的是创建了一个特殊的损失函数,它结合了二元交叉熵(BCE)和Dice损失。


这是如何解决类别不平衡问题的?

基本上,损失函数通过衡量模型预测与真实值之间的差距来工作——而任何模型的目标都是最小化这种损失。


考虑到我们只关注二进制分割(区分建筑和背景),一个强有力的损失函数选择是二元交叉熵,因为它衡量的是像素级的损失,即它会查看每一个预测像素与真实像素之间的差距。但仅使用这种类型的损失存在一个问题,那就是损失将由多数类(背景像素)主导。


这就是Dice损失发挥作用的地方——Dice损失通过查看预测掩码和真实掩码之间的重叠来衡量损失。


为了理解Dice损失,首先让我们了解如何判断预测结果:

  1. 真正例(TP):?✅ 正确识别为建筑的建筑。
  2. 假正例(FP):?❌ 错误标记为建筑的背景像素。
  3. 假负例(FN):?❌ 被误识别为背景的建筑。
  4. 真负例(TN):?✅ 正确标记为背景的背景。


Dice系数(本质上是图像分割的F1分数)平衡了精确率(模型在标记建筑像素时的准确性)和召回率(模型识别出的实际建筑像素数量)。


屏幕截图2024-11-20105109


基本上,这就像是在说——“让我们专注于建筑,看看我们在不犯太多错误的情况下能正确识别出多少建筑?”


Dice系数(F1分数)是衡量模型性能的一个指标(越高越好),而Dice损失则是在训练过程中用来最小化误差的(越低越好)。


1111


结合BCE和Dice损失

通过结合这两种损失函数,我们既能受益于像素级的监督,又能关注Dice损失中的重叠部分,从而确保对两个类别都有敏感的平衡。


组装模型 

在深入探讨模型架构之前,我想快速介绍两种显著提高了我的模型性能的关键方法,这些方法在训练过程中往往被忽视。


空间丢弃

为了防止模型过拟合(即模型在训练数据上表现得太好,以至于无法在新数据上进行良好预测),我在模型中加入了空间丢弃层。在训练过程中,它会随机隐藏每张图像中的一部分像素。这基本上迫使模型不再依赖单个像素,而是关注更大的图像,学习建筑周围的上下文和模式。


这是一个非常酷的方法,丢弃不仅限于图像模型中的空间层,你还可以在其他类型的神经网络中使用它!


核初始化器

我认为在构建神经网络时,核初始化器是最容易被忽视的组件之一。这在我攻读硕士课程时可能是最难理解的概念之一,但一旦我真正理解了它,我就明白了它在神经网络中的关键性。


那么,它到底是什么呢?简单来说,它决定了网络层在训练开始之前的权重设置。你可以把它想象成为房子打地基。如果地基薄弱或不平整,那么无论房子设计得多么出色,结构都不会稳定。


对于我的U-Net模型来说,最好的初始化器是LeCun Normal。它会根据输入层的大小成比例地缩放权重,这有助于降低梯度在网络中流动时变得太小(梯度消失)或太大(梯度爆炸)的风险。


Keras函数式API

好了,现在我们来深入构建U-Net模型。我使用Keras API定义了一个简化的U-Net模型。


  • Keras函数式API:这是Python中构建深度学习模型的一种非常灵活的方式,它允许你通过像搭积木一样连接不同的层来定义自定义架构。


编码器块

记得之前提到的,这是U-Net的收缩路径——它接收输入图像,捕捉特征并缩小空间信息(减小图像尺寸)。


每个块由以下部分组成:

  • 两个卷积层:每个卷积层都使用ReLU激活函数和lecun_normal核初始化。
  • 空间丢弃:在每个卷积层之后添加正则化,丢弃率为10%。
  • 最大池化:将空间维度下采样2倍,减小图像尺寸,但确保保留关键特征。


def encoder_block(filters, inputs, dropout_rate=0.1, kernel_initializer='lecun_normal'):
    x = Conv2D(filters, kernel_size=(3, 3), padding='same', strides=1, activation='relu',
               kernel_initializer=kernel_initializer)(inputs)
    x = SpatialDropout2D(dropout_rate)(x) 
    s = Conv2D(filters, kernel_size=(3, 3), padding='same', strides=1, activation='relu',
               kernel_initializer=kernel_initializer)(x)
    s = SpatialDropout2D(dropout_rate)(s) 
    p = MaxPooling2D(pool_size=(2, 2), padding='same')(s)
    return s, p


在我的模型中,我使用了四个块(当我尝试使用更多块或更多过滤器时,模型的表现反而变差了):

  • 块1:32个过滤器
  • 块2:64个过滤器
  • 块3:128个过滤器
  • 块4:256个过滤器


瓶颈层

这部分模型充当U-Net的瓶颈,位于编码器和解码器之间。它能够在最小的空间维度上捕捉最深的特征(非常酷)。它由以下部分构建:

  • 两个卷积层
  • 空间丢弃


这个基础为解码器从这些抽象特征中重建高分辨率图像奠定了基础。


def baseline_layer(filters, inputs, dropout_rate=0.1, kernel_initializer='lecun_normal'):
    x = Conv2D(filters, kernel_size=(3, 3), padding='same', strides=1, activation='relu',
               kernel_initializer=kernel_initializer)(inputs)
    x = SpatialDropout2D(dropout_rate)(x)
    x = Conv2D(filters, kernel_size=(3, 3), padding='same', strides=1, activation='relu',
               kernel_initializer=kernel_initializer)(x)
    x = SpatialDropout2D(dropout_rate)(x) 
    return x


解码器块

如果你还记得之前的内容,解码器是U-Net的扩展路径,它通过上采样和组合特征来重建图像。每个解码器块包括:

  • 上采样:使用带有ReLU激活函数的转置卷积层,将空间维度翻倍。
  • 跳跃连接(Skip Connections):这些连接将来自相应编码器块的特征组合起来,确保网络保留在下采样过程中丢失的所有细粒度细节。

记住,这些就像是从模型的一端到另一端的子弹列车。

  • 两个卷积层:用于细化上采样后的特征。
  • 空间丢弃:帮助模型学习泛化(即使是在模型的这个阶段)。


def decoder_block(filters, connections, inputs, dropout_rate=0.1, kernel_initializer='lecun_normal'):
    x = Conv2DTranspose(filters, kernel_size=(2, 2), padding='same', activation='relu', strides=2,
                        kernel_initializer=kernel_initializer)(inputs)
    skip_connections = concatenate([x, connections], axis=-1)
    x = Conv2D(filters, kernel_size=(3, 3), padding='same', activation='relu',
               kernel_initializer=kernel_initializer)(skip_connections)
    x = SpatialDropout2D(dropout_rate)(x)
    x = Conv2D(filters, kernel_size=(3, 3), padding='same', activation='relu',
               kernel_initializer=kernel_initializer)(x)
    x = SpatialDropout2D(dropout_rate)(x)  
    return x


最终输出层

最后但同样重要的是,模型的最后一层是一个1x1的卷积层,它具有一个输出通道和一个Sigmoid激活函数。这一层用于预测每个像素属于目标类别(建筑物)或背景的概率。


  outputs = Conv2D(1, 1, activation = 'sigmoid')(d4)1, 1, activation = 'sigmoid')(d4)


将所有部分整合在一起 


def unet():
  inputs = Input(shape = (256, 256, 3)) #defines the input layer and shape of images
  #encoder
  s1, p1 = encoder_block(32, inputs = inputs)
  s2, p2 = encoder_block(64, inputs = p1)
  s3, p3 = encoder_block(128, inputs = p2)
  s4, p4 = encoder_block(256, inputs = p3)
  #bottleneck
  baseline = baseline_layer(512, p4)
  #decoder
  d1 = decoder_block(256, s4, baseline)
  d2 = decoder_block(128, s3, d1)
  d3 = decoder_block(64, s2, d2)
  d4 = decoder_block(32, s1, d3)
  #output function for binary classification of pixels
  outputs = Conv2D(1, 1, activation = 'sigmoid')(d4)
  #finalizing the model
  model = Model(inputs = inputs, outputs = outputs, name = 'Unet')
  return model


训练模型 

以下是我训练模型的方法详解:

  • 学习率:我使用了TensorFlow的ExponentialDecay来逐渐降低学习率(初始值为0.001)。
  • 优化器:我选择了Adam优化器,它能利用过去的梯度动态调整每个参数的学习率。
  • 批量大小:8(为防止内存问题而降低)。
  • 训练轮数:50(如果在验证损失上没有改进,则提前停止,最多等待3轮)。


训练结束后,我通过绘制训练准确率与验证准确率、训练损失与验证损失的对比图来检查模型的性能。


11


训练准确率和验证准确率均呈现出上升趋势,这意味着模型能够从训练数据中学习并在验证数据上表现良好。


训练损失和验证损失均稳步下降,这意味着模型能够有效地最小化损失。


评估模型 

虽然准确率显示了整体正确预测的比例,但它并没有考虑到数据集中的类别不平衡问题。例如,如果背景像素数量远大于建筑物像素,模型只需大部分时间预测“背景”就可以获得高准确率。因此,高准确率并不总是意味着模型在识别建筑物方面表现良好。


那么,我们如何真正评估模型分割建筑物的能力呢?


这时,Dice指标再次派上了用场!与准确率不同,Dice指标衡量的是预测掩模与真实掩模的重叠程度。Dice得分越高,表示性能越好,意味着模型更准确地捕捉到了预测建筑物区域与实际建筑物区域之间的交集。


以下是我用来计算Dice指标的代码:


def dice_metric(y_true, y_pred):
    """Calculate Dice Coefficient for ground truth and predicted masks."""
    y_pred = tf.cast(y_pred > 0.5, tf.float32) #threshold set to 0.5
    intersection = tf.reduce_sum(y_true * y_pred)
    total_sum = tf.reduce_sum(y_true) + tf.reduce_sum(y_pred)
    dice = tf.math.divide_no_nan(2 * intersection, total_sum)
    return dice


测试集结果:

在测试集上评估模型后,我获得了以下指标:

  • 准确率:94.7%(预测的整体正确性)
  • 精确率:75.2%(预测为建筑物的实际为建筑物的比例)
  • 召回率:78.9%(模型正确识别出的实际建筑物的比例)
  • Dice指标:76.7%(衡量预测掩模与真实掩模之间的重叠程度)


通过预测概率可视化结果 

为了更好地理解模型的表现,我以多种方式可视化了预测结果,并将它们与真实标签进行了比较。这一步有助于观察模型在哪些地方表现出色,在哪些地方存在不足,是评估像建筑物识别这样的分割任务的关键部分。


12


观察预测标签(右上角)并将其与测试标签(第二张图像)进行比较,我们可以看到,在大多数情况下,模型成功地识别了建筑物,尽管并不完美。


误报图像突出了模型错误地将背景区域分类为建筑物的地方,而漏报图像则揭示了模型错误地将建筑物标记为背景的地方。这些错误表明仍有改进的空间。然而,总体而言,模型在分割建筑物轮廓方面做得相当不错,并显示出在实际应用中的巨大潜力!


文章来源:https://medium.com/ai-advances/how-i-used-a-u-net-to-map-building-footprints-from-the-sky-bf6d184c41d8
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
写评论取消
回复取消