文章目錄
-
-
- 1.ShuffleNetV1的介紹
- 2.ShuffleNetV1的結構
-
-
- 1)Channel Shuffle操作
- 2)ShuffleNet基本單元
-
- 3.ShuffleNetV1的性能統計
- 4.ShuffleNetV1的pytorch實作
-
1.ShuffleNetV1的介紹
- 分組卷積
Group convolution是将輸入層的不同特征圖進行分組,然後采用不同的卷積核再對各個組進行卷積,這樣會降低卷積的計算量。因為一般的卷積都是在所有的輸入特征圖上做卷積,可以說是全通道卷積,這是一種通道密集連接配接方式(channel dense connection),而group convolution相比則是一種通道稀疏連接配接方式(channel sparse connection)。
- 分組卷積的沖突——計算量
使用group convolution的網絡有很多,如Xception,MobileNet,ResNeXt等。其中Xception和MobileNet采用了depthwise convolution,這是一種比較特殊的group convolution,此時分組數恰好等于通道數,意味着每個組隻有一個特征圖。但這些網絡存在一個很大的弊端:采用了密集的1x1 pointwise convolution。在RexNeXt結構中,其實3x3的組卷積隻占據了很少的計算量,而93.4%的計算量都是1x1的卷積所占據的理論計算量。
這個問題可以解決:對1x1卷積采用channel sparse connection, 即分組卷積,那樣計算量就可以降下來了,但這就涉及到下面一個問題。
- 分組卷積的沖突——特征通信
group convolution層另一個問題是不同組之間的特征圖需要通信,否則就好像分了幾個互不相幹的路,大家各走各的,會降低網絡的特征提取能力,這也可以解釋為什麼Xception,MobileNet等網絡采用密集的1x1 pointwise convolution,因為要保證group convolution之後不同組的特征圖之間的資訊交流。
- channel shuffle的引出
為達到特征通信目的,我們不采用dense pointwise convolution,考慮其他的思路:channel shuffle。其含義就是對group convolution之後的特征圖進行“重組”,這樣可以保證接下了采用的group convolution其輸入來自不同的組,是以資訊可以在不同組之間流轉。進一步的展示了這一過程并随機,其實是“均勻地打亂”。
2.ShuffleNetV1的結構
1)Channel Shuffle操作

