MobileNetv1 論文詳解

語言: CN / TW / HK

開啟掘金成長之旅!這是我參與「掘金日新計劃 · 12 月更文挑戰」的第2天,點選檢視活動詳情

文章同步發於 github部落格園知乎。最新版以 github 為主。如果看完文章有所收穫,一定要先點贊後收藏。畢竟,贈人玫瑰,手有餘香

MobileNet 論文的主要貢獻在於提出了一種深度可分離卷積架構(DW+PW 卷積),先通過理論證明這種架構比常規的卷積計算成本(Mult-Adds)更小,然後通過分類、檢測等多種實驗證明模型的有效性。

1、相關工作

標準卷積

一個大小為 $h_1\times w_1$ 過濾器(2 維卷積核),沿著 feature map 的左上角移動到右下角,過濾器每移動一次,將過濾器引數矩陣和對應特徵圖 $h_1 \times w_1 \times c_1$ 大小的區域內的畫素點相乘後累加得到一個值,又因為 feature map 的數量(通道數)為 $c_1$,所以我們需要一個 shape 為 $ (c_1, h_1, w_1)$ 的濾波器( 3 維卷積核),將每個輸入 featue map 對應輸出畫素點位置計算和的值相加,即得到輸出 feature map 對應畫素點的值。又因為輸出 feature map 的數量為 $c_2$ 個,所以需要 $c_2$ 個濾波器。標準卷積抽象過程如下圖所示。

標準卷積過程

2D 卷積計算過程動態圖如下,通過這張圖能夠更直觀理解卷積核如何執行滑窗操作,又如何相加並輸出 $c_2$ 個 feature map ,動態圖來源 這裡

卷積過程

分組卷積

Group Convolution 分組卷積,最早見於 AlexNet。常規卷積與分組卷積的輸入 feature map 與輸出 feature map 的連線方式如下圖所示,圖片來自CondenseNet

分組卷積

分組卷積的定義:對輸入 feature map 進行分組,然後分組分別進行卷積。假設輸入 feature map 的尺寸為 $H \times W \times c_{1}$,輸出 feature map 數量為 $c_2$ 個,如果將輸入 feature map 按通道分為 $g$ 組,則每組特徵圖的尺寸為 $H \times W \times \frac{c_1}{g}$,每組對應的濾波器(卷積核)的 尺寸 為 $h_{1} \times w_{1} \times \frac{c_{1}}{g}$,每組的濾波器數量為 $\frac{c_{2}}{g}$ 個,濾波器總數依然為 $c_2$ 個,即分組卷積的卷積核 shape 為 $(c_2,\frac{c_1}{g}, h_1,w_1)$。每組的濾波器只與其同組的輸入 map 進行卷積,每組輸出特徵圖尺寸為 $H \times W \times \frac{c_{2}}{g}$,將 $g$ 組卷積後的結果進行拼接 (concatenate) 得到最終的得到最終尺寸為 $H \times W \times c_2$ 的輸出特徵圖,其分組卷積過程如下圖所示:

分組卷積過程2

分組卷積的意義:分組卷積是現在網路結構設計的核心,它通過通道之間的稀疏連線(也就是隻和同一個組內的特徵連線)來降低計算複雜度。一方面,它允許我們使用更多的通道數來增加網路容量進而提升準確率,但另一方面隨著通道數的增多也對帶來更多的 $MAC$。針對 $1 \times 1$ 的分組卷積,$MAC$ 和 $FLOPs$ 計算如下:

$$ \begin{aligned} & MACC = H \times W \times 1 \times 1 \times \frac{c_{1}}{g}\frac{c_{2}}{g} \times g = \frac{hwc_{1}c_{2}}{g} \ & FLOPs = 2 \times MACC \ & Params = g \times \frac{c_2}{g}\times\frac{c_1}{g} \times 1\times 1 + c_2 = \frac{c_{1}c_{2}}{g} \ & MAC = HW(c_1 + c_2) + \frac{c_{1}c_{2}}{g} \ \end{aligned} $$

從以上公式可以得出分組卷積的引數量和計算量是標準卷積的 $\frac{1}{g}$ 的結論 ,但其實對分組卷積過程進行深入理解之後也可以直接得出以上結論。

分組卷積的深入理解:對於 $1\times 1$ 卷積,常規卷積輸出的特徵圖上,每一個畫素點是由輸入特徵圖的 $c_1$ 個點計算得到,而分組卷積輸出的特徵圖上,每一個畫素點是由輸入特徵圖的 $ \frac{c_1}{g}$個點得到(參考常規卷積計算過程)。卷積運算過程是線性的,自然,分組卷積的引數量和計算量是標準卷積的 $\frac{1}{g}$ 了

