最近在利用SSD檢測物體時,由於實際項目要求,需要對模型進行輕量化,所以考慮利用輕量網絡替換原本的骨架VGG16,查找一些資料后最終采用了google開源的mobileNetV2。這里對學習mobileNet系列的過程做一些總結。mobileNetV1是由google在2017年發布的一個輕量級深度神經網絡,其主要特點是采用深度可分離卷積替換了普通卷積,2018年提出的mobileNetV2在V1的基礎上引入了線性瓶頸 (Linear Bottleneck)和倒殘差 (Inverted Residual)來提高網絡的表征能力。
2.mobileNetV2
mobileNetV2是對mobileNetV1的改進,同樣是一種輕量級的神經網絡。為了防止非線性層(ReLU)損失一部分信息,引入了線性瓶頸層(Linear Bottleneck);另外借鑒ResNet及DenseNet等一系列網絡采用了shortcut的網絡得到了很好的效果,作者結合depthwise convolution的特點,提出了倒殘差 (Inverted Residual)。
2.1 線性瓶頸層
對於mobileNetV1的深度可分離卷積而言, 寬度乘數壓縮后的M維空間后會通過一個非線性變換ReLU,根據ReLU的性質,輸入特征若為負數,該通道的特征會被清零,本來特征已經經過壓縮,這會進一步損失特征信息;若輸入特征是正數,經過激活層輸出特征是還原始的輸入值,則相當於線性變換。
下圖是將低維流形的ReLU嵌入高維空間中的例子,原始特征通過隨機矩陣T變換,后面接ReLU層,變換到n維空間后再通過反變換T^-1轉變回原始空間。 當n=2,3時,會導致比較嚴重的信息丟失,部分特征重疊到一起了;當n=15到30時,信息丟失程度降低,但是變換矩陣已經是高度非凸的了。 由於非線性層會損失一部分信息,因而使用線性瓶頸層。

2.2 倒殘差
殘差塊已經被證明有助於提高精度,所以mobileNetV2也引入了類似的塊。經典的殘差塊(residual block)的過程是:1x1(降維)-->3x3(卷積)-->1x1(升維), 但深度卷積層(Depthwise convolution layer)提取特征限制於輸入特征維度,若采用殘差塊,先經過1x1的逐點卷積(Pointwise convolution)操作先將輸入特征圖壓縮(一般壓縮率為0.25),再經過深度卷積后,提取的特征會更少。所以mobileNetV2是先經過1x1的逐點卷積操作將特征圖的通道進行擴張,豐富特征數量,進而提高精度。這一過程剛好和殘差塊的順序顛倒,這也就是倒殘差的由來:1x1(升維)-->3x3(dw conv+relu)-->1x1(降維+線性變換)。
2.3 網絡結構
瓶頸層的具體結構如下表所示。輸入通過1*1的conv+ReLU層將維度從k維增加到tk維,之后通過3*3conv+ReLU可分離卷積對圖像進行降采樣(stride>1時),此時特征維度已經為tk維度,最后通過1*1conv(無ReLU)進行降維,維度從tk降低到k維。
需要注意的是,整個模型中除了第一個瓶頸層的t=1之外,其他瓶頸層t=6(論文中Table 2),即第一個瓶頸層內部並不對特征進行升維。

另外對於瓶頸層,當stride=1時,才會使用elementwise 的sum將輸入和輸出特征連接(如下圖左側);stride=2時,無short cut連接輸入和輸出特征(下圖右側)。

MobileNetV2的模型如下圖所示,其中$t$為瓶頸層內部升維的倍數,$c$為特征的維數,$n$為該瓶頸層重復的次數,$s$為瓶頸層第一個conv的步幅。

需要注意的是:
- 當$n>1$時(即該瓶頸層重復的次數>1),只在第一個瓶頸層stride為對應的s,其他重復的瓶頸層stride均為1;
- 只在$stride=1$時,輸出特征尺寸和輸入特征尺寸一致,才會使用elementwise sum將輸出與輸入相加;
- 當$n>1$時,只在第一個瓶頸層特征維度為$c$,其他時候channel不變。
例如,對於該圖中562*24的那層,共有3個該瓶頸層,只在第一個瓶頸層使用stride=2,后兩個瓶頸層stride=1;第一個瓶頸層由於輸入和輸出尺寸不一致,因而無short cut連接,后兩個由於stride=1,輸入輸出特征尺寸一致,會使用short cut將輸入和輸出特征進行elementwise的sum;只在第一個瓶頸層最后的1*1conv對特征進行升維,后兩個瓶頸層輸出維度不變(不要和瓶頸層內部的升維弄混了)。該層輸入特征為56*56*24,第一個瓶頸層輸出為28*28*32(特征尺寸降低,特征維度增加,無short cut),第二個、第三個瓶頸層輸入和輸出均為28*28*32(此時c=32,s=1,有short cut)。
另外表中還有一個$k$。mobileNetV1中提出了寬度縮放因子,其作用是在整體上對網絡的每一層維度(特征數量)進行瘦身。mobileNetV2中,當該因子<1時,最后的那個1*1conv不進行寬度縮放;否則進行寬度縮放。
2.4 實現
1 import torch 2 import torch.nn as nn 3 import numpy as np 4 5 # 定義bottleneck 6 class Bottlenect(nn.Module): 7 def __init__(self, inplanes, outplanes, stride=1, expand_ratio=1): 8 super(Bottlenect, self).__init__() 9 10 self.stride = stride 11 assert stride in [1, 2] 12 self.use_res_connect = (self.stride == 1) and (inplanes == outplanes) # 是否連接殘差 13 14 hidden_dim = inplanes*expand_ratio # 中間層維度 15 self.conv1 = nn.Conv2d(inplanes, hidden_dim, kernel_size=1, stride=1, padding=0, bias=False) 16 self.bn1 = nn.BatchNorm2d(hidden_dim) 17 18 self.conv2 = nn.Conv2d(hidden_dim, hidden_dim, 3, stride, padding=1, groups=hidden_dim, bias=False) 19 self.bn2 = nn.BatchNorm2d(hidden_dim) 20 21 self.conv3 = nn.Conv2d(hidden_dim, outplanes, 1, 1, 0, bias=False) 22 self.bn3 = nn.BatchNorm2d(outplanes) 23 24 self.relu = nn.ReLU(inplace=True) 25 26 def forward(self, x): 27 residual = x 28 29 out = self.conv1(x) 30 out = self.bn1(out) 31 out = self.relu(out) 32 33 out = self.conv2(out) 34 out = self.bn2(out) 35 out = self.relu(out) 36 37 out = self.conv3(out) 38 out = self.bn3(out) 39 40 if self.use_res_connect: 41 out += residual 42 43 return out 44 45 def make_divisible(x, divisible_by=8): 46 return int(np.ceil(x * 1. / divisible_by) * divisible_by) 47 48 class MobileNetV2(nn.Module): 49 def __init__(self, n_class=1000, input_size=224, width_multi=1.0): 50 super(MobileNetV2, self).__init__() 51 52 input_channel = 32 53 last_channel = 1280 54 55 bottlenet_setting = [ 56 # t, c, n, s 57 [1, 16, 1, 1], 58 [6, 24, 2, 2], 59 [6, 32, 3, 2], 60 [6, 64, 4, 2], 61 [6, 96, 3, 1], 62 [6, 160, 3, 2], 63 [6, 320, 1, 1] 64 ] 65 66 assert input_size % 32 == 0 67 self.last_channel = make_divisible(last_channel*width_multi) if width_multi > 1.0 else last_channel 68 69 # first conv layer 1 70 self.conv1 = nn.Conv2d(3, input_channel, 3, 2, 1, bias=False) 71 self.bn1 = nn.BatchNorm2d(input_channel) 72 self.relu = nn.ReLU(inplace=True) 73 74 # bottlenect layer 2-->8 75 self.bottlenect_layer = [] 76 for t, c, n, s in bottlenet_setting: 77 output_channel = make_divisible(c*width_multi) if t > 1 else c 78 for i in range(n): 79 if i == 0: # 第一層的stride = stride, 其他層stride = 1 80 self.bottlenect_layer.append(Bottlenect(input_channel, output_channel, s, expand_ratio=t)) 81 else: 82 self.bottlenect_layer.append(Bottlenect(input_channel, output_channel, 1, expand_ratio=t)) 83 input_channel = output_channel 84 self.bottlenect_layer = nn.Sequential(*self.bottlenect_layer) 85 86 # conv layer 9 87 self.conv2 = nn.Conv2d(input_channel, self.last_channel, 1, 1, 0, bias=False) 88 self.bn2 = nn.BatchNorm2d(self.last_channel) 89 90 # avg pool layer 91 self.avg_pool = nn.AvgPool2d(7, stride=1) 92 93 # last conv layer 94 self.conv9 = nn.Conv2d(self.last_channel, n_class, 1, 1, bias=False) 95 self.bn3 = nn.BatchNorm2d(n_class) 96 97 def forward(self, x): 98 out = self.conv1(x) 99 out = self.bn1(out) 100 out = self.relu(out) 101 102 out = self.bottlenect_layer(out) 103 104 out = self.conv2(out) 105 out = self.bn2(out) 106 out = self.relu(out) 107 108 out = self.avg_pool(out) 109 110 out = self.conv9(out) 111 out = self.bn3(out) 112 out = self.relu(out) 113 114 out = out.view(x.size(0), -1) 115 116 return out 117 118 model = MobileNetV2(width_multi=1)