深度学习网络的轻量化
由于大部分的深度神经网络模型的参数量很大,无法满足直接部署到移动端的条件,因此在不严重影响模型性能的前提下对模型进行压缩加速,来减少网络参数量和计算复杂度,提升运算能力。
一、深度可分离卷积
了解深度可分离卷积之前,我们先看一下常规的卷积操作:对于一张 \(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\) 卷积来考虑通道之间的信息相关性,也过于简单。
二、其他结构上改进的方法
- 采用全局池化代替全连接层
- 使用多个小卷积核来代替一个大卷积核
- 使用并联的非对称卷积核来代替一个正常的卷积核。比如 Inception V3 中将一个 \(7 \times 7\) 的卷积拆分成了 \(1 \times 7\) 和 \(7 \times 1\) 的两个卷积核。在提高了卷积多样性的同时减少了参数量
三、剪枝
剪枝归纳起来就是取其精华去其糟粕。按照剪枝粒度可分为突触剪枝、神经元剪枝、权重矩阵剪枝等。总体思想是,将权重矩阵中不重要的参数设置为0,结合稀疏矩阵来进行存储和计算。通常为了保证performance,需要一小步一小步地进行迭代剪枝。剪枝的流程如下:
- 训练一个performance较好的大模型。
- 评估模型中参数的重要性。常用的评估方法是,越接近0的参数越不重要。当然还有其他一些评估方法,这一块也是目前剪枝研究的热点。
- 将不重要的参数去掉,或者说是设置为0。之后可以通过稀疏矩阵进行存储。比如只存储非零元素的index和value。
- 训练集上微调,从而使得由于去掉了部分参数导致的performance下降能够尽量调整回来。
- 验证模型大小和performance是否达到了预期,如果没有,则继续迭代进行。