PyTorch实战:调优猴痘病识别模型,攻克88%测试准确率并实现本地图片预测


前言

本文的核心目标,并非从零开始构建一个基础模型,而是聚焦于一个更关键的实战环节:如何通过精心地调整网络结构和优化训练超参数,显著提升猴痘病识别模型的性能,最终在我们的独立测试集上达到并突破88%的准确率。​​ 这个硬性指标是我们的“登山线”,我们将深入探讨更换骨干网络、调整学习率策略、优化数据增强、引入正则化、实施早停等核心调优手段,详细记录每一步策略的效果和最终汇聚成突破88%的完整过程。模型最终的泛化能力和鲁棒性,将严格通过这个未见过的测试集来验证。

​​您将在本文中看到我们努力的最终成果:​​ 在成功训练出满足精度要求的模型后,我们将展示如何加载验证过程中效果最佳的那个模型参数文件(.pth)。接着,我们会​​手把手演示如何利用这个训练好的模型,对用户本地存储的一张皮肤图片进行识别​​,直观地输出模型对该图片是“猴痘”还是“其他”的预测结果,并将其显示出来。

由于环境准备与数据预处理方面与我的上篇文章非常相似,在本文我们将略过这些,直接从构建模型开始。对数据预处理不太清楚的同学可以看我的上一篇文章。零基础实战:用自定义天气图片数据打造专属识别模型 (附本地预测)——深度学习入门(3)

一、搭建CNN网络

1、原文的CNN

class Network_bn(nn.Module):
    def __init__(self):
        super().__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, 2)

    def forward(self, x):
    	# 卷积层+BN层+ReLU激活函数
        x = F.relu(self.bn1(self.conv1(x)))      
        # 卷积层+BN层+ReLU激活函数
        x = F.relu(self.bn2(self.conv2(x)))    
        # 最大池化 
        x = self.pool(x)              
        # 卷积层+BN层+ReLU激活函数          
        x = F.relu(self.bn4(self.conv4(x)))
        # 卷积层+BN层+ReLU激活函数               
        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

本篇文章的CNN与前几篇文章的CNN有一个显著的区别在于引入的Batch Normalization (BN) 层,BN层解决了深度神经网络训练过程中的两大痛点:​​内部协变量偏移​​ 和 ​​梯度相关问题​​,从而​​显著加速训练、提升稳定性并通常能提高模型性能​​。
在使用BN层时要注意​​:

  • BN 层通常放置在 ​​全连接层(Linear/Dense)或卷积层(Conv)之后​​,​​激活函数层(如 ReLU, Sigmoid, Tanh)之前​​。即:Conv/Linear → BN → Activation Function。在激活函数之前对线性变换的结果进行归一化,有助于将输入稳定在激活函数最敏感的区间(如 ReLU 的线性区域),避免梯度饱和或消失。
  • BN 本身有轻微的正则化效果。将其与 Dropout 一起使用时,有时会出现互相削弱或性能不及预期的情况。
  • 使用了 BN,通常​​可以显著增大学习率(Learning Rate)​​(例如翻倍或更大),因为 BN 稳定了训练过程,减少了发散风险。更高的学习率能加速收敛。但具体值仍需实验调试。

查看模型详情后是这样的
在这里插入图片描述

2、构建损失函数及优化器

损失函数才用分类问题常用的交叉熵多分类损失函数
优化器使用基础的小批量梯度下降SGD优化器

loss_fn    = nn.CrossEntropyLoss() # 创建损失函数
learn_rate = 1e-4 # 学习率
opt        = torch.optim.SGD(model.parameters(),lr=learn_rate)

3、构建训练与测试函数并训练模型

# 训练循环
def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)  # 训练集的大小,一共60000张图片
    num_batches = len(dataloader)   # 批次数目,1875(60000/32)

    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)  # 计算网络输出和真实值之间的差距,targets为真实值,计算二者差值即为损失
        
        # 反向传播
        optimizer.zero_grad()  # grad属性归零
        loss.backward()        # 反向传播
        optimizer.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

def test (dataloader, model, loss_fn):
    size        = len(dataloader.dataset)  # 测试集的大小,一共10000张图片
    num_batches = len(dataloader)          # 批次数目,313(10000/32=312.5,向上取整)
    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

epochs     = 40
train_loss = []
train_acc  = []
test_loss  = []
test_acc   = []

for epoch in range(epochs):
    model2.train()
    epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)
    
    model2.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')

模型优化结果如下:经过40轮epoch后,测试集准确率只有83.0%,很明显是不满足我们88%的准确率要求的,接下来进行模型调优。
在这里插入图片描述

二、调优ing,提高准确率

1、改进CNN

这里我尝试使用更深的网络结果,从原来的2个卷积块增加到3个;使用更小的卷积核,使用3x3卷积核代替5x5;添加Padding=1保持特征图尺寸不变;增加通道数,逐步增加通道数(32->64->128),让网络能够学习更多特征;添加Dropout,在全连接层前添加Dropout层,减少过拟合风险,提高测试集准确率。

class Network_bn3(nn.Module):
    def __init__(self):
        super().__init__()
        # 更深的网络结构
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1)  # 保持尺寸
        self.bn1 = nn.BatchNorm2d(32)
        self.conv2 = nn.Conv2d(32, 32, kernel_size=3, stride=1, padding=1)
        self.bn2 = nn.BatchNorm2d(32)
        
        self.conv3 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
        self.bn3 = nn.BatchNorm2d(64)
        self.conv4 = nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1)
        self.bn4 = nn.BatchNorm2d(64)
        
        self.conv5 = nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1)
        self.bn5 = nn.BatchNorm2d(128)
        self.conv6 = nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1)
        self.bn6 = nn.BatchNorm2d(128)
        
        self.pool = nn.MaxPool2d(2, 2)
        self.dropout = nn.Dropout(0.5)  # 添加dropout减少过拟合
        
        # 计算全连接层输入尺寸
        # 输入224x224,经过3次pooling后为28x28 (224/2^3)
        self.fc1 = nn.Linear(128 * 28 * 28, 512)
        self.fc2 = nn.Linear(512, len(classeNames))

    def forward(self, x):
        # Block 1
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = self.pool(x)
        
        # Block 2
        x = F.relu(self.bn3(self.conv3(x)))
        x = F.relu(self.bn4(self.conv4(x)))
        x = self.pool(x)
        
        # Block 3
        x = F.relu(self.bn5(self.conv5(x)))
        x = F.relu(self.bn6(self.conv6(x)))
        x = self.pool(x)
        
        # 分类器
        x = x.view(-1, 128 * 28 * 28)
        x = self.dropout(x)
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.fc2(x)
        
        return x

优化器和损失函数依然是交叉熵损失函数和SGD优化器,同样经过40轮epoch,测试集准确率不升反降,可能我还是初学者对CNN还是不是很了解,大家可以多尝试一下不同网络架构试试。
在这里插入图片描述

2、增加batch_size

我把原来batch_size由32增加到64,准确率只增加了0.2%,不建议用这个方法提升准确率。

3、更改优化器

这里我们使用能更快速稳定收敛的Adam优化器,Adam优化器能自适应地调整每个参数的学习率和利用动量​​,从而在大多数情况下带来更快、更鲁棒的收敛。

opt        = torch.optim.Adam(model.parameters(),lr=learn_rate)     # 采用Adam优化器

以下是运行结果:经过15轮epoch就已经达到88.6%,经过30轮epoch后更是达到了90.7%的准确率。
Adam优化器的结果

三、可视化

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

在这里插入图片描述

三、用最佳模型识别单张图片

预测单张图片函数

from PIL import Image 

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='./data/Monkeypox/M01_01_00.jpg', 
                  model=model, 
                  transform=train_transforms, 
                  classes=classes)     # 输出:预测结果是:Monkeypox

保存最佳模型

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值