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

news/2025/1/18 16:16:37/

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/news/1564189.html

相关文章

reac 后端接口返回二进制文件流前端导出文件

axios配置 在你的请求中加入 responseType:blob导出函数 export interface DownloadFileOptions {filename: string; //文件名称}/*** 下载二进制文件流* param data - 二进制数据* param options - 下载配置*/export const downloadBinaryFile1 (data: any, // 这里使用 a…

3.数据库系统

3.1数据库的基本概念 3.1.1:数据库体系结构 3.1.1.1集中式数据库系统 数据是集中的 数据管理是集中的 数据库系统的素有功能(从形式的用户接口到DBMS核心)都集中在DBMS所在的计算机 3.1.1.2C/S结构 客户端负责数据表示服务服务器主要负责数据库服务 数据库系统分为前端和后端…

D3.js及实例应用

文章目录 D3.jsd3.js 应用实例图标展示点击选择拖拉拽应用 D3.js D3.js是一个功能强大的JavaScript库,除了图标展示,还能实现多种类型的交互效果: 数据可视化交互 动态更新图表:根据用户操作(如点击按钮、选择下拉菜…

初学stm32 --- flash模仿eeprom

目录 STM32内部FLASH简介 内部FLASH构成(F1) FLASH读写过程(F1) 闪存的读取 闪存的写入 内部FLASH构成(F4 / F7 / H7) FLASH读写过程(F4 / F7 / H7) 闪存的读取 闪存的写入 …

图像识别opencv翻转

运行界面 以下是代码: import cv2 import numpy #读取图像 img cv2.imread("../image/cat.jpg") #1,0,-1代表不一样的翻转效果 flipped cv2.flip(img, 1) #显示结果 cv2.imshow("Flip", flipped) cv2.imshow("original", img) cv2…

【C++ 类和对象 进阶篇】—— 逻辑森林的灵动精灵,舞动类与对象的奇幻圆舞曲

欢迎来到ZyyOvO的博客✨,一个关于探索技术的角落,记录学习的点滴📖,分享实用的技巧🛠️,偶尔还有一些奇思妙想💡 本文由ZyyOvO原创✍️,感谢支持❤️!请尊重原创&#x1…

4、dockerfile实现lnmp

dockerfile实现lnmp: nginx centos7 172.111.0.10 mysql centos7 172.111.0.20 php centos7 172.111.0.30 自定义镜像来实现整个架构 nginx dockerfile配置文件 nginx配置文件 创建镜像 声明网段 创建…

MYSQL的第一次作业

目录 前情提要 题目解析 连接并使用数据库 创建employees表 创建orders表 创建invoices表 ​查看建立的表 前情提要 需要下载mysql并进行配置,建议下载8.0.37,详情可见MySQL超详细安装配置教程(亲测有效)_mysql安装教程-CSDN博客 题目解析 …