【动手学深度学习】LeNet:卷积神经网络的开山之作
- 1,LeNet卷积神经网络简介
- 2,Fashion-MNIST图像分类数据集
- 3,LeNet总体架构
- 4,LeNet代码实现
- 4.1,定义LeNet模型
- 4.2,定义模型评估函数
- 4.3,定义训练函数进行训练
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()函数加载数据集;
具体定义如下:
"""
下载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之间,有助于非线性变换。
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,输出84nn.Linear(120, 84), nn.Sigmoid(),# 全连接层,输入特征数量84,输出10,对应Fashion-MNIST数据集的10个类别nn.Linear(84, 10))
下面,我们将一个大小为 28 × 28 28 \times 28 28×28的单通道(黑白)图像通过LeNet。通过在每一层打印输出的形状,我们可以检查模型,以确保其操作与我们期望的一致。
# 打印调试信息,检查模型
# 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图片分类数据集
batch_size = 256 # 批量大小
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
定义评估函数计算预测准确率
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和对应的标签yif 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训练的训练函数。
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_haty_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)}')
调用函数进行训练
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
运行结果如下: