U-Net及其变体


前言

U-net是一种主要为图像分割任务开发的图像分割技术,在医学图像分割领域有很高的实用性。
为希望探索U-net的研究人员提供一个起点。基于U-net的架构在医学图像分析中是有相当潜力和价值的。自2017年依赖U-net论文的增长证明了其作为医学影像深度学习技术的地位。预计U-net将是主要的前进道路之一。
忘了在哪听到的了,医学图像分割主要是解决位置和物体尺寸大小变化,扩展路径输出的图像有一定位置信息,加上收缩路径的输出对位置进行了更加详细的刻画;同时由于有池化类似于金字塔尺寸问题得到了一定程度解决,所以U-net效果才会这么好。
目前主要参考下面这个综述文章,并结合其中参考文献进行整理。不定时更新,处于小白阶段,有错误感谢指正,共同进步。
这里是自己DenseU-Net的代码,ResU-Net看这个文章即可。【医学图像分割网络】之Res U-Net网络PyTorch复现
我用的他的代码改了改,由于跑不动ResNet34就用的ResNet18。
综述文章:https://ieeexplore.ieee.org/document/9446143


一、U-Net

U-net架构(例如:最低分辨率为32×32像素)。每一个蓝色框对应的是多通道特征图。蓝色框顶部标着通道数。代表各个特征图大小的x-y尺寸在蓝色框的左下角。白色代表特征映射。箭头表示不同操作。最后那个箭头可能被挡住了,他代表1×1卷积,把通道数改到与原来图片通道数相等。
U-net分为两个部分。(中间对称)一部分是左边部分是典型的CNN架构的收缩路径(两个连续的3×3卷积+ReLU激活单元+最大池化层),每一次下采样后我们都把特征通道的数量加倍。第二部分是扩展路径(2×2上采样+收缩路径中对应的层裁剪得到与上采样得到的图片大小相同大小的图片concatenated上采样的特征地图上+2次连续的3×3conV+ReLU),每次使用反卷积都将特征通道数量减半,特征图大小加倍。最后阶段增加1×1卷积将特征图减少到所需数量的通道并产生分割图像。
之前它进行卷积由于没加padding,所以它每一次卷积过后图片的w和h都会减2,现在一般加上padding,使每次卷积后的图像大小不变,就省去了裁剪的操作(之前裁剪后才能与上采样的图片大小匹配,那篇文章中是说图片边缘信息不重要裁剪不会造成太大影响)。
对卷积不熟悉的可以看这个:
卷积算法:https://gitcode.net/mirrors/vdumoulin/conv_arithmetic?utm_source=csdn_github_accelerator

import torch
import torchvision.transforms.functional
from torch import nn
'''
两个3×3卷积层
不管是收缩路径还是扩张路径每一步都有两个3×3的卷积层,然后是ReLU激活。
在U-Net论文中,它们使用0 padding,这里使用1 padding,以便最后的特征图不会被裁剪
'''
import torch
import torchvision.transforms.functional
from torch import nn
import cv2 
from torchvision import transforms
'''
两个3×3卷积层
不管是收缩路径还是扩张路径每一步都有两个3×3的卷积层,然后是ReLU激活。
在U-Net论文中,它们使用0 padding,这里使用1 padding,以便最后的特征图不会被裁剪
'''
class DoubleConvolution(nn.Module):
    def __init__(self,in_channels:int,out_channels:int):#in_channels:输入通道数 out_channels:输出通道数
        super().__init__()
        self.first = nn.Conv2d(in_channels,out_channels,kernel_size=3,padding=1)
        self.act1 = nn.ReLU()   #这两行是第一个3×3卷积层,从U-net架构图可以看出在这一层图像的通道数已经变成out_channel
        self.second = nn.Conv2d(out_channels,out_channels,kernel_size=3,padding=1)
        self.act2 = nn.ReLU() #这两行是第二个卷积,从U-net架构图可以看出在这一层图像的通道数不变
        #函数实例化,下面调用相应的函数
    def forward(self,x:torch.Tensor):
        x = self.first(x)
        x = self.act1(x)
        x = self.second(x)
        return self.act2(x)
class DownSample(nn.Module):#下采样,收缩路径中的每一步都使用2×2最大池化层对特征图进行下采样
    def __init__(self):
        super().__init__()
        self.pool = nn.MaxPool2d(2) #最大池化层
    def forward(self,x:torch.Tensor):
        return self.pool(x)
class UpSample(nn.Module):#上采样,扩展路径中每一步都使用2×2上卷积
    def __init__(self,in_channels:int,out_channels:int):
        super().__init__()
        self.up = nn.ConvTranspose2d(in_channels,out_channels,kernel_size=2,stride=2)
        '''输出数据体在空间上的尺寸可以通过输入数据体尺寸,卷积层中卷积核尺寸(F对应kernel_size),步长(S对应stride)和零填充的数量(P该函数中默认为0)计算出来。
        W2=(W1-F+2P)/S+1,上采样大小减半->s=2,w2=w1/2->P=0,F=2
        对转置卷积感兴趣的可以看这个https://blog.csdn.net/qq_39478403/article/details/121181904,注意函数中对应的参数即可
        '''
    def forward(self,x:torch.Tensor):
        return self.up(x)
class CropAndConcat(nn.Module): #裁剪并串联要素地图,在扩展路径中的每一步,来自收缩路径的对应特征图与当前特征图连接
    def forward(self, x : torch.Tensor, contracting_x : torch.Tensor):
        contracting_x = torchvision.transforms.functional.center_crop(contracting_x,[x.shape[2],x.shape[3]])
        #torchvision.transforms.functional.center_crop ( img : Tensor , output_size : List [int ]), imgs是要中心裁剪的图像,后面List是裁剪后的大小
        x = torch.cat([x,contracting_x],dim=1)
        return x
class UNet(nn.Module):
        def __init__(self,in_channels:int,out_channels:int):
            super().__init__()
            self.down_conv = nn.ModuleList([DoubleConvolution(i,o) for i,o in
                                            [(in_channels,64),(64,128),(128,256),(256,512)]])#收缩路径的双层卷积。从64开始的每一步中,特征的数量加倍
            self.down_sample = nn.ModuleList([DownSample() for _ in range(4)])#循环4次
            self.middle_conv = DoubleConvolution(512,1024)#U-net的底部,分辨率最低的两个层
            self.up_sample = nn.ModuleList([UpSample(i,o) for i,o in
                                            [(1024,512),(512,256),(256,128),(128,64)]])
            self.up_conv = nn.ModuleList([DoubleConvolution(i,o) for i,o in
                                          [(1024,512),(512,256),(256,128),(128,64)]])
            self.concat = nn.ModuleList([CropAndConcat() for _ in range(4)])
            self.final_conv = nn.Conv2d(64,out_channels,kernel_size=1)
        def forward(self,x:torch.Tensor):
            pass_through = []
            for i in range(len(self.down_conv)):# 收缩路径,ModuleList可以理解为这个模型中的列表,具体可以查看其他资料
                x = self.down_conv[i](x) #两个3x3卷积层
                pass_through.append(x) #收集输出,在元素结尾插入指定内容
                x = self.down_sample[i](x) #下采样
            x = self.middle_conv(x)
            for i in range(len(self.up_conv)):#扩张路径
                x = self.up_sample[i](x)
                x = self.concat[i](x,pass_through.pop())#连续接收收缩路径的输出,pop删除并返回最后一个元素。堆栈
                x = self.up_conv[i](x)
            x = self.final_conv(x)
            return x

   




二、U-Net的变体

2.1 3D-Unet

3D-Unet是将U-net中所有2D操作替换为对应的3D操作。该篇文章中运用了动态弹性变形的数据增强方法。
论文:

https://arxiv.org/abs/1606.06650

在这里插入图片描述
为什么使用3D图像?
是因为3D图像可以提供额外的上下文信息。
3D U-net是U-net框架的基本拓展,支持3D立体分割。核心结构和U-net一样还是包含收缩和扩张路径,只是所有的2D操作都被相应的3D操作,即3D Conv、3D max pooling 和 3D upconvolutions所替代,从而产生三维分割图像。其中3D Conv与2DConv的区别的如下图,3D Conv包含了深度信息。
在这里插入图片描述

很多生物医学应用中,只需很少的注释示例就可以训练一个相当好的泛化网络。这是因为每个图像已经包含具有相应变化的重复结构。
3D Unet在生物医学领域得到了很好应用。例如下面这篇论文,创建了一个网络,该网络允许在进行诊断时进行抽象的多级分割图像。
3D U-net with Multi-level Deep Supervision: Fully Automatic Segmentation of Proximal Femur in 3D MR Images

2.2 Attention U-Net

论文:https://arxiv.org/abs/1804.03999
提出了用于医学图像处理的AG模型,该模型可以自动学会关注不同形状和大小的目标结构。
Attention U-Net的结构如下图所示。
在这里插入图片描述Attention-Unet模型是以Unet模型为基础的,可以从上图看出,Attention-Unet和U-net的区别就在于decoder时,从encoder提取的部分进行了Attention Gate再进行decoder。

代码如下:

