当前位置:   article > 正文

ResNet学习笔记(一)_resnet的shortcut

resnet的shortcut

ResNet

由于随着卷积层数的增加,会导致梯度消失/爆炸的问题,虽然这两种方法可以通过归一化等方法解决,但是还存在退化问题,所以提出了ResNet。
(关于梯度消失和爆炸,退化可参照 链接: link.)

模型的搭建

对于整个模型的搭建主要是Block类,接下来以BasicBlock为例来对代码进行解析。(代码来源:太阳花的小绿豆的CSDN 链接: link.))
** Block**的含义:
例如:ResNet34由以下几部分组成,conv1,conv2_x,conv3_x,conv4_x,conv5_x,以及最后的全连接层, BasicBlock指的是 conv2_x,conv3_x,conv4_x,conv5_x里的这些具有重复结构的块,如conv3_x层中具有四个([33,128]+[33,128])块, BasicBlock就指的是这些块,在ResNet中,通过重复调用BasicBlock方法就能方便的构建出这些块。简单来说,Block是为了方便调用代码,就是为了省力。
我们可以了解到ResNet中对于50层以下的构建块采用的是BasicBlock,而大于50的深层则采用的是Bottleneck

class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, in_channel, out_channel, stride = 1, downsample = None, **kwargs):
        super(BasicBlock, self).__init__()

        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
                               kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channel)
        self.relu = nn.ReLU()

        self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
                               kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channel)
        self.downsample = downsample

    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        out += identity
        out = self.relu(out)

        return out
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

Resnet网络结构
50层以下的残差模块

**步骤1:**令BasicBlock继承nn.Module(对于这一模块,可查看Pytorch文档中的nn.Module或 参考
LoveMIss-Y的CSDN链接link.,简单来说,就是利用库中定义的一个模型来改写)
**步骤2:**重写__init__和forward
(1)一般把网络中具有可学习参数的层(如全连接层、卷积层等)放在构造函数__init__()中,当然我也可以吧不具有参数的层也放在里面。
(2)一般把不具有可学习参数的层(如ReLU、dropout、BatchNormanation层)可放在构造函数中,也可不放在构造函数中,如果不放在构造函数__init__里面, 则在forward方法里面可以使用nn.functional来代替。
(3)forward方法是必须要重写的,它是实现模型的功能,实现各个层之间的连接关系的核心。

对__init__的理解,我们可以发现在18层和34层的网络中,卷积核大小均为3*3,且特征图数量没有增加,因此在BasicBlock中我们的conv的kernel_size=3,stride默认取1,这里需要注意BN层必须是在conv层和激活函数层中间,且conv中的bias(偏置参数)不需要设置,因为经过BN层后bias会被消掉。

关于downsample的理解

(参照马佳的男人的CSDN博文 链接link.)
首先我们要明确,在resnet中的downsample有两种:
1、真正意义上让output.shape长宽变成1/2的我暂且称之为real_downsample
2、shortcut(是指经过结构中捷径的过程)前的x的为了适应shortcut后变化的shape而做的自适应调节,暂且称之为identity_downsample
接下来看ResNet这个类:

class ResNet(nn.Module):
    def __init__(self,
                 block,
                 block_num,
                 num_classes=1000,
                 include_top=True,
                 groups=1,
                 width_per_group=64):
        super(ResNet, self).__init__()
        self.include_top = include_top
        self.in_channel = 64

        self.groups = groups
        self.width_per_group = width_per_group

        self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
                               padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(self.in_channel)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64, block_num[0])
        self.layer2 = self._make_layer(block, 128, block_num[1], stride=2)
        self.layer3 = self._make_layer(block, 256, block_num[2], stride=2)
        self.layer4 = self._make_layer(block, 512, block_num[3], stride=2)
        if self.include_top:
            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')  #权重初始化(正态分布)

    def _make_layer(self, block, channel, block_num, stride=1):
        downsample = None
        if stride != 1 or self.in_channel != channel * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(channel * block.expansion))

        layers = []
        layers.append(block(
            self.in_channel,
            channel,
            downsample=downsample,
            stride=stride,
            groups=self.groups,
            width_per_group=self.width_per_group))
        self.in_channel = channel * block.expansion

        for _ in range(1, block_num):
            layers.append(block(
                self.in_channel,
                channel,
                groups=self.groups,
                width_per_group=self.width_per_group))

        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)

        if self.include_top:
            x = self.avgpool(x)
            x = torch.flatten(x, 1)
            x = self.fc(x)

        return x

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76

** real_downsample:**
#这里的layer是指包含多个block的一个层,层>块
(1) 第一个大layer不做real_downsample,所以此时stride=1
(2)剩下的所有大layer都只在第一个block里的第一个3x3用stride=2做real_downsample 那么如何对所有的第一个block进行调整?
当然就是对于每个layer,先把第一个block拉出来用我们传进去的stride进行领导的特殊对待啦(stride默认为1)传进去stride = 1,那么第一个block就不用downsample;如果传进去stride = 2,那第一个block就要downsample。至于剩下的block, 就不用看领导脸色了,传进去stride = 1还是2都跟它们无关,因为它们反正都不downsample,所以可以看到在_make_layer的循环里没有指定接下来的stride。
** identity_downsample:**(identity:其实意思就是这个网络层的设计是用于占位的,即不干活,只是有这么一个层,放到残差网络里就是在跳过连接的地方用这个层,显得没有那么空虚!)
由于在残差结构里,参数通过主线和捷径后又一个相加操作,所以需要保证两者维度一致,所以需要做accommodation(适应)
什么时候需要做identity_downsample?
简单来说,做了real_downsample就要做identity_downsample

        if stride != 1 or self.in_channel != channel * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(channel * block.expansion))
  • 1
  • 2
  • 3
  • 4

现在,来看代码,我们对stride是否等于1进行了判断,因为我们可以知道除了第一个大layer没做real_downsample外,其余所有大layer的第一个block都做了real_downsample,所以后续对传入参数进行变换。

downsample总结:
real downsample是Resnet主动为了downsample而downsample的,而identity_downsample只是我们为了能让out += identity 的被动的适应性调整

对于参数expansion的理解:

通过对比BasicBlock和Bottleneck可以发现在前者中expansion=1,后者等于4。
所以这个参数的用途究竟是什么?
我们经过两个block的对比可以发现,实际上这个参数是用于区分这两个block,已经知道了对于50层以下的网络使用BasicBlock,而以上使用Bottleneck。两者的区别可以通过表格看出,在block中50层以下网络,卷积后没有改变深度(特征图的数目),而对于50层以上的网 络,最后一次卷积将深度扩大了4倍,所以这个参数就是这么来的。

小结

这是我在学习过程中,对于ResNet代码阅读的一些心得和参考的博文。刚刚开始学习深度学习,这笔记算是对学习的证明吧!接下来将使用迁移学习的方法对宝可梦数据集进行训练。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/73107
推荐阅读
相关标签
  

闽ICP备14008679号