- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
前言
本文的核心目标,并非从零开始构建一个基础模型,而是聚焦于一个更关键的实战环节:如何通过精心地调整网络结构和优化训练超参数,显著提升猴痘病识别模型的性能,最终在我们的独立测试集上达到并突破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%的准确率。
三、可视化
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))