【计算机视觉CV-图像分类】06 - VGGNet的鲜花分类实现:从数据预处理到模型优化的完整实战!

目录

  1. 引言

  2. VGGNet概述

  3. VGGNet的网络架构

  4. 基于预训练VGGNet的五类鲜花分类实现

    • 4.1 数据准备与预处理

    • 4.2 模型实例化与参数调整

    • 4.3 模型训练与保存最优模型

    • 4.4 模型导入与预测

    • 4.5 训练过程的可视化

  5. 模型优化与防止过拟合

  6. 总结与展望

  7. 参考文献


引言

在计算机视觉领域,图像分类是一个基础且关键的任务。随着深度学习技术的迅猛发展,卷积神经网络(CNN)在图像分类任务中展现出了卓越的性能。VGGNet作为一种经典的深层卷积神经网络,以其简洁而深邃的网络结构,在图像分类任务中取得了显著的成果。本文将详细介绍VGGNet的架构,并通过一个五类鲜花分类的实例,演示如何利用预训练的VGGNet模型进行图像分类。本文使用的工具链为PyTorch 2.5.1与TorchVision 0.20.1,确保代码的兼容性和稳定性。


VGGNet概述

VGGNet由牛津大学视觉几何组(Visual Geometry Group, VGG)和Google DeepMind的研究人员于2014年共同开发。VGGNet在2014年ImageNet大规模视觉识别挑战赛(ILSVRC2014)中表现优异,尤其在图像分类任务中取得了显著的成绩。

VGGNet的主要特点

  1. 统一的卷积核尺寸:VGGNet全部采用3×3的小卷积核。这种设计不仅减少了参数数量,还能通过堆叠多个卷积层来增加网络的深度和非线性表达能力。

  2. 深层网络结构:VGGNet通过增加网络的深度(层数),增强了模型的特征提取能力,相较于早期的网络如AlexNet,VGGNet在图像分类任务中表现更加出色。

  3. 重复使用简单的模块:VGGNet通过重复使用相同的卷积和池化模块,使得网络结构简洁且易于扩展,便于理解和实现。

  4. 全连接层:在卷积层之后,VGGNet使用多个全连接层进行高层次的特征组合和分类任务,增强了模型的分类能力。

VGGNet的版本

VGGNet有多个不同深度的版本,最常用的是VGG-16和VGG-19,分别包含16层和19层深度。这些版本在不同的任务中表现出色,尤其是在图像特征提取方面,被广泛应用于各种计算机视觉任务中。


VGGNet的网络架构

VGGNet的网络架构以其统一的小卷积核和深层结构著称。以VGG-16为例,其详细架构如下:

图片来源:Wikipedia

VGG-16详细架构

python 复制代码
from torchsummary import summary
# 通过net.summay()查看网络的形状
summary(model=model,input_size=(3,224,224),batch_size=1,device='cpu')
  1. 输入层:224×224 RGB图像。

  2. 卷积层:

    • Conv1: 2个3×3卷积,64个通道

    • Conv2: 2个3×3卷积,128个通道

    • Conv3: 3个3×3卷积,256个通道

    • Conv4: 3个3×3卷积,512个通道

    • Conv5: 3个3×3卷积,512个通道

  3. 池化层:每两个或三个卷积层后接一个2×2的最大池化层,进行空间降维。

  4. 全连接层:

    • FC1: 4096个神经元

    • FC2: 4096个神经元

    • FC3: 1000个神经元(对应ImageNet的1000类)

参数总数

VGG-16共有约138,357,544个参数,其中包括138,357,544个可训练参数。这一庞大的参数量赋予了VGGNet强大的特征表达能力,但也带来了计算和存储的挑战。


基于预训练VGGNet的五类鲜花分类实现

在实际应用中,训练一个深层次的卷积神经网络(如VGGNet)需要大量的计算资源和时间。为此,我们可以利用在大型数据集(如ImageNet)上预训练的模型,进行迁移学习,以便在较小的数据集上快速获得较好的性能。本文将通过一个五类鲜花分类的实例,详细讲解如何利用预训练的VGGNet模型进行图像分类任务。

4.1 数据准备与预处理

首先,我们需要准备好五类鲜花的图像数据集,并对其进行预处理,以适应VGGNet的输入要求。具体步骤包括调整图像大小、数据增强和批量加载。

数据集介绍

假设我们使用的是一个包含五类鲜花的图像数据集,每类约有100张图像。数据集分为训练集和验证集,分别存放在不同的文件夹中。

数据预处理

