为了提高网络性能,目前的研究趋向于更深,更复杂的网络,ImageNet刷分的前几名网络的参数量也非常巨大,没有高性能GPU的玩家只有长太息以掩涕兮了,然而,目前嵌入式设备和移动平台对深度学习模型部署的大量需求,促使轻量级网络成为一个热门的研究方向。于是,就记录一下看到的走轻量级路线的卷积吧。
在这里插入图片描述

1. 标准卷积

  nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)
  一般文献里面把我们常说的卷积核叫Filter,卷积核所含的一个通道叫Kernel,对于标准卷积过程,in_channels为特征输入的通道数,out_channels为Filter的个数,一个Filter带一个偏置。

import torch
import torch.nn as nn
k1 = nn.Conv2d(in_channels=3, out_channels=1, kernel_size=3)
print('k1的偏置:\n', k1.bias)
k2 = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3)
print('k2的偏置:\n',k2.bias)

输出为:

k1的偏置:
 Parameter containing:
tensor([0.0128], requires_grad=True)
k2的偏置:
 Parameter containing:
tensor([ 0.1486, -0.0689, 0.0052], requires_grad=True)

标准卷积过程如图所示【nn.Conv2d(in_channels=3, out_channels=1, kernel_size=2)】:
在这里插入图片描述

2. 组卷积(Group Convolution)

  GroupConv的概念最早是在AlexNet中,由于GPU性能有限,当时模型被分成两个GPU进行训练。GroupConv将卷积Filter分为G组,输入特征映射通道也分为G组,每组卷积Filter处理对应的一组输入特征映射通道。由于每一组卷积Filter只应用于相应的输入通道组,卷积的计算代价显著降低。
  但是,通道信息并不在不同的组之间共享,即不同组的输出Feature map通道只从对应组的输入通道接收信息。这阻碍了不同组通道之间的信息流动,降低了GroupConv的特征提取能力。为了解决这一问题,ShuffleNet进行了通道洗牌操作,增强了不同组通道之间的信息交换。

  组卷积减少参数示例计算:
  设In_channels = 6,Out_channels = 8,Filter为3×3:

  【1】对于标准卷积:3×3×6×8 = 432
  8个Filter,每个Filter有6个Kernel,每个Kernel有3×3 = 9个参数,故一共有3×3×6×8 = 432个参数。

  【2】对于组卷积(分2组):3×3×3×4 + 3×3×3×4 = 216
  若分为两组,则输入和输出通道都降低为原来的一半
  一组有4个Filter,每个Filter有3个Kernel,每个Kernel有3×3 = 9个参数,一共有2组,故一共有2×3×3×3×4 = 216个参数。

  组卷积过程如下图所示【nn.Conv2d(in_channels=6, out_channels=8, kernel_size=3, groups=2)】
在这里插入图片描述

3. 深度可分离卷积(Depthwise Separable Convolution)

  标准卷积同时对输入的特征图进行特征提取和通道融合。MobileNetV1中的深度可分离卷积将标准卷积分解为深度卷积,即DW卷积(depthwise convolution, DW)和点卷积(pointwise convolution, PW),在DW卷积中,对每个输入通道应用一个卷积核,通常3×3卷积用于特征提取,点卷积对DW卷积的输出Feature map进行1×1标准卷积,实现通道尺度上的融合。
  故,通过分割特征提取与通道融合,深度可分离卷积显著减少了参数量。
  标准卷积与DSC卷积的过程差异如下图所示:
在这里插入图片描述
  深度可分离卷积减少参数示例计算:
  设In_channels = 5,Out_channels = 10,Filter为3×3:

  【1】对于标准卷积:3×3×5×10 = 270
  10个Filter,每个Filter有5个Kernel,每个Kernel有3×3 = 9个参数,故一共有3×3×5×10 = 270个参数。

  【2】对于DSC卷积:3×3×5 + 1×1×5×10 = 95
  (1)DW:5个Filter,每个Filter有1个Kernel,共3×3×5 = 45个参数;
  (2)PW:10个Filter,每个Filter有5个Kernel,每个Kernel有1个参数,一共有1×1×5×10 = 50个参数;
  二者相加则有3×3×5 + 1×1×5×10 = 95个参数

DSC卷积实现代码(DW卷积也相当于分组为in_channels的组卷积):

class DSCConv(nn.Module):
    def __init__(self,in_ch,out_ch):
        super(DSCConv, self).__init__()
        
        self.depth_conv = nn.Conv2d(in_channels=in_ch,
                                    out_channels=in_ch,
                                    kernel_size=3,
                                    stride=1,
                                    padding=1,
                                    groups=in_ch)
        
        self.point_conv = nn.Conv2d(in_channels=in_ch,
                                    out_channels=out_ch,
                                    kernel_size=1,
                                    stride=1,
                                    padding=0,
                                    groups=1)
    def forward(self,x):
        out = self.depth_conv(x)
        out = self.point_conv(out)
        return out

4. 异构卷积(Heterogeneous Convolution)

  HetConv在一个卷积Filter中同时包含3×3卷积核和1×1 卷积核,异构Filter以移位方式排列,3×3卷积Kernel是离散排列的,3×3和1×1的卷积Kernel在Filter中交替使用。使用异构卷积,原始3×3标准卷积的计算复杂度可以降低3到8倍,而异构设计从本质上破坏了跨通道信息集成的连续性,并对输入特征图的完整信息的保存产生了负面影响。因此,这种策略会降低网络的准确性。
在这里插入图片描述
  原论文中图示如上:即输入特征图的一部分通道应用k×k的卷积核,其余的通道应用1×1的卷积核。其中,P为控制卷积核为k的比例,若M为一个Filter中的Kernel个数,则M/P为一个Filter中3×3卷积核的个数。多个Filter采用循环位移的方式排列3×3卷积核与1×1卷积核。
在这里插入图片描述
  异构卷积减少参数示例计算:
  设In_channels = 6,Out_channels = 4:

  【1】对于标准卷积:3×3×6×4 = 216
  Filter中每个Kernel都为3×3大小
  4个Filter,每个Filter有6个Kernel,每个Kernel有3×3 = 9个参数,故一共有3×3×6×4 = 216个参数。

  【2】对于异构卷积(P = 3):( 3×3×2 + 1×1×4 ) × 4 = 88
  4个Filter,每个Filter有6个Kernel,其中有2个为3×3大小,4个为1×1大小,故一共有( 3×3×2 + 1×1×4 ) × 4 = 88个参数

异构卷积实现代码:

class HetConv(nn.Module):
    def __init__(self, in_channels, out_channels, p): 
        super(HetConv, self).__init__()
        self.in_channels = in_channels
        self.out_channels = out_channels
        
        num_k3 = in_channels // p    
        num_k1 = in_channels - num_k3  
        interval = num_k1 // num_k3 
        
        self.kernels = []
        for i in range(num_k3):
            self.kernels.append(1)
            for j in range(interval):
                self.kernels.append(0)
                
        self.all_filters = nn.ModuleList()
        
        for k in range(out_channels):
            if k == 0:
                self.all_filters.append(self.make_filter())
            else:
                temp = self.kernels.pop(-1)
                self.kernels.insert(0, temp)
                self.all_filters.append(self.make_filter())
        
    def make_filter(self,):
        filters = nn.ModuleList()
        for i in range(self.in_channels):
            if self.kernels[i] == 1 :
                filters.append(nn.Conv2d(1, 1, 3, 1, 1))
            elif self.kernels[i] == 0:
                filters.append(nn.Conv2d(1, 1, 1, 1, 0))
        return filters

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

打印查看:

k = HetConv(in_channels=6, out_channels=4, p=3)
print(k)
HetConv(
  (all_filters): ModuleList(
    (0): ModuleList(
      (0): Conv2d(1, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (1): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
      (2): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
      (3): Conv2d(1, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (4): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
      (5): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
    )
    (1): ModuleList(
      (0): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
      (1): Conv2d(1, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (2): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
      (3): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
      (4): Conv2d(1, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (5): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
    )
    (2): ModuleList(
      (0): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
      (1): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
      (2): Conv2d(1, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (3): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
      (4): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
      (5): Conv2d(1, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    )
    (3): ModuleList(
      (0): Conv2d(1, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (1): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
      (2): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
      (3): Conv2d(1, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (4): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
      (5): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1))
    )
  )
)

hiddenlayer可视化:

import hiddenlayer as hl

k3 = HetConv(in_channels=6, out_channels=4, p=3)
hl_graph = hl.build_graph(k3, torch.zeros([3, 6, 224, 224]))
hl_graph.theme = hl.graph.THEMES['blue'].copy()
hl_graph.save('./k3.png', format='png')

在这里插入图片描述

参考文献:
【1】HetConv: Heterogeneous Kernel-Based Convolutions for Deep CNNs
【2】DualConv: Dual Convolutional Kernels for Lightweight Deep Neural Networks

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