• 再看ResNet


    在2020年刚接触深度学习的时候,学习了ResNet的架构。但是当时我并没有太关注ResNet,直到后来,真正开始接触CV、NLP、时序包括Graph的科研项目时,我才意识到ResNet对整个深度学习领域的影响之深远。

    ResNet之前,神经网络存在什么问题?

    • 当神经网络的层数很大,深度很深的时候,容易出现梯度消失或梯度爆炸的致命问题,导致模型的训练过程无法进行下去
    • 当神经网络的层数很大,深度很深的时候,模型可能无法学习到真正有效的信息,导致拟合目标的效果越来越差,距离目标越来越远
      在这里插入图片描述
      上图所示,可以看到:左图是之前的深度学习模型(如VGG),随着模型层数的增加,其实模型已经逐渐偏离需要拟合的目标,因此训练效果越来越差,甚至不如很小的层数得到的效果好;右图则是ResNet希望达到的目的,随着模型层数的增加,模型逐渐靠近需要拟合的目标,即使后期拟合效果缓慢,但是并没有出现拟合偏差越来越大的问题。

    ResNet的结构设计
    H ( x ) = F ( x ) + x H(x)=F(x)+x H(x)=F(x)+x

    其中, H ( x ) H(x) H(x)是每一层观测到的输出, F ( x ) F(x) F(x)是神经网络的层, x x x是每一层的输入(称为identity)。这个residual连接成为跳跃连接(skip connection)或短路(shortcut)。

    在这里插入图片描述

    从函数角度解释ResNet的有效性
    可以很直观地解释ResNet的有效性:即使经过的 F ( x ) F(x) F(x)层并没有学到任何东西(甚至是学到了负面影响的东西),模型也能够继承到输入 x x x的信息。第一幅图的右边可以直观解释,每一层的模型都可以保证自己完全包含了上一层模型所学到的信息。因此,随着模型的加深,效果不会偏离目标,至少会一直在之前的基础上,进行学习。

    从残差角度解释ResNet的有效性
    上面从函数角度解释ResNet的有效性非常直观,但是在Kaiming He的论文中,并不是这样解释的。因为 H ( x ) = F ( x ) + x H(x)=F(x)+x H(x)=F(x)+x,故 F ( x ) = H ( x ) − x F(x)=H(x)-x F(x)=H(x)x,这里的 F ( x ) F(x) F(x)是观测输出与该层输入的差值,我们称之为“残差(Residual)”。
    那么,训练目标就从原来的拟合目标,转化为了拟合残差。拟合残差是有益的,即使 F ( x ) F(x) F(x)学习不到有效的东西(甚至是学到了负面影响的东西),也并不会因此逐渐远离目标,也可以说是模型偏差不会越来越大。同时,这种跳跃连接也避免了梯度消失或梯度爆炸的训练问题。
    (这个思想,非常类似于集成学习中的Boosting,如GBDT梯度上升树。两者本质上都是拟合残差,但是也存在不同之处:GBDT是拟合的标签label,而ResNet是拟合的特征图feature)

    ResNet的架构设计
    下图为传统卷积神经网络和ResNet的区别:
    在这里插入图片描述
    根据卷积的方式,主要分为以下两种ResNet:

    1. 接多个高宽不变的的ResNet块(图左)
    2. 接的是高宽减半的ResNet块(stride=2),则通道数增大到2倍,那么就会引入了Conv1x1对通道数进行了变换,以便最终add在一起(图右)
      在这里插入图片描述

    ResNet关键代码
    这里设计一个网络:刚开始是一个conv7x7的卷积,不改变通道数。每个shortcut模块中包含两个conv层,每个resnet块包含两个前面的shortcut模块。除了conv7x7的块之外,第一个resnet块不对通道数进行变换。在之后的resnet块中,只有第一个shortcut模块是需要让通道数翻倍的。在通道数翻倍的模块中,shorcut需要通过一个conv1x1的卷积进行通道数的变换。

    import torch.nn as nn
    from torch.nn import functional as F
    import torch
    
    
    # 定义shortcut模块
    class Residual(nn.Module):
        def __init__(self, input_channels, num_channel, use_conv1x1=False, stride=1):
            super().__init__()
            self.conv1 = nn.Conv2d(
                in_channels=input_channels, out_channels=num_channel, kernel_size=3, stride=stride, padding=1
            )
            self.conv2 = nn.Conv2d(
                in_channels=num_channel, out_channels=num_channel, kernel_size=3, stride=1, padding=1
            )
            if use_conv1x1:
                self.conv3 = nn.Conv2d(
                    in_channels=input_channels, out_channels=num_channel, kernel_size=1, stride=stride
                )
            else:
                self.conv3 = None
            self.bn1 = nn.BatchNorm2d(num_features=num_channel)
            self.bn2 = nn.BatchNorm2d(num_features=num_channel)
            # batch_normalization有自己的参数,所以不能像relu一样只定义一个
            self.relu = nn.ReLU(inplace=True)  # 不需要重新开内存去存变量,更节省内存
    
        def forward(self, x):
            y = self.conv1(x)
            y = self.bn1(y)
            y = self.relu(y)
            y = self.conv2(y)
            y = self.bn2(y)
            if self.conv3:
                x = self.conv3(x)
            y += x
            return F.relu(y)
    
    
    ## residual test
    resblk1 = Residual(3, 3, use_conv1x1=False, stride=1)
    x = torch.rand(4, 3, 6, 6)
    y = resblk1(x)
    print(y.shape)
    
    # 通常feature map长宽减半,通道数翻倍
    resblk2 = Residual(3, 6, use_conv1x1=True, stride=2)
    x = torch.rand(4, 3, 6, 6)
    y = resblk2(x)
    print(y.shape)
    ## residual test
    
    
    # 定义resnet网络块
    def resnet_block(input_channels, num_channels, num_residuals, first_block=False):
        blks = []
        for i in range(num_residuals):
       		# 是renet块中的第一个(要改变通道数),同时它不是第一个块
            if i == 0 and not first_block:
                blks.append(
                    Residual(input_channels=input_channels, num_channel=num_channels, use_conv1x1=True, stride=2)
                )
            else:
                blks.append(Residual(input_channels=num_channels, num_channel=num_channels, use_conv1x1=False, stride=1))
        return blks
    
    
    b1 = nn.Sequential(
        nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=1),
        nn.BatchNorm2d(64),
        nn.ReLU(),
        nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
    )
    b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
    b3 = nn.Sequential(*resnet_block(64, 128, 2, first_block=False))
    b4 = nn.Sequential(*resnet_block(128, 256, 2, first_block=False))
    b5 = nn.Sequential(*resnet_block(256, 512, 2, first_block=False))
    # 这里的*是指把list展开
    
    net = nn.Sequential(
        b1, b2, b3, b4, b5,
        nn.AdaptiveAvgPool2d((1, 1)),
        nn.Flatten(),
        nn.Linear(512, 10)
    )
    
    x = torch.rand((1, 1, 224, 224))
    for i, layer in enumerate(net):
        x = layer(x)
        print('layer:', i, layer.__class__.__name__, 'output shape:', x.shape)
    
    • 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
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    torch.Size([4, 3, 6, 6])
    torch.Size([4, 6, 3, 3])
    layer: 0 Sequential output shape: torch.Size([1, 64, 55, 55])
    layer: 1 Sequential output shape: torch.Size([1, 64, 55, 55])
    layer: 2 Sequential output shape: torch.Size([1, 128, 28, 28])
    layer: 3 Sequential output shape: torch.Size([1, 256, 14, 14])
    layer: 4 Sequential output shape: torch.Size([1, 512, 7, 7])
    layer: 5 AdaptiveAvgPool2d output shape: torch.Size([1, 512, 1, 1])
    layer: 6 Flatten output shape: torch.Size([1, 512])
    layer: 7 Linear output shape: torch.Size([1, 10])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    除了代码中的注释外,编程时还需注意:

    • F.relu()是函数调用,一般用在forward函数最后的输出中;nn.ReLU()是模块调用,一般在定义网络时使用。
    • cos学习率相比于固定学习率好
    • 测试集的准确率会不会比训练集高?其实有可能,如果训练集里做了大量的data augmentation,那么测试集可能准确率更高,训练集中含有噪声
  • 相关阅读:
    # 算法与程序的灵魂
    命名空间namespace
    【C/C++动态内存 or 柔性数组】——对动态内存分配以及柔性数组的概念进行详细解读(张三 or 李四)
    网站优化要搞什么工作?网站关键词优化的技巧
    深入理解java虚拟机:虚拟机类加载机制(1)
    入户的第一眼,玄关设计小技巧!福州中宅装饰,福州装修
    免费、安全、可靠!一站式构建平台 ABS 介绍及实例演示 | 龙蜥技术
    POJ - 3278 Catch That Cow
    WPF界面设计
    liunx系统中毒 如何应急
  • 原文地址:https://blog.csdn.net/qq_16763983/article/details/126127458