LeNet 实现 Fashion-MNIST 图像分类
·
一、前言:LeNet—— 卷积神经网络的开山之作
LeNet(标准版本 LeNet-5)由 Yann LeCun 在 1998 年提出,是全球第一个成功商用的卷积神经网络,最初用于银行手写数字识别,奠定了现代 CNN 三大核心思想:局部感受野、权重共享、空间下采样(池化)。
在 ReLU、最大池化还未普及的年代,LeNet 采用Sigmoid激活 +平均池化,结构极简、参数量少,非常适合 CNN 初学者入门。 本文基于 PyTorch 完整复现适配 28×28 灰度图的 LeNet,使用 Fashion-MNIST(衣物 10 分类)完成数据集可视化、模型搭建、训练验证、模型保存、测试推理全流程,代码完全可运行。
二、数据集介绍:Fashion-MNIST
Fashion-MNIST 是 MNIST 手写数字的替代数据集,包含 60000 张训练图、10000 张测试图:
- 输入:单通道灰度图,尺寸 28×28
- 分类:10 类衣物(T 恤、裤子、连衣裙、鞋子等)
- 优势:比手写数字更有区分度,适合验证简单 CNN 性能
配套代码plot.py实现批量图像可视化,直观查看样本:
import torch
from torch import nn
from torchsummary import summary
class LeNet(nn.Module):
def __init__(self):
super(LeNet,self).__init__()
# 卷积块1
self.c1=nn.Conv2d(in_channels=1,out_channels=6,kernel_size=5,stride=1,padding=2)
self.sig=nn.Sigmoid()
self.s2=nn.AvgPool2d(kernel_size=2,stride=2)
# 卷积块2
self.c3=nn.Conv2d(in_channels=6,out_channels=16,kernel_size=5,stride=1,padding=0)
self.s4=nn.AvgPool2d(2,2)
# 展平+全连接
self.flatten=nn.Flatten()
self.f5=nn.Linear(400,120)
self.f6 = nn.Linear(120, 84)
self.f7 = nn.Linear(84, 10)
def forward(self,x):
# 前向传播数据流
x=self.sig(self.c1(x))
x=self.s2(x)
x=self.sig(self.c3(x))
x=self.s4(x)
x=self.flatten(x)
x = self.sig(self.f5(x))
x = self.sig(self.f6(x))
x=self.f7(x)
return x
if __name__=="__main__":
# 自动切换GPU/CPU
device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
model=LeNet().to(device)
# 打印网络结构与参数量,输入(通道,H,W)
summary(model,(1,28,28))
三、LeNet 网络结构深度解析(model.py)
3.1 整体结构总览
适配 28×28 输入的 LeNet 共 8 层(不含输入),分为卷积特征提取模块 + 全连接分类模块:
- C1:卷积层(1→6 通道,5×5 卷积,padding=2)
- Sigmoid 激活
- S2:平均池化 2×2,stride=2
- C3:卷积层(6→16 通道,5×5 卷积,无 padding)
- Sigmoid 激活
- S4:平均池化 2×2,stride=2
- Flatten 展平
- F5、F6、F7 三层全连接,最终输出 10 分类
3.2 逐层尺寸计算(核心)
输入张量:[batch, 1, 28, 28]
- C1 Conv2d (1,6,5,padding=2):输出
6×28×28(padding 保证尺寸不变) - S2 AvgPool2d (2,2):输出
6×14×14 - C3 Conv2d(6,16,5,padding=0):(14-5+1)=10 →
16×10×10 - S4 AvgPool2d (2,2):输出
16×5×5 - Flatten:1655 = 400,作为第一层全连接输入
- F5:400 → 120;F6:120 → 84;F7:84 → 10
3.3 PyTorch 模型完整代码 model.py
import torch
from torch import nn
from torchsummary import summary
class LeNet(nn.Module):
def __init__(self):
super(LeNet,self).__init__()
# 卷积块1
self.c1=nn.Conv2d(in_channels=1,out_channels=6,kernel_size=5,stride=1,padding=2)
self.sig=nn.Sigmoid()
self.s2=nn.AvgPool2d(kernel_size=2,stride=2)
# 卷积块2
self.c3=nn.Conv2d(in_channels=6,out_channels=16,kernel_size=5,stride=1,padding=0)
self.s4=nn.AvgPool2d(2,2)
# 展平+全连接
self.flatten=nn.Flatten()
self.f5=nn.Linear(400,120)
self.f6 = nn.Linear(120, 84)
self.f7 = nn.Linear(84, 10)
def forward(self,x):
# 前向传播数据流
x=self.sig(self.c1(x))
x=self.s2(x)
x=self.sig(self.c3(x))
x=self.s4(x)
x=self.flatten(x)
x = self.sig(self.f5(x))
x = self.sig(self.f6(x))
x=self.f7(x)
return x
if __name__=="__main__":
# 自动切换GPU/CPU
device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
model=LeNet().to(device)
# 打印网络结构与参数量,输入(通道,H,W)
summary(model,(1,28,28))
关键细节说明
- Sigmoid 激活:原始 LeNet 设计,历史原因无 ReLU;现代项目可替换为 ReLU 提升精度
- AvgPool2d 平均池化:和现代 MaxPool 最大池化区分,还原经典结构
padding=2第一层卷积:保证 28×28 输入卷积后尺寸不变,适配 Fashion-MNISTnn.Flatten():替代手动x.view(),自动将卷积特征转为一维向量送入全连接torchsummary.summary:一键打印每层输出维度、参数量,快速排查维度错误
四、模型训练与验证流程(model_train.py)
完整训练逻辑包含:数据集划分、Dataloader 构建、训练循环、验证循环、最优模型保存、损失精度绘图。
4.1 核心功能拆解
train_val_data_process:加载 FashionMNIST,8:2 划分训练 / 验证集,返回 DataLoadertrain_model_process:训练主逻辑,Adam 优化器 + 交叉熵损失,保存最优权重best_model.pthmatplot_acc_loss:绘制训练 / 验证 loss、accuracy 曲线,观察过拟合 / 欠拟合
import copy
import time
import pandas as pd
from torchvision.datasets import FashionMNIST
import numpy as np
from torchvision import transforms
import torch.utils.data as Data
import matplotlib.pyplot as plt
from model import LeNet
import torch
import torch.nn as nn
# 数据集加载与划分
def train_val_data_process():
train_data = FashionMNIST(root='./data',
train=True,
transform=transforms.Compose([transforms.Resize(size=28), transforms.ToTensor()]),
download=True)
# 8:2划分训练、验证集
train_data,val_data = Data.random_split(train_data,[round(0.8*len(train_data)),round(0.2*len(train_data))])
train_dataloader = Data.DataLoader(dataset=train_data,batch_size=32,shuffle=True,num_workers=0)
val_dataloader = Data.DataLoader(dataset=val_data,batch_size=32,shuffle=True,num_workers=0)
return train_dataloader,val_dataloader
# 训练验证主函数
def train_model_process(model,train_dataloader,val_dataloader,num_epochs):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
model = model.to(device)
best_model_wts= copy.deepcopy(model.state_dict())
best_acc = 0.0
# 记录指标
train_loss_all,val_loss_all = [],[]
train_acc_all,val_acc_all = [],[]
since = time.time()
for epoch in range(num_epochs):
print(f"Epoch {epoch}/{num_epochs-1}\n{'-'*10}")
train_loss,train_corrects,train_num = 0.0,0,0
val_loss,val_corrects,val_num = 0.0,0,0
# 训练阶段 model.train()
for b_x,b_y in train_dataloader:
b_x,b_y = b_x.to(device),b_y.to(device)
model.train()
output=model(b_x)
pre_lab=torch.argmax(output,dim=1)
loss = criterion(output,b_y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss+=loss.item() * b_x.size(0)
train_corrects += torch.sum(pre_lab == b_y.data)
train_num += b_x.size(0)
# 验证阶段 model.eval() 无梯度
for b_x, b_y in val_dataloader:
b_x,b_y = b_x.to(device),b_y.to(device)
model.eval()
output=model(b_x)
pre_lab = torch.argmax(output, dim=1)
loss = criterion(output, b_y)
val_loss += loss.item() * b_x.size(0)
val_corrects += torch.sum(pre_lab == b_y.data)
val_num += b_x.size(0)
# 计算每轮指标
train_loss_all.append(train_loss / train_num)
train_acc_all.append(train_corrects.double().item() / train_num)
val_loss_all.append(val_loss / val_num)
val_acc_all.append(val_corrects.double().item() / val_num)
print(f"{epoch} train loss:{train_loss_all[-1]:.4f} train acc: {train_acc_all[-1]:.4f}")
print(f"{epoch} val loss:{val_loss_all[-1]:.4f} val acc: {val_acc_all[-1]:.4f}")
# 保存验证集准确率最高的模型
if val_acc_all[-1]>best_acc:
best_acc=val_acc_all[-1]
best_model_wts=copy.deepcopy(model.state_dict())
torch.save(best_model_wts,'best_model.pth')
time_use=time.time()-since
print(f"已训练时长:{int(time_use/60)}m{int(time_use%60)}s\n")
# 保存训练日志
train_process = pd.DataFrame({
"epoch": range(num_epochs),
"train_loss_all":train_loss_all,
"val_loss_all":val_loss_all,
"train_acc_all":train_acc_all,
"val_acc_all":val_acc_all
})
return train_process
# 绘制loss&acc曲线
def matplot_acc_loss(train_process):
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(train_process['epoch'], train_process.train_loss_all, "ro-", label="Train loss")
plt.plot(train_process['epoch'], train_process.val_loss_all, "bs-", label="Val loss")
plt.legend(),plt.xlabel("epoch"),plt.ylabel("loss")
plt.subplot(1, 2, 2)
plt.plot(train_process['epoch'], train_process.train_acc_all, "ro-", label="Train acc")
plt.plot(train_process['epoch'], train_process.val_acc_all, "bs-", label="Val acc")
plt.xlabel("epoch"),plt.ylabel("acc"),plt.legend()
plt.show()
if __name__ == '__main__':
LeNet=LeNet()
train_dataloader, val_dataloader=train_val_data_process()
train_process=train_model_process(LeNet,train_dataloader,val_dataloader,num_epochs=20)
matplot_acc_loss(train_process)

训练关键知识点
model.train()/model.eval():切换训练 / 评估模式,eval 关闭 dropout、BN 更新torch.no_grad():验证阶段禁用梯度计算,节省显存、提速copy.deepcopy:深拷贝最优模型权重,避免后续 epoch 覆盖最优参数CrossEntropyLoss:内置 softmax,输出层无需额外激活,直接输入原始 logits- 训练完成自动生成
best_model.pth,保存验证集准确率最高的权重文件
五、模型测试推理(model_test.py)
加载保存好的best_model.pth,在独立测试集评估整体准确率,也可单张打印预测结果。
import torch
import torch.utils.data as Data
from torchvision import transforms
from torchvision.datasets import FashionMNIST
from model import LeNet
# 加载测试集
def test_data_process():
test_data = FashionMNIST(root='./data',
train=False,
transform=transforms.Compose([transforms.Resize(size=28), transforms.ToTensor()]),
download=True)
test_dataloader = Data.DataLoader(dataset=test_data,batch_size=1,shuffle=True,num_workers=0)
return test_dataloader
# 整体测试准确率
def test_model_process(model,test_dataloader):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model=model.to(device)
test_corrects=0.0
test_num=0
with torch.no_grad():
for test_data_x,test_data_y in test_dataloader:
test_data_x,test_data_y = test_data_x.to(device),test_data_y.to(device)
model.eval()
output = model(test_data_x)
pre_lab = torch.argmax(output, dim=1)
test_corrects += torch.sum(pre_lab == test_data_y.data)
test_num += test_data_x.size(0)
test_acc = test_corrects.double().item() / test_num
print("测试集整体准确率:", test_acc)
if __name__=="__main__":
# 初始化模型并加载训练好的权重
model = LeNet()
model.load_state_dict(torch.load('best_model.pth'))
test_dataloader= test_data_process()
test_model_process(model,test_dataloader)
六、完整项目目录结构
LeNet/
├── model.py # LeNet网络定义
├── model_train.py # 训练+验证+保存最优模型
├── model_test.py # 加载权重测试准确率
├── plot.py # 数据集可视化
├── best_model.pth # 训练生成的最优权重
└── data/ # FashionMNIST自动下载目录
七、实验结果与调优思路
1. 训练曲线解读
- 训练 loss 持续下降、验证 loss 同步下降:模型正常收敛
- 训练 acc 持续上升、验证 acc 趋于平稳:轻微过拟合,可减小 epoch、添加 dropout
- 训练 / 验证 loss 均居高不下:学习率不合适、网络容量不足
2. LeNet 简易优化方案(提升精度)
- 激活替换:
Sigmoid→ReLU,缓解梯度消失 - 池化替换:
AvgPool2d→MaxPool2d,现代 CNN 通用选择 - 正则化:全连接层添加
nn.Dropout(0.2)抑制过拟合 - 学习率调整:Adam lr=0.0005、增加 epoch 至 30
八、总结:LeNet 带给 CNN 的启发
- 卷积层提取局部特征:相比全连接大幅减少参数量,利用图像空间相关性
- 池化层降维:降低特征图尺寸,减少计算量,提升特征鲁棒性
- 分层特征提取:浅层卷积捕捉边缘纹理,深层卷积提取复杂物体轮廓
- 极简架构适合新手:通过 LeNet 可吃透卷积、池化、展平、全连接完整数据流,是学习 AlexNet、VGG、ResNet 的基础。
更多推荐


所有评论(0)