最近在利用SSD檢測物體時,由於實際項目要求,需要對模型進行輕量化,所以考慮利用輕量網絡替換原本的骨架VGG16,查找一些資料后最終采用了google開源的mobileNetV2。這里對學習mobileNet系列的過程做一些總結。mobileNetV1是由google在2017年發布的一個輕量級深度神經網絡,其主要特點是采用深度可分離卷積替換了普通卷積,2018年提出的mobileNetV2在V1的基礎上引入了線性瓶頸 (Linear Bottleneck)和倒殘差 (Inverted Residual)來提高網絡的表征能力。
1.mobileNetV1
mobileNet V1是一種體積較小、計算量較少、適用於移動設備的卷積神經網絡。mobileNet V1的主要創新點是用深度可分離卷積(depthwise separable convolution)代替普通的卷積,並使用寬度乘數(width multiply)減少參數量,不過減少參數的數量和操作的同時也會使特征丟失導致精度下降。
1.1 普通卷積和深度可分離卷積
標准的卷積過程如圖1,卷積核做卷積運算時得同時考慮對應圖像區域中的所有通道(channel),而深度可分離卷積對不同的通道采用不同的卷積核進行卷積,如圖2所示它將普通卷積分解成了深度卷積(Depthwise Convolution)和逐點卷積(Pointwise Convolution)兩個過程,這樣可以將通道(channel)相關性和空間(spatial)相關性解耦。原文中給出的深度可分離卷積后面都接了一個BN和ReLU層。
圖1 普通卷積 圖2 深度可分離卷積
1.1.1 標准卷積核
設輸入特征維度為DF*DF*M,M為通道數。標准卷積核的參數為DK*DK*M*N,DK為卷積核大小,M為輸入的通道數, N為輸出的通道數。卷積后輸出維度為:DF*DF*N。卷積過程中每個卷積核對圖像區域進行DF*DF次掃描,每次掃描的深度為M(channel),每個通道需要DK*DK次加權求和運算, 所以理論計算量(floating point operatios FLOPs)為:N*DF*DF*M*Dk*DK。



1.1.2 深度可分離卷積
-
深度卷積:設輸入特征維度為DF*DF*M,M為通道數。卷積核的參數為DK*DK*1*M。輸出深度卷積后的特征維度為:DF*DF*M。卷積時每個通道只對應一個卷積核(掃描深度為1),所以 FLOPs為:M*DF*DF*DK*DK。
-
逐點卷積:輸入為深度卷積后的特征,維度為DF*DF*M。卷積核參數為1*1*M*N。輸出維度為DF*DF*N。卷積過程中對每個特征做1*1的標准卷積, FLOPs為:N*DF*DF*M。
1.1.3 深度可分離卷積的優勢
-
參數量:關系到模型大小,通常參數用float32表示,所以模型大小一般時參數量的4倍。標准卷積的參數量為Dk*Dk*M*N(M為輸入通道數, N為輸出通道數),深度卷積的參數量為DK*DK*N,逐點卷積的參數量為1*1*M*N,所以深度可分離卷積相對於標准卷積的參數量為(DK*DK*N + M*N)/ DK*DK*M*N = 1/M + 1/DK*DK。
-
計算量: 可以用來衡量算法/模型的復雜度, 通常只考慮乘加操作(Multi-Adds)的數量,而且只考慮 CONV 和 FC 等參數層的計算量,忽略 BN 和PReLU 等等。一般情況,CONV 和 FC 層也會忽略僅純加操作的計算量,如 bias 偏置加和 shotcut 殘差加等。標准卷積的計算量為:N*DF*DF*M*DK*DK,深度可分離卷積的計算量為M*DF*DF*DK*DK+N*DF*DF*M。所以深度可分離卷積的計算量相比於標准卷積為(M*DF*DF*DK*DK+N*DF*DF*M)/ N*DF*DF*M*DK*DK = 1/N + 1/DK*DK。
-
區域和通道分離: 深度可分離卷積將以往普通卷積操作同時考慮通道和區域改變(卷積先只考慮區域,然后再考慮通道),實現了通道和區域的分離。
舉個例子:
假設輸入特征的維度為224*224*3,卷積核大小為3*3,輸出通道數為2,設置pad=1,stride=1,則如下圖(圖a)所示,標准卷積的輸出維度為224*224*2。參數量為3*3*3*2=54,計算量為2*224*224*3*3*3=2709504。
在深度卷積過程中(圖b),輸入為224*224*3,卷積核參數為3*3*1*3,每個通道做3*3的卷積,收集了每個通道的空間特征(Depthwise特征),輸出特征維度為224*224*3。
接着進入逐點卷積(圖c),卷積核參數為1*1*3*2,對Depthwise特征做2個1*1的普通卷積,這樣相當於收集了每個點的特征,輸出維度為224*224*2。
深度可分離卷積的參數量為3*3*2 + 3*2 = 24,相比於標准卷積縮減了2.25倍, 計算量為3*224*224*3*3 + 2*224*224*3 = 1655808,相比於標准卷積縮減了1.6倍。



