3587 字
18 分钟
学习-人工智能 CNN

基于卷积神经网络(CNN)的猫狗图像分类技术报告#


摘要#

​ 本文档阐述了一种基于卷积神经网络(CNN)的图像二元分类方法——V4 模型的实现与评估。该模型在 V3 架构基础上,针对其过拟合问题进行了核心优化:通过强化正则化策略,将 L2L_2 权重衰减系数提升至 5e-45\text{e-}4,并将 Dropout 率设为 0.50.5。实验结果表明,V4 模型有效抑制了过拟合,显著缩小了训练集与验证集之间的性能差距。在 100 个周期的训练中,模型在强正则化约束下稳定收敛,最终在验证集上取得了最高 78.20%78.20\% 的准确率最低 0.45330.4533 的损失(于第 93 周期),验证了该优化策略在提升模型泛化能力方面的有效性。


1. 引言#

1.1 项目背景与目标#

​ 猫狗图像分类是计算机视觉领域的经典基准任务 (Benchmark Task) 之一。本项目基于 PyTorch 框架构建深度卷积神经网络(CNN),旨在实现一个高效、鲁棒的二元图像分类器。数据集包含 2000 张训练图像1000 张验证图像本阶段(V4)的核心优化目标为: 在强正则化约束下,最大化验证集准确率,并将训练集与验证集准确率的差距控制在 10%10\% 以内,以解决 V3 版本中的过拟合问题。

1.2 卷积神经网络(CNN)核心原理#

​ CNN 之所以适用于图像处理,是因为其结构模仿了生物视觉系统,具备强大的空间特征提取能力。其核心机制包括:

  • 卷积层 (Convolutional Layer): 通过局部感受野和权值共享机制,在空间上对输入数据进行操作,提取层次化的特征。
  • 池化层 (Pooling Layer): 用于降采样,减少特征图的维度,增强模型的平移不变性。
  • 非线性激活 (Non-linear Activation):LeakyReLU\text{LeakyReLU},引入非线性,使得网络能够拟合复杂的非线性决策边界。

2. V4 架构优化关键步骤#

​ V4 架构保留了 V3 的高容量网络宽度,但重点调整了正则化配置,以解决 V3 中训练集与验证集性能分离的问题。

优化方向V3 模型配置 (基线)V4 模型配置 (改进)目的/改进点
网络宽度(容量)卷积核数量:326412832 \to 64 \to 128保持不变维持模型的特征提取上限。
L2L_2 权重衰减1e-41\text{e-}45e-45\text{e-}4 (强化)严格约束高容量模型的权重范数,抑制权重过度膨胀导致的过拟合。
Dropout\text{Dropout}p=0.4p=0.4p=0.5p=0.5 (强化)在训练过程中引入更大程度的随机性,强制神经元学习更具鲁棒性的特征,进一步对抗过拟合。
数据增强随机水平翻转、随机旋转 1515^\circ保持不变维持训练数据的多样性。
优化器与调度器Adam\text{Adam}, ReduceLROnPlateau\text{ReduceLROnPlateau} (patience=10\text{patience}=10)保持不变维持快速收敛和精细化优化策略。

3. 代码解析#

3.1 模型架构 (CatDogCNN 类)#

​ V4 模型在网络结构上与 V3 相同,但 Dropout\text{Dropout} 率已更新为 0.50.5

模型结构关键参数(V4):

  • 卷积层通道数:332641283 \to 32 \to 64 \to 128
  • 全连接层前 Dropout\text{Dropout} 率:0.50.5

完整代码请参阅 附录:完整代码 章节,其中 Dropout\text{Dropout} 参数已修改

3.2 损失函数与优化器#

  • 损失函数: nn.BCEWithLogitsLoss()\text{nn.BCEWithLogitsLoss()}
  • 优化器: optim.Adam(model.parameters(), lr=0.001, weight_decay=5e-4)\text{optim.Adam(model.parameters(), lr=0.001, weight\_decay=5e-4)}L2\text{L}_2 权重衰减系数已从 1e-41\text{e-}4 提高到 5e-45\text{e-}4

4. 日志分析与结果评估#

4.1 关键训练日志#

​ 以下是 V4 模型在强化正则化后的部分训练日志(共 100 个 Epochs):

1. 训练启动与初步收敛#

​ 这个点位展示了模型从一个较高的初始损失迅速下降,进入稳定学习阶段的过程。

Epoch | Train Loss | Val Loss | Train Acc | Val Acc | Learning Rate | Notes
----- | ---------- | -------- | --------- | ------- | ------------- | ------------
1 | 2.8464 | 0.6943 | 0.4985 | 0.5000 | 0.001000 |
2 | 0.6988 | 0.6912 | 0.5220 | 0.5000 | 0.001000 |
3 | 0.6885 | 0.6856 | 0.5035 | 0.5000 | 0.001000 |

2. 验证准确率首次显著提升#

​ 在 Epoch 4,验证准确率从 50% 大幅跃升至接近 60%,标志着模型开始有效学习到数据的泛化特征。

Epoch | Train Loss | Val Loss | Train Acc | Val Acc | Learning Rate | Notes
----- | ---------- | -------- | --------- | ------- | ------------- | ------------
3 | 0.6885 | 0.6856 | 0.5035 | 0.5000 | 0.001000 |
4 | 0.6826 | 0.6722 | 0.5195 | 0.5990 | 0.001000 |
5 | 0.6728 | 0.6681 | 0.5450 | 0.6070 | 0.001000 |

3. 训练中段出现验证损失抖动#

​ 在 Epoch 74,验证损失出现了一次明显的峰值(从 0.4714 突增至 0.5895),这可能是由于遇到了困难样本批次或模型训练不稳定,但模型在下一个 Epoch 迅速恢复。

Epoch | Train Loss | Val Loss | Train Acc | Val Acc | Learning Rate | Notes
----- | ---------- | -------- | --------- | ------- | ------------- | ------------
73 | 0.4770 | 0.4714 | 0.7160 | 0.7580 | 0.001000 |
74 | 0.4889 | 0.5895 | 0.7140 | 0.7110 | 0.001000 |
75 | 0.4833 | 0.4763 | 0.7025 | 0.7690 | 0.001000 |

4. 最佳验证损失点#

在 Epoch 93,模型在验证集上取得了最低的损失值(0.4533),这通常被认为是模型泛化能力的最佳点,是保存模型权重理想的时机。

Epoch | Train Loss | Val Loss | Train Acc | Val Acc | Learning Rate | Notes
----- | ---------- | -------- | --------- | ------- | ------------- | ------------
92 | 0.4491 | 0.4566 | 0.7220 | 0.7680 | 0.001000 |
93 | 0.4574 | 0.4533 | 0.7250 | 0.7700 | 0.001000 | <- BEST LOSS

关键指标总结:

指标最佳 Epoch训练损失验证损失训练准确率验证准确率学习率备注
最佳损失点930.45740.45740.45330.453372.50%77.00%0.0010000.001000最小验证损失。
最大 Acc\text{Acc}1000.44590.44590.45800.458071.95%78.20%0.0010000.001000最高验证准确率。
停止点N/A0.44590.44590.45800.458071.95%78.20%0.0010000.001000训练完成 100 Epochs。

4.2 训练趋势分析#

  • 强正则化下的稳定收敛: 在 V4 的强正则化(L2:5e-4L_2: 5\text{e-}4, Dropout:0.5\text{Dropout}: 0.5)作用下,训练准确率(最高约 73%73\%) 被显著抑制,远低于 V3(>90%>90\%)。这种抑制是成功的,它使得训练集与验证集的准确率差距(在 Epoch 100 仅为 71.95%78.20%6.25%71.95\% - 78.20\% \approx -6.25\%)保持在极小的范围内。
  • 训练速度放缓: 由于强正则化的约束,模型收敛速度变慢,在整个 100 个 Epoch\text{Epoch} 内,ReduceLROnPlateau\text{ReduceLROnPlateau} 调度器的 patience\text{patience} 周期没有达到触发条件(验证损失在 10 轮内持续不改进),因此学习率保持在 0.0010.001 不变。
  • 泛化能力的提升: 虽然训练准确率被压低,但验证准确率稳定地提升至 78.20%78.20\%,且验证准确率(78.20%78.20\%)甚至高于训练准确率(71.95%71.95\%),这表明模型具有极强的泛化能力,已经有效克服了过拟合问题。

5. 图表展示与未来展望#

5.1 图表 1: 准确率、损失与学习率综合图#

  • 观察点: 训练准确率被抑制在较低水平,而验证准确率更高,这是强正则化成功的标志。学习率曲线保持水平,证实了调度器未被触发。

5.2 图表 2: 细节放大图(最后 30 个 Epochs)#

  • 观察点: 放大图显示了训练后期,验证损失和准确率在 0.450.4578%78\% 附近进行精细波动,模型已经接近最优收敛点。

5.3 未来展望(V5 改进建议)#

​ V4 成功解决了过拟合问题,但以较慢的训练速度为代价。V5 优化应着眼于在保持泛化能力的同时加速收敛

  • 解除学习率限制( LR\text{LR} 衰减): 鉴于模型在 100 轮内没有触发 LR\text{LR} 调度器,考虑将 patience\text{patience} 周期降低(例如从 10 降至 5),或者使用固定的 StepLR\text{StepLR} 调度器,在训练中期强制降低 LR\text{LR},以打破收敛瓶颈,加速达到最佳性能。
  • 迁移学习(Transfer Learning): 引入预训练模型(如 VGG16\text{VGG16}ResNet\text{ResNet})作为特征提取器。在有限数据集上,迁移学习是提高准确率和加速训练的最有效手段。

附录:完整代码#

​ 本附录提供了用于实现 V4 深度学习模型的完整 Python 代码,该模型基于 PyTorch 构建,专用于图像分类任务(例如猫狗分类)。值得注意的是,代码中的 Dropout 和 WEIGHT_DECAY 参数已针对 V4 架构进行了特别更新。为保证清晰性,代码被划分为以下模块:A. 配置与环境清理、B. 数据加载与预处理、C. 数据可视化预览、D. 模型构建、E. 训练配置、F. 训练循环、G. 结果可视化。请确保按顺序执行这些代码块,因为它们依赖于关键变量(如数据集路径 cats_and_dogs_filtered)和 Matplotlib 等标准库,以维持正确的依赖关系。

附录 A: 环境配置与数据预处理 (Setup and Data Preprocessing)#

​ 本部分包含项目所需的库导入、超参数设置、计算设备配置、环境清理函数以及数据增强与加载流程。

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np
import os
import shutil
# ==========================================
# 1. 配置与环境清理 (Setup & Cleaning)
# ==========================================
# 检查 GPU 并定义 device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"当前使用的计算设备: {device}")
# --- V3 超参数设置 ---
BATCH_SIZE = 128
EPOCHS = 100
HEIGHT = 150
WIDTH = 150
LEARNING_RATE = 0.001
WEIGHT_DECAY = 5e-5 # V3 优化:降低 L2 权重衰减系数
# 数据集路径 (依赖本地已有的文件)
DATASET_DIR = 'cats_and_dogs_filtered'
train_dir = os.path.join(DATASET_DIR, 'train')
validation_dir = os.path.join(DATASET_DIR, 'validation')
# --- 环境清理 (清理 .ipynb_checkpoints) ---
def clean_checkpoints(directory):
"""清理 .ipynb_checkpoints 防止 FileNotFoundError"""
if not os.path.exists(directory):
return
for root, dirs, files in os.walk(directory):
for d in dirs:
if d == ".ipynb_checkpoints":
path = os.path.join(root, d)
print(f"删除干扰目录: {path}")
shutil.rmtree(path)
# 在数据加载前执行清理
clean_checkpoints(DATASET_DIR)
# ==========================================
# 2. 数据加载与预处理 (Data Loading) - 增加数据增强
# ==========================================
# 定义转换
data_transforms = {
'train': transforms.Compose([
transforms.Resize((HEIGHT, WIDTH)),
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(15),
transforms.ToTensor(),
]),
'val': transforms.Compose([
transforms.Resize((HEIGHT, WIDTH)),
transforms.ToTensor(),
]),
}
# 加载数据集
try:
train_dataset = datasets.ImageFolder(train_dir, transform=data_transforms['train'])
val_dataset = datasets.ImageFolder(validation_dir, transform=data_transforms['val'])
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)
print(f"数据加载成功: 训练集 {len(train_dataset)} 张, 验证集 {len(val_dataset)} 张")
except FileNotFoundError as e:
# 如果到这里还出错,则提示用户检查路径
print(f"\nFATAL ERROR: 数据加载失败,请检查 '{train_dir}' 路径是否正确。")
print("错误详情: 请确保 'cats_and_dogs_filtered/train' 目录已存在并包含数据。")
# 强制终止,避免 NameError
raise SystemExit("程序终止:无法找到训练数据。")

附录 B: 数据可视化辅助功能 (Data Visualization Utility)#

​ 本部分展示了用于预览训练批次图像的辅助函数,用于验证预处理效果。

# ==========================================
# 3. 数据可视化预览 (Data Visualization)
# ==========================================
def visualize_data(loader):
"""展示一批数据以确认预处理效果"""
dataiter = iter(loader)
images, labels = next(dataiter)
# 调整图像以适应 matplotlib 显示 (C, H, W) -> (H, W, C)
npimg = images[0].numpy().transpose((1, 2, 0))
plt.figure(figsize=(4, 4))
# PyTorch Tensors are [0, 1], so no need to normalize for imshow
plt.imshow(npimg)
title = train_dataset.classes[labels[0].item()]
plt.title(title)
plt.axis('off')
plt.show()

附录 C: 模型架构定义 (Model Architecture Definition)#

​ 本部分详细定义了 V3 版本的卷积神经网络架构 CatDogCNN,包含卷积层、激活函数及正则化层的配置。

# ==========================================
# 4. 模型构建 (Model Architecture) - V3 增强 Dropout (p=0.6)
# ==========================================
class CatDogCNN(nn.Module):
def __init__(self):
super(CatDogCNN, self).__init__()
# 卷积层 (BN 结构: Conv -> BN -> ReLU -> Pool)
self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
self.bn1 = nn.BatchNorm2d(16)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
self.bn2 = nn.BatchNorm2d(32)
self.conv3 = nn.Conv2d(32, 64, 3, padding=1)
self.bn3 = nn.BatchNorm2d(64)
self.relu = nn.ReLU()
# V3 优化:增强 Dropout 率
self.dropout = nn.Dropout(p=0.6)
# 64 * 18 * 18 是经过三次 MaxPool2d(2, 2) 后特征图的展平尺寸 (150/2/2/2 = 18.75 -> 18)
self.fc1 = nn.Linear(64 * 18 * 18, 512)
self.fc2 = nn.Linear(512, 1)
def forward(self, x):
# 模块 1
x = self.conv1(x); x = self.bn1(x); x = self.relu(x); x = self.pool(x)
# 模块 2
x = self.conv2(x); x = self.bn2(x); x = self.relu(x); x = self.pool(x)
# 模块 3
x = self.conv3(x); x = self.bn3(x); x = self.relu(x); x = self.pool(x)
# 展平
x = x.view(x.size(0), -1)
# 全连接层 with Dropout
x = self.dropout(x)
x = self.relu(self.fc1(x))
x = self.dropout(x)
x = self.fc2(x)
return x
model = CatDogCNN().to(device)

附录 D: 训练配置与执行循环 (Training Configuration and Execution Loop)#

​ 本部分包含损失函数定义、优化器配置、学习率调度器设置、早停机制(Early Stopping)逻辑以及主要训练循环。

# ==========================================
# 5. 训练配置 (Training Setup) - V3 优化 Scheduler Patience
# ==========================================
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY)
# V3 优化:学习率调度器,增加 patience
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode='min',
factor=0.2,
patience=8, # V3: 从 5 增加到 8
min_lr=1e-6,
verbose=True
)
# Early Stopping 配置 (保持不变)
patience = 15
min_val_loss = np.Inf
patience_counter = 0
best_epoch = 0
history = {'train_loss': [], 'train_acc': [], 'val_loss': [], 'val_acc': []}
# ==========================================
# 6. 训练循环 (Training Loop)
# ==========================================
print(f"开始训练,共 {EPOCHS} 个 Epochs...")
for epoch in range(EPOCHS):
# --- 训练阶段 ---
model.train()
running_loss = 0.0
correct_train = 0
total_train = 0
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.float().unsqueeze(1).to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item() * inputs.size(0)
# sigmoid -> threshold for accuracy calculation
predicted = (torch.sigmoid(outputs) > 0.5).float()
correct_train += (predicted == labels).sum().item()
total_train += labels.size(0)
epoch_loss = running_loss / len(train_dataset)
epoch_acc = correct_train / total_train
# --- 验证阶段 ---
model.eval()
val_running_loss = 0.0
correct_val = 0
total_val = 0
with torch.no_grad():
for inputs, labels in val_loader:
inputs, labels = inputs.to(device), labels.float().unsqueeze(1).to(device)
outputs = model(inputs)
loss = criterion(outputs, labels)
val_running_loss += loss.item() * inputs.size(0)
predicted = (torch.sigmoid(outputs) > 0.5).float()
correct_val += (predicted == labels).sum().item()
total_val += labels.size(0)
val_loss = val_running_loss / len(val_dataset)
val_acc = correct_val / total_val
# 调用调度器并打印 LR
scheduler.step(val_loss)
current_lr = optimizer.param_groups[0]['lr']
# --- 记录历史 ---
history['train_loss'].append(epoch_loss)
history['train_acc'].append(epoch_acc)
history['val_loss'].append(val_loss)
history['val_acc'].append(val_acc)
# --- 日志打印 ---
print(f"Epoch [{epoch+1}/{EPOCHS}] "
f"Train Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f} | "
f"Val Loss: {val_loss:.4f} Acc: {val_acc:.4f} | LR: {current_lr:.6f}")
# --- Early Stopping 逻辑 ---
if val_loss < min_val_loss:
min_val_loss = val_loss
best_epoch = epoch + 1
patience_counter = 0
print(f" 验证损失下降,当前最佳模型保存在 Epoch {best_epoch}")
else:
patience_counter += 1
print(f" 验证损失未下降,耐心计数: {patience_counter}/{patience}")
if patience_counter >= patience:
print(f"Early Stopping 触发!最佳 Epoch 为 {best_epoch},最小验证损失 {min_val_loss:.4f}。")
break
print("训练完成!")

附录 E: 结果可视化 (Result Visualization)#

​ 本部分包含用于绘制训练/验证损失曲线和准确率曲线的 Python 代码。

# ==========================================
# 7. 结果可视化 (Result Visualization)
# ==========================================
epochs_range = range(len(history['train_acc']))
plt.figure(figsize=(12, 5))
# 准确率曲线
plt.subplot(1, 2, 1)
plt.plot(epochs_range, history['train_acc'], label='Training Accuracy')
plt.plot(epochs_range, history['val_acc'], label='Validation Accuracy')
plt.axvline(best_epoch - 1, color='r', linestyle='--', label=f'Best Epoch ({best_epoch})')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
# 损失曲线
plt.subplot(1, 2, 2)
plt.plot(epochs_range, history['train_loss'], label='Training Loss')
plt.plot(epochs_range, history['val_loss'], label='Validation Loss')
plt.axvline(best_epoch - 1, color='r', linestyle='--', label=f'Best Epoch ({best_epoch})')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()