数据预处理是深度学习模型训练中的关键步骤。对于VGGNet,我们需要将输入图像调整为224×224的尺寸,并进行标准化处理。此外,为了提高模型的泛化能力,我们还可以应用数据增强技术,如随机水平翻转和随机旋转。

python 复制代码
import torch
import numpy as np  # 新增 numpy 导入
from torchvision import transforms
from torch.utils.data import DataLoader
from torchvision.datasets import ImageFolder
import matplotlib.pyplot as plt

# 指定PyTorch和TorchVision的版本兼容性
print(f"PyTorch version: {torch.__version__}")
print(f"TorchVision version: {torch.__version__}")  # TorchVision 0.20.1与PyTorch 2.5.1兼容

# 如果 MPS 可用,选择 "mps"。如果 CUDA 可用,选择 "cuda"。如果两者都不可用,选择 "cpu"。
device = torch.device("mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu")

# 将模型移到设备上
model.to(device)

# 指定批次大小
batch_size = 4

# 指定数据集路径
flower_train_path = '../01.图像分类/dataset/flower_datas/train/'
flower_val_path = '../01.图像分类/dataset/flower_datas/val/'

# 定义数据预处理方式
train_transforms = transforms.Compose([
    transforms.Resize((224, 224)),                     # 调整图像大小
    transforms.RandomHorizontalFlip(),                 # 随机水平翻转
    transforms.RandomRotation(15),                     # 随机旋转
    transforms.ToTensor(),                             # 转换为Tensor
    transforms.Normalize([0.485, 0.456, 0.406],       # 标准化
                         [0.229, 0.224, 0.225])
])

val_transforms = transforms.Compose([
    transforms.Resize((224, 224)),                     # 调整图像大小
    transforms.ToTensor(),                             # 转换为Tensor
    transforms.Normalize([0.485, 0.456, 0.406],       # 标准化
                         [0.229, 0.224, 0.225])
])

# 加载训练集和验证集
flower_train = ImageFolder(root=flower_train_path, transform=train_transforms)
flower_val = ImageFolder(root=flower_val_path, transform=val_transforms)

