华为轻量级神经网络架构GhostNet再升级,GPU上大显身手的G-GhostNet(IJCV22)

x33g5p2x  于2022-08-17 转载在 其他  
字(11.1k)|赞(0)|评价(0)|浏览(428)

结尾有测试代码

github上有预训练。

一、写在前面的话

本文针对网络部署时面临的内存和资源有限的问题,提出两种不同的Ghost模块,旨在利用成本低廉的线性运算来生成Ghost特征图。C-Ghost模块被应用于CPU等设备,并通过简单的模块堆叠实现C-GhostNet。适用于GPU等设备的G-Ghost模块利用阶段性特征冗余构建。最终实验结果表明两种模块分别实现了对应设备上精度和延迟的最佳权衡。

论文地址:https://arxiv.org/abs/2201.03297

代码地址:https://github.com/huawei-noah/CV-Backbones

二、导读

在GhostNet(CVPR 2020)出现之前,笔者对冗余特征一直保持刻板的偏见。但不管是过分的抑制冗余,还是过分的增加冗余,都会对架构性能造成不好的影响(存在即合理)。在GhostNet中,作者的等人通过细致的实验观察,提出“以有效方式接受冗余特征图”的思想,将以往的特征生成过程用更廉价的线性运算来替代,从而在保证性能的同时实现轻量化。

如下图所示,对中间特征图进行可视化之后,可以明显看出一些特征表达近似(图中用相同颜色标出的方框),因此作者等人提出,近似的特征图可以通过一些廉价的操作来获得。

图1:ResNet50中第一个残差组产生的一些特征图的可视化。

据此,作者等人提出GhostNet,并在本文中称作C-GhostNet,因此笔者将解读的重点放在G-Ghost,对C-Ghost仅做回顾。

C-GhostNet中为实现轻量化,使用了一些低运算密度的操作。低运算密度使得GPU的并行计算能力无法被充分利用,从而导致C-GhostNet在GPU等设备上糟糕的延迟,因此需要设计一种适用于GPU设备的Ghost模块。

作者等人发现,现有大多数CNN架构中,一个阶段通常包括几个卷积层/块,同时在每个阶段中的不同层/块,特征图的尺寸大小相同,因此一种猜想是:特征的相似性和冗余性不仅存在于一个层内,也存在于该阶段的多个层之间。下图的可视化结果验证了这种想法(如右边第三行第二列和第七行第三列的特征图存在一定相似性)。

图2:左边为ResNet34第二阶段的所有卷积块,右边为该阶段的第一块和最后一块的特征图。

作者等人利用观察到的阶段性特征冗余,设计G-Ghost模块并应用于GPU等设备,实现了一个在GPU上具有SOTA性能的轻量级CNN。

三、C-GhostNet回顾

图3:卷积层和C-Ghost模块的示意图。

如图3,给定输入,一个普通的卷积过程可以表示为:

其中,,表示由尺寸为的特征图生成尺寸为的特征图。由可视化结果可知,输出特征图中包含一些冗余存在,因此可以使用更廉价的操作来实现冗余生成。

作者等人将输出的特征图看做内在特征与Ghost特征的组合,其中Ghost特征可以通过作用于内在特征的廉价操作获得,具体过程如下:

对于需要的输出尺寸,首先利用一次普通卷积生成m个本征特征图,即,其中,。接着对中的每一个特征图进行廉价操作以生成s个Ghost特征(n=m×s),可用公式表示:

代表中的第i个特征图,是生成第j个鬼魂特征图的第j个(最后一个除外)廉价操作,不进行廉价操作,而使用恒等映射来保留内在特征。

一份简单的C-Ghost模块代码示例如下所示:

class GhostModule(nn.Module):
    def init(self, in_channel, out_channel, kernel_size=1, ratio=2, dw_size=3, stride=1, relu=True):
        super(GhostModule, self).init()

        self.out_channel = out_channel
        init_channels = math.ceil(out_channel / ratio)
        new_channels = init_channels*(ratio-1)

        #  生成内在特征图
        self.primary_conv = nn.Sequential(
            nn.Conv2d(in_channel, init_channels, kernel_size, stride, kernel_size//2, ), 
            nn.BatchNorm2d(init_channels),
            nn.ReLU(inplace=True) if relu else nn.Sequential(),
        )

        #  利用内在特征图生成Ghost特征
        self.cheap_operation = nn.Sequential(
            nn.Conv2d(init_channels, new_channels, dw_size, 1, dw_size//2, groups=init_channels),
            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.out_channel,:,:]

四、G-GhostNet

上述示例代码中使用低运算密度的depth-wise卷积作为生成Ghost特征的廉价操作,对于GPU等设备来说,无法充分利用其并行计算能力。另一方面,如果能去除部分特征图并减少激活,便可以概率地减少 GPU 上的延迟
Radosavovic等人引入激活度(所有卷积层的输出张量的大小)来衡量网络的复杂性,对于GPU的延迟来说,激活度比 FLOPs更为相关。

如何实现这一目标?就得利用导读部分提及的“特征的相似性和冗余性不仅存在于一个层内,也存在于该阶段的多个层之间”。由于现有流行CNN架构中,同一阶段的不同层,其输出特征图的尺寸大小不会发生变化,因此一种跨层的廉价替代便可以通过这一特点来实现。其具体实现过程如下:

在CNN的某个阶段中,深层特征被分为Ghost特征(可通过浅层廉价操作获得)和复杂特征(不可通过浅层廉价操作获得),以图2为例:

图2:左边为ResNet34第二阶段的所有卷积块,右边为该阶段的第一块和最后一块的特征图。

将第二阶段从浅到深的八个层分别记作,···,,假定的输出为,此时设定一个划分比例,那么输出的Ghost特征即为,复杂特征为。

其中复杂特征依次通过8个卷积层获得,具有更丰富的抽象语义信息,Ghost特征直接由的输出通过廉价操作获得,最终的输出通过通道拼接得以聚合。如下图所示():

图4:λ = 0.5 的 G-Ghost 阶段示意图。

但是,直接的特征拼接带来的影响是显而易见的。复杂特征经过逐层提取,包含更丰富的语义信息;而Ghost特征由浅层进行廉价操作所得,可能缺乏一部分深层信息。因此一种信息补偿的手段是有必要的,作者等人使用如下操作来提升廉价操作的表征能力:

图5:λ = 0.5 的含有mix操作的 G-Ghost 阶段示意图。

如图5,复杂特征经过连续的n个卷积块生成,Ghost特征则由第一个卷积块经过廉价操作所得。其中mix模块用于提升廉价操作表征能力,即先将复杂特征分支中第2至第n层的中间特征进行拼接,再使用变换函数,变换至与廉价操作的输出同域,最后再进行特征融合(如简单的逐元素相加)。

图6:mix操作的示意图。

下面以图6为例进行详细说明。如图,有至共n个同一阶段的卷积块输出,其中代表最后一层,即复杂特征;代表第一层,即廉价操作所应用的层。现将至的特征进行拼接,得到特征,并利用函数将Z变换至与廉价操作分支的输出一致的域,函数用公式表示如下:

代表对Z进行全局平均池化得到,和表示权重和偏差,即将Z池化后,通过一个全连接层进行域变换。

图6中两个分支的输出进行简单的逐元素相加,并应用非线性得到Ghost特征。

官方公布的g_ghost_regnet文件中,廉价操作的代码段为:

self.cheap = nn.Sequential(
            nn.Conv2d(cheap_planes, cheap_planes,
                      kernel_size=1, stride=1, bias=False),
            nn.BatchNorm2d(cheap_planes),
#             nn.ReLU(inplace=True),
        )

除了大小为1的卷积核,廉价操作还可以用3×3、5×5的卷积核,或者直接的恒等映射来计算。

五、性能对比

表1:ImageNet各方法比较。

表2:ImageNet各方法比较(GhostX-RegNet)。

六、总结与思考

本文利用可视化观察到的现象和大量的实验结果,提出了Ghost特征的思想,利用“特征的相似性和冗余性不仅存在于一个层内,也存在于该阶段的多个层之间”这一猜测,设计出相比C-Ghost更适用于GPU等设备的G-Ghost,并在实际延迟与性能之间取得了良好的权衡。

同时在G-Ghost中,合理的中间特征聚合模块有效缓解了Ghost特征信息损失的问题。但是Ghost特征与复杂特征的划分比例却需要手动调整,不同的对精度、延迟都有不同的影响,在作者等人的实际测试中,保持着精度与延迟的良好权衡。

另外关于使用C-Ghost、G-Ghost模块构建GhostNet整体网络,论文及代码中均有说明,感兴趣的读者可以自行阅览~

测试代码:

gpu1060  1x3x128x128 5ms cpu30ms

skipnet gpu1060  1x3x128x128 6ms cpu18ms

# 2022.07.17-Changed for building GhostNet
#            Huawei Technologies Co., Ltd. <foss@huawei.com>
"""
Creates a G-Ghost RegNet Model as defined in paper:
GhostNets on Heterogeneous Devices via Cheap Operations.
https://arxiv.org/abs/2201.03297
Modified from https://github.com/d-li14/regnet.pytorch
"""
import time

import numpy as np
import torch
import torch.nn as nn

__all__ = ['regnetx_032']

def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
    """3x3 convolution with padding"""
    return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, padding=dilation, groups=groups, bias=False, dilation=dilation)

def conv1x1(in_planes, out_planes, stride=1):
    """1x1 convolution"""
    return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)

