ResNet 论文精读&&代码逐行解析

文章目录

  • 前言
  • 一、论文精读
    • 0.摘要标题和结论
    • 1.Introduction:
      • 为什么要用Residual?
    • 2.Related Work:
    • 3.Deep Residual Learning:
    • 4.Experiments:
  • 二、代码阅读
    • 1.ResNet网络框架实现
    • 2.18层或34层残差网络的残差模块
    • 3. 50层,101层,152层的残差网络的残差模块
    • 4.代码心得


ResNet 论文精读&&代码逐行解析_第1张图片

前言

笔者从人工智能小白的角度,力求能够从原文中解析出最高效率的知识。
之前看了很多博客去学习AI,但发现虽然有时候会感觉很省时间,但到了复现的时候就会傻眼,因为太多实现的细节没有提及。而且博客具有很强的主观性,因此我建议还是搭配原文来看。

请下载原文《Deep Residual Learning for Image Recognition》搭配阅读本文,会更高效哦!

一、论文精读

0.摘要标题和结论

《Deep Residual Learning for Image Recognition》通过对标题,摘要,结论的阅读,我得到以下信息:

  1. 使用残差学习框架使训练深层网络变得更加容易。用了152层网络,比VGG深8倍,但计算复杂度更低(好奇为什么?)
  2. 将常用模型的CNN主干网络换成了残差网络,且在许多数据集上有不错的效果,例如目标检测数据集COCO。
  3. 未加残差时,34层网络比18层网络的误差上更高。加上残差后,误差更低。

1.Introduction:

(1)Why Deep?因为不同的层可以得到一些不同的特征,例如低级的视觉特征和高级的语义特征。
(2)深层网络的问题?存在梯度爆炸和梯度消失的情况。如何解决?
① 权重初始化时适当。
② 在中间加入BN(Batch Normalization),来校验每个层的输出,和梯度的均值和方差。避免层之间大小相差太大。
(3)收敛后精度降低。原因并非模型复杂及层数增加带来的过拟合,因为训练误差也变高了。理论上说更深网络,就可以训练成简单网络,然后加上identity mapping,但是SGD找不到。于是本文提出了残差网络模型。
(4)残差模型:某层输出H(x),新添加的层不去学H(x),而是去学H(x)-x(就是网络已学的知识和真实世界的差距)。最后输出相当于新学层F(x)加上之前的旧网络输出的x。

结果:越深精度越高。

为什么要用Residual?

思考后我认为:
① 不会增加学习参数,不会增加模型复杂度,不会增加计算(只不过是个加法)。
② 这可以使上一个残差块的信息没有阻碍的流入到下一个残差块,提高了信息流通,并且也避免了由与网络过深所引起的消失梯度问题和退化问题。
③ 从数学上理解ResNet,相当于在原有的梯度上做了加法,防止梯度消失,让SGD可以一直跑起来。
④ 加了残差,模型复杂度也许会降低,可能不那么容易overfitting

2.Related Work:

(1)Residual Representations:之前工作当有残差时,比没有残差的标准解法计算要快。
(2)Short connection:highway networks

3.Deep Residual Learning:

(1)残差连接处理输入和输出形状不同的方法:
① 输入和输出分别添加额外的0,使得两个形状所对应。
② 投影。通过11的卷积层,也就是在空间上不做任何东西(让我联想到了Network Compression里面的Depthwise Separable Convolution中的Pointwise Convolution,只考虑通道间关系,不考虑通道内部关系),主要是在通道维度上做改变,使得输出通道是输出通道的两倍,输入的高和宽被减半,所以步幅为2,使得高宽匹配。(存疑?)
③ 就算输入输出形状一样,在连接时,做1
1卷积。
(2)Implementation具体实现:
① 数据方面,调了调RGB,然后取的窗口[256,480]比较大,随机性强。测试集也随机sample了10个图片,降低了方差,而且还采用了不同的分辨率,提高了精度。
② 参数初始化,具体初始化参数设置,然后lr也是错误率比较平的时候就除以10,和AlexNet所用方法一致。(现在目前好像没怎么用,这得守着模型的训练才行。)没有用dropout因为没有全连接层。

4.Experiments:

(1)网络架构
FLOPs:浮点数运算=输入的高通道数输出通道数和的高*和的宽
疑问:为什么50层和34层FLOPs差不多?

(2)有无残差误差收敛比较
有残差收敛会快,而且后期更好。

(3)对比输出输出不同形状处理的三种方法:
C方法效果最好,但是计算量大,开销大。B方法计算量增加不多,效果也比较好。

(4)如何做到更深呢?Bottleneck

降维,投影映射,相当于特征空间降维,然后最后一层再映射回去,这样计算量和不降维差不多,又能增加这个深度和通道数,以抓取更多特征。

二、代码阅读

1.ResNet网络框架实现

class ResNet(nn.Module):       
 
    # 参数:block         如果定义的是18层或34层的框架 就是BasicBlock, 如果定义的是50,101,152层的框架,就是Bottleneck
    #       blocks_num   残差层的个数,对应34层的残差网络就是 [3,4,6,3]
    #       include_top  方便以后在resnet的基础上搭建更复杂的网络
 
    def __init__(self, block, blocks_num, num_classes=1000, include_top=True):
        super(ResNet, self).__init__()
        self.include_top = include_top
        self.in_channel = 64     # 上一层的输出channel数,及这一层的输入channel数
 
        #   part 1 卷积+池化  conv1+pooling
        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)   #利用in-place计算可以节省内(显)存,同时还可以省去反复申请和释放内存的时间。但是会对原变量覆盖,只要不带来错误就用。计算结果不会有影响
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
 
        #   part 2  残差网络的四部分残差块:conv2,3,4,5
        self.layer1 = self._make_layer(block, 64, blocks_num[0])  # 5中不同深度的残差网络的第一部分残差块个数:2,3,3,3,3
        self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)# 5中不同深度的残差网络的第一部分残差块个数:2,4,4,4,8
        self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)# 5中不同深度的残差网络的第一部分残差块个数:2,6,6,23,36
        self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)# 5中不同深度的残差网络的第一部分残差块个数:2,3,3,3,3
 
        #   part 3  平均池化层+全连接层
        if self.include_top:
            self.avgpool = nn.AdaptiveAvgPool2d((1, 1))  # output size = (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))
        self.in_channel = channel * block.expansion
 
        for _ in range(1, block_num):
            layers.append(block(self.in_channel, channel)) # stride=1,downsample=None
 
        return nn.Sequential(*layers)   # 将list转换为非关键字参数传入
 
    def forward(self, x):
 
        # part 1
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)
 
        # part 2
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
 
        # part 3
        if self.include_top:
            x = self.avgpool(x)
            x = torch.flatten(x, 1)
            x = self.fc(x)
 
        return x

2.18层或34层残差网络的残差模块

class BasicBlock(nn.Module):              
    expansion = 1                # 记录各个层的卷积核个数是否有变化
 
    def __init__(self, in_channel, out_channel, stride=1, downsample=None):
        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)  # 有无bias对bn没多大影响
        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

3. 50层,101层,152层的残差网络的残差模块

class Bottleneck(nn.Module):        
    expansion = 4        #   第三层卷积核的个数(256,512,1024,2048)是第一层或第二层的卷积核个数(64,128,256,512)的4倍
 
    def __init__(self, in_channel, out_channel, stride=1, downsample=None):
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
                               kernel_size=1, stride=1, bias=False)  # squeeze channels 降维
        self.bn1 = nn.BatchNorm2d(out_channel)
        # self.relu = nn.ReLU(inplace=True)
 
        self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
                               kernel_size=3, stride=stride, bias=False, padding=1)
        self.bn2 = nn.BatchNorm2d(out_channel)
        # self.relu = nn.ReLU(inplace=True)
 
        self.conv3 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel*self.expansion,
                               kernel_size=1, stride=1, bias=False)  # unsqueeze channels 升维
        self.bn3 = nn.BatchNorm2d(out_channel*self.expansion)
        self.relu = nn.ReLU(inplace=True)
 
        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 = self.relu(out)
 
        out = self.conv3(out)
        out = self.bn3(out)
 
        out += identity
        out = self.relu(out)
 
        return out

4.代码心得

主要关注在降维和升维的实现(如2,3,4部分中的第一个残差块的下采样操作)。残差网络块的实现(part 2)。18和34层的残差块是相似的,50/101/152层的残差块是一样的,这两种残差块进行了分开定义。

你可能感兴趣的:(ResNet 论文精读&&代码逐行解析)