简单神经网络实现手写数字识别

简单神经网络实现手写数字识别

突然发现已经来实验室半年了还没有写过 Pytorch 代码,觉得既然是做深度学习还是有必要学一下的吧。那么今天下定决心自己(照着教程)写一个手写数字识别的任务。

0. 环境搭建

0.1 python 环境准备

大多数人使用的是 conda 管理环境,但是这次我使用的是 PyCharm+venv+Jupyter 的组合。具体操作如下:

  1. PyCharm 中点击:新建项目
  2. 左侧选择:纯 Python
  3. 名称:MNIST(其实自己可以随便起的)
  4. 位置:自己选择
  5. Interpreter type:选择 Project venv
  6. Python 版本:如果你写过很多项目而且使用过 conda 的话,那么大概率会有乱七八糟的很多 Python 版本。但是最好选择本机的纯净版 Python 解释器,自己看路径就行了。新手就用默认的就可以。

点击创建项目,左侧的文件列表会出现一个 .venv 文件夹,这是独属于这个项目的环境。而你打开 PyCharm 集成控制台的时候会很贴心地自动激活。

然后在 MNIST 目录下创建一个 net.ipynb 文件,之后会警告你未安装 Jupyter,点击安装就好了。

0.1 pytorch

pytorch 是我们本次使用的深度学习框架。打开集成控制台,应该会出现:

1
(.venv) ...\MNIST>

前面的 (.venv) 代表你之前创建的虚拟环境,然后这里直接使用清华源安装 pytorch 的 CPU 版本(你怎么知道我是核显战士……):

1
pip install torch torchvision torchaudio -i https://pypi.tuna.tsinghua.edu.cn/simple

随后出现激动人心的:

1
Successfully installed filelock-3.13.1 fsspec-2023.12.2 mpmath-1.3.0 networkx-3.2.1 numpy-1.26.3 pillow-10.2.0 sympy-1.12 torch-2.1.2 torchaudio-2.1.2 torchvision-0.16.2

代表你安装成功了。

matplotlib

这是一个科研绘图工具,我们将使用它来展示数据集中的手写图片。安装命令:

1
pip install matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple

出现以下信息表示安装成功:

1
Successfully installed contourpy-1.2.0 cycler-0.12.1 fonttools-4.47.0 importlib-resources-6.1.1 kiwisolver-1.4.5 matplotlib-3.8.2 pyparsing-3.1.1

tqdm

进度条工具。安装命令:

1
pip install tqdm -i https://pypi.tuna.tsinghua.edu.cn/simple

出现以下信息表示安装成功:

1
Successfully installed tqdm-4.66.1

1. 简单神经网络的介绍

首先我们将搭建一个由四个全连接层组成的神经网络。在一个全连接层(也称为线性层,Linear)中,每个神经元与上一层的所有神经元相连接。全连接层的参数包括权重(weights)和偏置(biases)。以下是一个全连接层的参数说明:

  1. 权重(Weights):每个输入神经元到每个输出神经元都有一个权重。如果全连接层有 m 个输入神经元和 n 个输出神经元,那么权重矩阵的大小为 (n, m)。权重决定了输入信号在传递过程中的权重。
  2. 偏置(Biases):每个输出神经元都有一个偏置项。如果全连接层有 n 个输出神经元,那么偏置向量的大小为 (n, 1)。偏置的作用是为每个神经元引入一个可学习的偏移,使网络更灵活地适应不同的数据。

在数学表达中,给定输入向量 \(x\) 和全连接层的权重矩阵 \(W\) 以及偏置向量 \(b\),全连接层的输出(未经激活函数处理)可以表示为:

\[ Output = W \cdot x+b \]

在神经网络的训练过程中,这些权重和偏置是通过反向传播算法和优化器来学习的,以最小化模型在训练数据上的损失。

2. 代码实现

2.1 工具库的引入

1
2
3
4
5
6
import torch
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision.datasets import MNIST
from tqdm import tqdm
from matplotlib import pyplot as plt

2.2 数据集加载

本次我们使用的数据集是手写数字识别数据集 MNIST,其主要特征如下:

  • 图像大小:所有图像都是灰度图,大小为 28x28 像素。
  • 手写数字:数据集包含 0 到 9 的手写数字,总共 10 个类别。
  • 训练集和测试集:MNIST 数据集通常被分成两部分,训练集和测试集。训练集包含 60,000 张图像,而测试集包含 10,000 张图像。
  • 标签:每个图像都有一个相应的标签,表示图像中手写数字的真实值。标签是 0 到 9 之间的整数。

我们定义一个函数 get_dataloader 来获取数据集:

1
2
3
4
5
6
7
8
9
10
11
12
def get_dataloader(split, batch_size):
"""
创建 PyTorch 中的 DataLoader 对象,用于加载 MNIST 数据集的训练集或测试集。
:param split: 'train': 训练集 / 'test': 测试集
:param batch_size: 表示每个批次的样本数量
:return: DataLoader 对象,用于在训练或测试时加载数据
"""
assert split in ('train', 'test') # 使用 assert(断言)split 的取值仅限于 train 或 test
is_train = split == 'train'
to_tensor = transforms.Compose([transforms.ToTensor()]) # 创建一个 Compose 对象,其中包含一个转换操作
dataset = MNIST("./data", is_train, transform=to_tensor, download=True) # 下载数据集,保存在 "./data"
return DataLoader(dataset, batch_size=batch_size, shuffle=True) # 返回 DataLoader 使用设定好的 batch_size,数据打乱

