【深度学习】图片分类—ResNet
哔哩哔哩:29 残差网络 ResNet【动手学深度学习v2】BV1bV41177apResNet 中最核心的两种残差模块(Residual Block)设计,它们是让 ResNet 能训练到上百层甚至上千层的关键。ResNet18 使用具体结构如下:1. 主分支(Main Path)Conv1:3×3 卷积(带 padding=1,保持特征图尺寸不变;步幅可设为 2 实现下采样)BN1:批归一化层
哔哩哔哩:29 残差网络 ResNet【动手学深度学习v2】 BV1bV41177ap
ResNet 中最核心的两种残差模块(Residual Block)设计,它们是让 ResNet 能训练到上百层甚至上千层的关键。
一、残差学习
1.1残差层
ResNet18 使用BasicBlock(基础残差块),由两层 3×3 卷积 + 残差连接(Shortcut) 组成,
具体结构如下:
1. 主分支(Main Path)
Conv1:3×3 卷积(带 padding=1,保持特征图尺寸不变;步幅可设为 2 实现下采样)
BN1:批归一化层,稳定分布
ReLU:激活函数,引入非线性
Conv2:3×3 卷积(padding=1,步幅固定为 1,保持尺寸不变)
BN2:批归一化层
2. 残差分支(Shortcut Path)
实线连接:输入与输出通道数相同时,直接将原始输入与主分支输出相加
虚线连接:输入与输出通道数不同时,用 1×1 卷积调整通道数和尺寸,实现维度对齐
3. 最终输出
主分支输出与残差分支输出相加后,再经过一个 ReLU 激活。
1.2、残差层的核心作用
1、解决深度退化问题

2.缓解梯度消失
残差连接提供了一条 “梯度直传” 的路径,让梯度可以直接从深层传回浅层,避免了深层网络中梯度消失的问题。
3.实现特征复用
残差连接允许原始特征直接传递到后续层,实现了跨层特征复用,增强了模型的表达能力。
1.3、残差层的两种工作模式
1. 实线残差(通道数不变,无下采样)
当输入与输出通道数相同时(如layer1中的残差块),残差分支直接连接:
输入:64*56*56
主分支:Conv1→BN1→ReLU→Conv2→BN2 → 输出:64*56*56
残差分支:直接输入 → 输出:64*56*56
相加后 ReLU → 最终输出:64*56*56
2. 虚线残差(通道数变化,带下采样)
当输入与输出通道数不同时(如layer2的第一个残差块),残差分支用 1×1 卷积对齐维度:
输入:64*56*56
主分支:Conv1(stride=2)→BN1→ReLU→Conv2→BN2 → 输出:128*28*28
残差分支:1×1 卷积(stride=2)→ 输出:128*28*28
相加后 ReLU → 最终输出:128*28*28

1.1、为什么需要虚线残差结构
在 ResNet 中,实线残差连接要求主分支与 shortcut 分支的空间尺寸(H,W)和通道数(C)完全一致,才能直接相加。
当网络需要下采样(步幅 = 2)或改变通道数时,就必须使用虚线残差连接(Dotted Shortcut),通过 1×1 卷积来对齐维度。
虚线残差是 ResNet 中用于跨 stage 维度升级 + 下采样的残差连接方式,当残差块的输入 / 输出通道数、空间尺寸不一致时,通过在残差分支增加1×1卷积实现维度对齐,最终让主分支和残差分支的输出能顺利相加。
1.2、虚线残差的核心操作
虚线残差的关键是在 shortcut 分支上添加一个1×1 卷积层,它同时完成两个任务:
通道数变换:将输入通道数调整为与主分支输出通道数一致
空间下采样:通过设置stride=2,将特征图尺寸(H,W)减半

二、ResNet中两种不同的ResNet block

2.1 BasicBlock vs Bottleneck 核心对比表






三、代码:
3.1 导入模块 & 加载官方 ResNet18
import torch # 1. 导入PyTorch核心库,用于张量操作、模型构建
import torch.nn as nn # 2. 导入PyTorch神经网络模块,提供卷积、BN、池化等层
import torchvision.models as models # 3. 导入torchvision预定义模型库,包含官方ResNet18
resNet = models.resnet18() # 4. 实例化官方ResNet18模型(默认随机初始化权重)
print(resNet) # 5. 打印官方ResNet18的完整结构,用于和自定义版本对比
获取官方 ResNet18 的基准结构,方便后续自定义实现时对齐维度和层结构;
官方 ResNet18 由conv1+bn1+maxpool + 4 个 layer(各 2 个 BasicBlock) + avgpool + fc组成。
3.2 定义 Basic 残差块(Residual_block)




# 6. 定义ResNet18的基础残差块(BasicBlock),继承nn.Module
class Residual_block(nn.Module):
def __init__(self, input_channels, out_channels, down_sample=False, strides=1):
# 7. 构造函数参数说明:
# input_channels:输入通道数;out_channels:输出通道数;
# down_sample:是否下采样(未实际使用,strides控制步幅);strides:卷积步幅(默认1)
super().__init__() # 8. 调用父类nn.Module的构造函数,必须执行
# 9. 第一层3×3卷积:步幅由strides控制(下采样时=2,否则=1),padding=1保证same padding(尺寸不变,除非strides=2)
self.conv1 = nn.Conv2d(input_channels, out_channels,
kernel_size=3, padding=1, stride=strides)
# 10. 第二层3×3卷积:步幅固定=1,padding=1,保证尺寸不变
self.conv2 = nn.Conv2d(out_channels, out_channels,
kernel_size=3, padding=1, stride= 1)
# 11. 虚线残差判断:如果输入输出通道数不一致,需要1×1卷积对齐维度
if input_channels != out_channels:
# 12. 1×1卷积:调整通道数+同步下采样(strides和conv1一致),实现虚线残差
self.conv3 = nn.Conv2d(input_channels, out_channels,
kernel_size=1, stride=strides)
else:
# 13. 实线残差:通道数一致,无需卷积,直接相加
self.conv3 = None
# 14. 批归一化(BN):每层卷积后加BN,加速训练、防止过拟合
self.bn1 = nn.BatchNorm2d(out_channels)
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU() # 15. 定义ReLU激活函数(全局复用)
def forward(self, X): # 16. 定义残差块的前向传播
# 17. 主分支:conv1 → BN1 → ReLU
out = self.relu(self.bn1(self.conv1(X)))
# 18. 主分支:conv2 → BN2(注意:这里先不激活,等残差相加后再激活)
out= self.bn2(self.conv2(out))
# 19. 残差分支(shortcut):如果需要对齐维度(conv3存在),先过1×1卷积
if self.conv3:
X = self.conv3(X)
# 20. 残差相加:主分支输出 + 残差分支(实线/虚线)
out += X
# 21. 相加后激活:这是ResNet的核心设计(先加后激活),区别于普通CNN的“先激活后加”
return self.relu(out)
3.3 定义完整的 ResNet18(MyResNet18)
class MyResNet18(nn.Module): # 定义自定义ResNet18完整网络类,继承nn.Module核心基类
def __init__(self):
super(MyResNet18, self).__init__() # 调用父类nn.Module的构造函数
# 第一层卷积层:7×7大卷积核做初始特征提取
# in_channels=3(RGB图像),out_channels=64(输出64通道),kernel_size=7,stride=2(下采样),padding=3(same padding)
self.conv1 = nn.Conv2d(3, 64, 7, 2, 3)
# 尺寸变化:3*224*224→64*112*112(计算:(224-7+2*3)/2 +1 = 112)
self.bn1 = nn.BatchNorm2d(64)
# 卷积后接批归一化,作用于64个通道,加速训练、稳定分布
# 尺寸变化:64*112*112→64*112*112(BN不改变张量尺寸)
# 最大池化层:3×3核,步幅2,padding=1,进一步下采样压缩特征图
self.pool1 = nn.MaxPool2d(3, stride=2, padding=1) # 尺寸变化:64*112*112→64*56*56
self.relu = nn.ReLU() '''ReLU激活函数,引入非线性'''
# layer1:由2个Basic残差块组成,输入/输出均为64通道,无下采样(实线残差)
self.layer1 = nn.Sequential(
Residual_block(64, 64),
# 第一个残差块:输入64通道,输出64通道,strides=1(无下采样)# 尺寸变化:64*56*56→64*56*56(通道/尺寸均不变)
Residual_block(64, 64)
# 第二个残差块:同第一个,纯实线残差 # 尺寸变化:64*56*56→64*56*56
)
# layer2:由2个残差块组成,第一个64→128通道(虚线残差+下采样),第二个128→128(实线残差)
self.layer2 = nn.Sequential(
Residual_block(64, 128, strides=2),
# 第一个残差块:输入64→输出128通道,strides=2(下采样) # 尺寸变化:64*56*56→128*28*28(通道×2,尺寸÷2)
Residual_block(128, 128) # 第二个残差块:输入/输出128通道,实线残差
# 尺寸变化:128*28*28→128*28*28
)
# layer3:由2个残差块组成,第一个128→256通道(虚线残差+下采样),第二个256→256(实线残差)
self.layer3 = nn.Sequential(
Residual_block(128, 256, strides=2), # 第一个残差块:输入128→输出256通道,strides=2(下采样)
# 尺寸变化:128*28*28→256*14*14(通道×2,尺寸÷2)
Residual_block(256, 256) # 第二个残差块:输入/输出256通道,实线残差
# 尺寸变化:256*14*14→256*14*14
)
# layer4:由2个残差块组成,第一个256→512通道(虚线残差+下采样),第二个512→512(实线残差)
self.layer4 = nn.Sequential(
Residual_block(256, 512, strides=2), # 第一个残差块:输入256→输出512通道,strides=2(下采样)
# 尺寸变化:256*14*14→512*7*7(通道×2,尺寸÷2)
Residual_block(512, 512) # 第二个残差块:输入/输出512通道,实线残差
# 尺寸变化:512*7*7→512*7*7
)
self.flatten = nn.Flatten() # 定义展平层,将二维特征图转为一维向量,为全连接层做准备
# 尺寸变化:C*H*W→C*H*W(展平操作在forward中生效)
''' 自适应平均池化层:无论输入特征图尺寸是多少,强制输出1×1的特征图,兼容不同输入尺寸 '''
self.adv_pool = nn.AdaptiveAvgPool2d(1) # 尺寸变化(forward中):512*7*7→512*1*1
# 全连接层:将池化后的512维特征向量映射到1000类(ImageNet分类任务)
self.fc = nn.Linear(512, 1000) # 尺寸变化(forward中):512→1000
def forward(self, x): # 定义网络前向传播逻辑,数据流经网络的核心路径
# 第一层:卷积→BN→ReLU→最大池化
x = self.conv1(x) # 执行conv1卷积,尺寸变化:3*224*224→64*112*112
x = self.bn1(x) # 执行BN归一化,尺寸变化:64*112*112→64*112*112
x = self.relu(x) # 执行ReLU激活,尺寸变化:64*112*112→64*112*112
x = self.pool1(x) # 执行最大池化,尺寸变化:64*112*112→64*56*56
# 依次通过4个残差层堆叠
x = self.layer1(x) # 执行layer1,尺寸变化:64*56*56→64*56*56
x = self.layer2(x) # 执行layer2,尺寸变化:64*56*56→128*28*28
x = self.layer3(x) # 执行layer3,尺寸变化:128*28*28→256*14*14
x = self.layer4(x) # 执行layer4,尺寸变化:256*14*14→512*7*7
# 自适应池化→展平→全连接
x = self.adv_pool(x) # 执行自适应池化,尺寸变化:512*7*7→512*1*1
x = self.flatten(x) # 展平张量,尺寸变化:512*1*1→512(一维向量)
x = self.fc(x) # 执行全连接层,尺寸变化:512→1000
return x # 返回最终输出(1000类的预测得分)
四、测试自定义模型
myres = MyResNet18() # 42. 实例化自定义ResNet18模型
# 43. 定义函数:统计模型/层的参数量(总参数、可训练参数)
def get_parameter_number(model):
# 44. 统计总参数:sum(p.numel()) 计算所有参数的元素个数
total_num = sum(p.numel() for p in model.parameters())
# 45. 统计可训练参数:仅计算requires_grad=True的参数(默认全可训练)
trainable_num = sum(p.numel() for p in model.parameters() if p.requires_grad)
return {'Total': total_num, 'Trainable': trainable_num} # 46. 返回参数字典
# 47. 打印layer1的参数量(2个64→64残差块,均为实线残差)
print(get_parameter_number(myres.layer1))
# 48. 打印layer1第一个残差块conv1的参数量(3×3×64×64=110592)
print(get_parameter_number(myres.layer1[0].conv1))
# 49. 打印官方ResNet18 layer1第一个残差块conv1的参数量(和自定义版本一致)
print(get_parameter_number(resNet.layer1[0].conv1))
# 50. 创建随机输入:批量大小1,3通道,224×224(符合ImageNet输入规范)
x = torch.rand((1,3,224,224))
# 51. 官方ResNet18前向传播(测试可行性)
out = resNet(x)
# 52. 自定义ResNet18前向传播(测试可行性)
out = myres(x)
更多推荐


所有评论(0)