移动端语义分割

1.1 基础模型

我们在计算FLOPs时往往只考虑卷积中的乘法操作,但是一些Element-wise操作(例如ReLU激活,偏置,单位加等)往往被忽略掉。作者指出这些Element-wise操作看似数量很少,但它对模型的速度影响非常大。尤其是深度可分离卷积这种MAC/FLOPs比值较高的算法。图5中统计了ShuffleNet v1和MobileNet v2中各个操作在GPU和ARM上的消耗时间占比。

总结一下,在设计高性能网络时,我们要尽可能做到:

  1. G1). 使用输入通道和输出通道相同的卷积操作;
  2. G2). 谨慎使用分组卷积;
  3. G3). 减少网络分支数;
  4. G4). 减少element-wise操作。

例如在ShuffleNet v1中使用的分组卷积是违背G2的,而每个ShuffleNet v1单元使用了bottleneck结构是违背G1的。MobileNet v2中的大量分支是违背G3的,在Depthwise处使用ReLU6激活是违背G4的

例如:比较ShuffleNet v1 和 ShuffleNet v2

仔细观察(c),(d)对网络的改进我们发现了以下几点:

  1. 在(c)中ShuffleNet v2使用了一个通道分割(Channel Split)操作。这个操作非常简单,即将 c 个输入Feature分成 c-c' 和 c'  两组,一般情况下 c' = c/2 。这种设计是为了尽量控制分支数,为了满足G3。
  2. 在分割之后的两个分支,左侧是一个直接映射,右侧是一个输入通道数和输出通道数均相同的深度可分离卷积,为了满足G1。
  3. 在右侧的卷积中, 1x1 卷积并没有使用分组卷积,为了满足G2。
  4. 最后在合并的时候均是使用拼接操作,为了满足G4。
  5. 在堆叠ShuffleNet v2的时候,通道拼接,通道洗牌和通道分割可以合并成1个element-wise操作,也是为了满足G4。
  6. 最后当需要降采样的时候我们通过不进行通道分割的方式达到通道数量的加倍,如图6.(d)

1.1.1 GhostNet

图1是由ResNet-50中的第一个残差块生成的某些中间特征图的可视化。从图中我们可以看出,这里面有很多特征图是具有高度相似性的(在图中分别用不同的颜色示意),换句话说,就是存在许多的冗余特征图。所以从另一个角度想,我们是不是可以利用一系列的线性变化,以很小的代价生成许多能从原始特征发掘所需信息的“幻影”特整图呢?这个便是整篇文章的核心思想

深度卷积神经网络通常是由大量的卷积块所组成的,导致大量的计算成本。尽管最近的工作,例如MobileNet和ShuffleNet引入了深度可分离卷积或混洗操作,以使用较小的卷积核(浮点运算)来构建有效的CNN,其余1x1卷积层仍将占用大量内存和FLOPs。

1)Ghost module

 intrinsic feature maps 执行 linear operations 得到 ghost feature maps,其中cheap operations(linear operation),如图中的Φ表示,从问题3中可知,Φ是诸如3*3的卷积,并且是逐个特征图的进行卷积(Depth-wise convolutional)

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,:,:]

2) Ghost bottlenecks

Ghost Bottlenecks ,结构与ResNet的是类似的,并且与mobilenet-v2一样在第二个module之后不采用ReLU激活函数
左边是stride=1的Ghost Bottlenecks,右边是stride=2的Ghost Bottlenecks,目的是为了缩减特征图大小

注意 : Ghost Bottlenecks模块的 mid_chs 参数

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
        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
        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

3)SqueezeExcite 模块

如何解释SE模块?

Sequeeze 对C×H×W 进行global average pooling,得到 1×1×C 大小的特征图,这个特征图可以理解为具有全局感受野。

Excitation :使用一个全连接神经网络,对Sequeeze之后的结果做一个非线性变换。

特征重标定:使用Excitation 得到的结果作为权重,乘到输入特征上。

Sequeeze-and-Excitation的层次结构如下
1、AdaptiveAvgPool2d
2、Linear
3、ReLU
4、Linear
5、Sigmoid
先拆成两部分Squeeze部分和Excitation部分
Squeeze部分就是AdaptiveAvgPool2d
Excitation部分就是2到5
先是 squeeze 很形象的词挤压柠檬汁,挤压使用的函数是AdaptiveAvgPool2d(1),就像以管理小白兔的方式挤压柠檬汁,挤压柠檬汁之后就是Excitation,汁少( 特征少)的那就大棒伺候,汁多(特征多)的给胡萝卜,特征少的抑制它,特征多的就多多关注它。

