TensorFlow教程

论文阅读:ENet

本文主要是介绍论文阅读:ENet,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

引言

如语义分割之类的像素级别的分类任务已经有了很多的发展,现有的模型都在朝着越来越高的精度发展。但是这样的任务对于嵌入式设备也有着重要的应用,所以做到实时语义分割就是必不可少的环节,但是大多数模型都是朝着高精度去的,在实时性上非常的差。基于实时语义分割的问题,作者提出了一种轻量级的网络结构,有着非常少的参数量可以快速进行语义分割,并且在性能上并不会损失的太多。本文作者是Abhishek Chaurasia, Sangpil Kim, Eugenio Culurciello

原论文

网络结构介绍

首先先给出ENet的整个网络的结构,论文中是以$512\times512$的图片作为的输入YlW9II.png

可以从图中看出网络也采用了类似U型的结构,只不过是非对称的,下采样有5个阶段,下采样了8倍,上采样只有2个阶段。

整个网络核心的就是bottleneck与第一步的initial结构。YlfY1f.png

上图(a)就是Initial结构,把原图分别用步长卷积和最大池化下采样,再拼接起来进行卷积可以更好的学习得到2倍下采样的结果。同时一般网络结构并不会上来直接进行下采样而是要先进行卷积,但是这样会使得网络更大且有冗余信息,经过实验,在第一层进行下采样不会对最后的分类结果产生影响。

上图(b)就是网络中最为核心的结构,借鉴了ResNet的思想,称之为bottleneck就是因为主分支上先通过$1\times1$卷积减少了通道数,最后再恢复到输出通道数,是一种中间小两边大的结构,就像瓶颈一样,下面介绍下采样的bottleneck的细节。

主分支:

  • 先通过$1\times1$卷积进行投影并减少通道数,如果是下采样类型的bottleneck,可在此使用带步长的$1\times1$卷积。
  • 利用一层conv卷积层让模型有更强的学习力,这层conv有不同类型,通过之前网络架构图可以看出。type那一列标注有dilated的就是进行空洞卷积以获取更大的感受野,标注有asymmetric的先进行$1\times5$卷积再进行$5\times1$卷积,这有助于减少参数量,没有标注的就进行普通的$3\times3$卷积。
  • 利用$1\times1$卷积进行投影输出特定通道数的特征图。
  • 为了防止模型出现过拟合现象,再添加一层空间dropout层,在bottleneck2.0之前dropout参数为0.01,之后为0.1。

分支:

  • 如果bottleneck不是下采样的,则此分支不作任何操作直接与主分支跳跃连接,即与主分支相加。
  • 如果是下采样的则先通过最大池化下采样,再利用padding填充至于主分支特征图大小一致,最后进行相加操作。

以上的每个卷积操作后面都紧接BatchNormalization层和PReLU激活函数(作者使用此激活函数而不是ReLU的理由再后面会阐述)。对于$1\times1$卷积,作者都去掉了bias。

上采样类型的bottleneck结构完全相同的只不过把下采样操作换成上采样操作(可以用插值或者转置卷积)。

网络结构设计中的选择

Feature map resolution

下采样有两个缺点:

  • 下采样降低了特征图的分辨率,只保留了主要特征,这意味着丢失了一些空间的信息,例如边缘信息这样的细节信息。
  • 全像素的语义分割要求输出图的分辨率与输入图一样,这就要求上采样模块必须十分强大才能很好的还原图像的信息,这意味更大的模型和计算量。

于是ENet采用了类似SegNet的方法进行上采样,可以减少内存需求量。同时强力的下采样会损失精度,于是我们尽可能的限制下采样。

但与此同时下采样也带来一定的好处,那就是可以获取更大的感受野,有利于分辨不同类别的物体。于是为了增大感受野,我们在一些卷积中使用了空洞卷积。

Early downsampling

处理高分辨率的图像会消耗大量的计算资源,为了减少模型的复杂程度,我们在第一步就对图像进行了下采样操作。这样做是考虑到视觉信息在空间上是高度冗余的。我们认为最初的网络层更主要的是对特征进行提取,它并不直接有助于分类,因此过早下采样不会产生影响。最终,这样的思想也在实验中得到了很好的验证。

Decoder size

相比于SegNet中编码器和解码器的完全对称,ENet则用了一个较大的编码器和较小的解码器。作者认为Encoder主要进行信息处理和过滤,和流行的分类模型相似。而decoder主要是对encoder的输出做上采样,对细节做细微调整(我认为可能作者也是为了减少模型参数量,毕竟更多的解码器还是有助于信息的恢复的)。

Nonlinear operations