圖a 標准卷積過程 圖b 深度卷積 圖c 逐點卷積
1.2 mobileNetV1網絡結構
mobileNetV1的網絡結構如Table 1.前面的卷積層中除了第一層為標准卷積層外,其他都是深度可分離卷積(Conv dw + Conv/s1),卷積后接了一個7*7的平均池化層,之后通過全連接層,最后利用Softmax激活函數將全連接層輸出歸一化到0-1的一個概率值,根據概率值的高低可以得到圖像的分類情況。

1.3 超參數
-
寬度因子α(Width Multiplier)
對於深度可分離卷積層,輸入的通道數M乘上一個寬度因子α變為αM,輸出通道數變為αN,其中α區間為(0,1],此時深度可分離卷積的參數量為:DK*DK*αN + αM*αN = α*α(1/α *DK*DK*N + M*N),計算量變為αM*DF*DF*DK*DK+αN*DF*DF*αM = α*α (1/α*M*DF*DF*DK*DK+N*DF*DF*M),所以參數量和計算量差不多都變為原來的α*α倍。
-
分辨率因子ρ (Resolution Multiplier)
ρ改變輸入層的分辨率,所以深度可分離卷積的參數量不變,但計算量為M*ρDF*ρDF*DK*DK+N*ρDF*ρDF*M = ρ*ρ(M*DF*DF*DK*DK+N*DF*DF*M),即計算量變為原來的ρ*ρ倍。
1.4 mobileNetV1 實現(基於框架keras / pytorch)
-
pytorch實現: https://github.com/marvis/pytorch-mobilenet
import torch
import torch.nn as nn
def conv3x3(in_planes, out_planes, stride=1, padding=1):
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, padding=padding, bias=False)
# why no bias: 如果卷積層之后是BN層,那么可以不用偏置參數,可以節省內存
def conv1x1(in_planes, out_planes):
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=1, bias=False)
class DPBlock(nn.Module):
'''
Depthwise convolution and Pointwise convolution.
'''
def __init__(self, in_planes, out_planes, stride=1):
super(DPBlock, self).__init__() # 調用基類__init__函數初始化
self.conv1 = conv3x3(in_planes, out_planes, stride)
self.bn1 = nn.BatchNorm2d(in_planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv1x1(in_planes, out_planes)
self.bn2 = nn.BatchNorm2d(out_planes)
def forward(self, x):
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
return out
class mobileNetV1Net(nn.Module):
def __init__(self, block, num_class=1000):
super(mobileNetV1Net, self).__init__()
self.model = nn.Sequential(
conv3x3(3, 32, 2),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True)
block(32, 64, 1),
block(64, 128, 2),
block(128, 128, 1),
block(128, 256, 2),
block(256, 256, 1),
block(256, 512, 2),
block(512, 512, 1),
block(512, 512, 1),
block(512, 512, 1),
block(512, 512, 1),
block(512, 512, 1),
block(512, 1024, 2),
block(1024, 1024, 2),
nn.AvgPool2d(7)
)
self.fc = nn.Linear(1024, num_class)
def forward(self, x):
x = self.model(x)
x = x.view(-1, 1024) # reshape
out = self.fc(x)
return out
mobileNetV1 = mobileNetV1Net(DPBlock)