【深度学习】Pytorch:自实现残差网络

devtools/2025/1/17 11:45:34/

ResNet(残差网络)是由何凯明等人在2015年发表的论文《深度残差学习用于图像识别》中提出的一种开创性深度学习架构。它在ILSVRC 2015分类任务中获胜,并解决了深度神经网络中的退化问题,使得训练数百甚至数千层的网络成为可能。

残差网络

核心概念

ResNet的核心思想是使用恒等捷径跳跃连接。ResNet不直接学习底层的映射 H ( x ) H(x) H(x),而是强制网络学习残差映射 F ( x ) = H ( x ) − x F(x) = H(x) - x F(x)=H(x)x,这使得优化变得更加容易。

ResNet的一个构建块可以表示为:

y = F ( x , W i ) + x y=F(x,{W_i})+x y=F(x,Wi)+x
其中:

  • x:层的输入
  • F(x, {W_i}):学习到的残差映射
  • +x:跳跃连接

优势

在传统的深度神经网络中,随着层数的增加,网络会面临梯度消失或爆炸的问题,性能往往会下降而不是提升。ResNet通过引入残差连接解决了这一问题,使得梯度能够更顺畅地流动,从而确保极深网络的稳定训练。

PyTorch 实现

我们将使用 PyTorch 从头实现 ResNet,重点关注灵活性和代码清晰度。

导入库

import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import models

残差块

ResNet 的基础构建块(适用于小型网络,如 ResNet-18 和 ResNet-34)定义如下:

class BasicBlock(nn.Module):  expansion = 1  # 扩展因子,用于调整输出通道数  def __init__(self, in_channels, out_channels, stride=1, downsample=None):  super(BasicBlock, self).__init__()  # 第一个卷积层:3x3 卷积,指定输入和输出通道,stride 可调整以控制步幅  self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)  self.bn1 = nn.BatchNorm2d(out_channels)  # 对第一个卷积层的输出进行批归一化  # 第二个卷积层:3x3 卷积,stride 固定为 1,保持输出大小  self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)  self.bn2 = nn.BatchNorm2d(out_channels)  # 对第二个卷积层的输出进行批归一化  self.downsample = downsample  # 下采样模块的引用,调节输入和输出的维度匹配  def forward(self, x):  identity = x  # 保存输入值,用于实现残差连接  if self.downsample is not None:  identity = self.downsample(x)  # 如果存在下采样模块,使用它调整输入维度  # 第一个卷积 + 批归一化 + ReLU 激活函数  out = self.conv1(x)  out = self.bn1(out)  # 批归一化以提高训练稳定性  out = F.relu(out)  # 使用 ReLU 激活非线性  # 第二个卷积 + 批归一化  out = self.conv2(out)  out = self.bn2(out)  # 批归一化  out += identity  # 将输入通过残差连接添加到卷积输出  out = F.relu(out)  # 对残差连接的结果应用 ReLU 激活函数  return out  # 返回最终的输出

ResNet 架构

我们定义一个通用的 ResNet 类,使用 BasicBlock 来实现 ResNet-18 和 ResNet-34。

class ResNet(nn.Module):  def __init__(self, block, layers, num_classes=1000):  super(ResNet, self).__init__()  self.in_channels = 64  # 初始输入通道数  # 初始卷积层:7x7 卷积,stride=2,padding=3,用于特征提取  self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)  self.bn1 = nn.BatchNorm2d(64)  # 对初始卷积层的输出进行批归一化  self.relu = nn.ReLU(inplace=True)  # ReLU 激活函数,inplace=True 以节省内存  self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)  # 最大池化层降低特征图的尺寸  # 创建四个残差层  self.layer1 = self._make_layer(block, 64, layers[0])  # 第一层,输出通道数为 64  self.layer2 = self._make_layer(block, 128, layers[1], stride=2)  # 第二层,输出通道数为 128,步幅为 2  self.layer3 = self._make_layer(block, 256, layers[2], stride=2)  # 第三层,输出通道数为 256,步幅为 2  self.layer4 = self._make_layer(block, 512, layers[3], stride=2)  # 第四层,输出通道数为 512,步幅为 2  # 全局平均池化层和全连接层  self.avgpool = nn.AdaptiveAvgPool2d((1, 1))  # 自适应平均池化,输出 shape 为 (N, C, 1, 1)  self.fc = nn.Linear(512 * block.expansion, num_classes)  # 全连接层,该层输出的维度为 num_classes  def _make_layer(self, block, out_channels, blocks, stride=1):  downsample = None  # 如果需要下采样(调整输入维度)  if stride != 1 or self.in_channels != out_channels * block.expansion:  downsample = nn.Sequential(  nn.Conv2d(self.in_channels, out_channels * block.expansion,  kernel_size=1, stride=stride, bias=False),  # 1x1 卷积层用于调整通道数  nn.BatchNorm2d(out_channels * block.expansion),  # 批归一化  )  layers = []  # 将要构建的残差层列表  # 添加第一个残差块  layers.append(block(self.in_channels, out_channels, stride, downsample))  # 传入当前输入通道数、输出通道数和下采样  self.in_channels = out_channels * block.expansion  # 更新输入通道数  # 添加剩余的残差块  for _ in range(1, blocks):  layers.append(block(self.in_channels, out_channels))  # 连接剩余的块,不需要下采样  return nn.Sequential(*layers)  # 将所有层组合成一个顺序容器返回  def forward(self, x):  # 对输入进行前向传播  # 初始卷积层  x = self.conv1(x)  # 输入经过初始卷积层  x = self.bn1(x)  # 批归一化  x = self.relu(x)  # ReLU 激活  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)  # 展平,flatten 使得特征图变成一维  x = self.fc(x)  # 经过全连接层,得到最终输出  return x  # 返回分类结果

实例化 ResNet-18/34

def resnet18():return ResNet(BasicBlock, [2, 2, 2, 2])  # ResNet-18 的层结构
def resnet34():return ResNet(BasicBlock, [3, 4, 6, 3])  # ResNet-34 的层结构# 检查是否有 GPU,如果有则使用 GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")# 实例化模型并移动到设备
model = resnet18().to(device)

训练和评估

以下是一个简单的训练循环,用于训练 ResNet 模型。

import torch.optim as optim# 超参数
num_epochs = 10  # 训练轮数
learning_rate = 0.001  # 学习率# 损失函数和优化器
criterion = nn.CrossEntropyLoss()  # 交叉熵损失
optimizer = optim.Adam(model.parameters(), lr=learning_rate)  # Adam 优化器# 训练循环
for epoch in range(num_epochs):model.train()  # 设置模型为训练模式running_loss = 0.0  # 记录当前轮次的损失for inputs, labels in train_loader:  # 假设 train_loader 已定义inputs, labels = inputs.to(device), labels.to(device)  # 将数据移动到设备# 梯度清零optimizer.zero_grad()# 前向传播outputs = model(inputs)loss = criterion(outputs, labels)# 反向传播和优化loss.backward()optimizer.step()running_loss += loss.item()  # 累加损失print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}")print("训练完成。")

更深的 ResNet 网络实现

BottleneckBlock

BottleneckBlock 是 ResNet-50 及更深模型的基础构建块,其结构如下:

class BottleneckBlock(nn.Module):expansion = 4  # 扩展因子,用于调整输出通道数def __init__(self, in_channels, out_channels, stride=1, downsample=None):super(BottleneckBlock, self).__init__()# 第一个卷积层:1x1 卷积,用于降维self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)self.bn1 = nn.BatchNorm2d(out_channels)# 第二个卷积层:3x3 卷积,stride 可调整self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)self.bn2 = nn.BatchNorm2d(out_channels)# 第三个卷积层:1x1 卷积,用于升维self.conv3 = nn.Conv2d(out_channels, out_channels * self.expansion, kernel_size=1, bias=False)self.bn3 = nn.BatchNorm2d(out_channels * self.expansion)self.relu = nn.ReLU(inplace=True)self.downsample = downsample  # 下采样模块,用于调整输入维度def forward(self, x):identity = x  # 保存输入值,用于残差连接if self.downsample is not None:identity = self.downsample(x)  # 如果需要下采样,调整输入维度# 第一个卷积 + 批归一化 + ReLUout = self.conv1(x)out = self.bn1(out)out = self.relu(out)# 第二个卷积 + 批归一化 + ReLUout = 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

修改 ResNet 模型定义

通过调整残差块类型(BasicBlockBottleneckBlock)和层结构,可以轻松实现不同的 ResNet 模型。以下是常用 ResNet 模型的对应配置:

模型名称残差块类型层结构
ResNet-18BasicBlock[2, 2, 2, 2]
ResNet-34BasicBlock[3, 4, 6, 3]
ResNet-50BottleneckBlock[3, 4, 6, 3]
ResNet-101BottleneckBlock[3, 4, 23, 3]
ResNet-152BottleneckBlock[3, 8, 36, 3]

根据需求选择合适的模型即可!

准确率验证

在训练深度学习模型时,除了监控损失值外,验证模型的准确率同样重要。以下是一个完整的准确率验证模块,可以与训练循环结合使用,以评估模型在验证集上的性能。

# 验证循环
def validate_model(model, val_loader, device):model.eval()  # 设置模型为评估模式correct = 0  # 记录预测正确的样本数total = 0  # 记录总样本数val_loss = 0.0  # 记录验证集上的损失with torch.no_grad():  # 禁用梯度计算for inputs, labels in val_loader:  # 假设 val_loader 已定义inputs, labels = inputs.to(device), labels.to(device)  # 将数据移动到设备# 前向传播outputs = model(inputs)loss = criterion(outputs, labels)  # 计算损失val_loss += loss.item()  # 累加损失# 计算预测结果_, predicted = torch.max(outputs.data, 1)  # 获取预测类别total += labels.size(0)  # 更新总样本数correct += (predicted == labels).sum().item()  # 更新正确预测数# 计算平均损失和准确率avg_loss = val_loss / len(val_loader)accuracy = 100 * correct / totalprint(f"Validation Loss: {avg_loss:.4f}, Accuracy: {accuracy:.2f}%")return avg_loss, accuracy

总结

ResNet 通过使极深网络的训练成为可能,彻底改变了深度学习领域。通过在 PyTorch 中实现 ResNet,我们可以更深入地理解其架构和功能。这里学到的概念可以扩展到更复杂的版本,如 ResNet-50、ResNet-101 及更高版本。下一步我们可以:

  • 尝试更深的 ResNet 版本:例如 ResNet-50 或 ResNet-101,探索更深层次网络的性能表现。
  • 在自定义数据集上微调模型:将 ResNet 应用于特定任务,通过微调模型参数来适应新的数据集。
  • 探索迁移学习技术:在小数据集上使用预训练的 ResNet 模型,通过迁移学习提升性能。

通过以上步骤,您可以进一步掌握 ResNet 的应用,并将其扩展到更广泛的深度学习任务中。


http://www.ppmy.cn/devtools/151264.html

相关文章

用 Python 从零开始创建神经网络(二十):模型评估

模型评估 引言 引言 在第11章《测试或样本外数据》中,我们讨论了验证数据和测试数据之间的区别。对于目前的模型,我们在训练过程中进行了验证,但目前没有一个好的方法来对测试数据运行测试或进行预测。首先,我们将在Model类中添加…

C#局部函数 VS Lambda表达式

一、引言 在 C# 的编程世界里,我们常常会遇到各种实现功能的方式,其中 Lambda 表达式和局部函数都是非常强大的特性。Lambda 表达式自诞生以来,凭借其简洁的语法和强大的功能,深受广大开发者的喜爱,尤其是在处理集合操…

springboot中创建自定义注解和AOP

一、自定义注解的创建 在 Java 中,自定义注解使用 interface 关键字来定义。例如,我们创建一个名为 LogExecutionTime 的自定义注解,用于标记需要记录执行时间的方法: import java.lang.annotation.ElementType; import java.lan…

[NOIP2007 提高组] 矩阵取数游戏

[NOIP2007 提高组] 矩阵取数游戏 显示标签 题目讨论 题目统计 全部提交 时间限制:C/C 1000MS,其他语言 2000MS 内存限制:C/C 256MB,其他语言 512MB 难度:提高/省选- 分数:100 描述 帅帅经常跟同学玩一…

【Sharding-JDBC学习】读写分离_shardjdbc5 不支持 shardingdatasource

8.读写分离 8.1 理解读写分离 面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理…

【计算机网络】lab8 DNS协议

🌈 个人主页:十二月的猫-CSDN博客 🔥 系列专栏: 🏀计算机网络_十二月的猫的博客-CSDN博客 💪🏻 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光 目录 1. 前言 2.…

PyTorch框架——基于深度学习YOLOv11神经网络路面坑洞检测系统

基于深度学习YOLOv11神经网络路面坑洞检测系统,其能识别路面坑洞,见如下 第一步:YOLOv11介绍 YOLOv11是由Ultralytics公司开发的新一代目标检测算法,它在之前YOLO版本的基础上进行了显著的架构和训练方法改进。以下是YOLOv11的一…

带头双向循环链表(数据结构初阶)

文章目录 双向链表链表的分类概念与结构实现双向链表定义链表结构链表打印判空申请结点初始化头插尾插头删尾删查找指定位置插入和删除销毁链表 顺序表和链表的分析结语 欢迎大家来到我的博客,给生活来点impetus!! 这一节我们学习双向链表&a…