Press "Enter" to skip to content

深入研究模型压缩经典Ghostnet:如何用少量计算生成大量特征图?

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

作者丨科技猛兽

 

 

1 GhostNet: More Features from Cheap Operations (CVPR 2020 Oral)

 

1.1 GhostNet原理分析

 

1.2 GhostNet代码解读

 

2 GhostSR: Learning Ghost Features for Efficient Image Super Resolution

 

2.1 GhostSR原理分析

 

1GhostNet

 

https://arxiv.org/abs/1911.11907

 

1.1 GhostNet原理分析:

 

华为诺亚端侧作品Ghostnet:仅通过少量计算(cheap operations)就能生成大量特征图的结构。

 

引言:

 

卷积神经网络推动了计算机视觉诸多任务的进步,比如图像识别、目标检测等。但在嵌入式设备上部署深度学习模型的一个困难就是模型过大,以及所需要的计算量过大。比如我们所熟悉的ResNet-50,它有大概25.6M的参数量,按照32位存储,就需要102.4MB的内存空间,同时处理一张 的图片的计算量大概是4.1B FLOPs。因此,深度神经网络设计的最新趋势是探索 Portable 和 Efficient 的网络架构,为移动设备提供acceptable performance。

 

一些研究提出了模型的压缩方法,比如剪枝、量化、知识蒸馏等;还有一些则着重于高效的网络结构设计,比如MobileNet,ShuffleNet等。

 

这个工作从减少冗余特征图的角度出发,提出了一种全新的神经网络基本单元Ghost模块,从而搭建出轻量级神经网络架构GhostNet。

 

在一个well-trained的深度神经网络中,通常会包含丰富甚至冗余的特征图,以保证对输入数据有全面的理解。如下图1所示是在ResNet-50中,将经过第一个残差块处理后的特征图拿出来的可视化结果:

图1:ResNet-50中一些生成的特征

 

作者使用相同颜色的框标记相似的特征图,可以发现这些特征图中有些是两两相似的 (使用扳手图案表示),这就意味着该对中的一个特征图可以通过廉价操作将另一特征图变换而获得,所以,这对特征图中的一个可以视为另一个的一个 “幻影 (Ghost)” 。有了这一发现,作者就提出:并非所有特征图都要用卷积操作来得到,“幻影”特征图 可以用更廉价的操作来生成。

 

本文提出的Ghost模块是一种新颖的即插即用模块,主要目的是使用更少的参数来生成更多特征图 (generate more features by using fewer parameters)。具体来说,深度神经网络中的每一个卷积层将会分为两部分:第一部分是常规的卷积,但是特征图的数量将会严格控制,因为不能让计算量太大;那第二部分也得生成一些特征图,只是不采取常规的卷积来生成,而是通过简单的 “线性变换 (Linear Transformation)” 来生成。与普通卷积神经网络相比,在不更改输出特征图大小的情况下,该Ghost模块中所需的参数总数和计算复杂度均已降低。这就是Ghost模块。

 

基于Ghost模块,作者建立了一个高效的神经架构,即GhostNet。作者首先在基准神经体系结构中替换原始卷积层,以证明Ghost模块的有效性,然后在几个基准视觉数据集上验证GhostNet网络的优越性。实验结果表明,所提出的Ghost模块能够在保持相似识别性能的同时降低通用卷积层的计算成本。此外,在效率和准确性方面,使用提出的新模块构建的GhostNet均优于最新的轻量神经网络,如MobileNetV3。

 

具体方法:

 

MobileNet 和 ShuffleNet通过引入深度可分离卷积 (Depthwise Convolution) 与混洗操作 (Shuffle Operation) 来构建高效的CNN,但其余卷积层仍将占用大量内存和FLOPs。

 

假设输入特征是 ,卷积核表示为 ,输出特征是 ,则常规的卷积操作可以写作:

 

式中,和 分别是输出数据的高度和宽度,分别是卷积核 的内核大小。在此卷积过程中,卷积核数量 和通道数 通常非常大 (例如256或512) ,这个操作的计算量是: 。

图2:常规卷积层和Ghost module

 

根据上式(1),整个操作的计算量与输入和输出特征映射的维度相关,而结合一开始的发现,其实输出特征里面有很多特征是相似的 “幻影” ,因此,没有必要使用大量的FLOP和参数来生成这些冗余的特征图。我们假设,只有 个原始特征图 是使用一次卷积生成的:

 

式中,是使用的卷积核,,为简单起见,这里省略了偏差项。并且所有的超参数 (filter size, stride, padding) 都与普通卷积中的超参数相同,这样,输出特征的空间大小 和 保持一致。

 

但是现在我们只获得了 个特征图,为了补齐 个特征图,作者对 中的每个特征进行一次廉价的线性变换,得到 个 “幻影” 特征图。

 

式中, 代表第 个原始特征图,它经过线性变换 ,得到第 个 “幻影” 特征图 ,即:每个原始特征图 可以生成 个 “幻影” 特征图 。最后的是用于保留原始特征图的恒等映射,如图2(b)所示。

 

通过使用廉价操作,我们可以获得 个特征图 作为 Ghost 模块的输出数据。注意,线性运算  在每个通道上运行,其计算量比普通卷积少得多。 实际上,Ghost 模块中可能有几种不同的线性运算,例如 和 线性内核,将在实验部分进行分析。

 

问:这里的 “廉价的线性运算 (cheap linear operation)” 具体是什幺?

 

答:cheap operation其实就是group convolution,group数 = input channel number,相当于深度可分离卷积 (Depthwise Convolution)。

 

