【画像分類】 【ディープラーニング】 【軽量ネットワーク】 【Pytorch版】 ShuffleNet_V1モデルアルゴリズムの詳細説明

【画像分類】 【ディープラーニング】 【軽量ネットワーク】 【Pytorch版】 ShuffleNet_V1モデルアルゴリズムの詳細説明


序文

ShuffleNet_V1 は、記事「ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices [CVPR-2018]」[論文アドレス] で Megvii Technology の Zhang 氏、Xiangyu 氏らが提案したモデルです。ポイント ボリュームの形成 積とチャネルのシャッフルを備えた軽量の CNN ネットワークにより、精度を維持しながら計算コストが大幅に削減されます。


ShuffleNet_V1の説明

一般的な畳み込みはフルチャネル畳み込み、つまりすべての入力特徴マップに対して畳み込みが実行され、これがチャネル密接続であるのに対し、グループ畳み込みはチャネル スパース接続 (チャネル スパース接続) です。グループ コンボリューションでは、入力層の異なる特徴マップをグループ化し、異なるコンボリューション カーネルを使用して各グループに対してグループ内コンボリューションを実行します。これにより、コンボリューションの計算量が削減されます。ShuffleNet_V1 の中心的な設計コンセプトは、グループ畳み込みによって引き起こされる欠点を解決するために、異なるチャネルをランダムに組み合わせて再配置 (シャッフル) することです。

グループコンボリューション (グループコンボリューション)

グループ畳み込みは、 ResNeXt [参照]で効果的に実証されています。グループコンボリューションとは、入力特徴マップを複数のグループに分割し、グループごとに独立してコンボリューションを行う手法で、計算量を削減しながらモデルの非線形性や表現力を向上させることができます。

最近、MobileNet [参考文献] は、深さ方向の分離可能な畳み込みを利用して軽量モデルを取得し、軽量モデルで重要な結果を達成しました。ShuffleNet_V1 は、グループ畳み込みと深さ分離可能な畳み込みを新しい形式で促進します。

チャンネルシャッフル

グループ化された畳み込みには欠点があります。現在のネットワーク層の特定の出力チャネルは特定の入力チャネルにのみ関連しています。つまり、特定のチャネルの出力は入力チャネルのごく一部からのみ得られるため、チャネル間の情報交換が妨げられます。グループ。
次の図は、ShuffleNet_V1 論文における通常のグループ畳み込みとチャネル シャッフル グループ畳み込みの間の詳細な概略図です:

図 (a) の異なる色は異なるグループを表し、各グループの入力は他のグループの特性と混合されません。これは各自が独自の業務を行うことに相当し、その結果、グループ間の情報が遮断されてしまいます。図 (b) に示すように、グループ化された各畳み込みが異なるグループの特徴を取得できる場合、GConv1 のすべてのグループの出力特徴はグループの数に従って均等に分散され、GConv2 の各グループの入力として使用されます。出力 (Output) チャンネルと入力チャンネルは完全に関連しています。このシャッフル操作は、図 (c) のチャネル シャッフルを通じて効率的かつエレガントに実装できます。

ShuffleNet Uint (ShuffleNet 基本ユニット)

ResNet [参考文献]に基づく残差モジュールには、チャネル シャッフル演算と深さ分離可能な畳み込み演算が追加されます。
次の図は、ShuffleNet_V1 論文の ShuffleNet ユニットの詳細な概略図です。

図 (a) は深さ分離可能な畳み込みを備えた典型的な残差構造 [参考] であり、ShuffleNet_V1 はこれに基づいて ShuffleNet ユニットを設計しました。図 (b) は、stride=1 の場合の ShuffleNet ユニットを示しています。高密度 1x1 畳み込みの代わりに 1x1 グループ化畳み込みを使用して、元の 1x1 畳み込みのコストを削減し、チャネル シャッフルを追加してクロスチャネル情報交換を実現します。図 (c) は stride=2 の場合の ShuffleNet ユニットです。特徴マップをダウンサンプリングする必要があるため、図 (b) の構造に基づいて、stride=2 の 3x3 グローバル タイ プーリングが残りの接続ブランチに使用されます。トランク出力フィーチャとブランチ フィーチャは加算ではなく連結されるため、計算量とパラメータ サイズが大幅に削減されます。

