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窗口,步长2 | 14×14×6 |
C3层 | 卷积层:16个5×5滤波器,步长1 | 10×10×16 |
S4层 | 平均池化:2×2窗口,步长2 | 5×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()