AdaptiveAvgPool2d 的操作如下:

class SELayer(nn.Module):
    def __init__(self, channel, reduction=16):
        super(SELayer, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Sequential(
            nn.Linear(channel, channel // reduction, bias=False),
            nn.ReLU(inplace=True),
            nn.Linear(channel // reduction, channel, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        b, c, _, _ = x.size()
        y = self.avg_pool(x).view(b, c)
        y = self.fc(y).view(b, c, 1, 1)
        return x * y.expand_as(x)
  • squeeze方式:仅仅比较了max和avg,发现avg要好一点。
  • 一般设置reduction=16
  • excitation方式:使用了ReLU,Tanh,Sigmoid,发现Sigmoid好
  • stage: resnet50有不同的阶段,通过实验发现,将se施加到所有的阶段效果最好。
  • 集成策略:将se放在残差单元的前部,后部还是平行于残差单元,最终发现,放到前部比较好

注意:添加SE模块能提升大目标的检测效果,但是对小目标检测效果有不利影响,所以可以进行实验,然后决定是否使用该模块

参考:

https://github.com/huawei-noah/ghostnet/blob/master/pytorch/ghostnet.py

1.1.2  Structured Knowledge Distillation for Semantic Segmentation

从简单的像素式精馏方案开始, 该方案应用于图像分类的精馏方案, 并分别对每个像素进行知识精馏。我们进一步提出将结构化知识从大型网络提炼成小型网络, 其动机是语义分割是一个结构化预测问题。我们研究两种结构化蒸馏方案: (i) 对精馏, 蒸馏成对的相似性, (ii) 整体蒸馏, 使用 GAN 提取整体知识


基于配对的蒸馏方案是由广泛研究的对马尔可夫随机场框架 [22] 来加强空间标记连续性, 目的是加强空间标记的连续性, 其目的是使从紧凑型网络和繁琐的网络计算的像素之间的对等相似性对齐

 整个蒸馏过程分为了三个过程其按照顺序分别是:1) Pair-wise distillation 2)Pixel-wise distillation 3)Holistic distillation

注意:总共有4种loss,分别是 pre 和 gt 的loss、 pre_student[0] 和 pre_teacher[0]  、 pre_student[1:4] 和 pre_teacher[1:4]、还有对抗网络的loss

1)Pair-wise distillation

首先当输入的图像分别经过两个网络之后会生成两个维度相同的特征表示。作者根据pair-wise Markov random field framework的启发,通过两个像素之间的相似性关系来提升网络的效果。也可以认为是将Teacher生成的特征映射中像素相似性关系蒸馏到Student网络中。

static affinity graph表示空间成对的关系,受成对马尔可夫随机场框架被广泛用于改善空间标记的连续性的启发,这里使用成对关系,尤其是像素间的成对相似性。这里使用了一个平方误差来衡量各个位置之间的成对相似性蒸馏损失。

    

  • 节点(Node)表示不同的空间位置,两个节点之间的连接表示相似性(similarity)
  • 每个节点的连接范围(connection range alpha)和间隔尺寸(granularity beta)控制着静态关联图(static affinity graph)的大小
  • 由上面图示可知,alpha表示相邻的节点,beta表示聚合多少个像素作为一个节点
    总共会有 W ′ × H ′ β \frac{W'\times H'}{\beta}βW′×H′​个节点,S、T网络的静态关联图的L2 Loss就是pair-wise KD
  • 这里还是有点不太清楚,每个node之间的相似性a i j a_{ij}aij​就是余弦距离,但是α \alphaα起到什么作用,因为这样会生成NodexNode个大小的SAG
