随着深度神经网络在性能上的不断突破,模型本身的深度和宽度也逐渐扩展,运行时的时间复杂度和空间复杂度等指标也呈现出了爆炸式的增长。因而,深度神经网络的运行对计算平台的处理、存储能力提出了更高要求,加大了模型在资源受限的移动或嵌入式设备上的部署难度,特别以资源约束极端有限的微控制器为代表。这样的背景下,剪枝、量化等模型压缩和加速技术及轻量化的神经网络架构设计为上述挑战带来了可能的解决方案。此外,这些模型压缩技术往往利用“云-边协同”的方式在“重训练 → 再部署”的情况下进行,即在云端对模型压缩技术进行实现,再将轻量化模型部署到资源受限终端上。而这也就带来了一定的问题:模型需要在云端付出“重训练”的时间代价,不能在终端运行时快速地进行用户需求或资源自适的动态计算(比如当前应用在当前时刻只需要部分量级的运算,进行轻量化的推理)。
为了实现上述“云-边协同”的运行时自适应计算,在商用网络上进行“可插拔”的轻量级技术,“动态可加载的轻量级压缩算子”带来了可能。其本质是:利用“参数共享 + 参数分离”的思想,在商用网络上实现不同轻量级压缩算子的装载插拔设计,实现动态运行时可调优的模型压缩。当前主流的轻量级压缩算子包含:
(1) Bottleneck压缩算子
Bottleneck(沙漏型结构)作为早期轻量化网络模型中最为出名的构建块思想,在Inception、ResNet等网络中均有体现。其将1层传统卷积操作替换成了3层卷积,同时实现了模块轻量化的目标。以传统的3*3*256的卷积操作为例,Bottleneck即是将该卷积按顺序替换成1*1*num、3*3*num、1*1*256的卷积运算,其中num可设置为任意小于256(输出维度)的正整数,两个1*1核大小的卷积运算则分别用于降低和升高特征维度。通过这样的结构,模型的深度被加深,但参数量和计算量却同时降低了。
def Bottleneck_block(inputs, filters, alpha, beta, block_id, strides=(1, 1)):
filters = int(filters * alpha)
filters1 = int(filters * beta)
x = Conv2D(filters1, (1, 1), padding='same', use_bias=False, strides=(1, 1), name='conv%d_inception1_pw1' % block_id)(inputs)
x = Conv2D(filters1, (3, 3), padding='same', use_bias=False, strides=(1, 1), name='conv%d_inception1' % block_id)(x)
x = Conv2D(filters, (1, 1), padding='same', use_bias=False, strides=(1, 1), name='conv%d_inception1_pw2' % block_id)(x)
x = BatchNormalization(axis=-1, name='conv%d_inception1_bn' % block_id)(x)
return Activation('relu', name='conv%d_inception1_relu' % block_id)(x)
(2)低秩分解压缩算子
低秩分解结构块(例如Svd-Based)则利用了一种低秩近似的技术,将传统卷积操作进行不对称二分量分解、三分量分解或四分量分解。本课题所使用的是一种二分量分解方法,如将传统3*3*256的卷积运算顺序分解成1*3*num和3*1*256的两层卷积,加大模型深度的同时也降低了块的参数量和计算量。
def svd_block(inputs, filters, alpha, beta, block_id, kernel, strides=(1, 1)):
filters = int(filters * alpha)
filters1 = int(filters * beta) # beta为svd中间卷积核缩放比例,可设为1/2和1/4等
x = Conv2D(filters1, (1, kernel), padding='same', use_bias=False, strides=strides, name='conv%d_svd1' % block_id)(inputs)
x = Conv2D(filters, (kernel, 1), padding='same', use_bias=False, strides=strides, name='conv%d_svd2' % block_id)(x)
x = BatchNormalization(axis= -1, name='conv%d_svd_bn' % block_id)(x)
return Activation('relu', name='conv%d_svd_relu' % block_id)(x)
(3)多分支并行压缩算子
多分支并行结构块起源于Inception系列模型。与(1)和(2)的结构块不同,其认为通过加深深度但是减小中间输出维度的方式可能会造成信息损失。为了避免这样的情况,多分支并行结构从模型宽度层面出发在某一层采用了多组卷积运算,最后通过合并将多组卷积运算的结果进行整合。例如,将传统3*3*256的卷积运算替换成两组1*3*128和3*1*128的卷积,并在最后将两组卷积的输出进行合并(Concate-nation)。通过这样的方式,结构块的参数量和计算量也达到了减小的目的。
def inception_block(inputs, filters, alpha, block_id, kernel, strides=(1, 1)):
filters = int(filters * alpha)
filters1 = int(filters * 0.5)
son1 = Conv2D(filters1, (1, kernel), padding='same', use_bias=False, strides=strides, name='conv%d_inception2_son1' % block_id)(inputs)
son2 = Conv2D(filters1, (kernel, 1), padding='same', use_bias=False, strides=strides, name='conv%d_inception2_son2' % block_id)(inputs)
merge = Concatenate(axis=3)([son1, son2])
x = BatchNormalization(axis=-1, name='conv%d_inception2_bn' % block_id)(merge)
return Activation('relu', name='conv%d_inception2_relu' % block_id)(x)
(4)Fire压缩算子
Fire压缩算子诞生于SqueezeNet——于2016年发布的最早期的轻量化网络模型之一。作为此篇工作的核心创新之一,“Fire”利用一种“Squeeze-Expand”的思想,本质类似(1)Bottleneck结构块和(3)多分支并行结构快的结合体。以传统3*3*256的卷积运算为例,Fire结构块首先通过Squeeze操作——使用1*1*num的卷积对输入特征进行降维(num一般取值小于输出维度),再使用Expand操作——通过1*1*e1与3*3*e2两种卷积对Squeeze操作后的输出特征再进行运算并最后对两种卷积的输出进行合并,即整个Fire块的输出维度等于e1 + e2。本课题采用了两种Fire结构块的配置,即将num设置为整个结构块预定输出维度的1/8和1/4(上例中分别为32和64),同时e1与e2设置为相同大小(上例中为128)。
def fire_block(input, filters, alpha, block_id):
filters = int(filters/8 * alpha)
fire1_squeeze = Conv2D(filters, (1, 1), activation='relu', kernel_initializer='glorot_uniform', name='conv%d_fire_squeeze' % block_id, data_format="channels_last")(input)
fire1_expand1 = Conv2D(filters * 4, (1, 1), activation='relu', kernel_initializer='glorot_uniform', padding='same', name='conv%d_fire_expand1' % block_id, data_format="channels_last")(fire1_squeeze)
fire1_expand2 = Conv2D(filters * 4, (3, 3), activation='relu', kernel_initializer='glorot_uniform', padding='same', name='conv%d_fire_expand2' % block_id, data_format="channels_last")(fire1_squeeze)
merge = Concatenate(axis=3)([fire1_expand1, fire1_expand2])
maxpool = MaxPooling2D(pool_size=(2, 2), strides=(2, 2), name='conv%d_fire_maxpool' % block_id, data_format="channels_last")(merge)
return maxpool
(5)深度可分离压缩算子
深度可分离卷积由Google的MobileNet首次提出。类似Fire模块,深度可分离卷积也采用了两段式的卷积运算模式——深度卷积(Depthwise Convolution)和逐点卷积(Pointwise Convolution)。与前述四种压缩算子都不相同的是,其中深度卷积的操作并非传统的卷积运算——深度卷积只针对于单个通道(输入特征的一个维度)进行卷积,而不像标准CNN对所有输入通道进行卷积。因此,深度卷积可以理解为卷积核维度只能为1的卷积操作,且卷积核数量只能等于输入特征的维度。逐点卷积则是1*1*整个块输出维度的传统卷积,作用是对深度卷积操作后的特征维度进行调整(升降维度),类似Bottleneck。深度可分离卷积的命名正是来自于其能够将卷积对特征深度、宽度的操作分离开来。相比于具有同样输入和输出特征的传统卷积,深度可分离卷积大大降低了卷积块的参数量和计算量。以输入为7*7*3,输出为5*5*128的某相邻输出特征为例,传统的核大小为3*3、步长为1的卷积操作的计算量为128*(5*5)*(3*3*3) = 86400,参数量为128*(3*3*3) = 3456;而深度可分离卷积的计算量仅为3*(5*5)*(3*3*1) + 128*(5*5)*(1*1*3) = 10275,参数量仅为3*(3*3*1) + 128*(1*1*3) = 411。
def _depthwise_conv_block(inputs, pointwise_conv_filters, alpha, depth_multiplier=1, strides=(1, 1), block_id=1):
pointwise_conv_filters = int(pointwise_conv_filters * alpha)
x = DepthwiseConv2D((3, 3), padding='same', depth_multiplier=depth_multiplier, strides=strides, use_bias=True, name='conv%d_dw' % block_id)(inputs)
x = Conv2D(pointwise_conv_filters, (1, 1), padding='same', use_bias=False, strides=(1, 1), name='conv%d_pw' % block_id)(x)
x = BatchNormalization(axis=-1, name='conv%d_pw_bn' % block_id)(x)
return Activation('relu', name='conv%d_pw_relu' % block_id)(x)
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!