【从 0 学 AI】PyTorch 搭建 MLP 多层感知机实现 MNIST 手写数字识别
一、前言
作为从 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个神经元
隐藏层的核心计算公式:
-
x:上一层传来的数据
-
w(权重):可学习参数,代表特征重要程度
-
b(偏置):微调神经元输出
每一次训练、每一次反向传播,本质就是不断更新隐藏层的权重w和偏置b,让模型越来越准。
2.1.3. 输出层(Output Layer):网络的答案出口
输出层是神经网络的最后一层,负责给出最终预测结果。
分类任务中:输出层神经元个数 = 分类类别数
本项目识别数字 0~9,共10个类别,所以输出层设置为10个神经元。
网络最后输出的10个分数(logits),分别代表当前图片是数字0、1、2...9的置信分数,分数最大的就是模型预测结果。
2.1.4. 激活函数(ReLU):赋予网络智商
这是新手最容易忽略、但最重要的知识点。
如果只使用上面的 计算,无论堆叠多少层网络,本质都是线性运算,只能画直线,无法识别手写数字这种复杂、不规则的图案。
没有激活函数,神经网络再深都没用。
激活函数的作用:引入非线性,让网络可以学习复杂特征。
本项目使用最经典的 ReLU 激活函数:
通俗理解:负数直接归零、正数保留,过滤无效特征,强化有效特征。
我们在两层隐藏层后都添加了 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 第四步:权重更新(梯度下降优化)
依托反向传播算出的梯度,结合学习率,通过梯度下降算法更新网络所有的权重和偏置参数:
通过微调参数,让下一轮的预测损失更小、准确率更高。本项目使用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核心技术,从入门到进阶!
更多推荐



所有评论(0)