从零设计一个神经网络:实现手写数字识别

前言

为了能够更好的理解神经网络,从手写数字识别这个小任务来逐层弄清楚神经网络的工作原理以及一般流程是非常合适的。

这篇文章就来手写完成一个数字识别的任务,来说明如何设计、实现并训练一个标准的前馈神经网络,以期对神经网络有一个更加具体感性的认识。

概述

具体来说,我们要设计并训练一个3层的神经网络,这个神经网络会以数字图像作为输入,经过神经网络的计算,就会识别出图像中的数字是几,从而实现数字图像的分类:

在这个过程中,主要讲解三个方面:神经网络的设计和实现、训练数据的准备和处理、模型的训练和测试流程。

神经网络的设计和实现

为了设计一个处理图像数据的神经网络,需要首先明确输入的图像数据的大小和格式。

我们要处理的图片,尺寸是 28 × 28 像素的灰色通道图像(MNIST 数据集本身的格式)。

这样的灰色图像包括了 2828 = 784 个数据点,我们要先将它展平成 1784 大小的向量:

然后再将这个向量输入到神经网络中,我们会使用一个三层神经网络来处理图片对应的向量 x,输入层需要接收 784 维的图片向量 x,x 中的每个维度的数据都有一个神经元来接收,因此输入层要包含 784 个神经元:

隐藏层用于特征提取,将输入的特征向量处理为更高级的特征向量。

由于手写数字图像并不复杂,这里就将隐藏层的神经元个数设置为 256,这样输入层与隐藏层之间就会有一个 784*256 大小的线性层:

它可以将一个 784 维的输入向量转换维 256 维的输出向量,该输出向量会继续向前传播到达输出层。

由于最终要将数字图像识别为 0 到 9 十种可能的数字,因此输出层需要定义 10 个神经元来对应这十种数字:

256 维的向量在经过隐藏层和输出层之间的线性层计算后,就得到了 10 维的输出结果,这个 10 维的向量就代表了 10 个数字的预测得分:

为了继续得到 10 个数字的预测概率,我们还要将输出层的输出输入到 softmax 层,softmax 层会将 10 维的向量转换为 10 个概率值 P0 到 P9,每个概率值都对应一个数字,也就是输入图片是某一个数字的可能性,另外 P0 到 P9 这 10 个概率值相加到一起的总和是 1,这是由 softmax 函数的性质决定的:

以上就是神经网络的设计思路,接下来我们使用 PyTorch 框架来实现一下。

首先实现一下我们的神经网络:

代码如下:

python 复制代码
import torch
from torch import nn


# 定义神经网络
class NetWork(nn.Module):
    def __init__(self):
        super().__init__()
        # 线性层1,输入层和隐藏层之间的线性层
        self.layer1 = nn.Linear(784, 256)
        # 线性层2,隐藏层和输出层之间的线性层
        self.layer2 = nn.Linear(256, 10)
        # 这里没有直接定义 softmax 层
        # 这是因为后面会使用 CrossEntropyLoss 损失函数
        # 在这个损失函数中会实现 softmax 的计算

    def forward(self, x):
        x = x.view(-1, 28 * 28)  # 使用view 函数将 x 展平
        x = self.layer1(x)  # 将 x 输入至 layer1
        x = torch.relu(x)  # 使用 ReLu 函数激活
        return self.layer2(x)  # 输入至 layer2 计算结果

训练数据的准备和处理

接下来准备数据集:

对于数据集的获取,还可以使用下面这种方式从 PyTorch 官网上进行下载:

python 复制代码
# 准备数据集
train_data = torchvision.datasets.MNIST(root='data', train=True, download=True)
test_data = torchvision.datasets.MNIST(root='data', train=False, download=True)

通过这种方式下载下来的数据会自动存储在 train_data 以及 test_data 中,相对应的会有一个工具包被下载在我们在代码中所指定的目录下:

但是这并非是通用的方式,在以后的工作学习中有很多数据集需要我们自己进行操作处理并不会像上面这样方便,因此这里介绍更为通用的一种方式,也就是去官网下载原生的数据集,没错,就是一大堆图片!

我们将下载下来的数据保存为下面两个文件夹:

我们将数据分别保存到 train 和 test 两个目录中,其中 train 有 60000 个数据,test 有 10000 个数据,它们分别用来模型的训练和测试。

在 train 和 test 这两个目录中,都包括了十个子目录,子目录的名字就对应了图像中的数字。例如,在名为 3 的文件夹中就保存了数字 3 的图像:

其中图像的名称是随机的字符串签名。

完成数据的准备后,实现数据的读取功能,初学者在学习这一部分时只要知道大致的数据处理流程就可以了。

代码实现如下:

python 复制代码
# 首先实现图像的预处理pipeline
transform = torchvision.transforms.Compose([
    torchvision.transforms.Grayscale(num_output_channels=1),  # 转换为单通道灰度图像
    torchvision.transforms.ToTensor()  # 转换为PyTorch支持的张量
])

# 这是方式一准备数据集的方式嗷
train_data = torchvision.datasets.MNIST(root='data', train=True, download=True, transform=transform)
test_data = torchvision.datasets.MNIST(root='data', train=False, download=True, transform=transform)

# 下面这是方式二准备数据集的方式
# 使用 ImageFolder 函数读取数据文件夹,构建数据集 dataset
# 这个函数会将保存数据的文件夹的名字,作为数据的标签,组织数据
# 例如,对于名字为 3 的文件夹
# 就会将 3 作为文件夹中图像数据的标签,和图像配对用于后续的训练,使用起来非常方便
train_dataset = torchvision.datasets.ImageFolder(root='./mnist/train', transform=transform)
test_dataset = torchvision.datasets.ImageFolder(root='./mnist/test', transform=transform)

# 不管使用哪种准备数据集的方式,最后的效果都是一样的
# 打印它们的长度看一下
print(len(train_data))
print(len(test_data))

运行结果看一下:

可以看到训练数据和测试数据都拿到了,和我们之前说的也完全相符。

然后我们使用 train_loader 来实现小批量的数据读取:

python 复制代码
# 使用 train_loader 实现小批量的数据读取
# 这里设置小批量的大小,batch_size = 64。
# 也就是每个批次包括 64 个数据
train_loader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)
# 打印 train_loader 的长度
print(" train loader length: ", len(train_loader))
# 60000 个训练数据,如果每个小批量读入 64 个样本,那么 60000 个数据会被分为 938 组
# 计算 938 * 64 = 60032,这说明最后一组会不够 64 个数据

运行结果如下:

然后我们可以循环遍历 train_loader 来获取每个小批量数据:

python 复制代码
# 循环遍历 train_loader
# 每一次循环,都会取出 64 个图像数据,作为一个小批量 batch
for batch_idx, (data, label) in enumerate(train_loader):
    if batch_idx == 3:  # 打印前三个 batch 观察
        break
    print("batch_idx: ", batch_idx)
    print("data.shape: ", data.shape)  # 数据的尺寸
    print("label: ", label.shape)  # 图像中的数字
    print(label)

这里对于上述循环语句做一个简单的解释会更清晰:

enumerate(): 这是Python的内置函数,用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,即同时得到索引和值。在这里,它被用来遍历train_loader中的每个批次(batch),其中batch_idx是当前批次的索引(从0开始),(data, label)是当前批次的数据和标签。
for batch_idx, (data, label) in enumerate(train_loader):: 这个循环语句的意思是,对于train_loader中的每一个批次,都执行循环体内的代码。在每个循环迭代中,batch_idx是当前批次的索引,(data, label)是当前批次的数据和标签。data通常是一个包含多个数据点的张量(Tensor),每个数据点都是一个样本;label是与这些数据点相对应的标签张量,用于监督学习中的目标值。

运行效果如下:

从运行结果可以看出:

1、batch_idx = 0 表示是第一批数据

2、data.shape 表示数据的尺寸为 [64, 1, 28, 28]

上述尺寸表示每批数据包括 64 个图像,每个图像有 1 个灰色通道,图像的尺寸是 28*28 。

3、label.shape 表示该批次中共有 64 个数字对应的 label 数量总数为 64 个,每个数字都有一个 label 嘛

注意区别,真实的 label 类别肯定只有 9 个,因为数字就只有 1 到 9,而这里是指 label 值的数量有 64 个。

4、tensor 数组则表示的是这 64 张数字图片各自所对应的 label 标签值

模型的训练与测试流程

