深度學習網路的輕量化
由於大部分的深度神經網路模型的引數量很大,無法滿足直接部署到移動端的條件,因此在不嚴重影響模型效能的前提下對模型進行壓縮加速,來減少網路引數量和計算複雜度,提升運算能力。
一、深度可分離卷積
瞭解深度可分離卷積之前,我們先看一下常規的卷積操作:對於一張 \(3 \times 16 \times 16\) 的影像,如果採用 \(3\times3\) 的卷積核,輸出 \(32 \times 16 \times 16\) 的feature map,則所需要的引數量為:
常規卷積中每一個卷積核對輸入的所有通道進行卷積,如下圖所示:
1.1 逐通道卷積
depthwise中,每一個卷積核只對一個通道進行卷積,如下圖所示:
於是,還是對於一個 \(3 \times 16 \times 16\) 的影像來說,通過一個 \(3 \times 3\) 的卷積,其輸出feature map 的維度為 \(3 \times 16 \times 16\),所用到的卷積核的引數為:
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個,此時的引數量為:
所以,綜合兩個過程考慮,採用深度可分離卷積後的引數量為: \(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是否達到了預期,如果沒有,則繼續迭代進行。