LeNet-5 是由 Yann LeCun 于 1998 年提出的经典卷积神经网络(CNN),被视为深度学习卷积网络的开山鼻祖,主要用于手写数字识别(MNIST 数据集)。MNIST 准确率:约 99.2%。第一个成功商用的 CNN(银行手写支票识别),后续 AlexNet、VGG、GoogLeNet、ResNet 均沿用其核心范式
核心创新
- 局部连接:只连接局部感受野,而非全局全连接
- 权值共享:同一卷积核在整张图复用,大幅减少参数
- 池化(下采样):降维、保留特征、增强平移不变性
- 多层卷积-池化堆叠:分层提取简单→复杂特征
整体结构
LeNet-5 中的 5表示 卷积和全连接的个数, 没有池化一毛儿钱的关系:
输入层 → C1(卷积)→ 池化→ C2(卷积)→池化→ C3(卷积)→ F4(全连接)→F5 输出层

C1 卷积层
- 卷积核:6 个 5×5 卷积核,步长 1,无填充
- 输出:28×28×6(32−5+1=28)
- 激活:Tanh
- 参数量:6×(5×5+1) = 156
池化层(下采样)
- 池化:2×2 平均池化,步长 2
- 输出:14×14×6(28/2=14)
- 参数量:6×(1+1) = 12
C2 卷积层(关键:稀疏连接)
- 卷积核:16 个 5×5,步长 1,无填充
- 输出:10×10×16(14−5+1=10)
- 参数量:约 1516
池化层
- 池化:2×2 平均池化,步长 2
- 输出:5×5×16(10/2=5)
- 参数量:16×2 = 32
C3 卷积层
- 卷积核:120 个 5×5,步长 1,无填充
- 输出:1×1×120(输入 5×5,卷积后变成 1×1)
- 本质:等价于 全连接层
- 参数量:120×(5×5×16+1) = 48120
F4 全连接层
- 神经元:84 个
- 输入:120 维 → 全连接 → 84 维
- 激活:Tanh
- 参数量:120×84+84 = 10164
F5 输出层(Output)
- 神经元:10 个(对应 0–9)
- 原始:RBF(径向基函数)
- 现代:Softmax 输出概率
论文代码复现
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
class LeNet5(nn.Module):
def __init__(self, num_classes=65):
super(LeNet5, self).__init__()
self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=0)
self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0)
self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)
self.conv3 = nn.Conv2d(16, 120, kernel_size=5, stride=1, padding=0)
self.fc4 = nn.Linear(120, 84)
self.fc5 = nn.Linear(84, num_classes)
def forward(self, x):
x = self.pool1(torch.tanh(self.conv1(x)))
x = self.pool2(torch.tanh(self.conv2(x)))
x = torch.tanh(self.conv3(x))
x = x.view(x.size(0), -1)
x = torch.tanh(self.fc4(x))
x = self.fc5(x)
return x
if __name__ == '__main__':
model = LeNet5(num_classes=65)
# 1张图片 1个通道 高32,宽32
test_input = torch.randn(1, 1, 32, 32)
output = model(test_input)
print("input:", test_input.shape)
print("output:", output.shape)
现代常用变体
- 把 平均池化 → 最大池化
- 激活函数用 ReLU 替代 Tanh
- 输出层用 Softmax + CrossEntropyLoss
实战练习
import torch
import torch.nn as nn
import torch.optim as optim # 优化算法
import torchvision.transforms as transforms
from torchvision.datasets import MNIST
# 数据集
import torch.utils.data as data
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
# 选择是显卡还是cpu
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print("using {} device.".format(device))
# 下载数据集,需要数据的,因为下载过了
train_datasets = MNIST('data', train=True, transform=transforms.ToTensor(), download=True)
test_datasets = MNIST('../data', train=False, transform=transform)
# 数据集加载
train_loader = data.DataLoader(train_datasets, batch_size=32, shuffle=False)
test_loader = data.DataLoader(test_datasets, batch_size=32, shuffle=False)
class LeNet5(nn.Module):
def __init__(self):
super().__init__()
# - **输入**:**32×32×1**(灰度图,单通道)
# MNIST 原图是 28×28, padding=2 论文中填充为 32×32 以保留更多边界特征。
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, stride=1, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
# 第二次卷积
self.conv2 = nn.Sequential(
nn.Conv2d(6, 16, 5),
nn.ReLU(),
nn.MaxPool2d(2, 2)
)
# 第三层使用的 全连接没有 使用论文的卷积
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
# 定义前向传播,输入为x
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = torch.flatten(x, 1) # 展开(flatten) 变成全连接网络需要的输入,可以使用view改变张量维度 # 全连接 然后激活
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
x = self.fc3(x)
return x
# 创建模型net
net = LeNet5()
# 交叉损失函数
loss_function = nn.CrossEntropyLoss()
# Adam优化器 学习率0.01
optimizer = optim.Adam(net.parameters(), lr=0.001)
# 开始训练 0-10 循环每次
# 定义模型保存路径和文件名
model_path = 'LeNet5.pth'
arr_loss = []
# 训练模型
def train_Model(net, train_loader):
net.train()
for epoch in range(5):
for i, (images, labels) in enumerate(train_loader):
outputs = net.forward(images)
# 获取损失函数
loss = loss_function(outputs, labels)
# 反向更新
loss.backward()
# 更新参数
optimizer.step()
# 梯度清零
optimizer.zero_grad()
arr_loss.append(loss.item())
if (i+1) % 200 == 0 or i==0:
print('[%d, %5d] loss函数->: %.3f' % (epoch + 1, i + 1, loss.item()))
# 保存模型
torch.save(net.state_dict(), model_path)
# 测试模型
def test_Model(net, test_loader):
# # 把模型从训练模式,设置为测试模式
net.eval()
# 模型不可能100% 准确,
# 使用测试集,计算准确率
# 默认准确度是0
correct = 0
with torch.no_grad():
# 把测试集,一个一个的取出来,计算准确度
for i, (img, labels) in enumerate(test_loader):
outputs = net.forward(img)
# 找到最大可能性的索引
pred = torch.argmax(outputs, dim=1)
correct = correct + torch.sum(torch.eq(pred, labels)).numpy()
print(correct / len(test_datasets))
# 测试样例
examples = iter(test_loader)
# 显示真实数据的结果 32,1,28,28
imgs, labels = next(examples)
py = net.forward(imgs)
out = torch.argmax(py, dim=1).numpy()
fig = plt.figure()
for i in range(16):
plt.subplot(4, 4, i + 1)
plt.tight_layout()
plt.imshow(imgs[i][0], cmap='gray', interpolation='none')
plt.title("Prediction: {}".format(out[i]))
plt.xticks([])
plt.yticks([])
plt.show()
print('Finished Training')
plt.show()
# 存在,直接加载模型
if os.path.exists(model_path):
# 加载已经训练好的模型 文件地址
net.load_state_dict(torch.load(model_path))
# 测试已经训练好的模型
test_Model(net, test_loader)
else:
# 训练模型,保存模型
train_Model(net, train_loader)

3786

被折叠的 条评论
为什么被折叠?



