Unet网络剖析

[复制链接]
查看992 | 回复0 | 2023-8-23 11:38:37 | 显示全部楼层 |阅读模式
1 Unet网络概述

论文名称:U-Net: Convolutional Networks for Biomedical Image Segmentation
发表集会及时间 :MICCA ( 国际医学图像盘算和 盘算机辅 助干预会 议 ) 2 0 1 5
Unet提出的初志是为了办理医学图像分割的题目。
Unet网络非常的简朴,前半部分就是特征提取,后半部分是上采样。在一些文献中把这种布局叫做编码器-解码器布局,由于网络的团体布局是一个大些的英文字母U,所以叫做U-net。其实可以将图像->高语义feature map的过程当作编码器,高语义->像素级别的分类score map的过程看作解码器


  • Encoder:左半部分,由两个3x3的卷积层(RELU)再加上一个2x2的maxpooling层构成一个下采样的模块;
  • Decoder:右半部分,由一个上采样的卷积层(反卷积层),特征拼接concat,两个3x3的卷积层,非线性ReLU层;
在当时,Unet相比更早提出的FCN网络,利用拼接来作为特征图的融合方式。


  • FCN是通过特征图对应像素值的相加来融合特征的;
  • U-net通过通道数的拼接,这样可以形成更厚的特征,固然这样会更佳斲丧显存;
2 Unet与FCN网络的区别

U-Net和FCN非常的相似,U-Net比FCN稍晚提出来,但都发表在2015年,和FCN相比,U-Net的第一个特点是完全对称,也就是左边和右边是很类似的,而FCN的decoder相对简朴。第二个区别就是skip connection,FCN用的是加操纵(summation),U-Net用的是叠操纵(concatenation)。这些都是细节,重点是它们的布局用了一个比力经典的思路,也就是编码息争码(encoder-decoder)布局。其实可以将图像->高语义feature map的过程当作编码器,高语义->像素级别的分类score map的过程看作解码器
此外, 由于UNet也和FCN一样, 是全卷积情势, 没有全毗连层(即没有固定图的尺寸),所以容易适应许多输入尺寸巨细,但并不是全部的尺寸都可以,须要根据网络布局决定
3 为什么Unet在医疗图像分割种表现好



  • 医疗影像语义较为简朴、布局固定。因此语义信息相比自动驾驶等较为单一,因此并不须要去筛选过滤无用的信息。医疗影像的全部特征都很告急,因此低级特征和高级语义特征都很告急,所以U型布局的skip connection布局(特征拼接)更好派上用场
  • 医学影像的数据较少,获取难度大,数据量大概只有几百甚至不到100,因此假如利用大型的网络比方DeepLabv3+等模子,很容易过拟合。大型网络的长处是更强的图像表述本领,而较为简朴、数量少的医学影像并没有那么多的内容须要表述,因此也有人发现在小数量级中,分割的SOTA模子与轻量的Unet并没有上风
  • 医学影像往往是多模态的。比方说ISLES脑梗比赛中,官方提供了CBF,MTT,CBV等多中模态的数据(这一点听不懂也无妨)。因此医学影像使掷中,往往须要本身设计网络去提取差异的模态特征,因此轻量布局简朴的Unet可以有更大的操纵空间。
因此,大多数医疗影像语义分割使命都会首先用Unet作为baseline
4 Unet网络布局

Unet网络是建立在FCN网络根本上的,它的网络架构如下图所示,总体来说与FCN思路非常类似。这里须要留意的是,U-Net的输入巨细是572x572,但是输出却是388x388,按理说它们应该相当(因为图像分割相当于逐像素进行分类,所以要求输入图像和输出图像巨细同等),但是为什么这里的输入尺寸要比输出尺寸大呢?那是因为下图这个布局图是当年论文作者绘制的,该作者对输入图像的边沿进行了镜像添补,通过镜像添补将边界地区进行扩大,这样可以给模子提供更多信息来完成模子的分割。
   按照论文中的表明,镜像添补的缘故原由是:因为图像 的边界的外面是空缺的,没有别的有效像素,而我们猜测图像中的像素种别时往往须要参考它的附近像素作为上下文信息,这样才气保持分割的精确性,为了猜测边界像素,论文对边界地区进行镜像,来补全边界附近缺失的内容,然后进行猜测。这种计谋叫做"overlap-tile"
  这里的输入是单通道的缘故原由是因为输入图片是灰度图,而输出是两通道是因为这里是对像素进行二分类(远景和配景),所以输出通道是2

整个网络由编码部分(左) 和 解码部分(右)构成,类似于一个大大的U字母,具体先容如下:
1、编码部分是典型的卷积网络架构:


  • 它重要的作用是进行特征提取
  • 架构中含有着一种重复布局,每次重复中都有2个 3 x 3卷积层、非线性ReLU层和一个 2 x 2 max pooling层(stride为2)。(图中的蓝箭头、红箭头,没画ReLu)
  • 每一次下采样后我们都把特征通道的数量更加
2、解码部分也利用了类似的模式:


  • 它重要的作用是进行上采样 (上采样可以让包含高级抽象特征的低分辨率图片在保留高级抽象特征的同时变为高分辨率)
  • 架构中包含有一种重复布局,每次重复都有一个上采样的卷积层(反卷积层),特征拼接concat,两个3x3的卷积层,非线性ReLU层
  • 每一步都首先利用反卷积(up-convolution),每次利用反卷积都将特征通道数量减半,特征图巨细更加。(图中绿箭头)
  • 反卷积事后,将反卷积的效果与编码部分中对应步骤的特征图拼接起来(concat)(也就是将深层特征与浅层特征进行融合,使得信息变得更丰富)。(白/蓝块)
  • 编码部分中的特征图尺寸稍大,将其修剪事后进行拼接(这里是将两个特征图的尺寸调解同等后按通道数进行拼接)。(左边深蓝虚线部分就是要裁剪的部分,它对应右边的白色长方块部分)
  • 对拼接后的map再进行2次3 x 3的卷积。(右侧蓝箭头)
  • 末了一层的卷积核巨细为1 x 1,将64通道的特征图转化为特定种别数量(分类数量)的效果。(图中青色箭头)
5 代码复现

