• 🍨 本文为🔗365天深度学习训练营中的学习记录博客
  • 🍖 原作者:K同学啊

    本周是学习深度学习的第4周。编译器使用的是vscode,安装的是CPU版PyTorch:torch 2.12.0+cpu。

本次学习目标:

        1.训练过程中保存效果最好的模型参数。

        2.加载最佳模型参数识别本地的一张图片。

        3.调整网络结构使测试集accuracy到达88%。


1 数据集

1.1 数据集的导入

        本次使用的数据集是猴痘病患病图片,本文把文件夹命名为“4-data”,包含了Monkeypox和other俩个子文件夹。

        本部分的核心目的是从本地 ./4-data/ 目录读取按类别分文件夹存放的图像数据,通过遍历子文件夹名获取所有分类标签,然后将原始图像统一变换为模型可接受的标准化张量,最后使用ImageFolder按文件夹结构自动标注,生成可供DataLoader使用的数据集对象。

        pathlib.Path(data_dir).glob('*')用通配符匹配数据目录下的所有子文件夹,每个子文件夹代表一个类别,返回路径列表,pathlib相比传统的os模块更加现代且跨平台。        

  transforms.Resize([224, 224])把所有图像统一缩放到224×224像素。transforms.ToTensor() 将 PIL Image 的0-255像素值转换为0.0-1.0的浮点数张量,并自动把通道顺序从H×W×C转为PyTorch标准的C×H×W格式。transforms.Normalize(mean, std) 使用ImageNet数据集的经验均值和标准差进行标准化,使数据分布更稳定、加速模型收敛。

import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision
from torchvision import transforms, datasets

import os,PIL,pathlib

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

import os,PIL,random,pathlib

data_dir = './4-data/'
data_dir = pathlib.Path(data_dir)

data_paths = list(data_dir.glob('*'))
classeNames = [str(path).split("\\")[1] for path in data_paths]
print(classeNames)

total_datadir = './4-data/'

train_transforms = transforms.Compose([
    transforms.Resize([224, 224]),  
    transforms.ToTensor(),          
    transforms.Normalize(           
        mean=[0.485, 0.456, 0.406], 
        std=[0.229, 0.224, 0.225])  
])

total_data = datasets.ImageFolder(total_datadir,transform=train_transforms)
print(total_data)

输出结果为:

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

1.2 划分数据集

        将前面构建好的total_data按8:2的比例随机拆分为训练集和测试集,然后通过DataLoader将两个数据集分别包装成可迭代的批次加载器,最后用一个简单的循环检验数据加载并打印出一个批次的张量形状和标签信息。

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)
print(test_dataset)
print(train_size)
print(test_size)

batch_size = 32

train_dl = torch.utils.data.DataLoader(train_dataset,
                                               batch_size=batch_size,
                                               shuffle=True,
                                               num_workers=1)
test_dl = torch.utils.data.DataLoader(test_dataset,
                                              batch_size=batch_size,
                                              shuffle=False,
                                              num_workers=1)

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

运行结果为:

<torch.utils.data.dataset.Subset object at 0x00000234BE542CF0>
<torch.utils.data.dataset.Subset object at 0x00000234BE4B3750>
1713
429
Shape of X [N, C, H, W]:  torch.Size([32, 3, 224, 224])
Shape of y:  torch.Size([32]) torch.int64

        torch.utils.data.random_split(total_data, [train_size, test_size])是PyTorch提供的数据集切分函数,它按指定的数量列表将数据集随机打散后分成互不重叠的子集,这里80%用于训练、20%用于测试,保证训练集和测试集来自同一分布但又相互独立。

2 CNN结构

        本次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.fc1 = nn.Linear(24*50*50, len(classeNames))

    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.pool(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 cpu 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, bias=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, bias=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, bias=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, bias=True, track_running_stats=True)
  (fc1): Linear(in_features=60000, out_features=2, bias=True)
)

3 模型训练与可视化

3.1 模型训练

 训练与测试函数为:

def train(dataloader, model, loss_fn, optimizer):
    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)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        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


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)

            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

