利用Pytorch实现ShuffleNet网络

目  录

1. ShuffleNet v1网络

1.1 channel shuffle

1.2 ShuffleNet v1网络结构

2. ShuffleNet v2网络

2.1 设计block的4条准则

2.2 ShuffleNet v2网络结构

3 利用Pytorch实现ShuffleNet

3.1 channel shuffle的具体实现

3.2 ShuffleNet v2的block

3.3 ShuffleNet v2网络结构

扫描二维码关注公众号,回复: 15382328 查看本文章

4. 训练结果


1. ShuffleNet v1网络

1.1 channel shuffle

对于图(a)中简单的组卷积,输入特征矩阵通过两个串行的GConv,每次卷积都是针对每组内的channel信息进行卷积操作(图中对应着不同颜色),这样虽然能减少参数与计算量,但是每个group之间没有任何的信息交流。

针对这个问题,作者提出了channel shuffle的思想:输入特征矩阵先经过一个GConv,得到对应的输出特征矩阵,以图(b)为例,如果GConv分为3个组,得到的每个组的输出特征矩阵再进行划分,每个特征矩阵再划分为3份;再将每个group的第1份放在一起,每个group的第2份放在一起,第三份同理,融合结果如图(c)所示;将融合后的特征矩阵再进行GConv,这样就能融合不同group之间的channel信息。

在ResNeXt网络当中,残差结构中的1×1的卷积核占据了总计算量的93.4%,因此在ShuffleNet中,将1×1的卷积(图a)全部换成了1×1的GConv(图b,stride=1),图(c)是stride=2的情况,注意最后是拼接在一起而非相加。


1.2 ShuffleNet v1网络结构

其中:①对于每个stage的第一个block采用步长为2(上面图c);②对于下一个stage输出特征矩阵会进行翻倍操作;③和ResNet相似,1×1 GConv和3×3 DWConv的输出特征矩阵channel是整个block的输出特征矩阵channel的1/4;④对于stage2,不使用GConv,而是使用的普通卷积,因为输入channel很小。


2. ShuffleNet v2网络

FLOPs:指浮点运算数,理解为计算量,可以用来衡量算法或模型的复杂度。在ShuffleNet v2网络中,作者提出计算复杂度不能只看FLOPs,并提出了4条如何设计高效网络的准则,基于该准则提出了新的block设计。

2.1 设计block的4条准则

①等通道宽度使内存访问成本(MAC)最小化:当卷积层的输入特征矩阵与输出特征矩阵channel相等时MAC最小(FLOPs要保持不变),在构建网络时要使用平衡的卷积层,尽可能让输入特征矩阵的channel和输出特征矩阵的channel比值接近1:1;

②增大的组卷积的组会增加MAC:当GConv的groups增大时(保持FLOPs不变),MAC会增大,不能一昧的增大group数;

③网络碎片化会降低并行度:网络设计的碎片化程度越高(保持FLOPs不变),速度越慢,碎片化程度可以理解为分支程度,如果想要高效的网络不要设计过多的分支结构;

④Element-wise操作带来的影响不可忽视:Element-wise操作包括ReLU、AddTensor和AddBias等,这些操作的FLOPs很小但是MAC很大。

每个block单元会将输入特征矩阵划分为两个分支(对应图c的Channel Split),分别为c-{c}'{c}'。为了控制网络的碎片化程度(对应准则③),左面的分支没有任何操作;右面的分支由三个卷积层组成,他们的输入channel和输出channel相同(对应准则①);两个1×1的Conv没有使用组卷积(对应准则②);两个分支计算完之后是通过concat连接(对应输入时的split),保证整个block块的输入channel和输出channel一致(对应准则①);ShuffleNet v1是对Add或concat之后的特征矩阵进行ReLU,而ShuffleNet v2只对右面分支的输出进行ReLU,这样使得后面的Concat、Channel Shuffle和下一层的Channel Split可以归并为一个Element-wise,减少了操作个数(对应准则④)。

对于图(d)stride=2的情况,没有Channel Split的操作,所以输出特征矩阵深度会翻倍(因为是两个特征矩阵的concat)。


2.2 ShuffleNet v2网络结构


3 利用Pytorch实现ShuffleNet

3.1 channel shuffle的具体实现

view函数的作用:

transpose函数的作用:

再利用view函数还原:

实现函数:

def channel_shuffle(x: Tensor, groups: int):

    # 在pytorch中所得到的tensor通道排列顺序
    batch_size, num_channels, height, width = x.size()
    # 将channels划分为groups组
    channels_per_group = num_channels // groups

    # reshape
    # [batch_size, num_channels, height, width] → [batch_size, groups, channels_per_group, height, width]
    x = x.view(batch_size, groups, channels_per_group, height, width)

    # 将维度1(groups)和维度2(channels_per_group)的信息进行交换
    # transpose后,tensor在内存中的存储顺序不是连续的,contiguous可以将数据转化为连续的数据
    x = torch.transpose(x, 1, 2).contiguous()

    # flatten
    x = x.view(batch_size, -1, height, width)

    return x

3.2 ShuffleNet v2的block

class InvertedResidual(nn.Module):
    # 输入特征矩阵channel,输出特征矩阵channel,步长
    def __init__(self, input_c: int, output_c: int, stride: int):
        super(InvertedResidual, self).__init__()

        # 步长只能为1或2
        if stride not in [1, 2]:
            raise ValueError("illegal stride value.")
        self.stride = stride

        # 判断输出特征矩阵channel是否为2的整数倍,因为concat拼接两个channel相同的矩阵,输出一定是2的整数倍
        assert output_c % 2 == 0
        # 两个分支的channel等于输出特征矩阵channel/2
        branch_features = output_c // 2

        # 当stride为1时,input_channel应该是branch_features的两倍
        # python中 '<<' 是位运算,可理解为计算×2的快速方法
        assert (self.stride != 1) or (input_c == branch_features << 1)

        # stride=2的block
        if self.stride == 2:
            # 左分支
            self.branch1 = nn.Sequential(
                self.depthwise_conv(input_c, input_c, kernel_s=3, stride=self.stride, padding=1),
                nn.BatchNorm2d(input_c),
                nn.Conv2d(input_c, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
                nn.BatchNorm2d(branch_features),
                nn.ReLU(inplace=True)
            )
        else:
            # stride=1的左分支不做任何处理
            self.branch1 = nn.Sequential()

        # 右分支
        self.branch2 = nn.Sequential(
            # stride=2输入特征矩阵channel就是block的输入,stride=1输入特征矩阵channel是一半
            nn.Conv2d(input_c if self.stride > 1 else branch_features, branch_features, kernel_size=1,
                      stride=1, padding=0, bias=False),
            nn.BatchNorm2d(branch_features),
            nn.ReLU(inplace=True),
            self.depthwise_conv(branch_features, branch_features, kernel_s=3, stride=self.stride, padding=1),
            nn.BatchNorm2d(branch_features),
            nn.Conv2d(branch_features, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
            nn.BatchNorm2d(branch_features),
            nn.ReLU(inplace=True)
        )

    @staticmethod
    # 静态方法DW卷积
    def depthwise_conv(input_c: int,
                       output_c: int,
                       kernel_s: int,
                       stride: int = 1,
                       padding: int = 0,
                       bias: bool = False):
        # 有BN层不需要加入偏置,博客http://t.csdn.cn/5Xxb0
        return nn.Conv2d(in_channels=input_c, out_channels=output_c, kernel_size=kernel_s,
                         stride=stride, padding=padding, bias=bias, groups=input_c)

    def forward(self, x: Tensor):
        if self.stride == 1:
            # chunk均分为两份,channel对应的维度(dim)为1
            x1, x2 = x.chunk(2, dim=1)
            # stride=1对x1不做任何处理,和x2经过右分支后的输出进行concat拼接(在channel维度dim=1)
            out = torch.cat((x1, self.branch2(x2)), dim=1)
        else:
            # stride=2
            out = torch.cat((self.branch1(x), self.branch2(x)), dim=1)

        # 将输出进行channel shuffle处理
        out = channel_shuffle(out, 2)

        return out

3.3 ShuffleNet v2网络结构

class ShuffleNetV2(nn.Module):
    def __init__(self,
                 # 对应每个stage的block的重复次数
                 stages_repeats: List[int],
                 # 每个stage输出特征矩阵的channel
                 stages_out_channels: List[int],
                 num_classes: int = 1000,
                 # 每个stage的block
                 inverted_residual: Callable[..., nn.Module] = InvertedResidual):
        super(ShuffleNetV2, self).__init__()

        # 判断stages_repeats是否是3个元素,stages_out_channels是否是5个元素
        if len(stages_repeats) != 3:
            raise ValueError("expected stages_repeats as list of 3 positive ints")
        if len(stages_out_channels) != 5:
            raise ValueError("expected stages_out_channels as list of 5 positive ints")
        self._stage_out_channels = stages_out_channels

        # input RGB image
        input_channels = 3
        # Conv1的输出是_stage_out_channels里的第一个值
        output_channels = self._stage_out_channels[0]

        # Conv1
        self.conv1 = nn.Sequential(
            nn.Conv2d(input_channels, output_channels, kernel_size=3, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(output_channels),
            nn.ReLU(inplace=True)
        )
        # 将输出channel数传给下一个stage的输入channel数
        input_channels = output_channels

        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        # Static annotations for mypy
        self.stage2: nn.Sequential
        self.stage3: nn.Sequential
        self.stage4: nn.Sequential

        # 得到stage2、3、4
        stage_names = ["stage{}".format(i) for i in [2, 3, 4]]
        # 通过zip同时遍历stage_names、stages_repeats和_stage_out_channels(从1开始遍历)
        for name, repeats, output_channels in zip(stage_names, stages_repeats,
                                                  self._stage_out_channels[1:]):
            # 每个stage第一个block的stride=2
            seq = [inverted_residual(input_channels, output_channels, 2)]
            # 每个stage的剩余block的stride=1
            for i in range(repeats - 1):
                seq.append(inverted_residual(output_channels, output_channels, 1))
            # stage名+一系列层结构
            setattr(self, name, nn.Sequential(*seq))
            # 再把当前的output_channels赋值给下一个stage的input_channels
            input_channels = output_channels

        # 把stage_out_channels的最后一个元素(Conv5的输出特征矩阵channel)传给Conv5的output_channels
        output_channels = self._stage_out_channels[-1]
        # Conv5
        self.conv5 = nn.Sequential(
            nn.Conv2d(input_channels, output_channels, kernel_size=1, stride=1, padding=0, bias=False),
            nn.BatchNorm2d(output_channels),
            nn.ReLU(inplace=True)
        )

        # 全连接层
        self.fc = nn.Linear(output_channels, num_classes)

    def _forward_impl(self, x: Tensor):
        # See note [TorchScript super()]
        x = self.conv1(x)
        x = self.maxpool(x)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.stage4(x)
        x = self.conv5(x)
        x = x.mean([2, 3])  # global pool,解释:http://t.csdn.cn/11L6k
        x = self.fc(x)
        return x

    def forward(self, x: Tensor):
        return self._forward_impl(x)

4. 训练结果

不使用预训练权重,训练所有参数,经过30个epoch的结果:

[epoch 26] mean loss 0.554: 100%|██████████| 184/184 [00:18<00:00, 10.00it/s]
100%|██████████| 46/46 [00:09<00:00,  5.06it/s]
[epoch 26] accuracy: 0.826
[epoch 27] mean loss 0.547: 100%|██████████| 184/184 [00:18<00:00, 10.07it/s]
100%|██████████| 46/46 [00:10<00:00,  4.45it/s]
[epoch 27] accuracy: 0.822
[epoch 28] mean loss 0.526: 100%|██████████| 184/184 [00:18<00:00,  9.70it/s]
100%|██████████| 46/46 [00:09<00:00,  4.78it/s]
[epoch 28] accuracy: 0.843
[epoch 29] mean loss 0.535: 100%|██████████| 184/184 [00:18<00:00,  9.76it/s]
100%|██████████| 46/46 [00:10<00:00,  4.59it/s]
[epoch 29] accuracy: 0.843

猜你喜欢

转载自blog.csdn.net/AdjsWsgz/article/details/130539704