深度学习——狗的品种识别(ImageNet Dogs)

深度学习——狗的品种识别(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. 效果评估

  1. 查看每一类在验证集上的准确率

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}%")

在这里插入图片描述

  1. 分类报告
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))

在这里插入图片描述

  1. 混淆矩阵
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 进阶篇

  • 5
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
好的,实现猫识别任务需要进行迁移学习,使用预训练的VGG16模型可以帮助我们快速搭建一个高效的卷积神经网络。 以下是基本的实现步骤: 1. 导入相关库 ```python import os import numpy as np from keras.preprocessing.image import load_img, img_to_array from keras.applications.vgg16 import VGG16, preprocess_input from keras.models import Model from keras.layers import Dense, Dropout, Flatten from keras.optimizers import Adam from sklearn.model_selection import train_test_split ``` 2. 加载数据集 我们使用的数据集是Kaggle上的猫分类数据集,可以从以下链接下载:https://www.kaggle.com/c/dogs-vs-cats/data 数据集中包含25,000张大小不一的猫和的图片,我们需要将其转换为模型可以处理的格式。 ```python # 定义数据集根目录 data_dir = 'path/to/data' # 定义图片大小和批次大小 img_size = (224, 224) batch_size = 32 # 加载数据集并将其转换为模型可以处理的格式 def load_dataset(): X = [] Y = [] for file in os.listdir(data_dir): if file.endswith('.jpg'): img = load_img(os.path.join(data_dir, file), target_size=img_size) img = img_to_array(img) X.append(img) if 'cat' in file: Y.append(0) else: Y.append(1) return np.array(X), np.array(Y) X, Y = load_dataset() ``` 3. 划分数据集 我们将数据集划分为训练集、验证集和测试集。训练集用于训练模型,验证集用于调整模型参数,测试集用于评估模型性能。 ```python # 划分数据集 X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, random_state=42) X_train, X_val, Y_train, Y_val = train_test_split(X_train, Y_train, test_size=0.2, random_state=42) ``` 4. 加载预训练模型 我们使用Keras中已经训练好的VGG16模型作为特征提取器,将其载入并输出模型结构。 ```python # 加载预训练模型 base_model = VGG16(include_top=False, weights='imagenet', input_shape=img_size+(3,)) # 输出模型结构 for layer in base_model.layers: print(layer.name, layer.input_shape, layer.output_shape) ``` 5. 冻结模型权重 我们将模型的卷积层权重冻结,只训练新添加的全连接层的权重。 ```python # 冻结模型权重 for layer in base_model.layers: layer.trainable = False ``` 6. 构建模型 我们在VGG16模型的顶部添加了几个全连接层,用于分类任务。 ```python # 添加新的全连接层 x = base_model.output x = Flatten()(x) x = Dense(256, activation='relu')(x) x = Dropout(0.5)(x) x = Dense(1, activation='sigmoid')(x) # 构建新模型 model = Model(inputs=base_model.input, outputs=x) # 输出模型结构 model.summary() ``` 7. 训练模型 我们使用Adam优化器和二元交叉熵损失函数训练模型。 ```python # 编译模型 model.compile(optimizer=Adam(lr=0.001), loss='binary_crossentropy', metrics=['accuracy']) # 训练模型 history = model.fit( preprocess_input(X_train), Y_train, batch_size=batch_size, epochs=10, validation_data=(preprocess_input(X_val), Y_val), verbose=1 ) ``` 8. 评估模型 我们使用测试集评估模型性能。 ```python # 评估模型 score = model.evaluate(preprocess_input(X_test), Y_test, verbose=0) print('Test loss:', score[0]) print('Test accuracy:', score[1]) ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星石传说

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值