利用卷积神经网络(CNN)进行花朵分类任务

news/2024/11/13 13:41:57/文章来源:https://www.cnblogs.com/lostin9772/p/18540683

一、卷积神经网络

卷积神经网络(Convolutional Neural Netword,CNN)是一种深度学习模型,它在图像识别、视频分析、自然语言处理等领域表现出色。CNN 的核心思想是利用卷积运算来提取输入数据的特征,并且能够保持空间层次结构。

卷积神经网络的架构如下:

我们今天的重点是利用卷积神经网络来实践一个花朵分类任务。首先我们来看一下我们的数据集:

我们将不同的花朵分类(102种)存入到不同的文件夹中,根据文件名来进行花朵分类,并且分为训练集和测试集两个数据部分,最后需要进行结果展示的时候,我们通过引入一个 json 文件来将不同文件名映射到对应的花朵名字:

然后我们训练的模型采用 ResNet 网络结构,它是一种深度卷积神经网络,旨在解决深度学习中的退化问题。该网络结构通过引入“快捷连接”(Shortcut connection)来优化训练过程,允许网络层之间的直接连接,从而有效的训练更深层的网络结构。

由于其较浅的网络结构和较少的参数,常用于需要较快速度和较少计算资源的场景。同时它也是许多计算机视觉任务的基础网络结构,如图像分类、目标检测和图像分割。在实际应用中,ResNet 可以通过预训练模型进行迁移学习,快速适应新的数据集和任务。

下面是 ResNet 的网络结构图:

常用的 ResNet 网络结构有 18、50、101、152 层等,出于演示效率考虑,我们本次训练中采用 18 层的 ResNet 网络结构,即 resnet18,并采用迁移学习的方法:

  1. 首先替换模型的全连接层,以适应我们自己的分类任务
  2. 在第一次训练中,我们冻结除全连接层外的所有其它层参数(不进行参数更新),利用其它层训练好的参数作为初始参数,随机化我们自己添加的全连接层参数,并对其进行训练
  3. 在第二次训练中,我们解冻除全连接层外的所以其他参数(进行参数更新),并结合我们第一次训练中训练好的参数一起作为初始参数,对整个模型的参数进行重新训练

二、搭建模型并训练

1 导入数据并对数据进行预处理

我们首先在代码中假如如下语句,如有 GPU 则用 GPU 训练,这样能大大提高训练的速度:

# 如有 GPU 则用 GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

导入图片数据的路径:

# 定义图片数据的路径
data_dir = './data/CNN/flower_data/'
train_dir = data_dir + '/train'
valid_dir = data_dir + '/valid'

接下来我们对图片先进行数据预处理:

# 定义需要对图片数据进行的预处理操作
data_transforms = {'train':transforms.Compose([  # 一个操作组合transforms.Resize([96, 96]),  # 将所有图片调整为 96*96 的大小(考虑性能的情况下越大越好)# 根据实际情况设置大小,大部分经典网络用正方形结构transforms.RandomRotation(45),  # 随机旋转,表示从 -45 到 45 度之间随机选角度旋转transforms.CenterCrop(64),  # 从中心开始裁剪成 64*64 大小的图片(一般裁剪成 64、128、224、256 大小)transforms.RandomHorizontalFlip(p=0.5),  # 随机水平翻转,翻转概率为 50%transforms.RandomVerticalFlip(p=0.5),  # 随机垂直翻转# transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),    # 亮度、对比度、饱和度、色相(用的少)# transforms.RandomGrayscale(p=0.025),    # 概率转换成灰度率,3 通道就是 R=G=B(用的少)transforms.ToTensor(),  # 将数据转换为 Tensor 格式transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])  # 设置均值,标准差(R,G,B)]),'valid':transforms.Compose([transforms.Resize([64, 64]),transforms.ToTensor(),# 测试数据和训练数据的均值和标准差需要一致transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),}# 加载数据集并对不同数据集进行对应的预处理操作
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),data_transforms[x]) for x in ['train', 'valid']}# 设置 DataLoader
batch_size = 128  # 由于输入图片较小,所以 batch 可以指定大一点
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=batch_size,shuffle=True) for x in ['train', 'valid']}