# 创建数据加载器
train_loader = DataLoader(dataset=flower_train, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(dataset=flower_val, batch_size=batch_size, shuffle=False)

# 获取类别名称
classes = flower_train.classes
print(f"类别名称: {classes}")

# 可视化一个批次的数据
def imshow(inp, title=None):
    """显示一个Tensor图像"""
    inp = inp.numpy().transpose((1, 2, 0))  # 转换为 (H, W, C) 格式
    mean = np.array([0.485, 0.456, 0.406])  # 转换为 numpy 数组
    std = np.array([0.229, 0.224, 0.225])  # 转换为 numpy 数组
    inp = std * inp + mean  # 反标准化
    inp = np.clip(inp, 0, 1)  # 将像素值限制在 0 到 1 之间
    plt.imshow(inp)  # 显示图像
    if title:
        plt.title(title)  # 如果提供了标题,则显示类别名称作为标题
    plt.pause(0.001)  # 暂停以确保图像显示

# 展示训练集中的一个batch
inputs, classes_idx = next(iter(train_loader))  # 从训练集加载器中获取一个批次的数据
out = torchvision.utils.make_grid(inputs)  # 将多个图像拼接成网格

imshow(out, title=[classes[x] for x in classes_idx])  # 显示拼接后的图像,并显示对应的类别名称
plt.show()  # 显示图像窗口

结果展示:

图片示例来自自定义数据集

代码解析
  1. 导入必要的库 :包括torchtorchvision及其相关模块。

  2. 设置设备:判断是否有GPU可用,以加速训练过程。

  3. 定义数据预处理:

    • 训练集:包括随机水平翻转和随机旋转的数据增强,以提高模型的泛化能力。

    • 验证集:仅调整图像大小和标准化,确保评估时数据的一致性。

  4. 加载数据集 :使用ImageFolder读取指定路径下的图像数据,并应用相应的预处理。

  5. 创建数据加载器 :使用DataLoader批量加载数据,设置适当的批次大小和是否打乱数据。

  6. 获取类别名称:从训练集中提取类别标签,便于后续的结果解读。

  7. 可视化数据 :通过matplotlib展示一个批次的图像,帮助理解数据分布。

4.2 模型实例化与参数调整

接下来,我们将实例化预训练的VGG-16模型,并根据五类鲜花分类任务进行调整。具体步骤包括加载预训练权重、冻结卷积层参数、调整全连接层以适应五分类任务等。

python 复制代码
from torchvision.models import vgg16, VGG16_Weights
import torch.nn as nn

# 1. 实例化模型
# 指定 weights=VGG16_Weights.IMAGENET1K_V1 来获取官方的 ImageNet 预训练权重
model = vgg16(weights=VGG16_Weights.IMAGENET1K_V1)

# 查看模型结构(可选)
#print(model)

# 2. 冻结卷积层的参数,以防止在训练过程中更新
for param in model.features.parameters():
    param.requires_grad = False

# 3. 修改全连接层以适应5类分类任务
# VGG-16的classifier包含3个全连接层,最后一层输出为1000类
# 我们将其修改为5类
num_features = model.classifier[6].in_features
model.classifier[6] = nn.Linear(num_features, 5)

# 将模型移动到设备(GPU或CPU)
model = model.to(device)

# 4. 查看修改后的模型结构(可选)
#print(model)
代码解析
  1. 加载预训练模型:

    • 使用vgg16函数,并指定weights=VGG16_Weights.IMAGENET1K_V1来加载在ImageNet数据集上预训练的权重。
  2. 冻结卷积层参数:

    • 通过设置param.requires_grad = False,冻结卷积层的参数,避免在训练过程中更新。这有助于减少训练时间和防止过拟合,尤其在数据集较小时效果显著。
  3. 调整全连接层:

    • VGG-16的全连接层classifier的最后一层(classifier[6])原本输出为1000类。我们将其替换为输出5类,以适应五类鲜花分类任务。
  4. 移动模型到设备:

    • 将模型移动到GPU(如果可用)或CPU,确保后续的训练和推理过程在指定设备上进行。

4.3 模型训练与保存最优模型

在完成模型实例化和参数调整后,我们将定义训练过程,包括前向传播、损失计算、反向传播和参数更新。此外,我们还将实现保存验证集上表现最优的模型,以便在训练完成后进行加载和预测。

python 复制代码
import torch.optim as optim
from tqdm import tqdm  # 用于显示训练进度条
import copy  # 用于保存模型的最佳权重

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
# - **损失函数**:交叉熵损失函数,用于多分类问题,衡量预测值与真实标签的差异。

# 只优化最后一层全连接层的参数
optimizer = optim.Adam(model.classifier.parameters(), lr=1e-4)
# - **优化器**:使用 Adam 优化算法,只针对模型的全连接层(`classifier`)进行优化。
# - **学习率 (lr)**:设置为 1e-4,控制每次参数更新的步幅。

# 定义训练和验证的函数
def train_model(model, criterion, optimizer, train_loader, val_loader, num_epochs=25):
    """
    训练和验证模型
    :param model: 要训练的神经网络模型
    :param criterion: 损失函数
    :param optimizer: 优化器
    :param train_loader: 训练数据加载器
    :param val_loader: 验证数据加载器
    :param num_epochs: 训练的总轮次
    :return: 训练完成后的模型和记录的训练历史
    """
    best_model_wts = copy.deepcopy(model.state_dict())
    # 保存最佳模型的权重
    best_acc = 0.0  # 初始化最佳验证准确率
    
    # 用于记录每个epoch的训练和验证损失与准确率
    train_losses = []  # 记录训练集损失
    val_losses = []  # 记录验证集损失
    train_accuracies = []  # 记录训练集准确率
    val_accuracies = []  # 记录验证集准确率
    
    for epoch in range(num_epochs):
        print(f'Epoch {epoch+1}/{num_epochs}')  # 打印当前轮次
        print('-' * 10)  # 分隔线
        
        # 每个epoch都有训练和验证两个阶段
        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()  # 设置模型为训练模式(启用Dropout和BatchNorm)
                dataloader = train_loader  # 使用训练集加载器
            else:
                model.eval()   # 设置模型为评估模式(禁用Dropout和BatchNorm)
                dataloader = val_loader  # 使用验证集加载器
            
            running_loss = 0.0  # 累积损失初始化
            running_corrects = 0  # 累积正确预测数初始化
            
            # 迭代数据
            for inputs, labels in tqdm(dataloader, desc=f'{phase}'):
                inputs = inputs.to(device)  # 将输入数据移动到指定设备(GPU或CPU)
                labels = labels.to(device)  # 将标签移动到指定设备
                
                # 前向传播
                with torch.set_grad_enabled(phase == 'train'):
                    # 如果是训练阶段启用梯度计算,验证阶段关闭梯度计算以加速
                    outputs = model(inputs)  # 模型前向计算,得到输出
                    _, preds = torch.max(outputs, 1)  # 获取预测类别
                    loss = criterion(outputs, labels)  # 计算损失
                
                # 反向传播和优化(仅训练阶段)
                if phase == 'train':
                    optimizer.zero_grad()  # 清空梯度
                    loss.backward()  # 反向传播计算梯度
                    optimizer.step()  # 更新模型参数
                
                # 统计损失和正确预测数
                running_loss += loss.item() * inputs.size(0)  # 累积损失,乘以批次大小
                running_corrects += torch.sum(preds == labels.data)  # 累积正确预测数
            
            epoch_loss = running_loss / len(dataloader.dataset)
            # 平均损失:累积损失除以数据集大小
            epoch_acc = running_corrects.float() / len(dataloader.dataset)
            # 准确率:累积正确数除以数据集大小
            
            print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}')
            # 输出当前阶段的损失和准确率
            
            # 记录损失和准确率
            if phase == 'train':
                train_losses.append(epoch_loss)
                train_accuracies.append(epoch_acc.item())
            else:
                val_losses.append(epoch_loss)
                val_accuracies.append(epoch_acc.item())
                
                # 深拷贝模型
                if epoch_acc > best_acc:
                    best_acc = epoch_acc  # 更新最佳准确率
                    best_model_wts = copy.deepcopy(model.state_dict())
                    # 保存当前模型的最佳权重
                    torch.save(model.state_dict(), 'saved_models/best_vgg16_flower_lr0.001_bs32.pth')
                    # 保存模型到文件中
                    print('保存了最优模型')
        
        print()  # 换行
    
    print(f'最佳验证准确率: {best_acc:.4f}')
    # 打印最佳验证准确率
    
    # 加载最佳模型权重
    model.load_state_dict(best_model_wts)
    # 恢复为最佳模型的权重
    
    # 返回训练过程的记录
    history = {
        'train_losses': train_losses,
        'val_losses': val_losses,
        'train_accuracies': train_accuracies,
        'val_accuracies': val_accuracies
    }
    return model, history

