李沐【动手学深度学习v2】23经典卷积神经网络LeNet
其中,比较重要的层分别是:Conv2d是卷积层,输入到输出的过程中使用了适当的填充(padding),使得输出的高度和宽度与输入相同,且通道数增加6;Flatten将多维张量展平为一维向量,输入形状为 (1,16,5,5),展平后为 (1,16*5*5) = (1,400);LeNet网络的输入层通常为通道数为1的灰度图像,其大小为32×32,输出层是一个由10个神经元组成的softmax高斯连接
本文作为对该课程第23课“经典卷积神经网络 LeNet”的学习笔记,介绍LeNet网络并对其代码进行较为详细的注释与解读。
一. 什么是LeNet?
LeNet 是一个经典的卷积神经网络(CNN)架构,由 Yann LeCun 等人在 1998 年提出,最早应用于手写体字符识别,特别是在邮政编码识别中表现非常出色。它是现代卷积神经网络的起源之一,奠定了CNN在计算机视觉领域的基础。其原理如下图所示:

LeNet的网络结构主要由卷积层、池化层和全连接层组成,这里将介绍经典的 LeNet-5 架构。
LeNet网络的输入层通常为通道数为1的灰度图像,其大小为32×32,输出层是一个由10个神经元组成的softmax高斯连接层,可以用来做分类任务,例如手写数字0-9的分辨。
除了输入输出层外共有6个核心层组成,分别是
卷积层C1:使用6个大小为5×5的不同类型的卷积核,步幅为1,无填充,卷积后输出特征图大小为 28×28×6;池化层S2:使用最大池化层,池化窗口大小为2×2,步幅为2,经池化后得到输出特征图大小为14×14×6;卷积层C3:使用16个大小为5×5的不同卷积核,步幅为1,无填充,卷积后得到输出特征图大小为10×10×16;池化层 S4:使用最大池化层,池化窗口大小为2×2,步幅为 2,输出特征图大小为5×5×16;全连接层C5:将5×5×16=400的特征图展平为一维向量,输入到120个神经元的全连接层,输出120个1×1大小的特征图;全连接层F6:120个神经元作为输入,84个神经元输出,使其成为一个由84个神经元组成的全连接隐藏层,激活函数使用sigmoid 函数。
LeNet 是卷积神经网络的先驱,证明了卷积层在图像识别任务中的有效性,并为后来的深度学习模型(如 AlexNet、VGG、ResNet 等)奠定了基础,尽管结构相对简单,但它仍然是理解卷积神经网络原理的经典范例。
二、代码实现
我们要利用LeNet网络做mnist手写数字识别,而mnist数据集中的图像大小为28×28,也就是说,要对mnist的数据进行两圈值为零的像素填充,除此之外的其他部分与LeNET-5一致。
首先,我们导入需要用到的函数库并将LeNet中所有的层进行定义。
import torch
from torch import nn
from d2l import torch as d2l
class Reshape(torch.nn.Module): # 自定义类,用于调整输入张量形状
def forward(self,x):
return x.view(-1,1,28,28) # 批量数不变、通道数1(单通道灰度图像)、28×28的输入
# 定义神经网络net、torch.nn.Sequential用于按顺序堆叠神经网络层
net = torch.nn.Sequential(
Reshape(), # 将输入数据调整为刚才设定的形状
# 第一个卷积层,输入通道数为1,输出通道数为6,卷积核大小为5x5,填充为2
# Sigmoid激活函数,用于引入非线性
nn.Conv2d(1, 6, kernel_size=5,padding=2), nn.Sigmoid(),
# 最大池化层,池化窗口大小为2x2,步幅为2
nn.MaxPool2d(kernel_size=2, stride=2),
# 第二个卷积层,输入通道数为6,输出通道数为16,卷积核大小为5x5
nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
nn.MaxPool2d(kernel_size=2, stride=2), # 第二个最大池化层
nn.Flatten(), # 将多维张量展平为一维,以便输入全连接层。
# 第一个全连接层,输入大小为16*5*5(经过卷积和池化后的特征图大小)输出大小为120
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
nn.Linear(120, 84), nn.Sigmoid(), # 第二个全连接层,输入大小为120,输出大小为84
nn.Linear(84, 10)) # 最后一个全连接层,输入大小为84,输出大小为10
通过对每一层的打印,检查一下模型是否按照它的原理进行不断的输入输出。
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32) # 生成一个随机张量
for layer in net: # 遍历net的每一层
X = layer(X) # 将输入X通过当前层layer进行前向传播,得到新的X
print(layer.__class__.__name__, 'output shape: \t', X.shape)
# 打印当前层的类名和输出张量的形状
输出结果:

其中,比较重要的层分别是:Conv2d是卷积层,输入到输出的过程中使用了适当的填充(padding),使得输出的高度和宽度与输入相同,且通道数增加6;Sigmoid是对卷积层的输出应用的激活函数,其输出形状与输入形状相同;MaxPool2d是最大池化层,使用 2x2 的池化窗口,步幅为 2,因此高度和宽度减半;第二个Conv2d未使用填充(padding),因此输出的高度和宽度减小;Flatten将多维张量展平为一维向量,输入形状为 (1,16,5,5),展平后为 (1,16*5*5) = (1,400);Linear是全连接层,对展平后的数据进行线性变换,输出为特征数量。
通过这种方式,可以逐层检查网络的输出形状,确保每一层的设计符合预期。
接下来,导入数据集,这里使用Fashion-MNIST数据集(因为mnist太常见了)
batch_size = 256 # 设置批量大小,表示每次从数据集中加载的样本数量
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
# 调用d2l库中的load_data_fashion_mnist函数,加载Fashion-MNIST数据集
运行后会自动下载Fashion-MNIST数据集。
这里定义了一个函数evaluate_accuracy_gpu,用于计算模型在指定数据集上的精度,并且在GPU上运行(CPU也可以跑,但是GPU更快),因此我们要将内存上的数据集复制到显存中。以下是对代码的注释:
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
"""使用GPU计算模型在数据集上的精度"""
if isinstance(net, nn.Module):
net.eval() # 将模型设置为评估模式
if not device: # 如果未指定device,则从模型的参数中推断设备
device = next(iter(net.parameters())).device
# net.parameters()返回模型的所有参数 next()获取第一个参数
# .device获取该参数所在的设备
metric = d2l.Accumulator(2)
# 创建一个长度为2的累加器,用于存储两个值:metric[0]正确预测的数量和metric[1]总样本数
with torch.no_grad(): # 在评估过程中禁用梯度计算,以减少内存消耗并加速计算
for X, y in data_iter: # 遍历数据迭代器,每次获取一个批量的数据
if isinstance(X, list): # 将输入数据X和标签y移动到指定的设备
X = [x.to(device) for x in X]
else:
X = X.to(device)
y = y.to(device)
metric.add(d2l.accuracy(net(X), y), y.numel())
# 计算当前批量的精度,并更新累加器,算一下y的元素个数
return metric[0] / metric[1] # 计算并返回模型在整个数据集上的精度
# 精度 = 正确预测的数量/总样本数
接下来,我们定义训练函数:
这其中有个Xavier函数,它初始化的核心思想是根据输入和输出的维度,合理地初始化权重,使得每一层的输出值的方差保持一致,防止模型梯度消失或梯度爆炸。
#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
# train_iter训练数据迭代器, test_iter测试数据迭代器
"""用GPU训练模型"""
# 初始化模型权重
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight) # Xavier初始化方法,适用于激活函数为线性的情况
net.apply(init_weights) # 对模型的所有子模块递归调用init_weights函数
print('training on', device) # 打印当前训练设备
net.to(device) # 将模型移动到指定设备
optimizer = torch.optim.SGD(net.parameters(), lr=lr) # 随机梯度下降 SGD作为优化器
loss = nn.CrossEntropyLoss() # loss用于计算预测值与真实标签之间的差异
# 初始化一个动画绘制器,用于可视化训练过程中的损失和精度
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): # 开始逐轮 epoch训练模型
metric = d2l.Accumulator(3) # 初始化累加器 metric,用于统计损失、正确预测数和样本数
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 = net(X) # 模型通过前向传播得到预测值 y_hat
l = loss(y_hat, y) # 计算损失l
l.backward() # 反向传播计算梯度
optimizer.step() # 更新模型参数
with torch.no_grad(): # 统计训练指标
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))
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)}')
最后,指定学习率和epoch,调用训练函数开始训练。
lr, num_epochs = 0.9, 10 # 指定学习率和训练轮数
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) # 调用训练函数
训练结果如下: 测试精度为0.833,这里显示用cpu跑的,但也不影响,装gpu版本的pytorch会显示gpu,也有显示conda的情况。

更多推荐


所有评论(0)