欢迎访问我的博客首页。
1. BasicBlock 和 Bottleneck
resnet-18 和 resnet-34 基于 BasicBlock,层数更多的 ResNet 基于 Bottleneck。BasicBlock 包含两个 3x3 的卷积和一个利用 1x1 的卷积实现的下采样。Bottleneck 包含一个 1x1 的卷积 (为深度可分离卷积准备通道)、一个深度可分离卷积 (一个 3x3 的卷积DW、一个 1x1 的卷积PW) 和一个利用 1x1 的卷积实现的下采样。相比 BasicBlock,Bottleneck 的参数更少,适合在层数更多的 ResNet 中使用。
def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=dilation, groups=groups, bias=False, dilation=dilation)
def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
1.1 BasicBlock
相比 Bottleneck,BasicBlock 的结构教简单,所以它用于创建层数较少的 resnet-18、resnet-34。
图
1.1
r
e
s
n
e
t
−
18
和
r
e
s
n
e
t
−
34
中
使
用
B
a
s
i
c
B
l
o
c
k
创
建
的
4
组
残
差
块
图\ 1.1\quad resnet-18 和 resnet-34 中使用 BasicBlock 创建的 4 组残差块
图 1.1resnet−18和resnet−34中使用BasicBlock创建的4组残差块
创建 resnet-18 时 [ n 1 , n 2 , n 3 , n 4 ] = [ 2 , 2 , 2 , 2 ] [n1, n2, n3, n4] = [2, 2, 2, 2] [n1,n2,n3,n4]=[2,2,2,2],创建 resnet-34 时 [ n 1 , n 2 , n 3 , n 4 ] = [ 3 , 4 , 6 , 3 ] [n1, n2, n3, n4] = [3, 4, 6, 3] [n1,n2,n3,n4]=[3,4,6,3]。灰色区域表示这一块需要重复若干次,比如 resnet-34 的 n2=4 则第 2 列包含 1 个不是灰色的残差块和 3 个灰色的残差块。下面是 BasicBlock 的代码。
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(BasicBlock, self).__init__()
# 1.参数。
self.stride = stride
if norm_layer is None:
norm_layer = nn.BatchNorm2d
if groups != 1 or base_width != 64:
raise ValueError('BasicBlock only supports groups=1 and base_width=64')
if dilation > 1:
raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
# 2.两个卷积层和一个下采样层。当步长不为1时,self.conv1和self.downsample都会进行下采样。
# 2.1 卷积层。
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
# 2.2 卷积层。
self.conv2 = conv3x3(planes, planes)
self.bn2 = norm_layer(planes)
# 2.3 下采样层(有1x1的卷积层实现)。
self.downsample = downsample
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
由第 11 行和第 13 行可知,BasicBlock 不支持分组卷积和空洞卷积。
1.2 Bottleneck
Bottleneck 用于创建 resnet-50 等更深层的 ResNet。
图
1.2
r
e
s
n
e
t
−
50
等
更
深
层
的
R
e
s
N
e
t
中
使
用
B
o
t
t
l
e
n
e
c
k
创
建
的
4
组
残
差
块
图\ 1.2\quad resnet-50 等更深层的 ResNet 中使用 Bottleneck 创建的 4 组残差块
图 1.2resnet−50等更深层的ResNet中使用Bottleneck创建的4组残差块
作者提出的 ResNet 中 Bottleneck 在 1x1 的卷积 (self.conv1) 中实现下采样,而 torchvision 中的 Bottleneck 在 3x3 的卷积 (self.conv2) 中实现下采样。这时因为论文 Deep residual learning for image recognition 和 ResNet V1.5 证明这样的效果更好。
创建 resnet-50 时
[
n
1
,
n
2
,
n
3
,
n
4
]
=
[
3
,
4
,
6
,
3
]
[n1, n2, n3, n4] = [3, 4, 6, 3]
[n1,n2,n3,n4]=[3,4,6,3],创建 resnet-101 时
[
n
1
,
n
2
,
n
3
,
n
4
]
=
[
3
,
4
,
23
,
3
]
[n1, n2, n3, n4] = [3, 4, 23, 3]
[n1,n2,n3,n4]=[3,4,23,3],创建 resnet-152 时
[
n
1
,
n
2
,
n
3
,
n
4
]
=
[
3
,
8
,
36
,
3
]
[n1, n2, n3, n4] = [3, 8, 36, 3]
[n1,n2,n3,n4]=[3,8,36,3]。Bottleneck 包含 conv1x1、conv3x3、conv1x1 和一个可选的 downsample,下面是 Bottleneck 的代码。
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(Bottleneck, self).__init__()
# 1.参数。
self.stride = stride
if norm_layer is None:
norm_layer = nn.BatchNorm2d
width = int(planes * (base_width / 64.)) * groups
# 2.三个卷积层和一个下采样层。当步长不为1时,self.conv2和self.downsample都会进行下采样。
# 2.1 卷积层。
self.conv1 = conv1x1(inplanes, width)
self.bn1 = norm_layer(width)
# 2.2 深度可分离卷积。
self.conv2 = conv3x3(width, width, stride, groups, dilation)
self.bn2 = norm_layer(width)
self.conv3 = conv1x1(width, planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
# 2.3 下采样层(有1x1的卷积层实现)。
self.downsample = downsample
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
Bottleneck 支持 depthwise 卷积和空洞卷积,但 group 和 dilation 的默认值都是 1,也就是默认使用常规卷积。
2. 创建残差块
因为第一组残差块前使用了最大池化层,不再需要降采样,所以它和后三组残差块不太一样。从图 1.1 和图 1.2 可以看出,除第一组残差块外,其它组的第一个残差块都实现了下采样。
ResNet 共有 4 组残差块,每组残差块中包含若干个残差块。不同版本 ResNet 的各残差组包含的残差块如下:
ResNet 版本 | resnet-18 | resnet-34 | resnet-50 | resnet-101 | resnet-151 |
---|---|---|---|---|---|
block | BasicBlock | BasicBlock | Bottleneck | Bottleneck | Bottleneck |
残差组参数 | [2, 2, 2, 2] | [3, 4, 6, 3] | [3, 4, 6, 3] | [3, 4, 23, 3] | [3, 8, 36, 3] |
下面是创建 4 组残差块的代码,函数 _make_layer 根据残差组参数中 4 个参数中的一个参数,创建一组残差块。
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2, dilate=replace_stride_with_dilation[0]) # 1/2.
self.layer3 = self._make_layer(block, 256, layers[2], stride=2, dilate=replace_stride_with_dilation[1]) # 1/2.
self.layer4 = self._make_layer(block, 512, layers[3], stride=2, dilate=replace_stride_with_dilation[2]) # 1/2.
def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
norm_layer = self._norm_layer
downsample = None
previous_dilation = self.dilation
if dilate:
self.dilation *= stride
stride = 1
# 1.下采用层:缩小尺寸、通道乘4。
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)
# 2.第一个block。
layers = [block(self.inplanes, planes, stride, downsample, self.groups,
self.base_width, previous_dilation, norm_layer)]
# 3.剩下的block。
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
# block输出的特征的通道不是planes而是planes * block.expansion。
layers.append(block(self.inplanes, planes, groups=self.groups,
base_width=self.base_width, dilation=self.dilation,
norm_layer=norm_layer))
return nn.Sequential(*layers)
3. 创建 ResNet
def build_res_net():
return torchvision.models.resnet18(
pretrained=False, # 是否使用ImageNet上的预训练模型。
progress=True, # 是否显示下载预训练模型的进度条。
num_classes=1000, # 类别总数。
zero_init_residual=False, # 是否把每个Bottleneck和BasicBlock中的最后一个BN层的weight置0。
groups=1, # 在Bottleneck中使用分组卷积。
width_per_group=64, # 分组卷积中,每一组的通道数。
replace_stride_with_dilation=None, # 是否使用空洞卷积代替步长为2的卷积 。
norm_layer=None # 使用哪种归一化层。
)
上面创建 ResNet 使用的都是默认参数。通过调用下面的函数创建不同版本的 ResNet。
def _resnet(arch, block, layers, pretrained, progress, **kwargs):
model = ResNet(block, layers, **kwargs)
if pretrained:
state_dict = load_state_dict_from_url(model_urls[arch], progress=progress)
model.load_state_dict(state_dict)
return model
def resnet18(pretrained=False, progress=True, **kwargs):
return _resnet('resnet18', BasicBlock, [2, 2, 2, 2], pretrained, progress, **kwargs)
def resnet34(pretrained=False, progress=True, **kwargs):
return _resnet('resnet34', BasicBlock, [3, 4, 6, 3], pretrained, progress, **kwargs)
def resnet50(pretrained=False, progress=True, **kwargs):
return _resnet('resnet50', Bottleneck, [3, 4, 6, 3], pretrained, progress, **kwargs)
def resnet101(pretrained=False, progress=True, **kwargs):
return _resnet('resnet101', Bottleneck, [3, 4, 23, 3], pretrained, progress, **kwargs)
def resnet152(pretrained=False, progress=True, **kwargs):
return _resnet('resnet152', Bottleneck, [3, 8, 36, 3], pretrained, progress, **kwargs)
函数 _resnet 的第 1 个参数只是为了用对应版本的预训练参数初始化。第 2 个参数指定残差块,第 3 个参数指定网络结构,第 4、5 个参数指定是否使用预训练参数及下载预训练参数时是否显示进度条。