【动手学深度学习】LeNet:卷积神经网络的开山之作
1,LeNet卷积神经网络简介
LeNet 是一种经典的卷积神经网络,是现代卷积神经网络的起源之一。它是早期成功的神经网络;LeNet先使用卷积层来学习图片空间信息,使用池化层降低图片敏感度,然后使用全连接层来转换到类别空间。 其思想被广泛应用于图像分类、目标检测、图像分割等多个计算机视觉领域
,为这些领域的研究和发展提供了新的思路和方法。例如,在安防领域用于面部识别和监控系统,在自动驾驶领域用于实时视频分析和对象跟踪等。
1989年,Yann LeCun等人在贝尔实验室工作期间提出了LeNet-1。这个网络主要用于手写数字识别,引入了卷积操作和权值共享的概念,简化了网络结构,减少了参数数量,提高了模型的泛化能力和训练速度。此后经过多年的迭代改进,1998年,LeCun等人正式发表了LeNet-5。LeNet-5在LeNet-1的基础上进一步优化了网络结构,增加了网络的深度和复杂度,使其在手写数字识别任务上取得了更好的性能。LeNet-5的成功应用证明了CNN在图像识别领域的巨大潜力,为后续CNN的发展奠定了坚实的基础。
2,Fashion-MNIST图像分类数据集
Fashion-MNIST数据集是一个广泛使用的图像分类数据集。
Fashion-MNIST中包含的10个类别
,分别为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。
之前,已经学习过Fashion-MNIST数据集。 【动手学深度学习】Fashion-MNIST图片分类数据集,其基本情况如下:
- 训练集:包含60,000张图像,用于模型训练;
- 测试集:包含10,000张图像,用于评估模型性能;
- 数据集由灰度图像组成,其通道数为1;
- 每个图像的高度和宽度均为28像素;
- 调用load_data_fashion_mnist()函数加载数据集;
具体定义如下:
python
"""
下载Fashion-MNIST数据集,然后将其加载到内存中
参数resize表示调整图片大小
"""
def load_data_fashion_mnist(batch_size, resize=None):
# trans是一个用于转换的 *列表*
trans = [transforms.ToTensor()]
if resize: # resize不为空,表示需要调整图片大小
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)
return (data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=get_dataloader_workers()))
3,LeNet总体架构
总体来看,LeNet(LeNet-5)由两个部分组成:
- 卷积编码器:由两个卷积层组成;
- 全连接层密集块:由三个全连接层组成;

每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均汇聚层。(实际上使用ReLU激活函数和最大汇聚层更有效,但当时还没有发现):
-
Fashion-MNIST数据集的图像通道为1,大小为28×28,内部
经过卷积层填充
之后得到的实际输入数据是32×32的图像数据; -
第一卷积层有
6
个输出通道,而第二个卷积层有16
个输出通道; -
对应输出通道的数量,第一个卷积层有6个5×5的卷积核,第二个卷积层有16个5×5的卷积核;
-
每个卷积核应用于输入数据时会产生一个特征图(feature map),也就是一个输出通道;
-
每个卷积层都使用不同数量的5×5的卷积核和一个sigmoid激活函数。这些层将输入映射到多个二维特征输出,通常同时增加通道的数量;
-
卷积操作后,通过
2×2的池化操作
(默认步幅为2
和池化窗口大小保持一致)将原特征图的各维度减半。比如原来是28×28,池化后变为14×14;
4,LeNet代码实现
接下来使用深度学习框架实现LeNet模型,并进行训练和测试。
4.1,定义LeNet模型
LeNet模型总共七层: 两层卷积层、两层池化层、三层全连接层; 其中每层都使用sigmod作为激活函数,它将卷积层的输出压缩到0和1之间,有助于非线性变换。
python
import torch
from torch import nn
from d2l import torch as d2l
""" 默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同(窗口没有重叠)"""
# nn.Sequential 是一个容器,可按顺序包装一系列子模块(如层、激活函数)。使得模型的构建变得更加简洁
net = nn.Sequential(
# 第一个二维卷积层,输入通道是1(灰度图像),输出通道是6,卷积核大小5×5,图像周围加入两层0填充
# 使用sigmod激活函数
nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
# 第一个平均池化层:用2x2的池化窗口,步长为2。经此池化操作后得6个14×14的特征图
nn.AvgPool2d(kernel_size=2, stride=2),
# 这是第二个二维卷积层,输入通道数为6(与第一个卷积层的输出通道数相匹配),输出通道数为16。卷积核的大小为5x5,没有使用padding填充
# 使用sigmod激活函数
nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
# 第二个平均池化层:配置与第一层平均池化层相同。
nn.AvgPool2d(kernel_size=2, stride=2),
# 在将数据传递给全连接层之前,需要将多维的卷积和池化输出展平为一维向量。以便传给全连接层
nn.Flatten(),
# 经过前面的卷积和池化操作后,输出16个5×5的特征图
# 全连接层,输入特征的数量是16 * 5 * 5。输出特征的数量是120。
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
# 全连接层,输入特征数量120,输出84
nn.Linear(120, 84), nn.Sigmoid(),
# 全连接层,输入特征数量84,输出10,对应Fashion-MNIST数据集的10个类别
nn.Linear(84, 10))
下面,我们将一个大小为 28 × 28 28 \times 28 28×28的单通道(黑白)图像通过LeNet。通过在每一层打印输出的形状,我们可以检查模型,以确保其操作与我们期望的一致。
python
# 打印调试信息,检查模型
# size=(1, 1, 28, 28):批次大小1,通道数1,形状28*28
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
# 遍历了神经网络 net 中的每一层
for layer in net:
X = layer(X)
# 打印该层的类型(Conv2d、AvgPool2d、Flatten、Linear)以及输出张量的形状
print(layer.__class__.__name__,'output shape: \t',X.shape)
# torch.Size([1, 6, 28, 28])中的1代表批次大小,6表示通道数
运行结果如下:

4.2,定义模型评估函数
我们已经实现了LeNet,接下来让我们看看LeNet在Fashion-MNIST数据集上的表现。
加载Fashion-MNIST图片分类数据集
python
batch_size = 256 # 批量大小
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
定义评估函数计算预测准确率
python
def evaluate_accuracy_gpu(net, data_iter, device=None):
"""使用GPU计算模型在数据集上的精度"""
if isinstance(net, nn.Module):
net.eval() # 设置为评估模式
if not device:
# 若没有指定device,则通过获取模型参数的第一个元素的设备来确定应该使用的设备
# net.parameters()返回模型的所有可学习参数(如权重和偏置)
# next() 函数从迭代器中获取第一个元素。通常是第一个层的权重或偏置
# .device 是 PyTorch 张量(torch.Tensor)的一个属性,表示该张量所在的备(如 GPU 或 CPU)
# 例如,模型在 GPU 上运行,.device 的值可能是 device(type='cuda', index=0)
device = next(iter(net.parameters())).device
# 累加器记录正确预测的数量和总预测的数量
metric = d2l.Accumulator(2)
with torch.no_grad(): # 评估模型时,不需要计算梯度
for X, y in data_iter: # 每次迭代获取一个数据批次X和对应的标签y
if isinstance(X, list): # x为list,每个元素都挪到对应的设备
X = [x.to(device) for x in X]
else: # x是tensor,只需要挪一次
X = X.to(device)
y = y.to(device)
# accuracy可以计算出预测正确的样本数量
# y.numel()计算出样本总数
metric.add(d2l.accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
4.3,定义训练函数进行训练
定义可以使用GPU训练的训练函数。
python
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
"""用GPU训练模型"""
def init_weights(m): # 初始化权重
# 如果是全连接层或卷积层使用Xavier均匀初始化方法
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
print('training on', device)
# 模型移动到设备
net.to(device)
# 使用随机梯度下降(SGD)优化器
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
# 使用交叉熵损失函数(nn.CrossEntropyLoss),适用于分类任务
loss = nn.CrossEntropyLoss()
# 实现动画效果打印输出
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
timer, num_batches = d2l.Timer(), len(train_iter)
for epoch in range(num_epochs):
# 累加器记录训练损失之和,训练准确率之和,样本数
metric = d2l.Accumulator(3)
# 将模型设置为训练模式,这会启用Dropout等训练时特有的操作
net.train()
for i, (X, y) in enumerate(train_iter): # 遍历训练数据集
timer.start()
optimizer.zero_grad() # 梯度清零
X, y = X.to(device), y.to(device) # 将输入数据X和标签y移动到指定的设备
# 前向传播,得到预测结果 y_hat
y_hat = net(X)
"""
在 PyTorch 中,nn.CrossEntropyLoss 默认会对每个样本的损失值进行平均,返回的是批次中所有样本损失的平均值。
"""
l = loss(y_hat, y) # 计算损失
# 进行反向传播,计算梯度。
l.backward()
# 使用优化器更新模型参数。
optimizer.step()
with torch.no_grad(): # 禁用梯度计算
# l * X.shape[0]是当前批次的总损失。样本平均损失乘当前批次样本数
# d2l.accuracy(y_hat, y) 计算当前批次正确预测的样本数
# X.shape[0]代表当前批次的样本数
# 最终累加器累积了整个训练集的总损失,预测正确的样本总数和总样本数
metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
timer.stop()
# 计算整个训练集上每个训练样本的平均损失
train_l = metric[0] / metric[2]
# 计算训练准确率
train_acc = metric[1] / metric[2]
# 更新动画
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
# 在每个epoch结束时,计算测试集上的准确率
test_acc = evaluate_accuracy_gpu(net, test_iter)
# 更新动画
animator.add(epoch + 1, (None, None, test_acc))
# 打印训练损失、训练准确率和测试准确率
print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')
# 打印训练过程中每秒处理的样本数(即训练效率),以及训练所使用的设备。
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(device)}')
调用函数进行训练
python
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
运行结果如下:
