卷积神经网络学习路线(二十四)| 华为GhostNet

2020-06-02 14:58:26 浏览数 (1)

1. 引言

受限于内存以及计算资源,将常规的CNN架构部署到移动设备是件非常困难的事。近几年来有各种移动端网络架构设计,大部分都是从减少卷积计算量的思路出发,谷歌出品的Mobilenet系列是提出了「Depthwise Pointwise卷积」来减少计算量,旷视则是提出「通道混洗」,利用转置操作,均匀的shuffle各个通道进行卷积。Mixnet是在Mobilenet基础上,关注了卷积核的大小,通过「不同大小卷积核」所生成的卷积图在不增加计算量前提下进一步提高精度。而华为的Ghostnet则是聚焦于「特征图冗余」,希望通过少量的计算(即论文里的cheap operation)得到大量特征图。而Ghostnet在相同计算量下,精度超越了Mobilenetv3,达到了75.7%分类准确率( ImageNet ILSVRC-2012 classification dataset)

2. 何为特征图冗余?

首先作者对训练好的Resnet-50模型进行了特征图可视化

Resnet-50模型进行了特征图可视化

这里作者标出了三组特征图,认为这些特征图彼此是类似的,我们可以用一些cheap operation来得到

因此我们从这里入手,以一种更高效的方式生成这些"Ghost"一样的特征图

3. GhostModules提出

在引言我们也提到了Depthwise Pointwise卷积,其中1x1卷积层也需要大量的内存和计算量

Ghost 模块

我们认为没有必要耗费大量计算资源来生成冗余的特征图,一些特征图之间是一种相似的关系,所以我们想到在原始特征图基础上,经过简单线性变换再生成新的特征图

如图中,先通过常规卷积,生成部分特征图。再在这特征图基础上得到新的特征图。最后两部分特征图在通道维度上连结,生成最终的output

我们可以看下代码实现

代码语言:javascript复制
class GhostModule(nn.Module):
    def __init__(self, inp, oup, kernel_size=1, ratio=2, dw_size=3, stride=1, relu=True):
        super(GhostModule, self).__init__()
        self.oup = oup
        init_channels = math.ceil(oup / ratio)
        new_channels = init_channels*(ratio-1)

        self.primary_conv = nn.Sequential(
            nn.Conv2d(inp, init_channels, kernel_size, stride, kernel_size//2, bias=False),
            nn.BatchNorm2d(init_channels),
            nn.ReLU(inplace=True) if relu else nn.Sequential(),
        )

        self.cheap_operation = nn.Sequential(
            nn.Conv2d(init_channels, new_channels, dw_size, 1, dw_size//2, groups=init_channels, bias=False),
            nn.BatchNorm2d(new_channels),
            nn.ReLU(inplace=True) if relu else nn.Sequential(),
        )

    def forward(self, x):
        x1 = self.primary_conv(x)
        x2 = self.cheap_operation(x1)
        out = torch.cat([x1,x2], dim=1)
        return out[:,:self.oup,:,:]

代码中primary_conv就是常规的卷积,而cheap operation就是一个Depthwise卷积,通过参数ratio,来控制两部分生成特征图数量之比。最后调用torch.cat连结两部分特征图

作者也给出了计算量压缩比公式

计算量压缩比的公式

上面式子表示的是普通卷积的计算量

下面左边的式子,代表的是primaryconv的卷积量,右边式子代表的是cheap operation计算量,通过参数s(对应代码中的ratio)来控制两者卷积量之比

最后这里的近似做个说明, dxd计算量于k*k类似,而c代表的通道数远大于s,因此最终近似结果为s

3.1 其中的一个疑问

那么既然前面说了cheap operation是个线性变换,但是在代码实现中,作者在模块最后还是加入了Relu。其实我当时是很疑惑的,因为众所周知加入了Relu激活函数就打破了函数的线性关系。后面在github提了一个小issue

得到的回复是 Relu可以在最后的concat上做,之所以放到前面是为了降低latency

4. GhostBottleNeck

在GhostModule模块基础上,根据两种stride步长又提出了两种bottleneck结构

两种bottleneck结构

下面是其代码实现

代码语言:javascript复制
class GhostBottleneck(nn.Module):
    def __init__(self, inp, hidden_dim, oup, kernel_size, stride, use_se):
        super(GhostBottleneck, self).__init__()
        assert stride in [1, 2]

        self.conv = nn.Sequential(
            # pw
            GhostModule(inp, hidden_dim, kernel_size=1, relu=True),
            # dw
            depthwise_conv(hidden_dim, hidden_dim, kernel_size, stride, relu=False) if stride==2 else nn.Sequential(),
            # Squeeze-and-Excite
            SELayer(hidden_dim) if use_se else nn.Sequential(),
            # pw-linear
            GhostModule(hidden_dim, oup, kernel_size=1, relu=False),
        )

        if stride == 1 and inp == oup:
            self.shortcut = nn.Sequential()
        else:
            self.shortcut = nn.Sequential(
                depthwise_conv(inp, inp, 3, stride, relu=True),
                nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
                nn.BatchNorm2d(oup),
            )

    def forward(self, x):
        return self.conv(x)   self.shortcut(x)

通过两个ghostmodule与原始输入(shortcut)相加得到最终输出,若shortcut维度不一致,则需要用1x1卷积来调整通道数。并且引入了SElayer,增加了特征图注意力机制。

经过这个GhostnetBottleNeck结构堆叠,我们就得到了Ghostnet整体

下面是他的架构图

GhostNet 网络结构图

5. 总结

和主流的网络在精度和参数量上的对比结果

「GhostNet的思路,设计都很巧妙简单。着眼于特征图生成,用更cheap的Depthwise卷积来进一步生成特征图。论文中实验结果也是十分理想的,超越了大部分移动端SOTA网络。」

0 人点赞