緊接著上篇的MobileNet V1,Google在2018年的CVPR頂會上發表了MobileNetV2,論文全稱為《MobileNetV2: Inverted Residuals and Linear Bottlenecks》,原文地址見附錄。
MobileNet-V2的來源Mobilenet-V1的出現推動了移動端的神經網絡發展。但MobileNet V1主要使用的Depthwise Conv(深度可分離卷積)雖然降低了9倍的計算量,但遺留了一個問題是我們在實際使用的時候訓練完發現kernel有不少是空的。當時我們認為,Depthwise每個kernel dim相對於普通Conv要小得多,過小的kernel_dim, 加上ReLU的激活影響下,使得神經元輸出很容易變為0,然後就學廢了(因為對於ReLU來說,值為0的地方梯度也為0)。我們還發現,這個問題在定點化低精度訓練的時候會進一步放大。所以為了解決這一大缺點,MobileNet-V2橫空出世。
MobileNet-V2的創新點反殘差模塊MobileNet V1沒有很好的利用殘差連接,而殘差連接通常情況下總是好的,所以MobileNet V2加上殘差連接。先看看原始的殘差模塊長什麼樣,如Figure3左圖所示:
在原始的殘差模塊中,我們先用
而MobileNet V2提出的殘差模塊的結構如Figure 3右圖所示:中間的
首先說明一下ReLU6,卷積之後通常會接一個ReLU非線性激活函數,在MobileNet V1裡面使用了ReLU6,ReLU6就是普通的ReLU但是限制最大輸出值為6,這是為了在移動端設備float16/int8的低精度的時候,也能有很好的數值解析度,如果對ReLU的激活函數不加限制,輸出範圍0到正無窮,如果激活函數值很大,分布在一個很大的範圍內,則低精度的float16/int8無法很好地精確描述如此大範圍的數值,帶來精度損失。MobileNet V2論文提出,最後輸出的ReLU6去掉,直接線性輸出。理由是:ReLU變換後保留非0區域對應於一個線性變換,僅當輸入低維時ReLU能保留所有完整信息。
網絡結構這樣,我們就得到 MobileNet V2的基本結構了。下圖左邊是沒有殘差連接並且最後帶ReLU6的MobileNet V1的構建模塊,右邊是帶殘差連接並且去掉了最後的ReLU6層的MobileNet V2構建模塊:
網絡的詳細結構如Table2所示。
其中,
通過反殘差模塊這個新的結構,可以使用更少的運算量得到更高的精度,適用於移動端的需求,在 ImageNet 上的準確率如Table4所示。可以看到MobileNet V2又小又快。並且MobileNet V2在目標檢測任務上,也取得了十分不錯的結果。基於MobileNet V2的SSDLite在COCO數據集上map值超過了YOLO V2,且模型大小小10倍,速度快20倍。
在這裡插入圖片描述總結本文提出了一個新的反殘差模塊並構建了MobileNet V2,效果比MobileNet V1更好,且參數更少。本文最難理解的其實是反殘差模塊中最後的線性映射,論文中用了很多公式來描述這個思想,但是實現上非常簡單,就是在 MobileNet V1微結構(bottleneck)中第二個class Block(nn.Module):
'''expand + depthwise + pointwise'''
def __init__(self, in_planes, out_planes, expansion, stride):
super(Block, self).__init__()
self.stride = stride
planes = expansion * in_planes
self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, stride=1, padding=0, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=1, groups=planes, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, out_planes, kernel_size=1, stride=1, padding=0, bias=False)
self.bn3 = nn.BatchNorm2d(out_planes)
self.shortcut = nn.Sequential()
if stride == 1 and in_planes != out_planes:
self.shortcut = nn.Sequential(
nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(out_planes),
)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = F.relu(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
out = out + self.shortcut(x) if self.stride==1 else out
return out
class MobileNetV2(nn.Module):
# (expansion, out_planes, num_blocks, stride)
cfg = [(1, 16, 1, 1),
(6, 24, 2, 1), # NOTE: change stride 2 -> 1 for CIFAR10
(6, 32, 3, 2),
(6, 64, 4, 2),
(6, 96, 3, 1),
(6, 160, 3, 2),
(6, 320, 1, 1)]
def __init__(self, num_classes=10):
super(MobileNetV2, self).__init__()
# NOTE: change conv1 stride 2 -> 1 for CIFAR10
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(32)
self.layers = self._make_layers(in_planes=32)
self.conv2 = nn.Conv2d(320, 1280, kernel_size=1, stride=1, padding=0, bias=False)
self.bn2 = nn.BatchNorm2d(1280)
self.linear = nn.Linear(1280, num_classes)
def _make_layers(self, in_planes):
layers = []
for expansion, out_planes, num_blocks, stride in self.cfg:
strides = [stride] + [1]*(num_blocks-1)
for stride in strides:
layers.append(Block(in_planes, out_planes, expansion, stride))
in_planes = out_planes
return nn.Sequential(*layers)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.layers(out)
out = F.relu(self.bn2(self.conv2(out)))
# NOTE: change pooling kernel_size 7 -> 4 for CIFAR10
out = F.avg_pool2d(out, 4)
out = out.view(out.size(0), -1)
out = self.linear(out)
return out
附錄論文原文:https://arxiv.org/abs/1801.04381參考資料:https://blog.csdn.net/kangdi7547/article/details/81431572推薦閱讀歡迎關注GiantPandaCV, 在這裡你將看到獨家的深度學習分享,堅持原創,每天分享我們學習到的新鮮知識。( • ̀ω•́ )✧
有對文章相關的問題,或者想要加入交流群,歡迎添加BBuf微信:
在這裡插入圖片描述