分組卷積
分組卷積(Group Convolution)最早見於AlexNet以切分網絡,是一種降低參數量和計算量的方法,是模型輕量化的一種基礎方法。
分組卷積就是對輸入的特征圖進行分組,然后分別進行卷積,然后將結果堆積起來。設輸入特征圖為\(D_{in}\times H\times W\),輸出特征圖維度為\(D_{out}\),常規卷積核的參數量為\(K^2D_{in}D_{out}\);在分組卷積中,設分組數量為\(G\),那么每組輸出的特征圖維度為\(\frac{D_{out}}{G}\),每個卷積核參數量為\(K^2\frac{D_{in}}{G}\frac{D_{out}}{G}\),\(G\)個卷積的總參數量為\(K^2D_{in}D_{out}\frac{1}{G}\),為常規卷積的\(\frac{1}{G}\)。
分組卷積的思想被廣泛地運用在網絡設計中,除了可以降低參數量,還可以被視為一種結構化稀疏方法,相當於一種正則化方法。
當分組數與特征圖輸入輸出維度相等時,即\(G=D_{in}=D_{out}\),相當於MobileNet和Xception中的深度卷積(Depthwise Convolution)。
當分組數與特征圖輸入輸出維度相等時,即\(G=D_{in}=D_{out}\),且當卷積核的輸入尺度與輸入特征圖維度時,即\(K=W=H\),輸入的特征圖為\(C\times 1\times 1\),在MobileFaceNet中稱之為Global Depthwise Convolution(GDC),即全局加權池化,與Global Average Pooling(GAP)不同,GDC給每個位置賦予了可學習的權重(對於已對齊的圖像這很有效,比如人臉,中心位置和邊界位置的權重自然應該不同)。
深度可分離卷積
深度可分離卷積分為深度卷積和逐點卷積兩部分,即SeparableConv由DepthWiseConv和PointWiseConv組成,是降低卷積運算參數量的一種有效方法。
在Google的Xception以及MobileNet論文中均有描述。它的核心思想是將一個完整的卷積運算分解為兩步進行,分別為Depthwise Convolution與Pointwise Convolution。
Pytorch代碼為:
from torch import nn
class SeparableConv2d(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size=1, stride=1, padding=0, onnx_compatible=False):
super().__init__()
ReLU = nn.ReLU if onnx_compatible else nn.ReLU6
self.conv = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=in_channels, kernel_size=kernel_size,
groups=in_channels, stride=stride, padding=padding),
nn.BatchNorm2d(in_channels),
ReLU(),
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1),
)
def forward(self, x):
return self.conv(x)
設特征圖的輸入層數為\(D_{in}\),輸出層數為\(D_{out}\),卷積核的尺度為\(K\times K\)。
深度可分離卷積的參數量為\(K^2D_{in} + D_{in}D_{out}\),常規卷積的參數量為\(K^2D_{in}D_{out}\),兩者比例為 \(\frac{1}{D_{out}}+\frac{1}{K^2}\),因此可以減少參數量和計算量。
同時注意到采用了\(\rm{ReLU6}\)的激活函數,實際上\(\rm{ReLU6}(x)=\min(\max(0, x), 6)\),即給\(\rm{ReLU}\)激活函數添加一個上屆。作者認為\(\rm{ReLU6}\)在低精度計算下更加魯棒。
倒殘差模塊和線性瓶頸模塊
倒殘差模塊(Inverted Residuals)和(Linear Bottlenck)由MobileNetV2提出,用來解決MobileNet中Depthwise部分卷積核容易廢掉的問題。
出發點可以參考知乎,簡單講就是當低維信息映射到高維,經過ReLU后再映射回低維時,若映射到的維度相對較高,則信息變換回去的損失較小;若映射到的維度相對較低,則信息變換回去后損失很大。
圖中Input的的螺旋線數據記為\(X_m\),\(m=2\)表示二維數據,生成隨機矩陣\(T\)將\(X_m\)映射到\(n\)維上,通過激活函數\(ReLU\)在使用\(T^{-1}\)映射回2維空間,
即\(X'_m=T^{-1}\mathrm{ReLU}(TX_m)\),根據n的不同取值可以圖:
說明對低維數據做\(\mathrm{ReLU}\)容易造成信息的丟失,MobileNetV2設計了兩個模塊解決這個問題。
最直接的方法就是將MobileNet模塊中的最后的一個\(\mathrm{ReLU6}\)該為線性激活函數。
此外,DepthWise卷積本身並不能改變通道數量,因此可以在DW之前進行PW擴張通道數量,然后在高維度數據上進行DW,擴張因子選擇了6,即是6倍的擴張。
這樣一來就與殘差模塊中的設計不同,殘差網絡通過1x1卷積進行維度壓縮(因子0.25),因此這種設計被稱為倒殘差模塊。
兩者組合起來就是MobileNetV2中的模塊,當stride為2時,由於輸入和輸出特征圖的尺度不同,就沒有了shortcut,如圖所示:
pytorch代碼為:
class InvertedResidual(nn.Module):
def __init__(self, inp, oup, stride, expand_ratio):
super(InvertedResidual, self).__init__()
self.stride = stride
assert stride in [1, 2]
hidden_dim = int(round(inp * expand_ratio))
self.use_res_connect = self.stride == 1 and inp == oup
layers = []
if expand_ratio != 1:
# pw
layers.append(ConvBNReLU(inp, hidden_dim, kernel_size=1))
layers.extend([
# dw
ConvBNReLU(hidden_dim, hidden_dim, stride=stride, groups=hidden_dim),
# pw-linear
nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
])
self.conv = nn.Sequential(*layers)
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
激活函數
在嵌入式設備上執行sigmoid函數需要耗費相當大的計算資源,使用一種計算量小的函數去逼近它,讓它變硬(hard),可以有效降低計算消耗。
作者使用了ReLU6對其進行逼近,得到了
類似的,swish函數具備無上屆有下界、平滑、非單調的特性,並且在深層模型上的效果優於ReLU,作者使用ReLU6對其逼近,得到了h-swish激活函數。
作者認為由於特征圖尺度隨着網絡的加深而減少,非線性激活函數的成本也會隨之減少,因此僅在網絡的后半段使用h-swish代替了ReLU6,並使用h-sigmoid代替了sigmoid。
盡管使用h-swish會帶來一定量的延遲,但是在使用優化實現的h-swish可以一定量的降低延遲。
通道混合
分組卷積的一個問題是不同組之間的特征圖需要通信,不然會降低網絡的提取能力,因此在Xception和MobileNet中密集采用PW(1x1卷積)以保證不同特征圖的通信,這也導致了mobilenet 中1x1卷積占據了絕大部分的計算資源。
shufflenet提出了一種低計算復雜度的方法以保證不同組特征間的通信,如下圖(c)所示,對DW之后的特征進行混合(均勻打亂),在pytorch中僅通過維度和轉置操作即可以完成。
相應的pytorch代碼為:
class ShuffleBlock(nn.Module):
def __init__(self, groups):
super(ShuffleBlock, self).__init__()
self.groups = groups
def forward(self, x):
"""Channel shuffle: [N, C, H, W] -> [N, g, C/g, H, W] -> [N, c/g, g, H, W] -> [N, C, H, W]"""
N, C, H, W = x.size()
g = self.groups
return x.view(N, g, C / g, H, W).permute(0, 2, 1, 3, 4).contiguous().view(N, C, H, W)
如下圖所示,shufflenet中基本單元是從殘差單元改進而來,其中圖(a)是殘差單元,圖(b)和(c)分別是stride為1和2時的shufflenet基本單元。注意到在DW卷積之后沒有激活函數,1x1卷積采用了分組的形式,此外當stride=2中的shortcut采用了平均池化,和concat操作以降低運算量。
相應的pytorch代碼為:
class Bottleneck(nn.Module):
def __init__(self, in_planes, out_planes, stride, groups):
super(Bottleneck, self).__init__()
self.stride = stride
mid_planes = out_planes / 4
g = 1 if in_planes == 24 else groups
self.conv1 = nn.Conv2d(in_planes, mid_planes, kernel_size=1, groups=g, bias=False)
self.bn1 = nn.BatchNorm2d(mid_planes)
self.shuffle1 = ShuffleBlock(groups=g)
self.conv2 = nn.Conv2d(mid_planes, mid_planes, kernel_size=3, stride=stride,
padding=1, groups=mid_planes, bias=False)
self.bn2 = nn.BatchNorm2d(mid_planes)
self.conv3 = nn.Conv2d(mid_planes, out_planes, kernel_size=1, groups=groups, bias=False)
self.bn3 = nn.BatchNorm2d(out_planes)
self.shortcut = nn.Sequential() if stride == 2\
else nn.Sequential(nn.AvgPool2d(3, stride=2, padding=1))
self.relu = nn.ReLU(True)
def forward(self, x):
out = self.relu(self.bn1(self.conv1(x)))
out = self.shuffle1(out)
out = self.bn2(self.conv2(out))
out = self.bn3(self.conv3(out))
res = self.shortcut(x)
out = self.relu(torch.cat((out, res), 1)) if self.stride == 2 else self.relu(out + res)
return out
FLOPs不等同於Speed
FLOPS(floating point of per seconds)是每秒浮點運算次數,用來衡量硬件的性能。
FLOPs(floating point of operations)是浮點運算次數,可以用來衡量算法/模型的復雜度。
在輕量級網絡設計中,FLOPs經常被用來衡量網絡的速度,然而在ShuffleNetV2中提到,FLOPs並不直接等於網絡的執行速度,主要原因有:
- 內存訪問的成本(MAC)
- 模型計算的並行程度
- CuDNN對3x3卷積有特殊優化,因此1x1卷積的速度不可能9倍的快於3x3
基於以上原因,作者在使用直接指標(計算速度,即每秒batch數量)代替了間接指標(FLOPs),並在目標硬件平台(GPU和ARM)上進行了實驗,得到了四條實踐原則:
- 相同的通道寬度可以最小化內存訪問成本(MAC);
- 過量分組卷積增加MAC;
- 網絡碎片(多路徑結構)降低並行程度;
- 元素級操作(Add, ReLU等)不可忽視
在以上四條指導原則的基礎上,作者對ShuffleNetV1的基本單元進行了改進。
如下圖所示,其中(a)和(b)分別是shuffleNetV1的stride=1和2基本單元,(c)和(d)分別是shuffleNetV2的stride=1和2基本單元。
對於stried=1的基本單元,具體的改進步驟:
- 使用channel split 對輸入特征圖進行二分離,這一步相當於分組;
- 在G3的原則下,對左分支保持不變,僅右分支進行操作;
- 在G1的原則下,對右分支的操作進行三次卷積操作;
- 在G2的原則下,1x1卷積中的分組取消,第一個1x1卷積后的channel shuffle也隨之取消;
- 在G1的原則下,保持輸出特征圖不變,使用channel contact操作將左右分支合並,然后進行channel shuffle保證分組之后的通信。
這里有一個很有意思的地方,分組卷積是輕量級網絡設計的一種常用方法,但是過量的分組會增大MAC,使用channel split, shuffle, 和contact也可以實現分組與通信,且使用僅對split的一側分支進行操作,也符合G4的原則。
對應的pytorch代碼為:
import torch
from torch import nn
class MyShuffleV2Unit(nn.Module):
def __init__(self, in_channels, out_channels, stride=1, splits_left=2, groups=2):
super(MyShuffleV2Unit, self).__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.stride = stride
self.splits_left = splits_left
self.groups = groups
if stride == 2:
self.right_in_channels, self.right_out_channels = in_channels, out_channels // 2
else:
self.right_in_channels = in_channels - in_channels // splits_left
self.right_out_channels = self.right_in_channels
self.left = None if stride == 1 else \
nn.Sequential(*[
nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2,
padding=1, bias=True, groups=in_channels),
nn.BatchNorm2d(in_channels),
nn.Conv2d(in_channels, out_channels // 2, kernel_size=1, stride=1, padding=0, bias=True),
nn.BatchNorm2d(out_channels // 2),
nn.ReLU(True)
])
self.right = nn.Sequential(*[
nn.Conv2d(self.right_in_channels, self.right_in_channels, 1, 1, 0, True),
nn.BatchNorm2d(self.right_in_channels),
nn.ReLU(True),
nn.Conv2d(self.right_in_channels, self.right_in_channels, 3,
stride, 1, True, groups=self.right_in_channels),
nn.BatchNorm2d(self.right_in_channels),
nn.Conv2d(self.right_in_channels, self.right_out_channels, 1, 1, 0, True),
nn.BatchNorm2d(self.right_out_channels),
nn.ReLU(True)
])
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_uniform_(m.weight)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
def forward(self, x):
if self.stride == 2:
x_left, x_right = x, x
x_left, x_right = self.left(x_left), self.right(x_right)
else:
x_left, x_right = torch.split(x, [self.in_channels // self.splits_left,
self.in_channels // self.splits_left], dim=1)
x_right = self.right(x_right)
x = torch.cat([x_left, x_right], dim=1)
# channel_shuffle
N, C, H, W = x.size()
g = self.groups
x = x.view(N, g, C // g, H, W).permute(0, 2, 1, 3, 4).contiguous().view(N, C, H, W)
return x
參考
https://www.cnblogs.com/shine-lee/p/10243114.html
https://www.cnblogs.com/dengshunge/p/11334640.html
https://zhuanlan.zhihu.com/p/32304419
https://zhuanlan.zhihu.com/p/70703846
https://github.com/lufficc/SSD
https://github.com/Marcovaldong/LightModels