class AttU_Net(nn.Module):
    def __init__(self,img_ch=3,output_ch=1):
        super(AttU_Net,self).__init__()
        
        self.Maxpool = nn.MaxPool2d(kernel_size=2,stride=2)

        self.Conv1 = conv_block(ch_in=img_ch,ch_out=64)
        self.Conv2 = conv_block(ch_in=64,ch_out=128)
        self.Conv3 = conv_block(ch_in=128,ch_out=256)
        self.Conv4 = conv_block(ch_in=256,ch_out=512)
        self.Conv5 = conv_block(ch_in=512,ch_out=1024)

        self.Up5 = up_conv(ch_in=1024,ch_out=512)
        self.Att5 = Attention_block(F_g=512,F_l=512,F_int=256)
        self.Up_conv5 = conv_block(ch_in=1024, ch_out=512)

        self.Up4 = up_conv(ch_in=512,ch_out=256)
        self.Att4 = Attention_block(F_g=256,F_l=256,F_int=128)
        self.Up_conv4 = conv_block(ch_in=512, ch_out=256)
        
        self.Up3 = up_conv(ch_in=256,ch_out=128)
        self.Att3 = Attention_block(F_g=128,F_l=128,F_int=64)
        self.Up_conv3 = conv_block(ch_in=256, ch_out=128)
        
        self.Up2 = up_conv(ch_in=128,ch_out=64)
        self.Att2 = Attention_block(F_g=64,F_l=64,F_int=32)
        self.Up_conv2 = conv_block(ch_in=128, ch_out=64)

        self.Conv_1x1 = nn.Conv2d(64,output_ch,kernel_size=1,stride=1,padding=0)


    def forward(self,x):
        # encoding path
        x1 = self.Conv1(x)

        x2 = self.Maxpool(x1)
        x2 = self.Conv2(x2)
        
        x3 = self.Maxpool(x2)
        x3 = self.Conv3(x3)

        x4 = self.Maxpool(x3)
        x4 = self.Conv4(x4)

        x5 = self.Maxpool(x4)
        x5 = self.Conv5(x5)

        # decoding + concat path
        d5 = self.Up5(x5)
        x4 = self.Att5(g=d5,x=x4)
        d5 = torch.cat((x4,d5),dim=1)        
        d5 = self.Up_conv5(d5)
        
        d4 = self.Up4(d5)
        x3 = self.Att4(g=d4,x=x3)
        d4 = torch.cat((x3,d4),dim=1)
        d4 = self.Up_conv4(d4)

        d3 = self.Up3(d4)
        x2 = self.Att3(g=d3,x=x2)
        d3 = torch.cat((x2,d3),dim=1)
        d3 = self.Up_conv3(d3)

        d2 = self.Up2(d3)
        x1 = self.Att2(g=d2,x=x1)
        d2 = torch.cat((x1,d2),dim=1)
        d2 = self.Up_conv2(d2)

        d1 = self.Conv_1x1(d2)

        return d1

该模型将任务简化为定位和分割。AGs能够抑制不相关背景区域的响应,注意力系数α∈[0,1]识别显著的图像区域,修剪特征响应,仅仅保留与特定任务相关的响应。AGs合并到标准U-Net架构中,以突出通过skip连接的显著特征。将从粗尺度提取出的信息应用到门控中,可以消除跳跃连接产生的不相关和嘈杂的响应。

AG的结构如下图所示:
在这里插入图片描述

class Attention_block(nn.Module):
    def __init__(self,F_g,F_l,F_int):
        super(Attention_block,self).__init__()
        self.W_g = nn.Sequential(
            nn.Conv2d(F_g, F_int, kernel_size=1,stride=1,padding=0,bias=True),
            nn.BatchNorm2d(F_int)
            )
        
        self.W_x = nn.Sequential(
            nn.Conv2d(F_l, F_int, kernel_size=1,stride=1,padding=0,bias=True),
            nn.BatchNorm2d(F_int)
        )

        self.psi = nn.Sequential(
            nn.Conv2d(F_int, 1, kernel_size=1,stride=1,padding=0,bias=True),
            nn.BatchNorm2d(1),
            nn.Sigmoid()
        )
        
        self.relu = nn.ReLU(inplace=True)
        
    def forward(self,g,x):
        g1 = self.W_g(g)
        x1 = self.W_x(x)
        psi = self.relu(g1+x1)
        psi = self.psi(psi)

        return x*psi

2.3 Inception U-Net

大多数图像处理算法倾向于使用固定大小的filters进行卷积,但是调整模型以找到正确的筛选器大小通常很麻烦;此外,固定大小的filters仅适用于突出部分大小相似的图像,不适用于突出部分的形状大小变化较大的图像。一种解决方法是用更深的网络,另一种是Inception network。
Inception block的结构如下图所示,以下来自沐神的动手学深度学习的图片。https://zh-v2.d2l.ai/
Inception块由四条并行路径组成。 前三条路径使用窗口大小为 1×1、 3×3和 5×5 的卷积层,从不同空间大小中提取信息。 中间的两条路径在输入上执行 1×1卷积,以减少通道数,从而降低模型的复杂性。 第四条路径使用 3×3 最大汇聚层,然后使用 1×1卷积层来改变通道数。 这四条路径都使用合适的填充来使输入与输出的高和宽一致,最后我们将每条线路的输出在通道维度上连结,并构成Inception块的输出。在Inception块中,通常调整的超参数是每层输出通道数。(以上的话也是来自李沐的动手学深度学习https://zh-v2.d2l.ai/

class Inception(nn.Module):
    # c1--c4是每条路径的输出通道数
    def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
        super(Inception, self).__init__(**kwargs)
        # 线路1,单1x1卷积层
        self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
        # 线路2,1x1卷积层后接3x3卷积层
        self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
        # 线路3,1x1卷积层后接5x5卷积层
        self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
        # 线路4,3x3最大汇聚层后接1x1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)

    def forward(self, x):
        p1 = F.relu(self.p1_1(x))
        p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
        p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
        p4 = F.relu(self.p4_2(self.p4_1(x)))
        # 在通道维度上连结输出
        return torch.cat((p1, p2, p3, p4), dim=1)

下面借助《DENSE-INception-U-net-for-medical-image-segmentation》的代码看怎么应用到U-Net中。
下面是DIU-Net的模型。
在这里插入图片描述
这篇文章把inception module和dense connection的结构结合在一起,并且基于U-Net构建这个网络架构。这个网络架构包括analysis path 和 synthesis path,这两个路径由四种类型的模块构成,分别是:Inception-Res 模块、Dense Inception 模块、down-sample模块,up-sample模块。3个 Inception-Res 模块、一个Dense Inception模块和四个down-sample模块构成了分析路径。三个 Inception-Res 模块、一个Dense Inception 模块和四个up-sample模块构成了合成管道。单个Dense-Inception模块在模型中间,它比其它部分含有更多的 inception 层。下面是各个模块的结构。
Inception-Res block:
多用1×1、3×3 卷积、 AdaptiveAvgPool2d替代全连接 既可以加快速度,又可以达到与全连接、大卷积核一样的效果。还有一个规律,就是图像尺寸减半,同时通道数指数增长,可以很好地保留特征。小核多卷几次比大核效果好,一个5x5Conv可以被两次3x3Conv代替,所以Inception block中的5x5Conv用两次3x3Conv代替,结构如下图。(来自Rethinking the Inception Architecture for Computer Vision

在这里插入图片描述
代码如下,按照图来编写即可。

def inception_res_block_down(inputs, numFilters):
  c1 = Conv2D(numFilters, (1,1), padding = 'same', kernel_initializer = 'he_normal')(inputs)

  c11 = Conv2D(numFilters, (1,1), padding = 'same', kernel_initializer = 'he_normal')(c1)
  c12 = Conv2D(numFilters, (1,1), padding = 'same', kernel_initializer = 'he_normal')(c1)
  c13 = Conv2D(numFilters, (1,1), padding = 'same', kernel_initializer = 'he_normal')(c1)

  c11 = BatchNormalization()(c11)
  #c11 = Activation('relu')(c11)
  #c11 = Conv2D(numFilters, (1,1), padding = 'same', kernel_initializer = 'he_normal')(c11)

  c12 = Conv2D(numFilters, (3,3), padding = 'same', kernel_initializer = 'he_normal')(c12)
  c12 = BatchNormalization()(c12)
  #c12 = Activation('relu')(c12)
  #c12 = Conv2D(numFilters, (1,1), padding = 'same', kernel_initializer = 'he_normal')(c12)

  c13 = Conv2D(numFilters, (3,3), padding = 'same', kernel_initializer = 'he_normal')(c13)
  c13 = BatchNormalization()(c13)
  #c13 = Activation('relu')(c13)
  c13 = Conv2D(numFilters, (3,3), padding = 'same', kernel_initializer = 'he_normal')(c13)
  c13 = BatchNormalization()(c13)
  #c13 = Activation('relu')(c13)
  #c13 = Conv2D(numFilters, (1,1), padding = 'same', kernel_initializer = 'he_normal')(c13)

  inception_module = concatenate([c11,c12, c13], axis  = 3)

  concat = Conv2D(numFilters, (1,1), padding = 'same', kernel_initializer = 'he_normal')(inception_module)

  out = Add()([concat, c1])

  return out

Dense-Inception block :
在这里插入图片描述Down-sample and Up sample block :

在这里插入图片描述

2.4 Residual U-Net

这个是基于Res-Net的架构。训练很深的网络是一个很难的事情,深度变大,精度变差。按理说很深的网络有更多层可以学到更多,但是SGD找不到这个比较优的解,也就是说网络训练不动。
使用李沐论文精度bilibili最后10分钟里面的插图。(不会敲公式😂)
在这里插入图片描述

Resnet训练比较快,主要是因为它梯度上保持很好。大部分网络它的梯度越来越小是一个累乘。而Resnet还保留了上一层的梯度,梯度保持很好。
Resnet详细的介绍可以参考论文:Deep Residual Learning for Image Recognition李沐老师的视频,知乎你必须要知道CNN模型:ResNet
在这里插入图片描述
R2U-Net架构如下图。代码和图来自https://github.com/LeeJunHyun/Image_Segmentation
在这里插入图片描述

我们看一下他的RRCNN_block部分,返回值是改变通道数的输入加上经过两次Sequential后的输出,就相当于输入+输出。
注意Recurrent Conv block和Residual Conv unit的区别。Residual Conv unit是这里提到的resnet,输入直接连到输出。Recurrent Conv block需要循环t次,第n次包含前n次的部分信息。
在这里插入图片描述

class RRCNN_block(nn.Module):#默认t=2,就是2层Conv 然后skip
    def __init__(self,ch_in,ch_out,t=2):
        super(RRCNN_block,self).__init__()
        self.RCNN = nn.Sequential(
            Recurrent_block(ch_out,t=t),
            Recurrent_block(ch_out,t=t)
        )
        self.Conv_1x1 = nn.Conv2d(ch_in,ch_out,kernel_size=1,stride=1,padding=0)

    def forward(self,x):
        x = self.Conv_1x1(x) #先改变通道数
        x1 = self.RCNN(x) #然后Conv
        return x+x1 #输入+输出

在这里插入图片描述
上图是带有skip连接的三个连续ResNet块。skip信号通过逐元素加法与输出结合。最常见的ResNet实现是双层skip(如图所示)或三层skip。
模型的代码如下。

class R2U_Net(nn.Module):
    def __init__(self,img_ch=3,output_ch=1,t=2):
        super(R2U_Net,self).__init__()
        
        self.Maxpool = nn.MaxPool2d(kernel_size=2,stride=2)
        self.Upsample = nn.Upsample(scale_factor=2)

        self.RRCNN1 = RRCNN_block(ch_in=img_ch,ch_out=64,t=t)

        self.RRCNN2 = RRCNN_block(ch_in=64,ch_out=128,t=t)
        
        self.RRCNN3 = RRCNN_block(ch_in=128,ch_out=256,t=t)
        
        self.RRCNN4 = RRCNN_block(ch_in=256,ch_out=512,t=t)
        
        self.RRCNN5 = RRCNN_block(ch_in=512,ch_out=1024,t=t)
        

        self.Up5 = up_conv(ch_in=1024,ch_out=512)
        self.Up_RRCNN5 = RRCNN_block(ch_in=1024, ch_out=512,t=t)
        
        self.Up4 = up_conv(ch_in=512,ch_out=256)
        self.Up_RRCNN4 = RRCNN_block(ch_in=512, ch_out=256,t=t)
        
        self.Up3 = up_conv(ch_in=256,ch_out=128)
        self.Up_RRCNN3 = RRCNN_block(ch_in=256, ch_out=128,t=t)
        
        self.Up2 = up_conv(ch_in=128,ch_out=64)
        self.Up_RRCNN2 = RRCNN_block(ch_in=128, ch_out=64,t=t)

        self.Conv_1x1 = nn.Conv2d(64,output_ch,kernel_size=1,stride=1,padding=0)


    def forward(self,x):
        # encoding path
        x1 = self.RRCNN1(x)

        x2 = self.Maxpool(x1)
        x2 = self.RRCNN2(x2)
        
        x3 = self.Maxpool(x2)
        x3 = self.RRCNN3(x3)

        x4 = self.Maxpool(x3)
        x4 = self.RRCNN4(x4)

        x5 = self.Maxpool(x4)
        x5 = self.RRCNN5(x5)

        # decoding + concat path
        d5 = self.Up5(x5)
        d5 = torch.cat((x4,d5),dim=1)
        d5 = self.Up_RRCNN5(d5)
        
        d4 = self.Up4(d5)
        d4 = torch.cat((x3,d4),dim=1)
        d4 = self.Up_RRCNN4(d4)

        d3 = self.Up3(d4)
        d3 = torch.cat((x2,d3),dim=1)
        d3 = self.Up_RRCNN3(d3)

        d2 = self.Up2(d3)
        d2 = torch.cat((x1,d2),dim=1)
        d2 = self.Up_RRCNN2(d2)

        d1 = self.Conv_1x1(d2)

        return d1

2.5 Recurrent U-Net

在这里插入图片描述
上图是RNN的结构,就是它的当前的输出不仅与当前输入xt有关还与包含之前信息的ht-1有关。
递归神经网络是一种神经网络,最初被设计用于分析诸如文本或音频数据之类的序列数据。该网络以这样的方式设计,即节点的输出基于来自相同节点的先前输出而改变,即,与传统前馈网络相反的反馈回路。这个反馈回路也称为循环连接,它创建一个内部状态或记忆,为节点提供以离散时间步长改变输出的时间属性。当扩展到整个层时,这允许网络处理来自先前数据的上下文信息。
在这里插入图片描述
上图是循环神经网络。在这个简单的网络中,第二层和第三层是循环层。循环层中的每个神经元在离散时间周期接收来自其输出的反馈以及来自前一层的新信息,并相应地产生新输出。此组件允许网络处理顺序信息。

y i j k l ( t ) = ( w k f ) T x l f ( i , j ) ( t ) +   ( w k r ) T x l r ( i , j ) ( t − 1 ) + b k \begin{equation*} y_{ijk}^{l}\left ({t }\right)=\left ({w_{k}^{f} }\right)^{T}x_{l}^{f\left ({i,j }\right)}\left ({t }\right) +\,\left ({w_{k}^{r} }\right)^{T}x_{l}^{r\left ({i,j }\right)}\left ({t-1 }\right)+b_{k}\end{equation*} yijkl(t)=(wkf)Txlf(i,j)(t)+(wkr)Txlr(i,j)(t1)+bk
其中xfl(t)是前馈输入,xrl(t−1)是第l层的递归输入,wfk是前馈权重,wrk是递归权重,bk是第k个特征映射的偏差。
下面的代码是R2U-net中的Recurrent block。其中循环t次。当前输出等于当前输入进行conv后的结果加上上一时刻输出在做Conv。包含之前时刻的信息。

class Recurrent_block(nn.Module):
    def __init__(self,ch_out,t=2):
        super(Recurrent_block,self).__init__()
        self.t = t
        self.ch_out = ch_out
        self.conv = nn.Sequential(
            nn.Conv2d(ch_out,ch_out,kernel_size=3,stride=1,padding=1,bias=True),
		    nn.BatchNorm2d(ch_out),
			nn.ReLU(inplace=True)
        )

    def forward(self,x):
        for i in range(self.t):

            if i==0:
                x1 = self.conv(x)
            
            x1 = self.conv(x+x1)
        return x1

2.6 Dense U-net

DenseNet对于每一层所有前一层地特征图都用作输入,其自己的特征图做所有后序层的输入。
优势:它们缓解了梯度消失问题,加强了特征传播,鼓励了特征重用,并大大减少了参数的数量。
DenseNet倾向于在精度方面产生一致的提高,而没有任何性能下降或过度拟合的迹象。DenseNets可能是基于卷积特征构建的各种计算机视觉任务的良好特征提取器。
为了确保网络中各层之间的最大信息流,将所有层(具有匹配的特征映射大小)直接相互连接。为了保持前馈特性,每一层从前面的所有层获取额外的输入,并将自己特征图传递给所有后序层(具有匹配的特征映射大小)直接相互连接。为了保持前馈特性,每一层从前面的所有曾获取额外的输入,并将自己的特征图传递个所有后序层。
在这里插入图片描述
拼接单元从所有先前层接收特征图并将其传递到下一层。这可确保任何给定图层都具有来自块中任何先前图层的上下文信息。
借助这个论文:Bi-Directional ConvLSTM U-Net with Densley Connected Convolutions
这个代码:https://github.com/rezazad68/BCDU-Net
下图是这篇论文的模型(看代码的时候可以借助这个图)。
在这里插入图片描述
这个模型最下面是dense block。Dense U-net是每一个块都是Dense block。
在这里插入图片描述
上图结构在代码中是#D1,#D2,#D3标注的部分。可以看到D3的输入是D1和D2的concatenate。

def BCDU_net_D3(input_size = (256,256,1)):
    N = input_size[0]
    inputs = Input(input_size) 
    conv1 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(inputs)
    conv1 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv1)
  
    pool1 = MaxPooling2D(pool_size=(2, 2))(conv1)
    conv2 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool1)
    conv2 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv2)
    pool2 = MaxPooling2D(pool_size=(2, 2))(conv2)
    conv3 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool2)
    conv3 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv3)
    drop3 = Dropout(0.5)(conv3)
    pool3 = MaxPooling2D(pool_size=(2, 2))(conv3)
    # D1
    conv4 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool3)     
    conv4_1 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv4)
    drop4_1 = Dropout(0.5)(conv4_1)
    # D2
    conv4_2 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(drop4_1)     
    conv4_2 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv4_2)
    conv4_2 = Dropout(0.5)(conv4_2)
    # D3
    merge_dense = concatenate([conv4_2,drop4_1], axis = 3)
    conv4_3 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge_dense)     
    conv4_3 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv4_3)
    drop4_3 = Dropout(0.5)(conv4_3)
    up6 = Conv2DTranspose(256, kernel_size=2, strides=2, padding='same',kernel_initializer = 'he_normal')(drop4_3)
    up6 = BatchNormalization(axis=3)(up6)
    up6 = Activation('relu')(up6)

    x1 = Reshape(target_shape=(1, np.int32(N/4), np.int32(N/4), 256))(drop3)
    x2 = Reshape(target_shape=(1, np.int32(N/4), np.int32(N/4), 256))(up6)
    merge6  = concatenate([x1,x2], axis = 1) 
    merge6 = ConvLSTM2D(filters = 128, kernel_size=(3, 3), padding='same', return_sequences = False, go_backwards = True,kernel_initializer = 'he_normal' )(merge6)
            
    conv6 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge6)
    conv6 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv6)

    up7 = Conv2DTranspose(128, kernel_size=2, strides=2, padding='same',kernel_initializer = 'he_normal')(conv6)
    up7 = BatchNormalization(axis=3)(up7)
    up7 = Activation('relu')(up7)

    x1 = Reshape(target_shape=(1, np.int32(N/2), np.int32(N/2), 128))(conv2)
    x2 = Reshape(target_shape=(1, np.int32(N/2), np.int32(N/2), 128))(up7)
    merge7  = concatenate([x1,x2], axis = 1) 
    merge7 = ConvLSTM2D(filters = 64, kernel_size=(3, 3), padding='same', return_sequences = False, go_backwards = True,kernel_initializer = 'he_normal' )(merge7)
        
    conv7 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge7)
    conv7 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv7)

    up8 = Conv2DTranspose(64, kernel_size=2, strides=2, padding='same',kernel_initializer = 'he_normal')(conv7)
    up8 = BatchNormalization(axis=3)(up8)
    up8 = Activation('relu')(up8)    

    x1 = Reshape(target_shape=(1, N, N, 64))(conv1)
    x2 = Reshape(target_shape=(1, N, N, 64))(up8)
    merge8  = concatenate([x1,x2], axis = 1) 
    merge8 = ConvLSTM2D(filters = 32, kernel_size=(3, 3), padding='same', return_sequences = False, go_backwards = True,kernel_initializer = 'he_normal' )(merge8)    
    
    conv8 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge8)
    conv8 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv8)
    conv8 = Conv2D(2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv8)
    conv9 = Conv2D(1, 1, activation = 'sigmoid')(conv8)

    model = Model(input = inputs, output = conv9)
    model.compile(optimizer = Adam(lr = 1e-4), loss = 'binary_crossentropy', metrics = ['accuracy'])
    return model

