ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python机器学习手写数字识别源码解析:VGG19与DeepNET实战

Python机器学习手写数字识别源码解析:VGG19与DeepNET实战 简介这份资源是面向机器学习初学者与图像识别方向开发者的手写数字识别系统完整源码基于Python实现可帮助读者理解从数据加载、模型训练到界面交互的完整流程。压缩包共44个文件、约8.06MB以8个Python源码文件为核心辅以11张PNG图片、5个XML配置、6个pyc编译文件及MNIST数据集文件涵盖网络结构定义、训练脚本、绘图板与主界面模块目录结构清晰便于按功能模块阅读与二次开发。项目涉及VGG19等深度卷积网络架构并配有演示与训练入口脚本适合作为课程设计、毕业设计或算法练手的参考案例。目前已有514人学习下载读者可从中获取图像处理、模型训练与用户交互界面整合的实践思路快速搭建可运行的手写数字识别原型。1. 拆开这个手写数字识别源码包35 个文件里到底藏了什么如果你正在搜「Python 机器学习 手写数字识别 源码」大概率是三种人之一课程设计要交作业的学生、想找一个能跑通的 MNIST 实战项目练手的转行者、或者需要给团队做个手写识别 Demo 的工程师。这个包我拆过一遍它不是那种只有几个脚本的玩具而是一个带图形界面、带训练脚本、带推理演示的完整系统。核心逻辑是用 MNIST 数据集训练一个卷积网络然后在画板上手写数字程序实时识别。源码里同时出现了 VGG19 和 DeepNET 两套网络定义说明作者至少尝试过两种架构路线。文件构成上8 个 Python 源文件是主体6 个 pyc 编译文件说明它被实际运行过5 个 XML 是 IDE 配置11 张 PNG 里既有训练过程的代价曲线图也有界面截图和测试样本。换句话说这是一个「跑过、调过、有界面」的项目不是纯理论代码。2. 环境搭建与依赖安装把 Python 环境配到能跑为止2.1 为什么这个项目对 Python 版本有要求源码里出现了DeepNET.cpython-38.pyc和function.cpython-39.pyc这说明项目至少在 Python 3.8 和 3.9 两个版本下运行过。我一般建议直接用 Python 3.9因为 TensorFlow 2.x 和 PyTorch 对 3.9 的支持最稳。如果你用 3.11 或更高版本部分旧版深度学习库可能装不上会卡在编译阶段。常见做法是建一个独立虚拟环境避免和你系统里已有的包冲突。# 创建虚拟环境指定 Python 3.9 python3.9 -m venv mnist_env # 激活环境Windows mnist_env\Scripts\activate # 激活环境macOS / Linux source mnist_env/bin/activate # 升级 pip避免安装时出现依赖解析问题 pip install --upgrade pip逻辑说明虚拟环境的作用是把项目依赖和你系统全局的包隔离开。参数上python3.9要换成你本机实际安装的版本命令Windows 下可能是py -3.9 -m venv mnist_env。激活后命令行前面会出现(mnist_env)前缀看到这个就说明环境生效了。2.2 依赖清单与安装顺序这个项目涉及图像处理、深度学习框架和 GUI 三块依赖。从源码文件名推断PaintBoard.py和MainWidget.py大概率用了 PyQt 或 Tkinter 做界面train.py和VGG19.py需要深度学习框架。我一般会按下面的顺序装因为深度学习框架的依赖链最长先装它能把底层库一次性拉齐。# 先装深度学习框架二选一 pip install tensorflow2.10.0 # 或者 pip install torch torchvision # 再装图像处理和数值计算 pip install numpy opencv-python pillow matplotlib # 最后装 GUI 框架根据源码实际用的来 pip install PyQt5逻辑说明tensorflow2.10.0是最后一个原生支持 Windows GPU 的 TF 版本如果你在 Windows 上且有 NVIDIA 显卡这个版本能直接调用 GPU。没有 GPU 就用 CPU 版训练会慢但能跑。opencv-python用于图像预处理pillow用于打开和显示 PNG 文件matplotlib用来画代价曲线。GUI 框架这块如果运行main.py报ModuleNotFoundError: No module named PyQt5就装 PyQt5如果报的是tkinter相关错误那说明用的是 Tkinter不用额外装。提示安装 TensorFlow 时如果卡在Building wheel for grpcio先执行pip install --upgrade setuptools wheel再重试。这是最常见的安装翻车点。2.3 验证环境是否就绪装完之后别急着跑主程序先用一段最小代码验证核心库能不能正常导入和计算。# check_env.py import sys print(Python 版本:, sys.version) import numpy as np print(NumPy 版本:, np.__version__) import tensorflow as tf print(TensorFlow 版本:, tf.__version__) print(GPU 可用:, tf.config.list_physical_devices(GPU)) import cv2 print(OpenCV 版本:, cv2.__version__)逻辑说明这段脚本逐项检查关键库的导入和版本。tf.config.list_physical_devices(GPU)返回空列表说明用的是 CPU返回非空说明 GPU 可用。如果某一行报ImportError就回到上一步单独装那个库。参数上不需要改任何东西直接python check_env.py运行即可。3. MNIST 数据集的加载与预处理别让格式问题卡住训练3.1 idx 格式文件的读取方式项目里有一组train-images-idx3-ubyte、t10k-images-idx3-ubyte这样的文件这是 MNIST 官方的二进制格式。很多人第一次拿到这种文件不知道怎么读直接用open()读出来是一堆乱码。正确做法是按固定字节偏移解析头部信息再读出像素数据。# load_mnist.py import numpy as np import os def load_idx_images(filepath): 读取 idx3-ubyte 格式的图像文件 with open(filepath, rb) as f: # 前 4 字节是魔数接下来 4 字节是图像数量 magic int.from_bytes(f.read(4), big) num_images int.from_bytes(f.read(4), big) rows int.from_bytes(f.read(4), big) cols int.from_bytes(f.read(4), big) # 剩余字节全部是像素数据每张图 rows*cols 个字节 data np.frombuffer(f.read(), dtypenp.uint8) data data.reshape(num_images, rows, cols) return data def load_idx_labels(filepath): 读取 idx1-ubyte 格式的标签文件 with open(filepath, rb) as f: magic int.from_bytes(f.read(4), big) num_labels int.from_bytes(f.read(4), big) labels np.frombuffer(f.read(), dtypenp.uint8) return labels # 使用示例 train_images load_idx_images(train-images-idx3-ubyte) train_labels load_idx_labels(train-labels-idx1-ubyte) print(训练集图像形状:, train_images.shape) # 应为 (60000, 28, 28) print(训练集标签形状:, train_labels.shape) # 应为 (60000,)逻辑说明int.from_bytes(f.read(4), big)按大端序读取 4 字节整数这是 idx 格式的规范。np.frombuffer直接把二进制缓冲区转成数组比循环读取快几个数量级。参数上reshape的维度顺序是(样本数, 行, 列)MNIST 是 28×28。如果你的文件名是t10k-images.idx3-ubyte带点把路径参数改一下就行读取逻辑完全一样。3.2 归一化与维度扩展的实操细节原始像素值是 0 到 255 的整数直接喂给网络会导致梯度爆炸或收敛极慢。标准做法是归一化到 0 到 1 之间同时给图像加一个通道维度因为卷积层要求输入是(样本, 高, 宽, 通道)四维张量。# preprocess.py import numpy as np def preprocess(images, labels): # 归一化像素值除以 255 images images.astype(float32) / 255.0 # 扩展通道维度从 (N, 28, 28) 变成 (N, 28, 28, 1) images np.expand_dims(images, axis-1) # 标签转 one-hot 编码 labels np.eye(10)[labels] return images, labels train_images, train_labels preprocess(train_images, train_labels) print(预处理后图像形状:, train_images.shape) # (60000, 28, 28, 1) print(预处理后标签形状:, train_labels.shape) # (60000, 10)逻辑说明astype(float32)先把整数转成浮点再做除法避免整数除法截断。np.expand_dims(images, axis-1)在最后加一个维度这是 Keras/TensorFlow 卷积层的标准输入格式。np.eye(10)[labels]是 one-hot 编码的简洁写法生成一个 10×10 单位矩阵然后按标签索引取行。参数上如果你的网络用的是 PyTorch标签不需要转 one-hot直接用整数标签配合CrossEntropyLoss即可。注意归一化必须在划分训练集和测试集之前分别做不能先合并再归一化否则测试集的像素分布信息会泄漏到训练过程中。3.3 训练集与验证集的划分策略60000 张训练图不能全部用来训练得留一部分做验证否则你无法判断模型是否过拟合。常见做法是留 10% 做验证集。from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split( train_images, train_labels, test_size0.1, # 10% 做验证 random_state42, # 固定随机种子保证可复现 stratifytrain_labels.argmax(axis1) # 按类别分层抽样 ) print(训练集:, X_train.shape, 验证集:, X_val.shape)逻辑说明stratify参数保证每个数字类别在训练集和验证集中的比例一致避免某个数字在验证集中缺失。random_state42是固定种子保证你每次运行划分结果相同方便对比实验。参数上test_size可以改成 0.2 如果数据量够大但 MNIST 60000 张用 0.1 就够了。4. VGG19 与 DeepNET 两套网络结构的对比与选型4.1 VGG19 在这个项目里的角色源码里有VGG19.py和vgg19_demo.py说明作者用 VGG19 做过一轮实验。VGG19 原本是为 ImageNet 设计的输入是 224×224 的彩色图直接拿来跑 28×28 的灰度 MNIST 有两个问题一是参数量太大二是输入尺寸不匹配。常见做法是砍掉后面的全连接层只保留卷积部分做特征提取或者把第一层卷积的输入通道改成 1全连接层输出改成 10。# vgg19_mnist.py from tensorflow.keras import layers, models def build_vgg19_mnist(): model models.Sequential() # 第一层改成单通道输入 model.add(layers.Conv2D(64, (3, 3), activationrelu, paddingsame, input_shape(28, 28, 1))) model.add(layers.Conv2D(64, (3, 3), activationrelu, paddingsame)) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(128, (3, 3), activationrelu, paddingsame)) model.add(layers.Conv2D(128, (3, 3), activationrelu, paddingsame)) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(256, (3, 3), activationrelu, paddingsame)) model.add(layers.Conv2D(256, (3, 3), activationrelu, paddingsame)) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Flatten()) model.add(layers.Dense(256, activationrelu)) model.add(layers.Dropout(0.5)) model.add(layers.Dense(10, activationsoftmax)) return model model build_vgg19_mnist() model.summary()逻辑说明这里保留了 VGG 的堆叠卷积风格但把全连接层从 4096 缩到 256因为 MNIST 只有 10 类不需要那么大的分类头。paddingsame保证卷积后尺寸不变MaxPooling2D每次把尺寸减半。Dropout(0.5)是防过拟合的关键VGG 参数量大不加 Dropout 很容易在训练集上到 99% 但验证集卡在 98% 上不去。参数上如果你显存不够把每层的通道数减半比如 64 改成 32。4.2 DeepNET 的轻量路线DeepNET.py从命名看是作者自己搭的一个网络大概率比 VGG19 轻。对于 MNIST 这种简单任务其实不需要 VGG19 那么深的网络一个 3 到 4 层的卷积网络就能到 99% 以上的准确率。我一般会推荐先用轻量网络跑通流程再换 VGG19 对比效果。# deepnet.py from tensorflow.keras import layers, models def build_deepnet(): model models.Sequential([ layers.Conv2D(32, (3, 3), activationrelu, input_shape(28, 28, 1)), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu), layers.Flatten(), layers.Dense(64, activationrelu), layers.Dense(10, activationsoftmax) ]) return model model build_deepnet() model.summary()逻辑说明这个网络只有 3 个卷积层参数量在几十万级别训练一轮只要几秒。Conv2D(32, ...)的 32 是卷积核数量(3, 3)是卷积核大小。两层MaxPooling2D把 28×28 降到 7×7然后 Flatten 拉平送进全连接。参数上如果你想再简化可以把第三个卷积层去掉准确率大概掉 0.3 个百分点但训练速度翻倍。4.3 两套结构的选型建议对比维度VGG19 改造版DeepNET 轻量版参数量约 2000 万约 50 万单轮训练时间CPU3-5 分钟10-20 秒MNIST 测试准确率99.3% 左右99.0% 左右适合场景学习深层网络结构快速验证、课程设计显存占用较高低选型逻辑很直接如果你只是要交课程设计或者快速看到效果用 DeepNET如果你想研究网络深度对识别率的影响或者需要写实验对比报告用 VGG19。两者在 MNIST 上的准确率差距不到 0.5 个百分点但训练成本差了一个数量级。5. 训练脚本的编写与 GUI 画板联调从 train.py 到 PaintBoard.py5.1 train.py 的核心训练循环训练脚本的骨架是编译、拟合、保存模型三步。关键是回调函数的配置没有回调你无法在训练过程中保存最优模型。# train.py from tensorflow.keras import callbacks from deepnet import build_deepnet from load_mnist import load_idx_images, load_idx_labels from preprocess import preprocess from sklearn.model_selection import train_test_split # 加载和预处理 train_images load_idx_images(train-images-idx3-ubyte) train_labels load_idx_labels(train-labels-idx1-ubyte) train_images, train_labels preprocess(train_images, train_labels) X_train, X_val, y_train, y_val train_test_split( train_images, train_labels, test_size0.1, random_state42 ) # 构建模型 model build_deepnet() model.compile( optimizeradam, losscategorical_crossentropy, metrics[accuracy] ) # 回调保存最优模型 早停 callbacks_list [ callbacks.ModelCheckpoint( best_model.h5, monitorval_accuracy, save_best_onlyTrue, verbose1 ), callbacks.EarlyStopping( monitorval_loss, patience3, restore_best_weightsTrue ) ] # 训练 history model.fit( X_train, y_train, epochs20, batch_size128, validation_data(X_val, y_val), callbackscallbacks_list )逻辑说明ModelCheckpoint在每个 epoch 结束后检查验证准确率如果比之前高就保存模型这样你最终拿到的是验证集上表现最好的那个版本而不是最后一个 epoch 的。EarlyStopping的patience3表示验证损失连续 3 轮不下降就停止训练避免浪费时间。batch_size128是经验值太小训练慢太大泛化差。epochs20配合早停实际可能 10 轮左右就停了。5.2 GUI 画板的图像采集与预处理对齐PaintBoard.py和MainWidget.py负责界面用户在画板上写数字程序把画板内容截取出来送进模型。这里最容易翻车的地方是画板上的图像格式和训练数据的格式不一致。训练数据是 28×28 灰度图白字黑底而画板通常是黑字白底尺寸也不对。# 画板图像预处理对齐 MNIST 格式 import cv2 import numpy as np def preprocess_canvas(canvas_image): 把画板截图转成模型能吃的格式 # 转灰度 gray cv2.cvtColor(canvas_image, cv2.COLOR_BGR2GRAY) # 反色画板是黑字白底MNIST 是白字黑底 gray cv2.bitwise_not(gray) # 二值化去掉抗锯齿产生的灰色边缘 _, binary cv2.threshold(gray, 50, 255, cv2.THRESH_BINARY) # 找到数字的边界框并裁剪 contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None x, y, w, h cv2.boundingRect(max(contours, keycv2.contourArea)) digit binary[y:yh, x:xw] # 缩放到 20x20再放到 28x28 画布中央 digit cv2.resize(digit, (20, 20), interpolationcv2.INTER_AREA) canvas np.zeros((28, 28), dtypenp.uint8) canvas[4:24, 4:24] digit # 归一化并扩展维度 canvas canvas.astype(float32) / 255.0 canvas np.expand_dims(canvas, axis(0, -1)) return canvas逻辑说明bitwise_not做反色是必须的否则模型看到的是「负片」识别率会暴跌。threshold二值化去掉画板笔刷的抗锯齿边缘让数字轮廓更接近 MNIST 的硬边缘。boundingRect找到数字的实际范围并裁剪然后缩放到 20×20 再居中放到 28×28 里这是 MNIST 原始数据的标准做法——数字不会占满整个 28×28周围有 4 像素的边距。参数上threshold的 50 可以根据你画板的背景色调整如果背景不是纯白适当调高。5.3 main.py 的入口逻辑与模块调用关系main.py是程序入口它负责初始化 GUI、加载模型、绑定按钮事件。从文件结构看调用链是main.py→MainWidget.py→PaintBoard.pyfunction.py模型推理部分调DeepNET.py或VGG19.py。# main.py 的骨架逻辑 import sys from PyQt5.QtWidgets import QApplication from tensorflow.keras.models import load_model from MainWidget import MainWidget if __name__ __main__: app QApplication(sys.argv) # 加载训练好的模型 model load_model(best_model.h5) # 创建主窗口把模型传进去 window MainWidget(model) window.show() sys.exit(app.exec_())逻辑说明load_model(best_model.h5)加载的是train.py里ModelCheckpoint保存的最优模型。MainWidget(model)把模型实例传给界面界面里的画板识别按钮会调用model.predict()。参数上如果你用的是 PyTorch 训练的模型这里要改成torch.load()并手动构建网络结构再加载权重。提示如果运行main.py时报FileNotFoundError: best_model.h5说明你还没跑train.py或者模型保存路径不对。先跑训练脚本生成模型文件再启动界面。6. 避坑与排查那些让我重跑过三次的坑6.1 坑一idx 文件读取后形状不对现象load_idx_images返回的数组形状是(60000, 784)而不是(60000, 28, 28)。原因某些版本的 MNIST 文件在下载时被重新打包过头部信息里的 rows 和 cols 被写成了 1 和 784或者文件本身是扁平化存储的。解决在读完之后加一步判断如果rows * cols 784且rows 1手动 reshape 成 28×28。代码里加if rows 1: rows, cols 28, 28再 reshape。6.2 坑二训练准确率很高但画板识别全是错的现象train.py跑完验证集准确率 99%但在 GUI 画板上写数字识别结果基本靠猜。原因画板图像没有做反色和居中处理模型看到的输入分布和训练数据完全不同。这是最典型的「训练-推理不一致」问题。解决严格按照 5.2 节的preprocess_canvas函数处理画板图像重点检查反色和 20×20 居中这两步。可以在预处理后把图像用cv2.imshow显示出来肉眼确认它长得像 MNIST 的样本。6.3 坑三PyQt5 和 OpenCV 的冲突导致界面卡死现象启动main.py后界面能显示但一点击识别按钮就卡死命令行没有任何报错。原因OpenCV 的cv2.imshow和 PyQt5 的事件循环冲突或者cv2.waitKey阻塞了主线程。解决在 GUI 代码里彻底移除所有cv2.imshow和cv2.waitKey调用图像显示全部用 PyQt 的QLabel和QPixmap。如果需要在调试时看图像把图像保存成 PNG 文件再打开看。6.4 坑四模型保存后加载报「unknown layer」现象load_model(best_model.h5)报ValueError: Unknown layer: Functional或自定义层无法识别。原因如果你在模型里用了自定义层或者 Lambda 层Keras 保存时不会把层的代码一起存进去加载时找不到定义。解决保存时用model.save(best_model.h5)而不是model.save_weights()前者保存完整结构。如果还是报错在load_model里加custom_objects参数把自定义层传进去。最省事的办法是避免使用自定义层全部用 Keras 内置层。6.5 坑五CPU 训练太慢以为程序卡死现象model.fit跑起来后命令行十几分钟没输出以为死循环了。原因CPU 训练 VGG19 改造版一个 epoch 可能要 5 分钟以上加上verbose1的进度条刷新有缓冲看起来像卡住。解决把batch_size调大比如 256或者换 DeepNET 轻量网络。如果坚持用 VGG19把verbose改成 2每个 epoch 只输出一行减少刷新开销。训练前先用 100 张图跑一个 epoch 测试流程是否通畅。7. 进阶技巧用混淆矩阵定位模型的「偏科」问题训练完模型、界面也能跑之后别急着收工。我一般会做一步额外验证在测试集上跑一遍预测生成混淆矩阵看看模型在哪个数字上容易出错。MNIST 上最常见的混淆是 4 和 9、3 和 8、5 和 6因为手写时这些数字的形态容易重叠。# confusion_matrix.py import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, classification_report from tensorflow.keras.models import load_model from load_mnist import load_idx_images, load_idx_labels from preprocess import preprocess # 加载测试集 test_images load_idx_images(t10k-images-idx3-ubyte) test_labels load_idx_labels(t10k-labels-idx1-ubyte) test_images, test_labels_onehot preprocess(test_images, test_labels) # 加载模型并预测 model load_model(best_model.h5) predictions model.predict(test_images) pred_labels np.argmax(predictions, axis1) # 混淆矩阵 cm confusion_matrix(test_labels, pred_labels) print(classification_report(test_labels, pred_labels)) # 可视化 plt.figure(figsize(10, 8)) plt.imshow(cm, interpolationnearest, cmapplt.cm.Blues) plt.title(MNIST Confusion Matrix) plt.colorbar() plt.xlabel(Predicted) plt.ylabel(True) for i in range(10): for j in range(10): plt.text(j, i, str(cm[i, j]), hacenter, vacenter, fontsize8) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150) plt.show()逻辑说明classification_report会输出每个数字的精确率、召回率和 F1 分数比总体准确率更有信息量。如果某个数字的召回率明显低于其他说明模型对这个数字「偏科」。confusion_matrix的可视化能直观看到哪些数字对之间容易混淆。参数上dpi150保证保存的图片够清晰方便放进报告里。拿到混淆矩阵之后针对性的改进手段有两个一是对容易混淆的数字做数据增强比如把 4 和 9 的样本做轻微旋转和缩放二是在网络里加一个注意力模块让模型更关注数字的区分性区域。对于课程设计级别的项目做到混淆矩阵分析这一步已经比大多数同学多走了一步。从那以后我每次跑完训练都会强制走一遍混淆矩阵哪怕总体准确率已经 99% 以上——因为那 1% 的错误里往往藏着模型真正的短板。希望帮到你。本文还有配套的精品资源点击获取
返回列表