存货, 介绍了从Inception系列到Resnet系列的发展历史以及基本理念和pytorch实现
出自颜水成组的Network in Network(ICLR 2014)中. 传统CNN使用线性滤波器, 为了捕捉高度非线性关系, 需要堆积卷积核, 计算量太大. 本文的贡献主要是
- 提出MLP conv层, 即用多层感知机建模卷积层之间的非线性关系, 可以
- 实现跨通道的交互和信息整合.
- 进行卷积核通道数的降维和升维,减少网络参数
- 具体到实现上, 可以用1x1 卷积核来近似实现. 普通卷积层+多个1*1卷积(followed by 激活层)等价于类patch级别的MLP
- 提出用全局均值池化提到全连接层: 比如输出k类, 最后一层conv layer就有k个channel, 最后经过全局均值池化输出一个k维向量
- 原作把MLP conv和maxout进行比较, 并表示
maxout network imposes the prior that instances of a latent concept lie within a convex set in the input space, which does not necessarily hold
出自Google Brain. C Szegedy, CVPR 2015
- 整个网络由9个上图形成的block组成
- 第三个和第五个block有辅助分类器处理梯度消失问题
- 图中的1x1 conv降维不仅可以降低计算量, 还可以使得数据更加dense:
Clustering sparse matrices into relatively dense submatrices tends to give state of the art practical performance for sparse matrix multiplication
出自Google Brain. C Szegedy, CVPR 2016.
-
论文首先提出了一些在构建深度网络时值得借鉴的意见.
- 不要过早地引入Bottleneck. 并且Bottleneck之间过多地减少维度可能会造成信息的损失. 当卷积不会大幅度改变输入维度时,神经网络可能会执行地更好
- Higher dimensional representations are easier to process locally within a network. 没太理解. 似乎是说特征维度越高, 越容易训练
- 在卷积之前提前降维信息损失会很低. 这可能是因为相邻层之间有较大的协方差(strong correlation between adjacent unit), 所以可以先降维来降低运算量, 训练速度也会提升, 这就是Bottleneck概念的来源
- 平衡网络的深度和宽度
-
提出了分解大卷积核为小卷积核的想法(Factorizing Convolutions with Large Filter Size).
- 感受野相同的情况下, 将 5×5 的卷积分解为两个 3×3 的卷积, 降低了2.78倍运算量
- 将 n*n 的卷积核尺寸分解为 1×n 和 n×1 两个卷积
-
提出Bottleneck概念, 先降维再升维. 这个概念取自于自编码器? 例如:
- 输入输出皆为256 channel的卷积层, 由3x3x256个卷积核组成
- 转换成一组1x1x64, 3x3x64, 1x1x256维的卷积层, 计算量降低了接近10倍
最后提出了Label Smoothing来对网络输出正则化, 也是率先使用了batch norm的网络之一
v3是同一篇论文提出的, 增加了RMSProp优化器, Factorized 7x7 卷积, 辅助分类器使用了 BatchNorm
使用了更多骨骼惊奇的building block, 并且结合了resnet的跳跃式结构.
Xception: Deep Learning with Depthwise Separable Convolutions
某层卷积层输入channel为16, 输出channel为32
- 使用32个k×k×16的卷积核, 参数数量为:
32×k×k×16
- 使用16个k×k×1的卷积核分别在16个channel上独自进行卷积, 输出为16个channel, 再用32个1×1×16的一维卷积核, 输出为32个channel, 参数数量为:
16×k×k×1+32×1×1×16
由此可见参数量被大大减小. 在实现上, Depthwise Conv
等价于一个k×k×16的卷积核在尚未sum各个channel的值之前就输出成16个channel, 然后再接32个Pointwise Conv
, 即1×1卷积核
注: Depthwise Conv
在pytorch中可以用groups
参数实现:
1
2
|
# A Depthwise Conv layer
nn.Conv2d(128, 128, kernel_size=3, groups=128)
|
resnet由四个stage
组成, 而每个stage
又由数个bottleneck
组成.
bottleneck
结构借鉴了Inception v2
一个Bottleneck由3层CBR(conv-batch norm-relu)层组成, 见下:

同时一个stage
由若干重复bottleneck
组成, 整个网络由4个stage
组成
- 每个
stage
的开头第一个bottleneck
的3x3卷积层的步长是2, 使得每个stage
将feature map尺寸缩小一半
- 每个
stage
的开头第一个bottleneck
需要处理上一个stage
的输出通过shortcut与该bottleneck
的输出相加时通道不匹配的问题, 故需要增加一层Conv-BN处理通道一致性

构造Bottleneck
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
class Bottleneck(nn.Module):
"""
ResNet 50/101 BottleNeck
"""
def __init__(self, in_planes, k_planes, stride=1):
"""
:param in_planes: input channels
:param k_planes: conv kernel output channels in each stage
:param stride: the 3x3 kernel in a stage may set to 2
only in each group of stages' 1st stage
"""
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(in_planes, k_planes, 1, bias=False)
self.bn1 = nn.BatchNorm2d(k_planes)
self.conv2 = nn.Conv2d(k_planes, k_planes, 3, padding=1, stride=stride, bias=False)
self.bn2 = nn.BatchNorm2d(k_planes)
self.conv3 = nn.Conv2d(k_planes, k_planes * 4, 1, bias=False)
self.bn3 = nn.BatchNorm2d(k_planes * 4)
self.relu = nn.ReLU(inplace=True)
self.shortcut = nn.Sequential() # empty Sequential module returns the original input
if stride != 1 or in_planes != k_planes * 4: # tackle input/output size/channel mismatch during shortcut add
self.shortcut = nn.Sequential(
nn.Conv2d(in_planes, k_planes * 4, 1, stride=stride, bias=False),
nn.BatchNorm2d(k_planes * 4)
)
def forward(self, x):
out = self.relu(self.bn1(self.conv1(x)))
out = self.relu(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
out += self.shortcut(x)
out = self.relu(out)
return out
|
构造第一层conv1
1
2
3
4
5
6
7
|
def _make_conv1():
return nn.Sequential(
nn.Conv2d(3, 64, 7, stride=2, padding=3, bias=False), # (224-7+2*3) // 2 +1 = 112
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
# nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # shrink to 1/4 of original size
)
|
构造四个stage
1
2
3
4
5
6
|
def _make_stage(ch_in, ch, num_blocks, stride=1):
layers = []
layers.append(Bottleneck(ch_in, ch, stride)) # only the first stage in the module need stride=2
for i in range(1, num_blocks):
layers.append(Bottleneck(ch * 4, ch))
return nn.Sequential(*layers)
|
1大量使用BatchNorm. BN应该在激活函数之前还是之后呢?
- https://zhuanlan.zhihu.com/p/28124810
- https://www.zhihu.com/question/64494691
- https://zhuanlan.zhihu.com/p/28749411
2. 为什么resnet?
- https://www.zhihu.com/question/64494691
核心思路是在Bottleneck
中:
- 利用1x1卷积核, 先降维再升维的方式降低计算开销(下图c)
- 在降维过程中, 利用Xception的
Depthwise Conv
进一步降低计算开销(下图b)
- 将输入的128通道分成32组, 每组4个通道
- 对每组使用4个核为3x3的卷积层, 输出4个通道
- 将32个卷积层的4x32=128个通道concat为128个输出层


与ResNet的比较:
构造Bottleneck
, 和resnet相比改动很简单, 参考上图最右边的实现, 在每个stage的第二个卷积层启用group
参数即可.
大体思路是对各个channel(或者说整张图片)的一个attention, 个人理解就是结合了文章开头NIN的理念和Attention机制
- 通过全局均值池化将C个channel变成C维向量.
- 第一个FC层称为squeeze层, 将维度压缩为reduction, 使用ReLU, 起到LSTM中门控的作用
- 第二个FC层称为excitation层, 将维度还原为C, 使用Sigmoid, 起到输出权重作用, 重新赋予每个channel权重. 核心思想是建模通道间的相关性
- 最后输出的向量作为各层的权重乘到输出上
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class SEScale(nn.Module):
def __init__(self, channel, reduction=16):
super(SEScale, self).__init__()
self.fc1 = nn.Conv2d(channel, reduction, kernel_size=1, padding=0)
self.fc2 = nn.Conv2d(reduction, channel, kernel_size=1, padding=0)
def forward(self, x):
x = F.adaptive_avg_pool2d(x,1)
x = self.fc1(x)
x = F.relu(x, inplace=True)
x = self.fc2(x)
x = F.sigmoid(x)
return x
|
带SEScale的ResNext:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
class SENextBottleneckBlock(nn.Module):
def __init__(self, in_planes, planes, out_planes, groups, reduction=16, is_downsample=False, stride=1):
super(SENextBottleneckBlock, self).__init__()
self.is_downsample = is_downsample
self.conv_bn1 = ConvBn2d(in_planes, planes, kernel_size=1, padding=0, stride=1)
self.conv_bn2 = ConvBn2d( planes, planes, kernel_size=3, padding=1, stride=stride, groups=groups)
self.conv_bn3 = ConvBn2d( planes, out_planes, kernel_size=1, padding=0, stride=1)
self.scale = SEScale(out_planes, reduction)
if is_downsample:
self.downsample = ConvBn2d(in_planes, out_planes, kernel_size=1, padding=0, stride=stride)
def forward(self, x):
z = F.relu(self.conv_bn1(x),inplace=True)
z = F.relu(self.conv_bn2(z),inplace=True)
z = self.conv_bn3(z)
if self.is_downsample:
z = self.scale(z)*z + self.downsample(x)
else:
z = self.scale(z)*z + x
z = F.relu(z,inplace=True)
return z
|