找回密码
 立即注册
首页 业界区 业界 pytorch入门 - LetNet5 神经网络

pytorch入门 - LetNet5 神经网络

趣侮 前天 14:39
1.LetNet5简介

1.png

 
LeNet5是由Yann LeCun等人在1998年提出的一种卷积神经网络架构,主要用于手写数字识别。它是早期卷积神经网络的成功应用之一,为现代深度学习模型奠定了基础。LeNet5的名字来源于其发明者LeCun和网络层数(5层)。
LeNet5的主要特点包括:

  • 使用卷积层提取空间特征
  • 使用子采样层(池化层)降低特征维度
  • 使用全连接层进行分类
  • 采用梯度下降法进行训练
虽然LeNet5最初是为识别手写数字设计的,但我们可以将其应用于更广泛的图像分类任务,如FashionMNIST数据集。
2. LeNet5网络结构原理

LeNet5的网络结构可以分为7层(包含输入层),但通常我们说有5层可训练层(2个卷积层和3个全连接层)。让我们详细分析每一层的结构:
2.1 输入层

原始LeNet5的输入是32×32的灰度图像。在我们的实现中,为了适应FashionMNIST数据集,我们将其调整为28×28。
2.2 C1层 - 第一卷积层


  • 卷积核大小: 5×5
  • 卷积核数量: 6
  • 步长: 1
  • 填充: 2 (为了保持输出尺寸与输入相同)
  • 激活函数: Sigmoid
​神经元数量计算​​:
输入尺寸:28×28
输出尺寸:(28 + 2 * 2 - 5)/1 + 1 = 28×28
每个特征图有28×28=784个神经元
共有6个特征图,所以总神经元数=6×784=4704
2.3 S2层 - 第一池化层


  • 池化类型: 平均池化
  • 池化大小: 2×2
  • 步长: 2
​神经元数量计算​​:
输入尺寸:28×28
输出尺寸:(28 - 2)/2 + 1 = 14×14
每个特征图有14×14=196个神经元
共有6个特征图,所以总神经元数=6×196=1176
2.4 C3层 - 第二卷积层


  • 卷积核大小: 5×5
  • 卷积核数量: 16
  • 步长: 1
  • 填充: 0
  • 激活函数: Sigmoid
​神经元数量计算​​:
输入尺寸:14×14
输出尺寸:(14 - 5)/1 + 1 = 10×10
每个特征图有10×10=100个神经元
共有16个特征图,所以总神经元数=16×100=1600
2.5 S4层 - 第二池化层


  • 池化类型: 平均池化
  • 池化大小: 2×2
  • 步长: 2
​神经元数量计算​​:
输入尺寸:10×10
输出尺寸:(10 - 2)/2 + 1 = 5×5
每个特征图有5×5=25个神经元
共有16个特征图,所以总神经元数=16×25=400
2.6 C5层 - 第一全连接层


  • 输入: 16×5×5=400
  • 输出: 120
  • 激活函数: Sigmoid
神经元数量: 120
2.7 F6层 - 第二全连接层


  • 输入: 120
  • 输出: 84
  • 激活函数: Sigmoid
神经元数量: 84
2.8 输出层


  • 输入: 84
  • 输出: 10 (对应10个类别)
  • 激活函数: Softmax
神经元数量: 10
3. PyTorch实现详解

现在让我们详细分析LeNet5的PyTorch实现代码,包含每一行的解释。
3.1 模型定义 (main.py)
  1. import torch
  2. import torch.nn as nn
  3. from torchsummary import summary
  4. class LeNet5(nn.Module):
  5.     def __init__(self, num_classes=10):
  6.         super(LeNet5, self).__init__()
  7.         # 第一卷积层: 输入通道1(灰度图), 输出通道6, 5x5卷积核, padding=2保持尺寸
  8.         self.conv1 = nn.Conv2d(
  9.             in_channels=1, out_channels=6, kernel_size=5, stride=1, padding=2
  10.         )
  11.         self.sig = nn.Sigmoid()  # Sigmoid激活函数
  12.         self.pool = nn.AvgPool2d(kernel_size=2, stride=2)  # 平均池化层
  13.         # 第二卷积层: 输入通道6, 输出通道16, 5x5卷积核, 无padding
  14.         self.conv2 = nn.Conv2d(
  15.             in_channels=6, out_channels=16, kernel_size=5, stride=1, padding=0
  16.         )
  17.         self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)  # 第二个平均池化层
  18.         self.flatten = nn.Flatten()  # 展平层,将多维输入一维化
  19.         # 第一个全连接层: 输入16 * 5 * 5=400, 输出120
  20.         self.f5 = nn.Linear(16 * 5 * 5, 120)
  21.         # 第二个全连接层: 输入120, 输出84
  22.         self.f6 = nn.Linear(120, 84)
  23.         # 输出层: 输入84, 输出类别数
  24.         self.f7 = nn.Linear(84, num_classes)
  25.         self.softmax = nn.Softmax(dim=1)  # Softmax激活函数
  26.     def forward(self, x):
  27.         # 第一卷积块
  28.         x = self.conv1(x)  # 卷积
  29.         x = self.sig(x)     # 激活
  30.         x = self.pool(x)    # 池化
  31.         # 第二卷积块
  32.         x = self.conv2(x)   # 卷积
  33.         x = self.sig(x)     # 激活
  34.         x = self.pool2(x)   # 池化
  35.         # 全连接部分
  36.         x = self.flatten(x)  # 展平
  37.         x = self.f5(x)       # 全连接
  38.         x = self.sig(x)      # 激活
  39.         x = self.f6(x)       # 全连接
  40.         x = self.sig(x)      # 激活
  41.         x = self.f7(x)       # 输出层
  42.         # 注意: 训练时通常不在这里使用softmax,因为CrossEntropyLoss已经包含了softmax
  43.         
  44.         return x
  45. if __name__ == "__main__":
  46.     device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  47.     model = LeNet5(num_classes=10).to(device)
  48.     summary(model, (1, 28, 28))  # 打印模型结构摘要
复制代码
3.2 训练脚本 (train.py)
  1. import os
  2. import sys
  3. sys.path.append(os.getcwd())  # 添加当前目录到系统路径,以便导入自定义模块
  4. import time
  5. from torchvision.datasets import FashionMNIST  # 导入FashionMNIST数据集
  6. from torchvision import transforms  # 图像预处理
  7. from torch.utils.data import DataLoader, random_split  # 数据加载和划分
  8. import numpy as np
  9. import matplotlib.pyplot as plt  # 绘图
  10. import torch
  11. from torch import nn, optim  # 神经网络和优化器
  12. import copy  # 用于模型参数深拷贝
  13. import pandas as pd  # 数据处理
  14. from LetNet5_model.main import LeNet5  # 导入我们的LeNet5模型
  15. def train_val_date_load():
  16.     # 加载FashionMNIST训练集
  17.     train_dataset = FashionMNIST(
  18.         root="./data",  # 数据存储路径
  19.         train=True,     # 加载训练集
  20.         download=True,  # 自动下载
  21.         transform=transforms.Compose([
  22.             transforms.Resize(size=28),  # 调整大小到28x28
  23.             transforms.ToTensor(),       # 转为Tensor并归一化到[0,1]
  24.         ]),
  25.     )
  26.     # 按8:2划分训练集和验证集
  27.     train_date, val_data = random_split(
  28.         train_dataset,
  29.         [
  30.             int(len(train_dataset) * 0.8),  # 80%训练
  31.             len(train_dataset) - int(len(train_dataset) * 0.8),  # 20%验证
  32.         ],
  33.     )
  34.     # 创建数据加载器
  35.     train_loader = DataLoader(
  36.         dataset=train_date, batch_size=128, shuffle=True, num_workers=1
  37.     )
  38.     val_loader = DataLoader(
  39.         dataset=val_data, batch_size=128, shuffle=True, num_workers=1
  40.     )
  41.     return train_loader, val_loader
  42. def train_model_process(model, train_loader, val_loader, epochs=10):
  43.     device = "cuda" if torch.cuda.is_available() else "cpu"
  44.     optimizer = optim.Adam(model.parameters(), lr=0.001)  # Adam优化器
  45.     criterion = nn.CrossEntropyLoss()  # 交叉熵损失
  46.     model.to(device)  # 模型移到设备
  47.     # 初始化变量记录最佳模型和训练过程
  48.     best_model_wts = copy.deepcopy(model.state_dict())
  49.     best_acc = 0.0
  50.     train_loss_all = []
  51.     val_loss_all = []
  52.     train_acc_all = []
  53.     val_acc_all = []
  54.     since = time.time()  # 计时开始
  55.     for epoch in range(epochs):
  56.         print(f"Epoch {epoch + 1}/{epochs}")
  57.         # 初始化统计变量
  58.         train_loss = 0.0
  59.         train_correct = 0
  60.         val_loss = 0.0
  61.         val_correct = 0
  62.         train_num = 0
  63.         val_num = 0
  64.         # 训练阶段
  65.         for step, (images, labels) in enumerate(train_loader):
  66.             images, labels = images.to(device), labels.to(device)
  67.             model.train()  # 训练模式
  68.             outputs = model(images)
  69.             pre_lab = torch.argmax(outputs, dim=1)  # 预测标签
  70.             loss = criterion(outputs, labels)  # 计算损失
  71.             
  72.             optimizer.zero_grad()  # 梯度清零
  73.             loss.backward()        # 反向传播
  74.             optimizer.step()       # 参数更新
  75.             # 统计信息
  76.             train_loss += loss.item() * images.size(0)
  77.             train_correct += torch.sum(pre_lab == labels.data)
  78.             train_num += labels.size(0)
  79.         # 验证阶段
  80.         for step, (images, labels) in enumerate(val_loader):
  81.             images, labels = images.to(device), labels.to(device)
  82.             model.eval()  # 评估模式
  83.             with torch.no_grad():  # 不计算梯度
  84.                 outputs = model(images)
  85.                 pre_lab = torch.argmax(outputs, dim=1)
  86.                 loss = criterion(outputs, labels)
  87.                 val_loss += loss.item() * images.size(0)
  88.                 val_correct += torch.sum(pre_lab == labels.data)
  89.                 val_num += labels.size(0)
  90.         # 记录本轮结果
  91.         train_loss_all.append(train_loss / train_num)
  92.         val_loss_all.append(val_loss / val_num)
  93.         train_acc = train_correct.double() / train_num
  94.         val_acc = val_correct.double() / val_num
  95.         train_acc_all.append(train_acc.item())
  96.         val_acc_all.append(val_acc.item())
  97.         
  98.         print(f"Train Loss: {train_loss / train_num:.4f}, Train Acc: {train_acc:.4f}, "
  99.               f"Val Loss: {val_loss / val_num:.4f}, Val Acc: {val_acc:.4f}")
  100.         
  101.         # 更新最佳模型
  102.         if val_acc_all[-1] > best_acc:
  103.             best_acc = val_acc_all[-1]
  104.             best_model_wts = copy.deepcopy(model.state_dict())
  105.     # 训练结束
  106.     time_elapsed = time.time() - since
  107.     print(f"Training complete in {time_elapsed // 60:.0f}m {time_elapsed % 60:.0f}s\n"
  108.           f"Best val Acc: {best_acc:.4f}")
  109.     # 保存模型和训练过程
  110.     torch.save(model.state_dict(), "./models/le_net5_best_model.pth")
  111.     train_process = pd.DataFrame({
  112.         "epoch": range(1, epochs + 1),
  113.         "train_loss_all": train_loss_all,
  114.         "val_loss_all": val_loss_all,
  115.         "train_acc_all": train_acc_all,
  116.         "val_acc_all": val_acc_all,
  117.     })
  118.     return train_process
  119. def matplot_acc_loss(train_process):
  120.     # 绘制训练曲线
  121.     plt.figure(figsize=(12, 5))
  122.    
  123.     # 损失曲线
  124.     plt.subplot(1, 2, 1)
  125.     plt.plot(train_process["epoch"], train_process["train_loss_all"], label="Train Loss")
  126.     plt.plot(train_process["epoch"], train_process["val_loss_all"], label="Val Loss")
  127.     plt.xlabel("Epoch")
  128.     plt.ylabel("Loss")
  129.     plt.title("Loss vs Epoch")
  130.     plt.legend()
  131.    
  132.     # 准确率曲线
  133.     plt.subplot(1, 2, 2)
  134.     plt.plot(train_process["epoch"], train_process["train_acc_all"], label="Train Acc")
  135.     plt.plot(train_process["epoch"], train_process["val_acc_all"], label="Val Acc")
  136.     plt.xlabel("Epoch")
  137.     plt.ylabel("Accuracy")
  138.     plt.title("Accuracy vs Epoch")
  139.     plt.legend()
  140.    
  141.     plt.tight_layout()
  142.     plt.ion()
  143.     plt.show()
  144.     plt.savefig("./models/le_net5_output.png")
  145. if __name__ == "__main__":
  146.     traindatam, valdata = train_val_date_load()  # 加载数据
  147.     result = train_model_process(LeNet5(), traindatam, valdata, 10)  # 训练模型
  148.     matplot_acc_loss(result)  # 绘制曲线
复制代码
3.3 测试脚本 (test.py)
  1. import os
  2. import sys
  3. sys.path.append(os.getcwd())  # 添加当前目录到系统路径
  4. import torch
  5. from torch.utils.data import DataLoader
  6. from torchvision import transforms
  7. from torchvision.datasets import FashionMNIST
  8. from LetNet5_model.main import LeNet5
  9. def test_data_load():
  10.     # 加载测试集
  11.     test_dataset = FashionMNIST(
  12.         root="./data",
  13.         train=False,  # 测试集
  14.         download=True,
  15.         transform=transforms.Compose([
  16.             transforms.Resize(size=28),
  17.             transforms.ToTensor(),
  18.         ]),
  19.     )
  20.     # 创建测试数据加载器
  21.     test_loader = DataLoader(
  22.         dataset=test_dataset, batch_size=128, shuffle=True, num_workers=1
  23.     )
  24.     return test_loader
  25. def test_model_process(model, test_loader):
  26.     device = "cuda" if torch.cuda.is_available() else "cpu"
  27.     model.to(device)
  28.     model.eval()  # 评估模式
  29.     correct = 0
  30.     total = 0
  31.     with torch.no_grad():  # 不计算梯度
  32.         for images, labels in test_loader:
  33.             images, labels = images.to(device), labels.to(device)
  34.             outputs = model(images)
  35.             _, predicted = torch.max(outputs, 1)  # 获取预测类别
  36.             total += labels.size(0)
  37.             correct += torch.sum(predicted == labels.data)  # 统计正确数
  38.     accuracy = correct / total * 100
  39.     print(f"Test Accuracy: {accuracy:.2f}%")  # 打印测试准确率
  40. if __name__ == "__main__":
  41.     test_loader = test_data_load()  # 加载测试数据
  42.     model = LeNet5()  # 实例化模型
  43.     model.load_state_dict(torch.load("./models/le_net5_best_model.pth"))  # 加载训练好的权重
  44.     test_model_process(model, test_loader)  # 测试模型
复制代码
4. 训练与结果分析

4.1 训练过程

训练过程展示了模型在训练集和验证集上的损失和准确率变化。典型的训练过程会显示以下特征:

  • ​损失曲线​​:

    • 训练损失应随着epoch增加而持续下降
    • 验证损失初期下降,后期可能趋于平稳或略有上升(过拟合)

  • ​准确率曲线​​:

    • 训练准确率应持续上升
    • 验证准确率初期上升,后期趋于平稳

4.2 超参数选择

在我们的实现中使用了以下关键超参数:

  • 学习率: 0.001 (Adam优化器的默认学习率)
  • 批量大小: 128
  • 训练周期: 10
  • 优化器: Adam
这些参数可以根据具体任务进行调整以获得更好的性能。
4.3 模型性能

在FashionMNIST测试集上,LeNet5通常能达到85%-90%的准确率。虽然不如现代深度学习模型,但对于教学和理解CNN基本原理已经足够。
5. 总结与扩展

LeNet5虽然是一个简单的CNN模型,但它包含了现代深度学习模型的许多核心概念:

  • ​局部感受野​​: 通过卷积核实现
  • ​权值共享​​: 同一卷积核在整个图像上滑动
  • ​空间子采样​​: 通过池化层实现
  • ​多层感知机​​: 最后的全连接层
​扩展改进建议​​:

  • 使用ReLU代替Sigmoid作为激活函数
  • 使用最大池化代替平均池化
  • 添加Batch Normalization层
  • 增加数据增强技术
  • 尝试不同的学习率调度策略
通过这些改进,可以显著提高模型在FashionMNIST上的性能。
LeNet5作为卷积神经网络的鼻祖,其设计思想和实现方式至今仍在影响着深度学习领域。通过实现和理解LeNet5,我们可以更好地掌握现代深度学习模型的基础。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册