通常在卷积层之前做ReLU和Batch Norm效果会更好,但是在ENet上使用ReLU却降低了精度。相反,我们发现删除网络初始层中的大多数ReLU可以改善结果。所以最终使用了PReLU来代替ReLU,它带来了一个新参数,目的在于学习负数区域的斜率。Ylbe39.png

上图展示了PReLU中的参数的变化情况,蓝色的线代表均值,灰色的区域上界代表最大值,下界代表最小值。如果参数为0,则说明用ReLU函数更可取。开始的initial部分方差比较大,波动很剧烈。同时也可以看出在bottleneck部分,参数值更趋向于负值(即中间向下波动的部分),这也解释了为什么ReLU函数不能很好工作的原因。作者认为在bottleneck中ReLU不好用的原因是网络层数太浅,在ResNet中是有数百层的网络的。值得注意的是,解码器的权重变得偏向正数,学习到的函数功能更接近identity。这证实了我们的直觉,即解码器只用于微调上采样输出.

Information-preserving dimensionality changes

如前所述,尽早对输入进行降采样十分有必要,但过于激进的降维也会阻碍信息的流动。在Inception V3中提出了解决这一问题的一种非常好的办法。他们认为VGG使用的池化再卷积的扩展维数方式,尽管并不是十分明显,但却引入了代表性瓶颈(导致需要使用更多的filters,降低了计算效率)。
另一方面,卷积后的拼接增加了特征映射的深度(increases feature map depth),消耗了大量计算量。因此,正如在上文中所建议的,我们选择在使用步长2的卷积的同时并行执行池化操作,并将得到的特征图拼接(concatenate)起来。这种技术使我们可以将初始块的推理时间提高10倍。

此外,我们在原始ResNet架构中发现了一个问题。下采样时,卷积分支中的第一个1×1卷积在两个维度上以2的步长滑动,直接丢弃了75%的输入。

ENet将卷积核的大小增加到了2×2,这样可以让整个输入都参与下采样,从而提高信息流和精度。虽然这使得这些层的计算成本增加了4倍,但是在ENET中这些层的数量很少,开销并不明显。

Factorizing filters

卷积权重具有相当大的冗余度,并且每个n×n卷积可以被分解为彼此相继的两个较小的卷积,一个n×1和一个1×n,称为非对称卷积。我们在网络中使用了n= 5的非对称卷积,这两个操作的计算成本类似于单个3×3的卷积。增加了模块的学习功能并增加了感受野。

更重要的是,bottleneck模块中使用的一系列操作(投影,卷积,投影)可以看作是将一个大的卷积层分解为一系列更小更简单的操作,即它的低秩近似。这种因子分解大大的加速了计算速度,并减少了参数的数量,使它们更少冗余。此外,由于在层之间插入的非线性操作,功能也变的更丰富了。

Dilated convolutions

空洞卷积,可以增大感受野并更好的进行分类任务。

Regularization

大多数语义分割数据集图像数量较少,为了防止出现过拟合的现象,加入了正则化层。最开始使用了L2正则化发现效果并不好,最终选取了空间dropout层。

实验

作者实验部分可以参看原论文,可以看出相比其它一些大型网络,fps有非常显著的提升,分类精度的下降也可以接受,只在一些边缘地方能看出来比较大的瑕疵,下面就是论文中的一张实验结果对比图。

YlOdD1.png

能看在边缘以及小物体的分类上有比较多的瑕疵。

我的实验

实验使用的tensorflow2.0,在google colab上跑了此数据集。

下采样bottleneck

def bottleneck(x,output,s,methods,dropout_rate=0.1,scale=4,asymmetric=5,d_rate=5):
  temp = output//scale
  
  x_residual = x
  x_residual = Conv2D(temp,(1,1),(s,s),padding='same',use_bias=False)(x_residual)
  x_residual = BatchNormalization()(x_residual)
  x_residual = PReLU(shared_axes=[1,2])(x_residual)

  if methods == 'norm':
    x_residual = Conv2D(temp,(3,3),padding='same')(x_residual)
  elif methods == 'dilated':
    x_residual = Conv2D(temp,(3,3),padding='same',dilation_rate=d_rate)(x_residual)
  elif methods == 'asymmetric':
    x_residual = Conv2D(temp,(1,asymmetric),padding='same',dilation_rate=d_rate)(x_residual)
    x_residual = Conv2D(temp,(asymmetric,1),padding='same',dilation_rate=d_rate)(x_residual)
  x_residual = BatchNormalization()(x_residual)
  x_residual = PReLU(shared_axes=[1,2])(x_residual)

  x_residual = Conv2D(output,(1,1),use_bias=False)(x_residual)
  x_residual = SpatialDropout2D(rate=dropout_rate)(x_residual)

  if s == 2:
    x = MaxPool2D(padding='same')(x)
  x = Conv2D(output,(1,1),use_bias=False)(x)
  x = Add()([x,x_residual])
  x = BatchNormalization()(x)
  x = PReLU(shared_axes=[1,2])(x)

  return x