# 开始训练
num_epochs = 25  # 训练的轮次数
model, history = train_model(model, criterion, optimizer, train_loader, val_loader, num_epochs=num_epochs)
# 调用训练函数,返回训练完成的模型和训练过程的记录

输出:

代码解析
  1. 定义损失函数和优化器:

    • 使用交叉熵损失函数(CrossEntropyLoss)适用于多分类任务。

    • 采用Adam优化器,仅优化全连接层的参数,学习率设为1e-4。

  2. 训练函数train_model

    • 参数:

      • model:待训练的模型。

      • criterion:损失函数。

      • optimizer:优化器。

      • train_loaderval_loader:训练和验证数据加载器。

      • num_epochs:训练轮数。

    • 过程:

      • 初始化最佳模型权重和最佳准确率。

      • 遍历每个epoch,包含训练和验证两个阶段。

      • 在训练阶段,设置模型为训练模式,并进行前向传播、计算损失、反向传播和参数更新。

      • 在验证阶段,设置模型为评估模式,仅进行前向传播和损失计算。

      • 记录每个阶段的损失和准确率,并在验证准确率提升时保存模型权重。

    • 返回:

      • 训练完成后的最佳模型和训练过程中的记录(损失和准确率)。
  3. 保存最优模型:

    • 当验证集准确率提升时,使用torch.save保存当前模型权重为

      复制代码
      saved_models/best_vgg16_flower_lr0.001_bs32.pth
  4. 训练过程的记录:

    • 通过history字典记录每个epoch的训练和验证损失与准确率,便于后续的可视化分析。

4.4 模型导入与预测

在完成模型训练并保存最优模型后,我们将加载该模型,并对新图像进行预测。

python 复制代码
from PIL import Image  # 导入 PIL 库,用于打开和处理图像
from torchvision.models import vgg16, VGG16_Weights
import torch.nn as nn
from torchvision import transforms
import torch
# 如果 MPS 可用,选择 "mps"。如果 CUDA 可用,选择 "cuda"。如果两者都不可用,选择 "cpu"。
device = torch.device("mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu")
batch_size=1
# 1. 实例化模型
# 指定 weights=VGG16_Weights.IMAGENET1K_V1 来获取官方的 ImageNet 预训练权重
model = vgg16(weights=VGG16_Weights.IMAGENET1K_V1)
# 1. 实例化模型结构
model = vgg16(weights=VGG16_Weights.IMAGENET1K_V1)
# - 加载预训练的 VGG16 模型,权重为在 ImageNet 数据集上训练的版本 (IMAGENET1K_V1)

num_features = model.classifier[6].in_features
# - 获取 VGG16 最后一层全连接层的输入特征数量

model.classifier[6] = nn.Linear(num_features, 5)
# - 修改最后一层全连接层,将其输出节点数设置为 5,对应 5 个类别

