深度學習網絡的輕量化
由於大部分的深度神經網絡模型的參數量很大,無法滿足直接部署到移動端的條件,因此在不嚴重影響模型性能的前提下對模型進行壓縮加速,來減少網絡參數量和計算復雜度,提升運算能力。
一、深度可分離卷積
了解深度可分離卷積之前,我們先看一下常規的卷積操作:對於一張 \(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是否達到了預期,如果沒有,則繼續迭代進行。