下面利用pytorch框架对论文中的unet进行复现:
  1. import torch.nn as nn
  2. import torch
  3. # 编码器(论文中称之为收缩路径)的基本单元
  4. def contracting_block(in_channels, out_channels):
  5.     block = torch.nn.Sequential(
  6.         # 这里的卷积操作没有使用padding,所以每次卷积后图像的尺寸都会减少2个像素大小
  7.         nn.Conv2d(kernel_size=(3, 3), in_channels=in_channels, out_channels=out_channels),
  8.         nn.BatchNorm2d(out_channels),
  9.         nn.ReLU(),
  10.         nn.Conv2d(kernel_size=(3, 3), in_channels=out_channels, out_channels=out_channels),
  11.         nn.BatchNorm2d(out_channels),
  12.         nn.ReLU()
  13.     )
  14.     return block
  15. # 解码器(论文中称之为扩张路径)的基本单元
  16. class expansive_block(nn.Module):
  17.     def __init__(self, in_channels, mid_channels, out_channels):
  18.         super(expansive_block, self).__init__()
  19.         # 每进行一次反卷积,通道数减半,尺寸扩大2倍
  20.         self.up = nn.ConvTranspose2d(in_channels, in_channels // 2, kernel_size=(3, 3), stride=2, padding=1,
  21.                                      output_padding=1)
  22.         self.block = nn.Sequential(
  23.             # 这里的卷积操作没有使用padding,所以每次卷积后图像的尺寸都会减少2个像素大小
  24.             nn.Conv2d(kernel_size=(3, 3), in_channels=in_channels, out_channels=mid_channels),
  25.             nn.BatchNorm2d(mid_channels),
  26.             nn.ReLU(),
  27.             nn.Conv2d(kernel_size=(3, 3), in_channels=mid_channels, out_channels=out_channels),
  28.             nn.BatchNorm2d(out_channels),
  29.             nn.ReLU()
  30.         )
  31.     def forward(self, e, d):
  32.         d = self.up(d)
  33.         # concat
  34.         # e是来自编码器部分的特征图,d是来自解码器部分的特征图,它们的形状都是[B,C,H,W]
  35.         diffY = e.size()[2] - d.size()[2]
  36.         diffX = e.size()[3] - d.size()[3]
  37.         # 裁剪时,先计算e与d在高和宽方向的差距diffY和diffX,然后对e高方向进行裁剪,具体方法是两边分别裁剪diffY的一半,
  38.         # 最后对e宽方向进行裁剪,具体方法是两边分别裁剪diffX的一半,
  39.         # 具体的裁剪过程见下图一
  40.         e = e[:, :, diffY // 2:e.size()[2] - diffY // 2, diffX // 2:e.size()[3] - diffX // 2]
  41.         cat = torch.cat([e, d], dim=1)  # 在特征通道上进行拼接
  42.         out = self.block(cat)
  43.         return out
  44. # 最后的输出卷积层
  45. def final_block(in_channels, out_channels):
  46.     block = nn.Conv2d(kernel_size=(1, 1), in_channels=in_channels, out_channels=out_channels)
  47.     return block
  48. class UNet(nn.Module):
  49.     def __init__(self, in_channel, out_channel):
  50.         super(UNet, self).__init__()
  51.         # 编码器 (Encode)
  52.         self.conv_encode1 = contracting_block(in_channels=in_channel, out_channels=64)
  53.         self.conv_pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
  54.         self.conv_encode2 = contracting_block(in_channels=64, out_channels=128)
  55.         self.conv_pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
  56.         self.conv_encode3 = contracting_block(in_channels=128, out_channels=256)
  57.         self.conv_pool3 = nn.MaxPool2d(kernel_size=2, stride=2)
  58.         self.conv_encode4 = contracting_block(in_channels=256, out_channels=512)
  59.         self.conv_pool4 = nn.MaxPool2d(kernel_size=2, stride=2)
  60.         # 编码器与解码器之间的过渡部分(Bottleneck)
  61.         self.bottleneck = nn.Sequential(
  62.             nn.Conv2d(kernel_size=(3, 3), in_channels=512, out_channels=1024),
  63.             nn.BatchNorm2d(1024),
  64.             nn.ReLU(),
  65.             nn.Conv2d(kernel_size=(3, 3), in_channels=1024, out_channels=1024),
  66.             nn.BatchNorm2d(1024),
  67.             nn.ReLU()
  68.         )
  69.         # 解码器(Decode)
  70.         self.conv_decode4 = expansive_block(1024, 512, 512)
  71.         self.conv_decode3 = expansive_block(512, 256, 256)
  72.         self.conv_decode2 = expansive_block(256, 128, 128)
  73.         self.conv_decode1 = expansive_block(128, 64, 64)
  74.         self.final_layer = final_block(64, out_channel)
  75.     def forward(self, x):
  76.         # Encode
  77.         encode_block1 = self.conv_encode1(x)
  78.         encode_pool1 = self.conv_pool1(encode_block1)
  79.         encode_block2 = self.conv_encode2(encode_pool1)
  80.         encode_pool2 = self.conv_pool2(encode_block2)
  81.         encode_block3 = self.conv_encode3(encode_pool2)
  82.         encode_pool3 = self.conv_pool3(encode_block3)
  83.         encode_block4 = self.conv_encode4(encode_pool3)
  84.         encode_pool4 = self.conv_pool4(encode_block4)
  85.         # Bottleneck
  86.         bottleneck = self.bottleneck(encode_pool4)
  87.         # Decode
  88.         decode_block4 = self.conv_decode4(encode_block4, bottleneck)
  89.         decode_block3 = self.conv_decode3(encode_block3, decode_block4)
  90.         decode_block2 = self.conv_decode2(encode_block2, decode_block3)
  91.         decode_block1 = self.conv_decode1(encode_block1, decode_block2)
  92.         final_layer = self.final_layer(decode_block1)
  93.         return final_layer
  94. if __name__ == '__main__':
  95.     image = torch.rand((1, 3, 572, 572))
  96.     unet = UNet(in_channel=3, out_channel=2)
  97.     mask = unet(image)
  98.     print(mask.shape)
  99.    
  100.     #输出结果:
  101.     torch.Size([1, 2, 388, 388])
复制代码
图一:图像裁剪过程演示:
这里演示的是将64x64的特征图裁剪为56x56巨细的过程


来源:https://blog.csdn.net/m0_56192771/article/details/128708591
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则