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为例:

import torchvision
# 需要预训练模型的参数来初始化
model = torchvision.models.resnet50(pretrained=True)

# 不需要预训练模型的参数来初始化
model = torchvision.models.resnet50(pretrained=False)
# pretrained参数默认是False,等价于
model = torchvision.models.resnet50()

1.2 图解ResNet的多种结构

torch调用resnet50 torch resnet_ide

  • 彩图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

torch调用resnet50 torch resnet_卷积_02

该模块里定义了 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处理后的图像的维度是如何变化的:卷积的计算公式torch调用resnet50 torch resnet_2d_03,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 倍。

torch调用resnet50 torch resnet_卷积_04

  • 与基础版本的BasicBlock 不同的是这里有 3 个卷积,分别为1×1、3×3、1×1大小的卷积核,分别用于压缩维度、卷积处理、恢复维度。
  • inplanes 是输入通道数,planes = 输出通道数/expansion,在基础版本 BasicBlock 中 expansion =1,表示block内部最后一个卷积的输出channel与第一个卷积的输出channel比值(torch调用resnet50 torch resnet_计算机视觉_05),此时相当于没有倍乘,输出的通道数就等于 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模型的结构(重要)

torch调用resnet50 torch resnet_卷积_06

  • 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等等中的哪一个。
  • torch调用resnet50 torch resnet_ide_07

  • 输出尺寸的分析:在output size那里,维度是依次下降两倍的,224-112-56-28-14-7,但是我们会不会有疑问,经过那么多个卷积核为什么才下降两倍呢?这个实现是与ResNet类中的_make_layer()方法密切相关。看到_make_layer()源码,有没有怀疑过为什么都是建立block,但是要分开两个步骤建呢,是因为上面那个是设置了步长的,步长为2,下面那个是用默认步长的,步长为1,当步长为2时,结合卷积核分析,会减低2倍特征图尺寸;步长为1时,则不变。也就是说除了第一个layer不会缩小图片外,其余三个layer都会缩小一半图片(因为第一个layer的步长始终都为1)
  • torch调用resnet50 torch resnet_2d_08


  • torch调用resnet50 torch resnet_计算机视觉_09

  • 恒等映射分两种:高宽不变,维度变;高宽变为二分之一,维度变
  • torch调用resnet50 torch resnet_2d_10

  • 图像输入大小问题:在旧版的torchvision中,其预训练权重的默认图片大小为224×224,若图片大小经模型后缩小后和最后一层全连接层不匹配,则会抛出异常,比如输入大小256×256。新版已经兼容了输入图片的大小,方法就是使用AdaptiveAvgPool2d
  • 部分网络结构图:
  • torch调用resnet50 torch resnet_torch调用resnet50_11

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等等中的哪一个。

torch调用resnet50 torch resnet_卷积_12


其实所谓的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。综合来说

torch调用resnet50 torch resnet_卷积_13



4、ResNet50的分析

4.1 解决的问题

由于梯度消失,深层网络很难训练。因为梯度反向传播到前面的层,重复相乘可能使梯度无穷小。结果就是,随着网络的层数更深,其性能趋于饱和,甚至迅速下降。

4.2 核心思想

引入一个恒等映射(也称之为跳跃连接线),直接跳过一个或者多个层,尺寸一样时可以相加,尺寸,当尺寸不一样时,需要进行downsample操作,使得维度一致。

torch调用resnet50 torch resnet_2d_14

  • 在网络上堆叠这样的结构,就算梯度消失,我什么也学不到,我至少把原来的样子恒等映射了过去,相当于在浅层网络上堆叠了“复制层”,这样至少不会比浅层网络差。
  • 万一我不小心学到了什么,那就赚大了,由于我经常恒等映射,所以我学习到东西的概率很大。
  • 恒等映射分两种:高宽不变,维度变;高宽变为二分之一,维度变

4.3 数学推导

关于为什么残差结构(即多了一条跳跃连接线后)为什么一定程度缓解了梯度消散的数学推导:

torch调用resnet50 torch resnet_ide_15

4.4 50层的由来

torch调用resnet50 torch resnet_计算机视觉_16