1
LeNet网络是第一个商用成功的CNN模型,属于CNN的始祖级别的开山之作。
其输入到输出结构如下:
输入层=》卷积层=》激活函数=》池化层=》
卷积层=》激活函数=》池化层 =》
展平操作=》
全连接层=》激活函数=》全连接层=》输出层。
它原始是7层结构,如上面结构带"层"的一共8个,但是输入层不算(为啥?因为输入层不需要计算)。
现在又有个说法------有意思------我的理解就是他们按照类来重新划分了,现在这个结构由卷积块和全连接块组成。
卷积块 有如下几部分组成:
卷积层=》激活函数=》池化层
这个卷积块的作用就是特征信息学习。
原始LeNet有2个卷积块,那么现代改进可以加深,比如你可以搞成3个、4个等卷积块。
全连接块 就是展平后面的全连接神经网络部分:
全连接层=》激活函数=》全连接层=》激活函数=》输出层。
展平操作可以看作是全连接神经网络的数据输入层------这让我想起了因为是输入层所以不算层数的规则。
2 改进1
c
# 导入PyTorch深度学习框架
import torch
# 导入PyTorch的神经网络模块
import torch.nn as nn
# 导入numpy用于科学计算
import numpy as np
# 导入matplotlib用于绘图
import matplotlib.pyplot as plt
# 从torchsummary导入模型结构可视化工具
from torchsummary import summary
# 从torchvision.datasets导入CIFAR10数据集
from torchvision.datasets import CIFAR10
# 从torchvision.transforms导入数据转换模块
from torchvision.transforms import Compose, ToTensor, Normalize
# 从torch.utils.data导入数据加载相关模块
from torch.utils.data import DataLoader, Dataset
# 导入PyTorch的优化器模块
import torch.optim as optim
# 导入操作系统模块,用于文件和目录操作
import os
# 从datetime导入日期时间模块
from datetime import datetime
# 导入随机数模块
import random
# ========== 配置宏定义 ==========
# 注释行,说明下面是配置定义
# 设置为True使用GPU,False使用CPU
USE_GPU = True
# 定义批量大小,每次训练128个样本
BATCH_SIZE = 128
# 定义训练的总轮数
EPOCHS = 50
# 定义初始学习率
LEARNING_RATE = 0.001
# 是否保存模型的开关
SAVE_MODEL = True
# 模型保存的目录路径
MODEL_DIR = "./models"
# 随机种子,用于保证结果可重复
SEED = 42
# 检查CUDA是否可用(CUDA是NVIDIA的GPU计算平台)
cuda_available = torch.cuda.is_available()
# 打印CUDA可用性信息
print(f"CUDA Available: {cuda_available}")
# ===============================
# 设置随机种子,确保可重复性
def set_seed(seed=42):
# 设置Python内置random模块的随机种子
random.seed(seed)
# 设置numpy的随机种子
np.random.seed(seed)
# 设置PyTorch的CPU随机种子
torch.manual_seed(seed)
# 如果CUDA可用
if cuda_available:
# 设置当前GPU的随机种子
torch.cuda.manual_seed(seed)
# 设置所有GPU的随机种子
torch.cuda.manual_seed_all(seed)
# 设置cuDNN确定性模式,保证结果可重复
torch.backends.cudnn.deterministic = True
# 关闭cuDNN基准测试
torch.backends.cudnn.benchmark = False
# 调用函数设置随机种子
set_seed(SEED)
# 根据配置选择设备
if USE_GPU and cuda_available:
# 如果配置使用GPU且GPU可用,选择CUDA设备
device = torch.device("cuda")
# 打印GPU名称
print(f"Using GPU: {torch.cuda.get_device_name(0)}")
# 打印GPU属性
print(f"GPU Properties: {torch.cuda.get_device_properties(0)}")
else:
# 否则使用CPU设备
device = torch.device("cpu")
# 打印使用CPU信息
print("Using CPU")
# 数据集加载
# 加载CIFAR10训练数据集
cifar10_train_dataset = CIFAR10(root='./data', # 数据集保存路径
train=True, # 加载训练集
download=True, # 如果本地没有则下载
# 数据转换:将PIL图像转换为张量
transform=Compose([ToTensor()]))
# 创建训练数据加载器
cifar10_train_loader = DataLoader(cifar10_train_dataset, # 要加载的数据集
batch_size=BATCH_SIZE, # 批量大小
shuffle=True, # 打乱数据顺序
num_workers=0) # 加载数据的进程数
# 加载CIFAR10测试数据集
cifar10_test_dataset = CIFAR10(root='./data', # 数据集保存路径
train=False, # 加载测试集
download=True, # 如果本地没有则下载
# 数据转换:将PIL图像转换为张量
transform=Compose([ToTensor()]))
# 创建测试数据加载器
cifar10_test_loader = DataLoader(cifar10_test_dataset, # 要加载的数据集
batch_size=BATCH_SIZE, # 批量大小
shuffle=False, # 测试集不打乱
num_workers=0) # 加载数据的进程数
# 打印训练集数据的形状
print(f"Train dataset shape: {cifar10_train_dataset.data.shape}")
# 打印测试集数据的形状
print(f"Test dataset shape: {cifar10_test_dataset.data.shape}")
# 打印数据集类别名称
print(f"Classes: {cifar10_train_dataset.classes}")
# 打印类别到索引的映射
print(f"Class to index: {cifar10_train_dataset.class_to_idx}")
# 获取测试集的第一个样本
img, lb = cifar10_test_dataset[0]
# 打印图像的形状
print(f"Image shape: {img.shape}")
# 打印标签和对应的类别名称
print(f"Label: {lb} ({cifar10_train_dataset.classes[lb]})")
# 定义干净的LeNet模型类
class CleanLenet(nn.Module):
# 类的初始化函数
def __init__(self):
# 调用父类的初始化方法
super().__init__()
# 定义第一个卷积层:输入通道3(RGB),输出通道6,卷积核大小5×5
self.conv1 = nn.Conv2d(3, 6, 5)
# 定义第一个最大池化层:池化窗口2×2,步长2
self.pool1 = nn.MaxPool2d(2, 2)
# 定义第二个卷积层:输入通道6,输出通道16,卷积核大小5×5
self.conv2 = nn.Conv2d(6, 16, 5)
# 定义第二个最大池化层
self.pool2 = nn.MaxPool2d(2, 2)
# 定义Dropout层,丢弃概率0.25
self.dropout = nn.Dropout(0.25)
# 定义第一个全连接层:输入维度16×5×5=400,输出120
self.fc1 = nn.Linear(16 * 5 * 5, 120)
# 定义第二个全连接层:输入120,输出84
self.fc2 = nn.Linear(120, 84)
# 定义第三个全连接层:输入84,输出10(CIFAR10有10个类别)
self.fc3 = nn.Linear(84, 10)
# 定义前向传播函数
def forward(self, x):
# 第一个卷积块:卷积 -> ReLU激活 -> 池化
x = torch.relu(self.conv1(x))
x = self.pool1(x)
# 第二个卷积块:卷积 -> ReLU激活 -> 池化
x = torch.relu(self.conv2(x))
x = self.pool2(x)
# 将特征图展平为一维向量
x = x.view(x.size(0), -1)
# 第一个全连接层 -> ReLU激活 -> Dropout
x = torch.relu(self.fc1(x))
x = self.dropout(x)
# 第二个全连接层 -> ReLU激活 -> Dropout
x = torch.relu(self.fc2(x))
x = self.dropout(x)
# 第三个全连接层(输出层)
x = self.fc3(x)
# 返回输出
return x
# 定义模型评估函数
def evaluate(model, test_loader, device):
"""评估模型在测试集上的性能"""
# 将模型设置为评估模式
model.eval()
# 初始化正确预测的数量
correct = 0
# 初始化总样本数
total = 0
# 不计算梯度,节省内存
with torch.no_grad():
# 遍历测试数据加载器
for images, labels in test_loader:
# 将图像数据移动到指定设备
images = images.to(device)
# 将标签数据移动到指定设备
labels = labels.to(device)
# 前向传播,获取模型输出
outputs = model(images)
# 获取预测结果(最大概率的索引)
_, predicted = torch.max(outputs.data, 1)
# 累加总样本数
total += labels.size(0)
# 累加正确预测的样本数
correct += (predicted == labels).sum().item()
# 计算准确率百分比
accuracy = 100 * correct / total
# 返回准确率
return accuracy
# 定义模型训练函数
def train(model, train_loader, test_loader, epochs, device, model_name="model"):
"""训练函数,支持GPU/CPU,并保存最佳模型"""
# 将模型移动到指定设备
model = model.to(device)
# 定义Adam优化器
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
# 定义学习率调度器(当验证损失不再下降时降低学习率)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min',
factor=0.1, patience=5, verbose=True)
# 定义交叉熵损失函数
criterion = nn.CrossEntropyLoss()
# 记录训练损失的列表
train_losses = []
# 记录验证准确率的列表
val_accuracies = []
# 初始化最佳准确率
best_accuracy = 0.0
# 初始化最佳模型路径
best_model_path = None
# 创建模型保存目录
if SAVE_MODEL and not os.path.exists(MODEL_DIR):
os.makedirs(MODEL_DIR)
# 打印训练信息
print(f"\n开始训练,共 {epochs} 个epochs,batch size: {BATCH_SIZE}")
print(f"学习率: {LEARNING_RATE}")
print(f"随机种子: {SEED}")
print(f"训练设备: {device}")
print("-" * 60)
# 开始训练循环
for epoch in range(epochs):
# 将模型设置为训练模式
model.train()
# 初始化本轮损失累加值
running_loss = 0.0
# 初始化步数计数器
steps = 0
# 遍历训练数据加载器
for batch_idx, (images, labels) in enumerate(train_loader):
# 将数据移动到指定设备
images = images.to(device)
labels = labels.to(device)
# 前向传播
outputs = model(images)
# 计算损失
loss = criterion(outputs, labels)
# 反向传播
# 清空梯度
optimizer.zero_grad()
# 计算梯度
loss.backward()
# 更新参数
optimizer.step()
# 累加损失
running_loss += loss.item()
# 步数加1
steps += 1
# 每100个batch打印一次进度
if batch_idx % 100 == 0:
print(f'Epoch [{epoch + 1}/{epochs}], Step [{batch_idx + 1}/{len(train_loader)}], '
f'Loss: {loss.item():.4f}')
# 计算本轮平均损失
avg_loss = running_loss / steps
# 将平均损失添加到列表
train_losses.append(avg_loss)
# 在验证集上评估模型
val_accuracy = evaluate(model, test_loader, device)
# 将验证准确率添加到列表
val_accuracies.append(val_accuracy)
# 学习率调整
scheduler.step(avg_loss)
# 保存最佳模型
if SAVE_MODEL and val_accuracy > best_accuracy:
# 更新最佳准确率
best_accuracy = val_accuracy
# 生成时间戳
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 构建最佳模型保存路径
best_model_path = os.path.join(MODEL_DIR, f"{model_name}_best_epoch{epoch + 1}_{best_accuracy:.2f}.pth")
# 保存模型检查点
torch.save({
'epoch': epoch + 1, # 当前epoch
'model_state_dict': model.state_dict(), # 模型参数
'optimizer_state_dict': optimizer.state_dict(), # 优化器状态
'loss': avg_loss, # 当前损失
'accuracy': val_accuracy, # 当前准确率
'seed': SEED, # 随机种子
}, best_model_path)
# 打印保存信息
print(f"✨ 保存最佳模型 -> 准确率: {val_accuracy:.2f}%")
# 打印本轮训练信息
print(f'Epoch: {epoch + 1:3d}/{epochs}, '
f'Loss: {avg_loss:.6f}, '
f'Val Accuracy: {val_accuracy:.2f}%, '
f'LR: {optimizer.param_groups[0]["lr"]:.6f}')
# 绘制训练曲线
# 创建图形和子图,1行2列,图形大小12×4英寸
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
# 在第一个子图上绘制损失曲线
ax1.plot(range(1, epochs + 1), train_losses, 'b-', linewidth=2, label='Training Loss')
# 设置x轴标签
ax1.set_xlabel('Epoch')
# 设置y轴标签
ax1.set_ylabel('Loss')
# 设置子图标题
ax1.set_title('Training Loss')
# 显示网格,透明度0.3
ax1.grid(True, alpha=0.3)
# 显示图例
ax1.legend()
# 在第二个子图上绘制准确率曲线
ax2.plot(range(1, epochs + 1), val_accuracies, 'g-', linewidth=2, label='Validation Accuracy')
# 设置x轴标签
ax2.set_xlabel('Epoch')
# 设置y轴标签
ax2.set_ylabel('Accuracy (%)')
# 设置子图标题
ax2.set_title('Validation Accuracy')
# 显示网格,透明度0.3
ax2.grid(True, alpha=0.3)
# 显示图例
ax2.legend()
# 自动调整子图布局
plt.tight_layout()
# 显示图形
plt.show()
# 最终评估
# 在测试集上评估最终模型
final_accuracy = evaluate(model, test_loader, device)
# 打印分割线
print(f"\n{'=' * 60}")
# 打印最终准确率
print(f"训练完成!最终验证集准确率: {final_accuracy:.2f}%")
# 打印最佳准确率
print(f"最佳准确率: {best_accuracy:.2f}%")
# 如果保存了最佳模型,打印路径
if best_model_path:
print(f"最佳模型保存在: {best_model_path}")
# 打印分割线
print(f"{'=' * 60}")
# 返回训练好的模型、训练损失列表、验证准确率列表
return model, train_losses, val_accuracies
# 程序入口
if __name__ == '__main__':
# 创建模型实例
model = CleanLenet()
# 将模型移动到指定设备
model = model.to(device)
# 打印模型摘要
print(f"\n{'=' * 60}")
print(f"模型结构:")
print(f"{'=' * 60}\n")
# 根据设备类型打印模型摘要
if device.type == 'cuda':
# 在GPU上打印模型摘要
summary(model, (3, 32, 32), device='cuda', batch_size=BATCH_SIZE)
else:
# 在CPU上打印模型摘要
summary(model, (3, 32, 32), device='cpu', batch_size=BATCH_SIZE)
# 训练模型
trained_model, losses, accuracies = train(model, cifar10_train_loader,
cifar10_test_loader, EPOCHS, device,
model_name="cleanlenet_cifar10")
# 保存最终模型
if SAVE_MODEL:
# 生成时间戳
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 构建最终模型保存路径
final_model_path = os.path.join(MODEL_DIR, f"cleanlenet_final_{timestamp}.pth")
# 保存模型状态字典
torch.save(trained_model.state_dict(), final_model_path)
# 打印保存信息
print(f"最终模型保存在: {final_model_path}")
# 打印设备信息
print(f"\n{'=' * 60}")
print(f"设备信息:")
print(f" 设备: {device}")
# 如果是GPU设备,打印GPU信息
if device.type == 'cuda':
print(f" GPU名称: {torch.cuda.get_device_name(0)}")
print(f" 已分配显存: {torch.cuda.memory_allocated(0) / 1024 ** 2:.2f} MB")
print(f" 缓存显存: {torch.cuda.memory_reserved(0) / 1024 ** 2:.2f} MB")
# 打印训练配置信息
print(f" 随机种子: {SEED}")
print(f" Batch Size: {BATCH_SIZE}")
print(f" Epochs: {EPOCHS}")
print(f"{'=' * 60}")