如语义分割之类的像素级别的分类任务已经有了很多的发展,现有的模型都在朝着越来越高的精度发展。但是这样的任务对于嵌入式设备也有着重要的应用,所以做到实时语义分割就是必不可少的环节,但是大多数模型都是朝着高精度去的,在实时性上非常的差。基于实时语义分割的问题,作者提出了一种轻量级的网络结构,有着非常少的参数量可以快速进行语义分割,并且在性能上并不会损失的太多。本文作者是Abhishek Chaurasia, Sangpil Kim, Eugenio Culurciello
原论文
首先先给出ENet的整个网络的结构,论文中是以$512\times512$的图片作为的输入
可以从图中看出网络也采用了类似U型的结构,只不过是非对称的,下采样有5个阶段,下采样了8倍,上采样只有2个阶段。
整个网络核心的就是bottleneck与第一步的initial结构。
上图(a)就是Initial结构,把原图分别用步长卷积和最大池化下采样,再拼接起来进行卷积可以更好的学习得到2倍下采样的结果。同时一般网络结构并不会上来直接进行下采样而是要先进行卷积,但是这样会使得网络更大且有冗余信息,经过实验,在第一层进行下采样不会对最后的分类结果产生影响。
上图(b)就是网络中最为核心的结构,借鉴了ResNet的思想,称之为bottleneck就是因为主分支上先通过$1\times1$卷积减少了通道数,最后再恢复到输出通道数,是一种中间小两边大的结构,就像瓶颈一样,下面介绍下采样的bottleneck的细节。
主分支:
分支:
以上的每个卷积操作后面都紧接BatchNormalization层和PReLU激活函数(作者使用此激活函数而不是ReLU的理由再后面会阐述)。对于$1\times1$卷积,作者都去掉了bias。
上采样类型的bottleneck结构完全相同的只不过把下采样操作换成上采样操作(可以用插值或者转置卷积)。
下采样有两个缺点:
于是ENet采用了类似SegNet的方法进行上采样,可以减少内存需求量。同时强力的下采样会损失精度,于是我们尽可能的限制下采样。
但与此同时下采样也带来一定的好处,那就是可以获取更大的感受野,有利于分辨不同类别的物体。于是为了增大感受野,我们在一些卷积中使用了空洞卷积。
处理高分辨率的图像会消耗大量的计算资源,为了减少模型的复杂程度,我们在第一步就对图像进行了下采样操作。这样做是考虑到视觉信息在空间上是高度冗余的。我们认为最初的网络层更主要的是对特征进行提取,它并不直接有助于分类,因此过早下采样不会产生影响。最终,这样的思想也在实验中得到了很好的验证。
相比于SegNet中编码器和解码器的完全对称,ENet则用了一个较大的编码器和较小的解码器。作者认为Encoder主要进行信息处理和过滤,和流行的分类模型相似。而decoder主要是对encoder的输出做上采样,对细节做细微调整(我认为可能作者也是为了减少模型参数量,毕竟更多的解码器还是有助于信息的恢复的)。
通常在卷积层之前做ReLU和Batch Norm效果会更好,但是在ENet上使用ReLU却降低了精度。相反,我们发现删除网络初始层中的大多数ReLU可以改善结果。所以最终使用了PReLU来代替ReLU,它带来了一个新参数,目的在于学习负数区域的斜率。
上图展示了PReLU中的参数的变化情况,蓝色的线代表均值,灰色的区域上界代表最大值,下界代表最小值。如果参数为0,则说明用ReLU函数更可取。开始的initial部分方差比较大,波动很剧烈。同时也可以看出在bottleneck部分,参数值更趋向于负值(即中间向下波动的部分),这也解释了为什么ReLU函数不能很好工作的原因。作者认为在bottleneck中ReLU不好用的原因是网络层数太浅,在ResNet中是有数百层的网络的。值得注意的是,解码器的权重变得偏向正数,学习到的函数功能更接近identity。这证实了我们的直觉,即解码器只用于微调上采样输出.
如前所述,尽早对输入进行降采样十分有必要,但过于激进的降维也会阻碍信息的流动。在Inception V3中提出了解决这一问题的一种非常好的办法。他们认为VGG使用的池化再卷积的扩展维数方式,尽管并不是十分明显,但却引入了代表性瓶颈(导致需要使用更多的filters,降低了计算效率)。
另一方面,卷积后的拼接增加了特征映射的深度(increases feature map depth),消耗了大量计算量。因此,正如在上文中所建议的,我们选择在使用步长2的卷积的同时并行执行池化操作,并将得到的特征图拼接(concatenate)起来。这种技术使我们可以将初始块的推理时间提高10倍。
此外,我们在原始ResNet架构中发现了一个问题。下采样时,卷积分支中的第一个1×1卷积在两个维度上以2的步长滑动,直接丢弃了75%的输入。
ENet将卷积核的大小增加到了2×2,这样可以让整个输入都参与下采样,从而提高信息流和精度。虽然这使得这些层的计算成本增加了4倍,但是在ENET中这些层的数量很少,开销并不明显。
卷积权重具有相当大的冗余度,并且每个n×n卷积可以被分解为彼此相继的两个较小的卷积,一个n×1和一个1×n,称为非对称卷积。我们在网络中使用了n= 5的非对称卷积,这两个操作的计算成本类似于单个3×3的卷积。增加了模块的学习功能并增加了感受野。
更重要的是,bottleneck模块中使用的一系列操作(投影,卷积,投影)可以看作是将一个大的卷积层分解为一系列更小更简单的操作,即它的低秩近似。这种因子分解大大的加速了计算速度,并减少了参数的数量,使它们更少冗余。此外,由于在层之间插入的非线性操作,功能也变的更丰富了。
空洞卷积,可以增大感受野并更好的进行分类任务。
大多数语义分割数据集图像数量较少,为了防止出现过拟合的现象,加入了正则化层。最开始使用了L2正则化发现效果并不好,最终选取了空间dropout层。
作者实验部分可以参看原论文,可以看出相比其它一些大型网络,fps有非常显著的提升,分类精度的下降也可以接受,只在一些边缘地方能看出来比较大的瑕疵,下面就是论文中的一张实验结果对比图。
能看在边缘以及小物体的分类上有比较多的瑕疵。
实验使用的tensorflow2.0,在google colab上跑了此数据集。
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
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
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
最终的训练结果在训练集上到达91%的准确率,相比大型的网络模型要低不少,在小物体和边缘方面存在不精确的地方,肉眼就能看得出。但是它的推理时间则快了很多。训练一个epoch的时间仅有大型网络的$\frac{1}{3}-\frac{1}{7}$.