在上面的代码中,data_transform 中制定了所有图像预处理操作(比如调整大小、数据增强),ImageFolder 假设所有的文件按文件夹保存好,每个文件夹下面存储同一类别的图片,文件夹的名字为分类的名字,对训练集和验证集都进行预处理操作,然后将二者设置好 batch 大小加载到 dataloader 里面。

2 构建模型

接下来我们开始构建模型:

# 模型初始化
params_to_update = []    # 保存需要更新的参数列表
def initialize_model(model_name, num_classes, feature_extract, use_pretrained=True):global params_to_update    # 在函数内部声明全局变量# 从 models 模块中动态选择特定模型,并使用预训练权重model_ft = getattr(models, model_name)(pretrained=True)# model_ft = models.resnet18(pretrained=use_pretrained)  # 选择模型,18 层的比较快(18,50,101,152)# 如果需要进行迁移学习则先将所有参数设置为不更新if feature_extract:for param in model_ft.parameters():param.requires_grad = False# 该模型输出层是个 1000 分类,需要修改全连接层,所以需要获取模型中全连接层的输入特征数num_ftrs = model_ft.fc.in_features# 更改全连接层,输出类别数根据自己任务更改:102,更改后新层的 requires_grad 属性默认是 Truemodel_ft.fc = nn.Linear(num_ftrs, num_classes)# 打印需要更新梯度的参数params_to_update = [param for param in model_ft.parameters() if param.requires_grad]print('Params to learn:')for name, param in model_ft.named_parameters():if param.requires_grad:print("\t", name)return model_ft# 选择的模型名字
model_name = 'resnet18'  # 可选 resnet,alexnet,vgg,squeezenet,densenet,inception 等# 获取数据的总类别
num_classes = len(image_datasets['train'].classes)  # 注意类别排序为 1,10,100,101...,非默认从零开始# 只更新输出层权重,其他层权重冻住(指示是否仅提取特征,即冻住模型的某些层)
# 迁移学习:照搬别人的模型以及将训练好的权重作为我们的初始化,并根据数据量冻住一些层
feature_extract = True# 模型初始化并传入 GPU 或 CPU
model_ft = initialize_model(model_name, num_classes, feature_extract, use_pretrained=True).to(device)

这段代码的目的是根据给定的参数初始化一个预训练的模型,并对其进行必要的修改以适应新的分类任务。通过替换全连接层,模型可以输出正确数量的类别。同时,如果 feature_extract 为 True,则模型的参数在训练过程中不会更新,这在迁移学习中很常见,即利用预训练模型的特征提取能力,只训练顶层分类器。

3 设定模型的超参数

接下来我们需要为模型设定超参数,分别是迭代次数优化器学习率损失函数

epochs = 30    # 迭代次数
optimizer_ft = optim.Adam(params_to_update, lr=1e-2)    # 设置优化器
# 设置学习率衰减(学习率每 10 个 epoch 衰减成原来的 1/10)
scheduler = optim.lr_scheduler.StepLR(optimizer_ft, step_size=10, gamma=0.1)
criterion = nn.CrossEntropyLoss()    # 设置损失函数

值得注意的是我们不是采用固定的学习率,而是使用学习率衰减(Learning Rate Decay)策略,设置学习率衰减有以下好处:

  1. 避免震荡:在训练初期,如果学习率过高,模型的权重更新可能会过大,导致训练过程中的损失函数值出现较大震荡,难以稳定下降。通过学习率衰减,可以逐步减小更新步长,使模型在训练后期更加稳定。
  2. 提高收敛速度:在训练初期,较大的学习率可以帮助模型快速逃离局部最小值,但随着训练的进行,需要更小的学习率来细致地逼近全局最小值。

4 训练模型并保存参数

接下来我们就可以开始进行训练了:

# 模型参数的保存路径
filename = './data/CNN/best.pt'
def train_model(model, dataloaders, criterion, optimizer, epochs, filename):# 记录测试集中最高的准确率best_acc = 0# 记录训练中在测试集上准确率最高的模型参数best_model_wts = {}# 记录训练过程中训练集和验证集的历史损失和准确率train_losses_history = []train_acc_history = []valid_losses_history = []val_acc_history = []# 初始化了一个列表 LRs,用于存储每个训练周期(epoch)开始时的学习率# 该行代码获取的是优化器(optimizer)中第一个参数组的初始学习率LRs = [optimizer.param_groups[0]['lr']]# 获取 Unix 时间戳,记录开始训练的时间since = time.time()# 开始迭代训练,每个 epoch 先跑训练集再跑测试集for epoch in range(epochs):print('Epoch {}/{}'.format(epoch, epochs - 1))print('-' * 10)# 训练和验证for phase in ['train', 'valid']:if phase == 'train':model.train()  # 训练(默认行为,可以不需要显示的调用 model.train())else:model.eval()  # 验证running_loss = 0.0running_corrects = 0# 每次取一个 batch,直到跑完一个 epochfor inputs, labels in dataloaders[phase]:# 放到 CPU 或 GPUinputs = inputs.to(device)labels = labels.to(device)# 清零optimizer.zero_grad()# 只有训练的时候计算和更新梯度outputs = model(inputs)loss = criterion(outputs, labels)  # 注意这里的 loss 所有样本损失的平均值# 获取模型输出中每个样本最可能属于的类别_, preds = torch.max(outputs, 1)# 训练阶段更新权重if phase == 'train':loss.backward()optimizer.step()# 对每个 batch 的损失和正确总数进行累加running_loss += loss.item() * inputs.size(0)  # 得到整个批次的总损失,其中 0 表示 batch 那个维度running_corrects += torch.sum(preds == labels.data)  # 预测结果最大的和真实值是否一致# 计算每个 epoch 的平均损失和准确率epoch_loss = running_loss / len(dataloaders[phase].dataset)epoch_acc = running_corrects / len(dataloaders[phase].dataset)# 到目前为止已经花费的时间time_elapsed = time.time() - since# 打印花费的时间和训练集或测试集的损失和准确率print('Time elapsed {:0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))# 保存验证集上准确率最高的模型参数和准确率if phase == 'valid' and epoch_acc > best_acc:best_acc = epoch_accbest_model_wts = model.state_dict()state = {  # 字典里 Key 就是各层的名字,值就是训练好的权重'state_dict': model.state_dict(),'best_acc': best_acc,# 为了在需要时能够完全恢复训练过程,包括模型参数和优化器内部的# 状态,我们也需要保存优化器的状态'optimizer': optimizer.state_dict(),}torch.save(state, filename)# 保存训练集和验证集的历史损失和准确率if phase == 'train':train_acc_history.append(epoch_acc)train_losses_history.append(epoch_loss)if phase == 'valid':val_acc_history.append(epoch_acc)valid_losses_history.append(epoch_loss)# 在每个训练周期结束时,将当前的学习率添加到 LRs 列表中,以便跟踪学习率的变化print('Optimizer learning rate : {:.7f}'.format(optimizer.param_groups[0]['lr']))LRs.append(optimizer.param_groups[0]['lr'])# 调用学习率调度器,在每个训练周期结束后更新学习率scheduler.step()# 打印花费的总时间以及最高的准确率time_elapsed = time.time() - sinceprint('Training compete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))print('Best val Acc: {:4f}'.format(best_acc))# 训练完后用最好的一次当做模型的最终测试结果,等着一会测试model.load_state_dict(best_model_wts)return model, val_acc_history, train_acc_history, valid_losses_history, train_losses_history, LRs# 开始训练
model_ft, val_acc_history, train_acc_history, valid_losses_history, train_losses_history, LRs = \train_model(model_ft, dataloaders, criterion, optimizer_ft, epochs, filename)

训练完成后的结果如下:

迭代 30 个 epoch 后,在训练集上的准确率达到 67.55%,在验证集上的准确率达到 40.71%,模型训练中,在验证集上出现的最高准确率为 40.8313%,这是我们只训练自己定义的全连接层后的效果,接下来我们解冻其它层的所有参数,继续训练。

5 解冻其他层参数结合 fc 层训练好的参数一起重新训练

# 设置更新模型的所有参数
for param in model_ft.parameters():param.requires_grad = True# 设置模型的超参数
epochs = 30    # 迭代次数
optimizer_ft = optim.Adam(model_ft.parameters(), lr=1e-3)    # 优化器
scheduler = optim.lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)    # 学习率调小一点
criterion = nn.CrossEntropyLoss()    # 损失函数# 加载之前训练好的权重参数
checkpoint = torch.load(filename)
best_acc = checkpoint['best_acc']
model_ft.load_state_dict(checkpoint['state_dict'])# 再次开始重新训练
model_ft, val_acc_history, train_acc_history, valid_losses_history, train_losses_history, LRs = \train_model(model_ft, dataloaders, criterion, optimizer_ft, epochs, filename)