當分組卷積的分組數量 = 輸入 feature map 數量 = 輸出 feature map 數量,即 $g=c_1=c_2$,有 $c_1$ 個濾波器,且每個濾波器尺寸為 $1 \times K \times K$ 時,Group Convolution 就成了 Depthwise Convolution(DW 卷積),DW 卷積的卷積核權重尺寸為 $(c_{1}, 1, K, K)$。

常規卷積的卷積核權重 shape 都為(C_out, C_in, kernel_height, kernel_width),分組卷積的卷積核權重 shape 為(C_out, C_in/g, kernel_height, kernel_width),DW 卷積的卷積核權重 shape 為(C_in, 1, kernel_height, kernel_width)。

從 Inception module 到 depthwise separable convolutions

深度可分離卷積(depthwise separable convolutions)的提出最早來源於 Xception 論文,Xception 的論文中提到,對於卷積來說,卷積核可以看做一個三維的濾波器:通道維+空間維(Feature Map 的寬和高),常規的卷積操作其實就是實現通道相關性和空間相關性的聯合對映Inception 模組的背後存在這樣的一種假設:卷積層通道間的相關性和空間相關性是可以退耦合(完全可分)的,將它們分開對映,能達到更好的效果(the fundamental hypothesis behind Inception is that cross-channel correlations and spatial correlations are sufficiently decoupled that it is preferable not to map them jointly.)。

引入深度可分離卷積的 Inception,稱之為 Xception,其作為 Inception v3 的改進版,在 ImageNet 和 JFT 資料集上有一定的效能提升,但是引數量和速度並沒有太大的變化,因為 Xception 的目的也不在於模型的壓縮。深度可分離卷積的 Inception 模組如圖 Figure 4 所示。

Xeption

Figure 4 中的“極限” Inception 模組與本文的主角-深度可分離卷積模組相似,區別在於:深度可分離卷積先進行 channel-wise 的空間卷積,再進行 $1 \times 1$ 的通道卷積,Figure 4 的 Inception 則相反;

2、MobileNets 結構

2.1,深度可分離卷積

MobileNets 是谷歌 2017 年提出的一種高效的移動端輕量化網路,其核心是深度可分離卷積(depthwise separable convolutions),深度可分離卷積的核心思想是將一個完整的卷積運算分解為兩步進行,分別為 Depthwise Convolution(DW 卷積) 與 Pointwise Convolution(PW 卷積)。深度可分離卷積的計算步驟和濾波器尺寸如下所示。

深度可分離卷積的計算步驟

濾波器尺寸

Depthwise 卷積

注意本文 DW 和 PW 卷積計算量的計算與論文有所區別,本文的輸出 Feature map 大小是 $D_G \times D_G$, 論文公式是$D_F \times D_F$。

不同於常規卷積操作, Depthwise Convolution 的一個卷積核只負責一個通道,一個通道只能被一個卷積核卷積(不同的通道採用不同的卷積核卷積),也就是輸入通道、輸出通道和分組數相同的特殊分組卷積,因此 Depthwise(DW)卷積不會改變輸入特徵圖的通道數目。深度可分離卷積的 DW卷積步驟如下圖:

DW卷積計算步驟

DW 卷積的計算量 $MACC = M \times D_{G}^{2} \times D_{K}^{2}$

Pointwise 卷積

上述 Depthwise 卷積的問題在於它讓每個卷積核單獨對一個通道進行計算,但是各個通道的資訊沒有達到交換,從而在網路後續資訊流動中會損失通道之間的資訊,因此論文中就加入了 Pointwise 卷積操作,來進一步融合通道之間的資訊。PW 卷積是一種特殊的常規卷積,卷積核的尺寸為 $1 \times 1$。PW 卷積的過程如下圖:

PW卷積過程

假設輸入特徵圖大小為 $D_{G} \times D_{G} \times M$,輸出特徵圖大小為 $D_{G} \times D_{G} \times N$,則濾波器尺寸為 $1 \times 1 \times M$,且一共有 $N$ 個濾波器。因此可計算得到 PW 卷積的計算量 $MACC = N \times M \times D_{G}^{2}$。

綜上:DepthwisePointwise 卷積這兩部分的計算量相加為 $MACC1 = M \times D_{G}^{2} \times D_{K}^{2} + N \times M \times D_{G}^{2}$,而標準卷積的計算量 $MACC2 = N \times D_{G}^{2} \times D_{K}^{2} \times M$。所以深度可分離卷積計算量於標準卷積計算量比值的計算公式如下。

