一、前言

作为从 0 开始学习 AI 的新手,手写数字识别 MNIST 可以说是深度学习的 Hello‑World。本文将使用 PyTorch 搭建一个多层感知机 MLP完成手写数字识别,不仅给出完整可运行代码,同时把前向传播、反向传播、梯度下降的训练流程讲清楚。

不同于只在数据集上测试,最后还实现了手机实拍手写图片推理,自己手写数字图片也可以识别出来。

二、项目原理和介绍

2.1 MLP多层感知机是什么

MLP(Multi-Layer Perceptron)全称多层感知机,也被大家称为全连接神经网络。它是深度学习最基础、最入门的网络模型。

简单一句话:MLP就是由「输入层、若干隐藏层、输出层」堆叠而成,所有神经元上下层全部互相连接的神经网络。

本节我单独拆开讲解四个核心概念:输入层、隐藏层、输出层、激活函数,最后结合本次手写数字识别项目完整串联。

2.1.1. 输入层(Input Layer):网络的入口

输入层的作用非常简单:接收原始数据,不做任何运算,只负责传递信息

在我们的 MNIST 手写数字项目中:

图片是 28×28 的灰度图,总像素点 = 28 × 28 = 784

但 MLP 无法识别二维图片,只能识别一维数字,所以我们需要把图片展平。

展平后得到 784 个像素值,这 784 个数值就构成了输入层的784个神经元

输入层特点:只传入数据、不参与学习、没有权重参数。

2.1.2. 隐藏层(Hidden Layer):网络的核心学习区

输入层和输出层中间的所有层,统称为隐藏层

之所以叫“隐藏”,是因为我们肉眼看不到它的输出,它只在网络内部做特征提取、数据变换、学习规律。

整个神经网络的学习能力,全部来自隐藏层,并且隐藏层的数量不仅限于一层。

在本次项目中,我搭建了两层隐藏层:

  • 第一层隐藏层:256个神经元

  • 第二层隐藏层:128个神经元

隐藏层的核心计算公式:

z = w \cdot x + b

  • x:上一层传来的数据

  • w(权重):可学习参数,代表特征重要程度

  • b(偏置):微调神经元输出

每一次训练、每一次反向传播,本质就是不断更新隐藏层的权重w和偏置b,让模型越来越准。

2.1.3. 输出层(Output Layer):网络的答案出口

输出层是神经网络的最后一层,负责给出最终预测结果

分类任务中:输出层神经元个数 = 分类类别数

本项目识别数字 0~9,共10个类别,所以输出层设置为10个神经元

网络最后输出的10个分数(logits),分别代表当前图片是数字0、1、2...9的置信分数,分数最大的就是模型预测结果。

2.1.4. 激活函数(ReLU):赋予网络智商

这是新手最容易忽略、但最重要的知识点。

如果只使用上面的 z=wx+b 计算,无论堆叠多少层网络,本质都是线性运算,只能画直线,无法识别手写数字这种复杂、不规则的图案。

没有激活函数,神经网络再深都没用。

激活函数的作用:引入非线性,让网络可以学习复杂特征

本项目使用最经典的 ReLU 激活函数

\mathrm{ReLU}(z) = \max(0,z)

通俗理解:负数直接归零、正数保留,过滤无效特征,强化有效特征

我们在两层隐藏层后都添加了 ReLU,让网络拥有拟合手写数字特征的能力。

2.1.5. 结合本次项目:完整 MLP 网络结构串联

看完上面概念,我们把本次手写数字识别的 MLP 完整结构串起来:

输入层(784) → 全连接+ReLU → 隐藏层(256) → 全连接+ReLU → 隐藏层(128) → 全连接 → 输出层(10)

前向传播完整流程:

1. 28×28图片展平为784维向量,送入输入层;

2. 第一层全连接计算权重,输出256维特征,ReLU激活过滤特征;

3. 第二层全连接继续提炼特征,输出128维高级特征,再次激活;

4. 最后一层全连接映射为10个分类分数,得到预测结果。