上采样bottle

def de_bottleneck(x,output,s,dropout_rate=0.1,scale=4):
  temp = output//scale

  x_residual = x
  x_residual = Conv2D(temp,(1,1),padding='same',use_bias=False)(x_residual)
  x_residual = BatchNormalization()(x_residual)
  x_residual = PReLU(shared_axes=[1,2])(x_residual)

  if s == 2:
    x_residual = Conv2DTranspose(temp,(3,3),(s,s),padding='same')(x_residual)
  else:
    x_residual = Conv2D(temp,(3,3),padding='same')(x_residual)
  x_residual = BatchNormalization()(x_residual)
  x_residual = PReLU(shared_axes=[1,2])(x_residual)

  x_residual = Conv2D(output,(1,1),use_bias=False)(x_residual)
  x_residual = SpatialDropout2D(rate=dropout_rate)(x_residual)

  if s == 2:
    x = UpSampling2D((s,s),interpolation='bilinear')(x)
  x = Conv2D(output,(1,1),use_bias=False)(x)
  x = Add()([x,x_residual])
  x = BatchNormalization()(x)
  x = PReLU(shared_axes=[1,2])(x)

  return x

initial

def initial(x):
  x_1 = x
  x = MaxPool2D(padding='same')(x)
  x_1 = Conv2D(13,(3,3),(2,2),padding='same')(x_1)

  x = Concatenate()([x,x_1])
  return x

主体网络结构

def ENet(input_shape,n_class):
  x_input = Input(input_shape)

  #encoder
  x_1 = initial(x_input)
  x = bottleneck(x_1,64,2,'norm',dropout_rate=0.01)
  x = bottleneck(x,64,1,'norm',dropout_rate=0.01)
  x = bottleneck(x,64,1,'norm',dropout_rate=0.01)
  x = bottleneck(x,64,1,'norm',dropout_rate=0.01)
  x_2 = bottleneck(x,64,1,'norm',dropout_rate=0.01)
  
  x = bottleneck(x_2,128,2,'norm')
  x = bottleneck(x,128,1,'norm')
  x = bottleneck(x,128,1,'dilated',d_rate=2)
  x = bottleneck(x,128,1,'asymmetric')
  x = bottleneck(x,128,1,'dilated',d_rate=4)
  x = bottleneck(x,128,1,'norm')
  x = bottleneck(x,128,1,'dilated',d_rate=8)
  x = bottleneck(x,128,1,'asymmetric')
  x = bottleneck(x,128,1,'dilated',d_rate=16)

  x = bottleneck(x,128,1,'norm')
  x = bottleneck(x,128,1,'dilated',d_rate=2)
  x = bottleneck(x,128,1,'asymmetric')
  x = bottleneck(x,128,1,'dilated',d_rate=4)
  x = bottleneck(x,128,1,'norm')
  x = bottleneck(x,128,1,'dilated',d_rate=8)
  x = bottleneck(x,128,1,'asymmetric')
  x = bottleneck(x,128,1,'dilated',d_rate=16)

  #decoder
  x = de_bottleneck(x,64,2)
  x = Concatenate()([x,x_2])
  x = Conv2D(64,(3,3),padding='same')(x)
  x = BatchNormalization()(x)
  x = PReLU(shared_axes=[1,2])(x)
  x = de_bottleneck(x,64,1)
  x = de_bottleneck(x,64,1)

  x = de_bottleneck(x,16,2)
  x = Concatenate()([x,x_1])
  x = Conv2D(16,(3,3),padding='same')(x)
  x = BatchNormalization()(x)
  x = PReLU(shared_axes=[1,2])(x)
  x = de_bottleneck(x,16,1)

  x = Conv2DTranspose(n_class,(4,4),(2,2),padding='same')(x)
  x = Activation('softmax')(x)

  model = keras.Model(x_input,x)
  return model

结果图

YljlfU.png

最终的训练结果在训练集上到达91%的准确率,相比大型的网络模型要低不少,在小物体和边缘方面存在不精确的地方,肉眼就能看得出。但是它的推理时间则快了很多。训练一个epoch的时间仅有大型网络的$\frac{1}{3}-\frac{1}{7}$.

这篇关于论文阅读:ENet的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!