# 2. 加载最优模型权重
model.load_state_dict(torch.load('saved_models/best_vgg16_flower_lr0.001_bs32.pth', weights_only=True))
# - 从文件 'best_vgg16_flower.pth' 中加载之前训练好的模型权重

model = model.to(device)
# - 将模型移动到指定设备(GPU 或 CPU)


# 获取类别名称
classes = ['daisy', 'dandelion', 'roses', 'sunflowers', 'tulips']

model.eval()
# - 设置模型为评估模式,禁用 Dropout 和 Batch Normalization 的训练行为

# 3. 定义预测函数
def predict_image(image_path, model, transform, classes):
    """
    对单张图像进行预测
    :param image_path: 图像文件路径
    :param model: 加载了权重的 VGG16 模型
    :param transform: 数据预处理方法(与训练一致)
    :param classes: 类别名称列表
    :return: 预测的类别名称和置信度分数
    """
    image = Image.open(image_path).convert('RGB')
    # - 打开图像文件,并将其转换为 RGB 模式(确保有 3 个通道)

    image = transform(image).unsqueeze(0)
    # - 应用数据预处理方法,将图像转换为 Tensor
    # - 调用 `unsqueeze(0)` 增加一个批次维度,形状从 (C, H, W) 变为 (1, C, H, W)

    image = image.to(device)
    # - 将图像数据移动到指定设备(GPU 或 CPU)

    with torch.no_grad():
        # - 禁用梯度计算(推理阶段不需要计算梯度,节省内存并加速)
        output = model(image)
        # - 将图像输入模型,获得预测输出

        _, predicted = torch.max(output, 1)
        # - 获取预测的类别索引,`torch.max` 返回最大值和对应的索引

        confidence = torch.softmax(output, 1)[0] * 100
        # - 对模型的输出应用 softmax 函数,将其转化为概率分布,并转换为百分比形式

        predicted_class = classes[predicted.item()]
        # - 使用类别索引从类别列表中获取对应的类别名称

        confidence_score = confidence[predicted.item()].item()
        # - 获取预测类别对应的置信度分数

    return predicted_class, confidence_score
    # - 返回预测的类别名称和置信度分数

# 4. 定义与训练时相同的预处理方式
predict_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    # - 调整图像大小为 224x224 像素

    transforms.ToTensor(),
    # - 将图像转换为 PyTorch 的张量格式 (C, H, W)

    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    # - 使用 ImageNet 数据集的均值和标准差进行标准化
])

# 5. 进行预测
test_image_path = '../01.图像分类/dataset/flower_datas/val/daisy/105806915_a9c13e2106_n.jpg'
# - 指定测试图像的路径,请替换为实际的图像文件路径

predicted_class, confidence_score = predict_image(test_image_path, model, predict_transform, classes)
# - 调用预测函数,获取预测的类别名称和置信度分数

print(f'预测类别: {predicted_class}, 置信度: {confidence_score:.2f}%')
# - 打印预测结果,包括类别名称和置信度百分比

输出打印:

代码解析
  1. 实例化模型结构:

    • 重新实例化VGG-16模型,并调整全连接层以适应5类分类任务。
  2. 加载最优模型权重:

    • 使用torch.load加载保存的最优模型权重best_vgg16_flower_lr0.001_bs32.pth

    • 将模型移动到设备,并设置为评估模式(model.eval())。

  3. 定义预测函数predict_image

    • 接受图像路径、模型、预处理方法和类别名称作为输入。

    • 读取并预处理图像,添加批次维度后移动到设备。

    • 通过模型进行前向传播,获取预测结果。

    • 使用torch.softmax计算置信度分数,并返回预测类别及其置信度。

  4. 定义预测时的预处理方式:

    • 与训练时保持一致,确保图像的尺寸和标准化方式相同。
  5. 进行预测:

    • 指定测试图像路径,调用预测函数,输出预测类别及其置信度。

4.5 训练过程的可视化

为了更直观地理解模型的训练过程,我们将可视化训练和验证阶段的损失与准确率变化趋势。这有助于判断模型是否出现过拟合或欠拟合,并指导进一步的模型优化。

python 复制代码
import matplotlib.pyplot as plt

# 绘制训练和验证损失曲线
def plot_loss(history):
    plt.figure(figsize=(10,5))
    plt.plot(history['train_losses'], label='训练损失')
    plt.plot(history['val_losses'], label='验证损失')
    plt.xlabel('Epoch')
    plt.ylabel('损失')
    plt.title('训练与验证损失曲线')
    plt.legend()
    plt.show()