對于圖a可以看見,特征矩陣會通過兩個串行的組卷積操作計算。而對于普通的組卷積的計算可以發現,每次的卷積都是針對組内的一些特定的channel進行計卷積操作。也就是一直都是對同一個組進行卷積處理,每一個組内之間是沒有進行交流的。
GConv雖然能夠減少參數與計算量,但GConv中不同組之間資訊沒有交流。是以基于這個問題,ShuffleNetV1提出了channels shuffle的思想。
如圖b所示,對于輸入的特征矩陣,通過了GConv卷積之後得到的特征矩陣,對這些G組的特征矩陣的内部同樣劃分為G組,也就是現在有原來的G份變成了G*G份。那麼,對于每一個大組内的G組中的同樣位置,來重新構成一個channel,也就是有第1組的第1個channel,第2組的第1個channel,第3組的第1個channel,重新拼接成一個新的組。
這樣進行了Channel shuffle操作之後,再進行組卷積,那麼現在就可以融合不同group之間的特征資訊。這個就是ShuffleNetV1中的Channel shuffle思想。
2)ShuffleNet基本單元
下圖a展示了基本ResNet輕量級結構,這是一個包含3層的殘差單元:首先是1x1卷積,然後是3x3的depthwise convolution(DWConv,主要是為了降低計算量),緊接着是1x1卷積,最後是一個短路連接配接,将輸入直接加到輸出上。
下圖b展示了改進思路:将密集的1x1卷積替換成1x1的group convolution(因為前訴了主要計算量較大的地方就是這個密集的1x1的卷積操作),不過在第一個1x1卷積之後增加了一個channel shuffle操作。值得注意的是3x3卷積後面沒有增加channel shuffle,按paper的意思,對于這樣一個殘差單元,一個channel shuffle操作是足夠了。還有就是3x3的depthwise convolution之後沒有使用ReLU激活函數。這是針對stride為1的情況。
下圖c的降采樣版本,對原輸入采用stride=2的3x3 avg pool,在depthwise convolution卷積處取stride=2保證兩個通路shape相同,然後将得到特征圖與輸出進行連接配接concat操作而不是相加。極緻的降低計算量與參數大小。
3.ShuffleNetV1的性能統計
- 參數量
輕量級網絡——ShuffleNetV1
與ResNet和ResNeXt網絡的參數使用對比
計算可以知道,ShuffleNetV1的參數使用量比ResNet和ResNeXt網絡的參數都要少。
- 實時性
下圖可以看到,ShuffleNetV1與AlexNet的錯誤率相識,在曉龍820處理器上的推理時間上可以看見,ShuffleNetV1隻需要15ms,而AlexNet需要184ms,推理時間提升的還是比較高的。(是以的結果應用的是單線程處理)
- 準确率
下表給出了不同g值(分組數)的ShuffleNet在ImageNet上的實驗結果。可以看到基本上當g越大時,效果越好,這是因為采用更多的分組後,在相同的計算限制下可以使用更多的通道數,或者說特征圖數量增加,網絡的特征提取能力增強,網絡性能得到提升。注意Shuffle 1x是基準模型,而0.5x和0.25x表示的是在基準模型上将通道數縮小為原來的0.5和0.25。
除此之外,作者還對比了不采用channle shuffle和采用之後的網絡性能對比,如下表的看到,采用channle shuffle之後,網絡性能更好,進而證明channle shuffle的有效性。
然後是ShuffleNet與MobileNet的對比,如下表ShuffleNet不僅計算複雜度更低,而且精度更好。
4.ShuffleNetV1的pytorch實作
可以看到開始使用的普通的3x3的卷積和max pool層。然後是三個階段,每個階段都是重複堆積了幾個ShuffleNet的基本單元。對于每個階段,第一個基本單元采用的是stride=2,這樣特征圖width和height各降低一半,而通道數增加一倍。後面的基本單元都是stride=1,特征圖和通道數都保持不變。對于基本單元來說,其中瓶頸層,就是3x3卷積層的通道數為輸出通道數的1/4,這和殘差單元的設計理念是一樣的。還有其中的g表示的是分組的數量,其中較多論文使用的是g=3的版本。
參考代碼:
import torch
import torch.nn as nn
import torchvision
# 分類數
num_class = 5
# DW卷積
def Conv3x3BNReLU(in_channels,out_channels,stride,groups):
return nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=stride, padding=1,groups=groups),
nn.BatchNorm2d(out_channels),
nn.ReLU6(inplace=True)
)
# 普通的1x1卷積
def Conv1x1BNReLU(in_channels,out_channels,groups):
return nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1,groups=groups),
nn.BatchNorm2d(out_channels),
nn.ReLU6(inplace=True)
)
# PW卷積(不使用激活函數)
def Conv1x1BN(in_channels,out_channels,groups):
return nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1,groups=groups),
nn.BatchNorm2d(out_channels)
)
# channel重組操作
class ChannelShuffle(nn.Module):
def __init__(self, groups):
super(ChannelShuffle, 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, int(C / g), H, W).permute(0, 2, 1, 3, 4).contiguous().view(N, C, H, W)
# ShuffleNetV1的單元結構
class ShuffleNetUnits(nn.Module):
def __init__(self, in_channels, out_channels, stride, groups):
super(ShuffleNetUnits, self).__init__()
self.stride = stride
# print("in_channels:", in_channels, "out_channels:", out_channels)
# 當stride=2時,為了不因為 in_channels+out_channels 不與 out_channels相等,需要先減,這樣拼接的時候數值才會不變
out_channels = out_channels - in_channels if self.stride >1 else out_channels
# 結構中的前一個1x1組卷積與3x3元件是次元的最後一次1x1組卷積的1/4,與ResNet類似
mid_channels = out_channels // 4
# print("out_channels:",out_channels,"mid_channels:",mid_channels)
# ShuffleNet基本單元: 1x1組卷積 -> ChannelShuffle -> 3x3組卷積 -> 1x1組卷積
self.bottleneck = nn.Sequential(
# 1x1組卷積升維
Conv1x1BNReLU(in_channels, mid_channels,groups),
# channelshuffle實作channel重組
ChannelShuffle(groups),
# 3x3組卷積改變尺寸
Conv3x3BNReLU(mid_channels, mid_channels, stride,groups),
# 1x1組卷積降維
Conv1x1BN(mid_channels, out_channels,groups)
)
# 當stride=2時,需要進行池化操作然後拼接起來
if self.stride > 1:
# hw減半
self.shortcut = nn.AvgPool2d(kernel_size=3, stride=2, padding=1)
self.relu = nn.ReLU6(inplace=True)
def forward(self, x):
out = self.bottleneck(x)
# 如果是stride=2,則将池化後的結果與通過基本單元的結果拼接在一起, 否則直接将輸入與通過基本單元的結果相加
out = torch.cat([self.shortcut(x),out],dim=1) if self.stride >1 else (out + x)
# 假設目前想要輸出的channel為240,但此時stride=2,需要将輸出與池化後的輸入作拼接,此時的channel為24,24+240=264
# torch.Size([1, 264, 28, 28]), 但是想輸出的是240, 是以在這裡 out_channels 要先減去 in_channels
# torch.Size([1, 240, 28, 28]), 這是先減去的結果
# if self.stride > 1:
# out = torch.cat([self.shortcut(x),out],dim=1)
# 當stride為1時,直接相加即可
# if self.stride == 1:
# out = out+x
return self.relu(out)
class ShuffleNet(nn.Module):
def __init__(self, planes, layers, groups, num_classes=num_class):
super(ShuffleNet, self).__init__()
# Conv1的輸入channel隻有24, 不算大,是以可以不用使用組卷積
self.stage1 = nn.Sequential(
Conv3x3BNReLU(in_channels=3,out_channels=24,stride=2, groups=1), # torch.Size([1, 24, 112, 112])
nn.MaxPool2d(kernel_size=3,stride=2,padding=1) # torch.Size([1, 24, 56, 56])
)
# 以Group = 3為例 4/8/4層堆疊結構
# 24 -> 240, groups=3 4層 is_stage2=True,stage2第一層不需要使用組卷積,其餘全部使用組卷積
self.stage2 = self._make_layer(24,planes[0], groups, layers[0], True)
# 240 -> 480, groups=3 8層 is_stage2=False,全部使用組卷積,減少計算量
self.stage3 = self._make_layer(planes[0],planes[1], groups, layers[1], False)
# 480 -> 960, groups=3 4層 is_stage2=False,全部使用組卷積,減少計算量
self.stage4 = self._make_layer(planes[1],planes[2], groups, layers[2], False)
# in: torch.Size([1, 960, 7, 7])
self.global_pool = nn.AvgPool2d(kernel_size=7, stride=1)
self.dropout = nn.Dropout(p=0.2)
# group=3時最後channel為960,是以in_features=960
self.linear = nn.Linear(in_features=planes[2], out_features=num_classes)
# 權重初始化操作
self.init_params()
def _make_layer(self, in_channels,out_channels, groups, block_num, is_stage2):
layers = []
# torch.Size([1, 240, 28, 28])
# torch.Size([1, 480, 14, 14])
# torch.Size([1, 960, 7, 7])
# 每個Stage的第一個基本單元stride均為2,其他單元的stride為1。且stage2的第一個基本單元不使用組卷積,因為參數量不大。
layers.append(ShuffleNetUnits(in_channels=in_channels, out_channels=out_channels, stride=2, groups=1 if is_stage2 else groups))
# 每個Stage的非第一個基本單元stride均為1,且全部使用組卷積,來減少參數計算量, 再疊加block_num-1層
for idx in range(1, block_num):
layers.append(ShuffleNetUnits(in_channels=out_channels, out_channels=out_channels, stride=1, groups=groups))
return nn.Sequential(*layers)
# 初始化權重
def init_params(self):
for m in self.modules():
if isinstance(m,nn.Conv2d):
nn.init.kaiming_normal_(m.weight)
nn.init.constant_(m.bias,0)
elif isinstance(m, nn.BatchNorm2d) or isinstance(m, nn.Linear):
nn.init.constant_(m.weight,1)
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = self.stage1(x) # torch.Size([1, 24, 56, 56])
x = self.stage2(x) # torch.Size([1, 240, 28, 28])
x = self.stage3(x) # torch.Size([1, 480, 14, 14])
x = self.stage4(x) # torch.Size([1, 960, 7, 7])
x = self.global_pool(x) # torch.Size([1, 960, 1, 1])
x = x.view(x.size(0), -1) # torch.Size([1, 960])
x = self.dropout(x)
x = self.linear(x) # torch.Size([1, 5])
return x
# planes 是Stage2,Stage3,Stage4輸出的參數
# layers 是Stage2,Stage3,Stage4的層數
# g1/2/3/4/8 指的是組卷積操作時的分組數
def shufflenet_g8(**kwargs):
planes = [384, 768, 1536]
layers = [4, 8, 4]
model = ShuffleNet(planes, layers, groups=8)
return model
def shufflenet_g4(**kwargs):
planes = [272, 544, 1088]
layers = [4, 8, 4]
model = ShuffleNet(planes, layers, groups=4)
return model
def shufflenet_g3(**kwargs):
planes = [240, 480, 960]
layers = [4, 8, 4]
model = ShuffleNet(planes, layers, groups=3)
return model
def shufflenet_g2(**kwargs):
planes = [200, 400, 800]
layers = [4, 8, 4]
model = ShuffleNet(planes, layers, groups=2)
return model
def shufflenet_g1(**kwargs):
planes = [144, 288, 576]
layers = [4, 8, 4]
model = ShuffleNet(planes, layers, groups=1)
return model
if __name__ == '__main__':
# model = shufflenet_g3() # 常用
model = shufflenet_g8()
# print(model)
input = torch.randn(1, 3, 224, 224)
out = model(input)
print(out.shape)
訓練出來的模型大小有7M左右,與MobileNetV3的small版本結構相差不大。
參考:
https://blog.csdn.net/yzy__zju/article/details/107746203