总之,这篇文章充分利用U-Net、双向ConvLSTM(BConvLSTM)和Dense Conv,并且用BN加快网络收敛速度。证明了通过在skip连接中包含BConvLSTM并插入密集连接的卷积块,网络能够捕获更多区分信息,从而产生更精准的分割结果。
在这里插入图片描述
代码来源:https://github.com/THUHoloLab/Dense-U-net
在这里插入图片描述
知道dense_block的代码,替换对应的U-net里面卷积的代码即可。
可以看出第一层是输入input_tensor(假设为第0层输出),进行卷积。第二层输入是第0层输出input_tensor+第一层输出x1的concat。第三层是第0层输出input_tensor+第一层输出x1+第二层输出x2的concat。以此类推。

def dens_block(input_tensor, nb_filter):
    x1 = Conv_Block(input_tensor,nb_filter)
    add1 = concatenate([x1, input_tensor], axis=-1)
    x2 = Conv_Block(add1,nb_filter)
    add2 = concatenate([x1, input_tensor,x2], axis=-1)
    x3 = Conv_Block(add2,nb_filter)
    return x3
def unet(input_shape=(512, 512, 3)):

    inputs = Input(input_shape)
    # x  = Conv2D(32, 1, strides=1, activation='relu', padding='same', kernel_initializer='he_normal')(inputs)
    x = Conv2D(32, 7, kernel_initializer='he_normal', padding='same', strides=1,use_bias=False, kernel_regularizer=l2(1e-4))(inputs)
    #down first
    down1 = dens_block(x,nb_filter=64)
    pool1 = MaxPooling2D(pool_size=(2, 2))(down1)#256
    #down second
    down2 = dens_block(pool1,nb_filter=64)
    pool2 = MaxPooling2D(pool_size=(2, 2))(down2)#128
    #down third
    down3 = dens_block(pool2,nb_filter=128)
    pool3 = MaxPooling2D(pool_size=(2, 2))(down3)#64
    #down four
    down4 = dens_block(pool3,nb_filter=256)
    pool4 = MaxPooling2D(pool_size=(2, 2))(down4)#32
    #center
    conv5 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal')(pool4)
    conv5 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv5)
    drop5 = Dropout(0.5)(conv5)

    # up first
    up6 = UpSampling2D(size=(2, 2))(drop5)
    # up6 = UpSampling2D(size=(2, 2))(drop5)
    add6 = concatenate([down4, up6], axis=3)
    up6 = dens_block(add6,nb_filter=256)
    # up second
    up7 = UpSampling2D(size=(2, 2))(up6)
    #up7 = UpSampling2D(size=(2, 2))(conv6)
    add7 = concatenate([down3, up7], axis=3)
    up7 = dens_block(add7,nb_filter=128)
    # up third
    up8 = UpSampling2D(size=(2, 2))(up7)
    #up8 = UpSampling2D(size=(2, 2))(conv7)
    add8 = concatenate([down2, up8], axis=-1)
    up8 = dens_block(add8,nb_filter=64)
    #up four
    up9 =UpSampling2D(size=(2, 2))(up8)
    add9 = concatenate([down1, up9], axis=-1)
    up9 = dens_block(add9,nb_filter=64)
    # output
    conv10 = Conv2D(32, 7, strides=1, activation='relu', padding='same', kernel_initializer='he_normal')(up9)
    conv10 = Conv2D(3, 1, activation='sigmoid')(conv10)
    model = Model(input=inputs, output=conv10)
    print(model.summary())
    return model