# 绘制训练和验证准确率曲线
def plot_accuracy(history):
    plt.figure(figsize=(10,5))
    plt.plot(history['train_accuracies'], label='训练准确率')
    plt.plot(history['val_accuracies'], label='验证准确率')
    plt.xlabel('Epoch')
    plt.ylabel('准确率')
    plt.title('训练与验证准确率曲线')
    plt.legend()
    plt.show()

# 调用绘图函数
plot_loss(history)
plot_accuracy(history)
代码解析
  1. 定义绘图函数plot_loss

    • 接受history字典,绘制训练和验证阶段的损失曲线。

    • X轴为epoch,Y轴为损失值。

    • 添加图例、标题和标签以增强可读性。

  2. 定义绘图函数plot_accuracy

    • 接受history字典,绘制训练和验证阶段的准确率曲线。

    • X轴为epoch,Y轴为准确率。

    • 添加图例、标题和标签以增强可读性。

  3. 调用绘图函数:

    • 使用训练过程中记录的损失和准确率数据,生成可视化图表。

结果展示:

损失曲线示例

准确率曲线示例

可视化分析

通过损失和准确率曲线,我们可以观察到模型在训练过程中的表现:

  • 损失曲线:训练损失逐渐下降,验证损失在初期下降后趋于平稳,甚至可能出现轻微上升,提示模型可能开始过拟合。

  • 准确率曲线:训练准确率逐渐提高,验证准确率在初期上升后趋于稳定,验证集准确率的提升放缓,进一步提示过拟合的风险。

这些观察结果有助于我们决定是否需要进一步优化模型,如引入正则化、调整学习率或增加数据量等。


模型优化与防止过拟合

在前面的训练过程中,我们可能会观察到模型在训练集上表现良好,但在验证集上表现欠佳,或验证集准确率不再提升,甚至下降。这通常是由于模型过于复杂或数据不足导致的过拟合现象。为了解决这些问题,我们可以采取以下几种优化方法:

1. 使用更深层的预训练模型

虽然VGGNet已经表现出色,但在某些任务中,使用更深层或更先进的模型(如ResNet、DenseNet等)可能会带来更好的性能。然而,考虑到本文的教学目的,我们继续使用VGGNet。

2. 数据增强

数据增强通过对训练图像进行随机变换,增加数据的多样性,提升模型的泛化能力。我们已经在数据预处理中应用了一些基本的数据增强技术,如随机水平翻转和随机旋转。根据需要,还可以添加更多的数据增强方法,如随机裁剪、颜色抖动等。

python 复制代码
train_transforms = transforms.Compose([
    transforms.Resize((224, 224)),
    # - 调整图像大小为 224x224 像素
    # - 目的:统一图像输入尺寸,方便后续模型处理

    transforms.RandomHorizontalFlip(),
    # - 随机水平翻转图像,概率为 50%
    # - 目的:通过翻转图像增强数据集,增加模型的鲁棒性

    transforms.RandomRotation(15),
    # - 随机旋转图像,旋转角度范围在 ±15 度之间
    # - 目的:使模型更具泛化能力,适应旋转视角的变化

    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.2),
    # - 颜色抖动,随机调整图像的亮度、对比度、饱和度和色调
    # - 参数解释:
    #   brightness=0.2:亮度随机变化范围为 ±20%
    #   contrast=0.2:对比度随机变化范围为 ±20%
    #   saturation=0.2:饱和度随机变化范围为 ±20%
    #   hue=0.2:色调随机变化范围为 ±20%(值范围 -0.5 到 0.5)
    # - 目的:增强数据集的多样性,让模型适应不同光线和色彩条件

    transforms.ToTensor(),
    # - 将图像转换为 PyTorch 张量(Tensor),并归一化到 [0, 1] 范围
    # - 转换格式为 (H, W, C) → (C, H, W),适配 PyTorch 模型输入

    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    # - 按照 ImageNet 数据集的均值和标准差对图像进行标准化
    # - 均值:[0.485, 0.456, 0.406](分别对应 RGB 三个通道)
    # - 标准差:[0.229, 0.224, 0.225](分别对应 RGB 三个通道)
    # - 目的:让模型适应 ImageNet 预训练模型的输入要求,加速收敛并提高准确性
])

4. 引入正则化技术

Dropout 是一种有效的正则化技术,通过在训练过程中随机丢弃一部分神经元,防止模型过拟合。VGGNet的全连接层已经包含了Dropout层,但我们可以根据需要调整其概率或在卷积层中引入额外的Dropout层。

python 复制代码
# 修改全连接层,调整Dropout概率
model.classifier = nn.Sequential(
    nn.Linear(25088, 4096),
    nn.ReLU(),
    nn.Dropout(0.5),  # Dropout概率调整为0.5
    nn.Linear(4096, 4096),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(4096, 5)
)
model = model.to(device)