2.1.6. MLP 为什么能训练、为什么能学习?

初始权重是随机值,模型一开始只会瞎猜。

通过「前向传播算预测 → 损失函数算误差 → 反向传播求梯度 → 梯度下降更新权重」不断迭代,隐藏层的权重参数被持续优化,模型逐渐学会区分不同手写数字的特征,最终实现高准确率识别。

2.1.7. MLP的缺陷

MLP 需要将图片强行展平为一维向量,彻底丢失了图片的二维空间信息(笔画相邻、形状结构)。

因此 MLP 对图像任务能力有限,后续我们将使用CNN卷积神经网络 保留空间特征,进一步提升识别精度。

2.2 MNIST数据集介绍

MNIST 数据集是深度学习入门最经典的公开数据集,被称为AI领域的“Hello World”,专门用于手写数字图像分类任务。数据集全部由人手书写的灰度数字图片构成,数据干净、场景简单、适配新手入门训练神经网络模型。

2.2.1 数据集基本构成

MNIST 数据集整体分为训练集测试集两部分,数据完全独立,无交叉重叠,符合深度学习标准训练规范:

  • 训练集(Train Set):包含60000张手写数字图片,用于模型迭代训练、学习数字特征、更新网络权重参数。

  • 测试集(Test Set):包含10000张全新的手写数字图片,模型训练全程不会接触该部分数据,用于最终评估模型的泛化能力和识别准确率。

2.2.2 图像数据规格

数据集内所有图片规格统一,无尺寸差异,极大降低了模型训练的预处理难度:

  • 图像尺寸:统一 28×28 像素

  • 图像类型:单通道灰度图,无彩色信息

  • 像素范围:原始像素值 0~255,0代表纯黑、255代表纯白

  • 识别类别:包含 0~9 共10种手写数字类别,属于标准的十分类任务

在本项目中,我们通过 transforms.ToTensor() 对图像进行预处理,将原始0~255的像素值归一化到 0~1 区间,同时将图像数据转换为PyTorch适配的张量格式,适配MLP模型输入。

2.2.3 数据集特点与训练意义

1. 数据简单规整:图片尺寸统一、场景单一、无复杂背景,模型无需处理冗余噪声,新手可以专注理解神经网络的训练逻辑。

2. 数据量充足:6万训练样本足以支撑小型MLP模型收敛,能够充分拟合手写数字的轮廓、笔画特征。

3. 适配模型验证:可以快速验证模型结构、前向传播、反向传播、参数更新的整套训练闭环是否正常工作。

2.2.4 本项目数据集加载逻辑

项目中通过PyTorch内置的torchvision.datasets 直接加载MNIST数据集,无需手动下载、整理数据,核心逻辑如下:

1. 定义数据预处理规则,统一图像格式与像素范围;

2. 分别加载训练集与测试集,自动下载缺失数据;

3. 通过 DataLoader 批量打包数据、打乱训练集顺序,分批次送入MLP模型训练,提升训练效率与模型泛化性。

2.3 神经网络核心训练四步闭环

弄懂了MLP网络结构和MNIST数据集后,我们需要掌握神经网络真正的学习核心。无论是本文的MLP全连接网络,还是后续的CNN、Transformer,所有深度学习模型的训练,都遵循一套固定的闭环逻辑。

神经网络不会天生识别数字,它依靠迭代循环四步流程不断修正参数、降低误差,最终学会特征规律。完整训练闭环如下:

前向传播 → 计算损失 → 反向传播 → 权重更新

2.3.1 第一步:前向传播(得出预测结果)

将预处理好的MNIST手写数字图片张量输入MLP模型,数据从输入层、隐藏层逐层运算、激活,最终从输出层得到10个类别的预测分数。

这一步仅做数据正向计算,不会修改任何网络参数,目的是得到模型当前的预测值,对比真实标签。

2.3.2 第二步:计算损失(判断预测误差)

得到预测值后,通过损失函数对比「模型预测结果」和「图片真实标签」的差距,量化模型的识别误差。