2.7 U-Net++

论文:UNet++: A Nested U-Net Architecture
for Medical Image Segmentation

参考:研习U-Net(非常推荐)
知乎上研习U-Net讲解了U-Net++的来源。U-Net的简易结构如下图所示(来源是研习U-Net)。
在这里插入图片描述
作者提出了为什么U-Net有4层?然后他进行了1-4层U-Net的实验,结果并不是越深越好,所以说浅层和深层都有其对应的信息。对于特征提取阶段,浅层结构可以抓取图像的一些简单的特征,而深层结构因为感受野大了,经过卷积操作多了,能抓取到图像的一些的抽象特征。然后提出浅层结构和深层结构都重要,U-Net为什么只在4层之后才返回去,也就是只去抓深层特征。
作者想把1-4层U-Net结构合在一起,如下图,这样他们在编码器那边是共享参数的,但是更新模型参数的时候梯度只能沿着4层的U-Net网络传播,不经过1-3层U-Net的解码器。因为L与x0,4连接,x0,4与中间结构不相连。无法训练。
在这里插入图片描述
解决这个问题他提了两个方法。一种如下图。把长连接换成了短连接。这样梯度可以传播,但是缺少了长连接的优势。
长连接skip的优势:
1. Fights the vanishing(消失的) gradient problem.
2. Learns pyramid level features
3. Recover info(信息) loss in down-sampling
在这里插入图片描述
怎么既能发挥长连接的优势又能使网络能够训练就是U-Net++的结构,既有长连接,又有短链接。很类似于Dense连接。
第二个解决不能训练的方法是加deep supervision。然后作者在U-Net、上面提到的网络和U-net++上添加了deep supervision。最终在U-Net++上面效果更好。
在这里插入图片描述
使用Deep supervision可以进行剪枝。训练的时候用U-net++,测试的时候剪掉最右边一层,可以提高运行速度,减少参数量。
做实验的时候为了验证是模型结构使精度提高,而不是单纯的参数增加导致的,作者构造了wide U-Net,增加每一层的卷积参数。
在这里插入图片描述
图1:(a)UNet++由编码器和解码器组成,它们通过一系列嵌套的密集卷积块连接。UNet++背后的主要思想是在融合之前结合编码器和解码器特征图之间的语义差距。例如,(X0,0,X1,3)之间的语义差距是使用具有三个卷积层的密集卷积块来结合的。在上图中,黑色表示原始U-Net,绿色和蓝色表示跳过路径上的密集卷积块,红色表示深度监督。 红色、绿色和蓝色组件将UNet++与U-Net区分开来。(b) 对UNet++的第一个跳跃途径的详细分析。(c) UNet++如果在深度监督下进行训练,可以在推理时进行修剪。

