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网络的构建所需类的讲解

resnet优点和缺点 resnet网络的优点_深度学习

  上图表示的是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网络的构建所需类的讲解

resnet优点和缺点 resnet网络的优点_2d_02

  上图表示的是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