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

资讯详情

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

Python+Keras实现MNIST手写数字识别:深度学习入门实战指南

Python+Keras实现MNIST手写数字识别:深度学习入门实战指南 这几年被问得最多的问题永远是同一个想入门深度学习第一个项目到底应该做什么我的答案也一直没变过 ——MNIST手写数字识别。别看这个任务小它把“数据加载、模型搭建、训练评估、结果可视化”这一整条深度学习流程全串起来了。最有意思的是用 Python 和 Keras 来实现核心代码可以压缩到三四十行新手花半天时间就能跑通第一个真正属于自己的神经网络项目。这篇文章我准备把手写数字识别的完整实现过程、背后每一步的选型逻辑以及我在实操中踩过的坑全部拆开讲一遍。不管你是刚装好 Python 还没写过几行代码的纯新手还是已经会基础语法想跨进深度学习门槛的进阶者照着这份记录走都能把项目跑起来。1. 项目内容与整体设计思路1.1 为什么把 MNIST 当作深度学习入门的第一课MNIST 数据集全称是 Modified National Institute of Standards and Technology database里面是 6 万张训练图片和 1 万张测试图片每张都是一个 28x28 像素的灰度手写数字内容为 0 到 9 的阿拉伯数字。说直白点它就是一套“手写数字照片”的标准化题库。这个数据集在深度学习社区里的地位基本相当于编程领域的“Hello World”。它存在的重要意义在于图像尺寸统一、类别固定是 10 类、任务目标清楚到不能再清楚没有任何歧义。新手用这个数据集练手注意力可以完全集中在“搭建模型、训练模型”本身而不是被数据清洗、图片缩放、标注格式这些繁琐的工程问题拖住。等你把 MNIST 跑通、搞明白每一步在干什么再往后接触 CIFAR-10、ImageNet 这种更复杂的数据集时才不会发怵因为整套流程套路是一模一样的。还有一个现实原因MNIST 的数据量不大6 万张 28x28 的灰度图加起来大概 11 MB 左右。这意味着连 GPU 都不需要普通家用 CPU 跑几十秒到几分钟就能完成一轮训练对初学者极其友好。我见过太多人一上来就直奔大型数据集结果是环境配置折腾三小时、训练一跑就崩最后还没入门就放弃了。循序渐进是非常重要的。1.2 为什么用 Keras 而不是直接用 TensorFlow 或者 PyTorch这里我得先解释一下版本问题。现在你安装 TensorFlow里面其实就自带了 Keras 接口写法是from tensorflow import keras。我不推荐装独立的 Keras 包因为新版 Keras 早已深度集成进 TensorFlow独立安装反而容易出现版本匹配问题。选用 Keras 的核心理由是它的 API 设计足够“傻瓜化”——你不需要先理解张量怎么手动构建、计算图怎么定义只要按照“搭积木”的方式把层一层层叠起来模型就建好了。相比于 PyTorch 那种更灵活但也更自由的写法Keras 在入门阶段最大的优势是约束性强、报错信息友好。它把大量底层细节封装好了让“神经网络的每一层做什么”变得一目了然。你甚至可以这么说PyTorch 像手动挡汽车控制感强Keras 像自动挡汽车省心省力。新手阶段用自动挡把路跑熟之后想换手动挡再去研究底层也不迟。另一个重要考量是部署生态。Keras 训练出来的模型可以很方便地导出成 TensorFlow SavedModel 格式后续做 web 服务、移动端部署都有成熟方案。对只做学习验证的初学者来说这是一个不用急着考虑、但未来大概率能受益的隐藏优势。1.3 网络结构选型全连接网络已经足够很多人一听说图像识别第一反应就是“得上卷积神经网络 CNN”。这个想法本身没错CNN 确实是图像领域的标配。但对于 MNIST 这个任务如果只追求“把数字认对”用一个不带卷积层的全连接网络就能轻松达到 97% 以上的准确率。全连接网络又叫多层感知机结构是输入层 - 隐藏层 - 输出层每一层的每一个神经元都和上一层的所有神经元相连。它的优点是结构简单、容易理解、训练速度快对初学者来说最能看清“数据是怎么一层一层被处理”的。我在下面的实现中先用两层全连接网络打底把整个流程跑通后再告诉你加卷积层能提升到什么程度。这种“先简单后复杂、先跑通再优化”的思路我觉得是做任何技术项目的通用方法论。2. 环境准备与数据集加载2.1 Python 环境与 Keras 安装的正确姿势做这个项目Python 版本建议 3.8 到 3.11 之间太老或者太新都可能和 TensorFlow 版本有兼容性问题。我在实践中最省事的方式是直接装 Anaconda它会顺带把 conda 包管理器装好以后管理虚拟环境非常方便。如果你已经装好了原生的 Python用 pip 装包也一样。在干净的虚拟环境里安装 TensorFlow只需要一条命令pip install tensorflow这条命令会把 TensorFlow 连同 Keras 接口、NumPy 等依赖一起装好。要注意的是在比较新的版本中Keras 3 已经作为一个独立包存在TensorFlow 会自动依赖调用你完全不用区分彼此。装完之后简单验证一下python -c import tensorflow as tf; print(tf.__version__)如果正常输出版本号说明环境已经 OK 了。在 Windows 上如果装的是 CPU 版 TensorFlow直接跑 MNIST 这种小任务足够用。GPU 版本不是必须的有 NVIDIA 显卡可以装tensorflow-cpu反而是更稳妥的选择。很多人喜欢一开始就折腾 GPU 版结果 CUDA、cuDNN 版本对不上光配环境就卡几天在我看来完全没有必要。2.2 MNIST 数据集的获取以及那个经典的下载坑获取 MNIST 数据最省事的途径是直接用 Keras 内置的数据加载接口。只要一行代码它就会自动从网络下载并解析好数据from tensorflow.keras.datasets import mnist (x_train, y_train), (x_test, y_test) mnist.load_data()执行完这一步你会得到四个 NumPy 数组分别是训练图片、训练标签、测试图片、测试标签。注意不需要自己去网上找压缩包、手动解压、再写代码解析这些 Keras 都帮你做掉了。我在实际带人做项目时发现很多朋友会在这里踩坑。其中最常见的是“torchvision 下载 MNIST 时报 404 错误”——这是 PyTorch 用户常见的烦恼因为 torchvision 的数据集下载源有时会发生文件路径变动或服务不稳定。这类问题的本质是数据集托管服务器的访问问题不是你代码的问题。遇到这种情况常规的解决方式是切换数据集镜像源或者干脆用官方更稳定的下载渠道。如果你在 Keras 里用mnist.load_data()也遇到了下载超时的情况我后面在常见问题那一章会专门讲怎么应对。下载完成后数据长什么样x_train的形状是(60000, 28, 28)y_train的形状是(60000,)每个元素是 0-9 的整数。这组形状是后面搭建模型的一个重要依据输入层要能接收 28x28 的矩阵输出层必须至少 10 个神经元。2.3 数据预处理归一化到底在做什么拿到原始数据后不能直接塞给模型。MNIST 图片里的像素值范围是 0-255也就是 256 个灰阶。如果直接用这些大数值做输入会让神经网络在计算梯度时变得不稳定学习速度也会被拖慢。归一化的目标是把所有像素值压缩到 0-1 的范围最常见的做法是直接除以 255x_train x_train.astype(float32) / 255.0 x_test x_test.astype(float32) / 255.0这里为什么要转成 float32因为原始数据是 uint8 类型直接拿整数做除法会被舍入成整数精度就丢了。转成浮点再除 255每个像素才能变成 0.0 到 1.0 之间的小数。关于标签有两种常见的处理方式。一种是直接拿整数标签喂给模型配合sparse_categorical_crossentropy损失函数使用另一种是把标签做 one-hot 编码变成 10 维向量配合categorical_crossentropy使用。两种方式最终效果等价我下面的代码选用第一种代码更少、更直观。这部分经常有新手会疑惑归一化完了之后数据形状还是(60000, 28, 28)我的模型是二维输入会不会报错这就要说到下一章的 Flatten 层了。3. 模型搭建与核心代码实现3.1 网络架构设计从输入到输出的每一层我用 Keras 的 Sequential 顺序模型来搭建网络这意味着各层是像串糖葫芦一样依次排列的。每一层的输出就是下一层的输入。来看看代码from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Flatten, Dense model Sequential([ Flatten(input_shape(28, 28)), Dense(128, activationrelu), Dense(10, activationsoftmax) ])逐层拆开讲。第一层Flatten(input_shape(28, 28))作用是把 28x28 的二维矩阵“拉平”成一个 784 维的一维向量。为什么要这一步因为全连接层期望的输入是一个一维向量如果不做展平Dense 层无法正确处理二维图像数据。你可以把 Flatten 想象成把一张表格按行读成一个序列信息量完全不丢。第二层Dense(128, activationrelu)是真正的计算核心。128 表示这一层有 128 个神经元每个神经元都会对上一层的 784 个输入做加权求和再经过激活函数输出。128 这个数字怎么定的它不是某条定理严格推导的结果而是实践中比较常用的经验值。神经元太少模型的表达能力不够准确率会偏低神经元太多训练变慢且容易过拟合。我在 MNIST 上用 128 和 256 分别测过准确率差距并不大128 是性价比很高的选择。激活函数用的 ReLU全称 Rectified Linear Unit。它的公式简单粗暴输入为正则原样输出输入为负则输出 0。这个函数是目前全连接网络最主流的默认选择优点是计算快、能有效缓解梯度消失问题。第三层Dense(10, activationsoftmax)是输出层。10 对应 10 个数字分类。Softmax 函数会把上一层输出的 10 个原始分数转换成“概率分布”—— 10 个数加起来正好等于 1每个数代表模型认为这张图片是某个数字的概率。比如输出[0.05, 0.01, 0.02, 0.88, ...]模型就是在说“这张图最可能是数字 3”。这套结构虽然只有三层却完整展示了神经网络的核心机制展平输入、非线性变换、概率输出。初学者一定要把这三层的作用吃透之后学 CNN、RNN 都是在这个基础上加花样。3.2 编译阶段损失函数、优化器、评估指标模型结构定好之后还需要通过compile来配置训练过程。这一步要告诉 Keras 三件事用什么损失函数衡量错误、用什么优化算法更新参数、训练时关注什么指标。model.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy])先看损失函数sparse_categorical_crossentropy。它在数学上是交叉熵损失专门用于多分类问题。我这里选的是 sparse 版本原因是我们的标签是整数而不是 one-hot 向量用这个损失函数就不用额外做标签编码。损失值越小代表模型预测和真实标签越接近。配合前面说过的 one-hot 编码方式对应应该用categorical_crossentropy两者本质是一回事。再看优化器adam。Adam 是一种自适应学习率的优化算法它最大的优点是基本不需要手动调节学习率训练过程相对稳定非常适合新手。在 MNIST 这个任务上动量和 RMSProp 这些经典优化器也能跑出类似结果但 Adam 是现在最省心的默认选项。最后的metrics[accuracy]是告诉 Keras 在训练过程里除了记录损失还要计算准确率方便我们直观地看模型表现。多分类准确率的计算方式是预测概率最大的那个类别和真实标签一致时算对把所有正确的数量除以总数。3.3 训练与评估fit 和 evaluate 的完整流程编译完成后模型就处于“准备好了但还没学”的状态。接下来进入核心环节——训练。Keras 的训练只需要一行调接口history model.fit(x_train, y_train, batch_size32, epochs10, validation_split0.1)这里面的几个参数每一个都值得展开讲。batch_size32表示每次从 6 万张训练图片里随机抽 32 张计算一次梯度并更新一次权重。为什么不一次性用全部数据算梯度主要考虑了内存和稳定性两方面一是 6 万张图片一次性算的话内存占用大二是一批数据算出的梯度带有一定随机性这种“噪声”有时候反而能让模型逃离不好的局部最优点。32 是目前最经典的批大小小一点更新更频繁但不稳定大一点速度快但可能内存吃紧。epochs10表示把全部训练数据完整过 10 遍。每一遍模型都会对 6 万张图片做一次“扫描学习”。轮次也不是越多越好——训练到后期训练集准确率会接近 100%但验证集准确率可能反而开始下降这就说明模型进入了过拟合状态。10 轮在 MNIST 这个任务上是够用的。validation_split0.1是让我很推荐新手使用的一个参数。它的作用是把训练集分成两部分90% 用来训练10% 用来每轮结束后验证效果。这个验证集不完全参与训练只用来检查模型的泛化能力。Keras 在每个 epoch 结束后会自动在验证集上跑一遍我们就能实时看到模型在“没见过的数据”上表现如何这是判断过拟合最直接的方式。训练完之后用测试集做最终评估test_loss, test_acc model.evaluate(x_test, y_test) print(fTest accuracy: {test_acc:.4f})注意测试集和验证集是不同的概念。验证集参与了模型调参过程测试集是完全独立的“考场”。用测试集评估得到的结果才是对模型真实性能最可信的估计。4. 训练过程、结果分析与调优实验4.1 训练过程的输出与可视化解读跑完model.fit后控制台会输出每一轮的信息我机器上跑出来的效果大致如下Epoch 1/10 1688/1688 [] - 4s 2ms/step - loss: 0.3225 - accuracy: 0.9110 - val_loss: 0.1765 - val_accuracy: 0.9483 ... Epoch 10/10 1688/1688 [] - 3s 2ms/step - loss: 0.0542 - accuracy: 0.9832 - val_loss: 0.0756 - val_accuracy: 0.9787 Test accuracy: 0.9789从第一轮到第十轮训练集准确率从 91% 稳步爬升到 98% 以上验证集准确率最后基本维持在 97.8% 左右。注意一个细节验证集准确率一路略低于训练集准确率这是完全正常的现象因为模型天然会对“见过”的数据表现更好只要差距没有越拉越大就算健康。Keras 的fit返回值history里保存了每个 epoch 的 loss、accuracy、val_loss、val_accuracy 数据。把这些数据画成图表会比光看数字直观得多。画图不需要额外装复杂工具用 matplotlib 就可以import matplotlib.pyplot as plt plt.plot(history.history[accuracy], labeltrain accuracy) plt.plot(history.history[val_accuracy], labelval accuracy) plt.xlabel(Epoch) plt.ylabel(Accuracy) plt.legend() plt.show()这个图表非常有用。如果训练集准确率持续上升而验证集准确率掉头向下说明模型已经开始过拟合如果两条线都比较低且上升缓慢说明模型学习能力不足或者学习率设置有问题。4.2 几个关键超参数的效果对比既然文章标题是“手写数字识别”只跑通一个模型肯定不够过瘾。我把我实际做过的几组超参数对比实验列出来给大家做个参考。第一组是不同隐藏层神经元数量的对比。我用同样的训练方式和 10 个 epoch分别测试了 64、128、256、512 四个神经元数量。结果是 128 和 256 的最终准确率都在 97.8% 左右64 稍低些约 97.2%512 反而没有明显提升训练时长却涨了接近一倍。结论很明确对于 MNIST一层的神经元数量在 128-256 之间已经非常够用盲目加大宽度意义不大。第二组是网络深度的对比。在原有单隐藏层基础上我再加一层 Dense(128)变成两个隐藏层。最终准确率提升了 0.3%-0.5% 左右但训练时间几乎没有多花多少。这说明了增加网络深度在合理范围内是有益的它能让模型学习到更抽象的特征组合。第三组是我把全连接网络换成一个小型 CNN 的对比。用一层 Conv2D MaxPooling2D 的简单卷积结构测试准确率直接跳到 99% 以上。这说明卷积层捕捉局部空间特征的能力确实比纯全连接网络强。那为什么我一开始不用 CNN因为对第一次接触神经网络的人来说CNN 的卷积核、池化、特征图这些概念叠加在一起理解门槛会瞬间拉高。先把简单模型跑通再去做卷积升级才是更平滑的学习路径。4.3 实际预测与结果可视化模型训练完判断它好坏的标准除了准确率数字还应该亲眼看看预测效果。我习惯从测试集里随机抽几张图让模型预测并输出结果再把图片画出来对比真实标签。import numpy as np predictions model.predict(x_test[:10]) predicted_labels np.argmax(predictions, axis1) fig, axes plt.subplots(2, 5, figsize(10, 5)) for i, ax in enumerate(axes.flat): ax.imshow(x_test[i], cmapgray) ax.set_title(fTrue: {y_test[i]}, Pred: {predicted_labels[i]}) ax.axis(off) plt.show()这里有个关键点model.predict返回的是一组概率向量不能直接拿来当预测标签必须用np.argmax取出每个概率向量中最大值的下标那个下标才是模型预测的数字。这一步经常有人漏掉导致输出看起来是一堆小数看着就懵了。看可视化结果时如果发现模型把某个数字认错了别急着怀疑代码。MNIST 数据本身就有模棱两可的样本——有些人的手写数字连人眼都很难分清。比如“4”写得像“9”“7”写得像“1”这在数据集中真实存在。模型整体准确率在 98% 左右说明它已经把绝大多数清晰样本都学到位了。5. 常见问题与排查技巧实录5.1 数据集下载失败超时、404、SSL 证书报错这个问题的出现频率高得吓人。Keras 的mnist.load_data()默认从storage.googleapis.com下载数据国内一些网络环境下访问不稳定就可能抛出连接超时或 SSL 证书错误。Torchvision 用户遇到的 MNIST 404 错误也属于同类问题数据集托管源文件路径变动导致下载失败。我的处理方案是“手动下载 本地加载”。Keras 其实支持你预先下载好数据文件放到指定缓存目录。MNIST 数据在 Keras 里加载的是 mnist.npz 这个压缩包你可以先想办法把文件下载下来然后拷贝到本地缓存路径再执行mnist.load_data()。如果网络访问实在困难也可以从一些可靠的镜像站下载好 npz 文件放到~/.keras/datasets/目录下一行脚本验一下import numpy as np f np.load(mnist.npz) print(f.files)正常情况下会输出[x_test, x_train, y_test, y_train]这几个键的数组就是完整数据集。确认文件没问题后Keras 会自动优先使用本地缓存文件不再重复联网下载。5.2 维度不匹配ValueError 的各种形态新手跑这个项目最常见的报错信息大概长这样ValueError: Shapes (None, 10) and (None, 1) are incompatible或者ValueError: Input 0 of layer dense is incompatible with the layer...第一类报错基本都出在标签格式上。如果你用了categorical_crossentropy但标签还是整数Keras 就会疯掉。解决办法是二选一要么把标签转为 one-hot要么把损失函数换成sparse_categorical_crossentropy。我强烈建议新手统一用 sparse 版本少一层转换代码。第二类报错出在输入形状上。如果Flatten(input_shape(28, 28))这一层写错了或者压根忘了写模型就无法对接上(60000, 28, 28)的输入数据。可以打印一下x_train.shape确认再回头检查input_shape是否完全一致。这里的input_shape不包括批量维度所以写的是(28, 28)而不是(60000, 28, 28)。5.3 训练收敛慢或者准确率上不去有些朋友跑完后发现准确率一直卡在 20% 左右——这和随机猜 10 类差不多。这种情况几乎可以断定是预处理环节出了问题。检查清单如下像素值有没有归一化如果直接把 0-255 的整数喂给模型训练会很慢很慢。标签和特征有没有对齐错位可以把y_train[:10]打印出来再对比x_train[:10]的图片看每张图的标签是否对得上。激活函数是不是在最后一层用了 ReLU如果最后一层不是 softmax输出就不是概率损失计算会乱套。还有一种是准确率很高但始终在 99% 以下上不去比如卡在 98.5%。这时可以考虑加一层隐藏层、增加训练轮次、或者加一个 0.1 到 0.3 的 Dropout 层防止过拟合。把训练轮次从 10 提升到 15-20 轮往往也能再往上走一步。5.4 环境安装失败的常见因素最后说一下环境层面的问题。TensorFlow 装不上原因五花八门但最高频的是 Python 版本过新。比如 Python 3.12 刚出来那阵子TensorFlow 还没发布对应版本pip 就会报找不到合适的依赖。解决方案是装一个 3.8-3.11 之间的版本或者升级 TensorFlow 到最新版。还有一个经典问题是 pip 默认源速度慢、超时可以临时换用国内镜像源来安装命令是pip install tensorflow -i https://pypi.tuna.tsinghua.edu.cn/simple如果提示缺什么动态链接库文件多半是 Windows 下缺少 VC 运行库装上微软官方提供的 Visual C Redistributable 就好。这类环境问题虽然琐碎但绝大多数在搜索引擎上都能找到详细讨论不要自己闷头纠结太久。我把这个项目从头到尾完整复现下来最核心的感受是不要把“手写数字识别”理解成一个背代码的任务而是把它当作一条完整的思维链路——数据要归一化网络要选结构损失要算交叉熵参数要调得合适。只要你亲手把每一个环节都折腾过一遍后续再接触图像分类、文本分类本质上都是同一个骨架换不同的皮。真要说还有什么更想分享的那就是一个建议跑通代码后别急着关掉把 128 个神经元改成 16 个试试把 ReLU 改成 sigmoid 试试把一个隐藏层改成三个试试。亲自看到准确率怎么起起伏伏比记住任何公式都管用。
返回列表