深度学习网络模型的轻量化方法

深度学习网络的轻量化

由于大部分的深度神经网络模型的参数量很大,无法满足直接部署到移动端的条件,因此在不严重影响模型性能的前提下对模型进行压缩加速,来减少网络参数量和计算复杂度,提升运算能力。

一、深度可分离卷积

了解深度可分离卷积之前,我们先看一下常规的卷积操作:对于一张 \(3 \times 16 \times 16\) 的图像,如果采用 \(3\times3\) 的卷积核,输出 \(32 \times 16 \times 16\) 的feature map,则所需要的参数量为:

\[3 \times 3 \times3 \times 32 = 864 \]

常规卷积中每一个卷积核对输入的所有通道进行卷积,如下图所示:

深度学习网络模型的轻量化方法

与常规卷积不同,深度可分离卷积 (depthwise separable convolution) 分为两个部分,分为逐通道卷积 (depthwise) 和逐点卷积 (pointwise) 。

1.1 逐通道卷积

depthwise中,每一个卷积核只对一个通道进行卷积,如下图所示:

深度学习网络模型的轻量化方法

于是,还是对于一个 \(3 \times 16 \times 16\) 的图像来说,通过一个 \(3 \times 3\) 的卷积,其输出feature map 的维度为 \(3 \times 16 \times 16\),所用到的卷积核的参数为:

\[3 \times 3 \times 3 = 27 \]

Depthwise Convolution完成后的Feature map数量与输入层的通道数相同,无法扩展Feature map。而且这种运算对输入层的每个通道独立进行卷积运算,没有有效的利用不同通道在相同空间位置上的feature信息。因此需要Pointwise Convolution来将这些Feature map进行组合生成新的Feature map。

1.2 逐点卷积

pointconvolution的运算类似于 \(1\times1\) 卷积,对DW得到的feature map升维,在考虑到空间特征的同时,将维度变换到我们所期望的大小。

深度学习网络模型的轻量化方法

此时,如果需要输出 \(32 \times 16 \times 16\) 的feature map,那么需要的 \(1 \times 1\) 的卷积核的个数为32个,此时的参数量为:

\[1 \times 1 \times 3 \times 32 = 96 \]

所以,综合两个过程考虑,采用深度可分离卷积后的参数量为: \(96+27=123\);

而采用常规卷积,完成此过程所需要的参数量为:\(3 \times 3 \times3 \times 32 = 864\)。

1.3 深度可分离卷积实现代码

其实,深度可分离卷积的实现也是依靠常规的卷积函数:torch.nn.Conv2d(),首先我们先来看一下官方教程:

torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros', device=None, dtype=None)
  • in_channels–输入 feature map 的通道数
  • out_channels– 输出 feature map 的通道数
  • kernel_size– 卷积核的尺寸
  • stride – 卷积的步长,默认为1
  • padding –填充尺寸,默认为1
  • padding_mode – 填充的方式,默认为0填充
  • dilation – 卷积核元素之间的间隔,即空洞卷积. 默认为 1 时,为普通卷积
  • groups – 控制输入和输出之间的连接,默认为1

此外,官网上还给出了另外一段话:

When groups == in_channels and out_channels == K * in_channels, where K is a positive integer, this operation is also known as a “depthwise convolution”.

首先定义一个卷积类:

class CSDN_Tem(nn.Module):
    def __init__(self, in_ch, out_ch, kernel_size, padding, groups):
        super(CSDN_Tem, self).__init__()
        self.conv = nn.Conv2d(
            in_channels=in_ch,
            out_channels=out_ch,
            kernel_size=kernel_size,
            stride=1,
            padding=padding,
            groups=groups,
            bias=False
        )

    def forward(self, input):
        out = self.conv(input)
        return out
   
g_input = torch.FloatTensor(3, 16, 16)		# 定义随机输入
conv = CSDN_Tem(3, 32, 3, 1, 1)		# 实例化卷积
print(summary(conv, g_input.size()))		# 输出卷积的参数信息
# [1, 3, 16, 16] => [1, 32, 16, 16]
conv_result = conv(g_input.unsqueeze(0))	# 计算普通卷积的结果,要把输入变成4维

conv_dw = CSDN_Tem(3, 3, 3, padding=1, groups=3)
print(summary(conv_dw,  g_input.size()))	# 输出分组卷积的参数信息
# [1, 3, 16, 16] => [1, 3, 16, 16]
dw_result = conv_dw(g_input.unsqueeze(0))	# 计算逐通道卷积的结果,要把输入变成4维

conv_pw = CSDN_Tem(3, 32, 1, padding=0, groups = 1)
print(summary(conv_pw,  g_input.size()))	# 输出逐点卷积的参数信息
# [1, 3, 16, 16] => [1, 32, 16, 16]
pw_result = conv_pw(dw_result)			    # 在逐通道卷积结果的基础上,计算逐点卷积的结果

输出结果如下:

# 普通卷积的参数量
Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1           [-1, 32, 16, 16]             864
================================================================
Total params: 864
Trainable params: 864
Non-trainable params: 0

# DW 的 参数量
 Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1            [-1, 3, 16, 16]              27
================================================================
Total params: 27
Trainable params: 27
Non-trainable params: 0

# PW 的 参数量
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1           [-1, 32, 16, 16]              96
================================================================
Total params: 96
Trainable params: 96
Non-trainable params: 0

1.4 深度可分离卷积的缺点

普通的卷积,每输出一个 feature map,都考虑到了所有通道维度和通道之间的关系。从深度可分离卷积的原理可以看出,其先在通道域上提取特征,然后通过 \(1 \times 1\) 的卷积修改维度,这样做虽然也考虑到了通道维度和通道之间的信息,然而其通道维度上的特征只在 DW 时提取了一次,相当于无论最后输出的feature map是多少维度的,DW 输出的 feature map 永远都是同一个模板。这样的操作弱化了在通道维度上的特征提取过程,因此效果会打折扣。并且用简单的 \(1 \times 1\) 卷积来考虑通道之间的信息相关性,也过于简单。

二、其他结构上改进的方法

  1. 采用全局池化代替全连接层
  2. 使用多个小卷积核来代替一个大卷积核
  3. 使用并联的非对称卷积核来代替一个正常的卷积核。比如 Inception V3 中将一个 \(7 \times 7\) 的卷积拆分成了 \(1 \times 7\) 和 \(7 \times 1\) 的两个卷积核。在提高了卷积多样性的同时减少了参数量

三、剪枝

剪枝归纳起来就是取其精华去其糟粕。按照剪枝粒度可分为突触剪枝神经元剪枝权重矩阵剪枝等。总体思想是,将权重矩阵中不重要的参数设置为0,结合稀疏矩阵来进行存储和计算。通常为了保证performance,需要一小步一小步地进行迭代剪枝。剪枝的流程如下:

  1. 训练一个performance较好的大模型。
  2. 评估模型中参数的重要性。常用的评估方法是,越接近0的参数越不重要。当然还有其他一些评估方法,这一块也是目前剪枝研究的热点。
  3. 将不重要的参数去掉,或者说是设置为0。之后可以通过稀疏矩阵进行存储。比如只存储非零元素的index和value。
  4. 训练集上微调,从而使得由于去掉了部分参数导致的performance下降能够尽量调整回来。
  5. 验证模型大小和performance是否达到了预期,如果没有,则继续迭代进行。
上一篇:pytorch 网络模型可视化(七):PlotNeuralNet


下一篇:CF895A题解