最初1x1卷积是在Network in Network中提出的,之后1x1convolution最初在GoogLeNet中大量使用,1x1卷积有以下几个特点:
最初被使用在Inception模块中,主要是将跨通道相关性和空间相关性的操作拆分为一系列独立的操作。
先使用1x1 Convolution来约束通道个数,降低计算量,然后每个分支都是用3x3卷积,最终使用concat的方式融合特征。pytorch代码实现如下:
class InceptionA(nn.Module):
def __init__(self, in_channels, pool_features, conv_block=None):
super(InceptionA, self).__init__()
if conv_block is None:
conv_block = BasicConv2d
self.branch1x1 = conv_block(in_channels, 64, kernel_size=1)
self.branch5x5_1 = conv_block(in_channels, 48, kernel_size=1)
self.branch5x5_2 = conv_block(48, 64, kernel_size=5, padding=2)
self.branch3x3dbl_1 = conv_block(in_channels, 64, kernel_size=1)
self.branch3x3dbl_2 = conv_block(64, 96, kernel_size=3, padding=1)
self.branch3x3dbl_3 = conv_block(96, 96, kernel_size=3, padding=1)
self.branch_pool = conv_block(in_channels, pool_features, kernel_size=1)
def _forward(self, x):
branch1x1 = self.branch1x1(x)
branch5x5 = self.branch5x5_1(x)
branch5x5 = self.branch5x5_2(branch5x5)
branch3x3dbl = self.branch3x3dbl_1(x)
branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl)
branch3x3dbl = self.branch3x3dbl_3(branch3x3dbl)
branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
branch_pool = self.branch_pool(branch_pool)
outputs = [branch1x1, branch5x5, branch3x3dbl, branch_pool]
return outputs
def forward(self, x):
outputs = self._forward(x)
return torch.cat(outputs, 1)
组卷积最初是在AlexNet中提出的,之后被大量应用在ResNeXt网络结构中,提出的动机就是通过将feature 划分为不同的组来降低模型计算复杂度。下图则是一个组卷积的CNN结构。filters被分成了两个group。每一个group都只有原来一半的feature map。
所谓分组就是将输入feature map的通道进行分组,然后每个组内部进行卷积操作,最终将得到的组卷积的结果Concate到一起,得到输出的feature map。ResNeXt是ResNet和Inception的结合,其每个分支都采用的相同的拓扑结构。ResNeXt本质是使用组卷积(Grouped Convolutions),通过基数( cardinality )来控制组的数量。
使用组卷积的优点:
可分离卷积可以分为空间可分离卷积(Spatially Separable Convolutions)和深度可分离卷积(depthwise separable convolution)。假设feature的size为[channel, height , width]
简单来讲,空间可分离卷积就是将原nxn的卷积,分开计算变为1xn和nx1两步。普通的3x3卷积在一个5x5的feature map上,每个位置需要9次乘法,一共有9个位置,所以整个操作下来就是9x9=81次乘法操作。如果用空间可分离卷积的话,如下图所示:
第一步先使用3x1的filter,所需计算量为:15x3=45
第二步使用1x3的filter,所需计算量为:9x3=27
总共需要72次乘法就可以得到最终结果,要小于普通卷积的81次乘法。推广一下,假设对nxn的feature map使用kernel size为mxm的卷积,普通卷积需要计算的乘法次数为:
空间可分离卷积需要计算的乘法次数为:
那么代价比为:
在n>>m的情况下,这个比值将变为2/m,所以可以极大降低计算量。
虽然空间可分离卷积节省了计算成本,但是一般情况很少用到。原因是并非所有的kernel 都可以分为两个较小的kernel;空间可分离卷积可能会带来一定的信息损失;如果将全部的传统卷积替换为空间可分离卷积,将影响模型的容量, 这样得到的训练结果可能是次优的。
深度可分离卷积在Xception或者MobileNet中大量使用,主要有两个部分组成:
class DWConv(nn.Module):
def __init__(self, in_plane, out_plane):
super(DWConv, self).__init__()
self.depth_conv = nn.Conv2d(in_channels=in_plane,
out_channels=in_plane,
kernel_size=3,
stride=1,
padding=1,
groups=in_plane)
self.point_conv = nn.Conv2d(in_channels=in_plane,
out_channels=out_plane,
kernel_size=1,
stride=1,
padding=0,
groups=1)
def forward(self, x):
x = self.depth_conv(x)
x = self.point_conv(x)
return x
通过比对代码,很容易理解下图的操作过程:
Inception模块和可分离卷积的区别:
下面再来比较一下所需计算量:
以上图为例,普通卷积需要的计算量为:
128 × ( 3 × 3 × 3 ) × ( 5 × 5 ) = 86400 128\times(3\times3\times3)\times(5\times5)=86400 128×(3×3×3)×(5×5)=86400
对应128个3x3x3的卷积核移动5x5次的结果
深度可分离卷积计算量应该分为两个部分:
两步总计10275次乘法,只有普通卷积计算量的12%左右。推广一下,对于一个输入尺寸为[C,H,W]的feature map, 如果用stride=1、padding=0、kernel size=h(h为奇数),那么输出的尺寸为 [ N c , H − h + 1 , W − h + 1 ] [N_c, H-h+1,W-h+1] [Nc,H−h+1,W−h+1]。普通卷积需要的乘法次数为: N c × h × h × C × ( H − h + 1 ) × ( W − h + 1 ) N_c{\times}h{\times}h{\times}C{\times}(H-h+1){\times}(W-h+1) Nc×h×h×C×(H−h+1)×(W−h+1)
深度可分离卷积所需要乘法次数为:
代价比为:
在Nc>>h的情况下,代价比可约等于h的平方分之一。
同样,深度可分离卷积也有缺点,通过使用深度可分离卷积替代普通的卷积,可以显著降低模型的计算量,但是与此同时会导致模型的容量也显著降低。这将导致训练得到的结果可能也不是最优的。因此,在使用深度可分离卷积的时候要考虑模型容量和计算效率的平衡。
最初在Flattened Convolutional Neural Networks for Feedforward Acceleration中提出,是2015年ICLR的workshop。了解了空间可分离卷积以后,再来看Flattened Convolutions就比较简单了,Flattened Convolution将标准的卷积核拆分成3个1D卷积核(空间可分离卷积只拆分HxW维度),可以极大地降低了计算成本。
论文在结论中提到,使用Flattened Convolutions能够将计算量减少为原来的10倍,并可以达到类似或更高的准确率在CIFAR-10、CIFAR-100和MNIST数据集中。
深度学习中的学习型滤波器具有分布特征值,直接将分离应用在滤波器中会导致严重的信息损失,过多使用的话会对模型准确率产生一定影响。
最初是在ShuffleNet中提出的,使用了pointwise group convolution和channel shuffle两种操作,能够在保持精度的同时极大地降低计算量。Channel Shuffle操作主要是为了消除原来Grouped Convolution中存在的副作用,也就是输出feature map的通道仅仅来自输入通道的一小部分,因此每个滤波器组仅限于学习一些特定的特性,如下图(a)所示。Grouped Convolution的这个属性会阻碍信息在通道组之间的信息流动并削弱了模型的表达。通过使用Channel Shuffle可以促进通道间信息的融合从而解决以上问题。
从上图中,(a)代表的是组卷积,所有输出只和一部分输入有关(b)代表的是Channel Shuffle组合的方式,不同的组内部进行了重排,都是用到了输入的一部分(c)代表的是一种与(b)等价的实现方式。ShuffleNet还用到了pointwise grouped convolution, 作者认为1x1卷积成本也非常高,所以也对1x1卷积使用组卷积,具体模块化的设计如下图(b)和©所示。
所以实际上用到了三种类型的卷积:
空洞卷积是在DeepLabv1和《Multi-scale context aggregation by dilated convolutions》中提出的。空洞卷积是在kernel之间插入空洞,并引入了空洞率,具体操作如下:
空洞卷积能够在不增加计算量的情况下,增加模型的感受野。并且如果使用多个空洞卷积组成多层结构来构建网络,有效感受野将呈指数级增长,而所需要的参数数量仅仅呈线性增长。空洞卷积用于多尺度的上下文信息,并且不会丢失分辨率,在其应用到语义分割模型后,达到了当时的STOA。
不过也存在几个问题:
可变形卷积(DCN) 是一个特别新颖的想法,最初是为了提升目标检测模型,提出以后也成为刷榜利器。DCN提出的动机是为何特征提取形状一定要是正方形或者矩形,为何不能更加自适应的分布到目标上,进而提出了一种形状可学习的卷积。
实现过程如下图所示,在普通卷积之外又添加了一个分支,专门用于学习每个点的偏移量,将原始的卷积和offset结合就形成了可变形卷积。
SENet的核心想法是,每个通道的重要性不是一样的。基于这个想法,SENet添加了一个模块,如上图靠上的分支,这个模块作用是给每个通道打分,最终将打分的结果与卷积得到的feature map相乘,完成特征通道的权重重分配。SENet是通道注意力机制的最经典的实现。pytorch实现:
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)
缺点:不利于并行处理,添加SELayer以后导致在GPU上运行速度有一定的减慢。
CBAM模块算是比较早的一批将通道注意力机制和空间注意力机制结合起来的模型,通过添加该模块,能在一定程度上优化feature。
CBAM分为Channel Attention Module 和 Spatial Attention Module:
class ChannelAttention(nn.Module):
def __init__(self, in_planes, rotio=16):
super(ChannelAttention, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.max_pool = nn.AdaptiveMaxPool2d(1)
self.sharedMLP = nn.Sequential(
nn.Conv2d(in_planes, in_planes // ratio, 1, bias=False), nn.ReLU(),
nn.Conv2d(in_planes // rotio, in_planes, 1, bias=False))
self.sigmoid = nn.Sigmoid()
def forward(self, x):
avgout = self.sharedMLP(self.avg_pool(x))
maxout = self.sharedMLP(self.max_pool(x))
return self.sigmoid(avgout + maxout)
class SpatialAttention(nn.Module):
def __init__(self, kernel_size=7):
super(SpatialAttention, self).__init__()
assert kernel_size in (3,7), "kernel size must be 3 or 7"
padding = 3 if kernel_size == 7 else 1
self.conv = nn.Conv2d(2,1,kernel_size, padding=padding, bias=False)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
avgout = torch.mean(x, dim=1, keepdim=True)
maxout, _ = torch.max(x, dim=1, keepdim=True)
x = torch.cat([avgout, maxout], dim=1)
x = self.conv(x)
return self.sigmoid(x)
以上卷积核可以这样分类:
现在很多CNN模型准确率越来越高,很多研究人员的研究方向也转向如何在尽可能保证准确率的情况下,尽可能减少模型参数,做好准确率和速度的平衡。
总结一下效果优异的人工设计的backbone可能会用到以下策略: