当前位置: 首页 > java >正文

深度学习篇---ResNet-18网络结构

要理解 PyTorch 实现 ResNet-18,我们可以把它拆成 “搭积木” 的过程:先造好最核心的 “小积木”(残差块),再用这些积木拼出 “整个模型骨架”(从输入到输出的完整流程),最后验证模型是否能跑通。全程用 “生活化类比 + 代码拆解”,即使不懂复杂原理也能看明白。

前置知识:PyTorch 实现模型的核心逻辑

在 PyTorch 里写模型,本质是定义一个 继承自 torch.nn.Module 的类,这个类要包含两个关键部分:

  1. __init__ 方法:“准备积木”—— 定义模型需要的所有组件(比如卷积层、残差块、全连接层);
  2. 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→128self.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 → bn2out = 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 → maxpoolx = 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×64x = self.layer2(x)  # 56×56×64 → 28×28×128x = self.layer3(x)  # 28×28×128 → 14×14×256x = self.layer4(x)  # 14×14×256 → 7×7×512# 3. 池化+全连接:输出类别x = self.avgpool(x)  # 7×7×512 → 1×1×512x = 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 组残差块→池化→全连接” 的完整模型,最后用随机数据验证模型能跑通 —— 整个过程就像 “先做乐高零件,再拼乐高模型,最后试玩模型”。

http://www.xdnf.cn/news/18834.html

相关文章:

  • 【算法--链表题1】2. 两数相加:通俗详解
  • 用大语言模型实现语音到语音翻译的新方法:Scheduled Interleaved Speech-Text Training
  • 论文Review 激光3DGS GS-SDF | IROS2025 港大-MARS!| 激光+3DGS+NeRF会得到更好的几何一致性和渲染结果!?
  • React前端开发_Day1
  • Linux虚拟机ansible部署
  • OSPF 的工作过程、Router ID 机制、报文结构
  • Axios多实例封装
  • 产品运营必备职场通用能力及提升攻略,一文说明白
  • Kafa面试经典题--Kafka为什么吞吐量大,速度快
  • 字帖生成器怎么用?电脑手机双端操作指南
  • 【图像算法 - 24】基于深度学习与 OpenCV 实现人员跌倒识别系统(目标检测方案 - 跌倒即目标)
  • 如何在PC上轻松访问iPhone照片(已解决)
  • 【LeetCode - 每日1题】求对角线最长矩形的面积
  • WebSocket实时通信系统——js技能提升
  • 系统架构设计师备考第7天——网络协议中间件软件构件
  • 计算机网络:天气预报
  • Vue3 + Element Plus实现表格多行文本截断与智能Tooltip提示
  • 论文阅读 2025-8-26 一些半监督学习的工作
  • 04. 鸿蒙_获取app缓存大小和清除缓存
  • iOS 开发中的 UIStackView 使用详解
  • 飞算JavaAI:Java开发新时代的破晓之光
  • 【软考论文】论面向对象建模方法(动态、静态)
  • Go函数详解:从基础到高阶应用
  • 数据结构:单向链表的逆置;双向循环链表;栈,输出栈,销毁栈;顺序表和链表的区别和优缺点;0825
  • Java的四种优化资源密集型任务的策略
  • 每日一题——力扣498 对角线遍历
  • CentOS 部署 Prometheus 并用 systemd 管理
  • Mistral AI音频大模型Voxtral解读
  • 初识神经网络——《深度学习入门:基于Python的理论与实现》
  • QT(1)