问:与已有的方法相比区别在哪里?

 

答:以往的方法 (Depthwise Convolution)等使用Depthwise Convolution处理空间信息,使用Pointwise Convolution处理不同channel之间的信息。相比之下,Ghost 模块先使用常规的卷积得到一些特征,再使用廉价线性变换得到另一些特征。而且,这个 cheap linear operation 可以有其他很多种变化。比如说 affine transformation 和 wavelet transformation 。

 

复杂度的分析:

 

我们来进一步分析通过使用Ghost模块带来的内存和计算量的收益。具体来说,Ghost模块具有一个恒等映射和 个线性运算,并且每个线性运算的平均内核大小为。而且这每个线性运算都可以具有不同的形状和参数,但是特别是考虑到 CPU 或 GPU 的实用性,这样做会使得在线推理 (online inference) 受到阻碍。所以,为了实现硬件上的高效实现 (efficient implementation) ,建议每个Ghost module的线性运算采用相同大小的线性运算 (例如全或全)。

 

使用Ghost模块升级普通卷积的理论加速比为:

 

其中 的幅度与 和 相似。同样,参数压缩比可以计算为

 

它大约等于加速比。

 

构建GhostNet:

 

Ghost Bottleneck:作者设计了2种Ghost bottleneck(G-bneck)。如下图3所示,分别对应着 和 的情况。

 

Ghost bottleneck类似于ResNet中的基本残差块(Basic Residual Block),主要由两个堆叠的Ghost模块组成。

 

第1个Ghost模块用作扩展层,增加了通道数。这里将输出通道数与输入通道数之比称为expansion ratio。

 

第2个Ghost模块减少通道数,以与shortcut路径匹配。然后,使用shortcut连接这两个Ghost模块的输入和输出。这里借鉴了MobileNetV2,第二个Ghost模块之后不使用ReLU,其他层在每层之后都应用了批量归一化(BN)和ReLU非线性激活。上述Ghost bottleneck适用于stride= 1,对于stride = 2的情况,shortcut路径由下采样层和stride = 2的深度卷积(Depthwise Convolution)来实现。

 

考虑到效率的因素,实作中Ghost模块中的第1阶段的初始卷积是点卷积(Pointwise Convolution)。

图3:Ghost Bottleneck

 

GhostNet:基于Ghost bottleneck,作者提出GhostNet,如下图4所示。其中图4左图为GhostNet总体结构,右图为MobileNet-v3的结构。我们发现GhostNet的搭建过程就是使用Ghost bottleneck替换MobileNetV3中的bottleneck。GhostNet主要由一堆Ghost bottleneck组成,其中Ghost bottleneck以Ghost模块为构建基础。

 

第1层是具有16个卷积核的标准卷积层,然后是一系列Ghost bottleneck,通道逐渐增加。这些Ghost bottleneck根据其输入特征图的大小分为不同的阶段。除了 每个阶段的最后一个Ghost bottleneck是stride = 2,其他所有Ghost bottleneck都以stride = 1进行应用 。最后,利用全局平均池和卷积层将特征图转换为1280维特征向量以进行最终分类。SE模块也用在了某些Ghost bottleneck中的残留层,如表1中所示。

 

与MobileNetV3相比,这里用ReLU换掉了Hard-swish激活函数。尽管进一步的超参数调整或基于自动架构搜索的Ghost模块将进一步提高性能,但表1所提供的架构提供了一个基本设计参考。

图4:左图为GhostNet总体结构,右图为MobileNet-v3的结构。#exp代表expansion ratio,#out代表output channels输出通道数,SE代表是否使用SE模块,Stride代表stride

 

宽度压缩:

 

在某些情况下,我们可能需要更小、更快的模型或特定任务的更高准确性。为了定制网络以满足所需的需求,我们可以简单地在每一层均匀地乘以channel数量的因子 。这个因子 被称为Width Multiplier ,因为它可以改变整个网络的宽度。我们用Width Multiplier表示高斯网络为GhostNet- 。Width Multiplier可以通过大约 控制模型大小和计算成本。通常较小的 会导致较低的延迟和较低的性能,反之亦然。

 

Experiments:

 

作者分别针对ResNet-56和VGG-16进行了Ghost模块化, 这两个模型中的所有卷积层都被所提出的Ghost模块取代,得到Ghost-ResNet-56和Ghost-VGG-16。

 

实验1:超参数消融实验

 

Ghost模块具有两个超参数,也就是,用于生成 个内在特征图,以及用于计算幻影特征图的线性运算的 (即深度卷积核的大小)。作者测试了这两个参数的影响。

 

如图5上图所示,首先,作者首先研究超参数 的作用:固定 并在 范围中调整,并列出CIFAR-10验证集上的结果。作者可以看到,当的时候,Ghost模块的性能优于更小或更大的Ghost模块。这是因为大小为的内核无法在特征图上引入空间信息,而较大的内核(例如 或 )会导致过拟合和更多计算。因此,在以下实验中作者采用 来提高有效性和效率。

 

如图5下图所示,在研究了内核大小的影响之后,作者再研究超参数 的作用:固定 并在 的范围内调整超参数 。实际上, 与所得网络的计算成本直接相关,即,较大的 导致较大的压缩率和加速比。从表3中的结果可以看出,当作者增加 时,FLOP显着减少,并且准确性逐渐降低,这是在预期之内的。特别地,当 ,也就是将VGG-16压缩 时,Ghost模块的性能甚至比原始模型稍好,表明了所提出的Ghost模块的优越性。