5. 早停法(Early Stopping)

早停法通过监控验证集的性能,在验证集性能不再提升时提前停止训练,防止模型过拟合。

python 复制代码
import torch
import torch.optim as optim
from tqdm import tqdm
import copy

# 定义使用早停法的模型训练函数
def train_model_with_early_stopping(model, criterion, optimizer, train_loader, val_loader, num_epochs=25, patience=5):
    """
    使用早停法训练模型
    :param model: 神经网络模型
    :param criterion: 损失函数
    :param optimizer: 优化器
    :param train_loader: 训练集数据加载器
    :param val_loader: 验证集数据加载器
    :param num_epochs: 总训练轮次
    :param patience: 早停的耐心值(连续多少个 epoch 验证准确率不提升时停止训练)
    :return: 训练完成的模型和训练记录
    """
    best_model_wts = copy.deepcopy(model.state_dict())
    # 保存最佳模型的权重
    best_acc = 0.0  # 初始化最佳验证准确率
    epochs_no_improve = 0  # 连续验证准确率未提升的 epoch 数

    # 用于记录每个 epoch 的损失和准确率
    train_losses = []
    val_losses = []
    train_accuracies = []
    val_accuracies = []

    for epoch in range(num_epochs):
        print(f'Epoch {epoch+1}/{num_epochs}')  # 打印当前 epoch
        print('-' * 10)  # 分隔线

        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()  # 设置模型为训练模式
                dataloader = train_loader  # 使用训练集数据加载器
            else:
                model.eval()  # 设置模型为评估模式
                dataloader = val_loader  # 使用验证集数据加载器

            running_loss = 0.0  # 累积损失
            running_corrects = 0  # 累积正确预测数

            # 遍历每个批次
            for inputs, labels in tqdm(dataloader, desc=f'{phase}'):
                inputs = inputs.to(device)  # 将输入数据移动到指定设备(GPU 或 CPU)
                labels = labels.to(device)  # 将标签移动到指定设备

                # 前向传播
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)  # 计算模型输出
                    _, preds = torch.max(outputs, 1)  # 获取预测类别
                    loss = criterion(outputs, labels)  # 计算损失

                    # 反向传播和优化(仅训练阶段)
                    if phase == 'train':
                        optimizer.zero_grad()  # 清空梯度
                        loss.backward()  # 反向传播计算梯度
                        optimizer.step()  # 更新模型参数

                # 累积损失和正确预测数
                running_loss += loss.item() * inputs.size(0)
                # - `loss.item()` 是批次的平均损失,需要乘以批次大小恢复总损失
                running_corrects += torch.sum(preds == labels.data)
                # - 统计正确预测的样本数

            # 计算每个 epoch 的平均损失和准确率
            epoch_loss = running_loss / len(dataloader.dataset)
            # - 总损失除以数据集大小,得到平均损失
            epoch_acc = running_corrects.double() / len(dataloader.dataset)
            # - 正确预测数除以数据集大小,得到准确率

            print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}')
            # 打印当前阶段的损失和准确率

            # 记录损失和准确率
            if phase == 'train':
                train_losses.append(epoch_loss)
                train_accuracies.append(epoch_acc.item())
            else:
                val_losses.append(epoch_loss)
                val_accuracies.append(epoch_acc.item())

                # 检查是否为最佳模型
                if epoch_acc > best_acc:
                    best_acc = epoch_acc  # 更新最佳验证准确率
                    best_model_wts = copy.deepcopy(model.state_dict())
                    # 保存当前模型的最佳权重
                    torch.save(model.state_dict(), 'best_vgg16_flower.pth')
                    print('保存了最优模型')
                    epochs_no_improve = 0  # 重置耐心计数
                else:
                    epochs_no_improve += 1  # 验证准确率未提升
                    # 如果连续未提升的 epoch 数达到耐心值,则触发早停
                    if epochs_no_improve >= patience:
                        print('早停法触发,停止训练')
                        model.load_state_dict(best_model_wts)
                        # 加载最佳模型权重
                        history = {
                            'train_losses': train_losses,
                            'val_losses': val_losses,
                            'train_accuracies': train_accuracies,
                            'val_accuracies': val_accuracies
                        }
                        return model, history

        print()  # 每个 epoch 结束后换行

    print(f'最佳验证准确率: {best_acc:.4f}')
    # 打印最佳验证准确率

    model.load_state_dict(best_model_wts)
    # 加载最佳模型权重

    # 保存训练过程的记录
    history = {
        'train_losses': train_losses,  # 每个 epoch 的训练损失
        'val_losses': val_losses,  # 每个 epoch 的验证损失
        'train_accuracies': train_accuracies,  # 每个 epoch 的训练准确率
        'val_accuracies': val_accuracies  # 每个 epoch 的验证准确率
    }

    return model, history  # 返回最佳模型和训练记录

