欢迎关注『youcans动手学模型』系列
本专栏内容和资源同步到 GitHub/youcans
本文用 PyTorch 实现 LeNet5 网络模型,使用 MNIST 数据集训练模型,进行手写数字识别。
1. LeNet5 卷积神经网络模型
Yann LeCun (2018年获得图灵奖)等在 1998 年发表论文“Gradient-Based Learning Applied to Document Recognition”,提出的 LeNet5 模型是卷积神经网络的开山之作,也是深度学习的第一个里程碑。
论文下载地址:下载1,下载2
Yann LeCun 等在 1989年创造性地提出了 LeNet 卷积神经网络模型,并使用反向传播算法训练模型,解决识别手写邮政编码问题。1990年,LeNet 模型应用于美国邮政局的邮政编码识别系统,错误率仅为 1%,拒绝率约 9%,成为最早实用的手写数字识别系统。经过多年的迭代改进,成为 1998年论文中的 LeNet-5 网络模型,是最早的卷积神经网络模型。虽然今天看来这个网络非常简单,性能也很差,但其原理仍然是各种卷积神经网络的基础。
1.1 论文简介
使用反向传播算法(BP算法)训练多层神经网络,是梯度学习技术的一个最佳范例。对于给定的网络结构,经过简单的预处理,基于梯度的学习算法就可以用来构造一个复杂的决策面,对高维模式特征(例如手写字符)进行分类。本文回顾了用于手写字符识别的各种方法,并对它们在手写数字识别任务中的性能进行了比较。卷积神经网络(CNN)是专门为处理二维图像而设计的,其性能优于其它方法。
实际应用的文档识别系统由多个模块组成,包括字段提取、分割、识别和语言建模。一种新的学习范式,称为图变换网络(GTN),使用基于梯度的方法对这种多模块系统进行全局训练,以获得最优的总体评价指标。本文介绍了两种在线手写识别系统,实验证明了全局训练的优势以及图变换网络的灵活性。
本文描述了一种用于读取银行支票的图变换网络,使用卷积神经网络字符识别器,结合全局训练技术,准确地识别商业和个人支票。该系统已被商业化部署,每天读取数百万张支票。
1.2 卷积神经网络
本文的最大贡献在于开创性地提出了卷积神经网络(CNN),从而开创了深度学习的研究方向。我们首先回顾原文对于卷积神经网络的介绍。
卷积网络结合了三种架构思想,以确保一定程度的移位、缩放和失真的不变性:局部感受野、共享权重(或权值复制)以及空间或时间子采样。
图 2 显示了一个用于识别字符的典型卷积网络,称为 LeNet-5。输入层接收标准化的字符图像,每层的神经元只接收来自于前一层的较小邻域中的输入(注:这是与全连接层 FC 在网络结构上的根本差异)。
将神经元的输入连接到局部感受野的想法,可以追溯到 60年代早期的感知器,当时 Hubel 和 Wiesel (1981年获得诺贝尔奖)在猫的视觉系统中发现局部敏感的选择性神经元。1979年, 福岛邦彦(2021年获得鲍尔奖)提出了 Neocognitron 视觉学习神经模型,使用了局部连接、平均池化和 ReLU 非线性激活函数。1985年,Hinton(2018年获得图灵奖)等提出了反向传播(Back Propagation,BP)梯度学习算法 。
利用局部感受野,神经元可以提取基本的视觉特征,如定向边缘、端点、角点(或其他信号中的类似特征,如语音频谱)。这些特征由后续的网络层组合,以便检测高阶特征。
如前所述,输入的失真或偏移会导致图像特征的位置发生变化。此外,对图像的一部分有用的基本特征检测器可能对整个图像有用。这种知识可以通过在一组单元(其感受野位于图像上的不同位置)使用相同的权重向量来实现。同一层中的神经元共享一组连接权值。
一个卷积层由几个特征图(分别具有不同的权重向量)组成,因此可以在每个位置提取多个特征。例如 LeNet-5 的第一层有 6个特征图。特征图中的每个单元有 25个输入,连接到上一层的 5×5 区域,称为感受野。每个单元有 25个输入,共有 25个权值参数和 1个阈值参数。每个特征图共享一组权值参数,6个特征图共有 6组不同的权值参数,可以提取 6种不同类型的特征。
这种运算相当于卷积,卷积核就是特征图所使用的连接权值,因此称为卷积网络。
1.3 LeNet5 网络
最初的 LeNet 网络采用 5层网络,包括 2个卷积层、2个池化层(汇聚层)和 1个全连接层,网络结构如下:
-
输入层为 28×28 的单通道图像。
-
C1 卷积层:4个 5×5 卷积核,得到 4 个 24×24 特征图。
-
S1 池化层: 2×2 的平均池化层,图像高宽减半,得到 4 个 12×12 特征图。
-
C2 卷积层:12个 5×5 卷积核,得到 12 个 8×8 特征图。
-
S2 池化层:2×2 的平均池化层,图像高宽减半,,得到 12 个4×4 特征图。
-
FC 全连接层: 全连接隐藏层,使用 sigmoid 函数。
LeNet-5 网络是 LeNet 网络的改进版,采用 7层网络,包括 3个卷积层、2个池化层和 2个全连接层。
- 输入层为 32×32 的单通道图像。
- C1 卷积层:6个 5×5 卷积核,156个训练参数,得到 6个 28×28 特征图。
- S2 池化层:2×2 的最大池化层,图像高宽减半,12个训练参数,得到 6个 14×14 特征图。
- C3 卷积层:16个 5×5 卷积核,1516个训练参数,得到 16个 10×10 特征图。
- S4 池化层:2×2 的最大池化层,图像高宽减半,32个训练参数,得到 16个 5×5 特征图。
- C5 卷积层:120个 5×5 卷积核,48120个训练参数,得到 120个 1×1 的特征图,相当于全连接。
- F6 全连接层:由 84个神经元组成的隐藏层,10164个训练参数,使用 sigmoid 函数。
- F7 输出层:由10 个 RBF 神经元组成的输出层。
特殊地,C3 层与S2层不是全部连接,而是按照作者选取的方式连接,以减少计算和提取更多特征。但在后来的研究中,随着网络越来越复杂,通常不使用这种人工选择方法。
1.4 模型的运行结果
2. 在 PyTorch 中定义 LeNet5 模型类
2.1 使用 nn.Module 定义网络模型类
PyTorch 通过 torch.nn 模块提供了高阶的 API,可以从头开始构建网络。
使用 PyTorch 构造神经网络模型,需要运用__call__()
和__init__()
方法定义模型类 Class。nn.Module
是所有神经网络单元(neural network modules)的基类。
PyTorch在 nn.Module 中实现了__call__()
方法,在 __call__()
方法中调用 forward 函数。__init__()
方法是类的初始化函数,类似于C++的构造函数。
LeNet 模型类的例程如下:
import torch.nn as nn
import torch.nn.functional as F
# 定义 LeNet5 模型类 1
class LeNet5v1(nn.Module):
def __init__(self):
super(LeNet5v1, self).__init__()
self.conv1 = nn.Conv2d(1, 6, 5, padding='same') # C1: 输入 1,输出 6,卷积核 5x5
self.pool1 = nn.AvgPool2d(2, 2) # S2: 卷积核 2x2,步长 2
self.conv2 = nn.Conv2d(6, 16, 5) # C3: 输入 6,输出 16,卷积核 5x5
self.pool2 = nn.AvgPool2d(2, 2) # S4: 卷积核 2x2,步长 2
self.flatten = nn.Flatten() # 展平为一维
self.linear1 = nn.Linear(400, 120) # C5: 输入 400,输出 120
self.linear2 = nn.Linear(120, 84) # F6: 输入 120,输出 84
self.linear3 = nn.Linear(84, 10) # F7: 输入 84,输出 10
def forward(self, x):
x = F.relu(self.conv1(x)) # (1,28,28) -> (6,28,28)
x = self.pool1(x) # (1,28,28) -> (6,14,14)
x = F.relu(self.conv2(x)) # (6,14,14) -> (16,10,10)
x = self.pool2(x) # (16,10,10) -> (16,5,5)
x = self.flatten(x) # (16,5,5) -> (400)
x = F.relu(self.linear1(x)) # (400) -> (120)
x = F.relu(self.linear2(x)) # (120) -> (84)
x = self.linear3(x) # (84) -> (10)
return x
使用 print 可以输出 LeNet 模型的结构如下:
LeNet5(
(conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1), padding=same)
(pool1): AvgPool2d(kernel_size=2, stride=2, padding=0)
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(pool2): AvgPool2d(kernel_size=2, stride=2, padding=0)
(flatten): Flatten(start_dim=1, end_dim=-1)
(linear1): Linear(in_features=400, out_features=120, bias=True)
(linear2): Linear(in_features=120, out_features=84, bias=True)
(linear3): Linear(in_features=84, out_features=10, bias=True)
)
在 LeNet5v1 模型类中,激活函数并没有体现在模型结构中,而是在 forward 函数前向计算时实现的。
2.2 使用 Sequential 容器构造模型类
nn.Sequential() 是一个有序的容器,该类按照传入构造器的顺序,将多个构造函数依次添加到计算图中执行。通过 Sequential 可以构建序列化的模块,使得网络模块的层次更加清晰,便于构造大型和复杂的网络模型。
简单地,以参数列表方式将 LeNet5 模型中的各网络层顺序添加到 Sequential 容器,在 init 方法中直接定义一个model,简化 forward 方法 。
# 定义 LeNet5 模型类 2
class LeNet5v2(nn.Module):
def __init__(self):
super(LeNet5v2, self).__init__()
self.model = nn.Sequential( # 顺序容器
nn.Conv2d(1, 6, 5, padding='same'), # C1: 输入 1,输出 6,卷积核 5x5
nn.ReLU(), # 激活函数
nn.AvgPool2d(2, 2), # S2: 卷积核 2x2,步长 2
nn.Conv2d(6, 16, 5), # C3: 输入 6,输出 16,卷积核 5x5
nn.ReLU(), # 激活函数
nn.AvgPool2d(2, 2), # S4: 卷积核 2x2,步长 2
nn.Flatten(), # 展平为一维
nn.Linear(400, 120), # C5: 输入 400,输出 120
nn.ReLU(), # 激活函数
nn.Linear(120, 84), # F6: 输入 120,输出 84
nn.ReLU(), # 激活函数
nn.Linear(84, 10) # F7: 输入 84,输出 10
)
def forward(self, x):
x = self.model(x)
return x
使用 print 可以输出 LeNet v2模型的结构如下:
LeNet5v2(
(model): Sequential(
(0): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1), padding=same)
(1): ReLU()
(2): AvgPool2d(kernel_size=2, stride=2, padding=0)
(3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(4): ReLU()
(5): AvgPool2d(kernel_size=2, stride=2, padding=0)
(6): Flatten(start_dim=1, end_dim=-1)
(7): Linear(in_features=400, out_features=120, bias=True)
(8): ReLU()
(9): Linear(in_features=120, out_features=84, bias=True)
(10): ReLU()
(11): Linear(in_features=84, out_features=10, bias=True)
)
)
在 LeNet5v2 模型类中,激活函数直接体现在模型结构中,模型结构更加清晰完整。
2.3 使用 Sequential 分层构造模型类
通常把卷积、池化和非线性激活函数组合起来,作为一个网络层使用。通过 Sequential 可以逐层构造网络,也可以访问指定的层,并通过 parameters、weights 等参数显示网络的参数和权重。
用 Sequential 容器构造每个网络层,定义 LeNet5 模型类如下。
# 定义 LeNet5 网络结构 3
class LeNet5v3(nn.Module):
def __init__(self):
super(LeNet5v3, self).__init__() # 调用父类的构造函数
# 卷积池化层
self.conv_pool1 = nn.Sequential(
nn.Conv2d(1, 6, 5, padding=2), # C1: 输入 1,输出 6,卷积核 5x5,填充 2
nn.ReLU(), # ReLU 激活函数
nn.AvgPool2d(2, stride=2) # S2: 卷积核 2x2,步长 2
)
self.conv_pool2 = nn.Sequential(
nn.Conv2d(6, 16, 5), # C3: 输入 6,输出 16,卷积核 5x5
nn.ReLU(), # ReLU 激活函数
nn.AvgPool2d(2, stride=2) # S2: 卷积核 2x2,步长 2
)
# 全连接层
self.fc1 = nn.Sequential(
nn.Linear(16*5*5, 120),
nn.ReLU()
)
self.fc2 = nn.Sequential(
nn.Linear(120, 84),
nn.ReLU()
)
# 输出层
self.out = nn.Sequential(
nn.Linear(84, 10)
)
def forward(self, x):
x = self.conv_pool1(x) # (1,28,28) -> (6,14,14)
x = self.conv_pool2(x) # (6,14,14) -> (16,5,5)
x = x.view(x.size(0), -1) # (16,5,5) -> (400), 展平为一维
x = self.fc1(x) # (400) -> (120)
x = self.fc2(x) # (120) -> (84)
x = self.out(x) # (84) -> (10)
return x
使用 print 可以输出 LeNet v3模型的结构如下:
LeNet5(
(conv_pool1): Sequential(
(0): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(1): ReLU()
(2): AvgPool2d(kernel_size=2, stride=2, padding=0)
)
(conv_pool2): Sequential(
(0): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(1): ReLU()
(2): AvgPool2d(kernel_size=2, stride=2, padding=0)
)
(fc1): Sequential(
(0): Linear(in_features=400, out_features=120, bias=True)
(1): ReLU()
)
(fc2): Sequential(
(0): Linear(in_features=120, out_features=84, bias=True)
(1): ReLU()
)
(out): Sequential(
(0): Linear(in_features=84, out_features=10, bias=True)
)
)
3. 基于 LeNet5 模型的 MNIST 手写数字识别
3.1 PyTorch 建立神经网络模型的基本步骤
使用 PyTorch 建立、训练和使用神经网络模型的基本步骤如下。
- 准备数据集(Prepare dataset):加载数据集,对数据进行预处理。
- 建立模型(Design the model):实例化模型类,定义损失函数和优化器,确定模型结构和训练方法。
- 模型训练(Model trainning):使用训练数据集对模型进行训练,确定模型参数。
- 模型推理(Model inferring):使用训练好的模型进行推理,对输入数据预测输出结果。
- 模型保存与加载(Model saving/loading):保存训练好的模型,以便以后使用或部署。
以下按此步骤讲解 LeNet5模型的例程。
3.2 加载 MNIST 数据集
通用数据集的样本结构均衡、信息高效,而且组织规范、易于处理。使用通用的数据集训练神经网络,不仅可以提高工作效率,而且便于评估模型性能。
PyTorch 提供了一些常用的图像数据集,预加载在 torchvision.datasets
类中。torchvision
模块实现神经网络所需的核心类和方法, torchvision.datasets
包含流行的数据集、模型架构和常用的图像转换方法。
MNIST 数据集是经典的手写体数字数据集,内容是 0~9 的手写数字,图像是大小为 28*28 的单通道灰度图像。训练集包含 60000 张图像,测试集包含 10000 张图像。
MNIST 数据集可以从官网下载:http://yann.lecun.com/exdb/mnist/ 后使用,也可以使用 datasets 类自动加载(如果本地路径没有该文件则自动下载)。
下载数据集时,使用预定义的 transform 方法进行数据预处理,包括使用 MNIST 数据集的均值和方差对样本数据进行标准化处理,将数据格式转换为张量。注意MNIST是单通道图像,因此均值和方差也是单通道。
大型训练数据集不能一次性加载全部样本来训练,可以使用 Dataloader 类自动加载数据。Dataloader 是一个迭代器,基本功能是传入一个 Dataset 对象,根据参数 batch_size 生成一个 batch 的数据。
# (1) 将[0,1]的 PILImage 转换为[-1,1]的Tensor
transform = transforms.Compose([ # Transform Compose of the image
transforms.ToTensor(), # 将图像转换为张量 Tensor
transforms.Normalize(mean=(0.1307,), std=(0.3081,))]) # 标准化
# (2) 加载 MNIST 数据集
batch_size = 64
# 加载 MNIST 数据集, 如果 root 路径加载失败, 则自动在线下载
# 加载 MNIST 训练数据集, 50000张训练图片
train_set = torchvision.datasets.MNIST(root='../dataset', train=True,
download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size,
shuffle=True, num_workers=2)
# 加载 MNIST 验证数据集, 10000张验证图片
test_set = torchvision.datasets.MNIST(root='../dataset', train=False,
download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=1024,
shuffle=False, num_workers=2)
3.3 建立 LeNet5 网络模型
建立一个 LeNet5 网络模型进行训练,包括三个步骤:
- 实例化 LeNet5 模型对象;
- 设置训练的损失函数;
- 设置训练的优化器。
torch.nn.functional 模块提供了各种内置损失函数,本例使用交叉熵损失函数 CrossEntropyLoss。
torch.optim 模块提供了各种优化方法,本例使用 Adam 优化器。注意要将 model 的参数 model.parameters() 传给优化器对象,以便优化器扫描需要优化的参数。
# (3) 实例化 LeNet-5 网络模型
model = LeNet5() # 实例化 LeNet-5 网络模型
print(model)
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9) # SGD 随机梯度下降优化器
使用 print 可以输出 LeNet 模型的结构如下:
LeNet5(
(conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1), padding=same)
(pool1): AvgPool2d(kernel_size=2, stride=2, padding=0)
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(pool2): AvgPool2d(kernel_size=2, stride=2, padding=0)
(flatten): Flatten(start_dim=1, end_dim=-1)
(linear1): Linear(in_features=400, out_features=120, bias=True)
(linear2): Linear(in_features=120, out_features=84, bias=True)
(linear3): Linear(in_features=84, out_features=10, bias=True)
)
3.4 LeNet5 模型训练
PyTorch 模型训练的基本步骤是:
- 前馈计算模型的输出值;
- 计算损失函数值;
- 计算权重 weight 和偏差 bias 的梯度;
- 根据梯度值调整模型参数;
- 将梯度重置为 0(用于下一循环)。
在模型训练过程中,可以使用验证集数据评价训练过程中的模型精度,以便控制训练过程。模型验证就是用验证数据进行模型推理,前向计算得到模型输出,但不反向计算模型误差,因此需要设置 torch.no_grad()。
使用 PyTorch 进行模型训练的例程如下。
# (4) 训练 LeNet-5 网络模型
epoch_list = [] # 记录训练轮次
loss_list = [] # 记录训练集的损失值
accu_list = [] # 记录验证集的准确率
num_epochs = 50 # 训练轮次
for epoch in range(num_epochs): # 训练轮次 epoch
running_loss = 0.0 # 每个 epoch 的累加损失值清零
for step, data in enumerate(train_loader, start=0): # 迭代器加载数据
inputs, labels = data # inputs: [batch, 1, 28, 28] labels: [batch]
optimizer.zero_grad() # 损失梯度清零
outputs = model(inputs) # 前向传播, [batch, 10]
loss = criterion(outputs, labels) # 计算损失函数
loss.backward() # 反向传播
optimizer.step() # 参数更新
# 累加训练损失值
running_loss += loss.item()
if step%100==99: # 每 100 个 step 打印一次训练信息
print("epoch {}, step {}: loss = {:.4f}".format(epoch, step, loss.item()))
# 计算验证集的预测准确率
with torch.no_grad(): # 验证过程, 不计算损失函数梯度
outputs_valid = model(valid_images) # 对验证集进行模型推理 [batch, 10]
# loss_valid = criterion(outputs_valid, valid_labels) # 计算验证集损失函数
pred_labels = torch.max(outputs_valid, dim=1)[1] # 模型预测的类别 [batch]
accuracy = torch.eq(pred_labels, valid_labels).sum().item() / valid_size * 100 # 计算准确率
print("Epoch {}: train loss={:.4f}, accuracy={:.2f}%".format(epoch, running_loss, accuracy))
# 记录训练过程的统计数据
epoch_list.append(epoch) # 记录迭代次数
loss_list.append(running_loss) # 记录训练集上的损失函数
accu_list.append(accuracy) # 记录验证集上的损失函数值
程序运行结果如下:
Epoch 0: train loss=1334.7479, accuracy=85.50%
Epoch 1: train loss=318.6170, accuracy=91.30%
Epoch 2: train loss=219.7319, accuracy=94.20%
Epoch 3: train loss=168.5048, accuracy=95.50%
…Epoch 47: train loss=10.6612, accuracy=98.80%
Epoch 48: train loss=10.9356, accuracy=98.70%
Epoch 49: train loss=9.7699, accuracy=98.70%
经过 10 轮训练,使用验证集中的 1000 张图片进行验证,模型准确率就接近 98%。继续训练,可以进一步降低训练损失函数值,但验证集的准确率保持在 98~99%。从下图的训练集损失函数和验证集准确率曲线可以看出,对训练集进行 20 轮左右的训练,就可以得到比较满意的模型参数。
3.5 LeNet5 模型保存与加载
模型训练好以后,将模型保存起来,以便下次使用。PyTorch 中模型保存主要有两种方式,一是保存模型权值,二是保存整个模型。本例使用 model.state_dict() 方法以字典形式返回模型权值,torch.save() 方法将权值字典序列化到磁盘,将模型保存为 .pth 文件。
# (5) 保存 LeNet5 网络模型
model_path = "../models/LeNet_MNIST2.pth"
torch.save(model.state_dict(), model_path)
使用训练好的模型,首先要实例化模型类,然后调用 load_state_dict() 方法加载模型的权值参数。
# 以下模型加载和模型推理,可以是另一个独立的程序
# (6) 加载 LeNet5 网络模型进行推理
# 加载 LeNet 预训练模型
model_new = LeNet5() # 实例化 LeNet-5 网络模型
model_path = "../models/LeNet_MNIST1.pth"
model_new.load_state_dict(torch.load(model_path))
model_new.eval() # 模型推理模式
需要特别注意的是:
(1)PyTorch 中的 .pth 文件只保存了模型的权值参数,而没有模型的结构信息,因此必须先实例化模型对象,再加载模型参数。
(2)模型对象必须与模型参数严格对应,才能正常使用。注意即使都是 LeNet5 模型,模型类的具体定义也可能有细微的区别。如果从一个来源获取模型类的定义,从另一个来源获取模型参数文件,就很容易造成模型结构与参数不能匹配。
(3)无论从 PyTorch 模型仓库加载的模型和参数,或从其它来源获取的预训练模型,或自己训练得到的模型,模型加载的方法都是相同的,也都要注意模型结构与参数的匹配问题。
3.6 模型推理
使用加载的 LeNet5 模型,输入新的图片进行模型推理,可以由模型输出结果确定输入图片所属的类别。
使用测试集数据进行模型推理,可以计算得到测试模型的准确率。注意模型验证集与模型检验集不能交叉使用,但为了简化例程在本程序中未做区分。
# 模型检测
model_new.eval() # 模型推理模式
correct = 0
total = 0
for data in test_loader: # 迭代器加载测试数据集
inputs, labels = data
outputs = model_new(inputs)
labels_pred = torch.max(outputs, dim=1)[1] # 模型预测的类别 [batch]
# _, labels_pred = torch.max(outputs.data, 1)
total += labels.size(0)
correct += torch.eq(labels_pred, labels).sum().item()
accuracy = 100. * correct / total
print("Test accuracy={:.2f}%".format(accuracy))
使用测试集进行模型推理,测试模型准确率为 98.75%。
Test accuracy=98.75%
从测试集中选取几张图片,或者读取新的手写数字图片(注意格式转换和图片大小),输入图片进行模型推理,也可以识别输入图片中的数字。
# (7) 模型推理识别手写数字
# imgs, labels = next(iter(test_loader)) # 用 next 返回一个批次的数据
# print(imgs.shape, labels.shape) # torch.Size([64, 1, 28, 28])
plt.figure(figsize=(8, 5))
plt.suptitle("Inferring using LeNet-5 Model")
for i, img in enumerate(imgs[:10]):
out = model_new(imgs[i].unsqueeze(0)) # 增加维度,[1, 1, 28, 28]
pred = torch.max(out, dim=1)[1] # 模型预测的类别 torch.Size([1])
plt.subplot(2, 5, i+1)
imgNP = img.squeeze().numpy() # 删除维度,转换为 numpy 数组
plt.imshow(imgNP, cmap='gray') # 绘制第 i 张图片
plt.title("{:d}".format(pred.item()))
plt.axis('off')
plt.tight_layout()
plt.show()
手写数字识别的结果如下。
4. LeNet5 模型进行MNIST手写数字识别的完整例程
本文的完整例程如下。
# Beginner_LeNet_MNIST_1.py
# LeNet-5 model for beginner with PyTorch
# 经典模型: LeNet 模型 MNIST 手写数字识别
# Copyright: youcans@qq.com
# Crated: Huang Shan, 2023/05/12
# _*_coding:utf-8_*_
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from matplotlib import pyplot as plt
# 定义 LeNet5 网络结构
class LeNet5(nn.Module):
def __init__(self):
super(LeNet5, self).__init__()
self.conv1 = nn.Conv2d(1, 6, 5, padding='same') # C1: 输入 1,输出 6,卷积核 5x5
self.pool1 = nn.AvgPool2d(2, 2) # S2: 卷积核 2x2,步长 2
self.conv2 = nn.Conv2d(6, 16, 5) # C3: 输入 6,输出 16,卷积核 5x5
self.pool2 = nn.AvgPool2d(2, 2) # S4: 卷积核 2x2,步长 2
self.flatten = nn.Flatten() # 展平为一维
self.linear1 = nn.Linear(400, 120) # C5: 输入 400,输出 120
self.linear2 = nn.Linear(120, 84) # F6: 输入 120,输出 84
self.linear3 = nn.Linear(84, 10) # F7: 输入 84,输出 10
def forward(self, x):
x = F.relu(self.conv1(x)) # (1,28,28) -> (6,28,28)
x = self.pool1(x) # (1,28,28) -> (6,14,14)
x = F.relu(self.conv2(x)) # (6,14,14) -> (16,10,10)
x = self.pool2(x) # (16,10,10) -> (16,5,5)
x = self.flatten(x) # (16,5,5) -> (400)
x = F.relu(self.linear1(x)) # (400) -> (120)
x = F.relu(self.linear2(x)) # (120) -> (84)
x = self.linear3(x) # (84) -> (10)
return x
if __name__ == '__main__':
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
# (1) 将[0,1]的 PILImage 转换为[-1,1]的Tensor
transform = transforms.Compose([ # Transform Compose of the image
transforms.ToTensor(), # 将图像转换为张量 Tensor
transforms.Normalize(mean=(0.1307,), std=(0.3081,))]) # 标准化
# (2) 加载 MNIST 数据集
batch_size = 64
# 加载 MNIST 数据集, 如果 root 路径加载失败, 则自动在线下载
# 加载 MNIST 训练数据集, 50000张训练图片
train_set = torchvision.datasets.MNIST(root='../dataset', train=True,
download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size,
shuffle=True, num_workers=2)
# 加载 MNIST 验证数据集, 10000张验证图片
test_set = torchvision.datasets.MNIST(root='../dataset', train=False,
download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=1000,
shuffle=False, num_workers=2)
# 创建生成器,用 next 获取一个批次的数据
valid_data_iter = iter(test_loader) # _SingleProcessDataLoaderIter 对象
valid_images, valid_labels = next(valid_data_iter) # val_image: [batch, 1, 28, 28] val_label: [batch]
valid_size = valid_labels.size(0) # 验证数据集大小,1000
print(valid_images.shape, valid_labels.shape) # torch.Size([1000, 1, 28, 28]) torch.Size([1000]
# (3) 实例化 LeNet-5 网络模型
model = LeNet5() # 实例化 LeNet-5 网络模型
print(model)
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9) # SGD 随机梯度下降优化器
# (4) 训练 LeNet-5 网络模型
epoch_list = [] # 记录训练轮次
loss_list = [] # 记录训练集的损失值
accu_list = [] # 记录验证集的准确率
num_epochs = 50 # 训练轮次
for epoch in range(num_epochs): # 训练轮次 epoch
running_loss = 0.0 # 每个 epoch 的累加损失值清零
for step, data in enumerate(train_loader, start=0): # 迭代器加载数据
inputs, labels = data # inputs: [batch, 1, 28, 28] labels: [batch]
optimizer.zero_grad() # 损失梯度清零
outputs = model(inputs) # 前向传播, [batch, 10]
loss = criterion(outputs, labels) # 计算损失函数
loss.backward() # 反向传播
optimizer.step() # 参数更新
# 累加训练损失值
running_loss += loss.item()
# if step%100==99: # 每 100 个 step 打印一次训练信息
# print("epoch {}, step {}: loss = {:.4f}".format(epoch, step, loss.item()))
# 计算验证集的预测准确率
with torch.no_grad(): # 验证过程, 不计算损失函数梯度
outputs_valid = model(valid_images) # 对验证集进行模型推理 [batch, 10]
# loss_valid = criterion(outputs_valid, valid_labels) # 计算验证集损失函数
pred_labels = torch.max(outputs_valid, dim=1)[1] # 模型预测的类别 [batch]
accuracy = torch.eq(pred_labels, valid_labels).sum().item() / valid_size * 100 # 计算准确率
# 记录训练过程的统计数据
epoch_list.append(epoch) # 记录迭代次数
loss_list.append(running_loss) # 记录训练集上的损失函数
accu_list.append(accuracy) # 记录验证集上的损失函数值
print("Epoch {}: train loss={:.4f}, accuracy={:.2f}%".format(epoch, running_loss, accuracy))
# 训练结果可视化
plt.figure(figsize=(11, 5))
plt.suptitle("LeNet-5 Model in MNIST")
plt.subplot(121), plt.title("Train loss")
plt.plot(epoch_list, loss_list)
plt.xlabel('epoch'), plt.ylabel('loss')
plt.subplot(122), plt.title("Valid accuracy")
plt.plot(epoch_list, accu_list)
plt.xlabel('epoch'), plt.ylabel('accuracy')
plt.show()
# (5) 保存 LeNet5 网络模型
model_path = "../models/LeNet_MNIST1.pth"
torch.save(model.state_dict(), model_path) # 保存模型权值
# # 以下模型加载和模型推理,可以是另一个独立的程序
# # (6) 加载 LeNet5 网络模型进行推理
# # 加载 LeNet 预训练模型
# model_new = LeNet5() # 实例化 LeNet-5 网络模型
# model_path = "../models/LeNet_MNIST1.pth"
# model_new.load_state_dict(torch.load(model_path))
# model_new.eval() # 模型推理模式
#
# # 模型推理
# correct = 0
# total = 0
# for data in test_loader: # 迭代器加载测试数据集
# inputs, labels = data
# outputs = model_new(inputs)
# labels_pred = torch.max(outputs, dim=1)[1] # 模型预测的类别 [batch]
# # _, labels_pred = torch.max(outputs.data, 1)
# total += labels.size(0)
# correct += torch.eq(labels_pred, labels).sum().item()
# accuracy = 100. * correct / total
# print("Test accuracy={:.2f}%".format(accuracy))
【本节完】文章来源:https://www.toymoban.com/news/detail-463782.html
参考文献:
- Yann LeCun, Gradient-based learning applied to document recognition, 1998
- https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html
【本节完】
版权声明:
欢迎关注『youcans动手学模型』系列
转发请注明原文链接:
【youcans动手学模型】LeNet 模型 MNIST 手写数字识别
Copyright 2023 youcans, XUPT
Crated:2023-05-16文章来源地址https://www.toymoban.com/news/detail-463782.html
到了这里,关于【youcans动手学模型】LeNet 模型 MNIST 手写数字识别的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!