天天看點

輕量級網絡——ShuffleNetV1

文章目錄

      • 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操作
輕量級網絡——ShuffleNetV1

對于圖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,重新拼接成一個新的組。

輕量級網絡——ShuffleNetV1

這樣進行了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操作而不是相加。極緻的降低計算量與參數大小。

輕量級網絡——ShuffleNetV1

3.ShuffleNetV1的性能統計

  • 參數量
    輕量級網絡——ShuffleNetV1

與ResNet和ResNeXt網絡的參數使用對比

輕量級網絡——ShuffleNetV1

計算可以知道,ShuffleNetV1的參數使用量比ResNet和ResNeXt網絡的參數都要少。

  • 實時性

下圖可以看到,ShuffleNetV1與AlexNet的錯誤率相識,在曉龍820處理器上的推理時間上可以看見,ShuffleNetV1隻需要15ms,而AlexNet需要184ms,推理時間提升的還是比較高的。(是以的結果應用的是單線程處理)

輕量級網絡——ShuffleNetV1
  • 準确率

下表給出了不同g值(分組數)的ShuffleNet在ImageNet上的實驗結果。可以看到基本上當g越大時,效果越好,這是因為采用更多的分組後,在相同的計算限制下可以使用更多的通道數,或者說特征圖數量增加,網絡的特征提取能力增強,網絡性能得到提升。注意Shuffle 1x是基準模型,而0.5x和0.25x表示的是在基準模型上将通道數縮小為原來的0.5和0.25。

輕量級網絡——ShuffleNetV1

除此之外,作者還對比了不采用channle shuffle和采用之後的網絡性能對比,如下表的看到,采用channle shuffle之後,網絡性能更好,進而證明channle shuffle的有效性。

輕量級網絡——ShuffleNetV1

然後是ShuffleNet與MobileNet的對比,如下表ShuffleNet不僅計算複雜度更低,而且精度更好。

輕量級網絡——ShuffleNetV1

4.ShuffleNetV1的pytorch實作

輕量級網絡——ShuffleNetV1

可以看到開始使用的普通的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版本結構相差不大。

輕量級網絡——ShuffleNetV1

參考:

https://blog.csdn.net/yzy__zju/article/details/107746203

繼續閱讀