当前位置:   article > 正文

经典神经网络——ResNet模型论文详解及代码复现_restnet论文

restnet论文

论文地址:Deep Residual Learning for Image Recognition (thecvf.com) 

PyTorch官方代码实现:vision/torchvision/models/resnet.py at main · pytorch/vision (github.com)

B站讲解: 【精读AI论文】ResNet深度残差网络_哔哩哔哩_bilibili

一、背景

ResNet是何凯明等人在2015年提出的模型,获得了CVPR最佳论文奖,在ILSVRC和COCO上的比赛成绩:(以下比赛项目都是第一) 

  1. ImageNet Classification
  2. ImageNet Detection
  3. ImageNet Localization
  4. COCO Detection
  5. COCO Segmentation

Resnet,被誉为撑起计算机视觉半边天的文章,重要性不言而喻,另外,文章作者何凯明,在2022年AI 2000人工智能最具影响力学者排行里排名第一: 

深度学习的发展从LeNet到AlexNet,再到VGGNet和GoogLeNet,网络的深度在不断加深,经验表明,网络深度有着至关重要的影响,层数深的网络可以提取出图片的低层、中层和高层特征。 通常来说,在同等条件下,网络越深,性能越好(暂且这样认为)。但当网络足够深时,仅仅在后面继续堆叠更多层会带来很多问题:

第一个问题就是梯度爆炸 / 消失(vanishing / exploding gradients),这可以通过BN和更好的网络初始化解决;

第二个问题就是退化(degradation)问题,即当网络层数多得饱和了,加更多层进去会导致优化困难、且训练误差和预测误差更大了,注意这里误差更大并不是由过拟合导致的(后面实验细节部分会解释)。resnet的出现,解决了这个问题,模型可以轻易堆叠到几十层上百层(一千层的都有)。

那么,接下来就来看看这个网络是如何解决问题的吧。

二、论文解读

1、ResNet网络是什么

ResNet(Residual Network)是一种深度神经网络模型,也被称为残差网络。它通过引入残差块(Residual Building Block)来解决深层神经网络训练过程中的梯度消失问题。

在ResNet中,网络的输出由两部分组成:

恒等映射(identity mapping)和残差映射(residual mapping)。恒等映射指的是将输入直接传递到下一层,而残差映射则是对输入进行一些非线性变换后再进行传递。这种设计使得网络能够更好地学习残差信息,从而让网络变得更加深层。

ResNet的关键创新点:

在于引入了shortcut connections,即跳过一层或多层的连接。这些连接使得信息能够更加顺畅地传递,避免梯度在传播过程中消失。通过这种方式,ResNet可以训练非常深的网络,而不会出现性能下降的问题。

它添加了一个短路连接到第二层激活函数之前。那么激活函数的输入就由原来的输出H(x)=F(x)变为了H(x)=F(x)+x。在RestNet中,这种输出=输入的操作成为恒等映射。那么,上图中的identity其实功能也是恒等映射。

 那么这么做的好处是什么呢?

在深度神经网络中,随着训练过程中反向传播权重参数的更新,网络中某些卷积层已经达到最优解了,其实此时这些层的输入输出都是一样的,已经没有训练的必要。但实际训练过程中,我们是很难将权重参数训练为绝对0误差的,但是这种情况已经是最优解了,其实对这些层的训练过程是可以抛弃的,即此时可以设F(x)=0,那么这时的输出为H(x)=x就是最优输出。

        在传统平原网络中,即未加入identity之前,如果网络训练已经达到最优解了,那么随着网络继续训练、权重参数的更新,有可能将已经达到最优解的权重参数继续更新为误差更多的值。但随着identity的加入,在达到最优解的时候直接通过F(x)=x,那么权重参数可以达到至少不会比之前训练效果差的目的,并且可以加快网络收敛。

2、ResNets为什么能构建如此深的网络?

深度学习对于网络深度遇到的主要问题是梯度消失和梯度爆炸,传统对应的解决方案则是数据的初始化(normlized initializatiton)和(batch normlization)正则化,但是这样虽然解决了梯度的问题,深度加深了,却带来了另外的问题,就是网络性能的退化问题,深度加深了,错误率却上升了,而残差用来设计解决退化问题,其同时也解决了梯度问题,更使得网络的性能也提升了。

 

