深度學習網路模型的輕量化方法

ZhiboZhao 發表於 2021-07-17
深度學習

深度學習網路的輕量化

由於大部分的深度神經網路模型的引數量很大,無法滿足直接部署到移動端的條件,因此在不嚴重影響模型效能的前提下對模型進行壓縮加速,來減少網路引數量和計算複雜度,提升運算能力。

一、深度可分離卷積

瞭解深度可分離卷積之前,我們先看一下常規的卷積操作:對於一張 \(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是否達到了預期,如果沒有,則繼續迭代進行。