$$ \begin{aligned} \frac{Depthwise \ Separable \ Conv}{Standard \ Conv} &= \frac{M \times D_{G}^{2}(D_{K}^{2} + N)}{N \times D_{G}^{2} \times D_{K}^{2} \times M} \ &= \frac{D_{K}^{2} + N}{D_{K}^{2} \times N} \ &= \frac{1}{N} + \frac{1}{D_{K}^{2}} \ \end{aligned} $$

可以看到 Depthwise + Pointwise 卷積的計算量相較於標準卷積近乎減少了 $N$ 倍,$N$ 為輸出特徵圖的通道數目,同理引數量也會減少很多。在達到相同目的(即對相鄰元素以及通道之間資訊進行計算)下, 深度可分離卷積能極大減少卷積計算量,因此大量移動端網路的 backbone 都採用了這種卷積結構,再加上模型蒸餾,剪枝,能讓移動端更高效的推理。

深度可分離卷積的詳細計算過程可參考 Depthwise卷積與Pointwise卷積

2.2、網路結構

$3 \times 3$ 的深度可分離卷積 Block 結構如下圖所示:

3x3的深度可分離卷積Block結構

左邊是帶 bnrelu 的標準卷積層,右邊是帶 bn 和 relu 的深度可分離卷積層。 $3 \times 3$ 的深度可分離卷積 Block 網路的 pytorch 程式碼如下:

```python class MobilnetV1Block(nn.Module): """Depthwise conv + Pointwise conv""" def init(self, in_channels, out_channels, stride=1): super(MobilnetV1Block, self).init() # dw conv kernel shape is (in_channels, 1, ksize, ksize) self.dw = nn.Conv2d(in_channels, in_channels, kernel_size=3,stride=stride,padding=1, groups=in_channels, bias=False) self.bn1 = nn.BatchNorm2d(in_channels) self.pw = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False) self.bn2 = nn.BatchNorm2d(out_channels)

def forward(self, x):
    out1 = F.relu(self.bn1(self.dw(x)))
    out2 = F.relu(self.bn2(self.pw(out1)))
    return out2

```

MobileNet v1pytorch 模型匯出為 onnx 模型後,深度可分離卷積 block 結構圖如下圖所示。

深度可分離卷積block的onnx模型結構圖

僅用 MobileNets 的 Mult-Adds(乘加操作)次數更少來定義高效能網路是不夠的,確保這些操作能夠有效實施也很重要。例如非結構化稀疏矩陣運算(unstructured sparse matrix operations)通常並不會比密集矩陣運算(dense matrix operations)快,除非是非常高的稀疏度。

這句話是不是和 shufflenet v2 的觀點一致,即不能僅僅以 FLOPs 計算量來表現網路的執行速度,除非是同一種網路架構。

MobileNet 模型結構將幾乎所有計算都放入密集的 1×1 卷積中(dense 1 × 1 convolutions),卷積計算可以通過高度優化的通用矩陣乘法(GEMM)函式來實現。 卷積通常由 GEMM 實現,但需要在記憶體中進行名為 im2col 的初始重新排序,然後才對映到 GEMM。 caffe 框架就是使用這種方法實現卷積計算。 1×1 卷積不需要在記憶體中進行重新排序,可以直接使用 GEMM(最優化的數值線性代數演算法之一)來實現。

如表 2 所示,MobileNet 模型的 1x1 卷積佔據了 95% 的計算量和 75% 的引數,剩下的引數幾乎都在全連線層中, 3x3 的 DW 卷積核常規卷積佔據了很少的計算量(Mult-Adds)和引數。

表2

2.3、寬度乘係數-更小的模型

儘管基本的 MobileNet 體系結構已經很小且網路延遲 latency 很低,但很多情況下特定用例或應用可能要求模型變得更小,更快。為了構建這些更小且計算成本更低的模型,我們引入了一個非常簡單的引數 $\alpha$,稱為 width 乘數寬度乘數 $\alpha$ 的作用是使每一層的網路均勻變薄。對於給定的層和寬度乘數 $\alpha$,輸入通道的數量變為 $\alpha M$,而輸出通道的數量 $N$ 變為 $\alpha N$。具有寬度乘數 $\alpha$ 的深度可分離卷積(其它引數和上文一致)的計算成本為:

$$\alpha M \times D_{G}^{2} \times D_{K}^{2} + \alpha N \times \alpha M \times D_{G}^{2}$$