有了前面的准备之后,我们就可以开始模型的训练和测试了。

下面是训练代码:

python 复制代码
# 在使用 PyTorch 训练模型时,需要创建三个对象
model = NetWork()  # 1、模型本身,它就是我们设计的神经网络
optimizer = torch.optim.Adam(model.parameters())  # 2、优化器,优化模型中的参数
criterion = nn.CrossEntropyLoss()  # 3、损失函数,分类问题使用交叉熵损失误差

# 开始训练模型
for epoch in range(10):  # 外层循环,代表了整个训练数据集的遍历次数
    # 整个训练集要循环多少轮,是10次还是20次还是100次都是有可能的
    # 内层循环使用train_loader 进行小批量的数据读取
    for batch_idx, (data, label) in enumerate(train_loader):
        # 内层每循环一次,就会进行一次梯度下降算法
        # 包括五个步骤:
        output = model(data)  # 1、计算神经网络的前向传播结果
        loss = criterion(output, label)  # 2、计算 output 和标签 label 之间的误差损失 loss
        loss.backward()  # 3、使用 backward 计算梯度
        optimizer.step()  # 4、使用 optimizer.step 更新参数
        optimizer.zero_grad()  # 5、将梯度清零
        # 这五个步骤是使用 PyTorch 框架训练模型的定式,初学的时候记住就可以了
        # 每迭代 100 个小批量,就打印一次模型的损失,观察训练的过程
        if batch_idx % 100 == 0:
            print(f"Epoch {epoch + 1} / 10 "
                  f"| Batch {batch_idx} / {len(train_loader)}"
                  f"| Loss: {loss.item():.4f}")

torch.save(model.state_dict(), 'mnist.pth')  # 最后保存训练好的模型

运行效果如下:

中间省略...

可以看到最后的损失值已经非常非常小了,为 0.0239 。

最后是测试,测试的流程和训练基本差不多,代码如下:

python 复制代码
model = NetWork()  # 定义神经网络模型
model.load_state_dict(torch.load('mnist.pth'))  # 加载刚刚训练好的模型文件

right = 0  # 保存正确识别的数量
for i, (x, y) in enumerate(test_data):
    output = model(x)  # 将其中的数据 x 输入到模型中
    predict = output.argmax(1).item()  # 选择概率最大的标签作为预测结果
    # 对比预测值 predict 和真实标签 y
    if predict == y:
        right += 1
    else:
        # 将识别错误的样例打印出来
        print(f"wrong case: predict = {predict}, but y = {y}")

# 计算出测试结果
sample_num = len(test_data)
accuracy = right * 1.0 / sample_num
print("test accuracy = %d / %d = %.3lf" % (right, sample_num, accuracy))

运行结果如下:

可以看到测试的准确率为 98%,还是很高的。

以上就是从零设计并训练神经网络的过程。

总结与代码封装

上面都是按照各个功能部分进行讲解和描述的,可能会有点混乱,我们将上面的代码封装如下。

训练代码

python 复制代码
import torch
import torchvision.datasets
from torch import nn


# ----------------1、定义神经网络-------------------
class NetWork(nn.Module):
    def __init__(self):
        super().__init__()
        # 线性层1,输入层和隐藏层之间的线性层
        self.layer1 = nn.Linear(784, 256)
        # 线性层2,隐藏层和输出层之间的线性层
        self.layer2 = nn.Linear(256, 10)
        # 这里没有直接定义 softmax 层
        # 这是因为后面会使用 CrossEntropyLoss 损失函数
        # 在这个损失函数中会实现 softmax 的计算

    def forward(self, x):
        x = x.view(-1, 28 * 28)  # 使用view 函数将 x 展平
        x = self.layer1(x)  # 将 x 输入至 layer1
        x = torch.relu(x)  # 使用 ReLu 函数激活
        return self.layer2(x)  # 输入至 layer2 计算结果


# ----------------2、图像预处理-------------------
# 实现图像的预处理的pipeline
transform = torchvision.transforms.Compose([
    torchvision.transforms.Grayscale(num_output_channels=1),  # 转换为单通道灰度图像
    torchvision.transforms.ToTensor()  # 转换为PyTorch支持的张量
])

# ----------------3、数据集准备-------------------
# 准备训练数据集
train_data = torchvision.datasets.MNIST(root='data', train=True, download=True, transform=transform)

