ResNet学习笔记(一)

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

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

** 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))

现在,来看代码,我们对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代码阅读的一些心得和参考的博文。刚刚开始学习深度学习,这笔记算是对学习的证明吧!接下来将使用迁移学习的方法对宝可梦数据集进行训练。

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值