【Backbone】resNet

resnet

Deep Residual Learning for Image Recognition (2015-12)

https://arxiv.org/abs/1512.03385

ResNet 最重要的贡献就是short cut,通过增加恒等映射,解决模型退化的问题。
为什么可以解决模型退化问题呢?
首先什么是模型退化问题,也就是加深简单的模型,不能直接提升模型的性能。
我们在训练模型的时候,使用BP算法,进行反向传播,反向传播的导数经层层传递,虽然过程中可以增加BN层避免梯度消失(更何况ResNet出现的时候,BN仍然不普及),导数在模型浅层本来就很少,同时计算机的计算精度float32/float32有限,更新浅层参数的梯度不够精确,导致模型无法有效训练。

为什么加上了shortcut,模型就不退化呢?
主要观点总结如下:

  1. 避免梯度弥散
    梯度弥散,和梯度爆炸(vanishing/exploding gradients)在原文中就提到,可以使用规范初始化(normalized initialization)及 中间规范层(intermediate normalization layers)解决这个问题。
    但是这样模型肯定可以训,但是训的好不好就不一定了,因为中间规范层其实就是一个拉分布的过程,每一次模型的输入不一样,它的分布当然就不一样,每一次训练把tensor没有理由的拉到同一分布,势必损失了信息,而之后的BN则是优化了这一过程。
    所以resnet的出发点,仍然是去解决梯度弥散和爆炸的问题。加上了shortcut 其实就解决了梯度弥散的问题。

前 向 传 播 z = x + f ( x ) a = r e l u ( x ) 反 向 传 播 ∂ a ∂ x = α a α z ⋅ α z α x = 1 + α z α f ⋅ α f α x 前向传播 \\ z=x+f(x) \\ a=relu(x)\\ 反向传播\\ \frac{\partial a}{\partial x}=\frac{\alpha a}{\alpha z} \cdot \frac{\alpha z}{\alpha x}\\ =1+ \frac{\alpha z}{\alpha f} \cdot \frac{\alpha f}{\alpha x} z=x+f(x)a=relu(x)xa=αzαaαxαz=1+αfαzαxαf

这个1 是一直存在的,避免了梯度弥散的问题。

  1. 特征冗余
    认为在正向卷积时,通过感受野与张量的相互制约,逐渐提取高级语义特征。
    每一次卷积,就是一次提取语义特征的过程。可以想象语义特征的提取需要依赖与各个层级的特征。低级语义特征是检测边缘,中级语义特征检测形状,及背景,我们在做分类问题,这些都是需要考虑的。而传统卷积网络高级语义特征生成层只接受前一层的信息,信息必然丢失严重,就像是传话游戏,中间有失误的话,误差就会不断变大。增加了shortcut之后,深层网络至少接受了 1 2 n \frac{1}{2n} 2n1的浅层网络信息,一定程度上保留了更多的原始信息。这个 n n n代表前 n n n层的信息,假设信息是1+1这样的形式。
    后面的densenet发展了这个优势。

  2. ensambling
    应该在下面的论文会说。

  3. luck node
    luck node 源于ICLR 2019 best paper 《THE LOTTERY TICKET HYPOTHESIS: FINDING SPARSE, TRAINABLE NEURAL NETWORKS》。

    https://arxiv.org/abs/1803.03635

    这篇论文最大的贡献就是提出了新的思路去理解神经网络。
    简单概述就是一句话,神经网络不仅仅是在学习参数,更重要的是在学习一种结构。
    这篇论文通过剪枝实验证明了神经网络中存在子网络,剪枝出的子网络重新初始化,训练更快,预测更准,泛化性更好。
    整个神经网络中按照不同的训练集训练存在某一些结点,成为 luck node 对整个网络的新能贡献最大, luck node仅占整个网络的1/10。
    作者认为训练神经网络就像买彩票,买越多张彩票,中奖的几率就越高。所以往往大的网络效果更好,实际上是大网络中的子网络更优。
    那么用luck node 去解释resnet,加上了shortcut结构,增加了前后层之间的联系,更加容易寻找到luck node,同时由于shortcut 的存在,最优子网络的结构更加丰富,对模型性能的提升就更加明显。

resnet的实现上有两个重要的点:

  1. 残差块必须包含shortcut结构以及至少两层网络,例如两个卷积层以及一个激活层。
    少于两层,简单推导就可以证明残擦块退化为一层网络。
    满足这个条件后,保证shortcut,卷积的变化,激活函数的变化,至少不会降低模型的性能,能不能提高就看实验结果了。如后面的论文。
  2. tensor的size 与 channel 成反比。
在这里插入图片描述

简单解释一下 pytorch 的实现。
BasicBlock
18 和 34 层的resnet使用BasicBlock作为残差块。
每一个残差块包含两个卷积层。

class BasicBlock(nn.Module):
        # 可以看到上面的表格resnet是按照四个层顺序组成的。
        # 层与层之间有若干个残差块,
        # 在层内传输的时候,输入输出一致,stride为1, downsample = None;
        # 在层与层之间,通道数增加4倍,张量高宽缩小1倍,面积缩小4倍。这个时候,如果需要shortcut就需要对输入的张量下采样一次,保证通道数和张量高宽一致。
        # downsample 使用1x1 卷积实现。
    expansion = 1 #表示使用bottle结构,expansion表示输出扩张的倍数。

    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

Bottleneck
50 ,101,152 层的resnet使用Bottleneck作为残差块。

class Bottleneck(nn.Module):
        # 和BasicBlock一样,resnet是按照四个层顺序组成的,层与层之间有若干个残差块。
        # 在层内传输的时候,输出的chanel是输入的4倍,所以每一个残擦块的第一层1x1卷积都要改变通道数。
        # 在层与层之间,通道数增加4倍,张量高宽缩小1倍,面积缩小4倍。这个时候,如果需要shortcut就需要对输入的张量下采样一次,保证通道数和张量高宽一致。
        # downsample 使用1x1 卷积实现。
    expansion = 4  #表示使用bottle结构,expansion表示输出扩张的倍数。

    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

resnet
把不重要的判断,以及初始化删掉了。

class ResNet(nn.Module):
    
    def __init__(self, block, layers, num_classes=1000):
        super(ResNet, self).__init__()
        norm_layer = nn.BatchNorm2
        self.inplanes = 64
        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)
        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.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512 * block.expansion, num_classes)
        
    def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
    # 重要的就是这个_make_layer函数
    # 主要就是去按照表格去生成每一层的残差块。
    # 每一层的第一个残擦块单独处理,主要不同就是增加了downsample。
    # self.inplanes = planes * block.expansion 按照残差块的不同expansion决定了残擦块的输入channel的变化。
        norm_layer = self._norm_layer
        downsample = None
        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, groups=1,
                            base_width = 64, dilation = False, norm_layer = norm_layer))
        self.inplanes = planes * block.expansion
        for _ in range(1, blocks):
            layers.append(block(self.inplanes, planes, groups=self.groups,
                                base_width=64, dilation=False,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 = torch.flatten(x, 1)
        x = self.fc(x)

        return x

在这里插入图片描述

此图去掉最后的FC层.

完整的pytorch实现源码

参考
【resnet 最好讲解】https://blog.csdn.net/chenyuping333/article/details/82344334
【Resnet overview】https://towardsdatascience.com/an-overview-of-resnet-and-its-variants-5281e2f56035
【Resnet 理解】https://blog.csdn.net/lanran2/article/details/79057994
【Resnet 吴恩达课程】https://blog.csdn.net/qq_29893385/article/details/81207203
【Resnet 碎碎念】https://blog.csdn.net/u014296502/article/details/80438616
【Resnet emsable解释】https://blog.csdn.net/Buyi_Shizi/article/details/53336192
【Resnet 个人理解】https://blog.csdn.net/nini_coded/article/details/79582902
【Resnet 的解释和有趣的点】https://blog.csdn.net/qq_21190081/article/details/75933329
【luck node】https://zhuanlan.zhihu.com/p/65161889
【模型压缩】https://accepteddoge.com/cnblogs/mldl/network-compression
ResNet及其变种的结构梳理、有效性分析与代码解读

resnet18

BasicBlock, [2, 2, 2, 2]

2 ∗ ( 2 + 2 + 2 + 2 ) + 1 + 1 = 18 2*(2+2+2+2)+1+1 = 18 2(2+2+2+2)+1+1=18

BasicBlock 包含 2个 3 × 3 3\times3 3×3 卷积.

共有8个Block, 加上第一次卷积和最后的全连接层,共18层.
在这里插入图片描述

resnet34

BasicBlock, [3, 4, 6, 3]

2 ∗ ( 3 + 4 + 6 + 3 ) + 1 + 1 = 34 2*(3+4+6+3)+1+1 = 34 2(3+4+6+3)+1+1=34

resnet50

 Bottleneck, [3, 4, 6, 3]

3 ∗ ( 3 + 4 + 6 + 3 ) + 1 + 1 = 50 3*(3+4+6+3)+1+1 = 50 3(3+4+6+3)+1+1=50

在这里插入图片描述

Bottleneck 包含3个卷积, 同时, 卷积1,3为 1 × 1 1\times1 1×1 卷积负责对channel变化, 卷积2 为 3 × 3 3\times 3 3×3 卷积负责对处理特征,如果是layer2,3,4的第一个Bottleneck, 还要负责下采样.

Bottleneck对于channel的处理如下表所示

layerBottleneck1其他 Bottleneck
layer1 64 → 64 → 256 64\rightarrow64\rightarrow256 6464256 256 → 64 → 256 256\rightarrow64\rightarrow256 25664256
layer2 256 → 128 → 512 256\rightarrow128\rightarrow512 256128512 512 → 128 → 512 512\rightarrow128\rightarrow512 512128512
layer3 512 → 256 → 1024 512\rightarrow256\rightarrow1024 5122561024 1024 → 512 → 1024 1024\rightarrow512\rightarrow1024 10245121024
layer4 1024 → 512 → 2048 1024\rightarrow512\rightarrow2048 10245122048 2048 → 512 → 2048 2048\rightarrow512\rightarrow2048 20485122048

以layer2为例, Conv2d-40负责对输入特征下采样2倍, 并且增加channel为输入channel数的2倍.

通过使用 1 × 1 1\times1 1×1 卷积对特征降维, 减少了计算参数, 增加了非线性, 进一步降低了模型过拟合的问题.

       Bottleneck-36          [-1, 256, 64, 32]               0
***************************************************************************
           Conv2d-37          [-1, 128, 64, 32]          32,768
           Conv2d-40          [-1, 128, 32, 16]         147,456
           Conv2d-43          [-1, 512, 32, 16]          65,536
       Bottleneck-48          [-1, 512, 32, 16]               0
           Conv2d-49          [-1, 128, 32, 16]          65,536
           Conv2d-52          [-1, 128, 32, 16]         147,456
           Conv2d-55          [-1, 512, 32, 16]          65,536
       Bottleneck-58          [-1, 512, 32, 16]               0
           Conv2d-59          [-1, 128, 32, 16]          65,536
           Conv2d-62          [-1, 128, 32, 16]         147,456
           Conv2d-65          [-1, 512, 32, 16]          65,536
       Bottleneck-68          [-1, 512, 32, 16]               0
           Conv2d-69          [-1, 128, 32, 16]          65,536
           Conv2d-72          [-1, 128, 32, 16]         147,456
           Conv2d-75          [-1, 512, 32, 16]          65,536
       Bottleneck-78          [-1, 512, 32, 16]               0
***************************************************************************

resnet101

Bottleneck, [3, 4, 23, 3]

resnet152

Bottleneck, [3, 8, 36, 3]

我们注意到resnet 50/101/152 在layer1 和 layer4 都堆叠4次, 通过重复堆叠layer2, 和layer 3增加网络层数.

但是为什么这样堆, 为什么不是 3 ,8 ,15 ,3 或者 3, 11, 12, 3这样, 希望知道的同学可以留言.

那为什么前后都为3呢?

直观来讲, 模型在浅层处理颜色, 风格, 形状等浅层特征, 这些特征的处理并不需要特别深的模型, 而随着下采样, 感受野的扩大, 模型逐渐学习语义特征, 需要更多的参数及非线性运算. 在最layer4, channel数最终增加到2048, 需要参与卷积运算也随之增加, 参数量也在增加, 为了控制模型的参数数量, layer4的Bottleneck也应该设计的比较小.

resnetv2

Identity Mappings in Deep Residual Networks (2016-03)

https://arxiv.org/abs/1603.05027

如图所示, 在每一个Block跳跃连接处, 去掉了relu操作, 进一步维护了特征的完整性, 保持了恒定映射.

在这里插入图片描述

参考
【resnet 最好讲解】https://blog.csdn.net/chenyuping333/article/details/82344334
【resNet v2 翻译】https://blog.csdn.net/wspba/article/details/60750007

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值