class Bottleneck(nn.Module):
    expansion = 1
    __constants__ = ['downsample']

    def __init__(self, inplanes, planes, stride=1, downsample=None, group_width=1, dilation=1, norm_layer=None):
        super(Bottleneck, self).__init__()
        if norm_layer is None:
            norm_layer = nn.BatchNorm2d
        width = planes * self.expansion
        # Both self.conv2 and self.downsample layers downsample the input when stride != 1
        self.conv1 = conv1x1(inplanes, width)
        self.bn1 = norm_layer(width)
        self.conv2 = conv3x3(width, width, stride, width // min(width, group_width), dilation)
        self.bn2 = norm_layer(width)
        self.conv3 = conv1x1(width, planes)
        self.bn3 = norm_layer(planes)
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample
        self.stride = stride

    def forward(self, x):
        identity = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        if self.downsample is not None:
            identity = self.downsample(x)

        out += identity
        out = self.relu(out)

        return out

class LambdaLayer(nn.Module):
    def __init__(self, lambd):
        super(LambdaLayer, self).__init__()
        self.lambd = lambd

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

class Stage(nn.Module):

    def __init__(self, block, inplanes, planes, group_width, blocks, stride=1, dilate=False, cheap_ratio=0.5):
        super(Stage, self).__init__()
        norm_layer = nn.BatchNorm2d
        downsample = None
        self.dilation = 1
        previous_dilation = self.dilation
        if dilate:
            self.dilation *= stride
            stride = 1
        if stride != 1 or self.inplanes != planes:
            downsample = nn.Sequential(conv1x1(inplanes, planes, stride), norm_layer(planes), )

        self.base = block(inplanes, planes, stride, downsample, group_width, previous_dilation, norm_layer)
        self.end = block(planes, planes, group_width=group_width, dilation=self.dilation, norm_layer=norm_layer)

        group_width = int(group_width * 0.75)
        raw_planes = int(planes * (1 - cheap_ratio) / group_width) * group_width
        cheap_planes = planes - raw_planes
        self.cheap_planes = cheap_planes
        self.raw_planes = raw_planes

        self.merge = nn.Sequential(nn.AdaptiveAvgPool2d(1), nn.Conv2d(planes + raw_planes * (blocks - 2), cheap_planes, kernel_size=1, stride=1, bias=False), nn.BatchNorm2d(cheap_planes),
            nn.ReLU(inplace=True), nn.Conv2d(cheap_planes, cheap_planes, kernel_size=1, bias=False), nn.BatchNorm2d(cheap_planes), #             nn.ReLU(inplace=True),
        )
        self.cheap = nn.Sequential(nn.Conv2d(cheap_planes, cheap_planes, kernel_size=1, stride=1, bias=False), nn.BatchNorm2d(cheap_planes), #             nn.ReLU(inplace=True),
        )
        self.cheap_relu = nn.ReLU(inplace=True)

        layers = []
        downsample = nn.Sequential(LambdaLayer(lambda x: x[:, :raw_planes]))

        layers = []
        layers.append(block(raw_planes, raw_planes, 1, downsample, group_width, self.dilation, norm_layer))
        inplanes = raw_planes
        for _ in range(2, blocks - 1):
            layers.append(block(inplanes, raw_planes, group_width=group_width, dilation=self.dilation, norm_layer=norm_layer))

        self.layers = nn.Sequential(*layers)

    def forward(self, input):
        x0 = self.base(input)

        m_list = [x0]
        e = x0[:, :self.raw_planes]
        for l in self.layers:
            e = l(e)
            m_list.append(e)
        m = torch.cat(m_list, 1)
        m = self.merge(m)

        c = x0[:, self.raw_planes:]
        c = self.cheap_relu(self.cheap(c) + m)

        x = torch.cat((e, c), 1)
        x = self.end(x)
        return x

class RegNet(nn.Module):

    def __init__(self, block, layers, widths, num_classes=1000, zero_init_residual=True, group_width=1, replace_stride_with_dilation=None, norm_layer=None):
        super(RegNet, self).__init__()
        if norm_layer is None:
            norm_layer = nn.BatchNorm2d
        self._norm_layer = norm_layer

        self.inplanes = 32
        self.dilation = 1
        if replace_stride_with_dilation is None:
            # each element in the tuple indicates if we should replace
            # the 2x2 stride with a dilated convolution instead
            replace_stride_with_dilation = [False, False, False, False]
        if len(replace_stride_with_dilation) != 4:
            raise ValueError("replace_stride_with_dilation should be None "
                             "or a 4-element tuple, got {}".format(replace_stride_with_dilation))
        self.group_width = group_width
        self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=3, stride=2, padding=1, bias=False)
        self.bn1 = norm_layer(self.inplanes)
        self.relu = nn.ReLU(inplace=True)

        self.layer1 = self._make_layer(block, widths[0], layers[0], stride=2, dilate=replace_stride_with_dilation[0])

        self.inplanes = widths[0]
        if layers[1] > 2:
            self.layer2 = Stage(block, self.inplanes, widths[1], group_width, layers[1], stride=2, dilate=replace_stride_with_dilation[1], cheap_ratio=0.5)
        else:
            self.layer2 = self._make_layer(block, widths[1], layers[1], stride=2, dilate=replace_stride_with_dilation[1])

        self.inplanes = widths[1]
        self.layer3 = Stage(block, self.inplanes, widths[2], group_width, layers[2], stride=2, dilate=replace_stride_with_dilation[2], cheap_ratio=0.5)

        self.inplanes = widths[2]
        if layers[3] > 2:
            self.layer4 = Stage(block, self.inplanes, widths[3], group_width, layers[3], stride=2, dilate=replace_stride_with_dilation[3], cheap_ratio=0.5)
        else:
            self.layer4 = self._make_layer(block, widths[3], layers[3], stride=2, dilate=replace_stride_with_dilation[3])
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.dropout = nn.Dropout(0.2)
        self.fc = nn.Linear(widths[-1] * block.expansion, num_classes)

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)

        # Zero-initialize the last BN in each residual branch,  # so that the residual branch starts with zeros, and each residual block behaves like an identity.  # This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677

    #         if zero_init_residual:
    #             for m in self.modules():
    #                 if isinstance(m, Bottleneck):
    #                     nn.init.constant_(m.bn3.weight, 0)

    def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
        norm_layer = self._norm_layer
        downsample = None
        previous_dilation = self.dilation
        if dilate:
            self.dilation *= stride
            stride = 1
        if stride != 1 or self.inplanes != planes:
            downsample = nn.Sequential(conv1x1(self.inplanes, planes, stride), norm_layer(planes), )

        layers = []
        layers.append(block(self.inplanes, planes, stride, downsample, self.group_width, previous_dilation, norm_layer))
        self.inplanes = planes
        for _ in range(1, blocks):
            layers.append(block(self.inplanes, planes, group_width=self.group_width, dilation=self.dilation, norm_layer=norm_layer))

        return nn.Sequential(*layers)

    def _forward_impl(self, x):
        # See note [TorchScript super()]
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.dropout(x)
        x = self.fc(x)

        return x

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

def regnetx_032(**kwargs):
    return RegNet(Bottleneck, [2, 6, 15, 2], [96, 192, 432, 1008], group_width=48, **kwargs)

if __name__ == '__main__':
    num_classes=4
    model=RegNet(Bottleneck, [2, 6, 15, 2], [96, 192, 432, 1008], group_width=48, num_classes=num_classes)
    model.eval()#.cuda()
    for i in range(20):
        data = torch.randn(1, 3, 128, 128)#.cuda()
        start = time.time()
        out = model(data)
        print('time', time.time() - start, out.size())

相关文章