参数与训练:

 loss_fn    = nn.CrossEntropyLoss()
    learn_rate = 1e-3
    opt        = torch.optim.AdamW(model.parameters(), lr=learn_rate, weight_decay=1e-4)
    scheduler  = torch.optim.lr_scheduler.ReduceLROnPlateau(opt, mode='min', patience=3, factor=0.5)

    epochs     = 20
    best_test_acc = 0
    best_model_state = None

    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)

        scheduler.step(epoch_test_loss)

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

        if epoch_test_acc > best_test_acc:
            best_test_acc = epoch_test_acc
            best_model_state = model.state_dict().copy()

        template = ('Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%, Test_loss:{:.3f}, LR:{:.2e}')
        print(template.format(epoch+1, epoch_train_acc*100, epoch_train_loss,
                              epoch_test_acc*100, epoch_test_loss,
                              opt.param_groups[0]['lr']))
    print(f'Done. Best test accuracy: {best_test_acc*100:.1f}%')

3.2 可视化

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

from datetime import datetime
current_time = datetime.now() 

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.xlabel(current_time)

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()

结果为:

Epoch: 1, Train_acc:62.5%, Train_loss:0.675, Test_acc:60.3%, Test_loss:0.690
Epoch: 2, Train_acc:68.0%, Train_loss:0.586, Test_acc:73.7%, Test_loss:0.590
Epoch: 3, Train_acc:75.2%, Train_loss:0.523, Test_acc:73.3%, Test_loss:0.515
Epoch: 4, Train_acc:77.4%, Train_loss:0.506, Test_acc:75.6%, Test_loss:0.493
Epoch: 5, Train_acc:81.0%, Train_loss:0.455, Test_acc:77.4%, Test_loss:0.506
Epoch: 6, Train_acc:82.4%, Train_loss:0.430, Test_acc:75.0%, Test_loss:0.476
Epoch: 7, Train_acc:83.7%, Train_loss:0.412, Test_acc:79.3%, Test_loss:0.476
Epoch: 8, Train_acc:84.9%, Train_loss:0.402, Test_acc:79.3%, Test_loss:0.449
Epoch: 9, Train_acc:86.0%, Train_loss:0.384, Test_acc:80.8%, Test_loss:0.445
Epoch:10, Train_acc:87.1%, Train_loss:0.367, Test_acc:82.3%, Test_loss:0.435
Epoch:11, Train_acc:88.3%, Train_loss:0.356, Test_acc:79.9%, Test_loss:0.432
Epoch:12, Train_acc:87.6%, Train_loss:0.349, Test_acc:82.1%, Test_loss:0.413
Epoch:13, Train_acc:88.5%, Train_loss:0.339, Test_acc:82.4%, Test_loss:0.422
Epoch:14, Train_acc:88.9%, Train_loss:0.330, Test_acc:82.2%, Test_loss:0.427
Epoch:15, Train_acc:89.7%, Train_loss:0.322, Test_acc:83.1%, Test_loss:0.407
Epoch:16, Train_acc:89.8%, Train_loss:0.315, Test_acc:82.4%, Test_loss:0.396
Epoch:17, Train_acc:89.7%, Train_loss:0.310, Test_acc:83.2%, Test_loss:0.409
Epoch:18, Train_acc:90.6%, Train_loss:0.296, Test_acc:84.5%, Test_loss:0.425
Epoch:19, Train_acc:91.5%, Train_loss:0.287, Test_acc:84.8%, Test_loss:0.378
Epoch:20, Train_acc:92.2%, Train_loss:0.275, Test_acc:88.4%, Test_loss:0.382
Done

        上述结果表明,测试集准确率达到88%,达到本次学习要求。

3.3 保存模型与图片预测

# 模型保存
PATH = './model.pth'  # 保存的参数文件名
torch.save(model.state_dict(), PATH)

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

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}')

运行结果为:

<All keys matched successfully>
预测结果是:Monkeypox

4 个人总结

        本次学习遇到了较多的问题,optimizer.zero_grad()的位置必须在loss.backward()之前,否则梯度会累积,这个错误我曾经犯过,当时发现loss怎么震荡得厉害,查了半天才发现是梯度没清零。

        训练改为使用了Adam优化器并增加dropout防止过拟合问题,结果比原先结果得到提升,可达到88%以上的学习目标。本次训练页也学习了相对于上周增加指定图片预测与保存并加载模型这个两个模块。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