3、为什么能够解决梯度消失的问题?

首先看核心公式:

        ①式为恒等映射函数h(·)和残差函数F(·)之和,即:对第l层的输入x计算残差,再和x的恒等映射加起来,记作y;

        ②式表示对y进行激活,得到第l层的输出(也就是第l+1层的输入);③式是对①和②的整理,可以用一个式子统一起来书写。

        上面的公式是针对相邻两层的情况,那么对于任意深的单元L和任意浅的单元l有:

        该式表示第L层的输入等于第l层的输出加上第l层到第L-1层的残差和,那么在优化的时候,只需要拟合残差项(后面的∑(·)),使之尽可能为0,就能实现第L层和第l层恒等,从而做到信息不丢失。为什么需要拟合残差呢?我个人的理解是:第L层和第l层之间的每一个部分,都会对当前造成影响,有些地方是不好的残差,那么优化方向可能会被带偏,起到反作用。

        假如损失函数为ε,在反向传播时,对上面的式子求偏导,得到:

  可以看到,左边蓝框框里的项没有权重信息,意味着反向传播的时候,信息能够从第L层直接传递到第l层,而无需经过权重,这就保证了信息的完整性。右边蓝框框里是1+▲是防止梯度消失的关键,因为目前大多用的是批量梯度下降算法,每次是把一个批量(batch)的样本送进去计算,那么不可能所有批量计算偏导的结果都为-1,从而1+▲在大部分情况下不会为0,因此即便某个批量计算的权重很小,都不会发生梯度消失。

4、为什么能够加快收敛速度?

        实际上还是可以从上面的偏导公式来解释,在计算batch的梯度时,大多数情况可以获得一个较大的梯度值(因为有一个1在那里),从而可以大步向前走,更快地找到最优值。

        另外,在整个模型中,浅层网络提取到的是低级特征,深层网络提取到的是复杂特征,如果没有恒等映射连接,那么最后是利用复杂特征进行拟合,从而比较费时,加入恒等映射,相当于保留了一部分低级特征用来判断。

        理论方面讲完了,现在看看网络架构是什么样的:

以50-layer为例,也就是后面将要实现的resnet_50,可以看到总共有6个模块,分别是:conv1、conv2_x、conv3_x、conv4_x、conv5_x、fc。

        conv1主要是对原始输入图像进行第一波卷积,把输入图像从224缩小为112(这里的224和112指图像的长宽,后面同理);conv2/3/4/5_x是4个卷积模块,每个模块包含了多个由3个卷积层组成的小模块,例如,对于conv3_x这个模块来说,包含了4个小模块(右边有一个×4),每个小模块包含了3个卷积层。

三、ResNet使用PyTorch框架实现