ShuffleNet_V1 モデル構造

次の図は、元の論文で示されている ShuffleNet_V1 モデル構造の詳細な概略図です。

ShuffleNet_V1 は、画像分類において 2 つの部分に分かれています:バックボーン部分:主に ShuffleNet 基本ユニット、畳み込み層とプーリング層 (集約層)、分類器で構成されます。部分: グローバル プーリング層と完全接続層で構成されます。

ShuffleNet_V1 基本ユニットでは、グループ番号 g は 1×1 畳み込みの接続スパース性を制御します。同じパラメータ制限の下で、グループの数 g が大きいほど、ネットワークのチャネル数を増やすことができます。グループの数が大きい場合は、ネットワーク パラメータをほぼ変更せずに、出力チャネルの数を増やすことができます。


ShuffleNet_V1 Pytorch コード

ShuffleNet Uint のコンポーネント:最初に次元削減に 1×1 グループ畳み込みを使用し、次にチャネル シャッフル後の特徴抽出に 3×3 深度畳み込みを使用し、最後に次元増加に 1×1 グループ畳み込みを使用します。

# 1×1卷积(降维/升维)
def conv1x1(in_chans, out_chans, n_groups=1):
    return nn.Conv2d(in_chans, out_chans, kernel_size=1, stride=1, groups=n_groups)

# 3×3深度卷积
def conv3x3(in_chans, out_chans, stride, n_groups=1):
    # Attention: no matter what the stride is, the padding will always be 1.
    return nn.Conv2d(in_chans, out_chans, kernel_size=3, padding=1, stride=stride, groups=n_groups)

チャンネルシャッフル:機能のインタラクティブ性と表現力が向上します。

def channel_shuffle(x, n_groups):
    # 获得特征图的所以维度的数据
    batch_size, chans, height, width = x.shape
    # 对特征通道进行分组
    chans_group = chans // n_groups
    # reshape新增特征图的维度
    x = x.view(batch_size, n_groups, chans_group, height, width)
    # 通道混洗(将输入张量的指定维度进行交换)
    x = torch.transpose(x, 1, 2).contiguous()
    # reshape降低特征图的维度
    x = x.view(batch_size, -1, height, width)
    return x

チャネル シャッフルのコード図を以下に示します。

ShuffleNet Uint 基本ユニット):グループ化された畳み込み層と深さ分離可能な畳み込み層 + BN 層 + 活性化関数

class ShuffleUnit(nn.Module):
    def __init__(self, in_chans, out_chans, stride, n_groups=1):
        super(ShuffleUnit, self).__init__()
        # 1×1分组卷积降维后的维度
        self.bottle_chans = out_chans // 4
        # 分组卷积的分组数
        self.n_groups = n_groups
        # 是否进行下采样()
        if stride == 1:
            # 不进行下采样,分支和主干特征形状完全一致,直接执行add相加
            self.end_op = 'Add'
            self.out_chans = out_chans
        elif stride == 2:
            # 进行下采样,分支和主干特征形状不一致,分支也需进行下采样,而后再进行concat拼接
            self.end_op = 'Concat'
            self.out_chans = out_chans - in_chans
        # 1×1卷积进行降维
        self.unit_1 = nn.Sequential(conv1x1(in_chans, self.bottle_chans, n_groups=n_groups),
                                  nn.BatchNorm2d(self.bottle_chans),
                                  nn.ReLU())
        # 3×3深度卷积进行特征提取
        self.unit_2 = nn.Sequential(conv3x3(self.bottle_chans, self.bottle_chans, stride, n_groups=n_groups),
                                    nn.BatchNorm2d(self.bottle_chans))
        # 1×1卷积进行升维
        self.unit_3 = nn.Sequential(conv1x1(self.bottle_chans, self.out_chans, n_groups=n_groups),
                                    nn.BatchNorm2d(self.out_chans))
        self.relu = nn.ReLU(inplace=True)

    def forward(self, inp):
        # 分支的处理方式(是否需要下采样)
        if self.end_op == 'Add':
            residual = inp
        else:
            residual = F.avg_pool2d(inp, kernel_size=3, stride=2, padding=1)
        x = self.unit_1(inp)
        x = channel_shuffle(x, self.n_groups)
        x = self.unit_2(x)
        x = self.unit_3(x)
        # 分支与主干的融合方式
        if self.end_op == 'Add':
            return self.relu(residual + x)
        else:
            return self.relu(torch.cat((residual, x), 1))

