深度学习——狗的品种识别(ImageNet Dogs)
文章目录
前言
做一下kaggle中的狗的品种识别(Dog Breed Identification)
一、狗的品种识别
比赛数据集分为训练集和测试集,分别包含RGB(彩色)通道的10222张、10357张JPEG图像。
在训练数据集中,有120种犬类,如拉布拉多、贵宾、腊肠、萨摩耶、哈士奇、吉娃娃和约克夏等。
1.1.数据获取
同样登录kaggle之后,可以单击“Dog Breed Identification"竞赛页面上的”data"选项卡,然后单击“Download ALL"按钮下载数据集。
将下载的数据集压缩文件导入自己kaggle的自定义的notebook中:
导入成功
1.2. 划分数据集
#狗的品种识别
import collections
import math
import os
import shutil
import pandas as pd
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
data_dir = "/kaggle/input/wht-dog-breed-identification/"
def read_csv_labels(fname):
"""读取fname来给标签字典返回一个文件名"""
with open(fname, 'r') as f:
# 跳过文件头行(列名)
lines = f.readlines()[1:]
tokens = [l.rstrip().split(',') for l in lines]
return dict(((name, label) for name, label in tokens))
labels = read_csv_labels(os.path.join(data_dir, 'labels.csv'))
print('# 训练样本 :', len(labels))
print('# 类别 :', len(set(labels.values())))
target_dir = '/kaggle/working/my_directory'
def copyfile(filename, target_dir):
"""将文件复制到目标目录"""
os.makedirs(target_dir, exist_ok=True)
shutil.copy(filename, target_dir)
def reorg_train_valid(data_dir, labels, valid_ratio):
"""将验证集从原始的训练集中拆分出来"""
# 训练数据集中样本最少的类别中的样本数
n = collections.Counter(labels.values()).most_common()[-1][1]
# 验证集中每个类别的样本数
n_valid_per_label = max(1, math.floor(n * valid_ratio))
label_count = {}
for train_file in os.listdir(os.path.join(data_dir, 'train')):
label = labels[train_file.split('.')[0]]
fname = os.path.join(data_dir, 'train', train_file)
copyfile(fname, os.path.join(target_dir, 'train_valid_test',
'train_valid', label))
if label not in label_count or label_count[label] < n_valid_per_label:
copyfile(fname, os.path.join(target_dir, 'train_valid_test',
'valid', label))
label_count[label] = label_count.get(label, 0) + 1
else:
copyfile(fname, os.path.join(target_dir, 'train_valid_test',
'train', label))
return n_valid_per_label
def reorg_test(data_dir):
"""在预测期间整理测试集,以方便读取"""
for test_file in os.listdir(os.path.join(data_dir, 'test')):
copyfile(os.path.join(data_dir, 'test', test_file),
os.path.join(target_dir, 'train_valid_test', 'test',
'unknown'))
def reorg_dog_data(data_dir,valid_ratio):
labels = read_csv_labels(os.path.join(data_dir,"labels.csv"))
reorg_train_valid(data_dir,labels,valid_ratio)
reorg_test(data_dir)
batch_size = 128
valid_ratio = 0.1
reorg_dog_data(data_dir, valid_ratio)
跟之前一样,将得到的图片数据集进行整理,然后从训练集中划分验证集(这里的比率为0.1)
1.3. 定义图像预处理
这个狗品种数据集是ImageNet数据集的子集,其图像大于kaggle_cifar10中CIFAR-10数据集的图像。
transform_train = torchvision.transforms.Compose([
# 随机裁剪图像,所得图像为原始面积的0.08~1之间,高宽比在3/4和4/3之间。
# 然后,缩放图像以创建224x224的新图像
torchvision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0),
ratio=(3.0/4.0, 4.0/3.0)),
torchvision.transforms.RandomHorizontalFlip(),
# 随机更改亮度,对比度和饱和度
torchvision.transforms.ColorJitter(brightness=0.4,
contrast=0.4,
saturation=0.4),
#转换为张量格式
torchvision.transforms.ToTensor(),
# 标准化图像的每个通道
torchvision.transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
#测试时,我们只使用确定性的图像预处理操作
transform_test = torchvision.transforms.Compose([
torchvision.transforms.Resize(256),
# 从图像中心裁切224x224大小的图片
torchvision.transforms.CenterCrop(224),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
使用图像增广技术来对训练集进行数据增强,转换为张量格式,标准化图像的每个通道。对于非训练集则是只使用确定性的图像预处理操作。
1.4. 导入整理后的数据集
#读取整理后的含原始图像文件的数据集
train_ds, train_valid_ds = [torchvision.datasets.ImageFolder(
os.path.join(target_dir, 'train_valid_test', folder),
transform=transform_train) for folder in ['train', 'train_valid']]
"""
ImageFolder是一个用于处理包含图像文件的文件夹结构的类。
通过指定数据集文件夹的路径和变换操作(transform),可以创建一个ImageFolder对象。
"""
valid_ds, test_ds = [torchvision.datasets.ImageFolder(
os.path.join(target_dir, 'train_valid_test', folder),
transform=transform_test) for folder in ['valid', 'test']]
#使用DataLoader类转换为数据迭代器,drop_last=True表示如果最后一个批次的数据不足一个批量大小,则丢弃。
train_iter, train_valid_iter = [torch.utils.data.DataLoader(
dataset, batch_size, shuffle=True, drop_last=True)
for dataset in (train_ds, train_valid_ds)]
valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False,
drop_last=False)
test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False,
drop_last=False)
使用DataLoader函数来生成数据迭代器。(训练集每一代要使用shuffle函数打乱图片顺序,从总的训练集中随机选取batch_size批量大小
1.5. 微调预训练模型
def get_net(devices):
finetune_net = nn.Sequential() #创建一个空的顺序模型,用于存储整个网络结构
finetune_net.features = torchvision.models.resnet34(pretrained=True)
# 定义一个新的输出网络,共有120个输出类别
finetune_net.output_new = nn.Sequential(nn.Linear(1000, 256),
nn.ReLU(),
nn.Linear(256, 120))
# 将模型参数分配给用于计算的CPU或GPU
finetune_net = finetune_net.to(devices[0])
# 冻结特征提取部分的参数,即将其设置为不需要梯度计算,这样做是为了保持预训练的特征提取部分的权重不变,只训练新添加的输出网络部分
for param in finetune_net.features.parameters():
param.requires_grad = False
return finetune_net
定义一个微调预训练模型的函数:使用resnet34预训练模型,冻结特征提取部分的参数,只对自己设置的后面的输出网络进行权重更新。
1.6. 定义计算损失值的函数
#在[计算损失]之前,我们首先获取预训练模型的输出层的输入,即提取的特征。
#然后我们使用此特征作为我们小型自定义输出网络的输入来计算损失。
loss = nn.CrossEntropyLoss(reduction='none')
#reduction='none'表示不对损失进行平均或求和,而是返回每个样本的损失值。
def evaluate_loss(data_iter, net, devices):
l_sum, n = 0.0, 0
for features, labels in data_iter:
features, labels = features.to(devices[0]), labels.to(devices[0])
outputs = net(features)
l = loss(outputs, labels)
l_sum += l.sum()
n += labels.numel() #累加样本数量到n中,labels.numel()返回标签张量中元素的个数
return (l_sum / n).to('cpu') #计算所有样本的平均损失值,并将其移动到CPU上返回
使用交叉熵损失函数,定义损失值评估函数
1.7. 定义训练函数
#我们将根据模型在验证集上的表现选择模型并调整超参数。 模型训练函数train只迭代小型自定义输出网络的参数
def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
lr_decay):
# 只训练小型自定义输出网络
net = nn.DataParallel(net, device_ids=devices).to(devices[0]) #nn.DataParallel是为了支持多GPU训练
trainer = torch.optim.SGD((param for param in net.parameters()
if param.requires_grad), lr=lr,
momentum=0.9, weight_decay=wd) #momentum表示动量参数
scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_period, lr_decay) #创建一个学习率调度器scheduler,用于动态调整学习率
num_batches, timer = len(train_iter), d2l.Timer() #获取训练数据迭代器train_iter的总批次数,并创建一个计时器timer用于计算训练时间。
legend = ['train loss', 'train acc']
if valid_iter is not None:
legend += ['valid loss', 'valid acc']
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=legend)
for epoch in range(num_epochs): #开始进行epoch的循环
metric = d2l.Accumulator(3) #创建一个累加器metric
for i, (features, labels) in enumerate(train_iter):
timer.start()
# l , acc = d2l.train_batch_ch13(net,features,labels,loss,trainer,devices)
features, labels = features.to(devices[0]), labels.to(devices[0])
trainer.zero_grad()
output = net(features)
l = loss(output, labels).sum()
l.backward() #进行反向传播,计算梯度
trainer.step() #根据梯度更新模型参数
metric.add(l, labels.shape[0],d2l.accuracy(output,labels)) #将当前批次的损失值和样本数量添加到累加器中
timer.stop()
#每经过num_batches // 5个批次或者是最后一个批次时
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(metric[0] / metric[1],metric[2]/metric[1],None,None))
measures = f'train loss {metric[0] / metric[1]:.3f},train acc {metric[2]/metric[1]:.3f}'
if valid_iter is not None:
valid_loss,valid_acc = evaluate_loss(valid_iter,net,devices),d2l.evaluate_accuracy_gpu(net,valid_iter)
animator.add(epoch + 1, (None,None, valid_loss.detach(), valid_acc))
scheduler.step() #更新学习率
if valid_iter is not None:
measures += f', valid acc {valid_acc:.3f},valid loss {valid_loss:.3f}'
print(measures + f'\n{metric[1] * num_epochs / timer.sum():.1f}'
f' examples/sec on {str(devices)}')
定义训练函数,生成训练损失、训练集上的精确度、验证集损失、验证集上的精确度的动态更新曲线,返回具体值。
1.8. 开始训练
"""
现在我们可以训练和验证模型了,以下超参数都是可调的。 例如,我们可以增加迭代轮数。
"""
devices, num_epochs, lr, wd = d2l.try_all_gpus(), 30, 1e-5, 1e-5
lr_period, lr_decay, net = 2, 0.9, get_net(devices)
train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
lr_decay)
import os
model_path = '/kaggle/working/model.pth'
#torch.save(net.state_dict(), model_path)
torch.save(net, model_path)
print(f"Model saved to {model_path}")
设置超参数,开始训练(在这一步,要不断调参)。之后将训练好的模型参数保存
结果:
1.9. 效果评估
- 查看每一类在验证集上的准确率
import os
class_to_idx = {} #创建了一个空字典class_to_idx,用于存储每个类别的索引
# 遍历数据集文件夹中的子文件夹(每个子文件夹代表一个类别)
for idx, class_name in enumerate(sorted(os.listdir(os.path.join(target_dir, 'train_valid_test', 'valid')))):
if class_name.startswith('.'):
continue
class_dir = os.path.join(os.path.join(target_dir, 'train_valid_test', 'valid'), class_name) # 类别文件夹路径
#如果文件夹存在,则将该类别的索引和名称存储在class_to_idx字典中
if os.path.isdir(class_dir):
class_to_idx[idx] = class_name
#print(class_to_idx)
print("============================")
# 查看每一类在验证集上的准确率
classes = class_to_idx
print(classes)
print(len(classes))
class_correct = [0.] * 120
class_total = [0.] * 120
y_test, y_pred = [], [] #真实标签何预测结果
X_test = [] #存储输入的数据
#使用torch.no_grad()上下文管理器来禁用梯度计算
with torch.no_grad():
#在每次迭代中,valid_iter会返回一组大小为batch_size的图像数据和对应的标签
for images, labels in valid_iter:
X_test.extend([_ for _ in images])
outputs = net(images.to(devices[0]))
_, predicted = torch.max(outputs, 1) #在每一行中计算最大值,predicted返回最大值的索引(即标签)
predicted = predicted.cpu()
c = (predicted == labels).squeeze() #通过squeeze函数将其转换为一个长度为batch_size的tensor
for i, label in enumerate(labels):
class_correct[label] += c[i].item() #通过c[i].item()将tensor c中的第i个元素转换为一个标量值(即0或1)
class_total[label] += 1
#将预测结果(predicted)和真实标签(labels)转换为numpy数组
y_pred.extend(predicted.numpy())
y_test.extend(labels.cpu().numpy())
for i in range(120):
if class_total[i] != 0:
accuracy = 100 * class_correct[i] / class_total[i]
else:
accuracy = 0
print(f"Accuracy of {classes[i]:5s}: {accuracy:2.0f}%")
- 分类报告
try:
# 将类别名称列表作为target_names参数传递给classification_report函数
cr = classification_report(y_test, y_pred, target_names=list(classes.values())
print(cr)
except Exception as e:
print("An error:", str(e))
#print(y_test,len(y_test))
#print(y_pred,len(y_pred))
- 混淆矩阵
cm = confusion_matrix(y_test, y_pred)
import seaborn as sns, pandas as pd
labels = pd.DataFrame(cm).applymap(lambda v: f"{v}" if v!=0 else f"")
d2l.plt.figure(figsize=(25,20))
sns.heatmap(cm, annot=labels, fmt='s', xticklabels=classes.items(), yticklabels=classes.items(), linewidths=0.1 )
d2l.plt.show()
二、转为ONNX格式
当需要将PyTorch模型部署到其他框架或设备上时,可以使用ONNX格式来转换模型
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
# 1. 导出onnx模型
INPUT_DICT = r"D:\python\PycharmProjects\pythonProject1\算法\深度学习\PYTORCH学习\model.pth"
OUT_ONNX = 'best.onnx'
x = torch.randn(1, 3, 224, 224) # 定义一个随机的输入张量,用于导出模型。
input_names = ["input"]
out_names = ["output"]
def get_net():
finetune_net = nn.Sequential() # 创建一个空的顺序模型,用于存储整个网络结构
finetune_net.features = torchvision.models.resnet34(pretrained=True)
# 定义一个新的输出网络,共有120个输出类别
finetune_net.output_new = nn.Sequential(nn.Linear(1000, 256),
nn.ReLU(),
nn.Linear(256, 120))
# 将模型参数分配给用于计算的CPU或GPU
# finetune_net = finetune_net.to(devices[0])
# 冻结特征提取部分的参数,即将其设置为不需要梯度计算,这样做是为了保持预训练的特征提取部分的权重不变,只训练新添加的输出网络部分
for param in finetune_net.features.parameters():
param.requires_grad = False
return finetune_net
model = get_net()
model.load_state_dict(torch.load(INPUT_DICT,map_location=torch.device("cpu")))
# model = torch.load(INPUT_DICT, map_location=torch.device('cpu')) # 加载PyTorch模型,使用CPU进行推理
model.eval() # 将模型设置为评估模式
from torch.onnx import TrainingMode
torch.onnx._export(model, x, OUT_ONNX, export_params=True, training=TrainingMode.EVAL, input_names=input_names,
output_names=out_names, opset_version=11, do_constant_folding=True)
在终端运行onnxsim,来简化ONNX模型
python -m onnxsim best.onnx best_sim.onnx
。。。
总结
本章主要学习了如何将pytorch模型转换为ONNX格式,方便部署到其他框架或设备上。至于前面的狗的品种识别也只是运用到了resent34这个预训练模型。
千山鸟飞绝,万径人踪灭。
–2023-11-3 进阶篇