其中 $\alpha \in (0,1]$,典型值設定為 1、0.75、0.50.25。$\alpha = 1$ 是基準 MobileNet 模型,$\alpha < 1$ 是縮小版的 MobileNets寬度乘數的作用是將計算量和引數數量大約減少 $\alpha^2$ 倍,從而降低了網路計算成本( computational cost of a neural network)。 寬度乘數可以應用於任何模型結構,以定義新的較小模型,且具有合理的準確性、網路延遲 latency 和模型大小之間的權衡。 它用於定義新的精簡結構,需要從頭開始進行訓練模型。基準 MobileNet 模型的整體結構定義如表 1 所示。

表1

2.4、解析度乘係數-減少表示

減少模型計算成本的的第二個超引數(hyper-parameter)是解析度因子 $\rho$,論文將其應用於輸入影象,則網路的每一層 feature map 大小也要乘以 $\rho$。實際上,論文通過設定輸入解析度來隱式設定 $\rho$。 將網路核心層的計算成本表示為具有寬度乘數 $\alpha$ 和解析度乘數 $\rho$ 的深度可分離卷積的公式如下: $$\alpha M \times \rho D_{G}^{2} \times D_{K}^{2} + \alpha N \times \alpha M \times \rho D_{G}^{2}$$ 其中 $\rho \in (0,1]$,通常是隱式設定的,因此網路的輸入解析度為 224、192、160128。$\rho = 1$ 時是基準(baseline) MobilNet,$\rho < 1$ 時縮小版 MobileNets解析度乘數的作用是將計算量減少 $\rho^2$

2.5、模型結構總結

  • 整個網路不算平均池化層與 softmax 層,且將 DW 卷積和 PW 卷積計為單獨的一層,則 MobileNet28 層網路。+ 在整個網路結構中步長為2的卷積較有特點,卷積的同時充當下采樣的功能;
  • 第一層之後的 26 層都為深度可分離卷積的重複卷積操作,分為 4 個卷積 stage
  • 每一個卷積層(含常規卷積、深度卷積、逐點卷積)之後都緊跟著批規範化和 ReLU 啟用函式;
  • 最後一層全連線層不使用啟用函式。

3、實驗

作者分別進行了 Stanford Dogs dataset 資料集上的細粒度識別、大規模地理分類、人臉屬性分類、COCO 資料集上目標檢測的實驗,來證明與 Inception V3GoogleNetVGG16backbone 相比,MobilNets 模型可以在計算量(Mult-Adds)數 10 被下降的情況下,但是精度卻幾乎不變。

4、結論

論文提出了一種基於深度可分離卷積的新模型架構,稱為 MobileNets。 在相關工作章節中,作者首先調查了一些讓模型更有效的重要設計原則,然後,演示瞭如何通過寬度乘數和解析度乘數來構建更小,更快的 MobileNet,通過權衡合理的精度以減少模型大小和延遲。 然後,我們將不同的 MobileNets 與流行的模型進行了比較,這些模型展示了出色的尺寸,速度和準確性特性。 最後,論文演示了 MobileNet 在應用於各種任務時的有效性。

5、基準模型程式碼

自己復現的基準 MobileNet v1 代模型 pytorch 程式碼如下:

```python import torch import torch.nn as nn import torch.nn.functional as F import torchvision.models as models from torch import flatten

class MobilnetV1Block(nn.Module): """Depthwise conv + Pointwise conv"""

def __init__(self, in_channels, out_channels, stride=1):
    super(MobilnetV1Block, self).__init__()
    # dw conv kernel shape is (in_channels, 1, ksize, ksize)
    self.dw = nn.Sequential(
        nn.Conv2d(in_channels, 64, kernel_size=3,
                  stride=stride, padding=1, groups=4, bias=False),
        nn.BatchNorm2d(in_channels),
        nn.ReLU(inplace=True)
    )
    # print(self.dw[0].weight.shape)  # print dw conv kernel shape
    self.pw = nn.Sequential(
        nn.Conv2d(in_channels, out_channels, kernel_size=1,
                  stride=1, padding=0, bias=False),
        nn.BatchNorm2d(out_channels),
        nn.ReLU(inplace=True)
    )

def forward(self, x):
    x = self.dw(x)
    x = self.pw(x)
    return x

def convbn_relu(in_channels, out_channels, stride=2): return nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False), nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True))

class MobileNetV1(nn.Module): # (32, 64, 1) means MobilnetV1Block in_channnels is 32, out_channels is 64, no change in map size. stage_cfg = [(32, 64, 1), (64, 128, 2), (128, 128, 1), # stage1 conv (128, 256, 2), (256, 256, 1), # stage2 conv (256, 512, 2), (512, 512, 1), (512, 512, 1), (512, 512, 1), (512, 512, 1), (512, 512, 1), # stage3 conv (512, 1024, 2), (1024, 1024, 1) # stage4 conv ] def init(self, num_classes=1000): super(MobileNetV1, self).init() self.first_conv = convbn_relu(3, 32, 2) # Input image size reduced by half self.stage_layers = self._make_layers(in_channels=32) self.linear = nn.Linear(1024, num_classes) # 全連線層

def _make_layers(self, in_channels):
    layers = []
    for x in self.stage_cfg:
        in_channels = x[0]
        out_channels = x[1]
        stride = x[2]
        layers.append(MobilnetV1Block(in_channels, out_channels, stride))
        in_channels = out_channels
    return nn.Sequential(*layers)

def forward(self, x):
    """Feature map shape(h、w) is 224 -> 112 -> 56 -> 28 -> 14 -> 7 -> 1"""
    x = self.first_conv(x)
    x = self.stage_layers(x)

    x = F.avg_pool2d(x, 7)  # x shape is 7*7
    x = flatten(x, 1)       # x = x.view(x.size(0), -1)
    x = self.linear(x)

    return x

if name == "main": model = MobileNetV1() model.eval() # set the model to inference mode input_data = torch.rand(1, 3, 224, 224) outputs = model(input_data) print("Model output size is", outputs.size()) ```

程式執行結果如下:

Model output size is torch.Size([1, 1000])

個人思考

在降低 FLOPs 計算量上,MobileNet 的網路架構設計確實很好,但是 MobileNet 模型在 GPUDSPTPU 硬體上卻不一定效能好,原因是不同硬體進行運算時的行為不同,從而導致了 FLOPs少不等於 latency的問題。

如果要實際解釋 TPUDSP 的運作原理,可能有點麻煩,可以參考下圖,從結果直觀地理解他們行為上的差異。考慮一個簡單的 convolution,在 CPUlatency 隨著 inputoutputchannel 上升正相關的增加。然而在 DSP 上卻是階梯型,甚至在更高的 channel 數下存在特別低latency 的甜蜜點。

Convolution在CPU與DSP的行為差異

在一定的程度上,網路越深越寬,效能越好。寬度,即通道(channel)的數量,網路深度,即 layer 的層數,如 resnet1818 個卷積層。注意我們這裡說的和寬度學習一類的模型沒有關係,而是特指深度卷積神經網路的(通道)寬度。

  • 網路深度的意義CNN 的網路層能夠對輸入影象資料進行逐層抽象,比如第一層學習到了影象邊緣特徵,第二層學習到了簡單形狀特徵,第三層學習到了目標形狀的特徵,網路深度增加也提高了模型的抽象能力。
  • 網路寬度的意義:網路的寬度(通道數)代表了濾波器(3 維)的數量,濾波器越多,對目標特徵的提取能力越強,即讓每一層網路學習到更加豐富的特徵,比如不同方向、不同頻率的紋理特徵等。

後續改進-MobileDets

  1. FLOPs 低不等於 latency 低,尤其是在有加速功能的硬體 (GPUDSPTPU )上不成立。
  2. MobileNet conv block (depthwise separable convolution)在有加速功能的硬體(專用硬體設計-NPU 晶片)上比較沒有效率。
  3. channel 數的情況下 (如網路的前幾層),在有加速功能的硬體使用普通 convolution 通常會比separable convolution 有效率。
  4. 在大多數的硬體上,channel 數為 8 的倍數比較有利計算。
  5. DSPTPU 上,一般我們需要運算為 uint8 形式,quantization低精度量化)是常見的技巧。
  6. DSPTPU 上,h-Swishsqueeze-and-excitation 效果不明顯 (因為硬體設計與 uint8 壓縮的關係)。
  7. DSPTPU 上,5x5 convolution 比較沒效率。

參考資料

  1. Group Convolution分組卷積,以及Depthwise Convolution和Global Depthwise Convolution
  2. 理解分組卷積和深度可分離卷積如何降低引數量
  3. 深度可分離卷積(Xception 與 MobileNet 的點滴)
  4. MobileNetV1程式碼實現
  5. Depthwise卷積與Pointwise卷積
  6. 【CNN結構設計】深入理解深度可分離卷積
  7. FLOPs與模型推理速度
  8. MobileDets: FLOPs不等於Latency,考量不同硬體的高效架構