经典卷积神经网络LeNet实现(pytorch版)

LeNet卷积神经网络

  • 一、理论部分
    • 1.1 核心理论
    • 1.2 LeNet-5 网络结构
    • 1.3 关键细节
    • 1.4 后期改进
    • 1.6 意义与局限性
  • 二、代码实现
    • 2.1 导包
    • 2.1 数据加载和处理
    • 2.3 网络构建
    • 2.4 训练和测试函数
      • 2.4.1 训练函数
      • 2.4.2 测试函数
    • 2.5 训练和保存模型
    • 2.6 模型加载和预测

一、理论部分

LeNet是一种经典的卷积神经网络(CNN),由Yann LeCun等人于1998年提出,最初用于手写数字识别(如MNIST数据集)。它是CNN的奠基性工作之一,其核心思想是通过局部感受野、共享权重和空间下采样来提取有效特征


1.1 核心理论

  • 局部感受野(Local Receptive Fields)
    卷积层通过小尺寸的滤波器(如5×5)扫描输入图像,每个神经元仅连接输入图像的局部区域,从而捕捉局部特征(如边缘、纹理)

  • 共享权重(Weight Sharing)
    同一卷积层的滤波器在整张图像上共享参数,显著减少参数量,增强平移不变性

  • 空间下采样(Subsampling)
    池化层(如平均池化)降低特征图的分辨率,减少计算量并增强对微小平移的鲁棒性

  • 多层特征组合
    通过交替的卷积和池化层,逐步组合低层特征(边缘)为高层特征(数字形状)


1.2 LeNet-5 网络结构

LeNet-5是LeNet系列中最著名的版本,其结构如下(输入为32×32灰度图像):

层类型参数说明输出尺寸
输入层灰度图像32×32×1
C1层卷积层:6个5×5滤波器,步长1,无填充28×28×6
S2层平均池化:2×2窗口,步长214×14×6
C3层卷积层:16个5×5滤波器,步长110×10×16
S4层平均池化:2×2窗口,步长25×5×16
C5层卷积层:120个5×5滤波器1×1×120
F6层全连接层:84个神经元84
输出层全连接 + Softmax(10类)10

1.3 关键细节

  • 激活函数
    原始LeNet使用Tanh或Sigmoid,现代实现常用ReLU

  • 池化方式
    原始版本使用平均池化,后续改进可能用最大池化

  • 参数量优化
    C3层并非全连接至S2的所有通道,而是采用部分连接(如论文中的连接表),减少计算量

  • 输出处理
    最后通过全连接层(F6)和Softmax输出分类概率(如0-9数字)


1.4 后期改进

  • ReLU替代Tanh:解决梯度消失问题,加速训练
  • 最大池化:更关注显著特征,抑制噪声
  • Batch Normalization:稳定训练过程
  • Dropout:防止过拟合(原LeNet未使用)

1.6 意义与局限性

  • 意义
    证明了CNN在视觉任务中的有效性,启发了现代深度学习模型(如AlexNet、ResNet)

  • 局限性
    参数量小、层数浅,对复杂数据(如ImageNet)表现不足,需更深的网络结构

LeNet的设计思想至今仍是CNN的基础,理解它有助于掌握现代卷积神经网络的演变逻辑

二、代码实现

  • LeNet 是一个经典的卷积神经网络(CNN),由 Yann LeCun 等人于 1998 年提出,主要用于手写数字识别(如 MNIST 数据集)
  • MNIST数据集是机器学习领域中非常经典的一个数据集,由60000个训练样本和10000个测试样本组成,每个样本都是一张28 * 28像素的灰度手写数字图片
  • 总体来看,LeNet(LeNet-5)由两个部分组成:(1)卷积编码器:由两个卷积层组成(2)全连接层密集块:由三个全连接层组成

2.1 导包

import torch
import torch.nn as nn
import torchvision
from tqdm import tqdm
from torchsummary import summary

2.1 数据加载和处理

# 加载 MNIST 数据集
def load_data(batch_size=64):
    transform = torchvision.transforms.Compose([
        torchvision.transforms.ToTensor(),  # 将图像转换为张量
        torchvision.transforms.Normalize((0.5,), (0.5,))  # 归一化
    ])
    
    # 下载训练集和测试集
    train_dataset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
    test_dataset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
    
    # 创建 DataLoader
    train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
    test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)
    return train_loader, test_loader

2.3 网络构建

  • LeNet 的网络结构如下:
    • 卷积层 1:输入通道 1,输出通道 6,卷积核大小 5x5
    • 池化层 1:2x2 的最大池化
    • 卷积层 2:输入通道 6,输出通道 16,卷积核大小 5x5。
    • 池化层 2:2x2 的最大池化。
    • 全连接层 1:输入 16x5x5,输出 120
    • 全连接层 2:输入 120,输出 84
    • 全连接层 3:输入 84,输出 10(对应 10 个类别)
#定义LeNet网络架构
class LeNet(nn.Module):
    def __init__(self):
        super(LeNet,self).__init__()
        self.net=nn.Sequential(
            #卷积层1
            nn.Conv2d(in_channels=1,out_channels=6,kernel_size=5,stride=1,padding=2),nn.BatchNorm2d(6),nn.Sigmoid(),
            nn.MaxPool2d(kernel_size=2,stride=2),
            #卷积层2
            nn.Conv2d(in_channels=6,out_channels=16,kernel_size=5,stride=1),nn.BatchNorm2d(16),nn.Sigmoid(),
            nn.MaxPool2d(kernel_size=2,stride=2),
            nn.Flatten(),
            #全连接层1
            nn.Linear(16*5*5,120),nn.BatchNorm1d(120),nn.Sigmoid(),
            #全连接层2
            nn.Linear(120,84),nn.BatchNorm1d(84),nn.Sigmoid(),
            #全连接层3
            nn.Linear(84,10)
        )

    def forward(self,X):
        return self.net(X)

2.4 训练和测试函数

2.4.1 训练函数

# 训练函数
def train(model, device, train_loader, optimizer, criterion, scheduler,epoch):
    model.train() # 训练模式
    running_loss = 0.0 #累加器,用于统计当前 epoch中的总训练损失
    correct = 0 #累加器,用于统计当前 epoch 中预测正确的总样本数量
    total = 0 #累加器,用于统计当前 epoch 中已处理的总样本数量
    
    loop = tqdm(train_loader, desc=f"Epoch {epoch + 1}")
    for inputs, labels in loop:
        inputs, labels = inputs.to(device), labels.to(device)

        # 清零梯度
        optimizer.zero_grad()

        # 前向传播
        outputs = model(inputs)
        loss = criterion(outputs, labels)

        # 反向传播和优化
        loss.backward()
        optimizer.step()

        # 统计损失和准确率
        running_loss += loss.item()
        _, predicted = torch.max(outputs.data,dim=1) #将当前批次样本的预测概率,转为对应的预测样本种类
        total += labels.size(0)  #返回当前批次的样本数量
        correct += predicted.eq(labels).sum().item() #计算当前批次中预测正确的样本数量

        # 更新进度条
        loop.set_postfix(loss=running_loss / (loop.n + 1), acc=100. * correct / total,lr=optimizer.param_groups[0]['lr'])
    # 更新学习率
    scheduler.step()

2.4.2 测试函数

# 测试函数
def test(model, device, test_loader, criterion):
    model.eval()
    test_loss = 0.0 #累加器,用于统计当前 epoch中的总测试损失
    correct = 0.0 #累加器,用于统计当前 epoch 中预测正确的总样本数量
    
    with torch.no_grad():
        for inputs, labels in test_loader:
            inputs, labels = inputs.to(device), labels.to(device)
            outputs = model(inputs)
            # 计算当前批量的总损失(平均损失 * 批量大小)
            test_loss += criterion(outputs, labels).item()*inputs.size(0)
            _, predicted=torch.max(outputs.data,dim=1)
            correct += predicted.eq(labels).sum().item()

    acc=100. * correct / len(test_loader.dataset) #平均准确率
    test_loss/=len(test_loader.dataset) # 总的平均损失
    print(f"Test: acc={acc:.2f},loss={test_loss:.4f}")
    return acc 

2.5 训练和保存模型

  • 训练并保存测试集上性能最好的模型权重
# 设置超参数
batch_size = 64
epochs = 15
learning_rate = 0.1

# 检查设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 加载数据
train_loader, test_loader = load_data(batch_size)

# 初始化模型
model = LeNet().to(device)

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

# 定义学习率调度器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)  # 每 5 个 epoch 学习率乘以 0.1
# scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=2)  # 根据验证损失调整学习率

# 训练测试以及保存最佳模型
best_accuracy = 0.0
for epoch in range(epochs):
    train(model, device, train_loader, optimizer, criterion, scheduler, epoch)
    test_acc = test(model, device, test_loader, criterion)

    if test_acc>best_accuracy:
        best_accuracy=test_acc
        #仅保存模型的参数(权重和偏置),灵活性高,可以在不同的模型结构之间加载参数
        torch.save(model.state_dict(), "./model/best_lenet_mnist.pth") 
        
print(f"Best Test Accuracy: {best_accuracy:.2f}")
  • 打印模型结构(一)
summary(model, input_size=(1, 28, 28))  
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1            [-1, 6, 28, 28]             156
       BatchNorm2d-2            [-1, 6, 28, 28]              12
           Sigmoid-3            [-1, 6, 28, 28]               0
         MaxPool2d-4            [-1, 6, 14, 14]               0
            Conv2d-5           [-1, 16, 10, 10]           2,416
       BatchNorm2d-6           [-1, 16, 10, 10]              32
           Sigmoid-7           [-1, 16, 10, 10]               0
         MaxPool2d-8             [-1, 16, 5, 5]               0
           Flatten-9                  [-1, 400]               0
           Linear-10                  [-1, 120]          48,120
      BatchNorm1d-11                  [-1, 120]             240
          Sigmoid-12                  [-1, 120]               0
           Linear-13                   [-1, 84]          10,164
      BatchNorm1d-14                   [-1, 84]             168
          Sigmoid-15                   [-1, 84]               0
           Linear-16                   [-1, 10]             850
================================================================
Total params: 62,158
Trainable params: 62,158
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.16
Params size (MB): 0.24
Estimated Total Size (MB): 0.40
----------------------------------------------------------------
  • 打印模型结构(二)
X = torch.randn(64, 1, 28, 28).to(device)
for layer in model.net:
    X=layer(X)
    print(layer.__class__.__name__,'output shape:\t',X.shape)
Conv2d output shape:	 torch.Size([64, 6, 28, 28])
BatchNorm2d output shape:	 torch.Size([64, 6, 28, 28])
Sigmoid output shape:	 torch.Size([64, 6, 28, 28])
MaxPool2d output shape:	 torch.Size([64, 6, 14, 14])
Conv2d output shape:	 torch.Size([64, 16, 10, 10])
BatchNorm2d output shape:	 torch.Size([64, 16, 10, 10])
Sigmoid output shape:	 torch.Size([64, 16, 10, 10])
MaxPool2d output shape:	 torch.Size([64, 16, 5, 5])
Flatten output shape:	 torch.Size([64, 400])
Linear output shape:	 torch.Size([64, 120])
BatchNorm1d output shape:	 torch.Size([64, 120])
Sigmoid output shape:	 torch.Size([64, 120])
Linear output shape:	 torch.Size([64, 84])
BatchNorm1d output shape:	 torch.Size([64, 84])
Sigmoid output shape:	 torch.Size([64, 84])
Linear output shape:	 torch.Size([64, 10])

2.6 模型加载和预测

import os
from PIL import Image

# 定义模型结构
net = LeNet()
net.load_state_dict(torch.load("./model/best_lenet_mnist.pth",weights_only=True,map_location=device)) # 加载保存的参数
net.to(device) # 将模型移动到设备(GPU 或 CPU)
net.eval() # 将模型设置为评估模式


# 定义数据预处理
transform2 = torchvision.transforms.Compose([
    torchvision.transforms.Resize((28, 28)),  # 调整图像大小(LeNet 输入为 32x32)
    torchvision.transforms.ToTensor(),  # 将图像转换为张量
    torchvision.transforms.Normalize((0.5,), (0.5,))  # 归一化
])

#读取n张图片,并转为形状为[n,1,32,32]的张量
images = []
filenames = []
image_folder="./inference/mnist"
for img_name in os.listdir(image_folder):
    img_path = os.path.join(image_folder, img_name)
    try:
        img = Image.open(img_path).convert('L')  # 转为灰度图
        img_tensor = transform2(img).unsqueeze(0)  # [1, 1, 32, 32]
        images.append(img_tensor)
        filenames.append(img_name)
    except:
        print(f"跳过无法加载的图像: {img_name}")

# 将所有图像堆叠成一个batch [N, 1, 32, 32]
if images:
    batch_images = torch.cat(images, dim=0).to(device) #将n张图片移动到设备(GPU 或 CPU)上
    with torch.no_grad():
        outputs=net(batch_images)
        pred_probs = torch.softmax(outputs,dim=1) #将模型输出结果转为分别对0-9的预测概率
        _, predicteds = torch.max(pred_probs.data, dim=1) #提取出预测的数字结果(张量)
        for predicted in predicteds:
            print(f"Predicted figure: {predicted.item()}")
else:
    raise ValueError("没有有效的图像可处理!")
Predicted figure: 0
Predicted figure: 8
Predicted figure: 0
Predicted figure: 1
Predicted figure: 7
Predicted figure: 1
Predicted figure: 3
Predicted figure: 4
Predicted figure: 9
Predicted figure: 8
  • 展示输入图片及其预测值
import matplotlib.pyplot as plt

batch_images=batch_images.squeeze() # [10,1,28,28]->[10,28,28]
cnt = 0
for img,pred in zip(batch_images, predicteds):
    cnt += 1
    plt.subplot(2, 5, cnt)
    plt.xticks([], [])
    plt.yticks([], [])
   
    plt.title("Pred:{}".format(pred.item()))
    plt.imshow(img.cpu(), cmap="gray")
    
plt.tight_layout()
plt.show()

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

入梦风行

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值