kaggle项目地址:ResNet | Kaggle

  1. import torch
  2. from torch import nn
  3. import sys
  4. sys.path.append("../input/d2ld2l")
  5. import d2l
  6. from d2l.torch import load_data_fashion_mnist
  7. from d2l.torch import train_ch6
  8. from d2l.torch import try_gpu
  9. from torch.nn import functional as F
  1. class Residual(nn.Module):
  2. def __init__(self,in_channes,out_channes,use_1x1_conv=False,stride = 1):
  3. super().__init__()
  4. if use_1x1_conv:
  5. self.res = nn.Sequential(
  6. nn.Conv2d(in_channes,out_channes,kernel_size=1,stride=stride)
  7. )
  8. else:
  9. self.res = nn.Sequential()
  10. self.model =nn.Sequential(
  11. nn.Conv2d(in_channes,out_channes,kernel_size=3,padding=1,stride=stride),
  12. nn.BatchNorm2d(out_channes),
  13. nn.ReLU(),
  14. nn.Conv2d(out_channes,out_channes,kernel_size=3,padding=1),
  15. nn.BatchNorm2d(out_channes)
  16. )
  17. def forward(self,x):
  18. ret = self.model(x)
  19. ret = ret+self.res(x)
  20. return F.relu(ret)
  1. def get_residual_block(num_res,in_channels,out_channels,down_first = False):
  2. blk = []
  3. for i in range(num_res):
  4. blk.append(Residual(in_channels,out_channels,i==0 and down_first,1+(i==0 and down_first)))
  5. in_channels = out_channels
  6. return blk
  1. in_channels = 1
  2. b1 = nn.Sequential(
  3. nn.Conv2d(in_channels,64,kernel_size=7,stride=2,padding=3),
  4. nn.BatchNorm2d(64),
  5. nn.ReLU(),
  6. nn.MaxPool2d(kernel_size=3,stride=2,padding=1)
  7. )
  8. b2 = nn.Sequential(*get_residual_block(3,64,64))
  9. b3 = nn.Sequential(*get_residual_block(4,64,128,True))
  10. b4 = nn.Sequential(*get_residual_block(6,128,256,True))
  11. b5 = nn.Sequential(*get_residual_block(3,256,512,True))
  1. resnet = nn.Sequential(b1,b2,b3,b4,b5,
  2. nn.AdaptiveAvgPool2d((1,1)),
  3. nn.Flatten(),nn.Linear(512,10)
  4. )
  1. test = torch.rand((1,1,224,224))
  2. print("test net")
  3. for layer in resnet:
  4. test = layer(test)
  5. print(layer.__class__.__name__,"shape:",test.shape)

  1. lr,num_epochs,batch_size = 0.05,10, 128
  2. train_iter,test_iter =load_data_fashion_mnist(batch_size,resize=224)
  3. train_ch6(resnet,train_iter,test_iter,num_epochs,lr,try_gpu())

 四、 Resnet50使用keras框架实现

  1. import numpy as np
  2. from keras import layers
  3. from keras.layers import Input, Add, Dense, Activation, ZeroPadding2D, BatchNormalization, Flatten, Conv2D, AveragePooling2D, MaxPooling2D
  4. from keras.models import Model, load_model
  5. from keras.initializers import glorot_uniform

特征块 

 