图5:超参数消融实验

 

Ghost-ResNet-56和Ghost-VGG-16和几个代表性的最新模型的比较结果如下。Ghost-VGG-16取得了最优的性能 (93.7%),对于已经比VGG-16小得多的ResNet-56,可以在性能相当的条件下实现2倍的加速。

图6:Ghost-ResNet-56和Ghost-VGG-16和几个代表性的最新模型的比较结果

 

实验2:特征图可视化

 

作者还可视化了Ghost模块的特征图,如图7所示。红色框为Ghost模块的原始卷积,绿色框的特征图是经过廉价深度变换后的幻影特征图。尽管生成的特征图来自原始特征图,但它们之间确实存在显着差异,这意味着生成的特征足够灵活 (flexible enough),可以满足特定任务的需求。

图7:Ghost模块可视化

图8:原始VGG-16第2层的输出特征

 

实验3:ImageNet分类数据集

 

作者对ImageNet分类任务进行了实验,初始学习率0.4,batch size大小为1024,验证集上报告的所有结果均是single crop的top-1的性能。为简单起见,作者在初始卷积中设置了内核大小,在所有Ghost模块中设置了和。

 

作者和现有最优秀的几种小型网络结构作对比,包括MobileNet系列、ShuffleNet系列、IGCV3、ProxylessNAS、FBNet、MnasNet等。结果汇总在图9中,这些模型分为3个级别的计算复杂性,即~50,~150和200-300 MFLOPs。从结果中我们可以看到, 通常较大的FLOPs会在这些小型网络中带来更高的准确性,这表明了它们的有效性。 而GhostNet在各种 计算复杂度 级别 (outperforms other competitors consistently at various computational complexity levels) 上始终优于其他竞争对手,主要是因为GhostNet在利用计算资源生成特征图方面效率更高。

图9:ImageNet分类数据集实验结果

 

实验4:硬件推理速度

 

由于提出的GhostNet是为端侧设备设计的,因此作者使用TFLite tools在基于ARM的手机华为P30 Pro上进一步测量GhostNet和其他模型的 实际推理速度 (Actual Inference Speed) 。遵循MobileNet中的超参数设置,使用Batch size为1的单线程模式。

 

从下图10的结果中,我们可以看到与具有相同延迟的MobileNetV3相比,GhostNet大约提高了0.5%的 Top 1 Accuracy ,另一方面 GhostNet 需要更少的运行时间来达到相同的精度。例如,精度为75.0%的GhostNet仅具有40毫秒的延迟,而精度类似的MobileNetV3大约需要46毫秒来处理一张图像。总体而言,作者的模型总体上胜过其他最新模型,例如谷歌MobileNet系列,ProxylessNAS,FBNet和MnasNet。

图10:左:精度与计算量的比较;右:精度与推理速度的比较

 

值得指出的是,华为内部开发了一款神经网络部署工具Bolt,对GhostNet实现做了进一步优化,速度相比其他框架如NCNN、TFLite更快。感兴趣的读者可以参考:

 

https://github.com/huawei-noah/bolt

 

实验5:目标检测任务

 

为了进一步评估GhostNet的泛化能力,作者在MS COCO数据集上进行了目标检测实验。使用trainval35k作为训练数据,具有特征金字塔网络(FPN)的两阶段Faster R-CNN和单阶段的RetinaNet作为baseline,而GhostNet仅替换backbone作为特征提取器 (feature extractor)。对ImageNet pretrained weights再使用SGD训练12个epoches,下图11显示了检测结果。

图11:目标检测任务结果

 

其中,FLOPs 是通过尺寸为 的输入图像计算的。通过使用显着降低的计算成本,GhostNet可以在单阶段的RetinaNet和两阶段的Faster R-CNN框架上达到和MobileNetV2和MobileNetV3相似的mAP,但backbone的计算量得到了大幅度减少。

 

1.2 GhostNet代码解读:

 

代码来自:

 

https://github.com/huawei-noah/ghostnet

 

其他版本代码:

 

1. Darknet: [cfg file]( https://github.com/AlexeyAB/darknet/files/3997987/ghostnet.cfg.txt )

 

2. Gluon/Keras/Chainer: [code]( osmr/imgclsmob )

 

3. Paddle: [code]( https://github.com/PaddlePaddle/PaddleClas/blob/master/ppcls/modeling/architectures/ghostnet.py )

 

4. Bolt inference framework: [benckmark]( huawei-noah/bolt )

 

5. [姿态估计]:Human pose estimation: [code]( https://github.com/tensorboy/centerpose/blob/master/lib/models/backbones/ghost_net.py )

 

6. [目标检测]:YOLO with GhostNet backbone: [code]( HaloTrouvaille/YOLO-Multi-Backbones-Attention )

7. [人脸识别]:Face recognition: [cavaface]

(

 

https://github.com/cavalleria/cavaface.pytorch/blob/master/backbone/ghostnet.py

 

)

 

The code is modified from:

 

1. [分类任务]:MobileNet-v3 PyTorch:

 

https://github.com/d-li14/mobilenetv3.pytorch

 

2. [分类任务]:PyTorch Image Models:

 

https://github.com/rwightman/pytorch-image-models

 

输入v,返回new_v,代表可以被divisor整除的最接近v的值,

 

def _make_divisible(v, divisor, min_value=None):
    """
    This function is taken from the original tf repo.
    It ensures that all layers have a channel number that is divisible by 8
    It can be seen here:
    https://github.com/tensorflow/models/blob/master/research/slim/nets/mobilenet/mobilenet.py
    """
    if min_value is None:
        min_value = divisor
    new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
    # Make sure that round down does not go down by more than 10%.
    if new_v < 0.9 * v:
        new_v += divisor
    return new_v

_make_divisible() 函数保证输出的数可以整除
divisor ,其来自 mobilenet 代码,注释清楚的说明了使用该函数使用来保证 mobilenet 所有层的通道数都可以被 8 整除,那为什幺要这幺做呢?

图12:保证 mobilenet 所有层的通道数都可以被 8 整除

 

如果仅从数学角度来考虑,是得不到答案的,“为什幺保证 mobilenet 所有层的通道数都可以被 8 整除”这个问题要从计算机处理器单元的架构上考虑,按照上述文章中的说法:

 

在大多数硬件中,size 可以被 d = 8, 16, … 整除的矩阵乘法比较块,因为这些 size 符合处理器单元的对齐位宽。

 

其实,总结起来就一句话: 为了快 。以上是对_make_divisible的理解。

 

定义SE模块:

 

class SqueezeExcite(nn.Module):
    def __init__(self, in_chs, se_ratio=0.25, reduced_base_chs=None,
                 act_layer=nn.ReLU, gate_fn=hard_sigmoid, divisor=4, **_):
        super(SqueezeExcite, self).__init__()
        self.gate_fn = gate_fn
        reduced_chs = _make_divisible((reduced_base_chs or in_chs) * se_ratio, divisor)
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.conv_reduce = nn.Conv2d(in_chs, reduced_chs, 1, bias=True)
        self.act1 = act_layer(inplace=True)
        self.conv_expand = nn.Conv2d(reduced_chs, in_chs, 1, bias=True)


    def forward(self, x):
        x_se = self.avg_pool(x)
        x_se = self.conv_reduce(x_se)
        x_se = self.act1(x_se)
        x_se = self.conv_expand(x_se)
        x = x * self.gate_fn(x_se)
        return x 

 

定义卷积+BN+Activation:

 

class ConvBnAct(nn.Module):
    def __init__(self, in_chs, out_chs, kernel_size,
                 stride=1, act_layer=nn.ReLU):
        super(ConvBnAct, self).__init__()
        self.conv = nn.Conv2d(in_chs, out_chs, kernel_size, stride, kernel_size//2, bias=False)
        self.bn1 = nn.BatchNorm2d(out_chs)
        self.act1 = act_layer(inplace=True)


    def forward(self, x):
        x = self.conv(x)
        x = self.bn1(x)
        x = self.act1(x)
        return x

 

定义图2中的Ghost模块:

 

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  # oup:论文Fig2(a)中output的通道数
        init_channels = math.ceil(oup / ratio)  # init_channels: 在论文Fig2(b)中,黄色部分的通道数
                                                # ceil函数:向上取整,
                                                # ratio:在论文Fig2(b)中,output通道数与黄色部分通道数的比值
        new_channels = init_channels*(ratio-1)  # new_channels: 在论文Fig2(b)中,output红色部分的通道数


        self.primary_conv = nn.Sequential(      # 输入所用的普通的卷积运算,生成黄色部分
            nn.Conv2d(inp, init_channels, kernel_size, stride, kernel_size//2, bias=False),
                                                #1//2=0
            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),
                                                # 3//2=1;groups=init_channel 组卷积极限情况=depthwise卷积
            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)         # torch.cat: 在给定维度上对输入的张量序列进行连接操作
                                                # 将黄色部分和红色部分在通道上进行拼接
        return out[:,:self.oup,:,:]             # 输出Fig2中的output;由于向上取整,可以会导致通道数大于self.out

 

这里的ratio对应论文中的s,dw_size对应论文中的d,oup代表论文中的n,为目标通道数。

 

那幺很显然,代码中的init_channels 就是intrinsic modules的通道数,new_channels 就是ghost modules的通道数。

 

primary_conv 就是论文中的普通卷积运算,cheap_operation 是红色部分的cheap的卷积运算,你注意一下这里实现的区别是后者的groups=init_channels,所以你瞬间明白论文中的cheap operation其实就是group convolution。

 

最后的最后你需要把2部分concat在一起:torch.cat([x1,x2], dim=1),这里dim取1是因为channel对应的第1维。

 

定义Ghost Bottlenecks类:

 

再看高一级的Bottleneck的实现:

 

stride=1的时候:

 

Ghost Bottleneck由两个堆叠的Ghost modules构成。

 

第一个Ghost modules:

 

负责增加通道数,是一个膨胀层。

 

膨胀率(expansion ratio):输出通道数与输入通道数的比值。

 

第二个Ghost modules:

 

负责减小通道数,使得通道数与shortcut匹配。

 

stride=2的时候:

 

shortcut部分包括一个下采样层和一个stride=2的depthwise卷积层。

 

class GhostBottleneck(nn.Module):
    """ Ghost bottleneck w/ optional SE"""


    def __init__(self, in_chs, mid_chs, out_chs, dw_kernel_size=3,
                 stride=1, act_layer=nn.ReLU, se_ratio=0.):
        super(GhostBottleneck, self).__init__()
        has_se = se_ratio is not None and se_ratio > 0.
        self.stride = stride


        # Point-wise expansion
        self.ghost1 = GhostModule(in_chs, mid_chs, relu=True)


        # Depth-wise convolution
        # stride = 2 时, 多一个Depthwise Convolution, 如图3所示.
        if self.stride > 1:
            self.conv_dw = nn.Conv2d(mid_chs, mid_chs, dw_kernel_size, stride=stride,
                             padding=(dw_kernel_size-1)//2,
                             groups=mid_chs, bias=False)
            self.bn_dw = nn.BatchNorm2d(mid_chs)


        # Squeeze-and-excitation
        if has_se:
            self.se = SqueezeExcite(mid_chs, se_ratio=se_ratio)
        else:
            self.se = None


        # Point-wise linear projection
        self.ghost2 = GhostModule(mid_chs, out_chs, relu=False)


        # shortcut
        # 输入输出channel不一致时, 多一个Depthwise Convolution + Pointwise Convolution.
        # The shortcut path is implemented by a downsampling layer and a depthwise convolution with stride=2 is inserted between the two Ghost modules.
        if (in_chs == out_chs and self.stride == 1):
            self.shortcut = nn.Sequential()
        else:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_chs, in_chs, dw_kernel_size, stride=stride,
                       padding=(dw_kernel_size-1)//2, groups=in_chs, bias=False),
                nn.BatchNorm2d(in_chs),
                nn.Conv2d(in_chs, out_chs, 1, stride=1, padding=0, bias=False),
                nn.BatchNorm2d(out_chs),
            )




    def forward(self, x):
        residual = x


        # 1st ghost bottleneck
        x = self.ghost1(x)


        # Depth-wise convolution
        if self.stride > 1:
            x = self.conv_dw(x)
            x = self.bn_dw(x)


        # Squeeze-and-excitation
        if self.se is not None:
            x = self.se(x)


        # 2nd ghost bottleneck
        x = self.ghost2(x)


        x += self.shortcut(residual)
        return x

 

GhostNet类:

 

最后看最顶层的GhostNet类:

 

class GhostNet(nn.Module):
    def __init__(self, cfgs, num_classes=1000, width=1.0, dropout=0.2):
        super(GhostNet, self).__init__()
        # setting of inverted residual blocks
        self.cfgs = cfgs
        self.dropout = dropout


        # building first layer
        output_channel = _make_divisible(16 * width, 4)
        self.conv_stem = nn.Conv2d(3, output_channel, 3, 2, 1, bias=False)
        self.bn1 = nn.BatchNorm2d(output_channel)
        self.act1 = nn.ReLU(inplace=True)
        input_channel = output_channel


        # building inverted residual blocks
        stages = []
        block = GhostBottleneck
        for cfg in self.cfgs:
            layers = []
            for k, exp_size, c, se_ratio, s in cfg:
                output_channel = _make_divisible(c * width, 4)
                hidden_channel = _make_divisible(exp_size * width, 4)
                layers.append(block(input_channel, hidden_channel, output_channel, k, s,
                              se_ratio=se_ratio))
                input_channel = output_channel
            stages.append(nn.Sequential(*layers))


        output_channel = _make_divisible(exp_size * width, 4)
        stages.append(nn.Sequential(ConvBnAct(input_channel, output_channel, 1)))
        input_channel = output_channel


        self.blocks = nn.Sequential(*stages)        


        # building last several layers
        output_channel = 1280
        self.global_pool = nn.AdaptiveAvgPool2d((1, 1))
        self.conv_head = nn.Conv2d(input_channel, output_channel, 1, 1, 0, bias=True)
        self.act2 = nn.ReLU(inplace=True)
        self.classifier = nn.Linear(output_channel, num_classes)


    def forward(self, x):
        x = self.conv_stem(x)
        x = self.bn1(x)
        x = self.act1(x)
        x = self.blocks(x)
        x = self.global_pool(x)
        x = self.conv_head(x)
        x = self.act2(x)
        x = x.view(x.size(0), -1)
        if self.dropout > 0.:
            x = F.dropout(x, p=self.dropout, training=self.training)
        x = self.classifier(x)
        return x

 

GhostNet由以Ghost modules为基础的Ghost Bottlenecks构成。

 

如上图4所示,GhostNet的第一层是一个有16个卷积核的标准卷积层,接下来是一系列用于增加通道数的Ghost Bottlenecks。这些Ghost Bottlenecks根据输入特征图的尺寸在不同阶段被分组。

 

除了每个阶段的最后一层,所有Ghost Bottlenecks的stride都为1。

 

在网络的最后,全局平均池化和卷积层将特征图转换为1280维用于最终分类,注意其与图4左图的对应。

 

Ghostnet配置信息,写在这个函数里:

 

def ghostnet(**kwargs):
    """
    Constructs a GhostNet model
    """
    cfgs = [
        # k, t, c, SE, s 
        # stage1
        [[3,  16,  16, 0, 1]],
        # stage2
        [[3,  48,  24, 0, 2]],
        [[3,  72,  24, 0, 1]],
        # stage3
        [[5,  72,  40, 0.25, 2]],
        [[5, 120,  40, 0.25, 1]],
        # stage4
        [[3, 240,  80, 0, 2]],
        [[3, 200,  80, 0, 1],
         [3, 184,  80, 0, 1],
         [3, 184,  80, 0, 1],
         [3, 480, 112, 0.25, 1],
         [3, 672, 112, 0.25, 1]
        ],
        # stage5
        [[5, 672, 160, 0.25, 2]],
        [[5, 960, 160, 0, 1],
         [5, 960, 160, 0.25, 1],
         [5, 960, 160, 0, 1],
         [5, 960, 160, 0.25, 1]
        ]
    ]
    return GhostNet(cfgs, **kwargs)

 

我们只需要给Ghostnet传入一个配置参数cfgs即可,这个cfgs包括5部分:kernel size的大小,expansion ratio,output channel数,是否SE Block以及stride。

 

2 GhostSR

 

https://arxiv.org/abs/2101.08525

 

2.1 GhostSR原理分析:

 

华为诺亚&北大提出的一种轻量化图像超分的方案GhostSR:基于对超分模型特征图的观察,即SISR模型的特征图中部分相似程度很高,所以仅通过移位操作(shift operation)就能生成大量 “Ghost” 特征图。这里所提出的GhostSR一种通用性的轻量化方案,相比剪枝等技术,以及对于NPU,GPU不友好的Depthwise Convolution操作,该技术可以在性能几乎无损的约束下带来显着的推理速度提升、参数量降低、计算量降低。

 

引言:

 

单图像超分 (SISR)是一种经典的低级计算机视觉任务,其目标是从相应的低分辨率图像中恢复出高分辨率图像。现代图像超分技术借助卷积神经网络已经实现了不错的性能,但缺点依然是需要大量的计算量。上面我们举了个ResNet-50的例子,我们说它有大概 25.6M 的参数量,按照32位存储,就需要102.4MB的内存空间,同时处理一张 的图片的计算量大概是4.1 GFLOPs。而 EDSR[1] 处理一张 的图片的计算量大概是 2270.9 GFLOPs。

 

特征冗余 (Feature Redundancy)广泛存在于深度卷积神经网络。由于大多数现有的SISR模型需要保留输入图像的整体纹理和颜色信息 (overall texture and color information),因此在每一层中不可避免地会有许多相似的特征,如图13所示。为了在维持CNN性能的前提下尽量减少运算量,作者依旧首先选出一些 intrinsic features,再通过一些廉价的操作来结合 intrinsic features 生成这些冗余的特征,这也就是GhostNet的思想。只是在超分任务中,这种廉价的操作是移位操作(shift operation),对于 Shift操作 更详细的介绍可以参考下面的链接。

图13:VDSR[2]的特征图可视化,包含了许多十分相似的特征

 

科技猛兽: 解读模型压缩4:你一定从未见过如此通俗易懂的模型压缩Shift操作解读

 

本文提出的GhostSR是一种新颖的减少超分网络计算量的方法,主要目的是使用更少的参数来生成更多特征图 (generate more features by using fewer parameters)。具体来说, 作者首先通过对一个预先训练好的模型进行聚类来识别 intrinsic features 。然后对这些 intrinsic features 使用移位操作,该操作对GPU十分友好,其效果是对 intrinsic features 进行移位得到 “Ghost features”。为了有效地训练这些移位操作,作者结合了gumbel softmax trick。最后,将 intrinsic features 和得到的 “Ghost features” concat到一起,获得最终的输出特征。移位操作可以强调更多的 边缘和纹理信息 (edge and texture information) ,这可以被视为对传统卷积滤波器的补偿。此外,通过作者对移位操作的高效CUDA实现,在主流GPU上带来了实际的加速。与普通卷积神经网络相比,在不更改输出特征图大小的情况下,该Ghost模块中所需的参数总数 (Parameters) 和计算复杂度 (FLOPs) 和GPU延迟 (GPU latency) 均已降低。

 

具体方法:

 

如图14所示,假设卷积核表示为 ,输出特征是 ,则常规的卷积操作的计算量是: 。作者使用常规的卷积操作生成 intrinsic features,通过对 intrinsic features 进行移位操作生成 Ghost features ,充分利用了移位操作具有更多的纹理信息和更大的感受野的优势。假设垂直和水平偏移量为,其中, , , 表示最大偏移,那幺所得的 “Ghost” 特征可以描述为:

 

其中,分别表示 Ghost features 与 intrinsic features 的索引。而 的所有元素定义如下:

 

最后,我们对intrinsic features 与 Ghost features 进行concat得到完整输出。相比原始的卷积操作,所提方法的计算量直接减少比例,且shift操作是无计算量的。更重要的是,结合高效的CUDA实现,所提方法可以带来显着的GPU端 的推理加速。

图14:GhostSR原理图

 

简单说一下上面的式子是怎幺来的:

 

假设我们想实现移位操作,最大偏移量设为 ,这个意思是 方向的偏移的取值范围是 。而要想做到移位操作,式子应该是个weighted sum的形式,就是不同位置的权重乘以不同位置的输入,再求和。移位其实就是在这个权重上做文章。要保证偏移的取值范围是 ,这个权重 的大小应该是 。

 

假设现在权重的某个位置 的元素值为1,我们想知道的是,这个不为0的 元素对应的偏移量是多少?

 

我们知道 是一个 维的矩阵,那幺:

 

当 时偏移量一定对应最大值 。

 

当 时偏移量一定对应最小值 。

 

所以偏移量是: 。

 

也就是说,当 中的一个元素 时,它对应的偏移量是 。所以有 式成立。

 

Shift操作的优点:

 

超分辨率的目的是从相应的低分辨率图像中恢复高分辨率图像。增强的高频信息如纹理有助于提高恢复的高分辨率图像的质量。给定输入特征,对其移动一个像素的距离并进行 得到 ,生成的纹理信息可以由 表示,如图15上图所示。将 和 两者concat后送入到下一层卷积处理,此时卷积操作可以视作更复杂的包含减法的操作,同时可以一定程度上增强高频信息。

图15:Shift操作的优点

 

此外,两个空间扰乱特征的组合有助于提升CNN的感受野,而这对于超分任务而言非常重要。也就是说: shift操作为卷积滤波器提供了空间信息交互 。例如,当将特征图向左移动一个像素时,下一层特征图上相同位置的感受野相应地向左移动一个像素。在位错特征图的组合上进行的卷积运算产生了更宽的感受野如图15下图所示。

 

Learnable shift:

 

shift operation可以看做是Depth-wise convolution在只有1个权重等于1时的特殊情况,在原始的shift操作[3]中,作者采用了固定方向的shift操作,而本文为了在训练过程中灵活调整 intrinsic feature 的偏移量,提出了使偏移量权重 可学习的方法。然而, 中的one hot使得权重难以优化,因此利用Gumbel-Softmax技巧来解决这个问题。

 

为了理解的方便,这里首先介绍下 Gumbel-Softmax 技巧,如果你之前已有了解,可直接跳过分割线部分。

 

Gumbel-Softmax:

 

我们知道一个离散随机变量 的分布,比如说:,然后 我们想得到一些服从这个分布的离散的 的值。 我们一般的思路当然是,就按照这个概率去采样,采样一些 来用就行了。

 

但是这幺做有一个问题: 我们采样出来的 只有值,没有生成 的式子。 本来 的值和 是相关的,但是我们使用采样这幺一个办法之后,我们 得到的 没有办法对 求导 ,这在神经网络里面就是一个大问题,没法BP了。很多时候我们只是要 的期望,那幺我们就是  , 对 的导数都很清楚,反向传播很好实现。 但是我们这里的需求是采样,要得到一些实际的,离散的 值,就像上面说的,不能求导的问题就来了。

 

区别就在于:

 

之前是要期望, 概率 包含在前向传播的式子里 ,易于反向传播。

 

现在是采样,所以 概率 不包含在前向传播的式子里 ,不易反向传播。

 

那幺,能不能给一个以 为参数的公式,让这个公式返回的结果是 的采样呢?就是说 即使是采样,也要让概率包含在前向传播的式子里。 这样的话,我们就 可以对这个公式求导,从而得到采样的 对 的导数了!

 

我们所想要的就是下面这个式子,即 Gumbel-Softmax trick :

 

其中, 。向量 只有1个元素是1,其余的都是0。

 

问:这个 这一项是干嘛用的?为什幺一定要有?

 

你仔细观察下 如果没有 这一项 ,其实就等于是 取概率的最大值作为采样结果 ,注意: 不是按照概率采样,而是取概率的最大值作为采样结果 。也就是说,假设我想取10000个值,那幺你返回的10000个值都对应着概率最大的那个值,都是一样的。那这样也就失去了采样的意义。而有了 这一项,仅仅是相当于添加了一个噪声,使得每次采样的结果变得不确定了,也就体现出了采样的本质。

 

这一项名叫Gumbel噪声,这个噪声是用来使得 的返回结果不固定的 (每次都固定一个值就不叫采样了)。最终我们得到的 向量是一个one hot向量,用这个向量乘一下 ,得到的就是我们要采样的 , 这个过程就是前向过程 。

 

反向传播就要对 求导了。显然这个式子里面有argmax这个部分是无法求导的,所以此时采用softmax, 用可导的softmax代替一下这里的argmax函数 ,问题完全解决。最终得到的 向量为:

 

式中的 越小, 越接近one hot向量。此时就能对 求导了。

 

总结:

 

以上就是Gumbel-Softmax trick,其Gumbel的部分体现在Gumbel噪声,这个噪声是用来使得 的返回结果不固定的;Softmax代替argmax函数上。

 

在上面的例子里,向量  只有1个元素是1,其余的都是0 。再回到GhostSR上来, 这个矩阵 只有一个元素是1,其余都是0 , 为了在训练过程中灵活调整内在特征的偏移量,作者提出了使偏移量权重 可学习的方法。

 

我们先创建一个代理软权值 以表示one-hot形式的 ,定义如下服从指数衰减Gumbel分布的噪声:

 

其中, 表示超参数, 表示当前训练epoch。那幺one-hot形式的 就可以被relax为:

 

式中的 越小, 越接近one hot向量。此时就能对 求导了。

 

在前向传播的最后,我们可以得到偏移索引值来构建one-hot形式的 :

 

通过上述方式的转换,偏置权值 就可以纳入到CNN的训练过程中了。在训练完成后,我们再构建one-hot形式的 即可得到最后的GhostSR。

 

反向传播时,涉及到 的求解,作者直接使用:

 

这样,就可以训练偏置权值 了,其实训练的是 的权重,训练完以后,通过式 得到偏移索引值,也就得到了训练好的偏置权值 了。

 

Ghost Features in Pre-Trained Model:

 

作者的目标是以一种更高效的方式生成冗余特征。特别地,我们首先生成 的特征作为 intrinsic feature,然后使用移位操作基于 intrinsic feature 生成其他特征作为 Ghost feature,最后将二者concat在一起作为完整的输出特征。

 

如果从头开始训练GhostSR,那幺会简单的按顺序排列,这与期望不符。如果在预训练SISR基础上进行微调,我们就可以充分利用 intrinsic feature与 Ghost feature 的相关性并获得更好的性能。

 

对于一个预先训练好的SISR模型,我们的目标是用移位操作来代替卷积滤波器。

 

问:对于网络中的某一层,不清楚输出特征的哪一部分是 intrinsic feature ,哪一部分应该是 Ghost feature?

 

答:作者通过对预先训练好的模型中的过滤器进行 逐层聚类 来解决这个问题,并且将过滤器生成的靠近聚类质心的特征作为 intrinsic feature。具体而言,使用聚类方法,例如k-means,来生成具有高簇间距离和低簇内距离的簇组。

 

作者先对卷积滤波器向量化,即从转换为形式得到向量,这是 个向量。我们需要将上述权值核划分到个类,采用k-means的优化方式,这一过程可用下式表示:

 

式中, 表示第 个类 的聚类中心,在每个类中,聚类质心可被视为 intrinsic filter,由 intrinsic filter 生成的特征可被视为 intrinsic feature。对于第 个类 来讲,如果这个类里面只有一个filter,那幺它生成的特征可被视为 intrinsic feature。如果这个类里面有多个filter,那这多个filter的中心可能不是原来的kernel,那幺此时取距离这个中心最近的filter作为 intrinsic filter ,由它生成的特征可被视为 intrinsic feature。

 

所以 intrinsic filter 可以使用公式表达为:

 

所以我们最后得到了 个类,从每个类里面选出一个filter,得到了 个 intrinsic filter ,继而生成了 个 intrinsic feature,表示为 。其他的 个 ghost feature由 intrinsic feature通过移位操作获得。

 

整个聚类生成 intrinsic filter ,再得到 intrinsic feature 和 ghost feature 的流程如下图16所示。

图16:聚类过程的一个示例,红色虚线框表示intrinsic filter

 

写成公式就是:

 

在找到预训练模型的每一层中的 intrinsic filter 之后,我们将预训练模型中的相应权重分配给intrinsic filter 。因此,可以最大限度地利用预先训练的模型中的信息来识别 intrinsic feature ,并继承预先训练的滤波器以获得更好的性能。最后,算法如下图17所示:

图17:GhostSR算法

 

大致思路是:在每个循环里:

 

利用预训练模型的权重计算出 intrinsic feature 。

 

再计算出soft 移位矩阵 。再根据它得到hard 移位矩阵 。

 

根据hard 移位矩阵 计算 ghost feature 。至于这里为什幺使用 来计算 ,是因为这样做实际上是使用了hard 移位矩阵 来计算 ,而反向传播却只更新soft 移位矩阵 。

 

最后反向传播更新 。

 

Experiments:

 

训练数据集:DIV2K。 测试数据集: Set5、Set14、B100、Urban100。

 

度量指标 (Evaluation metrics):PSNR/[email protected]

 

对比模型:EDSR、RDN、CARN、CARN_M。

 

对LR 图像裁剪出16幅48×48大小的图像进行训练,通过减去DIV2K数据集的平均RGB值,对所有图像进行预处理。

 

实验1:对比Baseline性能:

图18:对比Baseline性能

 

如图18所示中,作者报告了Baseline卷积网络和我们的Ghost版本的缩放因子×2、×3和×4的定量结果。Conv类型的结果是通过作者的重新训练得到的,与原始论文中的结果相似。此外,浮点运算 FLOPs 和延迟Latency 是通过 图像和单个NVIDIA V100 GPU计算的。对于EDSR和RDN模型来说,当用移位操作代替常规卷积时,参数和浮点运算减少了近一半,而性能没有显着下降。此外,通过我们对移位操作的高效CUDA实现,GhostSR为GPU带来了实际的加速。对于×2的EDSR来说,GhostSR将参数量减少了47%,计算量减少了46%,延迟减少了41%,而性能却没有显着的变化。

 

视觉效果的评估如下图19所示,它展示了常规卷积和GhostSR之间的视觉质量差异。任务是×4的超分任务,在Set14和Urban100数据集,使用GhostSR,通过移位操作生成的细节和纹理与通过规则卷积生成的细节和纹理大致相同。

图19:×4任务在Set14和Urban100数据集,使用GhostSR,通过移位操作生成的细节和纹理与通过规则卷积生成的细节和纹理大致相同

 

实验2:对比不同模型压缩方法在相似的浮点运算预算下的性能

 

下图20为在相似的浮点运算预算下GhostSR与剪枝、Depthwise等的性能与效果对比,设置是 图像和单个NVIDIA V100 GPU计算浮点运算和延迟。图21为期视觉效果的对比。

图20:不同方法,相同计算量约束的比较

图21:不同方法,相同计算量约束的比较视觉效果图

 

GhostSR产生的图像质量接近原始网络的结果。作者将其归因于移位操作的特性,这可以部分补偿由于通道减少而导致的性能下降。

 

参考文献:

 

Paper:

 

[1] (CVPR 2017) Enhanced Deep Residual Networks for Single Image Super-Resolution( https://arxiv.org/pdf/1707.02921.pdfarxiv.org)

 

[2] (CVPR 2016) Accurate image super-resolution using very deep convolutional networks (https://arxiv.org/pdf/1511.04587.pdfarxiv.org)

 

[3] CVPR2018:Shift: A Zero FLOP, Zero Parameter Alternative to Spatial Convolutions( https://arxiv.org/pdf/1711.08141.pdfarxiv.org)

 

Blog:

 

王云鹤:CVPR 2020:华为GhostNet,超越谷歌MobileNet,已开源 (https://zhuanlan.zhihu.com/p/109325275)

 

Happy:GhostSR|针对图像超分的特征冗余,华为诺亚&北大联合提出GhostSR(https://zhuanlan.zhihu.com/p/346421729)

 

科技猛兽:解读模型压缩4:你一定从未见过如此通俗易懂的模型压缩Shift操作解读(https://zhuanlan.zhihu.com/p/312348288)

 

◎ 作者档案

 

作者:科技猛兽

Be First to Comment

发表评论

邮箱地址不会被公开。 必填项已用*标注