UNet model
Ronneberger 等人(2015)[79]提出的 UNet 体系结构,整个网络结构呈字母“ U”的形状,因此命名为 UNet。U 形模型由四个主要组件组成,如上图4所示: 卷积层、池化层、上采样层和跳跃连接。左边是下采样,右边是上采样。卷积层负责识别空间特征,它们与其他组件的协调操作允许不同的卷积层捕获不同分辨率的特征。通过解码-编码的结构实现图像特征的提取.
对于输入的数据是如何进行特征提取呢?在我刚刚了解这些深度学习模型时是非常想要探究的。
下面,我试图去揭示这背后的实现过程,在此之前,需要了解卷积和池化的作用,这在之前的笔记中曾记录过,这里简单复习一下:神经网络背后的数学。
卷积 | 池化
卷积
卷积实现了对于输入数据的特征提取。下面尽可能的说清楚它对于数组的size的改变,假设我们现在有一个7x7的网格,你可以将其理解为一个图片或者一个数组。
这很容易理解,然后接着我们假设一个3x3的卷积核
实现卷积的过程,可以理解为卷积核的范围与原始数组上对应位置格点相乘再相加的过程。什么意思呢,假设我们的卷积核的移动步长为1,那么对于原始数据进行步长为1的卷积过程,就是:
最终会输出一个5x5的特征图
依次向下同理,如果移动的步长为2呢,就是下面这样:
最终会输出一个3x3的特征图
那如果步长为3呢,这时就不满足了!!他会出现下面的这样的情况:卷积覆盖的地方没有对应的数值了
总结上面的规律,其实我们可以根据对应的数据大小NxN和卷积核大小FxF得到卷积过程后的数组大小,其中stride为步长
下面是一些动图的演示:
一般情况
存在偏差的情况
但是,这里会发现有个问题,如果单纯按照上面的过程去卷积,会导致卷积后的输入的尺寸不断减小,卷积操作仅仅会在输入的数据内部进行,边界的信息会被忽略。这样会丢失很多边界的信息,那应该怎么做呢?
可以通过对于边界进行零填充,在卷积操作中平衡尺寸缩减和特征提取之间的关系
如下所示:
这样填充完之后,原本的7x7的输入数据就变成了9x9,然后使用步长为1的3x3卷积核,我们会得到还是7x7的输出特征图。这样可以得到一个新的计算公式,假设输入为N,卷积核为F,填充的边界像素为P,步长为stride,那么输出尺寸为
(N+2P-F)/stride+1
对于一般的卷积层,我们通常设置stride为1,卷积核大小为FxF,零填充为(F-1)/2,这样来保证数组大小的守恒
此外,卷积核的数量可以控制输出数据的维度,对于较大的图片,我们需要较多的层来使得每一个output来"看见"整个图片的信息。
下面对于卷积层进行一个总结
编程实现
通常在Unet中,对于输入的数据会先进行两次卷积,下面是一个简单的基于tensorflow的编程示例,
对于输入数据大小为128x128x32的数组,经过两次卷积后,空间大小仍然是128x128,但是通道数由卷积核的数量控制(filter_num),为64
池化
池化层就相对容易一点,一般包含
平均池化 最大池化
以最大池化为例:
对于一个2x2的池化层,同样覆盖到原始的数据,选择覆盖范围内最大的作为显著的特征,基本没有学习的参数
下面是池化层的一些总结
编程实现
一般Unet架构中,两次卷积层后,后面跟着一个池化,通常使用最大池化层,大小为2x2
编程就更简单了,直接调用一个参数接口即可:
可以发现,空间尺寸从128x128减小到了64x64,但是通道维度上的大小没有变换,还是64
对于后续的Unet架构也是同理,对于池化后的数据再次进行相同的过程,两次卷积-一次池化。
这就是下采样的过程。后续再通过跳跃连接和反卷积实现上采样,这里直接给出相关的Unet代码了,直接封装为函数了,可以将其拆开看一下每一步数据的大小变换,方便进一步理解各个卷积池化的作用:
import tensorflow as tf
from tensorflow.keras import models, layers, regularizers
from tensorflow.keras import backend as K
##############################################################
'''
Useful blocks to build Unet
conv - BN - Activation - conv - BN - Activation - Dropout (if enabled)
'''
def conv_block(x, filter_size, size, dropout, batch_norm=False):
conv = layers.Conv2D(size, (filter_size, filter_size), padding="same")(x)
if batch_norm is True:
conv = layers.BatchNormalization(axis=3)(conv)
conv = layers.Activation("relu")(conv)
conv = layers.Conv2D(size, (filter_size, filter_size), padding="same")(conv)
if batch_norm is True:
conv = layers.BatchNormalization(axis=3)(conv)
conv = layers.Activation("relu")(conv)
if dropout > 0:
conv = layers.Dropout(dropout)(conv)
return conv
def UNet(input_shape, NUM_CLASSES=1, dropout_rate=0.0, batch_norm=True):
'''
U-Net: Convolutional Networks for Biomedical Image Segmentation
https://arxiv.org/abs/1505.04597
'''
# network structure
FILTER_NUM = 64 # number of filters for the first layer
FILTER_SIZE = 3 # size of the convolutional filter
UP_SAMP_SIZE = 2 # size of upsampling filters
inputs = layers.Input(input_shape, dtype=tf.float32)
# Downsampling layers
# DownRes 1, convolution + pooling
conv_128 = conv_block(inputs, FILTER_SIZE, FILTER_NUM, dropout_rate, batch_norm)
pool_64 = layers.MaxPooling2D(pool_size=(2,2))(conv_128)
# DownRes 2
conv_64 = conv_block(pool_64, FILTER_SIZE, 2*FILTER_NUM, dropout_rate, batch_norm)
pool_32 = layers.MaxPooling2D(pool_size=(2,2))(conv_64)
# DownRes 3
conv_32 = conv_block(pool_32, FILTER_SIZE, 4*FILTER_NUM, dropout_rate, batch_norm)
pool_16 = layers.MaxPooling2D(pool_size=(2,2))(conv_32)
# DownRes 4
conv_16 = conv_block(pool_16, FILTER_SIZE, 8*FILTER_NUM, dropout_rate, batch_norm)
pool_8 = layers.MaxPooling2D(pool_size=(2,2))(conv_16)
# DownRes 5, convolution only
conv_8 = conv_block(pool_8, FILTER_SIZE, 16*FILTER_NUM, dropout_rate, batch_norm)
# Upsampling layers
up_16 = layers.UpSampling2D(size=(UP_SAMP_SIZE, UP_SAMP_SIZE), data_format="channels_last")(conv_8)
up_16 = layers.concatenate([up_16, conv_16], axis=3)
up_conv_16 = conv_block(up_16, FILTER_SIZE, 8*FILTER_NUM, dropout_rate, batch_norm)
# UpRes 7
up_32 = layers.UpSampling2D(size=(UP_SAMP_SIZE, UP_SAMP_SIZE), data_format="channels_last")(up_conv_16)
up_32 = layers.concatenate([up_32, conv_32], axis=3)
up_conv_32 = conv_block(up_32, FILTER_SIZE, 4*FILTER_NUM, dropout_rate, batch_norm)
# UpRes 8
up_64 = layers.UpSampling2D(size=(UP_SAMP_SIZE, UP_SAMP_SIZE), data_format="channels_last")(up_conv_32)
up_64 = layers.concatenate([up_64, conv_64], axis=3)
up_conv_64 = conv_block(up_64, FILTER_SIZE, 2*FILTER_NUM, dropout_rate, batch_norm)
# UpRes 9
up_128 = layers.UpSampling2D(size=(UP_SAMP_SIZE, UP_SAMP_SIZE), data_format="channels_last")(up_conv_64)
up_128 = layers.concatenate([up_128, conv_128], axis=3)
up_conv_128 = conv_block(up_128, FILTER_SIZE, FILTER_NUM, dropout_rate, batch_norm)
# 1*1 convolutional layers
conv_final = layers.Conv2D(NUM_CLASSES, kernel_size=(1,1))(up_conv_128)
conv_final = layers.BatchNormalization(axis=3)(conv_final)
conv_final = layers.Activation('linear')(conv_final) #Change to softmax for multichannel
# linear
# Model
model = models.Model(inputs, conv_final, name="UNet")
print(model.summary())
return model
总结
从编程的角度介绍了卷积和池化的作用,可以更好的帮助理解深度学习模型。当然,对于背后的数学公式并没有过多的介绍,可以从之前的笔记中进行复习。
★https://lmb.informatik.uni-freiburg.de/people/ronneber/u-net/
https://arxiv.org/pdf/1505.04597
http://vision.stanford.edu/teaching/cs231n/
给我点在看的人
越来越好看