#For nested 3 channels are required

class conv_block_nested(nn.Module):
    
    def __init__(self, in_ch, mid_ch, out_ch):
        super(conv_block_nested, self).__init__()
        self.activation = nn.ReLU(inplace=True) #进行覆盖运算
        self.conv1 = nn.Conv2d(in_ch, mid_ch, kernel_size=3, padding=1, bias=True)
        self.bn1 = nn.BatchNorm2d(mid_ch)
        self.conv2 = nn.Conv2d(mid_ch, out_ch, kernel_size=3, padding=1, bias=True)
        self.bn2 = nn.BatchNorm2d(out_ch)

    def forward(self, x): #看起来是平平无奇的卷积-BN-ReLU组成的块
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.activation(x)
        
        x = self.conv2(x)
        x = self.bn2(x)
        output = self.activation(x)

        return output
    
#Nested Unet

class NestedUNet(nn.Module):
    """
    Implementation of this paper:
    https://arxiv.org/pdf/1807.10165.pdf
    """
    def __init__(self, in_ch=3, out_ch=1):
        super(NestedUNet, self).__init__()

        n1 = 64
        filters = [n1, n1 * 2, n1 * 4, n1 * 8, n1 * 16] #通道数除了第一次,下采样每次加倍,上采样减半

        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)#池化
        self.Up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)#上采样
        #这是左边半个U,通道数分别是从0->1,1->2,2->3,3->4,其他的看图上结构即可,对应图上相应的编号
        self.conv0_0 = conv_block_nested(in_ch, filters[0], filters[0])
        self.conv1_0 = conv_block_nested(filters[0], filters[1], filters[1])
        self.conv2_0 = conv_block_nested(filters[1], filters[2], filters[2])
        self.conv3_0 = conv_block_nested(filters[2], filters[3], filters[3])
        self.conv4_0 = conv_block_nested(filters[3], filters[4], filters[4])

        self.conv0_1 = conv_block_nested(filters[0] + filters[1], filters[0], filters[0])
        self.conv1_1 = conv_block_nested(filters[1] + filters[2], filters[1], filters[1])
        self.conv2_1 = conv_block_nested(filters[2] + filters[3], filters[2], filters[2])
        self.conv3_1 = conv_block_nested(filters[3] + filters[4], filters[3], filters[3])

        self.conv0_2 = conv_block_nested(filters[0]*2 + filters[1], filters[0], filters[0])
        self.conv1_2 = conv_block_nested(filters[1]*2 + filters[2], filters[1], filters[1])
        self.conv2_2 = conv_block_nested(filters[2]*2 + filters[3], filters[2], filters[2])

        self.conv0_3 = conv_block_nested(filters[0]*3 + filters[1], filters[0], filters[0])
        self.conv1_3 = conv_block_nested(filters[1]*3 + filters[2], filters[1], filters[1])

        self.conv0_4 = conv_block_nested(filters[0]*4 + filters[1], filters[0], filters[0])

        self.final = nn.Conv2d(filters[0], out_ch, kernel_size=1)


    def forward(self, x):
        
        x0_0 = self.conv0_0(x)
        x1_0 = self.conv1_0(self.pool(x0_0))#0->1,1->2,2->3,3->4的时候都有pool池化,减小图片大小
        x0_1 = self.conv0_1(torch.cat([x0_0, self.Up(x1_0)], 1))#当0层的图片与1层图片结合的时候,1层图片要做上采样使之与0层图片大小相同,x0_1代表图上第0层第1列的⚪

        x2_0 = self.conv2_0(self.pool(x1_0))
        x1_1 = self.conv1_1(torch.cat([x1_0, self.Up(x2_0)], 1))
        x0_2 = self.conv0_2(torch.cat([x0_0, x0_1, self.Up(x1_1)], 1))#x0_2是前面x0_0,x0_1,x1_1上采样结合,dense 连接,其他的类似

        x3_0 = self.conv3_0(self.pool(x2_0))
        x2_1 = self.conv2_1(torch.cat([x2_0, self.Up(x3_0)], 1))
        x1_2 = self.conv1_2(torch.cat([x1_0, x1_1, self.Up(x2_1)], 1))
        x0_3 = self.conv0_3(torch.cat([x0_0, x0_1, x0_2, self.Up(x1_2)], 1))

        x4_0 = self.conv4_0(self.pool(x3_0))
        x3_1 = self.conv3_1(torch.cat([x3_0, self.Up(x4_0)], 1))
        x2_2 = self.conv2_2(torch.cat([x2_0, x2_1, self.Up(x3_1)], 1))
        x1_3 = self.conv1_3(torch.cat([x1_0, x1_1, x1_2, self.Up(x2_2)], 1))
        x0_4 = self.conv0_4(torch.cat([x0_0, x0_1, x0_2, x0_3, self.Up(x1_3)], 1))

        output = self.final(x0_4)
        return output

2.8 Adversarial U-Net

2.9 Ensemble U-Net

2.10 Comparison With Other Architectures

总结

暂无

  • 13
    点赞
  • 83
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值