本项目为十分类任务,使用交叉熵损失函数(CrossEntropyLoss)。损失值越大,代表预测偏差越离谱;损失值越小,代表模型预测越精准。

损失函数的核心作用:给模型的错误打分,为后续优化提供依据

2.3.3 第三步:反向传播(计算梯度、定位误差来源)

有了损失误差后,模型需要知道:每一个权重、偏置参数应该如何调整,才能减小误差

这一步通过链式法则反向求导,从最终损失逐层向前回溯,计算出损失对每一个参数的梯度,精准定位每一层参数对误差的影响大小。

通俗理解:前向传播是“做题”,反向传播是“批改错题、找出出错步骤”。这一步只计算梯度,不更新参数

2.3.4 第四步:权重更新(梯度下降优化)

依托反向传播算出的梯度,结合学习率,通过梯度下降算法更新网络所有的权重和偏置参数:

w_{new} = w_{old} - \eta \cdot \frac{\partial L}{\partial w}

通过微调参数,让下一轮的预测损失更小、准确率更高。本项目使用Adam优化器自动完成参数更新,高效迭代优化模型。

2.3.5 闭环迭代逻辑

以上四步执行完成,即为一轮单次训练。我们通过设置多轮epoch,不断重复「前向传播-损失计算-反向传播-权重更新」闭环。

随着迭代次数增加,模型损失持续下降,逐步学习到手写数字的笔画、轮廓特征,最终具备精准识别手写数字的能力。

核心总结:只有四步闭环完整执行,模型才具备“学习能力”;仅做前向传播只能推理预测,无法优化模型、提升精度。

三. 环境准备

本项目基于 PyTorch 实现 MLP 手写数字识别,代码简洁、轻量、对新手友好。为保证代码可以直接运行,我列出本次项目所需的全部依赖库与环境版本要求。

3.1 开发环境

  • 编程语言:Python 3.8 及以上

  • 深度学习框架:PyTorch(GPU/CPU版本均可)

  • 开发工具:PyCharm / VSCode / Jupyter Notebook 均可

3.2 所需依赖库

本项目用到深度学习、数据处理、图像操作相关库,完整依赖如下:

  • torch:核心深度学习框架,用于搭建网络、训练、反向传播、参数更新

  • torchvision:配套视觉库,用于加载MNIST数据集、图像预处理

  • numpy:数值计算库,用于数组与矩阵运算

  • opencv-python:用于后期实拍图片预处理、二值化、轮廓检测

  • Pillow:图片读取与格式转换

3.3 一键安装命令

可直接在终端执行以下命令,批量安装所有依赖:

pip install torch torchvision numpy opencv-python pillow

注:上述安装命令是在服务器上实现,如果使用本地电脑请参考其他博客配置环境

3.4 环境说明

1. 本代码支持 CPU、GPU 双模式,程序会自动检测设备,无需手动修改代码;

2. MNIST 数据集可通过代码自动下载,无需手动解压、配置路径;

3. 所有代码均为标准 PyTorch 原生写法,无冷门依赖,兼容性极强,适合新手复现学习。

四. 完整训练代码 + 逐段详解

本章将完整贴出基于PyTorch的MLP手写数字识别训练代码,同时逐模块拆解每一段代码的作用、底层逻辑,结合前文讲解的MLP原理、训练四步闭环,做到原理与代码一一对应,新手可以轻松看懂、直接复现。

4.1 导入库与设备配置

完整代码
import os
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

# 自动匹配GPU/CPU设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"当前训练设备:{device}")
逐段讲解

1. 核心库导入:导入PyTorch核心模块,nn用于搭建神经网络层,optim用于配置优化器,DataLoader用于批量加载数据,datasets、transforms用于数据集加载与图像预处理。

2. 设备自适应配置:自动检测电脑是否支持CUDA GPU加速,有GPU则使用GPU训练,大幅提升训练速度;无GPU则自动切换CPU,代码无需手动修改,兼容性极强。

