ResNet网络的构建过程
构建ResNet网络是通过ResNet类进行的,ResNet类继承了Pytorch网络的基类:torch.nn.Module,然后重写了 _init_ 方法和 forward 方法,__init__方法用来定义一些参数,forward方法用来定义数据在层之间的流动顺序。
构建ResNet网络时,在函数中一般是调用torchvision.model中的resnet50()/resnet18()/resnet101()等函数完成的,调用resnet50()/resnet18()/resnet101()的源代码如下:
def resnet50(pretrained=False, **kwargs):
"""Constructs a ResNet-50 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(Bottleneck, [3, 4, 6, 3], **kwargs)
# 在这一句当中调用了ResNet类中的函数,因为此时是构建resnet50,所以在参数第一个为Bottleneck
# **kwargs是将参数以字典的形式传入 在此例中传入的参数还需要num_classes,则在调用的时候后面应该写num_classes=1000或者别的数字
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet50']))
# pretrained为true表示加入进入预训练的模型 load_state_dict()函数用于加载参数
return model
def resnet18(pretrained=False, **kwargs):
"""Constructs a ResNet-18 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(BasicBlock, [2, 2, 2, 2], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet18']))
return model
def resnet101(pretrained=False, **kwargs):
"""Constructs a ResNet-101 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(Bottleneck, [3, 4, 23, 3], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet101']))
return model
在上面调用resnet50()等函数构造网络的时候,如果要调用预训练的模型,使用load_state_dict(model_zoo.load_url(model_urls[‘resnet50’]))进行调用,model_zoo.load_url()的函数代码如下:
import torch.nn as nn
import torch.utils.model_zoo as model_zoo
# 默认的resnet网络,已预训练
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',
}
在上面调用resnet50()等函数的过程中调用ResNet()以及Bottleneck()以及BasicBlock()类在下面会进行详细的讲解
- nn.Conv2d(in_channels,out_channels,kernel_size,stride=1,padding=0,dilation=1,groups=1,bias=true) 函数参数的讲解,in_channels表示输入的维度,out_channels表示输出维度,Kernel_size表示卷积核大小,stride表示步长大小,padding表示填充多少个0,dilation表示kernel的间距,groups表示卷积核的个数,bias表示是否加偏移量
- torch.nn.AvgPool2d(kernel_size, stride=None, padding=0, ceil_mode=False, count_include_pad=True)
- torch.nn.Linear(in_features,out_features,bias=True)
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
# 参数block对应的就是残差结构,层数不同,传入的block也不同,比如说ResNet18/34传入的就是BasicBlock所对应的残差结构,而ResNet50/101/152传入的就是Bottleneck所对应的残差结构
# layers对应的是一个残差结构的数目,是以列表的形式存储,比如对于ResNet50而言就是[3,4,6,3]
self.inplanes = 64
# 定义输入特征矩阵的channel,channel定义为64是因为经过maxpool之后channel就会变为64
super(ResNet, self).__init__()
# ResNet18、ResNet34、ResNet50、ResNet101等都是先经过由7*7的卷积核、out_channel为64,stride为2的卷积层构成
# 输入的图片都是3通道的,所以Conv2d的第一个参数为3
# 在进入ResNet网络之前,图片的大小为224*224,这里设置padding为3是为了令output_size为112*112,即满足下述公式:
# output_size = (input_size - kernel + 2*padding)/stride + 1
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = nn.BatchNorm2d(64)
# inplace等于true,表示对上层传下来的tensor直接修改,这样能够节省运算内存
# 这里使用BatchNorm2d()而不使用BathNorm1d()的原因:BatchNorm2d 是处理 4D 数据的,BathNorm1d()处理的是2D和3D的数据 而在ResNet类中传递的数据为4D的数据,对应的维度为[batchsize,channel,Height,Weight] 所以使用BatchNorm2d
self.relu = nn.ReLU(inplace=True)
# 经过Kernel_size=3 stride=2,padding=1的最大池化之后,output_size为
# (input_size - kernel + 2*padding)/stride + 1 = (112-3+2)/2+1 = 56
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, layers[0])
# _make_layer函数中的第一个参数block为Bottleneck或者BasicBlock对应的残差结构,使用Bottleneck还是BasicBlock根据ResNet多少层决定
# layer1对应的是conv2_x中的一系列残差块,残差块的多少由layers[0]决定
# layer1不设值stride的原因是因为layer1经过maxpool之后宽和高已经和输出相同了,不需要在进行宽和高的变化了,所以使用默认的stride=1
# conv2_x的结构是通过_make_layer函数完成的
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
# layer2对应的是conv3_x中的一系列残差块,残差块的多少由layers[1]决定
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)
# 表示平均池化下采用 第一个参数表示kernel_size 因为经过经过平均池化之后要输出1*1的,所以卷积核大小为7 stride为1
self.fc = nn.Linear(512 * block.expansion, num_classes)
# fc表示的是全连接层 对于ResNet18/34而言,最后一层输出维度为512,对于ResNet50/101/152而言,最后一层输出维度为2048,所以要使用512*block.expansion,全连接层的输出为num_classes
for m in self.modules():
if isinstance(m, nn.Conv2d):
# 判断该层是否是Conv2d,如果当前遍历的层是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):
# 这里应该是初始化BN算法中的γ,β ,分别初始化为1和0
m.weight.data.fill_(1)
m.bias.data.zero_()
# 上述的循环是卷积层的初始化操作
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
if stride != 1 or self.inplanes != planes * block.expansion:
# 这里这个判断的意义在于:不管是ResNet18/34还是ResNet50/101/152,他们的conv3_1 conv4_1 conv5_1 都需要进行下采样,即第一个残差块的shortcut虚线连接,并且conv3_1 conv4_1 conv5_1的另一个意义就是进行宽和高的变化,所以这几个都是stride=2。但是ResNet18/34和ResNet50/101/152的有一个地方不同,对于conv2_1而言,ResNet18/34不需要进行维度和宽高的变化(因为已经经过了maxpool),所以ResNet18/34不需要执行这个判断里面的语句,但是ResNet50/101/152的conv2_1则不一样,ResNet50/101/152不需要进行宽高的变化,但是需要进行维度的变化,所以conv2_1的stride=1,但是self.inplanes != planes * block.expansion,所以还是要执行判断下的语句
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion,
kernel_size=1, stride=stride, bias=False),
# shortcut虚线连接对于ResNet50/101/152而言需要进行维度的变化,对应的block.expansion为4,对于ResNet18/34而言不需要进行维度的变化,对应的block.expansion为1,stride对应的是宽和高的变化
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample))
# 这个对应的是conv2_1 conv3_1 conv4_1 conv5_1,在这些残差块中都需要shortcut虚线连接(除了ResNet18/34的conv2_1,ResNet18/34的conv2_1对应的downsample为none),所以要传入downsample和stride
self.inplanes = planes * block.expansion
# 在每一个大块的第一个残差结构执行完毕之后,维度已经发生了变化(ResNet18/34未发生变化 ResNet50/101/152已经变化了),所以在没一大块的后面的几个残差块的输入channel都变成了 planes*block.expansion
for i in range(1, blocks):
layers.append(block(self.inplanes, planes))
# 除去每一大块的第一个残差块,其他的残差块输入的channel都是self.inplanes(经过expansion变化之后的),但是后面的残差块的第一个卷积层的卷积核仍然是planes
return nn.Sequential(*layers)
# 通过非关键字参数传入,nn.Sequential将上面定义的一系列层结构组合在一起并返回
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 是一个tensor类型 对应 C*H*W 所以size(0)对应的为channel -1表示电脑自动计算 C*H*W / x.size(0) 在这里的含义就是将x各个维度的值平摊为一维的(因为avgpool完成之后 H和W都会变为1了)
x = self.fc(x)
return x
- ResNet50/101/152网络的构建所需类的讲解
上图表示的是ResNet50/101/152残差块的部分结构,用于和下面的源码进行对比分析
class Bottleneck(nn.Module):
expansion = 4
# 对于ResNet50而言,每一个残差块中最后一个卷积核都会扩大四倍,比如conv2_x中前两个是64 channel,而最后一个卷积层是 256 channel,所以在Bottleneck中的expansion为4
def __init__(self, inplanes, planes, stride=1, downsample=None):
# downsample对应的是ResNet网络结构中的虚线连接 比如说 ResNet50中的conv2_x到conv3_x的过渡,在shortcut分支需要使用虚线连接,从而将 56*56*64 的输入特征矩阵转变为 28*28*512 的输出特征矩阵
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
# 上面的conv1表示每一个残差块的第一个卷积层,从上面的图片可以看出第一个卷积层的卷积核为1*1 且步长为1 所以在这里使用默认步长,且使用padding为0即可(因为卷积核为1,不需要进行padding的设置)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,
padding=1, bias=False)
# conv2表示每一个残差块的第二个卷积层,从上面图片的右图可以看出第二个卷积层的卷积核为3*3 并且stride=2,所以在这里设置stride等于2,但是这里设置stride=stride的原因是:stride等于2这是针对每一个大块中的第一个残差块,因为一个残差块需要进行特征矩阵的变化(在这里特指宽和高的变化),所以需要设置为2,但是从上面图片的左图可以看出,其余的第二层卷积层只需要stride=1即可
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)
# 这里对应的是每一个残差块的第三个卷积层,和第一个卷积层不同的地方在于,第三个卷积层的卷积核的个数相比较第一个卷积层而言扩大了4倍,所以使用planes*4
self.bn3 = nn.BatchNorm2d(planes * 4)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
# forward函数表示数据在层之间的传递顺序
residual = x
# 这里residual=x对应的残差块中的shortcut分支,对于大部分的shortcut分支来说,直接输入和输出相连即可(对应的shortcut实线连接),而对于conv3_1 conv4_1对应的shortcut对原特征矩阵进行改变,所以才有了后面的if self.downsample is not None判断
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:
residual = self.downsample(x)
# 这里对应的shortcut中的虚线分支,即将原始的输入特征矩阵进行相应的改变,从而使其满足主分支和shortcut分支的输出特征矩阵的shape相同
out += residual
# 这里表示的是在ReLU激活函数之前的相加过程 相加完之后的结果再进行ReLU激活
out = self.relu(out)
return out
- ResNet18/34网络的构建所需类的讲解
上图表示的是ResNet18/34残差块的部分结构,用于和下面的源码进行对比分析
class BasicBlock(nn.Module):
expansion = 1
# 这里的expansion和Bottleneck类中的expansion不同,是因为对于ResNet18/34而言,每一个残差块中的channel都是恒定的,比如ResNet34中的conv2_x都是64个channel,而对于ResNet50而言,conv2_x中前两个是64 channel,而最后一个卷积层是 256 channel,所以在Bottleneck中的expansion为4
def __init__(self, inplanes, planes, stride=1, downsample=None):
# downsample对应的是ResNet网络结构中的虚线连接 比如说 ResNet34中的conv2_x到conv3_x的过渡,在shortcut分支需要使用虚线连接,从而将 56*56*64 的输入特征矩阵转变为 28*28*128 的输出特征矩阵
super(BasicBlock, self).__init__()
self.conv1 = conv3x3(inplanes, planes, stride)
# 在BasicBlock中都是使用的conv3*3,是因为对于ResNet18/34而言,所有的残差块中的卷积层都是使用的kernel_size为3的卷积核
# inplanes是输入的channel planes是输出的channel stride对应步长 conv3x3函数的padding默认为1 因为这里要求padding要为1 bias这里也要求为false 因为BN不需要bias
# 这里的stride在每一个大块的第一个残差块需要设置stride等于2,因为要进行特征矩阵的变化,比如说conv3_1 conv4_1都需要设置stride=2,但是对除去每一大块中的第一个残差块之外,其余的都应该设置为stride=1
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
# inplace=true 表示对上层传下来的tensor直接修改,这样能够节省运算内存
self.conv2 = conv3x3(planes, planes)
# 这里和上面的conv3x3的不同在于没有了stride参数 因为第一个conv3x3可能位于每一个大的残差块的开头,比如conv3_1 conv4_1,这个时候需要stride等于2来调整输出特征矩阵的宽和高 而在之后的卷积层stride等于1即可,所以后面都是使用的默认的1
self.bn2 = nn.BatchNorm2d(planes)
self.downsample = downsample
# 定义下采样的方法
self.stride = stride
def forward(self, x):
# forward函数表示数据在层之间的传递顺序
residual = x
# 这里residual=x对应的残差块中的shortcut分支,对于大部分的shortcut分支来说,直接输入和输出相连即可(对应的shortcut实线连接),而对于conv3_1 conv4_1对应的shortcut对原特征矩阵进行改变,所以才有了后面的if self.downsample is not None判断
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:
residual = self.downsample(x)
# 这里对应的shortcut中的虚线分支,即将原始的输入特征矩阵进行相应的改变,从而使其满足主分支和shortcut分支的输出特征矩阵的shape相同
out += residual
# 这里表示的是在ReLU激活函数之前的相加过程 相加完之后的结果再进行ReLU激活
out = self.relu(out)
return out