class CriterionPairWiseforWholeFeatAfterPool(nn.Module):
    def __init__(self, scale, feat_ind):
        '''inter pair-wise loss from inter feature maps'''
        super(CriterionPairWiseforWholeFeatAfterPool, self).__init__()
        self.criterion = sim_dis_compute
        self.feat_ind = feat_ind #-5
        self.scale = scale #0.5

    def forward(self, preds_S, preds_T):
        '''
        :preds_S 学生网络feature
        :preds_T 教师网络feature
        '''
        feat_S = preds_S[self.feat_ind]
        feat_T = preds_T[self.feat_ind]
        #这里注意detach的写法,好像补写也ok?
        feat_T.detach()

        total_w, total_h = feat_T.shape[2], feat_T.shape[3]
        #patch_w, patch_h scale即beta聚合后feature大小,按照文章说法不是avgpooling,然后余弦距离
        #这里scale还不是我想的将featurescale的大小,而是pool的大小,即beta aggregate后feature大小为 int(1/scale)-(2)
        #这么小吗,一个feature只只剩下1/scale x 1/scale (4)个node,那alpha有什么用。。。
        patch_w, patch_h = int(total_w*self.scale), int(total_h*self.scale)
        #maxpooling ceil_mode 是否将kernel以外不满足大小的部分也做maxpooling,False则舍弃
        maxpool = nn.MaxPool2d(kernel_size=(patch_w, patch_h), stride=(patch_w, patch_h), padding=0, ceil_mode=True) # change
        loss = self.criterion(maxpool(feat_S), maxpool(feat_T))
        return loss
def L2(f_):
    '''
    按通道求根号下(求和(x))
    -> c通道二范数
    '''
    return (((f_**2).sum(dim=1))**0.5).reshape(f_.shape[0],1,f_.shape[2],f_.shape[3]) + 1e-8

def similarity(feat):
    '''
    计算相似性
    :feat feature map after pooling
    -> nodexnode similarity matrix
    '''
    feat = feat.float()
    tmp = L2(feat).detach()
    #先除以范数归一化
    feat = feat/tmp
    #reshape -> nxcxhw
    feat = feat.reshape(feat.shape[0],feat.shape[1],-1)
    #底下这个爱因斯坦求和是真的秀,由于node数量只有4ge,分patch都省了,直接矩阵相乘即可
    #这样返回nodexnode的similarity feature,node所在通道和其他所有node通道之间的向量点积
    return torch.einsum('icm,icn->imn', [feat, feat])

def sim_dis_compute(f_S, f_T):
    '''
    :f_S, f_T pooling后的feature
    '''
    sim_err = ((similarity(f_T) - similarity(f_S))**2)/((f_T.shape[-1]*f_T.shape[-2])**2)/f_T.shape[0]
    sim_dis = sim_err.sum()
    return sim_dis

2)Pixel-wise distillation

计算S和T输出的概率图之间KL散度,对两个网络最后输出得到的特征表示做Pixel Labeling操作,得到了一个Score Map,这个可以看作是最终得到的分割结果(W'×H'×C)因此作者希望Student网络得到的Score Map表示能够与Teacher网络保持一致,也就是使得每个像素下的表示是相同的

3)Holistic distillation

  • conditional GAN
    将输出和条件(输入图像)concatenate送到net_D
  • Wasserstein distance
    wgan 限制w范围的一个参考写法

整体蒸馏的损失可以表达如下:

  • E表示计算期望的操作
  • D表示嵌入网络,表现如同GAN中的判别器,将Q和I一起投影到整体嵌入得分上
  • 通过梯度惩罚来满足Lipschitz条件

Lipschitz限制则体现为,在整个样本空间  上,要求判别器函数D(x)梯度的Lp-norm不大于一个有限的常数K:

Wasserstein GAN最新进展:从weight clipping到gradient penalty,更加先进的Lipschitz限制手法

  • 来自S和T网络的分割图和作为条件的RGB图像分别拼接作为嵌入网络D的输入
  • D是一个有着五个卷积层的全卷积网络,两个自注意力模块被插入到最后三层中间来捕获结构信息,这样的判别器能够生成一个整体嵌入表达,以指示输入图像和分割图的匹配程度

参考:

生成式对抗网络GAN有哪些最新的发展,可以实际应用到哪些场景中?

论文-知识蒸馏

模型压缩之Structured Knowledge Distillation for Semantic Segmentation

CVPR2019 语义分割之教师知识提取

1.1.3 MobileNeXt | 新一代手机端模型,精度速度双超MobileNetV2

SandGlass

​ 已有研究表明:(1) 更宽的网络有利于缓解梯度混淆问题并有助于提升模型性能;(2)逆残差模块中的短连接可能会影响梯度回传

作者对其设计规则进行重思考并提出了SandGlass模块缓解上述问题。该模块的设计主要源自如下几点分析:

  • 保持更多的信息从bottom传递给top层,进而有助于梯度回传;
  • 深度卷积是一种轻量型单元,可以执行两次深度卷积以编码更多的空间信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值