ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Keras实战MNIST手写数字识别:完整流程与避坑指南

Keras实战MNIST手写数字识别:完整流程与避坑指南 如果你在网上搜“深度学习入门项目”十个结果里有八个会指向同一个名字MNIST手写数字识别。这个任务简单到什么程度用Keras写一个三层神经网络在CPU上训练几分钟测试准确率就能摸到97%所以它一直被称为“深度学习界的Hello World”。可奇怪的是我这么多年看下来真正能一次跑通全流程的人还是不多。很多人不是卡在算法上而是卡在环境、下载、shape不匹配、归一化忘了做这类基础环节上。这篇文章我就用Keras把MNIST手写数字识别完完整整走一遍。从环境搭建、数据集加载、模型设计、训练评估到最后用自己的手写图片做预测每一步我都会说明白为什么要这么做也会把我在不同机器上踩过的坑写出来。适合刚学Python、准备迈入深度学习大门的朋友也适合那些“跑过教程但没跑通过”的人。读完你会对神经网络的工作方式有一个非常真实的手感而不是只会复制粘贴。1. 为什么选Keras跑MNIST环境搭建与版本避坑1.1 先让环境跑通再说别的新手做深度学习最怕的往往不是模型本身而是装环境装到怀疑人生。选Keras就是冲着它最简单。Keras是TensorFlow的高层API用几行代码就能把网络搭出来不用手动处理张量求导、不用写训练循环。你只需要定义层、指定损失函数剩下的事情框架全包。我的建议很直接不要单独去装一个叫keras的包直接装TensorFlow然后从tensorflow.keras里导入。现在TensorFlow 2.x已经把Keras内置进去了两者兼容性最好。命令行执行pip install tensorflow如果你在国内网络环境加个镜像源会快很多pip install tensorflow -i https://pypi.tuna.tsinghua.edu.cn/simple我建议再装两个必须的依赖pip install numpy matplotlib装完之后不要急着写代码先验证环境python -c import tensorflow as tf; print(tf.__version__)能打印出版本号说明环境没问题。这里有个很容易踩的坑如果你之前单独用pip install keras装过最新版Keras然后又装TensorFlow两个Keras可能会打架。最稳妥的思路是代码里统一from tensorflow import keras不要混用。1.2 安装Keras时最容易忽略的版本细节Python版本和TensorFlow的搭配没选好后面会很难受。一般建议Python 3.9到3.12之间低于3.8基本别想了高于3.12可能会遇到依赖包还没跟上的情况。你在新建项目的时候最好用虚拟环境不要直接往系统Python里塞东西。创建虚拟环境python -m venv keras_mnist source keras_mnist/bin/activate # Windows用 keras_mnist\Scripts\activate然后再执行pip命令。这一步能救你无数次尤其是后面你又想装PyTorch、想装别的库的时候虚拟环境隔离能避免大量“这个库和那个库版本冲突”的问题。还有一个细节新手可能纠结GPU和CPU。MNIST这个任务CPU完全够用。全连接网络一个epoch也就几十秒CNN在普通CPU上多等个一两分钟也能跑完。完全没有必要一上来就折腾CUDA、cuDNN。等你以后跑大模型、跑图像分类项目的时候再考虑GPU也不迟。2. MNIST数据集加载与预处理99%的初学者在这里埋了雷2.1 load_data一步到位但下载失败怎么办Keras的好处之一是数据集内置了加载器。你不需要手动去下载图片、解压、分文件夹三行代码就能拿到数据from tensorflow import keras (x_train, y_train), (x_test, y_test) keras.datasets.mnist.load_data()第一次运行的时候Keras会自动下载MNIST数据集。下载过程可能会比较慢或者因为网络原因卡住甚至失败。处理办法并不复杂找到下载提示里的URL手动用浏览器下载对应的mnist.npz文件然后放到用户目录下的.keras/datasets/里。比如Windows是C:\Users\你的用户名\.keras\datasets\Linux和macOS是~/.keras/datasets/。文件名一定要和代码里请求的一致放好之后重新运行load_data它检测到本地文件就会直接加载。顺带提一句最近很多人用PyTorch的torchvision下载MNIST会报404因为数据集托管地址做了调整。Keras内置的加载器相对省心这也是我劝新手先玩Keras的原因之一。如果你以后确实要用PyTorch遇到404也别慌去网上搜新的下载地址或者离线下载好放到项目目录里别依赖自动下载。加载完之后你应该看一眼数据的shapeprint(x_train.shape, y_train.shape) print(x_test.shape, y_test.shape)正常会输出(60000, 28, 28) (60000,) (10000, 28, 28) (10000,)也就是说训练集有6万张28x28的灰度图测试集有1万张。每张图对应一个0到9的整数标签。到这里数据才算真正到手。2.2 归一化和reshape为什么要做很多第一次跑MNIST的人拿到的数据直接塞进模型然后发现loss飘忽不定准确率要么卡在0.1要么干脆不收敛。原因大概率就是忘了归一化。原始图像像素值范围是0到255这个数值范围对于神经网络来说太大了。如果不做归一化进入网络的第一层输入就会很大经过加权求和后激活函数的输入动辄几百上千梯度更新会非常不稳定。把它缩放到0到1之间数值尺度统一训练过程会平滑非常多。实际操作就两行x_train x_train.astype(float32) / 255.0 x_test x_test.astype(float32) / 255.0千万别忘了astype(float32)。整数除以255最后会变成浮点数但如果不显式转换某些环境下会出现类型问题。养成习惯先转类型再除。关于reshape取决于你用什么模型。如果用的是全连接网络Dense需要把每张28x28的图片展平成784的一维向量x_train_flat x_train.reshape(-1, 784) x_test_flat x_test.reshape(-1, 784)这里的-1意思是“自动推算这个维度的大小”也就是60000和10000。如果你用的是卷积神经网络CNN输入就要保留空间结构reshape成(数量, 28, 28, 1)最后一维1表示灰度通道。我后面写CNN时会重新处理一次。最省事的办法其实是在模型里加一层Flatten让网络自己展平这样你只需要把数据以28x28的形态喂进去就行。2.3 标签到底要不要one-hotMNIST的标签是0到9的整数不是one-hot编码的向量。Keras里训练分类模型有两种常见的损失函数sparse_categorical_crossentropy和categorical_crossentropy。用sparse_categorical_crossentropy标签可以保持成整数0、1、2、3这种。用categorical_crossentropy需要先把标签转成one-hot向量。比如数字3变成[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]。我的建议是新手直接用sparse_categorical_crossentropy少一步转换代码少一次出错机会。等以后处理多标签分类、理解数据分布的时候再去研究one-hot的细节也不迟。3. 三层全连接网络的完整代码每一层为什么这么设置3.1 一个能直接复制的模型下面这份代码就是完整的全连接网络你可以直接复制跑起来。它非常短但在MNIST上已经很能打了from tensorflow import keras from tensorflow.keras import layers model keras.Sequential([ layers.Input(shape(28, 28)), layers.Flatten(), layers.Dense(128, activationrelu), layers.Dropout(0.2), layers.Dense(10, activationsoftmax) ]) model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] ) model.summary()这里的Flatten层负责把28x28变成784。Dense(128, activationrelu)是中间隐藏层有128个神经元。Dropout(0.2)表示训练时随机丢弃20%的神经元这是一种正则化手段能抑制过拟合。最后一层Dense(10, activationsoftmax)输出一个长度为10的概率向量哪个位置的概率最高就预测为哪个数字。Input(shape(28, 28))显式声明输入形状好处是模型结构一目了然并且在后面保存模型、重新加载的时候更清晰。不写它也能跑但建议写上。3.2 从参数数量理解神经网络在做什么用model.summary()会打印出每一层的参数量。对这个模型来说Flatten层0参数只做形状变换。Dense(128)层784个输入128个输出。参数量是784 * 128 128 100480。那个加的128是偏置项bias每个神经元都有一个。Dropout层0参数。Dense(10)层128个输入10个输出。参数量是128 * 10 10 1290。总和约10万个参数。这些参数就是网络要学的东西。你可以把它理解成10万个可调节的旋钮训练的过程就是不断拧这些旋钮让模型在训练集上的预测越来越准。很多教程会告诉你“神经元越多效果越好”这个说法不严谨。神经元越多参数量越大网络表达能力越强但同时也更容易过拟合。128个神经元的隐藏层对MNIST来说绰绰有余你把张量改成256甚至512也能跑但收益很小训练时间反而变长。我建议新手从一开始就建立“够用就好”的意识而不是无脑堆参数。3.3 激活函数和损失函数的组合逻辑为什么隐藏层用relu而不是sigmoid因为relu在正区间梯度恒为1计算简单能有效缓解梯度消失问题。早期的神经网络喜欢用sigmoid但层数一深反向传播时梯度一路乘下去会变得极小网络就很难训练。relu让梯度保持稳定的流动实际收敛速度快很多。为什么输出层用softmax因为这是一个10分类问题softmax能把10个输出值变成一组和为1的概率。最后做预测时取概率最大的下标作为预测类别。为什么损失函数用sparse_categorical_crossentropy分类问题本质上是让模型输出的概率分布尽量接近真实标签的分布。交叉熵专门衡量两个概率分布的差距真实标签是“确定的”模型预测是“不确定的”两者差距越大损失越大。它比回归任务常用的均方误差更合适。你可以简单理解成交叉熵会对“错误分类”施加更大的惩罚逼着模型快速纠正。这里再多说一句adam优化器是新手最省心的选择。它会自适应地调整每个参数的学习率基本不需要你去调。你直接optimizeradam用就行等以后理解了学习率、动量这些概念再去换SGD、试调别的优化器也不迟。4. 训练与评估看懂loss和准确率比跑出高分数更重要4.1 fit的核心参数模型编译好之后训练就一行代码history model.fit( x_train, y_train, batch_size32, epochs5, validation_split0.2, verbose1 )几个参数逐个说清楚。batch_size32每次给网络32张图计算一次梯度并更新一次参数。这个值太小比如1梯度更新会非常频繁训练极不稳定这个值太大比如512一次更新的计算量又大内存占用也高。32是个经验值在准确率、速度和稳定性三者之间平衡得最好。MNIST总共6万张图32张一批一个epoch就有1875次更新。epochs5表示把整个训练集完整地过5遍。MNIST太简单了全连接网络5个epoch就能到97%以上。第一次跑我建议你设成5先跑通再说。跑完之后再加大epoch观察效果不要迷信“越大越好”。validation_split0.2从训练集里切20%出来当验证集不参与训练。60000张图里会有12000张被分出去实际用来训练的只有48000张。为什么要切验证集因为你想判断模型是不是过拟合。如果训练集准确率99%验证集准确率却只有96%说明模型开始“背答案”了泛化能力不行。MNIST本身有独立的测试集但你需要在训练过程中实时监控模型状态所以保留验证集仍然很有价值。训练过程中verbose1会在终端打印进度条能看到loss和accuracy的变化。第一次跑的时候看到数字一点点往下掉、准确率往上爬那种感觉很直观。4.2 怎么判断模型是不是过拟合了训练结束之后用测试集做最终评估test_loss, test_acc model.evaluate(x_test, y_test, verbose0) print(fTest accuracy: {test_acc:.4f})MNIST测试集上这个三层全连接网络拿到0.975到0.98之间的准确率完全正常。如果要看得更细我建议把训练过程中的loss画出来这一步能帮你建立对训练的直觉import matplotlib.pyplot as plt plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.legend() plt.show()如果两条曲线都平稳下降最后基本持平说明训练状态健康。如果训练loss一直下降但val_loss降到某一点后开始反弹那就是过拟合的典型信号。模型对训练集记得太牢已经失去了泛化能力。解决办法有几种增加Dropout的比例、减小网络容量、增大数据量。在MNIST这个任务上数据增强不是首选后面我会细说。history.history是一个字典里面保存了每个epoch的loss、accuracy、val_loss、val_accuracy。拿它画曲线、看趋势都非常有用。很多人训练完就只看最后一行的准确率完全不看趋势这其实浪费了很多信息。4.3 第一次训练最常见的三个异常第一个异常loss显示nan。出现这个多半是数据没归一化输入数值太大导致梯度爆炸。可以先检查数据预处理再考虑调低学习率。第二个异常准确率一直卡在0.1左右。0.1是随机猜的概率因为MNIST有10个类别瞎猜正好1/10。这种情况基本都是“教会了但没有完全教会”。常见原因是标签和数据没对好、模型结构写错了、或者输入的数据形状不对。我的排查办法是先用一小批数据在训练集上过拟合一两个epoch也就是model.fit(x_train[:64], y_train[:64], epochs20)。如果几个epoch之后准确率冲到1.0说明网络本身没问题问题出在全量数据上如果连小批都学不动那一定是模型结构或者数据出了问题。第三个异常训练时间长得离谱。有些人用老旧的笔记本CPU跑MNIST一个epoch要几分钟。此时可以把batch_size调到128训练速度会明显加快准确率不会有太大损失。MNIST这种小任务没必要为了追求batch_size的所谓“理论最优”去牺牲时间。5. 从98%到99%卷积网络改造与效果对比5.1 CNN的直觉和模型结构全连接网络对图像的处理方式是“看成向量”这其实有点反直觉。一张图片在空间上是有结构的——数字的笔画、边缘、纹理都是相邻像素联合形成的。全连接网络一展平空间信息就丢了。这也是它准确率上限不高的原因。卷积神经网络CNN就是冲着这个问题去的。它用一个小窗口卷积核在图像上滑动每个窗口只处理局部区域然后通过多层堆叠逐步提取更抽象的特征。比如先提取横线、竖线、圆圈再组合成数字的局部形状最后判断是哪个数字。给MNIST写一个非常小的CNNmodel_cnn keras.Sequential([ layers.Input(shape(28, 28, 1)), layers.Conv2D(32, kernel_size(3, 3), activationrelu), layers.MaxPooling2D(pool_size(2, 2)), layers.Conv2D(64, kernel_size(3, 3), activationrelu), layers.MaxPooling2D(pool_size(2, 2)), layers.Flatten(), layers.Dense(128, activationrelu), layers.Dropout(0.3), layers.Dense(10, activationsoftmax) ]) model_cnn.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] )注意CNN的输入要带通道维度。训练数据需要reshape一下x_train_cnn x_train.reshape(-1, 28, 28, 1) x_test_cnn x_test.reshape(-1, 28, 28, 1)第一层卷积是32个3x3的卷积核提取低级特征。经过一次2x2的最大池化后特征图尺寸减半变成14x14。第二层卷积有64个卷积核提取更复杂的特征再池化一次变成7x7。之后展平接全连接层输出分类概率。这里的MaxPooling2D也很关键。它把每个局部区域里最显著的值保留下来丢弃其他信息。好处是缩小特征图尺寸、减少计算量同时增强模型对微小位移的鲁棒性。你不需要精确到像素级的位置只要大致的笔画形状对了就能正确识别。5.2 全连接与CNN的实测对比我自己在CPU机器上跑过这两个模型有一个直观对比模型参数量训练时间(CPU)测试准确率三层全连接约10万每个epoch约15秒左右97.5%-98%小型CNN约2.6万每个epoch约50秒左右99%以上CNN参数量比全连接还少但准确率更高这正是卷积神经网络在图像任务上有优势的证据。因为它通过“局部连接权重共享”大幅减少了需要学习的参数量同时没有丢掉空间信息。你一对比就能体会到结构设计对了效果自然上来。如果你想让CNN再往上冲可以把网络加宽、加深。但对MNIST来说99%以上已经是个很不错的成绩。更高端的技巧比如残差网络、BatchNormalization、学习率调度在这个任务上的边际收益很小不建议入门阶段追求。先把这条流水线跑通理解为什么CNN比全连接更适合图像比刷到99.9%重要得多。5.3 为什么不建议新手急着做数据增强数据增强听起来高大上对图片做随机旋转、平移、缩放让模型看到更多变体提高泛化能力。但在MNIST上这条路收益有限反而容易踩坑。原因很简单MNIST本身是高度标准化过的数字居中、笔画清晰、大小基本一致。真实场景的手写数字才需要更多的鲁棒性处理。如果你给MNIST做随机旋转旋转角度一大会出现非常奇怪的样本——比如把6旋转成9或者把7旋转成其他形状反而混淆了模型。如果你把数字平移得太多可能把原本清晰的数字移出中心区域让模型学到“图像边缘是空白”这种毫无意义的模式。我的建议是先把基本模型跑好等以后去挑战CIFAR-10这类更复杂的数据集时再系统学习数据增强。那时候你会感受到它的威力在MNIST上暂时不必折腾。6. 把模型用到真实手写图片上保存、加载与推理6.1 保存模型与本地数字图片预测训练完模型如果只是跑完测试集就结束了成就感还是会差一些。真正有意思的是把自己随手写的数字扔进模型看它能不能识别出来。这就涉及到模型的保存和加载。保存模型model.save(mnist_model.keras)以后想用直接加载from tensorflow import keras model keras.models.load_model(mnist_model.keras)之后要预测自己的图片需要先把它处理成MNIST的格式。假设你有一张用手机拍的照片或者画图工具画的my_digit.png预测代码大致是这样import numpy as np from PIL import Image img Image.open(my_digit.png).convert(L) img img.resize((28, 28)) arr np.array(img).astype(float32) / 255.0 # 如果是白底黑字需要反转 # arr 1.0 - arr x_pred arr.reshape(1, 28, 28) pred model.predict(x_pred, verbose0) print(np.argmax(pred))如果你用的是CNN模型reshape的时候要带上通道维度x_pred arr.reshape(1, 28, 28, 1)这里说一句很关键的话np.argmax(pred)拿到的是概率最大的那个下标也就是模型认为的“最像哪个数字”。这个下标就是你的预测类别。6.2 黑白反转和居中问题两个最经典的翻车现场第一个翻车现场是黑白反转。MNIST里的数字是白色背景是黑色。你在画图工具里新建一个白底画布用黑色签字笔画个数字保存下来直接丢给模型预测大概率是错的。因为整个图像的明暗关系反过来了模型看到的“前景”和“背景”和训练时正好相反。解决办法很简单预测前把数组做一次反转代码就是arr 1.0 - arr前提是你已经把像素归一化到了0到1。如果图像不是纯黑纯白最好再加一步二值化把浅灰当成背景、深灰当成前景。MNIST的数字实际上基本是两端极值这么处理后效果最稳定。第二个翻车现场是位置和大小。MNIST的训练数据都是在28x28的框里居中、占满的。如果你用手机拍一张真实手写数字原图必然是很大的直接resize成28x28数字会缩成一小块或者歪到角落。模型没见过这样的图预测当然会乱。我自己在跑这一步的时候经常为了预处理花比训练多两倍的时间。后来养成了一个习惯把resize前后的图像可视化出来看一眼确认数字在28x28里是居中、清晰、占大面积的再喂给模型。你可以在预测之前加上import matplotlib.pyplot as plt plt.imshow(arr, cmapgray) plt.show()看一眼你要预测的到底是什么很多“莫名其妙预测错误”的问题当场就能解决。这一点非常实用尤其是以后你要处理真实场景的图像识别预处理永远比模型本身更影响结果。跑完这一整条流程我的体会是MNIST项目最大的价值不是最后那个98%或99%的准确率而是它让新手在一天之内把环境、数据、模型、训练、评估、保存、推理这一整条流水线完整过了一遍。你后面做任何Python机器学习项目无论是换数据集、换模型结构、换框架底层逻辑都是一样的。这套“先跑通、再理解、后优化”的节奏是入门阶段性价比最高的方式。
返回列表