1、torchvision的介绍
torchvision 是 pytorch 中一个很好用的包,主要由 3 个子包,分别是 torchvision.datasets,torchvision.models 和 torchvision.transforms
参考官网:http://pytorch.org/docs/master/torchvision/index.html 代码:https://github.com/pytorch/vision/tree/master/torchvision
1.1 torchvision.models
在 torchvision的models中实现了几个模型,包含 AlexNet,DenseNet,ResNet,VGG 等常用结构,并提供了预训练模型,以ResNet50为例:
- 源码:models/resnet.py
- 导入模型示例:
import torchvision
# 需要预训练模型的参数来初始化
model = torchvision.models.resnet50(pretrained=True)
# 不需要预训练模型的参数来初始化
model = torchvision.models.resnet50(pretrained=False)
# pretrained参数默认是False,等价于
model = torchvision.models.resnet50()
1.2 图解ResNet的多种结构
彩图resnet18的结构图中,虚曲线表示不同维度的连接,实曲线表示相同维度的连接
- resnet18都是由BasicBlock组成的,并且从表中也可以得知,50层(包括50层)以上的resnet才由Bottleneck组成。
- 所有类型的resnet卷积操作的通道数(无论是输入通道还是输出通道)都是64的倍数
- 所有类型的resnet的卷积核只有3x3和1x1两种
- 无论哪一种resnet,除了公共部分(conv1)外,都是由4大块组成(con2_x,con3_x,con4_x,con5_x,),每一块的起始通道数都是64,128,256,512,这点非常重要。暂且称它为“基准 通道数”
- 无论哪种resnet,都有4个layer,进入layer之前,输入图片就已经被缩小了4倍了(一个卷积和一个最大池化操作各1/2)。除了第一个layer不会缩小图片外,其余三个layer都会缩小一半图片。
2、3×3与1×1卷积模板
2.1 3×3卷积模板
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)
- 没有用到bias
- 卷积步长 stride=1 (视情况而定)
- 扩张大小 dilation=1(也就是padding)
- in_planes 和 out_planes 分别是输入和输出的通道数
- groups 是分组卷积参数,这里 groups=1 相当于没有分组
2.2 1×1卷积模板
def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
- 没有用到bias
- 卷积步长 stride=1 ()
3、代码解析
3.1 BasicBlock
class BasicBlock(nn.Module):
expansion = 1 #expansion是BasicBlock和Bottleneck的核心区别之一
# 这是第一种连接方式组成的块,用于18层和34层,大于等于50层的用第二种
# inplanes、inplanes其实就是channel,叫法不同
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(BasicBlock, self).__init__()
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")
# Both self.conv1 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = norm_layer(planes)
self.downsample = downsample
self.stride = stride
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
该模块里定义了 ResNet 最重要的残差模块,BasicBlock 是基础版本,使用了两个 3*3 的卷积,卷积后接着 BN 和 ReLU
- super(BasicBlock, self).init() 这句是固定的标准写法。一般神经网络的类都继承自 torch.nn.Module,init() 和 forward() 是自定义类的两个主要函数,在自定义类的 init() 中需要添加一句 super(Net, self).init(),其中 Net 是自定义的类名,用于继承父类的初始化函数。
注意在 __init()__ 中只是对神经网络的模块进行了声明,真正的搭建是在 forward() 中实现
。自定义类的成员都通过 self 指针来访问,所以参数列表中都包含了 self - out += identity 就是 ResNet 的精髓,在输出上叠加了输入 x xx
- self.downsample = downsample和if self.downsample is not None 看出,在默认情况downsample=None,表示不做downsample,但有一个情况需要做,就是一个 BasicBlock的分支x要与output相加时,若x和output的通道数不一样,则要做一个downsample,
剧透一下,在resnet里的downsample就是用一个1x1的卷积核处理,变成想要的通道数。为什么要这样做?因为最后要x要和output相加啊, 通道不同相加不了。所以downsample是专门用来改变x的通道数的
。 接下来分析BasicBlock处理后的图像的维度是如何变化的
:卷积的计算公式,W为特征图的长度(或宽度),F为卷积核的长度(或宽度),S为步长(默认是1
),P为padding(padding在没指定的情况下,默认也是1
)。所以卷积后的特征图的尺寸就跟步长很有关系。BasicBlock虽然是经过两个3x3的卷积,但是前一个是设置了步长的(设置的步长是2),后一个则是没有设置步长的,这意味着用的是默认步长(默认步长是1)
。F=3,P=1,当S=1时,W是不变的; F=3,P=1,当S=2时,W会减少两倍;下面的Bottleneck也是这个原理。
3.2 Bottleneck
class Bottleneck(nn.Module):
expansion = 4 #expansion是BasicBlock和Bottleneck的核心区别之一
# 第二种连接方式,这种参数少,适合层数超多的
# inplanes、inplanes其实就是channel,叫法不同
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(Bottleneck, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
width = int(planes * (base_width / 64.)) * groups
# Both self.conv2 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv1x1(inplanes, width)
self.bn1 = norm_layer(width)
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)
self.downsample = downsample
self.stride = stride
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
BasicBlock的结构如左图所示,Bottleneck 结构如右图所示。两个 1×1 的卷积分别负责减少通道数量和恢复通道数量,从而为中间的 3*3 卷积降低参数量,虽然减少通道数量会有信息损失,但是影响不太大
,比如这里第一个卷积将 256 维的 channel 先降到 64 维,然后再通过第二个卷积恢复到 256 维,整体的参数量为 (1x1x256)x64 + (3x3x64)x64 + (1x1x64)x256 = 69632,而使用 BasicBlock结构的话,就是两个 3x3x256 的卷积,参数数目为 (3x3x256)x256x2 = 1179648,相差 16.94 倍。
- 与基础版本的BasicBlock 不同的是这里有 3 个卷积,分别为1×1、3×3、1×1大小的卷积核,分别用于压缩维度、卷积处理、恢复维度。
- inplanes 是输入通道数,planes = 输出通道数/expansion,在基础版本 BasicBlock 中 expansion =1,表示block内部最后一个卷积的输出channel与第一个卷积的输出channel比值(),此时相当于没有倍乘,输出的通道数就等于 planes.
- 在使用 Bottleneck 时,它先对通道数进行压缩,再放大,所以传入的参数 planes 不是实际输出的通道数,而是 block 内部压缩后的通道数,真正的输出通道数为 plane*expansion
- 这样做的主要目的是,使用 Bottleneck 结构可以减少网络参数数量
3.3 BasicBlock和Bottleneck的区别与共同点
3.3.1 区别
- BasicBlock的卷积核都是2个3x3,Bottleneck则是一个1x1,3x3,1x1共三个卷积核组成。
- BasicBlock的expansion为1,即输入和输出的通道数是一致的。而Bottleneck的expansion为4,即输出通道数是输入通道数的4倍。
3.3.2 共同点
- 不管是BasicBlock还是Bottleneck,最后都会做一个判断是否需要给x做downsample,因为必须要把x的通道数变成与输出的通道一致,才能相加(详细内容)。
把下图视为为一个box_block,即多个block叠加在一起,x3说明有3个上图一样的结构串起来,所以box_block
:
3.3.3 缓解梯度消失
- 为什么ResNet网络可以缓解梯度消失的问题呢? 跳连结构也被称为恒等映射:H(x)=F(x)+x。当F(x)=0时,H(x)=x,这就是所谓的恒等映射。跳连的这根线,可以实现差分放大的效果,将梯度放大,来缓解梯度的消失。
假设F(x)=2x,当x从5变化成为5.1时,F(x)从10变为10.2。
如果这时候求F(x)的导数的话,公式为(10.2-10)/(5.1-5)=2。而如果变成H(x)的话,导数为(10.2+5.1-(10.0+5))/(5.1-5)=3.这样就放大了导数,即梯度。
3.4 ResNet(重要)
3.4.1 所有网络都是通过ResNet类产生的,只要传入不同的参数即可
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
self.inplanes = 64
super(ResNet, self).__init__()
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
self.avgpool = nn.AvgPool2d(7, stride=1)
self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
def _make_layer(self, block, planes, blocks, stride=1):
# downsample 主要用来处理H(x)=F(x)+x中F(x)和xchannel维度不匹配问题
downsample = None
# self.inplanes为上个box_block的输出channel,planes为当前box_block块的输入channel
# (注意:_make_layer()中的planes参数是“基准通道数”,不是输出通道数!!!)
# 在shotcut中若维度或者feature_size不一致则需要downsample
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
# 只在这里传递了stride=2的参数,因而一个box_block中的图片大小只在第一次除以2
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion
for i in range(1, blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
3.4.2 不同层数ResNet模型的结构(重要)
- ResNet 共有五个阶段,其中第一阶段为一个 7*7 的卷积,stride = 2,padding = 3,然后经过 BN、ReLU 和 maxpooling,此时特征图的尺寸已成为输入的 1/4
- 接下来是四个阶段(con2_x,con3_x,con4_x,con5_x,),也就是代码中 layer1,layer2,layer3,layer4。这里用 _make_layer 函数产生四个 Layer,需要用户输入每个 layer 的 box_block 数目( 即layers列表 )以及采用的 block 类型(基础版 BasicBlock 还是 Bottleneck 版)
- _make_layer() 方法的第一个输入参数 block 选择要使用的模块是 BasicBlock 还是 Bottleneck 类,第二个输入参数 planes 是每个块的起始通道数(64,128,256,512),第三个输入参数 blocks 是每个 blocks 中包含多少个 residual 子结构。
无论哪种resnet,都有4个layer,进入layer之前,输入图片就已经被缩小了4倍了(一个卷积和一个最大池化操作各1/2)。除了第一个layer不会缩小图片外,其余三个layer都会缩小一半图片
。- 从ResNet的forward()代码来看,它是先经过conv1(),bn,relu和maxpool(),从表格得知,这几层无论是resnet18,resnet34,resnet50,resnet101等等的resnet一开始都必须经过这几层,这是静态的。然后进入四层layer(),这是动态以的区别具体是resnet18,resnet34,resnet50,resnet101等等中的哪一个。
输出尺寸的分析
:在output size那里,维度是依次下降两倍的,224-112-56-28-14-7,但是我们会不会有疑问,经过那么多个卷积核为什么才下降两倍呢?这个实现是与ResNet类中的_make_layer()方法密切相关。看到_make_layer()源码,有没有怀疑过为什么都是建立block,但是要分开两个步骤建呢,是因为上面那个是设置了步长的,步长为2,下面那个是用默认步长的,步长为1,当步长为2时,结合卷积核分析,会减低2倍特征图尺寸;步长为1时,则不变。也就是说除了第一个layer不会缩小图片外,其余三个layer都会缩小一半图片(因为第一个layer的步长始终都为1)恒等映射分两种:高宽不变,维度变;高宽变为二分之一,维度变
图像输入大小问题
:在旧版的torchvision中,其预训练权重的默认图片大小为224×224,若图片大小经模型后缩小后和最后一层全连接层不匹配,则会抛出异常,比如输入大小256×256。新版已经兼容了输入图片的大小,方法就是使用AdaptiveAvgPool2d- 部分网络结构图:
3.4.3 不同层数ResNet模型具体模型的定义
-
__all__ 列表
定义了可以从外部 import 的函数名或类名,_all_
列表的每一个resnet都提供了实现函数 - 根据 model_urls 的地址可以加载网络与训练权重
import torch
import torch.nn as nn
from .utils import load_state_dict_from_url
# 实现了不同层数的ResNet模型
__all__ = ['ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101',
'resnet152', 'resnext50_32x4d', 'resnext101_32x8d',
'wide_resnet50_2', 'wide_resnet101_2']
model_urls = {
'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth',
'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth',
'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',
'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth',
'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
'resnext50_32x4d': 'https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth',
'resnext101_32x8d': 'https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth',
'wide_resnet50_2': 'https://download.pytorch.org/models/wide_resnet50_2-95faca4d.pth',
'wide_resnet101_2': 'https://download.pytorch.org/models/wide_resnet101_2-32ee1156.pth',
}
def resnet18(pretrained=False, progress=True, **kwargs):
"""Constructs a ResNet-18 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet18', BasicBlock, [2, 2, 2, 2], pretrained, progress,
**kwargs)
def resnet34(pretrained=False, progress=True, **kwargs):
"""Constructs a ResNet-34 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet34', BasicBlock, [3, 4, 6, 3], pretrained, progress,
**kwargs)
def resnet50(pretrained=False, progress=True, **kwargs):
"""Constructs a ResNet-50 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet50', Bottleneck, [3, 4, 6, 3], pretrained, progress,
**kwargs)
def resnet101(pretrained=False, progress=True, **kwargs):
"""Constructs a ResNet-101 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet101', Bottleneck, [3, 4, 23, 3], pretrained, progress,
**kwargs)
def resnet152(pretrained=False, progress=True, **kwargs):
"""Constructs a ResNet-152 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet152', Bottleneck, [3, 8, 36, 3], pretrained, progress,
**kwargs)
def resnext50_32x4d(pretrained=False, progress=True, **kwargs):
"""Constructs a ResNeXt-50 32x4d model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 4
return _resnet('resnext50_32x4d', Bottleneck, [3, 4, 6, 3],
pretrained, progress, **kwargs)
def resnext101_32x8d(pretrained=False, progress=True, **kwargs):
"""Constructs a ResNeXt-101 32x8d model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 8
return _resnet('resnext101_32x8d', Bottleneck, [3, 4, 23, 3],
pretrained, progress, **kwargs)
3.4.4 以ResNet50举例
源码中的ResNet类可以根据输入参数的不同,变成resnet18,34,50,101等,比如调用ResNet50:
from torchvision.models.resnet import resnet50
resnet = resnet50(pretrained=True)
短短两行代码就可以调用resnet了。接下来我们顺着源码“顺藤摸瓜”地看一下代码的执行流程,从resnet50(pretrained=True)可以看出,调用的是resnet50网络,然后通过在torchvision.models.resnet模块中可以找到resnet50()的定义:
def resnet50(pretrained=False, progress=True, **kwargs):
"""Constructs a ResNet-50 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet50', Bottleneck, [3, 4, 6, 3], pretrained, progress,
**kwargs)
可以看到,resnet50()仅仅做了一件事,就是调用了配备了特定参数的_resnet()方法,即_resnet(‘resnet50’, Bottleneck, [3, 4, 6, 3], pretrained, progress, **kwargs),第一个参数不用说了,而我们看到 Bottleneck参数,就可以联想到_resnet的第二个参数是resnet网络的组成部分,即不是BasicBlock就是Bottleneck,而[3,4,6,3]具体是什么意思我们要接着看_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
- 可以看到在_resnet()的[3,4,6,3]对应的位置,是显示的layers(即layers[0]是3,layers[1]是4,layers[2]是6,layers[3]是3),因此[3,4,6,3]表示按次序生成3个Bottleneck,4个Bottleneck,6个Bottleneck,3个Bottleneck,所以layers[0]是3个Bottleneck,layers[1]是4个Bottleneck,layers[2]是6个Bottleneck,layers[3]是3个Bottleneck。
- 接着判断是否需要预训练(pretrained是否为True),为True则加载权重后返回模型,为False就直接返回模型。 load_state_dict 方法用预训练的模型参数来初始化你构建的网络结构,该方法有一个重要参数是 strict,默认值为 True,
表示预训练模型的层和你的网络结构层严格对应相等(比如维度和层名)
。 - _resnet()中block参数对应的位置就是BasicBlock或Bottleneck。即block就是表示BasicBlock或Bottleneck。
所以一句话概括_resnet()的作用就是先调用Resnet类生成一个Resnet的壳子,若需要预训练则加载权重,不需要就直接返回Resnet的壳子
。
接下来就仔细看看Resnet类的代码了:
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000, zero_init_residual=False,
groups=1, width_per_group=64, replace_stride_with_dilation=None,
norm_layer=None):
super(ResNet, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self._norm_layer = norm_layer
self.inplanes = 64
self.dilation = 1
if replace_stride_with_dilation is None:
# each element in the tuple indicates if we should replace
# the 2x2 stride with a dilated convolution instead
replace_stride_with_dilation = [False, False, False]
if len(replace_stride_with_dilation) != 3:
raise ValueError("replace_stride_with_dilation should be None "
"or a 3-element tuple, got {}".format(replace_stride_with_dilation))
self.groups = groups
self.base_width = width_per_group
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = norm_layer(self.inplanes)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
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])
self.layer3 = self._make_layer(block, 256, layers[2], stride=2,
dilate=replace_stride_with_dilation[1])
self.layer4 = self._make_layer(block, 512, layers[3], stride=2,
dilate=replace_stride_with_dilation[2])
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight, 0)
elif isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)
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
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
self.base_width, previous_dilation, norm_layer))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
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)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.reshape(x.size(0), -1)
x = self.fc(x)
return x
一眼看下去不知道该从何入手,但看pytorch的网络,要想知道它的执行顺序是怎么样的,看它的forward方法即可。从ResNet的forward代码来看,它是先经过conv1(),bn,relu和maxpool()。从之前的resnet表格得知,这几层无论是resnet18,resnet34,resnet50,resnet101等等的resnet一开始都必须经过这几层,这是静态的。然后进入四层layer()才是动态以区别具体是resnet18,resnet34,resnet50,resnet101等等中的哪一个。
其实所谓的layer1,2,3,4都是由不同参数的_make_layer()方法得到的。看_make_layer()的参数,发现了layers[0~3]就是上面输入的[3,4,6,3],即layers[0]是3,layers[1]是4,layers[2]是6,layers[3]是3。我们继续追寻_make_layer()的定义看看这些数字表示什么意思:
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
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
self.base_width, previous_dilation, norm_layer))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
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)
(注意:_make_layer()中的planes参数是“基准通道数”,不是输出通道数!!!不是输出通道数!!!不是输出通道数!!!),我们定位到_make_layer()的第三个(不算上self)参数blocks,在_make_layer()中用到blocks的地方是:
layers = []
layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
self.base_width, previous_dilation, norm_layer))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes, groups=self.groups,
base_width=self.base_width, dilation=self.dilation,
norm_layer=norm_layer))
可以看到其实blocks这个整数就是表示生成的block的数目。由于在resnet50填入的block是Bottleneck,所以blocks表示Bottleneck的数目。因此[3,4,6,3]表示按次序生成3个Bottleneck,4个Bottleneck,6个Bottleneck,3个Bottleneck。这个表格中resnet50的结构是一致的。所以layers[0]是3个Bottleneck,layers[1]是4个Bottleneck,layers[2]是6个Bottleneck,layers[3]是3个Bottleneck。综合来说
4、ResNet50的分析
4.1 解决的问题
由于梯度消失,深层网络很难训练。因为梯度反向传播到前面的层,重复相乘可能使梯度无穷小。结果就是,随着网络的层数更深,其性能趋于饱和,甚至迅速下降。
4.2 核心思想
引入一个恒等映射(也称之为跳跃连接线),直接跳过一个或者多个层,尺寸一样时可以相加,尺寸,当尺寸不一样时,需要进行downsample操作,使得维度一致。
- 在网络上堆叠这样的结构,就算梯度消失,我什么也学不到,我至少把原来的样子恒等映射了过去,相当于在浅层网络上堆叠了“复制层”,这样至少不会比浅层网络差。
- 万一我不小心学到了什么,那就赚大了,由于我经常恒等映射,所以我学习到东西的概率很大。
- 恒等映射分两种:高宽不变,维度变;高宽变为二分之一,维度变
4.3 数学推导
关于为什么残差结构(即多了一条跳跃连接线后)为什么一定程度缓解了梯度消散的数学推导:
4.4 50层的由来