1.Unet

作为图像语义分割里比较基本的分割网络,自然不能缺席
毕竟文题也叫Unet的深入浅出啊

1.1语义分割做什么

一开始我认为是这样的

pytorch 模型拆分到多个GPU pytorch unet 图像分割_pytorch 模型拆分到多个GPU


这么理解是没错的,深度学习确实也是这样端到端的小黑盒,

目前大火的原因也是想让这个小黑盒变白

在一维信号或者特征上非常好理解

输入1*n的数据,n是特征的数量,输出一个预测值,这个值代表某种意思

但是一直不太理解在图像中应该怎么理解

直到某一天看的论文累积够了,灵光一闪,才想明白了

pytorch 模型拆分到多个GPU pytorch unet 图像分割_pytorch 模型拆分到多个GPU_02


大致意思是,深度学习给得到是概率,

一维的情况是,二维的情况也是

二维给的是概率图,

在用一些核函数的技巧进入实际问题中之后,

深度学习其实就是线性代数+概率论+高等数学

在Unet的结构图中,通过中间的许多层之后,得到概率图,

根据金标准来对自身的概率进行调整和BP

达到最优解,因此后续还会引入一些优化的算法

1.2 网络结构

FCN作为图像分割的鼻祖,建议先了解一下FCN ,这里也会提到FCN

延续了FCN 的深层次的结构,Unet随着网络结构的加深,图像尺寸也在减小

FCN对encode采取的策略是同level的层,对应像素对应相加,Unet则是叠加(concatenate)

放上Unet的网络结构图

pytorch 模型拆分到多个GPU pytorch unet 图像分割_Unet_03

在每一个层中均用到了一个概念——卷积,就不得不详细说一下了

因为以前是研究经典算法的,诸如SIFT特征这样的,所以对高斯金字塔特别熟悉

看到卷积的时候也是笑了,知识总是触类旁通和极大的相似

先说一下高斯金字塔,原po传送门 如图,用不同的卷积核产生不同模糊程度的图像

这个图像人眼看不懂,计算机可以识别

相当于原博客中说的,戴上不同度数的眼镜看到的画面不同

pytorch 模型拆分到多个GPU pytorch unet 图像分割_ide_04

顺道发散一下,高斯金字塔的做法看着是不是很熟悉,比较新的deeplab啥的也是触类旁通的结构

pytorch 模型拆分到多个GPU pytorch unet 图像分割_ide_05


unet第一层网络结构从channel 1到 channel 64

就是使用了不同的卷积核,产生了指定数量的channel

pytorch 模型拆分到多个GPU pytorch unet 图像分割_2d_06

网络的后续的channel的变化也是如此,指定数量之后,得到的每个channel,

又唤做feature map,记录了不同尺度下的图像的特征

可能有人会问为什么指定数量之后就能产生对应数量的channel

这里请参考这篇博客 我这里引用其中的图片来解释是如何通过卷积核来产生指定数量的channel的

pytorch 模型拆分到多个GPU pytorch unet 图像分割_Pytorch_07

这些特征可以通过不断的调整参数,学习到
所以如何最大可能的使用卷积提取图像的特征,在我看来,是语义分割的关键

在卷积过程中

可以通过设置不同的卷积核以及padding,stride的大小,

控制同一层图像的尺寸不变

同样下采样的尺寸也是可以控制的

具体的公式如下:

pytorch 模型拆分到多个GPU pytorch unet 图像分割_Unet_08

其中k是卷积核尺寸,p是padding的值,s是stride的值

根据上述公式,令k=3, p=1, s=1,有

pytorch 模型拆分到多个GPU pytorch unet 图像分割_Unet_09

可以使得图像在卷积过程中的尺寸不发生变化

这是一个经常使用的trick

下采样,常用的maxpooling,通常是使得图像尺寸减小1/2

pytorch 模型拆分到多个GPU pytorch unet 图像分割_Unet_10


这会儿再看网络结构

原论文中k=3,p=0,s=1,所以原始的尺寸是572

根据公式有pytorch 模型拆分到多个GPU pytorch unet 图像分割_ide_11

所以从channel1-64,图像尺寸从572减小到了570

以此类推,蓝色小箭头表示卷积操作,每次图像宽和高减小2

红色小箭头表示maxpooling,图像尺寸减半,

如图中第一个红箭头处,从568减小到了284

pytorch 模型拆分到多个GPU pytorch unet 图像分割_pytorch 模型拆分到多个GPU_12


绿色小箭头代表上采样,与红色相对应,这里使图像尺寸增加2倍,如下图中1024到512

pytorch 模型拆分到多个GPU pytorch unet 图像分割_Unet_13


网络的创新之处就是concatenate这个操作了,也就是图中的灰色箭头这个位置

pytorch 模型拆分到多个GPU pytorch unet 图像分割_pytorch 模型拆分到多个GPU_14


可以看到,从1024个channel上采样过来的512个channel

和上一个对应有512个channel的同层channel被叠加起来了,

成为了一个新的1024个channel,如图红圈部分

同理类似256+256构成了新的512

与FCN不同的是,FCN在这一步中是直接与之前的进行对应像素相加操作

都是很巧妙的操作

到此,整个Unet的结构细节全部解析完毕

跟着推导一遍尺寸,会受益匪浅,入门其他的网络也会相对容易

给出pytorch实现的Unet模型,用到了我所说的同层尺寸不变的trick

还是老规矩,高度集成,面向对象

import torch.nn as nn


class conv_block(nn.Module):
    def __init__(self, ch_in, ch_out):
        super(conv_block, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(ch_in, ch_out, kernel_size=3, stride=1, padding=1, bias=True),
            nn.BatchNorm2d(ch_out),
            nn.ReLU(inplace=True),
            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):
        x = self.conv(x)
        return x

class up_conv(nn.Module):
    def __init__(self, ch_in, ch_out):
        super(up_conv, self).__init__()
        self.up = nn.Sequential(
            nn.Upsample(scale_factor=2),
            nn.Conv2d(ch_in, ch_out, kernel_size=3, stride=1, padding=1, bias=True),
            nn.BatchNorm2d(ch_out),
            nn.ReLU(inplace=True)
        )

    def forward(self, x):
        x = self.up(x)
        return x


class U_Net(nn.Module):
    def __init__(self, img_ch=3, output_ch=1):
        super(U_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.Up_conv5 = conv_block(ch_in=1024, ch_out=512)

        self.Up4 = up_conv(ch_in=512, ch_out=256)
        self.Up_conv4 = conv_block(ch_in=512, ch_out=256)

        self.Up3 = up_conv(ch_in=256, ch_out=128)
        self.Up_conv3 = conv_block(ch_in=256, ch_out=128)

        self.Up2 = up_conv(ch_in=128, ch_out=64)
        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)
        d5 = torch.cat((x4, d5), dim=1)

        d5 = self.Up_conv5(d5)

        d4 = self.Up4(d5)
        d4 = torch.cat((x3, d4), dim=1)
        d4 = self.Up_conv4(d4)

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

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

        d1 = self.Conv_1x1(d2)

        return d1

分割线,觉得不错点个赞即可
狂推----深度学习项目架构模板传送门 狂推----启蒙的repo传送门