尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Paddle手写数字识别全流程:LeNet-5训练、模型导出与便携打包

Paddle手写数字识别全流程:LeNet-5训练、模型导出与便携打包 简介这是一份基于 PaddlePaddle 框架实现手写数字识别的完整项目包面向深度学习初学者和希望快速上手百度飞桨的开发者可帮助理解图像分类任务从数据预处理、CNN 模型搭建到训练评估的完整流程。压缩包共 12 个文件大小约 19.25MB核心包括 Train.py、Test.py 两个 Python 训练与测试脚本mnist.json.gz 格式的 MNIST 数据文件以及 README.md 项目说明文档此外还包含少量 xml 配置、示例图片等辅助文件便于直接运行和二次修改。已有 318 人学习下载。项目源码经过助教老师实际测试正确性和可运行性有保障下载后按 README 指引即可快速复现实验。内部目录结构清晰涵盖模型脚本、数据集、可视化图片和说明文档适合结合 CNN 中卷积、池化、Softmax 分类、优化器等知识点逐段理解是一份兼具实战价值与教学意义的基础入门资料。1. 手写数字识别从数据集到 Paddle 选型手写数字识别是很多人第一次把神经网络从训练跑到部署的起点Paddle 恰好把这两段都留好了接口用paddle.vision.datasets.MNIST一条命令拿数据用paddle.jit.to_static把动态图模型导出成静态图最后装进 zip 分发。真正麻烦的不是“识别”而是训练完之后怎么让一个没装飞桨的人也能双击跑起来。这篇内容直接围绕“基于 Paddle 的手写数字识别.zip”这种交付形态来写覆盖数据准备、LeNet-5 训练、模型导出以及便携打包。合适人群是对飞桨已有基本概念、想完整走一遍训练到部署的开发者也适合遇到打包报错后回来补课的工程人员。2. 准备 Paddle 运行环境与 MNIST 手写数字识别数据集2.1 安装 Paddle 并确认 CPU/GPU 可用性先解决环境问题。手写数字识别对算力要求不高CPU 上训练 LeNet-5 跑满 10 个 epoch 通常也就几分钟GPU 只是让迭代更舒服。安装命令按官方源走python -m pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple安装后再确认版本和设备状态python -c import paddle; print(paddle.__version__) python -c import paddle; print(paddle.device.is_compiled_with_cuda())第一段命令安装了 CPU 版本的 PaddlePaddle对 MNIST 任务足够。第二段用来验证框架是否能正常导入以及当前安装版本是否带 CUDA 编译。如果第二个输出False说明你拿到的是 CPU 推理库后续训练仍然能跑只是卷积层会慢一些。需要注意GPU 版本对应的是paddlepaddle-gpu并且版本号要和本机 CUDA 版本匹配否则运行时日志会提示找不到驱动库。2.2 下载 MNIST 数据集到本地目录Paddle 的视觉数据集模块内置了 MNIST下载逻辑会按默认路径缓存到用户目录下。也可以手动指定root方便项目打包时一并带上数据目录。加载时建议顺手做归一化这样模型收敛更快。import paddle from paddle.vision.datasets import MNIST from paddle.vision.transforms import Normalize transform Normalize(mean[0.0], std[1.0], to_rgbFalse) train_dataset MNIST(modetrain, transformtransform, downloadTrue) test_dataset MNIST(modetest, transformtransform, downloadTrue) print(len(train_dataset), len(test_dataset)) sample, label train_dataset[0] print(sample.shape, label)上面的代码将数据归一化到 0 到 1 之间因为 MNIST 原始像素值范围是 0 到 255归一化能避免输入数值过大导致训练不稳定。to_rgbFalse表示保持单通道灰度图如果漏掉这个参数部分 Paddle 版本会尝试将图像转成三通道 RGB导致Linear层输入尺寸对不上。输出结果中sample.shape应该是(28, 28)或者(1, 28, 28)两种形态都要在进入模型前统一 reshape。MNISt 数据集本身是六万张训练图和一万张测试图。如果要在本地跑起来downloadTrue会让程序自动从远端拉取数据。需要交付给内网环境时可以先把缓存目录下载好再把数据目录复制进项目包里运行时设置downloadFalse即可。2.3 用 DataLoader 组织批次输入paddle.io.DataLoader负责把数据集切成 batch同时支持打乱和并行读取。训练 LeNet-5 常用 batch size 是 128 或 256。这里按 128 配置train_loader paddle.io.DataLoader( train_dataset, batch_size128, shuffleTrue, num_workers0, drop_lastTrue ) test_loader paddle.io.DataLoader( test_dataset, batch_size256, shuffleFalse, num_workers0 )shuffleTrue让每个 epoch 的训练顺序不同避免模型记住固定顺序drop_lastTrue丢弃最后不足一个 batch 的样本防止 batch 数量不同影响 BatchNorm 层。num_workers在if __name__ __main__:保护块之外使用容易出现多进程启动报错所以在 Windows 上先设为 0等代码稳定后再调大。这里的 DataLoader 会在每次迭代时返回(image, label)其中 image 的 shape 是[batch_size, 28, 28]或[batch_size, 1, 28, 28]label 是整数数组。实际操作中最好丢一个打印进训练循环确认尺寸是真的。3. 用 Paddle 搭建并训练手写数字识别模型3.1 使用 Paddle 定义 LeNet-5输入输出尺寸经典 LeNet-5 结构很适合 MNIST输入是 28×28 灰度图。第一个卷积层用 6 个 5×5 卷积核尺寸变为 24×24池化后变成 12×12第二个卷积层用 16 个 5×5 卷积核尺寸变为 8×8池化后变成 4×4。这样每个样本的最终特征图通道数是 16空间尺寸 4×4展平后是 256 维。import paddle import paddle.nn as nn class LeNet5(nn.Layer): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2D(1, 6, kernel_size5, stride1, padding0), nn.ReLU(), nn.MaxPool2D(kernel_size2, stride2), nn.Conv2D(6, 16, kernel_size5, stride1), nn.ReLU(), nn.MaxPool2D(kernel_size2, stride2) ) self.classifier nn.Sequential( nn.Linear(256, 120), nn.ReLU(), nn.Linear(120, 84), nn.ReLU(), nn.Linear(84, num_classes) ) def forward(self, x): x self.features(x) x paddle.flatten(x, 1) return self.classifier(x)这里的输入x期望 shape 是[batch_size, 1, 28, 28]。很多新手直接把[batch_size, 28, 28]传进去会得到维度不匹配的报错。分类层最后一层输出 10 类对应数字 0 到 9。Paddle 的flatten指定了start_axis1所以不会把 batch 维展平这一点和reshape(x, [x.shape[0], -1])等价。也可以直接用paddle.vision.models.LeNet内置模型权重初始化策略已经调好。不过自己手写一遍有助于后面改结构比如调整卷积核数量、加上 Dropout 层时更清楚每一层做了什么。3.2 训练循环损失函数、优化器和参数设置训练环节最稳定的搭配是 交叉熵损失 Adam 优化器。手写数字分类是十类别任务Paddle 的SparseCategoricalCrossentropy接收整数标签省去 one-hot 转换。import paddle model LeNet5() optimizer paddle.optimizer.Adam(learning_rate0.001, parametersmodel.parameters()) criterion paddle.nn.SparseCategoricalCrossentropy() epochs 10 for epoch in range(epochs): model.train() total_loss 0.0 for images, labels in train_loader: images paddle.reshape(images, [-1, 1, 28, 28]) logits model(images) loss criterion(logits, labels) loss.backward() optimizer.step() optimizer.clear_grad() total_loss loss.item() print(fepoch {epoch 1}, avg loss {total_loss / len(train_loader):.4f})为什么每个 iter 都要调用optimizer.clear_grad()Paddle 的动态图模式会累加梯度不清理会导致参数更新方向和速度失真。paddle.reshape(images, [-1, 1, 28, 28])里-1表示由数据量自动推导后面的1, 28, 28是强行规定通道维和图像尺寸。训练 10 个 epoch 后MNIST 一般能到 99% 左右的准确率。如果 loss 不降或发散先检查学习率。下面这张表给出常见参数的经验范围超参数常用取值影响batch_size64 到 256太小收敛慢且抖动大太大跑完一个 epoch 更慢learning_rate0.001 到 0.0001太大会出现 loss 跳变太小则收敛迟缓epochs10 到 20MNIST 上 10 个 epoch 已够过强训练易过拟合optimizerAdam 或 SGD momentumAdam 适合快速验证SGD 最终精度可能更高3.3 训练时容易忽视的两类坑第一类坑是图像没有归一化。MNIST 原图是 0 到 255如果直接输入卷积网络权重要花更长时间去适应大数值输入。理想的做法是把像素缩放到 0 到 1 之间甚至用均值和标准差做标准化。Paddle 的Normalize默认只做简单除法但在某些旧版本里需要将std显式写成[1.0]否则会报除数为零的错误。第二类坑是训练时打开了model.eval()。Dropout层在eval状态会关闭BatchNorm会使用全局统计量而训练中应该使用本 batch 的统计量。LeNet-5 没有这些层时表现不明显一旦改成带 BatchNorm 的卷积结构有没有切换模式会直接影响精度。还要留意 DatLoader 返回的images是 PIL 图像还是 numpy 数组。部分 Paddle 版本的 MNIST 数据集在transformNone时返回 PIL 对象直接reshape会失败。给数据集传入transform后返回的才是numpy.ndarray这也是 2.2 节特意加Normalize的原因之一。4. 评估、导出模型与离线推理4.1 用测试集评估准确率训练结束后需要在测试集上做正式评估。测试集和训练集没有重叠能反映模型对未见数据的泛化能力。model.eval() correct 0 total 0 with paddle.no_grad(): for images, labels in test_loader: images paddle.reshape(images, [-1, 1, 28, 28]) logits model(images) preds paddle.argmax(logits, axis1) correct (preds labels).numpy().sum() total labels.shape[0] print(ftest acc: {correct / total:.4f})model.eval()的作用是切换成推理模式开关训练专用层paddle.no_grad()避免构建反向计算图节省显存和计算时间。paddle.argmax沿类别维度取最大值的下标得到的就是预测数字。这个准确率通常高于 0.99属于正常水平。如果低于 0.98优先怀疑数据预处理部分出了问题而不是网络结构不行。4.2 使用 paddle.jit 导出静态图模型训练得到的是动态图参数不方便直接用推理引擎加载。Paddle 提供了paddle.jit.to_static和paddle.jit.save把动态图模型转成静态图模型导出后得到.pdmodel和.pdiparams两个文件。这样后续加载可以脱离原始 Python 类定义。class LeNet5Infer(nn.Layer): def __init__(self): super().__init__() self.model LeNet5() def forward(self, x): logits self.model(x) return paddle.nn.functional.softmax(logits, axis1) model LeNet5Infer() model.load_dict(paddle.load(mnist_lenet.pdparams)) model.eval() model paddle.jit.to_static( model, input_spec[ paddle.static.InputSpec(shape[-1, 1, 28, 28], dtypefloat32, nameimage) ] ) paddle.jit.save(model, inference_mnist)导出时要把模型包一层带softmax的类否则输出是 logits。虽然argmax对 logits 和 softmax 结果相同但对外部署时直接拿到概率值更直观。input_spec指定输入张量的 shape-1表示 batch 维可变。换成固定1会在后续批量推理时被锁死常见做法是保留-1。导出成功后当前目录会出现inference_mnist.pdmodel和inference_mnist.pdiparams。这两个文件就是最终模型文件后续打包只依赖它们。4.3 对自定义手写图片做离线推理从项目交付角度真正有价值的部分是“喂一张图片进来得到预测数字”。这里需要处理图像预处理和模型加载。import numpy as np from PIL import Image import paddle model paddle.jit.load(inference_mnist) model.eval() def preprocess_image(path): img Image.open(path).convert(L).resize((28, 28)) arr np.array(img, dtypenp.float32) / 255.0 arr 1.0 - arr return paddle.to_tensor(arr.reshape(1, 1, 28, 28)) image_path custom_digit.png with paddle.no_grad(): out model(preprocess_image(image_path)) pred int(paddle.argmax(out, axis1).numpy()[0]) print(预测数字, pred)convert(L)把图片转成灰度图resize((28, 28))是强行缩放到模型输入尺寸。这里的缩放会拉伸图像实际手写数字长宽比并非正方形时效果可能打折。更好的做法是先做边缘检测找到数字的外接框再按比例缩放并填补到 28×28这一步可以在后面的优化阶段加上。1.0 - arr是非常关键的操作。MNIST 数据集的背景是黑色、数字是白色而普通手机拍照或扫描得到的图片往往是白底黑字。不做反转模型看到的图像语义完全反了输出置信度会混乱。如果你确认图片本身就是黑底白字则不需要这一步。5. 把 Paddle 手写数字识别项目打包成便携 zip5.1 先确定交付方式源码包、依赖包还是单文件打包前需要回答一个问题用户机器里有没有 Paddle 环境如果对方是开发者交付源码 zip要求他安装 Paddle 即可如果对方是业务人员则要把 Paddle 推理所需运行库一起打进去。常见做法有三种只交付.py源码和模型文件对方自行安装依赖。做虚拟环境目录把site-packages里的 Paddle 相关库复制出来连同启动脚本组成一个可解压运行的文件夹。用 PyInstaller 把 Python 解释器、依赖库、模型文件和入口脚本打进一个可执行文件再压成 zip。对于“基于 Paddle 的手写数字识别.zip”这种命名用户更倾向于解压即用。因此采用 PyInstaller 打包方式最接近目标。尤其处理过 PaddleOCR 便携打包版的人对 PyInstaller 的思路应该很熟悉手写数字识别项目本质上是同样流程只是少了 OCR 那部分文字检测和方向分类模型。5.2 用 PyInstaller 打包 Paddle 应用的常用参数PyInstaller 的模式分单文件-F和目录-D两种。Paddle 动态库体积大、.so文件数量多-F模式启动时把所有文件释放到临时目录速度慢且容易被杀毒软件拦截。更稳的方式是用-D生成目录再压缩成 zip 分发。pip install pyinstaller pyinstaller -D -w --name mnist_digit \ --add-data inference_mnist.pdmodel;. \ --add-data inference_mnist.pdiparams;. \ --hidden-import paddle \ --hidden-import paddle.inference \ entry.py命令说明-D生成一个文件夹而不是单个可执行文件-w在 Windows 上隐藏控制台窗口如果要从终端看输出则去掉--add-data把模型文件复制到解算后的当前目录Windows 上分隔符是分号Linux 和 macOS 是冒号。--hidden-import用于兜底哪些没有显式 import 但推理时会用到的模块。这里用entry.py作为入口里面必须包含paddle.jit.load和对图像预处理、预测的调用逻辑。PyInstaller 从入口开始静态分析 import 关系所以entry.py里要明确写出import paddle和from paddle.vision.datasets import MNIST等不能只放在函数内部。5.3 复制底层动态库并压缩成 zipPyInstaller 模块自动搜集运行时依赖但 PaddlePaddle 的底层.dll或者.so有时不能正确识别。打包完成后如果运行报libiomp5md.dll找不到或paddle_fluid.dll找不到需要手动从 Python 安装目录复制这些文件到打包输出根目录。检查 Python 安装路径可以用一段命令查看python -c import paddle; print(paddle.__file__)进入paddle库所在目录后把libs或paddle/libs下的动态库一并复制到dist/mnist_digit/下。大部分情况下Paddle 的动态库依赖会被 PyInstaller 通过打包机制收集进.pyd所在位置真正缺的是个别第三方运行时库。复制完成后用脚本或压缩软件把整个目录压成 zip。压缩时不要带上 Python 的缓存目录比如__pycache__、.pyc文件这些对整个项目没有实际作用只增加体积。下面是一段精简压缩目录的 Python 脚本import os import zipfile def zip_dir(target_dir, output_zip): with zipfile.ZipFile(output_zip, w, zipfile.ZIP_DEFLATED) as zf: for root, dirs, files in os.walk(target_dir): dirs[:] [d for d in dirs if d ! __pycache__] for file in files: full_path os.path.join(root, file) arcname os.path.relpath(full_path, target_dir) zf.write(full_path, arcname) zip_dir(dist/mnist_digit, 基于paddle的手写数字识别.zip)这段代码会自动过滤掉__pycache__目录并把dist/mnist_digit下所有内容写进一个 zip。arcname的作用是保留文件夹内部的相对结构用户解压后就能直接运行mnist_digit.exe。5.4 交付前必须验证的 3 个场景打包完成后不要只在开发机测试。把 zip 复制到一台没有安装 Paddle 的机器上最好是一台全新虚拟机然后依次检查双击启动程序能否正常弹出窗口。用一张白底黑字的手写数字图片做推理输出结果是否正确。断网环境下运行不检查 Paddle 版本、不下载任何资源。如果第一步就报错优先查看entry.py里是否使用了相对路径加载模型。解压目录不同当前工作目录也不同模型路径最好用下面这段代码定位import sys import os base_dir getattr(sys, _MEIPASS, os.path.abspath(.)) model_path os.path.join(base_dir, inference_mnist) model paddle.jit.load(model_path)sys._MEIPASS是 PyInstaller 在单文件模式下指向临时解压目录的变量-D模式下不存在因此回退到程序所在目录。这种写法兼容两种模式也是 PaddleOCR 便携打包版里常见的路径处理手法。6. 调参、自检与体积控制的几个实用技巧6.1 在 zip 包里加一个自检脚本用户拿到 zip 后不一定跑完整流程最好内置一个self_check.py一键验证模型文件、依赖库和推理输出是否正常。import paddle from paddle.vision.datasets import MNIST model paddle.jit.load(inference_mnist) model.eval() test_dataset MNIST(modetest, downloadFalse) image, label test_dataset[0] image_tensor paddle.to_tensor(image.reshape(1, 1, 28, 28).astype(float32)) result paddle.argmax(model(image_tensor), axis1).item() print(自检完成模型预测为, result, 真实标签为, label) assert int(result) int(label), 自检失败模型精度异常这段代码把 MNIST 测试集第一张图片作为固定样本模型预测必须和数据集标签一致才算通过。测试集中的第一张样本在 Paddle 原始数据集里基本是一个手写数字 7训练良好的 LeNet-5 不会在这个简单样本上翻车。如果自检失败优先检查模型文件和像素归一化是否匹配。6.2 用混淆矩阵定位易错数字准确率达到 99% 后剩下的错误往往集中在形状相近的数字上比如 3 和 8、4 和 9、7 和 1。单纯看准确率看不到这些问题可以在测试集上统计混淆矩阵import numpy as np confusion np.zeros((10, 10), dtypenp.int32) with paddle.no_grad(): for images, labels in test_loader: images paddle.reshape(images, [-1, 1, 28, 28]) preds paddle.argmax(model(images), axis1).numpy() for true_label, pred_label in zip(labels.numpy(), preds): confusion[true_label, pred_label] 1 error_pairs [] for i in range(10): for j in range(10): if i ! j and confusion[i][j] 0: error_pairs.append((i, j, confusion[i][j])) error_pairs.sort(keylambda x: x[2], reverseTrue) print(error_pairs[:5])如果看到明显的 4 到 9、3 到 8 的错误说明模型对局部细节不够敏感。常见做法是增加这些类别的训练样本或者对训练集做随机旋转和缩放增强。6.3 减小 zip 体积的简单办法Paddle 打包后的体积通常不会太小主要来自动态库。一个直接有效的做法是把模型压缩成半精度存储。MNIST 分类任务对精度损失不敏感模型参数换成 FP16 后准确率几乎不变文件体积减少一半。Paddle 提供paddle.save时直接传入numpy.float16类型的权重时可以缩小.pdiparams但推理时要转换成 float32 才能加载。做法是在训练后导出模型前手动遍历state_dict并转换数值类型。state model.state_dict() for key, value in state.items(): value value.astype(float16) state[key] value paddle.save(state, mnist_lenet_fp16.pdparams)这个策略适合发布模型而不是训练模型。FP16 参数体积小但大的 Paddle 运行库才是 zip 体积的主要来源想要进一步压缩就需要把不需用的 OCR、NLP 模型从依赖中排除。本篇文章场景相对轻量动态库压缩空间仍存在但还是要以你的项目实际体积为准。本文还有配套的精品资源点击获取
返回列表