日期:2020年04月23日    0

目录:

语义分割网络

研究语义分割,源于现在的文字检测和识别算法,为了提高正确率,要变态地去区分到像素级别,比如在EAST、PSENET、TextScanner等算法当中,都应用到语义分割网络。而语义分割网络,作为目标识别的重要算法,也确实非常重要,之前虽然看过,但是,不自己撸一篇文章,还是很容易遗忘很多细节,所以,写下此篇,把一些细节总结下来,他日可以快速回忆起来。

目前主要的语义分割网络,有很多种,如FCN、UNet、SegNet、DeepLab、RefineNet、PSPNet等等,我这里仅仅研究了FCN和UNet,其他的有时间再去看了。

作为一个工程党,我不会去研究论文细节和原理本质,而是更关注实现的细节。对这个领域感兴趣的同学,可以看一下这篇知乎上的综述,快速了解一下这个领域。

FCN(Full Convolutional Network) 全卷积网络

先贴论文地址:Fully Convolutional Networks for Semantic Segmentation

名字由来:这名很容易和FCN(Full Connection Network)- 全连接网络混淆啊。其实,全卷积网络就是为了避免全连接网络的弊端(比如必须固定尺寸,比如全连接层参数过度哦,不得不用dropout这类手段防止过拟合啥的),改进成,所有的位置都使用卷积网络,避免使用全链接。这名就是这么来的。他不像全连接那样,把图像全部拉平,所以,还可以保持图像的空间结构。

全卷积网络三大件:

  • 全卷积化(Convolutionalization):跟之前差不多,但是去掉了全连接层
  • 反卷积(Deconvolution):用在把小的feature map上采样上去
  • 跳层结构(Skip Layer):

反卷积

原始经过卷基层,如VGG或者ResNet后,变成了1/32 x 1/32大小,缩小了32倍,我们为了做语义分割,肯定是要把他还原成原图大小,这样才好判断每个像素的分类。

如何把小的feature map变成原图的大小,最简单的办法是①线性插值,或者用②反池化,但是这种过于简单粗暴,还有一个更好一些的办法,就是通过③“反卷积”,需要使用一个的卷积核,来帮着变大。到底如何做呢?看下图:

一图胜千言,如上图,下方是反卷积之前的输入的feature map,阴影是反卷积核,上面深绿色的是反卷积后的结果,因为原图小,所以要给他加padding(白色),这样就引出反卷积的两种方式(上图所示)。第二种更常用。可以读一下知乎上的这篇文章,了解更多细节。

跳层

如果只把最后一层(如何pool5,原图1/32)直接上卷积,妈呀,一下子就把最后的feature map的宽和高,增大了32倍,这会丧失很多细节啊!所以,最好是把之前的卷积层(如pool4,甚至pool3)的反卷积到原图大小,这样好多细节信息就不会都是了,这些信息也揉到一起,这件事就叫做跳层(skip-layer)。

参考

这里用的是VGG为例,pool1到pool5是五个最大池化层,因此图中的pool5层的大小是原图image的1/32(1/251/25),最粗糙的做法就是直接把pool5层进行步长为32的上采样(逆卷积),一步得到跟原图一样大小的概率图(fcn-32s)。但是这样做会丢失掉很多浅层的特征,尤其是浅层特征往往包含跟多的位置信息,所以我们需要把浅层的特征加上来,作者这里做法很简单,就是直接“加”上来,求和操作,也就完成了跳跃融合。也就是先将pool5层进行步长为2的上采样,然后加上pool4层的特征(这里pool4层后面跟了一个改变维度的卷积层,卷积核初始化为0),之后再进行一次步长为16的上采样得到原图大小的概率图即可(fcn-16s)。另外fcn-8s也是同样的做法,至于后面为什么没有fcn-4s、fcn-2s,我认为是因为太浅层的特征实际上不具有泛化性,加上了也没什么用,反而会使效果变差,所以作者也没继续下去了。

上图中$\color{red}{红色}$是反卷积,而$\color{green}{绿色}$是双线性插值。

另外,

  • FCN-16s就是pool4+pool5的信息,柔和到一起
  • FCN-8s就是pool3+pool4+pool5的信息,柔和到了一起

ResNET50的FCN

FCN的backbone可以是任何主流的backbone,常用的ResNet50应该是怎么样的呢?我们来详细说说。

首先,可以看看Resnet50的结构,我们都知道Resnet50就比较深了,为了防止梯度小时,设计了shortcut结构。

好,可以先看看Resnet50的详细结构,参见这篇

不过这个太复杂了,我们找个简化版本的:

我们最关心的是他什么时候feature map缩小一倍,如图所示,我标识到了上图中。

为什么是这些位置了,具体可以参考这篇

	#conv1 ->1/2 , 1/4
	x = Conv2d_BN(x, nb_filter=64, kernel_size=(7, 7), strides=(2, 2), padding='valid')
	x = MaxPooling2D(pool_size=(3, 3), strides=(2, 2), padding='same')(x)

	#conv3_x ->1/8
	x = identity_Block(x, nb_filter=128, kernel_size=(3, 3), strides=(2, 2), with_conv_shortcut=True) 

	#conv4_x ->1/16
	x = identity_Block(x, nb_filter=256, kernel_size=(3, 3), strides=(2, 2), with_conv_shortcut=True)
	
	#conv5_x ->1/32
	x = identity_Block(x, nb_filter=512, kernel_size=(3, 3), strides=(2, 2), with_conv_shortcut=True)

Identity_Block,是Resnet设计的一个可重复的卷基层,由3组个卷积、BatchNormal、Relu激活组成,就是上图中中括弧表示的内容。

但是,我们不是直接在它刚刚变成1/2的地方就取出来去做FCN,而是在上图中的逻辑层Conv?_x的输出,也就是最后一个Relu激活函数后输出。另外,我们FCN其实只用到后3层的输出,也就是Conv3_x、Conv4_x、Conv5_x的输出,对应的在一般的pretrain的Resnet50的层的命名中,对应着activate_22,activate_40,activation_49三处。详细位置和标号,可以参见这张图

对于原图尺寸不断缩小一半的地方,有必要再啰嗦两句:参考代码

  • 1/2的时候:line 143:x = Conv2D(64, (7, 7), data_format=IMAGE_ORDERING,strides=(2, 2), name='conv1')(x)
  • 1/4的时候:line 149:x = MaxPooling2D((3, 3), data_format=IMAGE_ORDERING, strides=(2, 2))(x)
  • 1/8的时候:line 151:x = conv_block(x, 3, [128, 128, 512], stage=3, block='a')
  • 1/16的时候:line 162:x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a')
  • 1/32的时候:line 170:x = conv_block(x, 3, [512, 512, 2048], stage=5, block='a')

可以看出,除了第二个(1/4处 line149),剩下的都是靠stride=2的卷积实现的缩小一倍。

反卷积的实现

tensorflow提供了反卷积函数,采用的方式就是上面提到的“第二种”的反卷积方式,即在feature map的像素之间padding零,padding多少个呢?就是strip-1个。

TF的函数tf.nn.conv2d_transpose

传入函数的参数有value,filter,output_shape,strides,padding,data_format和name,最主要的三个参数是value,output_shape和strides。value传入函数是为了确定输入尺寸,output_shape是为了确定输出尺寸,strides就是kernel在vertical和horizon上的步长。

  • value的格式为[batch, height, width, in_channels],height和width是用来计算输出尺寸用到的最重要的两个参数,表示输入该层feature map的高度和宽度,典型的NHWC格式;
  • filter的格式为[height, width, output_channels, input_channels],务必注意这里的channel数是输出的channel数在前,输入的channel数在后。
  • output_shape是一个1-D张量,传入的可以是一个tuple或者list,在不指定data_format参数的情况下,格式必须为NHWC。注意:这里的C要与filter中的output_channels保持一致;
  • strides的格式为一个整数列表,与conv2d方法在官方文档中写的一样,必须保证strides[0]=strides[3]=1,格式为[1, stirde, stride, 1];
  • padding依然只有’SAME’和’VALID’;
  • (与conv2d方法不同的是,这里需要人为指定输出的尺寸,这是为了使用value、output_shape和strides三个参数一起确定反卷积尺寸的正确性,具体在下个部分解释。)

FCN的实现

先贴论文Fully Convolutional Networks for Semantic Segmentation

我分别找了4种实现,其实细节上还是有不少不同的,为了简化分析,我只关注fcn-32s的实现:

  • Keras实现
      o = f5
      o = (Conv2D(4096, (7, 7), activation='relu',padding='same', data_format=IMAGE_ORDERING))(o)
      o = Dropout(0.5)(o)
      o = (Conv2D(4096, (1, 1), activation='relu',padding='same', data_format=IMAGE_ORDERING))(o)
      o = Dropout(0.5)(o)
      o = (Conv2D(n_classes,  (1, 1), kernel_initializer='he_normal',data_format=IMAGE_ORDERING))(o)
      o = Conv2DTranspose(n_classes, kernel_size=(64, 64),  strides=(32, 32), use_bias=False,  data_format=IMAGE_ORDERING)(o)
    

    看!反卷积之前,对原始的feature map做了3次卷积(7x7和1x1和1x1)

  • Tensorflow实现
      conv_final_layer = image_net["conv5_3"]
      pool5 = utils.max_pool_2x2(conv_final_layer)
      W6 = utils.weight_variable([7, 7, 512, 4096], name="W6")
      conv6 = utils.conv2d_basic(pool5, W6, b6)<--------------7x7卷积
      relu6 = tf.nn.relu(conv6, name="relu6")
      W7 = utils.weight_variable([1, 1, 4096, 4096], name="W7")
      conv7 = utils.conv2d_basic(relu6, W7, b7)<---------------1x1卷积
      relu7 = tf.nn.relu(conv7, name="relu7")
      W8 = utils.weight_variable([1, 1, 4096, NUM_OF_CLASSESS], name="W8")
      conv8 = utils.conv2d_basic(relu7, W8, b8)<---------------1x1卷积
      W_t1 = utils.weight_variable([4, 4, deconv_shape1[3].value, NUM_OF_CLASSESS], name="W_t1")
      conv_t1 = utils.conv2d_transpose_strided(conv8, W_t1, b_t1, output_shape=tf.shape(image_net["pool4"]))<----------------反卷积(这个是pool5经过一些列卷加后的反卷积,strip=2)
      fuse_1 = tf.add(conv_t1, image_net["pool4"], name="fuse_1")<----------和pool4融合
    

    这块有个疑问?pool5经过卷积后的conv8,为何要上卷积呢?论文里明明是2 x unsampled,也就是反池化啊?!

    不过,这里还是跟上面的做法一样,反卷积之前,对原始的feature map做了3次卷积(7x7和1x1和1x1)。

  • Caffi的实现,
      n.fc6, n.relu6 = conv_relu(n.pool5, 4096, ks=7, pad=0)
      n.fc7, n.relu7 = conv_relu(n.drop6, 4096, ks=1, pad=0)
      n.score_fr = L.Convolution(n.drop7, num_output=60, kernel_size=1, pad=0,param=[dict(lr_mult=1, decay_mult=1), dict(lr_mult=2, decay_mult=0)])
      n.upscore = L.Deconvolution(n.score_fr,convolution_param=dict(num_output=60, kernel_size=64, stride=32,bias_term=False)
    

    也是7x7卷积,2个1x1卷积后,才做反卷积

  • Pytorch实现
          self.pool5 = nn.MaxPool2d(2, stride=2, ceil_mode=True)  # 1/32
          self.fc6 = nn.Conv2d(512, 4096, 7)<----7x7卷积
          self.relu6 = nn.ReLU(inplace=True)
          self.fc7 = nn.Conv2d(4096, 4096, 1)<----1x1卷积
          self.relu7 = nn.ReLU(inplace=True)
          self.score_fr = nn.Conv2d(4096, n_class, 1)<----1x1卷积
          self.upscore = nn.ConvTranspose2d(n_class, n_class, 64, stride=32,bias=False)    
    

    一样一样,都是7x7,1x1,1x1三个卷积。

我查看了原始论文中,并没有这样的一个固定的结构。

参考

UNet

UNet就是对FCN的改进,

FPN(特征金字塔网络)

FCN

FCN、UNet、FPN的关系和区别

FCN是鼻祖,2014.11年就出来了,2015年3最终稿,它是开山之作。为了解决XXX问题。

UNet是2015.5发表,FPN都是2016.12发表,终稿是2017.3的。他俩都是站在FCN的基础之上。

区别: 引自知乎参考链接

  • FPN出自detection任务;U-Net出自segmentation任务
  • FPN的“放大”部分是直接插值放大的,没有deconvolution的filters学习参数;U-Net“放大”部分就是Decoder,需要deconvolution的filters学习参数的。
  • FPN及其大多数改进都是把原Feature Map和FPN的Feature Map做加法;U-Net及其大多数改进都是把原Feature Map和Decoder的Feature Map做Concatiantion,再做1x1卷积。
  • FPN对每一个融合的层都做detection;U-Net 只在最后一层做segmentation的pixel预测。


--