回头再看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)
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])

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

  • F.relu()是函数调用,一般用在forward函数最后的输出中;nn.ReLU()是模块调用,一般在定义网络时使用。
  • cos学习率相比于固定学习率好
  • 测试集的准确率会不会比训练集高?其实有可能,如果训练集里做了大量的data augmentation,那么测试集可能准确率更高,训练集中含有噪声
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr.zwX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值