shuffleNet v2笔记

作者从内存访问代价(Memory Access Cost,MAC)和GPU并行性的方向分析了网络应该怎么设计才能进一步减少运行时间,直接的提高模型的效率

设计原则

  • Equal channel width minimizes memory access cost (MAC)
  • Excessive group convolution increases MAC
  • Network fragmentation reduces degree of parallelism
  • Element-wise operations are non-negligible

Equal channel width minimizes memory access cost

当输入通道和输出通道数量相同的时候,mac最小.

假如一个卷积操作的输入特征图尺寸为:C_{1} * W * H,输出特征图大小为:C_{2} * W * H . 则该卷积操作的FLOPS为C_{1}*C_{2} * W * H.
在这个计算过程中,输入特征图占用内存为:C_{1} * W * H,输出特征图占用内存为:C_{2} * W * H,卷积核占用内存为:C_{1} * C_{2}

则总计内存访问为:

B=H*W *C_{1} *C_{2},则得:
\begin{aligned} MAC&= H * W *(C_{1}+C_{2})+C_{1}*C_{2}\\ &=\sqrt{(H * W)^{2} * (C_{1}+ C_{2})^2} + \frac {B}{H * W}\\ &\geq \sqrt{(H * W)^{2} * 4 * C_{1}*C_{2}}+\frac {B}{H * W}\\ &=\geq 2 * \sqrt{H * W *(H * W *C_{1}*C_{2})} + \frac {B}{H * W}\\ &=\geq 2* \sqrt{H * W * B} + \frac {B}{H * W } \end{aligned}

因为:

(C_{1}+C_{2})^{2} \geq 4 * C_{1} *C_{2}
C_{1}^{2}+C_{2}^{2} \geq 2 * C_{1} *C_{2}

当C1 等于C2的时候,mac最小

Excessive group convolution increases MAC.

group convolution,分组卷积,通过将所有通道之间的密集卷积改为稀疏(仅在通道组内),可以显著降低计算复杂度.
FLOPs=H*W*\frac{C_{1}}{g}*\frac{C_{2}}{g} * g

其MAC计算公式为:

\begin{aligned} MAC &= H * W *(C_{1}+ C{2})+\frac{C_{1}*C_{2}}{g}\\ &=B * g *(\frac{1}{C_{1}}+\frac{1}{C_{2}}+\frac{B}{H * W}) \end{aligned}


B=\frac{H * W * C_{1} * C_{2}}{g}

所以当分组数g增大时,内存访问增大;而论文中也用一组实验证明了该观点:


image

显然,使用较大的组数会大大降低运行速度。 例如,在GPU上使用8组的速度比在1组(标准密集卷积)上的速度慢两倍以上,而在ARM上则要慢30%。 这主要是由于MAC增加。因此要谨慎地设计变量g;

Network fragmentation reduces degree of parallelism

“multi- path” structure,多路径网络(如googlenet中的 四个分支)使用了许多小型操作符,尽管这种零散的结构已经显示了对准确性的提高,但是它对于GPU并行计算不太友好,因此可能会降低效率.作者设计了以下不同网络来计算其效率:


image

以下结果证明并行操作会降低运行速度,如 e的结果比c慢了三倍;


image

Element-wise operations are non-negligible

element-wise 操作非常耗时.

在计算FlOPs时,我们通常只考虑卷积中的乘法操作,但是一些逐个元素(element-wise)的操作会占用相当长的时间,尤其是在GPU.它们的FLOP较小,但MAC相对较重。各个操作占时如下图所示:


image

guide conclusion

在设计高性能网络时,尽量做到:

  • 使用输入通道和输出通道相同的卷积操作
  • 谨慎使用分组卷积
  • 减少网络并行分支
  • 减少元素级(element-wise)操作

网络基础结构

作者在论文中提到,回顾于shuffleNet v1,其改进经验应该为:

Therefore, in order to achieve high model capacity and efficiency, the key issue is how to maintain a large number and equally wide channels with neither dense convolution nor too many groups

如何在既不密集卷积(使用分组卷积)又不分太多组的情况下保持大量且同样宽的通道数量.

Channel split