4.2 数据集加载与预处理

完整代码
# 图像预处理:归一化张量
transform = transforms.Compose([
    transforms.ToTensor()
])

# 加载MNIST训练集、测试集
train_dataset = datasets.MNIST(root="./data", train=True, download=True, transform=transform)
test_dataset  = datasets.MNIST(root="./data", train=False, download=True, transform=transform)

# 批量数据加载器
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader  = DataLoader(test_dataset,  batch_size=64, shuffle=False)
逐段讲解

1. 数据预处理规则transforms.ToTensor()完成两个核心操作,一是将PIL图像/数组转为PyTorch专属张量格式,二是将0~255的像素值归一化到0~1区间,适配模型输入计算。

2. 数据集加载:分别加载MNIST训练集和测试集,root指定数据存储路径,download=True自动检测并下载缺失数据集,全程无需手动操作。

3. DataLoader数据加载器:将海量数据分批次打包,batch_size=64代表每次送入模型64张图片,避免一次性加载全部数据导致显存溢出;训练集设置shuffle=True打乱数据顺序,防止模型记忆数据顺序,提升泛化能力;测试集无需打乱,保证评估结果稳定。

4.3 MLP网络模型定义

完整代码
# 自定义MLP多层感知机模型
class MNIST_MLP(nn.Module):
    def __init__(self):
        super().__init__()
        # 定义三层全连接层
        self.fc1 = nn.Linear(28*28, 256)
        self.fc2 = nn.Linear(256, 128)
        self.fc3 = nn.Linear(128, 10)
        # 定义ReLU激活函数
        self.relu = nn.ReLU()

    # 前向传播逻辑
    def forward(self, x):
        # 展平二维图片为一维向量
        x = x.view(x.size(0), -1)
        # 逐层前向计算 + 激活
        x = self.relu(self.fc1(x))
        x = self.relu(self.fc2(x))
        out = self.fc3(x)
        return out

# 初始化模型并迁移至对应设备
model = MNIST_MLP().to(device)
逐段讲解

1. 模型继承:所有PyTorch神经网络模型必须继承nn.Module,这是模型的标准写法,支持参数管理、训练、保存、加载等全套功能。

2. 网络层定义:严格对应前文讲解的MLP结构,三层全连接层搭建「输入层-隐藏层-输出层」结构,搭配ReLU激活函数引入非线性能力。

3. 前向传播函数forward是模型核心,定义数据的流转逻辑。首先将28×28的二维图片展平为784维一维向量,再逐层完成全连接计算、激活,最终输出10维分类分数。

4. 模型初始化:创建模型实例,并将模型参数迁移至GPU/CPU设备,与数据设备保持一致。

4.4 损失函数、优化器配置

完整代码
# 定义多分类损失函数
criterion = nn.CrossEntropyLoss()
# 定义Adam优化器,学习率0.001
optimizer = optim.Adam(model.parameters(), lr=1e-3)

1. 交叉熵损失函数:适配本项目的十分类任务,自动整合Softmax归一化和损失计算,无需手动添加激活,是图像分类任务的首选损失函数,用于量化模型预测误差。

2. Adam优化器:相比于传统SGD,Adam收敛速度更快、训练更稳定,通过传入model.parameters()获取模型所有可学习的权重、偏置参数,设置学习率lr=0.001,控制参数更新步长。

4.5 训练循环详解(核心四步闭环)

完整代码
# 训练轮数
epochs = 15
for epoch in range(epochs):
    # 开启训练模式
    model.train()
    total_loss = 0
    # 遍历每一批次数据
    for imgs, labels in train_loader:
        # 数据迁移至对应设备
        imgs, labels = imgs.to(device), labels.to(device)
        
        # 1. 梯度清零(关键步骤)
        optimizer.zero_grad()
        # 2. 前向传播,获取预测结果
        logits = model(imgs)
        # 3. 计算损失
        loss = criterion(logits, labels)
        # 4. 反向传播,计算梯度
        loss.backward()
        # 5. 梯度下降,更新权重
        optimizer.step()
        
        # 累加批次损失
        total_loss += loss.item()
    
    # 打印每一轮平均损失
    print(f"Epoch {epoch+1}/{epochs}, Train Loss: {total_loss/len(train_loader):.4f}")

# 训练完成保存模型权重
torch.save(model.state_dict(), "mnist_mlp.pth")
print("模型权重已保存为:mnist_mlp.pth")
逐段讲解

1. 训练轮数设置:设置15轮完整训练,每一轮都会遍历全部60000张训练集图片,让模型充分学习数据特征。

2. 训练模式开启model.train()开启模型训练状态,启用梯度计算、参数更新功能。

3. 训练四步闭环落地

- 梯度清零:PyTorch梯度默认累加,每批次训练前必须清零,避免梯度混杂;

- 前向传播:数据输入模型,得到预测分数;

- 计算损失:对比预测值与真实标签,量化误差;

- 反向传播:自动链式求导,计算所有参数梯度;

- 权重更新:优化器根据梯度微调参数,降低损失。

4. 损失统计与打印:累加每批次损失,计算每一轮的平均损失,直观观察模型收敛趋势。

5. 模型权重保存:训练完成后保存模型所有可学习参数,生成mnist_mlp.pth权重文件,后续可直接加载推理,无需重复训练。

4.6 模型测试评估、保存权重

完整代码
# 模型测试评估
model.eval()
correct = 0
total = 0

# 关闭梯度计算,节省显存、提升速度
with torch.no_grad():
    for imgs, labels in test_loader:
        imgs, labels = imgs.to(device), labels.to(device)
        logits = model(imgs)
        # 取概率最大的类别作为预测结果
        preds = torch.argmax(logits, dim=1)
        # 统计总样本数、正确预测样本数
        total += labels.size(0)
        correct += (preds == labels).sum().item()

# 计算测试集准确率
acc = 100.0 * correct / total
print(f"Test Accuracy: {acc:.2f}%")
逐段讲解

1. 评估模式开启model.eval()关闭模型训练专属功能,固定参数,保证评估结果准确。

2. 关闭梯度计算torch.no_grad()上下文管理器,测试阶段无需计算梯度,可大幅节省显存、加快推理速度。

3. 预测与统计:通过torch.argmax筛选最优预测类别,遍历所有测试集数据,统计预测正确的样本数量。

4. 准确率计算:最终输出模型在10000张全新测试集图片上的识别准确率,衡量模型泛化能力。

五. 训练结果展示与分析

5.1 训练日志输出示例

完整训练15轮后,控制台输出结果如下(标准收敛效果):

当前训练设备:cuda
Epoch 1/15, Train Loss: 0.2876
Epoch 2/15, Train Loss: 0.1103
Epoch 3/15, Train Loss: 0.0742
Epoch 4/15, Train Loss: 0.0551
Epoch 5/15, Train Loss: 0.0404
Epoch 6/15, Train Loss: 0.0308
Epoch 7/15, Train Loss: 0.0244
Epoch 8/15, Train Loss: 0.0223
Epoch 9/15, Train Loss: 0.0173
Epoch 10/15, Train Loss: 0.0138
Epoch 11/15, Train Loss: 0.0130
Epoch 12/15, Train Loss: 0.0124
Epoch 13/15, Train Loss: 0.0115
Epoch 14/15, Train Loss: 0.0100
Epoch 15/15, Train Loss: 0.0103
模型权重已保存为:mnist_mlp.pth
Test Accuracy: 98.06%

5.2 结果分析

1. 损失收敛正常:训练损失从初始0.28+持续下降至0.01左右,模型不断拟合手写数字特征,训练效果正常。

2. 准确率达标:标准MLP模型在MNIST数据集上准确率稳定在98%左右,对于基础全连接网络,该精度属于优秀水平。

3. 存在局限性:虽然数据集测试精度较高,但由于MLP丢失图像空间信息,对实拍、模糊、变形的手写数字识别效果较差,后续CNN模型可精准解决该问题。

六. 外部实拍图片推理实战

前文训练、测试均基于官方标准数据集图片,为验证模型的实际落地能力,本节使用手机实拍手写数字图片进行推理测试,搭配OpenCV完成图像预处理,适配真实场景图片。

预测数字:8
预测置信度:99.99%
Top3 预测结果:
  数字8:99.99%
  数字2:0.00%
  数字3:0.00%

6.1 实拍图片踩坑问题总结

在真实图片推理过程中,我遇到了多个数据集测试不会出现的问题,逐一解决并总结如下:

问题1:实拍图片存在光照、阴影干扰

数据集图片无阴影、光线均匀,而手机实拍图片容易出现明暗不均,直接识别会完全出错。

解决方案:使用adaptiveThreshold自适应二值化,局部阈值处理,彻底消除光照阴影干扰。

问题2:实拍图片白底黑字,与训练集风格相反

MNIST数据集为黑底白字,实拍手写图大多为白底黑字,模型无法适配。

解决方案:使用THRESH_BINARY_INV反向二值化,自动反转像素颜色,统一为训练集风格。

问题3:数字大小、位置不规范

数据集数字居中、大小统一,实拍数字位置偏移、大小不一,识别准确率极低。

解决方案:轮廓检测提取数字主体,等比例缩放、居中放置到28×28标准画布,统一输入规格。

问题4:笔画过细、噪点干扰

手写笔画过细、背景有杂点,模型无法识别有效特征。

解决方案:形态学膨胀加粗笔画,过滤无效噪点,贴合数据集笔画特征。

七. 项目整体踩坑与问题解决汇总

本次从零搭建MLP手写数字识别项目,汇总新手高频报错、踩坑问题,帮助大家避坑:

坑点1:MNIST数据集下载超时/失败

解决:配置本地代理,或手动下载数据集放入对应路径,避免网络问题导致下载失败。

坑点2:权重加载报错

解决:推理代码的模型结构必须与训练代码完全一致,同时添加map_location=device,支持GPU训练、CPU加载,跨设备兼容。

坑点3:推理准确率极低、随机预测

解决:推理时忘记添加model.eval()torch.no_grad(),导致模型参数不稳定、梯度异常,必须严格添加。

坑点4:实拍图片识别效果远差于数据集

解决:并非模型问题,而是预处理不规范,严格按照二值化、去噪、居中、归一化流程处理图片,统一输入标准。

坑点5:训练梯度累加、损失异常波动

解决:每批次训练前必须执行optimizer.zero_grad()梯度清零,否则梯度累加,模型无法正常收敛。

八. 项目总结与后续学习规划

8.1 项目总结

本次项目作为【从0学AI】系列的第一篇实战教程,我从零搭建了MLP多层感知机,完整实现MNIST手写数字识别,熟练掌握了深度学习最核心的基础知识:

1. 吃透MLP网络核心结构:输入层、隐藏层、输出层、激活函数的底层作用与搭配逻辑;

2. 掌握神经网络万能训练四步闭环:前向传播→损失计算→反向传播→权重更新;

3. 熟练使用PyTorch完成数据集加载、模型搭建、训练、评估、权重保存与加载;

4. 实现真实场景图片预处理与推理,打通模型训练到落地的简易流程。

同时也明确了MLP模型的核心缺陷:必须展平二维图像,丢失像素空间位置信息,无法充分利用图像特征,对真实场景手写数字的泛化能力有限。

8.2 后续学习规划

为解决MLP模型的短板,下一篇博客将带来CNN卷积神经网络实战教程

1. 详解卷积、池化、Padding等CNN核心原理;

2. 从零搭建CNN模型实现手写数字识别;

3. 全方位对比MLP与CNN的识别精度、真实场景适配能力;

4. 带你理解为什么CNN是图像任务的首选模型。

持续更新【从0学AI】零基础深度学习实战系列,循序渐进吃透AI核心技术,从入门到进阶!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