深度学习篇---ResNet-50网络结构
本文介绍了PyTorch实现ResNet-50的关键要点。ResNet-50的核心是使用Bottleneck残差块替代BasicBlock,通过1×1降维、3×3特征提取和1×1升维的三层卷积结构,在保证深度(50层)的同时减少计算量。文章详细拆解了Bottleneck块的实现代码,并类比为"高效快递打包流水线"。随后展示了如何用该模块构建完整的ResNet-50网络,包含初始
理解 PyTorch 实现 ResNet-50,核心是抓住它和 ResNet-18 的 “核心差异”—— 用 “Bottleneck 残差块” 替代 “BasicBlock”(把 2 层卷积的小积木,换成 3 层卷积的 “高效积木”)。整个过程依然是 “先造核心积木,再拼模型骨架,最后验证跑通”,用 “生活化类比 + 代码拆解” 讲透,零基础也能理解。
前置回顾:ResNet-50 与 ResNet-18 的核心区别
在动手前先明确关键差异,避免混淆:
对比项 | ResNet-18 | ResNet-50 |
---|---|---|
核心残差块 | BasicBlock(2 层卷积:3×3 + 3×3) | Bottleneck(3 层卷积:1×1 + 3×3 + 1×1) |
层数来源 | 16 层卷积 + 2 层全连接 = 18 层 | 49 层卷积 + 1 层全连接 = 50 层 |
核心优势 | 轻量快速 | 用 “降维 - 提特征 - 升维” 减少计算量,兼顾精度与效率 |
简单说:ResNet-50 的 “Bottleneck 块” 像 “带压缩功能的加工站”,能在加深层数(50 层)的同时,避免计算量爆炸 —— 这是它成为工业界 “最常用 ResNet 型号” 的关键。
第一步:造核心 “高效积木”——Bottleneck 残差块
ResNet-50 的核心是 Bottleneck 块(瓶颈块),它通过 “1×1 卷积降维→3×3 卷积提特征→1×1 卷积升维” 的流程,用更少计算量实现更深层的特征提取。
先看 Bottleneck 的结构(类比 “高效快递打包流水线”):
输入数据 → 1×1 卷积(降维,压缩特征维度)→ BN → ReLU → 3×3 卷积(提核心特征)→ BN → ReLU → 1×1 卷积(升维,恢复维度)→ BN → 残差连接(加原始输入)→ 最终 ReLU。
代码拆解 Bottleneck(带通俗注释)
import torch
import torch.nn as nn # PyTorch神经网络工具箱
class Bottleneck(nn.Module):
# 注意:Bottleneck有个固定比例“expansion=4”——升维后通道数是中间层的4倍
expansion = 4 # 核心参数:比如中间层是64通道,升维后就是64×4=256通道
def __init__(self, in_channels, mid_channels, stride=1):
super(Bottleneck, self).__init__()
out_channels = mid_channels * self.expansion # 最终输出通道数=中间通道数×4
# 1. 第一步:1×1卷积(降维)——压缩特征维度,减少后续计算量
self.conv1 = nn.Conv2d(
in_channels=in_channels,
out_channels=mid_channels, # 输出“中间通道数”(比如64),比输入维度小
kernel_size=1, # 1×1卷积核:只改变通道数,不改变特征图尺寸
stride=1,
bias=False # 后续有BN,BN会处理偏置,此处省略
)
self.bn1 = nn.BatchNorm2d(mid_channels) # 批量归一化:稳定数据分布
# 2. 第二步:3×3卷积(提特征)——核心特征提取,和ResNet-18的3×3卷积功能一致
self.conv2 = nn.Conv2d(
in_channels=mid_channels,
out_channels=mid_channels,
kernel_size=3, # 3×3卷积:提取局部特征(边缘、纹理、形状)
stride=stride, # 步长控制尺寸(1=不变,2=缩小一半)
padding=1, # 边缘填充:保证卷积后尺寸符合预期
bias=False
)
self.bn2 = nn.BatchNorm2d(mid_channels)
# 3. 第三步:1×1卷积(升维)——恢复通道数,和输入通道数匹配(方便后续残差连接)
self.conv3 = nn.Conv2d(
in_channels=mid_channels,
out_channels=out_channels, # 输出=中间通道×4,和输入通道数对齐
kernel_size=1,
stride=1,
bias=False
)
self.bn3 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True) # 激活函数:加非线性,让模型学复杂特征
# 4. 残差连接的“适配层”:当输入输出通道数/尺寸不匹配时,用1×1卷积调整
self.downsample = None
if stride != 1 or in_channels != out_channels:
self.downsample = nn.Sequential(
# 1×1卷积:同时调整通道数(in→out)和尺寸(stride控制)
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
# 定义数据在Bottleneck中的流动路径(向前传播)
def forward(self, x):
residual = x # 保存原始输入(对应残差连接的“捷径”)
# 1. 降维路径:conv1(1×1)→ bn1 → relu
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
# 2. 提特征路径:conv2(3×3)→ bn2 → relu
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
# 3. 升维路径:conv3(1×1)→ bn3
out = self.conv3(out)
out = self.bn3(out)
# 4. 残差连接:适配原始输入后,和卷积结果相加
if self.downsample is not None:
residual = self.downsample(x) # 调整原始输入的通道/尺寸
out += residual # 核心:卷积结果 + 原始输入(残差连接)
# 5. 最终激活,输出该块结果
out = self.relu(out)
return out
通俗理解 Bottleneck:
就像 “打包大箱子” 的高效流程:
- 原始大箱子(输入数据,比如 256 通道)→ 压缩成小盒子(1×1 卷积降维到 64 通道)→ 贴标签(3×3 卷积提特征)→ 恢复成大箱子(1×1 卷积升维回 256 通道)→ 最后把 “原始大箱子(residual)” 和 “加工后的大箱子(out)” 合并 → 成品。
- 压缩步骤的好处:中间 3×3 卷积只需要处理 “小盒子”(64 通道),计算量是直接处理 “大箱子”(256 通道)的 1/16,效率极高。
第二步:拼 “大模型”——ResNet-50 完整结构
ResNet-50 的整体框架和 ResNet-18 一致(“初始层→残差块组→池化→全连接”),核心差异是:
- 用 Bottleneck 替代 BasicBlock;
- 残差块组的 “块数量” 更多(每组 3/4/6/3 个 Bottleneck,共 16 个块,对应 48 层卷积)。
ResNet-50 完整结构(类比 “多阶段流水线”):
输入图像 → 初始卷积层 → BN → ReLU → 最大池化 → 4 组 Bottleneck(共 16 个块) → 全局平均池化 → 全连接层 → 输出类别
代码拆解 ResNet-50(带通俗注释)
class ResNet50(nn.Module):
def __init__(self, num_classes=1000): # num_classes:分类任务的类别数(如ImageNet 1000类)
super(ResNet50, self).__init__()
# 1. 初始处理层:把3通道RGB图转成64通道特征图,同时压缩尺寸
self.in_channels = 64 # 后续Bottleneck的“输入通道数”初始值
self.conv1 = nn.Conv2d(
in_channels=3, # 输入:3通道(RGB图)
out_channels=64, # 输出:64通道特征图
kernel_size=7, # 7×7大卷积核:快速压缩尺寸
stride=2, # 步长2:图片尺寸缩小一半(如224×224→112×112)
padding=3, # 边缘填充:保证卷积后尺寸正确
bias=False
)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(
kernel_size=3, stride=2, padding=1 # 3×3池化:尺寸再缩小一半(112×112→56×56)
)
# 2. 4组Bottleneck(共16个块,对应48层卷积)
# 每组参数:(Bottleneck类, 中间通道数, 块数量, 步长)
# 注意:Bottleneck的输出通道数=中间通道数×4(expansion=4)
self.layer1 = self._make_layer(Bottleneck, mid_channels=64, blocks_num=3, stride=1) # 输出64×4=256通道,尺寸56×56
self.layer2 = self._make_layer(Bottleneck, mid_channels=128, blocks_num=4, stride=2) # 输出128×4=512通道,尺寸28×28
self.layer3 = self._make_layer(Bottleneck, mid_channels=256, blocks_num=6, stride=2) # 输出256×4=1024通道,尺寸14×14
self.layer4 = self._make_layer(Bottleneck, mid_channels=512, blocks_num=3, stride=2) # 输出512×4=2048通道,尺寸7×7
# 3. 全局平均池化:把7×7×2048的特征图,转成1×1×2048的向量(每个通道取平均值)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) # 不管输入尺寸,输出都是(1,1)
# 4. 全连接层:把2048维向量,转成“类别数”维输出(如1000类→1000个数值)
self.fc = nn.Linear(512 * Bottleneck.expansion, num_classes) # 512×4=2048
# 辅助函数:批量创建Bottleneck组(避免重复代码)
def _make_layer(self, block, mid_channels, blocks_num, stride=1):
layers = [] # 用列表存当前组的所有Bottleneck
# 第一块Bottleneck:可能需要步长stride(缩小尺寸)或适配通道,单独创建
layers.append(block(self.in_channels, mid_channels, stride))
# 更新后续块的“输入通道数”(=当前块的输出通道数=mid_channels×4)
self.in_channels = mid_channels * block.expansion
# 剩下的blocks_num-1块:步长固定为1(不改变尺寸),通道数已适配
for _ in range(blocks_num - 1):
layers.append(block(self.in_channels, mid_channels, stride=1))
# 用“序列容器”打包所有块,返回一个“残差块组”
return nn.Sequential(*layers)
# 定义整个模型的数据流动路径
def forward(self, x):
# 1. 初始处理:conv1 → bn1 → relu → maxpool
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
# 2. 4组Bottleneck:逐层提取更复杂的特征
x = self.layer1(x) # 56×56×64 → 56×56×256
x = self.layer2(x) # 56×56×256 → 28×28×512
x = self.layer3(x) # 28×28×512 → 14×14×1024
x = self.layer4(x) # 14×14×1024 → 7×7×2048
# 3. 池化+全连接:输出类别
x = self.avgpool(x) # 7×7×2048 → 1×1×2048
x = torch.flatten(x, 1) # 展平成2048维向量(去掉空间维度)
x = self.fc(x) # 2048 → num_classes(如1000)
return x
第三步:验证模型 —— 让 ResNet-50 跑起来
和 ResNet-18 一样,用 “随机模拟图片” 测试模型是否能正常输出,相当于 “试运转流水线”。
测试代码(带通俗注释)
# 1. 创建ResNet-50模型实例(分类1000类,和ImageNet一致)
model = ResNet50(num_classes=1000)
# 设为“评估模式”(训练时用model.train(),评估时用model.eval())
model.eval()
# 2. 模拟输入图片:PyTorch要求格式为“(批次大小, 通道数, 高度, 宽度)”
# 这里模拟“1张3通道RGB图,尺寸224×224”(ImageNet标准输入)
fake_image = torch.randn(1, 3, 224, 224) # randn:生成随机像素(模拟真实图片)
# 3. 让数据流过模型,得到输出(评估时不计算梯度,节省内存)
with torch.no_grad():
output = model(fake_image)
# 4. 查看输出结果
print("模型输出形状:", output.shape) # 预期:(1, 1000) → 1个样本,1000个类别得分
print("预测概率最高的类别索引:", torch.argmax(output, dim=1).item()) # 得分最高的类别(0-999)
输出结果示例:
模型输出形状: torch.Size([1, 1000])
预测概率最高的类别索引: 789
这说明模型正常工作:输入 “假图片” 后,能输出 1000 个类别得分,并找到最可能的类别(789 类,具体类别需结合训练数据)。
关键补充:ResNet-50 的 “50 层” 到底在哪?
很多人疑惑 “代码里没看到 50 层”,这里明确计算(ResNet 家族的 “层数” 指 “可训练的卷积层 + 全连接层”):
ResNet-50 的 “50 层”= 49 层卷积 + 1 层全连接:
- 初始卷积层:1 层(conv1);
- 4 组 Bottleneck:每组含 “块数量 ×3 层卷积”,共 3×3 + 4×3 + 6×3 + 3×3 = 48 层;
- 全连接层:1 层(fc);
- 总计:1 + 48 + 1 = 50 层。
无需纠结 “层数统计细节”,核心是记住:用 Bottleneck 块,且残差块组为 “3/4/6/3” 个块,就是 ResNet-50。
一句话总结 PyTorch 实现 ResNet-50
先定义 “Bottleneck 残差块”(1×1 降维 + 3×3 提特征 + 1×1 升维,高效省计算),再用这个块拼出 “初始层→4 组残差块(3/4/6/3 个块)→池化→全连接” 的完整模型,最后用随机数据验证跑通 —— 就像 “先做高效乐高零件,再拼复杂乐高模型,最后试玩”。

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐
所有评论(0)