C3: Concentrated-Comprehensive Convolution and its application to semantic segmentation

题目:C3模块-空洞可分离卷积存在的问题及轻量化语义分割模型架构技巧

❝ 轻量化语义分割网络多采用深度可分离卷积和空洞卷积结合的方式,提升性能的同时保持尽可能大的感受野,但是前者对于标准卷积的不恰当近似和后者存在的空格效应,使得特征图信息丢失,模型表现不佳。为此文章提出C3模块即"Concentrated-Comprehensive Convolution Block",用于构建新型的轻量化语义分割网络。

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

工程地址:https://github.com/clovaai/c3_sinet

深度可分离空洞卷积的问题

空洞卷积是语义分割中的常用操作,实现感受野扩张的同时不会减小特征图尺寸,为了降低复杂度,通常会为其结合深度可分离卷积。令C_{i},C_{o},H_{i(0)},W_{i(0)},M,N分别表示:输入和输出的特征图通道数,输入/输出特征图的高和宽,卷积核的尺寸,输入特征图F\in R^{C_{i}\times H_{i}\times W_{i}},卷积K\in R^{C_{i}\times C_{0}\times M\times N},扩张率为d的扩张卷积的输出特征图O\in R^{C_{0}\times H_{0}\times W_{0}}的计算如下所示:

O_{​{C}'},h,w= \sum_{c}\sum_{m}\sum_{n}F_{c},_{h}+dm,w+dnKc{}',m,n

为该扩张卷积应用深度可分类卷积,有:

{F}'_{​{C}'},h,w= \sum_{c}{F}'_{c,h,w}K_{​{c}',c}^{p}

上述过程并没有计算跨通道的信息,也就是通道之间没有实现有效的信息交流,仅仅是在空间维度进行了计算。这里参数量从k^{2}C_{i}C_{0}减少到了C_{i}(k^{2}+C_{0})(当M=N=k),而浮点数运算则从2k^{2}C_{i}C_{0}减少到了2C_{i}(k^{2}+C_{0})

但是这种对标准空洞卷积的近似会造成明显的模型表现衰退,况且标准空洞卷积本身也并非“完人”(是空洞卷积本质上是空间离散的操作,这会导致信息的丢失;是空洞卷积跳过了邻近特征反而会受到更远的区域的影响,这使得对于小目标或者窄目标的分割容易失误。如下图所示)。

一、Concentrated-Comprehensive Convolution

基于上面的观察提出C3模块,包含两个阶段,concentration阶段和comprehensive convolution阶段。如下图所示:前者通过在空洞卷积之前应用一个简单的深度卷积来消除特征信息的损失,这能够压缩跳跃的特征信息并且增强局部一致性。相较于使用常规的深度卷积参数量和FLOPS分别为(2d-1)^{2}C_{i}2(2d-1)^{2}H_{0}W_{0}C_{i},论文使用了两个不对称的的深度卷积,将计算复杂度从降低到了O(N^{2})降低到了2O(N),并且在二者之间加入了PReLU和BN;后者则使用深度空洞卷积来扩张感受野,之后通过一个1×1的点卷积进行跨通道混合。简单来看,C3综合了深度可分离卷积和空洞卷积的优势,使得模型参数量和计算复杂度降低的同时能够实现较好的模型表现。

二、C3模块

基于上述的C3 block为轻量分割模型设计了一个C3模块, 如下图所示,首先通过一个1×1点卷积降低通道数,然后通过一个并行结构,经过一个分层特征融合(扩张率从小到大的融合),与跳跃结构形成最后的输出。

YOlOv5中的C3代码实现

  """
 定义自动padding,为了保持图像大小卷积前后一致,就需要用到自动padding
  """
def autopad(k, p=None):                        # kernel  padding 根据卷积核大小k自动计算卷积核padding数(0填充)
    """
    :param k: 卷积核的 kernel_size
    :param p: 卷积的padding  一般是None
    :return:  自动计算的需要pad值(0填充)
    """
    if p is None:
        # k 是 int 整数则除以2, 若干的整数值则循环整除
        p = k // 2 if isinstance(k, int) else [x // 2 for x in k]
    return p

  """
 C3模块中的基本Conv模块,方便调取复用,提高代码的复用性
  """
class Conv(nn.Module):
    def __init__(self, c1, c2, k=1, s=1, p=None, act=True, g=1):
        """
        :param c1: 输入的channel值
        :param c2: 输出的channel值
        :param k: 卷积的kernel_size
        :param s: 卷积的stride
        :param p: 卷积的padding  一般是None
        :param act: 激活函数类型   True就是SiLU(), False就是不使用激活函数
        :param g: 卷积的groups数  =1就是普通的卷积  >1就是深度可分离卷积
        """
        super(Conv, self).__init__()

        self.conv_1 = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=True)
        self.bn = nn.BatchNorm2d(c2)

        self.act = nn.SiLU() if act else nn.Identity()     # 若act=True, 则激活,  act=False, 不激活

    def forward(self, x):

        return self.act(self.bn(self.conv_1(x)))

  """
 Bottleneck模块中包含一个残差连接结构(左),和不包含的残差结构(右),就需要传入参数,来判断是否需要使用残差结构
  """
class Bottleneck(nn.Module):
    def __init__(self, c1, c2, e=0.5, shortcut=True, g=1):
        """
        :param c1: 整个Bottleneck的输入channel
        :param c2: 整个Bottleneck的输出channel
        :param e: expansion ratio  c2*e 就是第一个卷积的输出channel=第二个卷积的输入channel
        :param shortcut: bool Bottleneck中是否有shortcut,默认True
        :param g: Bottleneck中的3x3卷积类型  =1普通卷积  >1深度可分离卷积
        """
        super(Bottleneck, self).__init__()

        c_ = int(c2*e)                            # 使通道减半, c_具体多少取决于e
        self.conv_1 = Conv(c1, c_, 1, 1)
        self.conv_2 = Conv(c_, c2, 3, 1, g=g)
        self.add = shortcut and c1 == c2

    def forward(self, x):
        return x + self.conv_2(self.conv_1(x)) if self.add else self.conv_2(self.conv_1(x))


"""
定义C3结构
"""
class C3(nn.Module):
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
        """
        :param c1: 整个 C3 的输入channel
        :param c2: 整个 C3 的输出channel
        :param n: 有n个Bottleneck
        :param shortcut: bool Bottleneck中是否有shortcut,默认True
        :param g: C3中的3x3卷积类型  =1普通卷积  >1深度可分离卷积
        :param e: expansion ratio
        """
        super(C3, self).__init__()
        c_ = int(c2 * e)
        self.cv_1 = Conv(c1, c_, 1, 1)
        self.cv_2 = Conv(c1, c_, 1, 1)
        # *操作符可以把一个list拆开成一个个独立的元素,然后再送入Sequential来构造m,相当于m用了n次Bottleneck的操作
        self.m = nn.Sequential(*[Bottleneck(c_, c_, e=1, shortcut=True, g=1) for _ in range(n)])
        self.cv_3 = Conv(2*c_, c2, 1, 1)

    def forward(self, x):
        return self.cv_3(torch.cat((self.m(self.cv_1(x)), self.cv_2(x)), dim=1))

实验结果

参考:C3模块-空洞可分离卷积存在的问题及轻量化语义分割模型架构技巧_cnn中c3模块中文全称_不会算命的赵半仙的博客-程序员宅基地 - 程序员宅基地 (its301.com)

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦在黎明破晓时啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值