如下图所示:
在每个单元的开头,将c个特征通道输入分为两个分支cc^{`},依据之前的网络设计原则G3(不宜有太多并行分支),其中一个分支将作为高速残差通道直接与另一分支结果直接concat;另一个分支中,由三个具有相同的输入通道数和输出通道数的卷积组成(以满足设计原则G1).两个1 * 1的卷积不再按组进行,原因:channel split 已经产生了两个组.concat后按照shuffleNet v1的方式,将两个分支的通道内容进行"channel shuffle"的操作来启用两个分支之间的信息通信.
像ReLU和深度卷积之类的元素操作仅存在于一个分支中。同样,三个连续的元素方式操作(“ Concat”,“ Channel Shuffle”和“ Channel Split”)合并为单个元素方式操作。

Channel split
import torch
import torch.nn as nn

def channel_shuffle(x, groups):
    # type: (torch.Tensor, int) -> torch.Tensor
    batchsize, num_channels, height, width = x.data.size()
    channels_per_group = num_channels // groups

    # reshape
    x = x.view(batchsize, groups,
               channels_per_group, height, width)

    x = torch.transpose(x, 1, 2).contiguous()

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

    return x

class InvertedResidual(nn.Module):
    def __init__(self,inp,oup,stride):
        super(InvertedResidual,self).__init__()
        branch_features = oup // 2
        self.stride = stride

        self.branch_1 = nn.Sequential()

        self.branch_2 = nn.Sequential(
            nn.Conv2d(inp 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),
            ##depth_wise_conv
            nn.Conv2d(branch_features,branch_features,kernel_size=3,stride=stride,padding=0,bias=False,groups=branch_features),
            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),
        )

    def forward(self, x):
        out = torch.cat((self.branch_1(x),self.branch_2(x)),dim=1)
        out = channel_shuffle(out,2)
        return out

down sample

channel split 用来保持图片分辨率,而更多情况下,我们需要对图片进行采样信息抽取,所以stride=2,改为如下结构:通道拆分运算符已删除。因此,输出通道的数量增加了一倍.branch_1 不再是直接高速通道直接连接,而是变成一个3 * 3 的深度分离卷积和一个1 * 1的卷积进行信息抽取.

image
#所以我们修改branch_1的定义即可
self.branch_1 = nn.Sequential(
            nn.Conv2d(inp,inp,kernel_size=3,stride=2,padding=1,groups=inp),
            nn.BatchNorm2d(inp),
            nn.Conv2d(inp,branch_features,kernel_size=1,stride=1,padding=0,bias=False),
            nn.BatchNorm2d(branch_features),
            nn.ReLU(inplace=True)
        )

在shuffle net中,按照bottlenet的通道数按照比例缩放以生成复杂度不同的网络.

shuffle net v2整体结构

按照论文中提供网络结构图,


image

我们来用pytorch代码搭建shufflenet v2;

首先是改写bottlenet结构,按照stride是否为2,来判断另一个分支上是否需要用33深度分离卷积跟11卷积进行信息抽取;

class InvertedResidual(nn.Module):
    def __init__(self, inp, oup, stride):
        super(InvertedResidual, self).__init__()

        if not (1 <= stride <= 3):
            raise ValueError('illegal stride value')
        self.stride = stride

        branch_features = oup // 2
        assert (self.stride != 1) or (inp == branch_features << 1)

        if self.stride > 1:
            self.branch1 = nn.Sequential(
                self.depthwise_conv(inp, inp, kernel_size=3, stride=self.stride, padding=1),
                nn.BatchNorm2d(inp),
                nn.Conv2d(inp, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
                nn.BatchNorm2d(branch_features),
                nn.ReLU(inplace=True),
            )
        else:
            self.branch1 = nn.Sequential()

        self.branch2 = nn.Sequential(
            nn.Conv2d(inp 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_size=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
    def depthwise_conv(i, o, kernel_size, stride=1, padding=0, bias=False):
        return nn.Conv2d(i, o, kernel_size, stride, padding, bias=bias, groups=i)

    def forward(self, x):
        if self.stride == 1:
            x1, x2 = x.chunk(2, dim=1)
            out = torch.cat((x1, self.branch2(x2)), dim=1)
        else:
            out = torch.cat((self.branch1(x), self.branch2(x)), dim=1)

        out = channel_shuffle(out, 2)

        return out

然后我们按照stage不同,搭建起整体shufflenet 用于imagenet 1000分类的整体结构:

class ShuffleNetV2(nn.Module):
    def __init__(self, stages_repeats, stages_out_channels, num_classes=1000, inverted_residual=InvertedResidual):
        super(ShuffleNetV2, self).__init__()

        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_channels = 3
        output_channels = self._stage_out_channels[0]
        self.conv1 = nn.Sequential(
            nn.Conv2d(input_channels, output_channels, 3, 2, 1, bias=False),
            nn.BatchNorm2d(output_channels),
            nn.ReLU(inplace=True),
        )
        input_channels = output_channels

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

        stage_names = ['stage{}'.format(i) for i in [2, 3, 4]]
        for name, repeats, output_channels in zip(
                stage_names, stages_repeats, self._stage_out_channels[1:]):
            seq = [inverted_residual(input_channels, output_channels, 2)]
            for i in range(repeats - 1):
                seq.append(inverted_residual(output_channels, output_channels, 1))
            setattr(self, name, nn.Sequential(*seq))
            input_channels = output_channels

        output_channels = self._stage_out_channels[-1]
        self.conv5 = nn.Sequential(
            nn.Conv2d(input_channels, output_channels, 1, 1, 0, bias=False),
            nn.BatchNorm2d(output_channels),
            nn.ReLU(inplace=True),
        )

        self.fc = nn.Linear(output_channels, num_classes)

    def _forward_impl(self, x):
        # 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])  # globalpool
        x = self.fc(x)
        return x

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


def _shufflenetv2(arch, pretrained, progress, *args, **kwargs):
    model = ShuffleNetV2(*args, **kwargs)

    if pretrained:
        model_url = model_urls[arch]
        if model_url is None:
            raise NotImplementedError('pretrained {} is not supported as of now'.format(arch))
        else:
            state_dict = load_state_dict_from_url(model_url, progress=progress)
            model.load_state_dict(state_dict)

    return model

如果按照通道数不同来进行对网络的缩放,则:
0.5:

return _shufflenetv2('shufflenetv2_x0.5', pretrained, progress,
                         [4, 8, 4], [24, 48, 96, 192, 1024], **kwargs)

1:

return _shufflenetv2('shufflenetv2_x1.0', pretrained, progress,
                         [4, 8, 4], [24, 116, 232, 464, 1024], **kwargs)

总结与分析

shuffleNet v2效率高且准确的原因主要有两个:

  • 每个构建模块(building block)中高效地利用了feature channel和network capacity;
  • 每个模块中,一半的特征通道信息直接参与下一个block中(高速残差信息通道).被看做是另一种方式的特征复用.

在densenet中,分析得到相邻层之间的连接要强于其他层之间的信息连接,也就是意味着所有层之间的密集连接可能带来冗余,特征重用量随两个块之间的距离呈指数衰减。在远距离的块之间,功能重用变得很弱。shuffleNet 用 channel split的方式进行特征复用.

Reference

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容