可以从本人以前的文章中可以看出作者以前从事的是嵌入式控制方面相关的工作,是一个机器视觉小白,之所以开始入门机器视觉的学习主要是一个idea,想把机器视觉与控制相融合未来做一点小东西。废话不多说开始正题。(如有侵权立即删稿)
摘要
本文是介绍ResNet目标检测网络搭建,个人对其的知识总结,以及结合论文进行讲解,网络设计的知识点,代码撰写部分本人是借鉴的大佬的源码(下文会给出网址),基于pytorch编写代码。作为一个刚入门的小白怎么去学习别人的代码,一步一步的去理解每一行代码,怎么将网络设计变成代码,模仿大佬的代码去撰写。作为小白如有不足之处请批评指正哈。
ResNet
在网络设计之前需要明白什么是ResNet。
以下是我借鉴的文章的参考链接:
[ 图像分类 ] 经典网络模型4——ResNet 详解与复现
CNN经典网络模型(五):ResNet简介及代码实现(PyTorch超详细注释版)
ResNet论文地址:https://arxiv.org/abs/1512.03385
接下来,我将结合论文进行讲解,明白论文的构思以及步骤,本人将论文的关键部分摘要出来,并且用中文翻译表示,进行着重讲述。
原文摘要:更深层次的神经网络更难训练。我们提出了一个残差学习框架,以简化网络的训练,大大深入比以前使用的。我们明确地将层重新表述为参考层输入学习残差函数,而不是学习未参考的函数。我们提供了全面的经验证据,表明这些残差网络更容易优化,并且可以从相当大的深度增加中获得精度。在ImageNet数据集上,我们评估了深度高达152层的残差网络-比VGG网络深8倍,但仍然具有较低的复杂性。在ImageNet测试集上,这些残差网络的集合达到了3.57%的误差。该结果在ILSVRC 2015年分类任务中获得第一名。我们还对100层和1000层的CIFAR-10进行了分析。表征的深度对于许多视觉识别任务来说是至关重要的。仅由于我们的深度表示,我们在COCO对象检测数据集上获得了28%的相对改进。深度残差网络是我们在ILSVRC和COCO 2015竞赛中提交的基础,在该竞赛中,我们还在ImageNet检测、ImageNet定位、COCO检测和COCO分割任务中获得了第一名。
小结:摘要部分,引入了残差学习的概念,也就是ResNet核心思想。
原文介绍1:
图中在CIFAR-10上使用20层和56层“普通”网络时的训练误差(左)和测试误差(右)。网络越深,训练误差越大,测试误差也越大。
小结1:引出消失/爆炸梯度的问题,及随着层数增加,网络训练不一定会提升准确率。
原文介绍2:
小结2:基于以上问题,提出解决方案,引入残差块思想。
原文内容3:本文通过引入深度残差学习框架来解决退化问题。我们不是希望每几个堆叠的层直接适合所需的底层映射,而是明确地让这些层适合残差映射。形式上,将所需的底层映射表示为H(x),我们让堆叠的非线性层拟合F(x)的另一个映射:= H(x)−x。将原始映射重新转换为F(x)+x。我们假设优化残差映射比优化原始的、未引用的映射更容易。在极端情况下,如果一个恒等映射是最优的,那么将残差推到零将比通过一堆非线性层拟合一个恒等映射更容易。
小结3:可以看出残差模块主要是经过两层weight,与原先的x叠加,好处就是,训练结果不好的时候,weight大量元素为0,反向回流也有identity支路,也能保证梯度不为0,来缓解梯度消失的问题。
原文内容4:这种重新表述的动机是关于退化问题的反直觉现象。正如我们在引言中所讨论的,如果添加的层可以构造为身份映射,则更深的模型的训练误差应该不大于较浅的模型。退化问题表明,求解器可能有困难,在逼近多个非线性层的单位映射。通过残差学习重构,如果恒等映射是最优的,则求解器可以简单地将多个非线性层的权重向零驱动以接近恒等映射。
小结4:这里也指出了,如果求解器已经是最优解,则他会让多层非线性层的权重为0,使输出为恒等映射,恒等映射也就是只有x,F(x)为0。
原文内容5:
这里,x和y是所考虑的层的输入和输出向量。函数F(x,{Wi})表示要学习的残差映射。对于图2中具有两层的示例,F = W2σ(W1 x),其中σ表示为了简化符号,省略了ReLU和偏置。运算F + x通过快捷连接和元素式加法来执行。
小结5:这里给出了一个公式例子,解释了残差网络,并且解释了上图的残差网络构造。
原文内容6:残差函数F的形式是灵活的。本文中的实验涉及具有两层或三层的函数F(图5),而更多层也是可能的。但是,如果F只有单层,则方程(1)类似于线性层:y = W1 x +x,对此我们没有观察到优点。我们还注意到,尽管为了简单起见,上面的符号是关于全连接层的,但是它们也适用于卷积层。函数F(x,{Wi})可以表示多个卷积层。逐个通道地在两个特征图上执行元素相加。
小结6:论文中说明了,W可以是卷积也可以是全连接。左图用于ResNet 34。右图:ResNet-50/101/152的构建。不同深度对应的构建块有所不同。
原文内容7:我们主要受VGG网络的启发。卷积层大多具有3×3滤波器,并遵循两个简单的设计规则:(i)对于相同的输出特征图大小,层具有相同数量的滤波器;(ii)如果特征图大小减半,则滤波器的数量加倍,以保持每层的时间复杂度。我们通过跨距为2的卷积层直接执行下采样。该网络以一个全局平均池层和一个具有softmax的1000路全连接层结束。加权层的总数为34。值得注意的是,我们的模型比VGG具有更少的滤波器和更低的复杂度。我们的34层基准具有36亿FLOP(乘法相加),仅为VGG-19(196亿FLOP)的18%。
小结7:这里也指出了ResNet34的优势,运算占用小,计算成本低。
原文实施步骤内容8:在[256,480]中随机采样图像的较短边以进行缩放,从而调整图像的大小。从图像或其水平翻转随机采样224×224的裁剪,减去每像素平均值。使用标准颜色增强。我们在每次卷积之后和激活之前,采用批归一化(BN)。我们如[13]中那样初始化权重,并从头开始训练所有普通/残差网络。我们使用SGD,最小批次大小为256。学习速率从0.1开始,当误差达到平台时除以10,模型的训练迭代次数高达60×104次。我们使用的重量衰减为0.0001,动量为0.9。在试验中,为了进行比较研究,我们采用了标准的10作物试验[21]。为了获得最佳结果,我们采用[41,13]中的完全卷积形式,并在多个尺度上对分数取平均值(调整图像大小,使短边在{224,256,384,480,640}中)。
小结8:这里详细概述了训练步骤,图像送入前如何处理,学习率设置的多少等等。
原文内容9:ImageNet的体系结构。构建块显示在括号中(另见图5),块的编号堆叠在一起。下采样是由conv3 1、conv4 1和conv5 1执行的,步长为2。
小结9:假设我要训练的网络为ResNet50,那么它的网络构造是
1. 7×7, 64, stride 2
2. 3×3 max pool, stride 2
3.[1×1, 64
3×3, 64
1×1, 256]
4.[1×1, 64
3×3, 64
1×1, 256]
5.[1×1, 64
3×3, 64
1×1, 256]
6.[1×1, 128
3×3, 128
1×1, 512]
7.[1×1, 128
3×3, 128
1×1, 512]
8.[1×1, 128
3×3, 128
1×1, 512]
9.[1×1, 128
3×3, 128
1×1, 512]
10.[1×1, 256
3×3, 256
1×1, 1024]
11.[1×1, 256
3×3, 256
1×1, 1024]
12.[1×1, 256
3×3, 256
1×1, 1024]
13.[1×1, 256
3×3, 256
1×1, 1024]
14.[1×1, 256
3×3, 256
1×1, 1024]
15.[1×1, 256
3×3, 256
1×1, 1024]
16.[1×1, 512
3×3, 512
1×1, 2048]
17.[1×1, 512
3×3, 512
1×1, 2048]
18.[1×1, 512
3×3, 512
1×1, 2048] 共计50层卷积
有了以上理论基础后,开始构建代码思路,整体构建思路如下图所示,写代码之前一定要构思好大致思路,代码永远是为你思路框架服务的。整体代码构思如下图。
ResNet网络构建
1.定义ResNet18/34的残差结构,为2个3x3的卷积,这部分代码按照图5构建。
import torch.nn as nn
import torch
# -------------------1.定义ResNet18/34的残差结构,为2个3x3的卷积-------------------
class BasicBlock(nn.Module):
# 判断残差结构中,主分支的卷积核个数是否发生变化,不变则为1
expansion = 1
# init():进行初始化,申明模型中各层的定义
# downsample=None对应实线残差结构,否则为虚线残差结构
def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
kernel_size=3, stride=stride, padding=1, bias=False)
# 使用批量归一化
self.bn1 = nn.BatchNorm2d(out_channel)
# 使用ReLU作为激活函数
self.relu = nn.ReLU()
self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channel)
self.downsample = downsample
# forward():定义前向传播过程,描述了各层之间的连接关系
def forward(self, x):
# 残差块保留原始输入
identity = x
# 如果是虚线残差结构,则进行下采样
if self.downsample is not None:
identity = self.downsample(x)
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
# -----------------------------------------
out = self.conv2(out)
out = self.bn2(out)
# 主分支与shortcut分支数据相加
out += identity
out = self.relu(out)
return out
2.定义ResNet50/101/152的残差结构,这部分代码按照图5构建。
# --------------2.定义ResNet50/101/152的残差结构,为1x1+3x3+1x1的卷积-----------------
class Bottleneck(nn.Module):
# expansion是指在每个小残差块内,减小尺度增加维度的倍数,如64*4=256
# Bottleneck层输出通道是输入的4倍
expansion = 4
# init():进行初始化,申明模型中各层的定义
# downsample=None对应实线残差结构,否则为虚线残差结构,专门用来改变x的通道数
def __init__(self, in_channel, out_channel, stride=1, downsample=None,
groups=1, width_per_group=64):
super(Bottleneck, self).__init__()
width = int(out_channel * (width_per_group / 64.)) * groups
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=width,
kernel_size=1, stride=1, bias=False)
# 使用批量归一化
self.bn1 = nn.BatchNorm2d(width)
# -----------------------------------------
self.conv2 = nn.Conv2d(in_channels=width, out_channels=width, groups=groups,
kernel_size=3, stride=stride, bias=False, padding=1)
self.bn2 = nn.BatchNorm2d(width)
# -----------------------------------------
self.conv3 = nn.Conv2d(in_channels=width, out_channels=out_channel * self.expansion,
kernel_size=1, stride=1, bias=False)
self.bn3 = nn.BatchNorm2d(out_channel * self.expansion)
# 使用ReLU作为激活函数
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
3.定义ResNet类,这部分按照上文中的表格撰写代码,定义一个可以选择块模型的函数, 选择块内容的数量的函数例如[3, 4, 6, 3]。
# -------------------------3.定义ResNet类-------------------------------
class ResNet(nn.Module):
# 初始化函数
def __init__(self,block,blocks_num,num_classes,include_top=True,groups=1,width_per_group=64):
super(ResNet, self).__init__()
self.include_top = include_top
# maxpool的输出通道数为64,残差结构输入通道数为64
self.in_channel = 64
self.groups = groups
self.width_per_group = width_per_group
self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(self.in_channel)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 浅层的stride=1,深层的stride=2
# block:定义的两种残差模块
# block_num:模块中残差块的个数
self.layer1 = self._make_layer(block, 64, blocks_num[0])
self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)
if self.include_top:
# 自适应平均池化,指定输出(H,W),通道数不变
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
# 全连接层
self.fc = nn.Linear(512 * block.expansion, num_classes)
# 遍历网络中的每一层
# 继承nn.Module类中的一个方法:self.modules(), 他会返回该网络中的所有modules
for m in self.modules():
# isinstance(object, type):如果指定对象是指定类型,则isinstance()函数返回True
# 如果是卷积层
if isinstance(m, nn.Conv2d):
# kaiming正态分布初始化,使得Conv2d卷积层反向传播的输出的方差都为1
# fan_in:权重是通过线性层(卷积或全连接)隐性确定
# fan_out:通过创建随机矩阵显式创建权重
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
# 定义残差模块,由若干个残差块组成
# block:定义的两种残差模块,channel:该模块中所有卷积层的基准通道数。block_num:模块中残差块的个数
def _make_layer(self, block, channel, block_num, stride=1):
downsample = None
# 如果满足条件,则是虚线残差结构
if stride != 1 or self.in_channel != channel * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(channel * block.expansion))
layers = []
layers.append(block(self.in_channel,
channel,
downsample=downsample,
stride=stride,
groups=self.groups,
width_per_group=self.width_per_group))
self.in_channel = channel * block.expansion
for _ in range(1, block_num): #卷积多次实现 卷积数=block_num*模块中的卷积数
layers.append(block(self.in_channel,
channel,
groups=self.groups,
width_per_group=self.width_per_group))
# Sequential:自定义顺序连接成模型,生成网络结构
return nn.Sequential(*layers)
# forward():定义前向传播过程,描述了各层之间的连接关系
def forward(self, x):
# 无论哪种ResNet,都需要的静态层
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
# 动态层
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
if self.include_top:
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
4.定义50层的resnet函数
# ResNet()中block参数对应的位置是BasicBlock或Bottleneck
# ResNet()中blocks_num[0-3]对应[3, 4, 6, 3],表示残差模块中的残差数
# 34层的resnet
def resnet34(num_classes, include_top=True):
# https://download.pytorch.org/models/resnet34-333f7ec4.pth
return ResNet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)
# ---------------4.定义50层的resnet函数--------------------------------
def resnet50(num_classes, include_top=True):
# https://download.pytorch.org/models/resnet50-19c8e357.pth
return ResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)
# 101层的resnet
def resnet101(num_classes, include_top=True):
# https://download.pytorch.org/models/resnet101-5d3b4d8f.pth
return ResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes, include_top=include_top)
# -------------------5.构造测试函数------------------------------
def main():
my_resnet = resnet50(num_classes=3)
test = torch.rand(1, 3, 448, 448)
output = my_resnet(test)
print("Output shape:", output.shape)
if __name__=="__main__":
main()
train训练代码
1.初始化,标准参数初始化
import os
import torch
import torchvision.transforms as transforms
from PIL import Image
from torch.utils.data import Dataset, random_split, DataLoader
import argparse
# 训练resnet34
from tqdm import tqdm
from ResNet import resnet50, ResNet
# ------------------------------1.初始化,标准参数初始化-------------------------------
def parse_opt():
parser = argparse.ArgumentParser()
parser.add_argument('--epochs',type=int,default=3,help='total training epochs')
parser.add_argument('--batch_size',type=int,default=16,help='size of each batch')
parser.add_argument('--learning_rate', type=int, default=0.2, help='size of learning_rate') # 添加批次大小参数
# --device "cuda:0,cuda:1" 启用多个设备
parser.add_argument('--device', default='cuda:0', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') # --device cuda:1
# 解析参数
opt = parser.parse_args()
return opt
opt = parse_opt() # 调用解析函数
epochs = opt.epochs # 训练的轮数
batch_size = opt.batch_size # 每个批次的样本数量
learning_rate = opt.learning_rate
device = torch.device(opt.device)
class CustomDataset(Dataset):
def __init__(self, img_dir, label_dir, transform=None):
self.img_dir = img_dir
self.label_dir = label_dir
self.transform = transform
self.img_labels = self.load_labels()
def load_labels(self):
label_map = {'cats': 0, 'dogs': 1,'bycicles': 2} #字典label_map 来定义标签与数字之间的映射关系
labels = [] #创建一个空列表 labels,用来存储从文件中读取到的标签的数字编码
for label_file in os.listdir(self.label_dir):
with open(os.path.join(self.label_dir, label_file), 'r') as f: #对于每个标签文件,使用 open() 打开文件并读取第一行内容readline()
line = f.readline().strip() #strip() 方法用于去掉行首和行尾的空白字符(包括换行符)
#从label_map 中获取当前行的标签对应的数字编码。如果当前行的内容不在 label_map 中,则返回-1
labels.append(label_map.get(line, -1))
return labels
def __len__(self):
return len(self.img_labels)
def __getitem__(self, idx):
#使用 os.path.join 将图像目录路径 (self.img_dir) 和图像文件名拼接起来。文件名格式为 idx (即从0开始计数),加上 .jpg 后缀。
#例如,如果idx为0,则img_path会是 D:\\Pycharm\\Pytorch_test\\数据集\\练手数据集\\train\\dogs\\0.jpg
img_path = os.path.join(self.img_dir, str(idx) + '.jpg')
image = Image.open(img_path).convert('RGB') #打开指定路径的图像文件,并将其转换为 RGB 模式。
label = self.img_labels[idx] #列表中获取当前图像的标签,idx 是当前图像的索引
if self.transform: #transform变量是在CustomDataset中最终调用的transforms.Compose实现
image = self.transform(image)
return image, label
# 定义转换,包括调整大小
transform = transforms.Compose([
transforms.Resize(448), # 将图像的短边调整为448,保持等比缩放
transforms.RandomResizedCrop(448, scale=(0.8, 1.0)), # 随机裁剪为448x448,并调整尺寸
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1), # 随机调整颜色
transforms.ToTensor(), # 转换为 Tensor
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # 归一化
# 可以添加其他转换
])
# 初始化数据集
dogs_data = CustomDataset(
img_dir='D:\\Pycharm\\ResNet\\dataset\\dogs',
label_dir='D:\\Pycharm\\ResNet\\dataset\\dogs_label',
transform=transform
)
cats_data = CustomDataset(
img_dir='D:\\Pycharm\\ResNet\\dataset\\cats',
label_dir='D:\\Pycharm\\ResNet\\dataset\\cats_label',
transform=transform
)
bycicles_data = CustomDataset(
img_dir='D:\\Pycharm\\ResNet\\dataset\\bycicles',
label_dir='D:\\Pycharm\\ResNet\\dataset\\bycicles_label',
transform=transform
)
2.将数据集总和并随机打乱并分成训练集测试集
#-----------------------2.将数据集总和并随机打乱并分成训练集测试集-------------------
# 合并数据集
total_data = dogs_data+cats_data+bycicles_data
# 计算训练集和验证集的大小
total_size = len(total_data)
train_size = int(0.7 * total_size) # 70%
val_size = total_size - train_size # 30%
train_data_size = train_size
val_data_size = val_size
print("训练集长度:{}".format(train_data_size))
print("测试集长度:{}".format(val_data_size))
# 随机分割数据集
train_data, val_data = random_split(total_data, [train_size, val_size])
# 创建 DataLoader,分成小批量(batches),以便于进行训练和验证
train_dataloader = DataLoader(train_data, batch_size, shuffle=True) #shuffle=True可以随机打乱数据
val_dataloader = DataLoader(val_data, batch_size, shuffle=False)
# 打印数据集大小
print("训练集大小:{}".format(len(train_data)))
print("验证集大小:{}".format(len(val_data)))
3.创建网络模型
#----------------------------3.创建网络模型--------------------------------
My_ResNet = resnet50(num_classes=3) #调用类
My_ResNet = My_ResNet.to(device)
4.训练加测试,训练这部分加入了预训练权重,它的好处在于不用从头开始训练,缩短训练时间。学习率随着训练次数增加递减。
#------------------------------------4.训练加测试-----------------------
import matplotlib
matplotlib.use('TkAgg') # 或者尝试 'Qt5Agg',有这行代码会多一个弹窗显示
import matplotlib.pyplot as plt
import torch.nn as nn
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
class CustomLearningRateScheduler:
def __init__(self, optimizer, learning_rate, decay_factor=0.4):
self.optimizer = optimizer
self.initial_lr = learning_rate
self.decay_factor = decay_factor
self.lr = learning_rate
def on_epoch_end(self, epoch):
# 每个 epoch 结束时将学习率降低
self.lr *= self.decay_factor
for param_group in self.optimizer.param_groups:
param_group['lr'] = self.lr
print(f"Epoch {epoch + 1}: Learning rate reduced to: {self.lr:.6f}")
def train():
best_accuracy = 0.0
# (1).损失函数构建
loss_fn = nn.CrossEntropyLoss() #交叉熵损失函数,计算预测值与真实标签之间的差异
loss_fn = loss_fn.to(device) #将模型和数据都放在同一个设备上,GPU
# (2).优化器
# #随机梯度下降(Stochastic Gradient Descent)优化器的一种实现。SGD 是一种常见的优化算法
optimizer = optim.SGD(My_ResNet.parameters(), lr=learning_rate, weight_decay=0.0001, momentum=0.9)
scheduler = CustomLearningRateScheduler(optimizer, learning_rate)
# 用于存储损失和准确率
train_loss = []
accuracies = []
test_loss = []
for i in range(epochs):
loss_temp = 0 # 临时变量
print("--------第{}轮训练开始--------".format(i + 1))
# 训练阶段
My_ResNet.train() # 设置为训练模式,用来管理Dropout方法:训练时使用Dropout方法,验证时不使用Dropout方法
# 使用 tqdm 创建一个进度条,len(train_dataloader) 是总的迭代次数
for data in tqdm(train_dataloader, desc="Training Epoch {}".format(i + 1), unit="batch"):
imgs, targets = data
imgs = imgs.to(device)
targets = targets.to(device)
# 将历史损失梯度清零
optimizer.zero_grad()
# 参数更新
# 因为采用了辅助分类器,得到了三个输出(主输出和两个辅助输出)
logits = My_ResNet(imgs) #训练模式,且开启了分类器 fail
loss = loss_fn(logits, targets)
# 优化器优化模型
loss.backward() # 反向传播
optimizer.step() # 梯度更新
# 将当前损失添加到 train_loss 列表
train_loss.append(loss.item())
#-----------------------测试阶段---------------------------------
# 测试阶段
My_ResNet.eval() # 设置为评估模式,关闭Dropout
total_test_loss = 0
total_accuracy = 0
# 验证过程中不计算损失梯度
with torch.no_grad():
# 初始化 test_loss 列表
for data in val_dataloader:
imgs, targets = data
imgs = imgs.to(device)
targets = targets.to(device)
outputs = My_ResNet(imgs)
loss = loss_fn(outputs, targets)
# 将当前损失添加到 test_loss 列表
test_loss.append(loss.item())
# 找到每个样本的预测类别,然后与真实标签进行比较
accuracy = (outputs.argmax(1) == targets).sum().item() # 使用.item()将Tensor转换为Python数值
total_accuracy += accuracy # 计算正确预测的数量并累加到 total_accuracy
# -----------------------5.保存数据至.pth文件---------------------------
# 如果当前测试集准确率大于历史最优准确率
if (total_accuracy / val_data_size) > best_accuracy:
# 更新历史最优准确率
best_accuracy = (total_accuracy / val_data_size)
# 保存当前权重
torch.save(My_ResNet, "ResNet_{}.pth".format(2))
print("模型已保存")
# 更新学习率
scheduler.on_epoch_end(i)
# 记录验证损失和准确率
accuracies.append(total_accuracy / val_data_size)
print("整体测试集上的正确率:{}".format(total_accuracy / val_data_size))
print("测试集上的Loss:{}".format(test_loss[-1]))
5.保存损失及正确率至xlsx
#-------------------------5.保存损失及正确率至xlsx----------------------
# 创建一个字典,用于存储数据
data = {
'accuracies': accuracies,
'test_loss': test_loss,
'train_loss': train_loss
}
# 将字典转换为 DataFrame
df = pd.DataFrame(dict([(k, pd.Series(v)) for k, v in data.items()]))
# 保存到 Excel 文件
df.to_excel('ResNet.xlsx', index=False)
6.绘制损失和准确率曲线
# ----------------------------6.绘制损失和准确率曲线---------------------------
if len(train_loss)>=len(test_loss):
epochs_range = range(1, len(test_loss) + 1)
else:
epochs_range = range(1, len(train_loss)+1)
plt.figure(figsize=(12, 5)) # 创建一个新的图形窗口,设置图形的大小
# 训练和验证损失
plt.subplot(1, 2, 1) # 第一个图像框
plt.plot(epochs_range, train_loss[:len(epochs_range)], label='Training Loss')
plt.plot(epochs_range, test_loss[:len(epochs_range)], label='Validation Loss')
plt.title('Loss vs Epochs')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
# 准确率
plt.subplot(1, 2, 2)
plt.plot(epochs_range[:len(accuracies)], accuracies[:len(accuracies)], label='Validation Accuracy')
plt.title('Accuracy vs Epochs')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()
plt.tight_layout()
plt.show()
if __name__ == '__main__':
parse_opt()
train()
detect
1.参数数据初始化
import os
import torch
import torchvision
from PIL import Image
import argparse
#--------------------------1.参数数据初始化-----------------------------------
from matplotlib import pyplot as plt
from ResNet import resnet50
def parse_opt():
parser = argparse.ArgumentParser() # 创建 ArgumentParser 对象
parser.add_argument('--Dropout_Val', type=int, default=0.5, help='Dropout_Val number') # 添加参数
#--device "cuda:0,cuda:1" 启用多个设备
parser.add_argument('--device', default='cuda:0', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') #--device cuda:1
# 解析参数
opt = parser.parse_args()
return opt
opt = parse_opt() # 调用解析函数
Dropout_Val = opt.Dropout_Val
device = torch.device(opt.device)
#确保你的图像预处理步骤(如归一化)与模型训练时使用的步骤相匹配。通常,GoogleNet 需要将图像归一化到 [0, 1] 范围内,
# 并可能需要减去均值和除以标准差
transform = torchvision.transforms.Compose([
torchvision.transforms.Resize((448, 448)),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
# 类别标签
class_names = ['cat', 'dog', 'bycicle'] # 根据实际情况填写
# 图片路径
image_folder = "/home/inspur/AI/lab302/ljj/ResNet/dataset/test_image" # 修改为你的路径
2.搭建ResNet网络结构
#-------------------------------------2.搭建ResNet网络结构--------------------------
model = resnet50(num_classes=3) #调用类
model = model.to(device)
3.输出测试结果
#--------------------------3.输出测试结果-----------------------------
model = torch.load("ResNet_2.pth")
#模型的权重在GPU上,而输入张量在CPU上。要解决这个问题,你可以将输入张量移动到GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
model.eval()
# 遍历图片文件夹并进行预测
for image_name in os.listdir(image_folder):
if image_name.endswith(('.jpg', '.png', '.jpeg')): # 只处理常见图片格式
image_path = os.path.join(image_folder, image_name)
image = Image.open(image_path).convert("RGB") # 打开并转换为RGB
image_tensor = transform(image).unsqueeze(0).to(device) # 预处理并添加批次维度
# 预测
with torch.no_grad():
output = model(image_tensor)
predicted_index = output.argmax(1).item() # 获取预测的索引
# 打印图片名及预测类型
print(f"Image: {image_name}, Predicted class: {class_names[predicted_index]}")
# 在SciView中显示图片
plt.figure(figsize=(6, 6))
plt.imshow(image) # 显示原始图片
plt.title(f"Predicted class: {class_names[predicted_index]}")
plt.axis('off') # 不显示坐标轴
plt.show() # 显示图片
实验效果
测试效果如图所示
训练图片
测试画图时,注意xlsx运行这个程序
测试
以上就是本人的心得与总结,如有不足之处请多多包涵。
百度网盘链接代码权值及测试图片,训练图片链接: https://pan.baidu.com/s/1agOOrKG0Bg5T2TXjirtZTg?pwd=wrvp
提取码: wrvp