2.3 神经网络的结构

我们将定义一个简单的神经网络模型,该模型包含四个全连接层,并且在每个全连接层后都使用了 ReLU 激活函数,最后使用 softmax 激活函数将输出映射为概率分布。(所以概率最高的那个就是神经网络预测的数字)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Net(torch.nn.Module):

def __init__(self):
super().__init__()
self.fc1 = torch.nn.Linear(28*28, 256) # 输入大小为 28x28(MNIST图像大小),输出大小为 256
self.fc2 = torch.nn.Linear(256, 128) # 输入大小为 256,输出大小为 128
self.fc3 = torch.nn.Linear(128, 64) # 输入大小为 128,输出大小为 64
self.fc4 = torch.nn.Linear(64, 10) # 输入大小为 64,输出大小为 10(代表10个类别)

def forward(self, x):
"""前向传播函数"""

# 对四个全连接层均应用 ReLU 激活函数
x = torch.nn.functional.relu(self.fc1(x))
x = torch.nn.functional.relu(self.fc2(x))
x = torch.nn.functional.relu(self.fc3(x))
x = torch.nn.functional.relu(self.fc4(x))

# 对最终输出应用 softmax 激活函数,将网络的输出转换为一个概率分布
# dim=1 表示对每一行进行 softmax 操作,确保输出的每一行都表示一个样本的类别概率分布
return torch.nn.functional.softmax(x, dim=1)

2.4 模型测试和评估

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def evaluate(net, test_data):
total = 0
correct = 0
with torch.no_grad(): # 上下文管理器,在评估阶段,不需要计算梯度
for batch_data, batch_labels in test_data:

# 使用神经网络进行前向传播,将图像数据视图展平为一维张量,并获取模型的输出
outputs = net.forward(batch_data.view(-1, 28 * 28))

for i, output in enumerate(outputs):
if torch.argmax(output) == batch_labels[i]:
correct += 1
total += 1
return correct / total

2.5 模型的训练

1
2
3
4
5
6
7
8
9
10
11
12
13
def train(net: Net, train_data, test_data, epochs=2, lr=1e-3):
criterion = torch.nn.CrossEntropyLoss() # 定义了损失函数
optimizer = torch.optim.Adam(net.parameters(), lr=lr) # 定义了优化器

for epoch in range(epochs):
for batch_data, batch_labels in tqdm(train_data):
net.zero_grad() # 将神经网络模型的梯度清零,以避免梯度累积。
output = net(batch_data.view(-1, 28 * 28)) # 进行一次前向传播,获取模型的输出
loss = criterion(output, batch_labels) # 计算模型输出与真实标签之间的损失
optimizer.zero_grad() # 将优化器中的梯度清零,以准备进行下一次反向传播。
loss.backward() # 进行反向传播,计算模型参数的梯度
optimizer.step() # 使用优化器更新模型的参数,执行一步梯度下降算法。
print("epoch: {}, accuracy: {}".format(epoch, evaluate(net, test_data)))

这个 train 函数的目的是通过迭代训练数据集,使用反向传播和梯度下降算法,来优化神经网络模型的参数,以提高在测试数据上的性能。同时,每个epoch结束时,通过调用 evaluate 函数来计算并输出在测试数据上的准确率。

其中损失函数我们使用的是交叉熵损失(CrossEntropyLoss),这是深度学习中用于多类别分类问题的一种常用损失函数。它是一个衡量模型输出概率分布与真实标签之间差异的指标。

优化器我们使用的是 Adma 优化器,Adam 是一种常用的梯度下降优化算法,用于调整神经网络的权重以最小化损失函数。

2.6 main 函数流程

1
2
3
4
5
6
7
8
9
10
if __name__ == '__main__':
print("Loading data...")
train_data = get_dataloader('train', 64)
test_data = get_dataloader('test', 64)

print("Initialize network...")
net = Net()
print("Initial Accuracy: {}".format(evaluate(net, test_data)))

train(net, train_data, test_data, epochs=10, lr=1e-3)

3 总结

总的来说,手写数字识别是深度学习入门的经典任务,通常作为神经网络构建和理解的第一步。我们通过清晰的步骤和解释理解了构建一个神经网络所需要的各种要素。这种知识不仅适用于简单任务,如手写数字识别,还可以拓展应用到更复杂的深度学习项目和研究中。未来,可以在这个基础上尝试更加复杂的网络架构,使用卷积神经网络等先进技术,或针对更复杂的数据集应用这些原则,以解决更广泛的问题。


简单神经网络实现手写数字识别
https://onlyar.site/2024/01/09/ML-net-MINIST/
作者
Only(AR)
发布于
2024年1月9日
许可协议