# 设置优化器为 AdamW
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-2)
# - 使用 AdamW 优化器
# - 参数:
#   - `lr=1e-3`:学习率设置为 0.001
#   - `weight_decay=1e-2`:权重衰减因子,用于 L2 正则化,防止过拟合

# 使用早停法进行训练
model, history = train_model_with_early_stopping(
    model, 
    criterion, 
    optimizer, 
    train_loader, 
    val_loader, 
    num_epochs=25, 
    patience=5
)

6. 模型优化总结

通过上述优化方法,我们可以显著提升VGGNet在五类鲜花分类任务中的性能,具体包括:

  • 利用预训练模型:加速训练过程,提升初始性能。

  • 数据增强:增加数据多样性,提升模型泛化能力。

  • 学习率调度:动态调整学习率,优化训练过程。

  • 正则化技术:防止模型过拟合,提升验证集性能。

  • 早停法:避免过度训练,保存最佳模型。

这些优化方法在实际应用中相辅相成,共同提升模型的整体表现。


总结与展望

本文详细介绍了VGGNet的架构及其在图像分类任务中的应用,特别是基于预训练模型进行五类鲜花分类的实战案例。通过数据准备与预处理、模型实例化与参数调整、模型训练与保存最优模型、模型导入与预测以及训练过程的可视化,全面展示了如何在PyTorch 2.5.1与TorchVision 0.20.1环境下高效应用VGGNet。

尽管VGGNet在图像分类任务中表现出色,但其庞大的参数量和计算资源需求也带来了挑战。随着深度学习技术的不断进步,更多高效且精确的网络架构如ResNet、DenseNet等被提出,未来的研究将继续探索更为高效的网络结构和训练方法。

通过本文的学习,读者不仅掌握了VGGNet的基本知识和实现方法,还了解了如何在实际项目中优化模型,提升其性能。希望本文能为您的深度学习之路提供有力的支持和指导。


参考文献

  1. Simonyan, K., & Zisserman, A. (2014). Very Deep Convolutional Networks for Large-Scale Image Recognition . arXiv:1409.1556

  2. PyTorch Documentation. torchvision.models.VGG . Retrieved from Models and pre-trained weights --- Torchvision 0.20 documentation

  3. Wikipedia. VGGNet . Retrieved from https://en.wikipedia.org/wiki/VGG_net

  4. CSDN博客. VGGNet详解与实现 . Retrieved from https://blog.csdn.net


如果觉得文章有用,辛苦点赞,收藏,转发,关注一波,如果有其他不懂的可以私信我哈~

你想用深度学习解决哪个现实世界的那些问题?在评论区告诉我们

相关推荐
deephub3 分钟前
LEC: 基于Transformer中间层隐藏状态的高效特征提取与内容安全分类方法
人工智能·深度学习·transformer·大语言模型·特征提取
Eric.Lee20219 分钟前
数据集-目标检测系列 车牌检测&识别 数据集 CCPD2019
人工智能·python·目标检测·计算机视觉·车牌识别·车牌检测·yolo检测
Hugging Face15 分钟前
欢迎 PaliGemma 2 – 来自 Google 的新视觉语言模型
人工智能·语言模型·自然语言处理
MUTA️27 分钟前
Deformable DETR中的look forword once
计算机视觉
Bony-33 分钟前
基于卷积神经网络(CNN)和ResNet50的水果与蔬菜图像分类系统
人工智能·分类·cnn
UQI-LIUWJ39 分钟前
datasets 笔记: 文本数据集的预处理(Tokenization)
人工智能·笔记·深度学习
架构师李肯41 分钟前
【活动邀请·深圳】深圳COC社区 & 深圳 AWS UG 2024 re:Invent re:Cap
人工智能
Python机器学习AI1 小时前
融合机器学习算法:用VotingClassifier实现分类多模型的投票集成
人工智能·机器学习·分类
WeeJot嵌入式1 小时前
长短期记忆网络(LSTM):深度学习中的序列数据处理利器
人工智能·深度学习·lstm
胡耀超1 小时前
如何从全局视角规划项目与战略决策(“精准接送”案例、技术架构设计与选型、业务逻辑及产品商业模式探讨)
大数据·数据挖掘·软件架构·商业模式·数据管理