輕量級卷積


分組卷積

分組卷積(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對其進行逼近,得到了

\[\rm{h-sigmoid} = \frac{\rm{ReLU6}(x+3)}{6} \]

類似的,swish函數具備無上屆有下界、平滑、非單調的特性,並且在深層模型上的效果優於ReLU,作者使用ReLU6對其逼近,得到了h-swish激活函數。

\[\rm{h-swish} = x\frac{\rm{ReLU6}(x+3)}{6} \]

作者認為由於特征圖尺度隨着網絡的加深而減少,非線性激活函數的成本也會隨之減少,因此僅在網絡的后半段使用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並不直接等於網絡的執行速度,主要原因有:

  1. 內存訪問的成本(MAC)
  2. 模型計算的並行程度
  3. CuDNN對3x3卷積有特殊優化,因此1x1卷積的速度不可能9倍的快於3x3

基於以上原因,作者在使用直接指標(計算速度,即每秒batch數量)代替了間接指標(FLOPs),並在目標硬件平台(GPU和ARM)上進行了實驗,得到了四條實踐原則:

  1. 相同的通道寬度可以最小化內存訪問成本(MAC);
  2. 過量分組卷積增加MAC;
  3. 網絡碎片(多路徑結構)降低並行程度;
  4. 元素級操作(Add, ReLU等)不可忽視

在以上四條指導原則的基礎上,作者對ShuffleNetV1的基本單元進行了改進。
如下圖所示,其中(a)和(b)分別是shuffleNetV1的stride=1和2基本單元,(c)和(d)分別是shuffleNetV2的stride=1和2基本單元。
對於stried=1的基本單元,具體的改進步驟:

  1. 使用channel split 對輸入特征圖進行二分離,這一步相當於分組;
  2. 在G3的原則下,對左分支保持不變,僅右分支進行操作;
  3. 在G1的原則下,對右分支的操作進行三次卷積操作;
  4. 在G2的原則下,1x1卷積中的分組取消,第一個1x1卷積后的channel shuffle也隨之取消;
  5. 在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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM