Pytorch入门实战第四周:猴痘病识别

目录

前言

一、前期准备

1.1 设置GPU

1.2 导入数据

二、构建CNN网络

2.1 网络结构推导

2.2 代码实现

三.训练模型

3.1 设置超参数:损失函数、学习率、优化器

3.2 编写训练函数

3.3 编写测试函数

3.4 正式训练

四、结果可视化

4.1 Loss和Accuracy图

4.2 指定图片进行预测

五、保存并加载模型

六、提高精度尝试

总结


前言

  • 🍨 本文为[🔗365天深度学习训练营](https://mp.weixin.qq.com/s/0dvHCaOoFnW8SCp3JpzKxg) 中的学习记录博客
  • 🍖 原作者:[K同学啊](https://mtyjkh.blog.csdn.net/)

说在前面:

  • 本周学习目标:在训练过程中保存效果最好的模型参数,加载最佳模型参数并识别本地的一张图片,调整网络结构使得测试集accuracy达到88%;在此基础上试着调整模型参数并观察测试集的准确率变化,尝试设置动态学习率,测试集accuracy到达90%
  • 学习重点:指定图片预测、保存并加载模型
  • 我的环境:Python3.8、Pycharm2020、torch1.12.1+cu113(数据来源——[K同学啊](https://mtyjkh.blog.csdn.net/))

一、前期准备

1.1 设置GPU

#一、前期准备
'''
1.1设置GPU
'''
import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision
from torchvision import transforms, datasets
import os, PIL, pathlib, random
import torch.nn.functional as F
import matplotlib.pyplot as plt
from PIL import Image

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)

结果输出:cuda

1.2 导入数据

1.数据下载并设置相应文件目录以便图片读取,将数据下载放在对应代码下目录下新建的4-data目录

  • 数据集介绍:包含了两类图片,为猴痘病和不是猴痘病的图片,分为两个文件夹

导入数据的步骤

 1)使用函数将字符串类型的文件夹路径转换为pathlib.Path对象

 2)使用glob方法获取data_dir路径下的所有文件路径,并以列表的形式存储在data_paths中

 3)利用split()函数对data_paths中的每个文件路径执行分割操作,获取各个文件所属的类别名称并储存在classNames中

数据文件导入代码如下:

'''
1.2 导入数据
'''
data_dir = './4-data/'
data_dir = pathlib.Path(data_dir)           #使用pathlib.Path()函数将字符串类型的文件夹路径转换为pathlib.Path对象
data_paths = list(data_dir.glob('*'))       #使用glob()方法获取data_dir路径下的所有文件路径,并以列表形式存储在data_paths中
classNames = [str(path).split("\\")[1] for path in data_paths]
print(classNames)

打印结果为:['Monkeypox', 'Others']

2.图片数据处理

torchvision.transforms是pytorch中的图像预处理包。一般用Compose把多个步骤整合到一起 
1)Resize:将输入图片resize成统一尺寸
2)ToTensor:将PIL Image或numpy.ndarray转换为tensor,并归一化到[0,1]之间
3)Normalize:标准化处理-->转换为标准正态分布,使模型更容易收敛;其中mean=[0.485,0.456,0.406]与std=[0.229,0.224,0.225] 从数据集中随机抽样计算得到的
ImageFolder类来创建一个数据集对象,total_data将是一个包含所有图像数据的数据集对象,可以用于训练神经网络模型

代码如下:

total_datadir = './4-data/'
train_transforms = transforms.Compose([transforms.Resize([224, 224]),   #将输入图片resize成统一尺寸
                                       transforms.ToTensor(),           #将PIL Image或numpy.ndarray转换为tensor,并归一化到[0,1]之间
                                       transforms.Normalize             #标准化处理-->转换为标准正态分布,使模型更容易收敛
                                       (mean=[0.485, 0.456, 0.406],   # 其中 mean=[0.485,0.456,0.406]与std=[0.229,0.224,0.225] 从数据集中随机抽样计算得到的
                                       std=[0.229, 0.224, 0.225])
                                       ])
total_data = datasets.ImageFolder(total_datadir, transform = train_transforms)
print(total_data)

print(total_data.class_to_idx)     #total_data.class_to_idx是一个存储了数据集类别和对于索引的字典

打印结果如下:

Dataset ImageFolder
    Number of datapoints: 2142
    Root location: ./4-data/
    StandardTransform
Transform: Compose(
               Resize(size=[224, 224], interpolation=bilinear, max_size=None, antialias=warn)
               ToTensor()
               Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
           )
 

{'Monkeypox': 0, 'Others': 1}

3.划分数据集

train_size:表示训练集大小,为总体数据长度的80%

test_size:表示测试集大小,是总体数据长度减去训练集大小

torch.utils.data.random_split():将总体数据total_data按照指定的比例大小随机划分为训练集和测试集,并将划分的结果分别赋值给train_dataset和test_dataset两个变量

代码如下:

'''
1.3 划分数据集
'''
train_size = int(0.8 * len(total_data))
test_size = len(total_data) - train_size
train_dataset, test_dataset = torch.utils.data.random_split(total_data, [train_size, test_size])
print(train_dataset, test_dataset)
print(train_size, test_size)

打印输出结果为:

<torch.utils.data.dataset.Subset object at 0x000002D2E11486D0> <torch.utils.data.dataset.Subset object at 0x000002D2E1148730>
1713 429

4.查看一个batch_size的数据结构

torch.utils.data.DataLoader():这是Pytorch中用于加载和管理数据的一个实用工具类,它允许以小批次的方式迭代数据集,这对于训练神经网络和其他机器学习任务非常有用。具体参数解释如下:

1)dataset(必需参数):这是你的数据集对象,通常是 torch.utils.data.Dataset 的子类,它包含了你的数据样本。

2)batch_size(可选参数):指定每个小批次中包含的样本数。默认值为 1。

3)shuffle(可选参数):如果设置为 True,则在每个 epoch 开始时对数据进行洗牌,以随机打乱样本的顺序。这对于训练数据的随机性很重要,以避免模型学习到数据的顺序性。默认值为 False。

4)num_workers(可选参数):用于数据加载的子进程数量。通常,将其设置为大于 0 的值可以加快数据加载速度,特别是当数据集很大时。默认值为 0,表示在主进程中加载数据。

5)pin_memory(可选参数):如果设置为 True,则数据加载到 GPU 时会将数据存储在 CUDA 的锁页内存中,这可以加速数据传输到 GPU。默认值为 False。

6)drop_last(可选参数):如果设置为 True,则在最后一个小批次可能包含样本数小于 batch_size 时,丢弃该小批次。这在某些情况下很有用,以确保所有小批次具有相同的大小。默认值为 False。

7)timeout(可选参数):如果设置为正整数,它定义了每个子进程在等待数据加载器传递数据时的超时时间(以秒为单位)。这可以用于避免子进程卡住的情况。默认值为 0,表示没有超时限制。

8)worker_init_fn(可选参数):一个可选的函数,用于初始化每个子进程的状态。这对于设置每个子进程的随机种子或其他初始化操作很有用。

代码如下:

batch_size = 32
train_dl = torch.utils.data.DataLoader(train_dataset,
                                       batch_size=batch_size,
                                       shuffle=True,
                                       num_workers=0)
test_dl = torch.utils.data.DataLoader(test_dataset,
                                      batch_size=batch_size,
                                      shuffle=True,
                                      num_workers=0)
for X, y in test_dl:
    print("Shape of X [N, C, H, W]: ", X.shape)
    print("Shape of y: ", y.shape, y.dtype)
    break

打印结果如下:

Shape of X [N, C, H, W]:  torch.Size([32, 3, 224, 224])
Shape of y:  torch.Size([32]) torch.int64

二、构建CNN网络

2.1 网络结构推导

1.本次使用的初始网络结构如下:

2.卷积层和池化层的推导

1)网络数据shape变化的过程如下:

上面的网络数据shape变化过程为:

3, 224, 224(输入数据)-> 12, 220, 220(经过卷积层1)-> 12, 216, 216(经过卷积层2)-> 12, 108, 108(经过池化层1)-> 24, 104, 104(经过卷积层3)-> 24, 100, 100(经过池化层4)-> 24, 50, 50(经过池化层2)-> 60000 -> num_classes(4)

2)手算过程见下图

2.2 代码实现

#二、构建CNN网络
class Network_bn(nn.Module):
    def __init__(self):
        super(Network_bn, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=12, kernel_size=5, stride=1, padding=0)
        self.bn1 = nn.BatchNorm2d(12)
        self.conv2 = nn.Conv2d(in_channels=12, out_channels=12, kernel_size=5, stride=1, padding=0)
        self.bn2 = nn.BatchNorm2d(12)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv4 = nn.Conv2d(in_channels=12, out_channels=24, kernel_size=5, stride=1, padding=0)
        self.bn4 = nn.BatchNorm2d(24)
        self.conv5 = nn.Conv2d(in_channels=24, out_channels=24, kernel_size=5, stride=1, padding=0)
        self.bn5 = nn.BatchNorm2d(24)
        self.pool2 = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(24 * 50 * 50, len(classNames))

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = self.pool(x)
        x = F.relu(self.bn4(self.conv4(x)))
        x = F.relu(self.bn5(self.conv5(x)))
        x = self.pool2(x)
        x = x.view(-1, 24 * 50 * 50)
        x = self.fc1(x)

        return x
device = "cuda" if torch.cuda.is_available() else "cpu"
print("Using {} device".format(device))
model = Network_bn().to(device)
print(model)

模型打印结果如下:

Using cuda device
Network_bn(
  (conv1): Conv2d(3, 12, kernel_size=(5, 5), stride=(1, 1))
  (bn1): BatchNorm2d(12, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (conv2): Conv2d(12, 12, kernel_size=(5, 5), stride=(1, 1))
  (bn2): BatchNorm2d(12, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv4): Conv2d(12, 24, kernel_size=(5, 5), stride=(1, 1))
  (bn4): BatchNorm2d(24, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (conv5): Conv2d(24, 24, kernel_size=(5, 5), stride=(1, 1))
  (bn5): BatchNorm2d(24, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=60000, out_features=2, bias=True)
)

三.训练模型

3.1 设置超参数:损失函数、学习率、优化器

''
3.1 设置超参数
'''
loss_fn = nn.CrossEntropyLoss()   #交叉熵函数
learn_rate = 1e-4
opt = torch.optim.SGD(model.parameters(), lr=learn_rate)

3.2 编写训练函数

代码如下:

'''
3.2编写训练函数
'''
  #训练循环
def train(dataloader, model, loss_fn, optimier):
    size = len(dataloader.dataset)    #训练集的大小
    num_batches = len(dataloader)

    train_loss, train_acc =0, 0

    for X, y in dataloader:
        X, y = X.to(device), y.to(device)

        #计算预测误差
        pred = model(X)
        loss = loss_fn(pred, y)
        #反向传播
        optimier.zero_grad()   #grad属性归零
        loss.backward()        #反向传播
        optimier.step()        #每一步自动更新
        #记录acc和loss
        train_acc += (pred.argmax(1) == y).type(torch.float).sum().item()
        train_loss +=loss.item()

    train_acc /= size
    train_loss /= num_batches

    return train_acc, train_loss

3.3 编写测试函数

代码如下:

'''
3.3编写测试函数
'''
def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)  # 训练集的大小
    num_batches = len(dataloader)
    test_loss, test_acc = 0, 0

    # 当不进行训练时,停止梯度更新,节省计算内存消耗
    with torch.no_grad():
        for imgs, target in dataloader:
            imgs, target = imgs.to(device), target.to(device)

            # 计算loss
            target_pred = model(imgs)
            loss = loss_fn(target_pred, target)

            test_loss += loss.item()
            test_acc += (target_pred.argmax(1) == target).type(torch.float).sum().item()

    test_acc /= size
    test_loss /= num_batches

    return test_acc, test_loss

3.4 正式训练

代码如下:

'''
3.4 正式训练
'''
epochs = 20
train_loss = []
train_acc = []
test_loss = []
test_acc = []

for epoch in range(epochs):
    model.train()
    epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)

    model.eval()
    epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)

    train_acc.append(epoch_train_acc)
    train_loss.append(epoch_train_loss)
    test_acc.append(epoch_test_acc)
    test_loss.append(epoch_test_loss)

    template = ('Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%,Test_loss:{:.3f}')
    print(template.format(epoch + 1, epoch_train_acc * 100, epoch_train_loss, epoch_test_acc * 100, epoch_test_loss))
print('Done')

运行结果如下:

模型输出的最佳结果也只为87.2%

四、结果可视化

4.1 Loss和Accuracy图

代码如下:

'''
4.1 loss and accuracy图
'''
import warnings
warnings.filterwarnings("ignore")             #忽略警告信息
plt.rcParams['font.sans-serif'] = ['SimHei']  #用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False    #用来正常显示负号
plt.rcParams['figure.dpi'] = 100              #分辨率

epochs_range = range(epochs)

plt.figure(figsize=(12, 3))
plt.subplot(1, 2, 1)

plt.plot(epochs_range, train_acc, label='Training Accuracy')
plt.plot(epochs_range, test_acc, label='Test Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')

plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='Training Loss')
plt.plot(epochs_range, test_loss, label='Test Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()

打印输出如下图所示:

4.2 指定图片进行预测

torch.squeeze()详解:对数据的维度进行压缩,去掉维数为1的的维度

函数原型---torch.squeeze(input, dim=None, *, out=None)

关键参数说明---input (Tensor):输入Tensor;dim (int, optional):如果给定,输入将只在这个维度上被压缩

示范代码:

x = torch.zeros(2, 1, 2, 1, 2)
print(x.size())
y = torch.squeeze(x)
print(y.size)
y = torch.squeeze(x, 0)
print(y.size)
y = torch.squeeze(x, 1)
print(y.size)

打印结果

torch.Size([2, 1, 2, 1, 2])  torch.Size([2, 2, 2]) torch.Size([2, 1, 2, 1, 2])  torch.Size([2, 2, 1, 2])

torch.unsqueeze()详解:对数据维度进行扩充。给指定位置加上维数为一的维度

函数原型---torch.unsqueeze(input, dim)

关键参数说明---input (Tensor):输入Tensor;dim (int):插入单例维度的索引

示范代码:

x2 = torch.tensor([1, 2, 3, 4])
print(torch.unsqueeze(x2, 0))
print(torch.unsqueeze(x2, 1))

打印结果:

tensor([[1, 2, 3, 4]])
tensor([[1],
        [2],
        [3],
        [4]])

指定代码预测如下:

classes = list(total_data.class_to_idx)
def predict_one_image(image_path, model, transform, classes):
    test_img = Image.open(image_path).convert('RGB')
    plt.imshow(test_img)  #展示要预测的图片

    test_img = transform(test_img)
    img = test_img.to(device).unsqueeze(0)

    model.eval()
    output = model(img)

    _, pred = torch.max(output, 1)
    pred_class = classes[pred]
    print(f'预测结果是:{pred_class}')

predict_one_image(image_path='./4-data/Monkeypox/M01_01_00.jpg',model=model, transform = train_transforms,classes=classes)

打印输出如下:预测结果是:Monkeypox

五、保存并加载模型

函数解释

torch.save(obj, f, pickle_module=<module '...'>, pickle_protocol=2)
obj:保存对象

f:类文件对象 (必须实现写和刷新)或一个保存文件名的字符串

pickle_module:类文件对象 (必须实现写和刷新)或一个保存文件名的字符串

pickle_protocol:指定 pickle protocal 可以覆盖默认参数

#实例代码
torch.save(model,'save.pt')   #保存整个模型
torch.save(model.state_dict(), 'save.pt')     #只保存训练好的参数

代码如下:

PATH = './model.pth'             #保存的路径
torch.save(model.state_dict(), PATH)

#将参数加载到model放在
model.load_state_dict(torch.load(PATH, map_location=device))

保存成功后在文件目录中就会多一个这样的文件mode.pth

六、提高精度尝试

由于初始设置的网络结果和超参数的结果运行效果并不是很好,所以这里设置了多组实验探究提升测试集Accuracy的方法

1.修改优化器为Adam

loss_fn = nn.CrossEntropyLoss()   #交叉熵函数
learn_rate = 1e-4
opt = torch.optim.Adam(model.parameters(), lr=learn_rate)

结果输出:

测试集上最佳Acuuracy可达88.8%,在原来的基础已经有提升

2.增加一个dropout层,设置为0.3:因为在训练集上的结果是挺好的,但是在测试集上没那么好,可能的原因就有过拟合

Dropout说的简单一点就是:我们在前向传播的时候,让某个神经元的激活值以一定的概率p(伯努利分布)停止工作,这样可以使模型泛化性更强,因为它不会太依赖某些局部的特征
训练阶段假设数据输入为X,以概率p丢弃,所以服从B(p)分布,E(x) = pXW, W为权重。
测试阶段需要将权重恢复到和训练阶段相同,才可以保证数据分布的一致性,所以要乘P

网络结构部分代码修改如下:

class Network_bn(nn.Module):
    def __init__(self):
        super(Network_bn, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=12, kernel_size=5, stride=1, padding=0)
        self.bn1 = nn.BatchNorm2d(12)
        self.conv2 = nn.Conv2d(in_channels=12, out_channels=12, kernel_size=5, stride=1, padding=0)
        self.bn2 = nn.BatchNorm2d(12)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv4 = nn.Conv2d(in_channels=12, out_channels=24, kernel_size=5, stride=1, padding=0)
        self.bn4 = nn.BatchNorm2d(24)
        self.conv5 = nn.Conv2d(in_channels=24, out_channels=24, kernel_size=5, stride=1, padding=0)
        self.bn5 = nn.BatchNorm2d(24)
        self.pool2 = nn.MaxPool2d(2, 2)
        self.dropout = nn.Dropout(p=0.3)
        self.fc1 = nn.Linear(24 * 50 * 50, len(classNames))

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = self.pool(x)
        x = F.relu(self.bn4(self.conv4(x)))
        x = F.relu(self.bn5(self.conv5(x)))
        x = self.pool2(x)
        x = self.dropout(x)
        x = x.view(-1, 24 * 50 * 50)
        x = self.fc1(x)

网络结构打印如下:

运行结果如下:

发现增加后效果反而没有之前好了,怀疑是dropout的参数设置不对,进行修改

3.修改dropout=0.5

运行结果如下:

这次结果最佳达到了89.5%,所以正确的增加dropout层对测试集上预测性能是可以有提升的

4.在前面3的基础上修改学习率为0.00001

在前面的基础上将学习率降低,在测试集上的性能大幅度下降,所以一味降低学习率并不是就是最好的


总结

  • 通过本周的学习了解了如何从本地加载一张图片进行预测
  • 同时也学习了如何保存并加载模型的方法
  • 还从增加dropout层修改网络结构、修改优化器、修改学习率等几个方面对模型的性能提升进行探究,最后发现增加dropout层是可以有效提升模型在测试集上的Acuuracy,修改优化器同样可以(所以不同的问题适用的优化器是不同的),以及并不是减小学习率就一定可以换得更好的性能(本处将学习率从0.0001修改到0.00001模型的预测准确度反而降低了)
  • 但是这里还没尝试动态学习率应用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值