深度学习篇---ResNet-18网络结构
本文介绍了如何使用PyTorch实现ResNet-18模型。主要内容包括:1)将模型构建过程类比为"搭积木":先创建核心的BasicBlock残差块(含2层卷积和残差连接),再用这些块搭建完整模型;2)详细代码实现,包含初始卷积层、4组残差块(共8个BasicBlock)、全局池化和全连接层;3)验证模型流程,通过随机输入数据测试模型输出。文章强调ResNet-18的关键在于残
要理解 PyTorch 实现 ResNet-18,我们可以把它拆成 “搭积木” 的过程:先造好最核心的 “小积木”(残差块),再用这些积木拼出 “整个模型骨架”(从输入到输出的完整流程),最后验证模型是否能跑通。全程用 “生活化类比 + 代码拆解”,即使不懂复杂原理也能看明白。
前置知识:PyTorch 实现模型的核心逻辑
在 PyTorch 里写模型,本质是定义一个 继承自 torch.nn.Module
的类,这个类要包含两个关键部分:
__init__
方法:“准备积木”—— 定义模型需要的所有组件(比如卷积层、残差块、全连接层);forward
方法:“拼积木”—— 定义数据在模型里的流动路径(输入→卷积→残差块→输出)。
我们就按照 “先做小积木,再拼大模型” 的顺序来实现 ResNet-18。
第一步:造核心 “小积木”——BasicBlock(基础残差块)
ResNet-18 的核心是 BasicBlock(2 层卷积的残差块),它就像模型的 “最小功能单元”,负责提取图像特征并通过 “残差连接” 避免梯度消失。
先看 BasicBlock 的结构(类比 “迷你加工站”):
- 输入数据 → 1 层 3×3 卷积 → 批量归一化(BN)→ ReLU 激活 → 1 层 3×3 卷积 → BN → 残差连接(加原始输入)→ 最终 ReLU 激活。
代码拆解 BasicBlock(带通俗注释)
import torch
import torch.nn as nn # PyTorch的神经网络工具箱
class BasicBlock(nn.Module):
# 初始化:定义残差块里的所有“小零件”
def __init__(self, in_channels, out_channels, stride=1):
super(BasicBlock, self).__init__()
# 1. 第一层卷积:3×3卷积核,步长stride(控制特征图尺寸是否缩小)
self.conv1 = nn.Conv2d(
in_channels=in_channels, # 输入特征图的“通道数”(比如64个通道=64种特征)
out_channels=out_channels, # 输出特征图的通道数
kernel_size=3, # 卷积核大小(3×3,提取局部特征)
stride=stride, # 步长(1=尺寸不变,2=尺寸缩小一半)
padding=1, # 边缘填充(保证卷积后尺寸符合预期)
bias=False # 因为后面有BN,BN会处理偏置,这里设为False
)
# 2. 第一层卷积后的批量归一化(BN):让数据分布更稳定,加速训练
self.bn1 = nn.BatchNorm2d(out_channels)
# 3. ReLU激活函数:给模型加“非线性”,让它能学习复杂特征(比如从边缘学到纹理)
self.relu = nn.ReLU(inplace=True) # inplace=True:节省内存
# 4. 第二层卷积:和第一层结构类似,但步长固定为1(不改变尺寸)
self.conv2 = nn.Conv2d(
in_channels=out_channels,
out_channels=out_channels,
kernel_size=3,
stride=1,
padding=1,
bias=False
)
self.bn2 = nn.BatchNorm2d(out_channels)
# 5. 残差连接的“适配层”:当输入输出通道数/尺寸不一样时,用1×1卷积调整
# 比如:输入通道64,输出通道128,直接加会“尺寸不匹配”,需要用1×1卷积把64→128
self.downsample = None # 默认没有适配层(输入输出一致时)
if stride != 1 or in_channels != out_channels:
self.downsample = nn.Sequential( # 用“序列容器”把层打包
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
# 定义数据流动路径(forward=向前传播)
def forward(self, x):
residual = x # 先把原始输入存起来(对应“残差连接的捷径”)
# 1. 走“正常卷积路径”:conv1 → bn1 → relu → conv2 → bn2
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
# 2. 残差连接:如果需要适配,先调整原始输入的通道/尺寸,再和卷积结果相加
if self.downsample is not None:
residual = self.downsample(x) # 适配原始输入
out += residual # 核心:卷积结果 + 原始输入(残差连接)
# 3. 最后激活,输出该残差块的结果
out = self.relu(out)
return out
通俗理解 BasicBlock:
就像 “加工苹果”:
- 原始苹果(x)→ 洗苹果(conv1)→ 擦干(bn1)→ 切小块(relu)→ 加糖(conv2)→ 装盒(bn2)→ 最后把 “原始苹果(residual)” 和 “加工后的苹果(out)” 混合 → 最终成品(激活后的 out)。
- 如果原始苹果太大(输入输出尺寸不匹配),先把原始苹果切小(downsample)再混合。
第二步:拼 “大模型”——ResNet-18 完整结构
ResNet-18 的整体结构,我们在之前的介绍里提过(6 个阶段),现在用 BasicBlock 把这些阶段 “拼起来”:
输入图像 → 初始卷积层 → 初始 BN → ReLU → 最大池化 → 4 组残差块(共 8 个 BasicBlock)→ 全局平均池化 → 全连接层 → 输出类别
代码拆解 ResNet-18(带通俗注释)
class ResNet18(nn.Module):
# 初始化:拼出整个模型的“骨架”
def __init__(self, num_classes=1000): # num_classes:分类任务的类别数(比如ImageNet是1000类)
super(ResNet18, self).__init__()
# 1. 初始处理层:把输入图片(比如3通道RGB图)转成64通道特征图,同时缩小尺寸
self.in_channels = 64 # 后续残差块的“输入通道数”初始值
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组残差块(共8个BasicBlock,对应ResNet-18的“18层”中16个卷积层)
# 每组残差块的参数:(块数, 输出通道数, 步长)
self.layer1 = self._make_layer(BasicBlock, 64, 2, stride=1) # 2个块,输出64通道,尺寸56×56(不变)
self.layer2 = self._make_layer(BasicBlock, 128, 2, stride=2) # 2个块,输出128通道,尺寸28×28(缩小一半)
self.layer3 = self._make_layer(BasicBlock, 256, 2, stride=2) # 2个块,输出256通道,尺寸14×14(缩小一半)
self.layer4 = self._make_layer(BasicBlock, 512, 2, stride=2) # 2个块,输出512通道,尺寸7×7(缩小一半)
# 3. 全局平均池化:把7×7×512的特征图,转成1×1×512的向量(每个通道取平均值)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) # 不管输入尺寸,输出都是(1,1)
# 4. 全连接层:把512维向量,转成“类别数”维的输出(比如1000类,输出1000个数值)
self.fc = nn.Linear(512, num_classes)
# 辅助函数:批量创建残差块(避免重复写代码)
def _make_layer(self, block, out_channels, blocks_num, stride=1):
layers = [] # 用列表存所有残差块
# 第一块残差块:可能需要步长stride(缩小尺寸)或适配通道,所以单独创建
layers.append(block(self.in_channels, out_channels, stride))
self.in_channels = out_channels # 更新后续块的“输入通道数”(和当前输出通道一致)
# 剩下的blocks_num-1块:步长固定为1(不改变尺寸),通道数已适配
for _ in range(blocks_num - 1):
layers.append(block(self.in_channels, out_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组残差块:逐层提取更复杂的特征
x = self.layer1(x) # 56×56×64 → 56×56×64
x = self.layer2(x) # 56×56×64 → 28×28×128
x = self.layer3(x) # 28×28×128 → 14×14×256
x = self.layer4(x) # 14×14×256 → 7×7×512
# 3. 池化+全连接:输出类别
x = self.avgpool(x) # 7×7×512 → 1×1×512
x = torch.flatten(x, 1) # 把(1×1×512)展平成(512,)的向量(去掉空间维度)
x = self.fc(x) # 512 → num_classes(比如1000)
return x
第三步:验证模型 —— 让 ResNet-18 跑起来
写好模型后,我们需要 “喂点数据”,验证模型是否能正常输出结果(相当于 “试运转”)。
测试代码(带通俗注释)
# 1. 创建ResNet-18模型实例(比如分类1000类)
model = ResNet18(num_classes=1000)
# 设为“评估模式”(如果是训练,需要用model.train())
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])
预测概率最高的类别索引: 456
这说明模型能正常工作:输入一张 “假图片”,输出 1000 个类别得分,并找到得分最高的类别(456 类,具体是什么类取决于训练数据)。
关键补充:ResNet-18 的 “18 层” 到底在哪?
很多人会疑惑 “代码里没看到 18 层啊”,这里明确计算:
ResNet-18 的 “18 层可训练层”= 卷积层(16 层) + 全连接层(2 层):
- 初始卷积层:1 层(conv1);
- 4 组残差块:每组 2 个 BasicBlock,每个 Block 含 2 层卷积 → 4×2×2=16 层?不,初始卷积层单独算,残差块里的卷积层是 4 组 ×2 块 ×2 层 = 16 层?不对,重新算:
正确计算:初始卷积层(1) + 4 组残差块(每组 2 块 ×2 层卷积 = 4 层,4 组共 16 层) + 全连接层(1 层)?不,PyTorch 官方 ResNet-18 的 “18 层” 定义是 16 层卷积层 + 2 层全连接层,但实际代码中全连接层只有 1 层(fc)—— 核心是 “关注残差连接的设计”,层数统计是学术定义,不影响使用。
简单记:只要用了 “BasicBlock + 残差连接”,且结构符合 “初始层 + 4 组残差块 + 池化 + 全连接”,就是正确的 ResNet-18。
一句话总结 PyTorch 实现 ResNet-18
先定义 “BasicBlock 残差块”(含 2 层卷积 + 残差连接),再用这个块拼出 “初始层→4 组残差块→池化→全连接” 的完整模型,最后用随机数据验证模型能跑通 —— 整个过程就像 “先做乐高零件,再拼乐高模型,最后试玩模型”。

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