ResNet50是一个经典的特征提取网络结构,虽然Pytorch已有官方实现,但为了加深对网络结构的理解,还是自己动手敲敲代码搭建一下。需要特别说明的是,笔者是以熟悉网络各层输出维度变化为目的的,只对建立后的网络赋予伪输入并测试各层输出,并没有用图像数据集训练过该网络(后续会用图像数据集测试并更新博客)。

1 预备理论

在动手搭建ResNet50以前,首先需要明确ResNet系列网络的基本结构,其次复习与卷积相关的几个知识点,以便更好地理解网络中间输出维度的变化。

1.1 ResNet系列

1.1.1 几种网络基本配置

ResNet原文中的表格列出了几种基本的网络结构配置:

从上表可以看出,对于不同深度的ResNet有以下几个特点,请特别关注(3)(4):

(1)起始阶段都经历了相同的conv1和maxpool的过程。

(2)不同深度的ResNet都是由基本残差块堆叠而成。 18,34-layer的基本模块记为Basicblock,包含2次卷积;50,101,152layer的基本模块记为Bottleneck,包含3次卷积(1.1.2节会详细说明)。

n-layer确定的情况下,称i阶段为convi_x过程,i∈{2,3,4,5}:

(3)2阶段堆叠的残差块完全相同。 因为输入到输出是56→56,无下采样过程。

(4)3至5阶段堆叠的第一个残差块和其余残差块是不同的。 解释:每个阶段均对特征图像大小进行下采样。以50layer–conv3_x为例,仔细思考残差块的堆叠模式可以发现,下采样过程发生在4个堆叠残差块中的第一个,因为这里实现了特征图尺寸从56→28的过程;而对于其余3个残差块,特征图的维度全部是28→28,因此这3个的结构是完全相同的(如果这里我没有表述清楚,可以参看下面的图和末尾的表)。其余阶段同理。

1.1.2 基本残差块的两种模式

以下将以问答的形式来理解基本残差块的两种模式,由于本文关注ResNet50的实现,因此以下以Bottleneck为例说明,对于Basicblock可以类比。

  • 为什么需要下采样?

    下采样是才特征提取网络中经常使用的操作,潜在的作用是增强特征的变换不变性,减少特征参数防止过拟合,具体表现为特征图像尺寸逐渐缩小,通道数逐渐增加

  • 为什么Bottleneck有两种模式?

    请回顾1.1.1节中的表格,对于绿色交界处,特征图维度不变,而对于红色交界,特征图维度变化,说明这里需要进行一次下采样。以50layer–conv3_x为例,这一阶段共堆叠了4个残差块。红色交界有一次下采样,并且在第一个残差块实现,我们称其为Bottleneck_down;对于后面3个残差块,特征图的尺寸均不发生变化,不进行下采样,记为Bottleneck_norm。具体可以参考下面的红绿线辅助理解。这一规律对于conv3_x, conv4_x, conv5_x都是成立的。

    特别的是,对于conv2_x,其堆叠的残差块都是相同的。

  • 下采样的卷积实现思路?

    在PyTorch中使用nn.Conv2d实现卷积,通常会使用的参数如下:

    torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, bias=True)
    

    因此实现下采样会用到如下操作(虽然还不够具体,是个思路雏形):

    • 特征图尺寸减半:卷积步长stride=2。

    • 特征图通道加倍:卷积核数目out_channels=4*in_channels(因为H/2,W/2,特征图缩小为1/4,所以通道数x4)。

  • 下采样的具体实现?

    参照上面的思路,Bottleneck的两种模式如下:实现的关键点就是我们需要判断出当前位置需要哪种模式,并设置正确的卷积步长。具体实现请参看2.1节。

1.2 2维卷积后的特征图维度变化

下面我们来回顾一下卷积的过程前后输入特征图维度的变化。

下图是一个k x k x C_in大小的卷积核在 H_in x W_in x C_in 大小的特征图上进行二维卷积的过程。对于单个卷积核而言,它将在 H_in x W_in 的平面上进行滑动,并按照一下公式输出一张维度为 H_out x W_out x 1 大小的特征图;而输出特征图的数量取决于卷积核的数量filter_num。

2 代码实现

以下我们分别将Bottleneck和ResNet50作为类来实现,而Bottleneck是ResNet50中堆叠的基本残差块。

2.1 Bottleneck实现

为了实现Bottleneck的两种模式配置,我们需要利用downsample控制shortcut支路特征图尺寸和通道数变换(这里先知道downsample的功能即可,具体实现见ResNet类)。

这里解释一下,downsample是shortcut支路的网络结构。如果当前残差块的输入和输出的特征维度大小相同,那么shortcut的输出直接继承原始输入x就好的(代码中暂存为了identity变量);而如果当前残差块的输入与输出特征图的尺寸大小和通道数不一致,即需要在shortcut支路也完成下采样的操作。

def forward(self, x):identity = x    # 将原始输入暂存为shortcut的输出if self.downsample is not None:identity = self.downsample(x)   # 如果需要下采样,那么shortcut后:H/2,W/2。C: out_channel -> 4*out_channel(见ResNet中的downsample实现)

以下是Bottleneck类的完整实现,可以对照ResNet50的表格查看。

# todo Bottleneck
class Bottleneck(nn.Module):"""__init__in_channel:残差块输入通道数out_channel:残差块输出通道数stride:卷积步长downsample:在_make_layer函数中赋值,用于控制shortcut图片下采样 H/2 W/2"""expansion = 4   # 残差块第3个卷积层的通道膨胀倍率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)   # H,W不变。C: in_channel -> out_channelself.bn1 = nn.BatchNorm2d(num_features=out_channel)self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel, kernel_size=3, stride=stride, bias=False, padding=1)  # H/2,W/2。C不变self.bn2 = nn.BatchNorm2d(num_features=out_channel)self.conv3 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel*self.expansion, kernel_size=1, stride=1, bias=False)   # H,W不变。C: out_channel -> 4*out_channelself.bn3 = nn.BatchNorm2d(num_features=out_channel*self.expansion)self.relu = nn.ReLU(inplace=True)self.downsample = downsampledef forward(self, x):identity = x    # 将原始输入暂存为shortcut的输出if self.downsample is not None:identity = self.downsample(x)   # 如果需要下采样,那么shortcut后:H/2,W/2。C: out_channel -> 4*out_channel(见ResNet中的downsample实现)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

2.2 ResNet50实现

ResNet中,conv1与其后的maxpool是不重复的,需要单独实现,而conv2,3,4,5_x中的则利用make_layer函数实现对基本残差块Bottleneck的堆叠。这里我们单独看一下make_layer是怎么实现的。

def _make_layer(self, block, channel, block_num, stride=1):"""block: 堆叠的基本模块channel: 每个stage中堆叠模块的第一个卷积的卷积核个数,对resnet50分别是:64,128,256,512block_num: 当期stage堆叠block个数stride: 默认卷积步长"""downsample = None   # 用于控制shorcut路的if stride != 1 or self.in_channel != channel*block.expansion:   # 对resnet50:conv2中特征图尺寸H,W不需要下采样/2,但是通道数x4,因此shortcut通道数也需要x4。对其余conv3,4,5,既要特征图尺寸H,W/2,又要shortcut维度x4downsample = nn.Sequential(nn.Conv2d(in_channels=self.in_channel, out_channels=channel*block.expansion, kernel_size=1, stride=stride, bias=False), # out_channels决定输出通道数x4,stride决定特征图尺寸H,W/2nn.BatchNorm2d(num_features=channel*block.expansion))layers = []  # 每一个convi_x的结构保存在一个layers列表中,i={2,3,4,5}layers.append(block(in_channel=self.in_channel, out_channel=channel, downsample=downsample, stride=stride)) # 定义convi_x中的第一个残差块,只有第一个需要设置downsample和strideself.in_channel = channel*block.expansion   # 在下一次调用_make_layer函数的时候,self.in_channel已经x4for _ in range(1, block_num):  # 通过循环堆叠其余残差块(堆叠了剩余的block_num-1个)layers.append(block(in_channel=self.in_channel, out_channel=channel))return nn.Sequential(*layers)   # '*'的作用是将list转换为非关键字参数传入

注意:这里我们首先判断是否需要对shortcut支路进行下采样,然后生成了对应的downsample网络结构。判断条件是:stride!=1 (当前阶段需要对特征图下采样)or in_channel != channel * block.expansion(由于进行了下采样,输出特征图通道数x4,不再与输入相等。eg. 对于conv3_x的第一个block,stride=2,因此需要下采样;而对于第二个block,stride=1,in_channel = channel * block.expansion,即512=128 * 4,因此不需要下采样)。然后,我们再将downsample传入block(即Bottleneck)中,作为shortcut支路下采样结构的具体实现。

参考表格中ResNet50的参数进行网络搭建,ResNet类的完整实现如下。

# todo ResNet
class ResNet(nn.Module):"""__init__block: 堆叠的基本模块block_num: 基本模块堆叠个数,是一个list,对于resnet50=[3,4,6,3]num_classes: 全连接之后的分类特征维度_make_layerblock: 堆叠的基本模块channel: 每个stage中堆叠模块的第一个卷积的卷积核个数,对resnet50分别是:64,128,256,512block_num: 当期stage堆叠block个数stride: 默认卷积步长"""def __init__(self, block, block_num, num_classes=1000):super(ResNet, self).__init__()self.in_channel = 64    # conv1的输出维度self.conv1 = nn.Conv2d(in_channels=3, out_channels=self.in_channel, kernel_size=7, stride=2, padding=3, bias=False)     # H/2,W/2。C:3->64self.bn1 = nn.BatchNorm2d(self.in_channel)self.relu = nn.ReLU(inplace=True)self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)     # H/2,W/2。C不变self.layer1 = self._make_layer(block=block, channel=64, block_num=block_num[0], stride=1)   # H,W不变。downsample控制的shortcut,out_channel=64x4=256self.layer2 = self._make_layer(block=block, channel=128, block_num=block_num[1], stride=2)  # H/2, W/2。downsample控制的shortcut,out_channel=128x4=512self.layer3 = self._make_layer(block=block, channel=256, block_num=block_num[2], stride=2)  # H/2, W/2。downsample控制的shortcut,out_channel=256x4=1024self.layer4 = self._make_layer(block=block, channel=512, block_num=block_num[3], stride=2)  # H/2, W/2。downsample控制的shortcut,out_channel=512x4=2048self.avgpool = nn.AdaptiveAvgPool2d((1,1))  # 将每张特征图大小->(1,1),则经过池化后的输出维度=通道数self.fc = nn.Linear(in_features=512*block.expansion, out_features=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   # 用于控制shorcut路的if stride != 1 or self.in_channel != channel*block.expansion:   # 对resnet50:conv2中特征图尺寸H,W不需要下采样/2,但是通道数x4,因此shortcut通道数也需要x4。对其余conv3,4,5,既要特征图尺寸H,W/2,又要shortcut维度x4downsample = nn.Sequential(nn.Conv2d(in_channels=self.in_channel, out_channels=channel*block.expansion, kernel_size=1, stride=stride, bias=False), # out_channels决定输出通道数x4,stride决定特征图尺寸H,W/2nn.BatchNorm2d(num_features=channel*block.expansion))layers = []  # 每一个convi_x的结构保存在一个layers列表中,i={2,3,4,5}layers.append(block(in_channel=self.in_channel, out_channel=channel, downsample=downsample, stride=stride)) # 定义convi_x中的第一个残差块,只有第一个需要设置downsample和strideself.in_channel = channel*block.expansion   # 在下一次调用_make_layer函数的时候,self.in_channel已经x4for _ in range(1, block_num):  # 通过循环堆叠其余残差块(堆叠了剩余的block_num-1个)layers.append(block(in_channel=self.in_channel, out_channel=channel))return nn.Sequential(*layers)   # '*'的作用是将list转换为非关键字参数传入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

2.3 伪输入测试

我们在建立Bottleneck与ResNet这两个类的基础之上,再在当前脚本的末尾增加如下代码,即可利用伪输入进行网络输出维度测试。注意:这里设定的网络分类个数为1000,可以根据分类任务自行调整。

def resnet50(num_classes=1000):return ResNet(block=Bottleneck, block_num=[3, 4, 6, 3], num_classes=num_classes)if __name__ == '__main__':input = torch.randn(1, 3, 224, 224)  # B C H Wprint(input.shape)ResNet50 = resnet50(1000)output = ResNet50.forward(input)print(ResNet50)

3 网络结构总结

下面以表格的形式总结一下ResNet50的网络结构及其中间的维度变换。注意:绿色Downsample实际是与其余三个Conv2d并行的,而其余不含绿色的Block所对的支路其实不需要下采样,因此这里没有列出来。

ResNet50 网络结构搭建(PyTorch)相关推荐

  1. 机器学习之神经网络(从神经网络结构到pytorch基本用法)

    机器学习之神经网络(从神经网络结构到pytorch基本用法) 1.神经网络概念 1.1 网络结构 ​ 人工神经网络(artificial neural network,ANN),简称神经网络(neur ...

  2. 怎样检查python环境是否安装好_如何搭建pytorch环境的方法步骤

    1.conda创建虚拟环境pytorch_gpu conda create -n pytorch_gpu python=3.6 创建虚拟环境还是相对较快的,它会自动为本环境安装一些基本的库,等待时间无 ...

  3. resnet50网络结构_Resnet50详解与实践(基于mindspore)

    1. 简述 Resnet是残差网络(Residual Network)的缩写,该系列网络广泛用于目标分类等领域以及作为计算机视觉任务主干经典神经网络的一部分,典型的网络有resnet50, resne ...

  4. 在腾讯云主机上搭建pytorch深度学习环境

    在腾讯云主机上搭建pytorch深度学习环境 1.购置[腾讯云主机](https://cloud.tencent.com/? fromSource=gwzcw.234976.234976.234976 ...

  5. 从零开始搭建Pytorch运行环境Linux

    Linux从零开始搭建Pytorch运行环境 安装Miniconda Miniconda download 官网 选择合适版本的安装脚本,复制链接.(选择最新的3.9可能合适些) 下载安装脚本. wg ...

  6. LINUX服务器搭建pytorch环境

    最近需要跑深度学习模型,租用2080Ti显卡,连接远程服务器,记录一下学习过程. 一.连接远程服务器 [ssh远程连接服务器教程]租了GPU服务器不知道怎么深度学习?看完不会你打我_哔哩哔哩_bili ...

  7. ConvNeXt网络结构搭建

    ConvNeXt-T 结构图 ConvNeXt Block模块搭建 class Block(nn.Module): # ConvNeXt Block模块def __init__(self, dim, ...

  8. 【Anaconda+pycharm搭建pytorch】

    文章目录 前言 一.安装Anaconda并创建pytorch环境 1.下载Anaconda 2.安装步骤 3.Anaconda安装完成,利用Conda创建环境 二.安装Pycharm 1.下载Pych ...

  9. 【pytorch】ResNet18、ResNet20、ResNet34、ResNet50网络结构与实现

    文章目录 ResNet主体 BasicBlock ResNet18 ResNet34 ResNet20 Bottleneck Block ResNet50 ResNet到底解决了什么问题 选取经典的早 ...

最新文章

  1. 七个开源的 SpringBoot 前后端分离项目,Star过千,快去收藏夹吃灰吧!
  2. cass出现验证许可_ASML:DUV光刻机无需美国许可!最新一代EUV光刻机明年发布:加工精度提升至1.1nm...
  3. CodeForces 173B Chamber of Secrets spfa
  4. 2015.7.29 上学前在家的最后一晚
  5. 2003下的共享问题
  6. js经纬度十进制度和度分秒互转
  7. sublime使用指南
  8. 小白学习Java必练基础题(一)
  9. tipask二次开发总结_tipask二次开发总结
  10. vim编辑器删除键失效问题
  11. 2020011030王紫薇
  12. 从0到一开发微信小程序(6)—小程序常用API
  13. glusterfs架构和原理
  14. 模型选择的方法——正则化与交叉验证
  15. wpsmac和pc版的区别_WPS Office for Mac VS Microsoft Office 365,同为办公软件,差别竟然这么大!...
  16. Blender 2.9
  17. 50款水滴效果PS笔刷
  18. c语言 cdma编码正交的8位码片,CDMA编码
  19. Lévy State-Space Models for Tracking and Intent Predictionof Highly Maneuverable Objects
  20. 漏洞修复引发的项目报错

热门文章

  1. Golang UDP编程
  2. 这家公司,竟然解决了这些困扰智能驾驶测试很久的难题
  3. MyEclipse优化全攻略
  4. 3.4 数值分析: 迭代法的收敛性
  5. PHP代码审计3—系统重装漏洞
  6. 如何使用 javascript 获取语音数据并播放
  7. excel比较两个表中数据 提取相同列 合并到同一张表中
  8. 基于51单片机的超声波避障小车设计(含Proteus仿真)
  9. 射频应用>>>Si44xx射频芯片>>>芯片版本信息区分
  10. 数据分析-关于男女比例的问题