完全なコード

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn import init
from collections import OrderedDict
from torchsummary import summary

# 1×1卷积(降维/升维)
def conv1x1(in_chans, out_chans, n_groups=1):
    return nn.Conv2d(in_chans, out_chans, kernel_size=1, stride=1, groups=n_groups)

# 3×3深度卷积
def conv3x3(in_chans, out_chans, stride, n_groups=1):
    # Attention: no matter what the stride is, the padding will always be 1.
    return nn.Conv2d(in_chans, out_chans, kernel_size=3, padding=1, stride=stride, groups=n_groups)

def channel_shuffle(x, n_groups):
    # 获得特征图的所以维度的数据
    batch_size, chans, height, width = x.shape
    # 对特征通道进行分组
    chans_group = chans // n_groups
    # reshape新增特征图的维度
    x = x.view(batch_size, n_groups, chans_group, height, width)
    # 通道混洗(将输入张量的指定维度进行交换)
    x = torch.transpose(x, 1, 2).contiguous()
    # reshape降低特征图的维度
    x = x.view(batch_size, -1, height, width)
    return x

class ShuffleUnit(nn.Module):
    def __init__(self, in_chans, out_chans, stride, n_groups=1):
        super(ShuffleUnit, self).__init__()
        # 1×1分组卷积降维后的维度
        self.bottle_chans = out_chans // 4
        # 分组卷积的分组数
        self.n_groups = n_groups
        # 是否进行下采样()
        if stride == 1:
            # 不进行下采样,分支和主干特征形状完全一致,直接执行add相加
            self.end_op = 'Add'
            self.out_chans = out_chans
        elif stride == 2:
            # 进行下采样,分支和主干特征形状不一致,分支也需进行下采样,而后再进行concat拼接
            self.end_op = 'Concat'
            self.out_chans = out_chans - in_chans
        # 1×1卷积进行降维
        self.unit_1 = nn.Sequential(conv1x1(in_chans, self.bottle_chans, n_groups=n_groups),
                                  nn.BatchNorm2d(self.bottle_chans),
                                  nn.ReLU())
        # 3×3深度卷积进行特征提取
        self.unit_2 = nn.Sequential(conv3x3(self.bottle_chans, self.bottle_chans, stride, n_groups=n_groups),
                                    nn.BatchNorm2d(self.bottle_chans))
        # 1×1卷积进行升维
        self.unit_3 = nn.Sequential(conv1x1(self.bottle_chans, self.out_chans, n_groups=n_groups),
                                    nn.BatchNorm2d(self.out_chans))
        self.relu = nn.ReLU(inplace=True)

    def forward(self, inp):
        # 分支的处理方式(是否需要下采样)
        if self.end_op == 'Add':
            residual = inp
        else:
            residual = F.avg_pool2d(inp, kernel_size=3, stride=2, padding=1)
        x = self.unit_1(inp)
        x = channel_shuffle(x, self.n_groups)
        x = self.unit_2(x)
        x = self.unit_3(x)
        # 分支与主干的融合方式
        if self.end_op == 'Add':
            return self.relu(residual + x)
        else:
            return self.relu(torch.cat((residual, x), 1))

class ShuffleNetV1(nn.Module):
    def __init__(self, n_groups, n_classes, stage_out_chans):
        super(ShuffleNetV1, self).__init__()
        # 输入通道
        self.in_chans = 3
        # 分组组数
        self.n_groups = n_groups
        # 分类个数
        self.n_classes = n_classes

        self.conv1 = conv3x3(self.in_chans, 24, 2)
        self.maxpool = nn.MaxPool2d(3, 2, 1)

        # Stage 2
        op = OrderedDict()
        unit_prefix = 'stage_2_unit_'
        # 每个Stage的首个基础单元都需要进行下采样,其他单元不需要
        op[unit_prefix+'0'] = ShuffleUnit(24, stage_out_chans[0], 2, self.n_groups)
        for i in range(3):
            op[unit_prefix+str(i+1)] = ShuffleUnit(stage_out_chans[0], stage_out_chans[0], 1, self.n_groups)
        self.stage2 = nn.Sequential(op)

        # Stage 3
        op = OrderedDict()
        unit_prefix = 'stage_3_unit_'
        op[unit_prefix+'0'] = ShuffleUnit(stage_out_chans[0], stage_out_chans[1], 2, self.n_groups)
        for i in range(7):
            op[unit_prefix+str(i+1)] = ShuffleUnit(stage_out_chans[1], stage_out_chans[1], 1, self.n_groups)
        self.stage3 = nn.Sequential(op)

        # Stage 4
        op = OrderedDict()
        unit_prefix = 'stage_4_unit_'
        op[unit_prefix+'0'] = ShuffleUnit(stage_out_chans[1], stage_out_chans[2], 2, self.n_groups)
        for i in range(3):
            op[unit_prefix+str(i+1)] = ShuffleUnit(stage_out_chans[2], stage_out_chans[2], 1, self.n_groups)
        self.stage4 = nn.Sequential(op)

        # 全局平局池化
        self.global_pool =nn.AdaptiveAvgPool2d((1, 1))
        # 全连接层
        self.fc = nn.Linear(stage_out_chans[-1], self.n_classes)
        # 权重初始化
        self.init_params()

    # 权重初始化
    def init_params(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out')
                if m.bias is not None:
                    nn.init.zeros_(m.bias)
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.ones_(m.weight)
                nn.init.zeros_(m.bias)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)
                nn.init.zeros_(m.bias)

    def forward(self, x):
        x = self.conv1(x)
        x = self.maxpool(x)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.stage4(x)
        x = self.global_pool(x)
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        return x

# 不同分组数对应的通道数也不同
stage_out_chans_list = [[144, 288, 576], [200, 400, 800], [240, 480, 960],
                                [272, 544, 1088], [384, 768, 1536]]
def shufflenet_v1_groups1(n_groups=1, n_classes=1000):
    model = ShuffleNetV1(n_groups=n_groups, n_classes=n_classes, stage_out_chans=stage_out_chans_list[n_groups-1])
    return model

def shufflenet_v1_groups2(n_groups=2, n_classes=1000):
    model = ShuffleNetV1(n_groups=n_groups, n_classes=n_classes, stage_out_chans=stage_out_chans_list[n_groups-1])
    return model

def shufflenet_v1_groups3(n_groups=3, n_classes=1000):
    model = ShuffleNetV1(n_groups=n_groups, n_classes=n_classes, stage_out_chans=stage_out_chans_list[n_groups-1])
    return model

def shufflenet_v1_groups4(n_groups=4, n_classes=1000):
    model = ShuffleNetV1(n_groups=n_groups, n_classes=n_classes, stage_out_chans=stage_out_chans_list[n_groups-1])
    return model

def shufflenet_v1_groupsother(n_groups=5, n_classes=1000):
    # groups>4
    model = ShuffleNetV1(n_groups=n_groups, n_classes=n_classes, stage_out_chans=stage_out_chans_list[-1])
    return model

if __name__ == '__main__':
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    model = shufflenet_v1_groups1().to(device)
    summary(model, input_size=(3, 224, 224))

summary ではネットワーク構造とパラメータを出力できるため、構築されたネットワーク構造を簡単に確認できます。


要約する

グループ化畳み込みチャネル シャッフルの原理とプロセスをできるだけ簡単かつ詳細に紹介し、ShuffleNet_V1 モデルの構造と pytorch コードを説明します。

おすすめ

転載: blog.csdn.net/yangyu0515/article/details/134929409
おすすめ