减少块 

 

  1. def bottleneck_residual_block(X, f, filters, stage, block, reduce=False, s=2):
  2. """
  3. Arguments:
  4. X -- input tensor of shape (m, height, width, channels)
  5. f -- integer, specifying the shape of the middle CONV's window for the main path
  6. filters -- python list of integers, defining the number of filters in the CONV layers of the main path
  7. stage -- integer, used to name the layers, depending on their position in the network
  8. block -- string/character, used to name the layers, depending on their position in the network
  9. reduce -- boolean, True = identifies the reduction layer at the beginning of each learning stage
  10. s -- integer, strides
  11. Returns:
  12. X -- output of the identity block, tensor of shape (H, W, C)
  13. """
  14. # defining name basis
  15. conv_name_base = 'res' + str(stage) + block + '_branch'
  16. bn_name_base = 'bn' + str(stage) + block + '_branch'
  17. # Retrieve Filters
  18. F1, F2, F3 = filters
  19. # Save the input value. You'll need this later to add back to the main path.
  20. X_shortcut = X
  21. if reduce:
  22. # if we are to reduce the spatial size, apply a 1x1 CONV layer to the shortcut path
  23. # to do that, we need both CONV layers to have similar strides
  24. X = Conv2D(filters = F1, kernel_size = (1, 1), strides = (s,s), padding = 'valid', name = conv_name_base + '2a', kernel_initializer = glorot_uniform(seed=0))(X)
  25. X = BatchNormalization(axis = 3, name = bn_name_base + '2a')(X)
  26. X = Activation('relu')(X)
  27. X_shortcut = Conv2D(filters = F3, kernel_size = (1, 1), strides = (s,s), padding = 'valid', name = conv_name_base + '1',
  28. kernel_initializer = glorot_uniform(seed=0))(X_shortcut)
  29. X_shortcut = BatchNormalization(axis = 3, name = bn_name_base + '1')(X_shortcut)
  30. else:
  31. # First component of main path
  32. X = Conv2D(filters = F1, kernel_size = (1, 1), strides = (1,1), padding = 'valid', name = conv_name_base + '2a', kernel_initializer = glorot_uniform(seed=0))(X)
  33. X = BatchNormalization(axis = 3, name = bn_name_base + '2a')(X)
  34. X = Activation('relu')(X)
  35. # Second component of main path
  36. X = Conv2D(filters = F2, kernel_size = (f, f), strides = (1,1), padding = 'same', name = conv_name_base + '2b', kernel_initializer = glorot_uniform(seed=0))(X)
  37. X = BatchNormalization(axis = 3, name = bn_name_base + '2b')(X)
  38. X = Activation('relu')(X)
  39. # Third component of main path
  40. X = Conv2D(filters = F3, kernel_size = (1, 1), strides = (1,1), padding = 'valid', name = conv_name_base + '2c', kernel_initializer = glorot_uniform(seed=0))(X)
  41. X = BatchNormalization(axis = 3, name = bn_name_base + '2c')(X)
  42. # Final step: Add shortcut value to main path, and pass it through a RELU activation
  43. X = Add()([X, X_shortcut])
  44. X = Activation('relu')(X)
  45. return X

 ResNet50 采用以下架构

  1. def ResNet50(input_shape, classes):
  2. """
  3. Arguments:
  4. input_shape -- tuple shape of the images of the dataset
  5. classes -- integer, number of classes
  6. Returns:
  7. model -- a Model() instance in Keras
  8. """
  9. # Define the input as a tensor with shape input_shape
  10. X_input = Input(input_shape)
  11. # Stage 1
  12. X = Conv2D(64, (7, 7), strides=(2, 2), name='conv1', kernel_initializer=glorot_uniform(seed=0))(X_input)
  13. X = BatchNormalization(axis=3, name='bn_conv1')(X)
  14. X = Activation('relu')(X)
  15. X = MaxPooling2D((3, 3), strides=(2, 2))(X)
  16. # Stage 2
  17. X = bottleneck_residual_block(X, 3, [64, 64, 256], stage=2, block='a', reduce=True, s=1)
  18. X = bottleneck_residual_block(X, 3, [64, 64, 256], stage=2, block='b')
  19. X = bottleneck_residual_block(X, 3, [64, 64, 256], stage=2, block='c')
  20. # Stage 3
  21. X = bottleneck_residual_block(X, 3, [128, 128, 512], stage=3, block='a', reduce=True, s=2)
  22. X = bottleneck_residual_block(X, 3, [128, 128, 512], stage=3, block='b')
  23. X = bottleneck_residual_block(X, 3, [128, 128, 512], stage=3, block='c')
  24. X = bottleneck_residual_block(X, 3, [128, 128, 512], stage=3, block='d')
  25. # Stage 4
  26. X = bottleneck_residual_block(X, 3, [256, 256, 1024], stage=4, block='a', reduce=True, s=2)
  27. X = bottleneck_residual_block(X, 3, [256, 256, 1024], stage=4, block='b')
  28. X = bottleneck_residual_block(X, 3, [256, 256, 1024], stage=4, block='c')
  29. X = bottleneck_residual_block(X, 3, [256, 256, 1024], stage=4, block='d')
  30. X = bottleneck_residual_block(X, 3, [256, 256, 1024], stage=4, block='e')
  31. X = bottleneck_residual_block(X, 3, [256, 256, 1024], stage=4, block='f')
  32. # Stage 5
  33. X = bottleneck_residual_block(X, 3, [512, 512, 2048], stage=5, block='a', reduce=True, s=2)
  34. X = bottleneck_residual_block(X, 3, [512, 512, 2048], stage=5, block='b')
  35. X = bottleneck_residual_block(X, 3, [512, 512, 2048], stage=5, block='c')
  36. # AVGPOOL
  37. X = AveragePooling2D((1,1), name="avg_pool")(X)
  38. # output layer
  39. X = Flatten()(X)
  40. X = Dense(classes, activation='softmax', name='fc' + str(classes), kernel_initializer = glorot_uniform(seed=0))(X)
  41. # Create the model
  42. model = Model(inputs = X_input, outputs = X, name='ResNet50')
  43. return model
  1. model = ResNet50(input_shape = (32,32, 3), classes = 10)
  2. model.summary()

使用 plot_model 可视化网络

安装

  • conda install graphviz
  • conda install pydotplus
  1. from keras.utils import plot_model
  2. plot_model(model, to_file="images/resnet50.png", show_shapes=True)

 

参考:

ResNet50学习笔记 (附代码)_resnet中的恒等映射-CSDN博客

【AI】《ResNet》论文解读、代码实现与调试找错_resnet论文地址_Dreamcatcher风的博客-CSDN博客​​​​​​​

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/598135
推荐阅读
相关标签
  

闽ICP备14008679号