ResNet50 网络结构搭建(PyTorch)

article/2025/9/18 5:36:33

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所对的支路其实不需要下采样,因此这里没有列出来。
在这里插入图片描述


http://chatgpt.dhexx.cn/article/wQj87owT.shtml

相关文章

ResNet-50网络理解

本文主要针对ResNet-50对深度残差网络进行一个理解和分析 ResNet已经被广泛运用于各种特征提取应用中,当深度学习网络层数越深时,理论上表达能力会更强,但是CNN网络达到一定的深度后,再加深,分类性能不会提高&#xff…

庖丁解牛-Resnet50 深度剖析,细致讲解,深入理解

背景介绍 ResNet-50侧边输出形状 假设输入为352,则 output2 256x88x88 output3 512x44x44 output4 1024x22x22 output5 2048x11x11 VGG-16侧边输出形状 假设输入为352,则 output1 64x320x320 output2 128x160x160 output3 256x88x88 output4 512x44x44 output5 512x22…

Resnet-50网络结构详解

解决的问题: 梯度消失,深层网络难训练。 因为梯度反向传播到前面的层,重复相乘可能使梯度无穷小。结果就是,随着网络的层数更深,其性能趋于饱和,甚至迅速下降。 关于为什么残差结构(即多了一…

卷积神经网络学习—Resnet50(论文精读+pytorch代码复现)

前言一、Resnet论文精读引入残差残差块ResNet50模型基本构成BN层Resnet50总体结构 二、Resnet50代码复现完整代码 前言 如果说在CNN领域一定要学习一个卷积神经网络,那一定非Resnet莫属了。 接下来我将按照:Resnet论文解读、Pytorch实现ResNet50模型两部…

Java类加载器介绍

1.类加载器介绍 类加载器负责将class文件加载到内存中,并为之生成对应的java.lang.Class对象。对于任意一个类,都需要加载它的类加载器和这个类本身来确定该类在JVM中唯一性,也就是说,同一个class文件用两个不同的类加载器加载并…

类加载与类加载器概述

目录 一、类加载 类的加载: 类的连接: 类的初始化: 类初始化步骤: 类的初始化时机: 二、类加载器 类加载器的作用 JVM的类加载机制 Java运行时具有以下内置类加载器: 一、类加载 当程序要使用某…

十一、类加载器的作用

狂神说Java:https://www.bilibili.com/video/BV1p4411P7V3 1、类加载的作用 将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据…

2.类加载器

回顾 上一节我们学习了JVM类加载机制,我们学习到大概的过程:通过类加载器将编译好的class文件加载到JVM进程中,通过字节码执行引擎去执行代码。这只是一个整体的过程,具体的细节我们从本节开始分析。 通过本节我们将掌握以下知识&…

Java类加载器

一.类的生命周期 1. 加载(Loading):找 Class 文件 1. 通过一个类的全限定名来获取定义此类的二进制字节流。 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 3.在内存中生成一个代表这个类的java.lang.Class对象&#xf…

Java类加载器的使用

Java类加载器 classloader顾名思义,即是类加载。虚拟机把描述类的数据从class字节码文件加载到内存,并对数据进行检验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。 先认识一下类加载…

JVM 类加载器

什么是类加载器 类加载器负责在运行时将Java类动态加载到Java虚拟机,他们也是JRE(Java运行时环境)的一部分。因此,借助类加载器,JVM无需了解底层文件或文件系统即可运行Java程序。此外,这些Java类不会一次…

类加载器深入理解

虚拟机设计团队把类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的模块称为“类加载器”。 类加载器在类层次划分、OSGI、热部署、代码加密等领域…

java获取类加载器

获取类加载器的方法: //扩展类加载器MainClassLoader classLoader MainTest.class.getClassLoader();//表示当前线程的类加载器——应用程序类加载器ClassLoader contextClassLoader Thread.currentThread().getContextClassLoader();//—启动类加载器ClassLoader systemClas…

类加载器的种类

类加载器的种类有四种,如下图所示: 1.启动类加载器(引导类加载器,Bootstrap ClassLoader) 这个类加载使用C/C语言实现的,嵌套在JVM内部它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、res…

Java类加载器详解

1 特点 双亲委派: 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载…

【类加载器】java类加载器

类装载器ClassLoader(一个抽象类) 描述一下JVM加载class文件的原理机制 类装载器就是寻找类或接口字节码文件进行解析并构造JVM内部对象表示的组件,在java中类装载器把一个类装入JVM,经过以下步骤: 1、装载&#xff…

什么是类加载器?

类加载器 什么是类加载器,作用是什么? 类加载器就是加载字节码文件(.class)的类 Java语言是一种具有动态性的解释语言,类(CLASS) 只有被加载到 JVM 中后才能运行。当运行指定程序时,JVM会将编译生成的.class文件按照需求和一定的规…

类加载器

类加载过程 加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。 类加载器分类 JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader: 启动类加载器&…

类加载器作用

深入探讨 Java 类加载器 成 富, 软件工程师, IBM 中国软件开发中心 成富任职于 IBM 中国软件开发中心,目前在 Lotus 部门从事 IBM Mashup Center 的开发工作。他毕业于北京大学信息科学技术学院,获得计算机软件与理论专业硕士学位。他的个人网站是 http:…

java中的类加载器

文章目录 前言,一、加载器的作用是什么二、详解类加载器1.不得不说的双亲委派机制2.各个加载器加载的内容3.线程上下文类加载器4.类加载器的庐山真面目 总结 前言, java中一般来说有三种类加载器,分别为: 引导加载器,扩展加载器,应用加载器,还有一个线程上下文类加载器 一、加…