# ----------------4、数据集加载-------------------
# 使用 train_loader 实现小批量的数据读取
# 这里设置小批量的大小,batch_size = 64。
# 也就是每个批次包括 64 个数据
train_loader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)

# ----------------5、训练神经网络-------------------
# 在使用 PyTorch 训练模型时,需要创建三个对象
model = NetWork()  # 1、模型本身,它就是我们设计的神经网络
optimizer = torch.optim.Adam(model.parameters())  # 2、优化器,优化模型中的参数
criterion = nn.CrossEntropyLoss()  # 3、损失函数,分类问题使用交叉熵损失误差

# 开始训练模型
for epoch in range(10):  # 外层循环,代表了整个训练数据集的遍历次数
    # 整个训练集要循环多少轮,是10次还是20次还是100次都是有可能的
    # 内层循环使用train_loader 进行小批量的数据读取
    for batch_idx, (data, label) in enumerate(train_loader):
        # 内层每循环一次,就会进行一次梯度下降算法
        # 包括五个步骤:
        output = model(data)  # 1、计算神经网络的前向传播结果
        loss = criterion(output, label)  # 2、计算 output 和标签 label 之间的误差损失 loss
        loss.backward()  # 3、使用 backward 计算梯度
        optimizer.step()  # 4、使用 optimizer.step 更新参数
        optimizer.zero_grad()  # 5、将梯度清零
        # 这五个步骤是使用 PyTorch 框架训练模型的定式,初学的时候记住就可以了
        # 每迭代 100 个小批量,就打印一次模型的损失,观察训练的过程
        if batch_idx % 100 == 0:
            print(f"Epoch {epoch + 1} / 10 "
                  f"| Batch {batch_idx} / {len(train_loader)}"
                  f"| Loss: {loss.item():.4f}")

torch.save(model.state_dict(), 'mnist.pth')  # 最后保存训练好的模型

测试代码

python 复制代码
import torch
import torchvision.datasets
from torch import nn


# ----------------1、定义神经网络-------------------
class NetWork(nn.Module):
    def __init__(self):
        super().__init__()
        # 线性层1,输入层和隐藏层之间的线性层
        self.layer1 = nn.Linear(784, 256)
        # 线性层2,隐藏层和输出层之间的线性层
        self.layer2 = nn.Linear(256, 10)
        # 这里没有直接定义 softmax 层
        # 这是因为后面会使用 CrossEntropyLoss 损失函数
        # 在这个损失函数中会实现 softmax 的计算

    def forward(self, x):
        x = x.view(-1, 28 * 28)  # 使用view 函数将 x 展平
        x = self.layer1(x)  # 将 x 输入至 layer1
        x = torch.relu(x)  # 使用 ReLu 函数激活
        return self.layer2(x)  # 输入至 layer2 计算结果


# ----------------2、图像预处理-------------------
# 实现图像的预处理的pipeline
transform = torchvision.transforms.Compose([
    torchvision.transforms.Grayscale(num_output_channels=1),  # 转换为单通道灰度图像
    torchvision.transforms.ToTensor()  # 转换为PyTorch支持的张量
])

# ----------------3、数据集准备-------------------
# 准备测试数据集
test_data = torchvision.datasets.MNIST(root='data', train=False, download=True, transform=transform)

# ----------------4、测试神经网络-------------------
model = NetWork()  # 定义神经网络模型
model.load_state_dict(torch.load('mnist.pth'))  # 加载刚刚训练好的模型文件

right = 0  # 保存正确识别的数量
for i, (x, y) in enumerate(test_data):
    output = model(x)  # 将其中的数据 x 输入到模型中
    predict = output.argmax(1).item()  # 选择概率最大的标签作为预测结果
    # 对比预测值 predict 和真实标签 y
    if predict == y:
        right += 1
    else:
        # 将识别错误的样例打印出来
        print(f"wrong case: predict = {predict}, but y = {y}")

# 计算出测试结果
sample_num = len(test_data)
accuracy = right * 1.0 / sample_num
print("test accuracy = %d / %d = %.3lf" % (right, sample_num, accuracy))
相关推荐
NAGNIP5 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab6 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab6 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP10 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年10 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼10 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS11 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区12 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈12 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang12 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx