recent convolutions

octave conv(octconv)、hetconv、res2net

1. octave conv

Paper:

Drop an Octave: Reducing Spatial Redundancy in Convolutional Neural Networks with Octave Convolution

Motivation:

如下图所示:(a)自然图像可以被分解为高频部分和低频部分。高频分量表达细节,低频分量表达整体。很显然,低频分量是存在冗余的,在编码过程中可以节省。(b) 卷积层的输出通道也是如此,可以被分解为低频分量和高频分量并进行重组。(c)作者将低频分量的通道大小设置为高频分量通道大小的一半,用来减少冗余。(d) 低频部分和高频部分可以各自更新,并进行通道之间的交流。

mothods

作者提出了 conv 的改进版 OctConv 用来降低低频部分的冗余度。 Octconv 的结构如下所示:

当在第一个 OctConv 是 $\alpha_{in} = 0$,此时执行 ①② 两个操作。

if self.type == 'first':
    return self.H2H(x), self.H2L(self.avg_pool(x))
# H2H、L2H 均为传统卷积

当在最后一个 OctConv 中 $\alpha_{out} = 0$, 此时执行 ①③ 两个操作。

if self.type == 'last':
    hf, lf = x
    return self.H2H(hf) + self.L2H(self.upsample(lf))
# H2H、L2H 均为传统卷积

当其他情况时,执行 ①②③④四个操作

else:
    hf, lf = x
    return self.H2H(hf) + self.upsample(self.L2H(lf)), 
           self.L2L(lf) + self.H2L(self.avg_pool(hf))
# L2H、L2L、H2H、H2L 均为传统卷积

octconv 整体的实现代码如下所示:

class OctConv(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, 
    	         alpha_in=0.25, alpha_out=0.25, type='normal'):
        super(OctConv, self).__init__()
        self.kernel_size = kernel_size
        self.stride = stride
        self.type = type
        hf_in = int(in_channels * (1 - alpha_in))
        hf_out = int(out_channels * (1 - alpha_out))
        lf_in = in_channels - hf_in
        lf_out = out_channels - hf_out

        if stride == 2:
            self.downsample = nn.AvgPool2d(kernel_size=2, stride=stride)

        if type == 'first':
            self.H2H = nn.Conv2d(in_channels, hf_out, kernel_size=kernel_size, stride=1, padding=padding)
            self.H2L = nn.Conv2d(in_channels, lf_out, kernel_size=kernel_size, stride=1, padding=padding)
            self.avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)
        elif type == 'last':
            self.H2H = nn.Conv2d(hf_in, out_channels, kernel_size=kernel_size, padding=padding)
            self.L2H = nn.Conv2d(lf_in, out_channels, kernel_size=kernel_size, padding=padding)
            self.upsample = partial(F.interpolate, scale_factor=2, mode="nearest")
        else:
            self.L2L = nn.Conv2d(lf_in, lf_out, kernel_size=kernel_size, stride=1, padding=padding)
            self.L2H = nn.Conv2d(lf_in, hf_out, kernel_size=kernel_size, stride=1, padding=padding)
            self.H2L = nn.Conv2d(hf_in, lf_out, kernel_size=kernel_size, stride=1, padding=padding)
            self.H2H = nn.Conv2d(hf_in, hf_out, kernel_size=kernel_size, stride=1, padding=padding)
            self.upsample = partial(F.interpolate, scale_factor=2, mode="nearest")
            self.avg_pool = partial(F.avg_pool2d, kernel_size=2, stride=2)

    def forward(self, x):
        if self.type == 'first':
            if self.stride == 2:
                x = self.downsample(x)
            return self.H2H(x), self.H2L(self.avg_pool(x))

        elif self.type == 'last':
            hf, lf = x
            if self.stride == 2:
                hf = self.downsample(hf)
                return self.H2H(hf) + self.L2H(lf)
            else:
                return self.H2H(hf) + self.L2H(self.upsample(lf))
        else:
            hf, lf = x
            if self.stride == 2:
                hf = self.downsample(hf)
                return self.H2H(hf) + self.L2H(lf), self.L2L(self.avg_pool(lf)) + self.H2L(self.avg_pool(hf))
            else:
                return self.H2H(hf) + self.upsample(self.L2H(lf)), self.L2L(lf) + self.H2L(self.avg_pool(hf))

2. res2net

Paper:

Res2Net: A New Multi-scale Backbone Architecture

Methods:

作者提出了一种在更加细粒度(卷积层)的层面提升多尺度表达能力。其基本结构如下图(b) 所示:

传统的resnet结构如上图所示,作者在其基础上进行改进,在不增加计算量的同时,使其具备更强的多尺度提取能力。如上图(b)所示,作者采用了更小的卷积组来替代 bottleneck block 里面的 3x3 卷积。具体操作为:

  • 首先将 1x1 卷积后的特征图均分为 s 个特征图子集。每个特征图子集的大小相同,但是通道数是输入特征图的 1/s。
  • 对每一个特征图子集 $Xi$,有一个对应的 3x3 卷积 $K_i$ , 假设 $K_i$的输出是 $y_i$。接下来每个特征图子集 $X_i $会加上 $K{i-1}$ 的输出,然后一起输入进 $K_i$。为了在增大 s 的值时减少参数量,作者省去了 $X_1$ 的 3x3 网络。因此,输出 $y_i$ 可以用如下公式表示:

根据图(b),可以发现每一个 $X_j (j<=i)$ 下的 3x3 卷积可以利用之前所有的特性信息,它的输出会有比 $X_j$ 更大的感受野。因此这样的组合可以使 Res2Net 的输出有更多样的感受野信息。为了更好的融合不同尺度的信息,作者将它们的输出拼接起来,然后再送入 1x1 卷积,如上图(b)所示。

res2Net module 的实现代码如下所示:

class Res2NetBottleneck(nn.Module):
   expansion = 4

   def __init__(self, in_channels, out_channels, downsample=None, stride=1, scales=4, groups=1, se=False, norm_layer=None):
       super(Res2NetBottleneck, self).__init__()
       if out_channels % scales != 0:
           raise ValueError('Planes must be divisible by scales')
       if norm_layer is None:
           norm_layer = nn.BatchNorm2d
           
       # Both self.conv2 and self.downsample layers downsample the input when stride != 1
       self.conv1 = conv1x1(in_channels, out_channels, stride)
       self.bn1 = norm_layer(out_channels)

       self.conv2 = nn.ModuleList([conv3x3(out_channels // scales, out_channels // scales, groups=groups) for _ in range(scales-1)])
       self.bn2 = nn.ModuleList([norm_layer(out_channels // scales) for _ in range(scales-1)])

       self.conv3 = conv1x1(out_channels, out_channels * self.expansion)
       self.bn3 = norm_layer(out_channels * self.expansion)
       self.relu = nn.ReLU(inplace=True)

       self.downsample = downsample
       self.stride = stride
       self.scales = scales

   def forward(self, x):
       identity = x
       
       out = self.relu(self.bn1(self.conv1(x)))
       xs = torch.chunk(out, self.scales, 1)
       ys = []
       for s in range(self.scales):
           if s == 0:
               ys.append(xs[s])
           elif s == 1:
               ys.append(self.relu(self.bn2[s-1](self.conv2[s-1](xs[s]))))
           else:
               ys.append(self.relu(self.bn2[s-1](self.conv2[s-1](xs[s] + ys[-1]))))
       out = torch.cat(ys, 1)

       out = self.bn3(self.conv3(out))
       
       if self.downsample is not None:
           identity = self.downsample(identity)

       return self.relu(out + identity)

3. Hetconv

Paper:

HetConv: Heterogeneous Kernel-Based Convolutions for Deep CNNs

Methods:

本文提出了一种高效的异构卷积过滤器(一些核的大小是 3x3, 其余的是1x1),相较于 mobilenet的原始的 depthwise conv,能在不牺牲准确度的同时提升这些架构的效率。实现的方案很简单, 将传统卷积进行改进,对于某一个卷积,只有1/p个通道 使用 3x3 的卷积,其余均使用 1x1卷积,然后将所有通道相加,作为一个输出通道。

Hexconv module 的实现代码如下所示

class HetConv(nn.Module):
    def __init__(self, in_channels, out_channels, p=2):
        super(HetConv, self).__init__()
        if in_channels % groups != 0:
            raise ValueError('in_channels must be divisible by groups')
        self.in_channels = in_channels
        self.out_channels = out_channels
        self.blocks = nn.ModuleList()
        for i in range(out_channels):
            self.blocks.append(self._make_hetconv_layer(i, p))

    def _make_hetconv_layer(self, n, p):
        layers = nn.ModuleList()
        for i in range(self.in_channels):
            if ((i - n) % (p)) == 0:
                layers.append(nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1))
            else:
                layers.append(nn.Conv2d(in_channels=1, out_channels=1, kernel_size=1, padding=0))
        return layers

    def forward(self, x):
        out = []
        for i in range(0, self.out_channels):
            out_ = self.blocks[i][0](x[:, 0: 1, :, :])
            for j in range(1, self.in_channels):
               out_ += self.blocks[i][j](x[:, j:j + 1, :, :])
            out.append(out_)
        return torch.cat(out, 1)

三种卷积的构造方式总结:

  1. 三种卷积均在输入通道进行改进,前两种方案(res2net、octconv)都进行了特征通道的融合。但是第一种方案对硬件并不友好(没有进行试验的验证)。第三种方案可以看做是传统卷积的改善,将其中的某些通道设置为3x3,其他通道设置为1x1卷积。
  2. 常见的多尺度的获取方式:
    • 细粒度卷积(res2net方式)
    • NIN(例如 Inception 样式的卷积也可以)
    • 多尺度图像输出(图像金字塔)
    • 特征多尺度(融合多个尺度的特征图:FPN网络)
    • 空间金字塔池化(Spatial Pyramid Pooling)

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!