LeNet-5 详解:从理论到实践
一、LeNet-5 的历史意义
LeNet-5 是由 Yann LeCun 等人在 1998 年提出的卷积神经网络,主要用于手写数字识别,是早期最成功的深度学习模型之一。它在 MNIST 数据集上取得了优异的表现,并为后来的深度学习发展奠定了基础。
LeNet-5 的重要性在于:
首次展示了卷积神经网络在图像识别任务中的潜力
引入了卷积、池化等现代 CNN 的基本结构
为后续更复杂的网络架构(如 AlexNet、VGG 等)提供了设计思路
二、LeNet-5 网络结构详解
2.1 整体结构
LeNet-5 由 7 层组成(不包括输入层),包含 2 个卷积层、2 个池化层和 3 个全连接层(其中最后一个全连接层是输出层)。其结构可以表示为:
输入 → C1 → S2 → C3 → S4 → C5 → F6 → Output
2.2 各层详细解析
2.2.1 C1层 - 卷积层
输入:32×32 的灰度图像
卷积核:5×5,6 个
输出:6 个 28×28 的特征图((32-5+1)=28)
激活函数:Sigmoid 或 Tanh(原始论文使用)
2.2.2 S2层 - 池化层(下采样层)
输入:6 个 28×28 的特征图
池化方式:平均池化(原始论文使用),现在更常用最大池化
池化窗口:2×2
步长:2
输出:6 个 14×14 的特征图
2.2.3 C3层 - 卷积层
输入:6 个 14×14 的特征图
卷积核:5×5,16 个
输出:16 个 10×10 的特征图
特殊设计:不是全连接卷积,而是部分连接(减少参数)
2.2.4 S4层 - 池化层
类似 S2 层
输入:16 个 10×10 的特征图
输出:16 个 5×5 的特征图
2.2.5 C5层 - 卷积层
输入:16 个 5×5 的特征图
卷积核:5×5,120 个
输出:120 个 1×1 的特征图(实际上是全连接)
2.2.6 F6层 - 全连接层
输入:120 维向量
输出:84 维向量
激活函数:Sigmoid 或 Tanh
2.2.7 Output层 - 全连接层
输入:84 维向量
输出:10 维(对应 0-9 数字分类)
激活函数:原始论文使用 RBF(径向基函数),现代实现通常用 Softmax
三、PyTorch 实现 LeNet-5
3.1 模型定义
import torch
import torch.nn as nn
import torch.nn.functional as Fclass LeNet5(nn.Module):def __init__(self, num_classes=10):"""LeNet-5 网络结构实现参数:num_classes (int): 输出类别数,默认为10(MNIST的0-9数字分类)"""super(LeNet5, self).__init__()# 特征提取部分self.features = nn.Sequential(# C1: 卷积层nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=0),# S2: 平均池化层(原始论文使用)nn.AvgPool2d(kernel_size=2, stride=2),# C3: 卷积层nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0),# S4: 平均池化层nn.AvgPool2d(kernel_size=2, stride=2),# C5: 卷积层(等同于全连接)nn.Conv2d(16, 120, kernel_size=5, stride=1, padding=0),)# 分类器部分self.classifier = nn.Sequential(# F6: 全连接层nn.Linear(120, 84),# Output: 全连接层nn.Linear(84, num_classes),)# 原始论文使用tanh激活函数self.activation = nn.Tanh()def forward(self, x):"""前向传播参数:x (torch.Tensor): 输入张量,形状为(batch_size, 1, 32, 32)返回:torch.Tensor: 输出张量,形状为(batch_size, num_classes)"""# 特征提取x = self.features(x)# 展平处理x = torch.flatten(x, 1)# 分类器x = self.classifier(x)# 原始论文输出层使用RBF,现代实现通常省略或使用softmaxreturn x
3.2 关键API详解
3.2.1 nn.Conv2d
nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros')
in_channels
: 输入通道数(灰度图为1,RGB为3)out_channels
: 输出通道数(卷积核数量)kernel_size
: 卷积核大小(整数或元组)stride
: 步长(默认为1)padding
: 零填充(默认为0)bias
: 是否使用偏置(默认为True)
3.2.2 nn.AvgPool2d / nn.MaxPool2d
nn.AvgPool2d(kernel_size, stride=None, padding=0, ceil_mode=False, count_include_pad=True, divisor_override=None)
kernel_size
: 池化窗口大小stride
: 步长(默认为kernel_size)padding
: 填充(默认为0)ceil_mode
: 当为True时,使用ceil而非floor计算输出形状
3.2.3 nn.Linear
nn.Linear(in_features, out_features, bias=True)
in_features
: 输入特征维度out_features
: 输出特征维度bias
: 是否使用偏置
3.3 模型训练完整示例
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader# 1. 数据准备
def prepare_mnist_data(batch_size=64):"""准备MNIST数据集参数:batch_size (int): 批量大小返回:train_loader, test_loader: 训练和测试数据加载器"""# 数据转换transform = transforms.Compose([transforms.Resize((32, 32)), # LeNet-5原始输入是32x32transforms.ToTensor(),transforms.Normalize((0.1307,), (0.3081,)) # MNIST的均值和标准差])# 下载并加载训练集train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)# 下载并加载测试集test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)return train_loader, test_loader# 2. 训练函数
def train(model, device, train_loader, optimizer, epoch, criterion):"""训练一个epoch参数:model: 模型实例device: 训练设备(CPU/GPU)train_loader: 训练数据加载器optimizer: 优化器epoch: 当前epoch数criterion: 损失函数"""model.train()for batch_idx, (data, target) in enumerate(train_loader):data, target = data.to(device), target.to(device)# 梯度清零optimizer.zero_grad()# 前向传播output = model(data)# 计算损失loss = criterion(output, target)# 反向传播loss.backward()# 参数更新optimizer.step()# 打印训练进度if batch_idx % 100 == 0:print(f'Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} 'f'({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}')# 3. 测试函数
def test(model, device, test_loader, criterion):"""测试模型性能参数:model: 模型实例device: 测试设备(CPU/GPU)test_loader: 测试数据加载器criterion: 损失函数返回:test_loss: 平均测试损失accuracy: 测试准确率"""model.eval()test_loss = 0correct = 0with torch.no_grad():for data, target in test_loader:data, target = data.to(device), target.to(device)output = model(data)# 累加损失test_loss += criterion(output, target).item()# 计算正确预测数pred = output.argmax(dim=1, keepdim=True)correct += pred.eq(target.view_as(pred)).sum().item()# 计算平均损失和准确率test_loss /= len(test_loader.dataset)accuracy = 100. * correct / len(test_loader.dataset)print(f'\nTest set: Average loss: {test_loss:.4f}, 'f'Accuracy: {correct}/{len(test_loader.dataset)} ({accuracy:.2f}%)\n')return test_loss, accuracy# 4. 主函数
def main():# 超参数设置batch_size = 64epochs = 10learning_rate = 0.01momentum = 0.9# 设备配置device = torch.device("cuda" if torch.cuda.is_available() else "cpu")# 准备数据train_loader, test_loader = prepare_mnist_data(batch_size)# 初始化模型model = LeNet5().to(device)# 定义损失函数和优化器criterion = nn.CrossEntropyLoss()optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum)# 训练循环for epoch in range(1, epochs + 1):train(model, device, train_loader, optimizer, epoch, criterion)test(model, device, test_loader, criterion)# 保存模型torch.save(model.state_dict(), "lenet5_mnist.pth")if __name__ == '__main__':main()
3.4 模型验证与预测
训练完成后,我们可以使用训练好的模型进行预测:
def predict(model, device, image):"""使用训练好的模型进行预测参数:model: 训练好的模型device: 设备(CPU/GPU)image: 输入图像(PIL Image或numpy数组)返回:pred (int): 预测类别prob (torch.Tensor): 各类别概率"""# 转换为张量并添加批次维度transform = transforms.Compose([transforms.Resize((32, 32)),transforms.ToTensor(),transforms.Normalize((0.1307,), (0.3081,))])image = transform(image).unsqueeze(0).to(device)# 预测model.eval()with torch.no_grad():output = model(image)prob = F.softmax(output, dim=1)pred = output.argmax(dim=1, keepdim=True)return pred.item(), prob[0]# 示例使用
from PIL import Image
import matplotlib.pyplot as plt# 加载训练好的模型
model = LeNet5().to(device)
model.load_state_dict(torch.load("lenet5_mnist.pth"))# 加载测试图像
image = Image.open("test_digit.png").convert('L') # 转换为灰度图
plt.imshow(image, cmap='gray')
plt.show()# 进行预测
pred, prob = predict(model, device, image)
print(f"Predicted digit: {pred}")
print(f"Class probabilities: {prob}")
四、LeNet-5 的现代改进
虽然原始的 LeNet-5 已经不再是最先进的模型,但我们可以对其进行一些现代改进:
激活函数:将 Tanh 改为 ReLU
池化方式:将平均池化改为最大池化
Dropout:在全连接层添加 Dropout 防止过拟合
Batch Normalization:添加批归一化层加速训练
改进版的 LeNet-5 实现:
class ModernLeNet5(nn.Module):def __init__(self, num_classes=10):super(ModernLeNet5, self).__init__()self.features = nn.Sequential(nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=0),nn.BatchNorm2d(6),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=2, stride=2),nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0),nn.BatchNorm2d(16),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=2, stride=2),nn.Conv2d(16, 120, kernel_size=5, stride=1, padding=0),nn.BatchNorm2d(120),nn.ReLU(inplace=True),)self.classifier = nn.Sequential(nn.Linear(120, 84),nn.BatchNorm1d(84),nn.ReLU(inplace=True),nn.Dropout(0.5),nn.Linear(84, num_classes),)def forward(self, x):x = self.features(x)x = torch.flatten(x, 1)x = self.classifier(x)return x
五、总结
LeNet-5 作为卷积神经网络的先驱,虽然结构简单,但包含了现代 CNN 的核心思想。通过本教程,我们:
详细分析了 LeNet-5 的网络结构
使用 PyTorch 实现了原始版本和改进版本
解释了关键 API 的参数和使用方法
提供了完整的训练和预测代码
尽管 LeNet-5 已经无法处理现代复杂的计算机视觉任务,但理解它的设计思想对于学习更先进的 CNN 架构至关重要。建议读者在掌握 LeNet-5 后,继续学习 AlexNet、VGG、ResNet 等更复杂的网络结构。