训练的结果如下:

我们可以看到,在解冻其他层的所有参数后进行训练,模型的准确率有了较大的提升,在训练集上达到了 98.03%,测试集上达到了 72.25%,在测试集上最高的准确率达到了 73.3496%,最后我们加载训练好的模型参数用来进行预测。

6 加载模型和训练好的参数用于预测

接下来我们随机从 dataloader 中得到一个 batch 的测试数据用于预测:

# 如有 GPU 则用 GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")# 初始化模型并将模型加载到 GPU 或 CPU
model_ft = initialize_model(model_name, num_classes, feature_extract, use_pretrained=True).to(device)# 加载模型参数
checkpoint = torch.load('./data/CNN/best.pt')
best_acc = checkpoint['best_acc']
model_ft.load_state_dict(checkpoint['state_dict'])# 将模型设置为评估模式
model_ft.eval()# 从 dataloader 中随机得到一个 batch 的测试数据
dataiter = iter(dataloaders['valid'])
images, labels = dataiter.next()# 将数据传入到 GPU 或 CPU
train_on_gpu = torch.cuda.is_available()
if train_on_gpu:output = model_ft(images.cuda())
else:output = model_ft(images)# 从预测值中获得每行最大值及其索引
_, preds_tensor = torch.max(output, 1)# squeeze 用于去除 numpy 数组中所有长度为1的维度(这里将一个二维数组 (batch, 1) 转换为一个一维数组)
preds = np.squeeze(preds_tensor.numpy()) if not train_on_gpu \else np.squeeze(preds_tensor.cpu().numpy())# 图像张量转换函数
def im_convert(tensor):# 将张量移动到 CPU,创建一个副本,并从计算图中分离,使其可以转换为 NumPy 数组image = tensor.to("cpu").clone().detach()# 将张量转换为 NumPy 数组,并去除长度为1的维度image = image.numpy().squeeze()# 调整数组的维度顺序,以匹配图像显示库的期望格式(通常是高度、宽度、通道)image = image.transpose(1, 2, 0)    # torch 中 3*64*64 是(0, 1, 2),其他工具包通道顺序不一样# 对图像进行反标准化,将其转换回原始的像素值范围。这里使用的是 ImageNet 数据集的标准差和均值(标准化是RGB值-均值再除以标准差)image = image * np.array((0.229, 0.224, 0.225)) + np.array((0.485, 0.456, 0.406))# 将图像的像素值限制在 [0, 1] 范围内,确保它们是有效的像素值image = image.clip(0, 1)return image# 创建一个大小为 20x20 英寸的图表
fig = plt.figure(figsize=(20, 20))# 设置图表的列数和行数
columns = 4
rows = 2# 读取标签对应的实际名字
with open('./data/CNN/cat_to_name.json', 'r') as f:cat_to_name = json.load(f)# 将 tensor 图像转换格式后添加到子图中
for idx in range (columns * rows):# 在图表中添加一个子图,并移除 x 和 y 轴的刻度ax = fig.add_subplot(rows, columns, idx+1, xticks=[], yticks=[])# 使用 im_convert 函数将图像张量转换为显示格式,并显示在子图上plt.imshow(im_convert(images[idx]))# 设置子图的标题,显示预测的类别和实际的类别。如果预测正确,则标题颜色为绿色;如果预测错误,则标题颜色为红色ax.set_title("{} ({})".format(cat_to_name[str(preds[idx])], cat_to_name[str(labels[idx].item())]),color=("green" if cat_to_name[str(preds[idx])] == cat_to_name[str(labels[idx].item())] else "red"))# 显示图表,展示所有图像及其预测和实际类别
plt.show()

预测的结果如下:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/832162.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

南沙C++信奥赛老师解一本通题 1385:团伙(group)

​ 【题目描述】在某城市里住着n个人,任何两个认识的人不是朋友就是敌人,而且满足: 1、我朋友的朋友是我的朋友; 2、我敌人的敌人是我的朋友; 所有是朋友的人组成一个团伙。告诉你关于这n个人的m条信息,即某两个人是朋友,或者某两个人是敌人,请你编写一个程序,计算出这…

Docker:部署kkFileView所有格式文档在线预览服务

前言 kkFileView是一个文档在线预览服务,基本支持主流文档格式预览,目前支持的文件类型如下:支持 doc, docx, xls, xlsx, xlsm, ppt, pptx, csv, tsv, dotm, xlt, xltm, dot, dotx,xlam, xla 等 Office 办公文档 支持 wps, dps, et, ett, wpt 等国产 WPS Office 办公文档 支…

学校厕所防欺凌检测系统

学校厕所防欺凌检测系统通过在关键区域安装的音频和视频监控设备,学校厕所防欺凌检测系统实时捕捉现场的声音和画面。AI音频分析技术能够对前端音频进行实时处理,当系统识别到“救命”、“打架”、“老师快来”等敏感词汇时,会自动触发预警机制,联动值班老师或校园安全中心…

物流园区烟火烟雾检测系统

物流园区烟火烟雾检测系统通过在园区关键位置安装的高清摄像头,物流园区烟火烟雾检测系统实现对监控区域的无人值守和不间断工作。系统利用先进的AI视觉算法,能够主动发现监控区域内的烟雾和火灾苗头,并进行实时分析报警。与传统的火灾监测系统相比,该系统不需要依赖其他传…

错误代码的个人见解以及逻辑分析题

一、代码错误分析代码中的错误: 1.src 指针指向字符串字面值,不可修改: 字符串 "hello,world" 是存储在只读区域的常量字符串,不能通过指针直接修改。 如果需要倒序操作,需要把字符串复制到一个可修改的内存中。2.dest 未正确分配内存: 在 malloc(len) 时,没有…

docx 生成word报告

# -*- coding: utf-8 -*- import base64 import os from io import BytesIO from docx import Document from docx.shared import Inches, Pt from bs4 import BeautifulSoup from matplotlib import pyplot as plt from wordcloud import WordCloud # 设置全局字体 plt.rcPara…

leetcode算法题-有效的括号(简单)

有效的括号(简单) leetcode:https://leetcode.cn/problems/valid-parentheses/description/ 前言 防止脑袋生锈,做一下leetcode的简单算法题,难得也做不来哈哈。 大佬绕道,小白可看。 题目描述 给定一个只包括 (,),{,},[,] 的字符串 s ,判断字符串是否有效。 有效字符…

30+企业高管齐聚!医疗器械企业渠道优化与健康增长主题沙龙成功举办

10月29日,深圳医疗器械行业协会携手纷享销客,共同举办了一场以“渠道优化与健康增长”为主题,探索医疗器械企业在新形势下渠道管理及落地实践的沙龙活动。此次活动吸引了33位医疗器械企业的管理层,共同探寻医疗器械企业营销增长的新思路、新渠道与新路径。<活动照片>…

AutoCAD Blockview .net在wpf项目中的问题

之前使用Blockview是遇到平移的问题, 这几天在学习使用CommunityToolkit.MVVM框架来创建用户界面, 当创建GsPreviewCtrl控件时会遇到错误, 导致整个窗体不能显示, 错误信息如下:************** 异常文本 ************** System.InvalidProgramException: 公共语言运行时检…

html`` - function html(str) { return str+111 } 调用方式 - solidjs文档里面发现的

html`` - function html(str) { return str+111 } 调用方式 标签模板字符串Tagged Template Literals 这里是自己实现这个字符串模板,等于函数调用的另一种方式 html(111) html`111`solidjs文档里面发现的 https://www.solidjs.com/guides/getting-started#不使用构建工具----…

模态内重叠优化,简单有效的CLIP微调方法 | BMVC24 Oral

来源:晓飞的算法工程笔记 公众号,转载请注明出处论文: CLIP Adaptation by Intra-modal Overlap Reduction论文地址:https://arxiv.org/abs/2409.11338创新点提出一种基于轻量级适配的新方法,直接在图像空间中减少CLIP中的模态内重叠(IMO)。新特征与任何利用缓存模型的无…

一文了解:如何多纬度阐述数据安全传输问题,部署及解决方案!

企业的业务正常开展依赖安全有序的数据流转,数据传输环节融合在企业生产办公、日常经营、技术研究、战略发展等活动的方方面面。数据是任何企业的命脉,但企业数据在传输过程中仍然面临着监管机制不健全、传输主体涉及面广、网络环境复杂、攻击手段多样、数据泄露引发多米诺骨…