{excerpt:从环境搭建到MNIST手写数字识别完整拆解Keras基础实验的每一步包括深度学习实验中的常见坑、排查思路与报告写作加分点。,content:提到深度学习课程里的实验四也就是Keras基础与简单应用不少同学第一反应是“跑个手写数字识别嘛代码网上到处都是”。但真正动手以后才发现报错一个接一个loss曲线不走、shape不匹配、训练慢得离谱网上搜来的代码在自己环境里根本跑不通。这篇文章就是围绕实验四最常遇到的这些真实状况来写的。\n\n实验四这门实验在整门课里的定位很特殊它是第一次把你从“看理论公式”推到“亲手搭一个能学习的网络”的位置。同时它也是后面图像分类、文本分类等应用实验的基础。无论你用的是TensorFlow自带Keras还是单独安装Keras整体的流程都差不多区别只在导入方式和版本细节上。\n\n我会按“环境准备、基础概念、入门Demo、完整案例、问题排查、心得扩展”这个顺序来写并且把代码和思路放在一起讲。这样不管你之前有没有接触过Keras都能跟着步骤把实验走完还能在报告里写清楚每一步为什么要这样做。\n\n## 1. 实验环境搭建与整体思路拆解\n\n### 1.1 版本选择与安装细节\n\n在实验环境上踩坑是很不明智的因为这一部分完全可以用几分钟避掉。我推荐Python 3.9到3.10之间的版本配合TensorFlow 2.10左右。这里有一个小背景TensorFlow的版本更新很快对操作系统和硬件环境的要求经常变化初学者没必要追求最新。选一个文档多、跑的人多的稳定版本反而能把精力集中在模型本身。CPU机器完全可以跑实验四MNIST每张图片只有28×28像素数据量不大全连接网络在CPU上的训练时间按分钟计算不需要GPU。很多同学为了这个实验折腾显卡驱动最后驱动没装好环境先崩了投入产出比很低。等到后面需要处理大型图像数据和卷积网络时再考虑GPU环境完全来得及。\n\n安装时有两种常见做法。如果直接用TensorFlow自带Keras安装命令很简单\n\nbash\npip install tensorflow2.10.0\n\n\n如果希望Keras源码独立维护也可以再补一条\n\nbash\npip install keras2.10.0\n\n\n安装完成以后建议立刻打印版本确认\n\npython\nimport tensorflow as tf\nprint(tf.__version__)\nprint(tf.keras.__version__)\n\n\n正常输出两个版本号环境就没有大问题了。还有一个我这些年养成的习惯装完环境后先不急着写完整代码而是先用一个最简单的单层网络把全部流程跑一遍。这样做的好处是后面遇到报错时能先排除环境因素把精力集中在模型逻辑上。\n\n 注意不同实验平台的Python版本可能预装不同使用python3 -m pip install可以避免pip指向其他Python版本的问题。\n\n### 1.2 实验目标拆解与任务规划\n\n这一节讲思路。很多同学把实验四做成“复制代码、运行成功、截图提交”最后问起来连自己搭的网络有几层都说不清楚。为了不变成这种状态拿到实验题之后建议先把目标拆成三层。\n\n第一层是能独立写出模型。并不要求你完全记住API但至少要理解Sequential的顺序执行逻辑知道Dense层、Flatten层、激活函数在做什么。第二层是能跑通完整链路。从数据读取、预处理到模型训练、评估再到用模型预测新样本这条链路上每个环节都要能解释。第三层是能分析训练结果。训练日志里的loss是什么含义验证集准确率为什么可能比训练集低绘制出来的曲线代表什么样的学习状态这些问题比最终准确率数字本身更值得琢磨。\n\n拆完之后可以发现实验四真正考察的是“你懂不懂网络是怎么被训练出来的”。代码只是工具概念理解才是得分点。后面所有内容都围绕这三层目标展开建议你按这个框架来安排实验和写报告。\n\n## 2. Keras核心模型基础认知\n\n### 2.1 Sequential顺序模型是怎么一回事\n\n初次接触Keras时最常见的困惑是“Sequential到底是什么”。你可以把Sequential理解成一个有顺序的容器它按顺序保存网络各层数据依次从前向后流动。每一层只需要声明自己的类型和参数Keras会自动计算层与层之间的连接关系不需要你手工处理权重矩阵的维度。\n\n一个最简单的例子\n\npython\nmodel tf.keras.Sequential([\n layers.Dense(128, activationrelu),\n layers.Dense(10, activationsoftmax)\n])\n\n\n这里第一层Dense有128个神经元第二层Dense有10个神经元。数据进入模型后先经过第一层得到128维特征再经过第二层输出10个类别的概率分布。如果做一个生活类比Sequential像一条流水线Dense层是流水线上的工位每个工位对零件做一次变换数据就是不断被加工的零件。你在设定工位时只需要关心输入输出尺寸和加工方式不需要关心设备内部怎么运转。\n\n对课程实验来说90%的网络结构都可以用Sequential表达。如果后面接触到需要多输入、多输出的复杂模型再考虑函数式API或子类化模型也不迟。实验阶段把Sequential吃透已经足够了。\n\n### 2.2 Dense层、激活函数与维度变化\n\nDense层在中文里叫全连接层。所谓全连接就是上一层的每个神经元都和下一层的每个神经元相连。Dense层内部做的事情可以概括成一个公式输出 输入 × 权重矩阵 偏置。训练过程中更新的就是权重矩阵和偏置这些参数。\n\n激活函数的选择有一些基本规律隐藏层优先用ReLU计算简单、收敛快还能缓解梯度消失问题二分类的输出层用Sigmoid把输出压缩到0到1之间表示属于正类的概率多分类的输出层用Softmax把多个输出值转换成一个概率分布所有类别概率之和为1。\n\n很多同学在维度上栽过跟头。假设输入是形状为(28,28)的图像Dense层不接受这种二维结构它要求输入是一维向量。最省事的做法是在模型开头加一个Flatten()层\n\npython\nmodel tf.keras.Sequential([\n layers.Flatten(input_shape(28, 28)),\n layers.Dense(128, activationrelu),\n layers.Dense(10, activationsoftmax)\n])\n\n\nFlatten层的作用是把28×28的二维图像展开成784个值的一维向量。这一步是很多shape报错的根源所以我会在后面问题排查的章节里再专门展开一次。\n\n### 2.3 损失函数与优化器的匹配逻辑\n\n损失函数衡量的是模型预测值和真实值之间的差距训练过程就是反复减小这个差距。选错损失函数模型可能根本学不动。\n\n具体到常见任务多分类用categorical_crossentropy或sparse_categorical_crossentropy二分类用binary_crossentropy回归任务用mse均方误差。这里最容易混的是两个多分类损失函数的区别关键不在模型而在标签编码方式。如果标签是整数例如数字0到9就用sparse_categorical_crossentropy。如果标签已经转成one-hot向量例如数字3对应[0,0,0,1,0,0,0,0,0,0]就用categorical_crossentropy。两种写法效果等价但要和标签形式匹配。\n\n优化器方面入门阶段直接用Adam基本不会错。Adam相当于把动量法和自适应学习率结合起来对学习率的敏感程度较低收敛速度也快。不少资料会建议你手动调整learning_rate但我更推荐先把默认配置跑通再通过对比实验来验证调参的影响。盲目调参很容易让结果都变差然后你根本不知道问题出在哪里。\n\n## 3. 第一个Keras可运行Demo\n\n### 3.1 数据加载与预处理细节\n\n前面知识点比较多我们先不管复杂应用从最朴素的可运行Demo开始。这里我特意选择使用Keras内置的MNIST目的只有一个把流程跑通看清每一步输出。\n\n加载数据的代码非常简短\n\npython\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\n\n(x_train, y_train), (x_test, y_test) keras.datasets.mnist.load_data()\n\n\n加载完立即检查形状\n\npython\nprint(x_train.shape)\nprint(y_train.shape)\n\n\n一般来说会输出(60000, 28, 28)和(60000,)。前者表示60000张28×28的图片后者表示60000个整数标签。看到这个形状你就能推测下一步应该做什么了。\n\n预处理做两件事。第一件是归一化把像素值从0到255缩放到0到1之间\n\npython\nx_train x_train.astype(float32) / 255.0\nx_test x_test.astype(float32) / 255.0\n\n\n这一步为什么重要因为神经网络的权重更新依赖梯度和数值稳定性。当输入数值范围太大时激活函数的梯度可能进入饱和区尤其是Sigmoid这类两端饱和的函数会让训练变得非常慢。归一化之后数值分布更均匀梯度下降的收敛速度会明显改善。第二件是决定数据的形状。如果后续模型第一层是Flatten那么数据保持(60000,28,28)就可以如果模型中不写Flatten就需要手动把数据展平。两种方式最终结果相同但是代码结构不同。\n\n### 3.2 搭建一个简单网络并完成编译\n\n把网络搭出来是实验四的主线任务。给第一次接触的同学一个“一定不会错”的写法\n\npython\nmodel keras.Sequential([\n layers.Dense(64, activationrelu, input_shape(784,)),\n layers.Dense(10, activationsoftmax)\n])\n\n\n这里第一层直接指定input_shape(784,)。这种写法意味着你把数据预先处理成了展平状态所以前面3.1里的reshape不能省略。如果不想手工展平也可以这样\n\npython\nmodel keras.Sequential([\n layers.Flatten(input_shape(28, 28)),\n layers.Dense(64, activationrelu),\n layers.Dense(10, activationsoftmax)\n])\n\n\n两种写法跑出来的结果一样但第一种把维度管理放在数据处理里第二种把维度管理放在模型内部。我个人的建议是初学阶段用第二种因为更不容易出错而且阅读代码的人能直接看出输入图像是28×28。\n\n模型建好之后使用compile方法配置训练方式\n\npython\nmodel.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy])\n\n\n一定要认真对待这里的loss参数。MNIST标签是整数所以sparse_categorical_crossentropy是正确选择。很多同学把categorical_crossentropy和sparse_categorical_crossentropy混用于是报错这是实验中出现频率特别高的错误。\n\n### 3.3 从训练日志读懂模型状态\n\n训练的核心代码只有一行\n\npython\nmodel.fit(x_train, y_train, epochs5, batch_size32, validation_data(x_test, y_test))\n\n\nepochs表示完整遍历训练集的次数batch_size表示每次计算梯度时使用的样本数量。如果训练集有60000条样本batch_size设为32那么一轮要更新1875次参数。这两个参数直接影响训练速度和最终效果也是实验报告里适合做对比实验的内容。\n\n训练过程中日志会输出类似这样的信息\n\n\nEpoch 1/5\n1875/1875 [] - 3s 1ms/step - loss: 0.4000 - accuracy: 0.8900 - val_loss: 0.2200 - val_accuracy: 0.9350\n\n\n每次迭代的打印都很直接loss是训练集上的损失值accuracy是训练集上的准确率val_loss和val_accuracy则来自验证集。正常状态下loss应该一轮比一轮小accuracy一轮比一轮高。如果训练集上loss下降但验证集上val_loss上升就要怀疑过拟合。验证结果也可以用一行代码输出\n\npython\ntest_loss, test_acc model.evaluate(x_test, y_test)\nprint(Test accuracy:, test_acc)\n\n\n到这里你已经完成了“数据预处理、模型搭建、模型训练、模型评估”的完整闭环。这个闭环是实验四核心中的核心。\n\n## 4. 进阶应用手写数字识别完整实现\n\n### 4.1 数据读取与预处理的关键细节\n\nDemo跑通之后就朝向更完整的应用实现迈进。手写数字识别虽然也被称为入门级Case但完整做完并讲清楚细节足够支撑起一份有内容的实验报告。\n\nMNIST数据集的预处理我通常建议固定三步。第一步是归一化和之前的Demo一致将像素值转换到0到1区间。第二步是增加通道维度。MNIST是灰度图但很多后续任务接口会期望三维输入用下面代码把数据变成(60000,28,28,1)\n\npython\nx_train x_train.reshape(-1, 28, 28, 1)\nx_test x_test.reshape(-1, 28, 28, 1)\n\n\n如果你只做全连接网络这一步加不加都可以。但从“为后续实验铺路”的角度看保留这个维度变化能让你后面接卷积层时少改一次代码。第三标签处理。使用sparse_categorical_crossentropy时标签保持整数即可不需要手动做one-hot。这也是为什么实验四多数场景下训练日志不会出现明显异常。\n\n 提示数据集下载偶尔会失败可以事先确认本地有没有缓存的MNIST文件。Keras默认会缓存到用户目录下的.keras/datasets文件夹如果你发现训练前卡在下载进度条多半需要检查网络或手动放置数据文件。\n\n### 4.2 网络结构设计与参数量估算\n\n完整实现里我建议用一个包含两个隐藏层的全连接网络\n\npython\nmodel keras.Sequential([\n layers.Flatten(input_shape(28, 28)),\n layers.Dense(128, activationrelu),\n layers.Dense(64, activationrelu),\n layers.Dense(10, activationsoftmax)\n])\n\n\n中间加两个隐藏层的目的是让特征提取能力更强一些但又不至于复杂到难以分析。Dense层的参数量计算公式是参数 输入维度 × 输出维度 输出维度。第二项是偏置项。\n\n第一层是784×128128100480第二层是128×64648256第三层是64×1010650总参数量约10.9万。这个数字并不大所以才适合在CPU上跑。你可以在代码里调用model.summary()查看每一层的输出形状和参数量这样写报告时就不用靠手算还能顺便检查网络是否有维度错误。\n\n实验报告里如果能写明这个参数量是怎么来的会明显体现出你理解了全连接层的工作原理。不要只贴model.summary()的输出截图要解释一下为什么第一层有100480个参数这种数字背后的推导比结果更重要。\n\n### 4.3 模型编译、训练与可视化\n\n训练这一步用的是固定组合\n\npython\nmodel.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy])\nhistory model.fit(x_train, y_train, epochs10, batch_size32, validation_split0.2)\n\n\nvalidation_split0.2表示把训练集中20%的数据划分出来当验证集训练过程中会在每个epoch结束后计算验证指标。这一步不是必须的但能让你看到训练集和验证集两个维度上的表现。\n\n训练完成后可以用matplotlib把训练过程画出来\n\npython\nimport matplotlib.pyplot as plt\n\nplt.plot(history.history[accuracy], labeltrain_acc)\nplt.plot(history.history[val_accuracy], labelval_acc)\nplt.xlabel(epoch)\nplt.ylabel(accuracy)\nplt.legend()\nplt.show()\n\n\nhistory.history是一个字典保存了每个epoch的loss、accuracy、val_loss、val_accuracy。画图之后你立刻能看出模型是否存在过拟合、收敛速度是否正常。这也是实验报告中最好写、效果最好的一块内容。很多同学喜欢把训练终点accuracy当成全部但训练过程中两条曲线的变化趋势才是区别“会跑代码”和“理解训练”的关键证据。\n\n### 4.4 预测结果分析与混淆矩阵思路\n\n训练完成后怎么判断模型真的“认识”手写数字一种直接方法是取几张测试图片让模型预测。下面这样写\n\npython\nimport numpy as np\n\npredictions model.predict(x_test[:10])\npredicted_classes np.argmax(predictions, axis1)\nprint(predicted_classes)\nprint(y_test[:10])\n\n\npredictions的每一行代表一张图片属于10个类别的概率分布np.argmax取出概率最大的类别作为最终预测。对比predicted_classes和y_test的前10个数值就能直观看到预测是否正确。第一次跑通这个步骤的体验其实很神奇网络训练完以后真的能把手写数字图片转成正确的标签。\n\n如果实验时间充裕我建议进一步计算混淆矩阵\n\npython\nfrom sklearn.metrics import confusion_matrix\nimport seaborn as sns\n\ny_true y_test\ny_pred np.argmax(model.predict(x_test), axis1)\ncm confusion_matrix(y_true, y_pred)\n\n\n混淆矩阵能告诉你模型把哪些数字容易混在一起。比如数字4和9、7和2都是常见易混对。用混淆矩阵定位问题能给你后续调优提供明确方向。实验报告里放一张混淆矩阵热力图比一大段文字描述更有说服力。\n\n## 5. 实验中的常见问题与排查\n\n### 5.1 模型不收敛的常见原因\n\n每年都会有一批同学报告“loss一点没降”或者“准确率一直是随机水平”。模型不收敛故障点十有八九在下面几处。\n\n第一数据没归一化。如果直接把0到255的像素值喂进去loss可能出现锯齿状震荡或者下降极其缓慢。先用除以255的方式归一化绝大多数不收敛问题都会消失。第二隐藏层激活函数选了Sigmoid。网络稍深一点Sigmoid在反向传播时会出现梯度衰减浅层几乎学不到东西。换成ReLU之后梯度可以更顺利地传回浅层。第三学习率不合适。如果用了Adam仍然剧烈震荡可以尝试降低学习率\n\npython\noptimizer keras.optimizers.Adam(learning_rate1e-4)\nmodel.compile(optimizeroptimizer, losssparse_categorical_crossentropy, metrics[accuracy])\n\n\n排查不收敛问题的顺序应该是先检查网络前向传播的形状和标签形式再看数据归一化再看激活函数再看学习率。按这个顺序来基本能把问题定位到具体环节。\n\n### 5.2 过拟合与欠拟合的识别方法\n\n训练结束以后看到准确率不高就先怀疑过拟合其实是很多同学的通病。先学会区分两个概念。\n\n欠拟合的典型表现是训练集和测试集上的准确率都偏低模型根本没学到规律。原因可能是网络太简单、特征不足或者训练轮数太少。处理手段是增加模型容量、延长训练时间。过拟合的典型表现是训练集准确率很高比如99%但测试集准确率明显偏低比如91%两条曲线的差距随着轮数增大而扩大说明模型把训练集里的噪声也记住了。\n\n处理过拟合的手段有很多。最简单的是减少模型复杂度把神经元数量和层数降下来。还可以加Dropout\n\npython\nmodel keras.Sequential([\n layers.Flatten(input_shape(28, 28)),\n layers.Dense(128, activationrelu),\n layers.Dropout(0.3),\n layers.Dense(10, activationsoftmax)\n])\n\n\nDropout会在训练时随机让一部分神经元的输出置零强迫模型不依赖某些特定神经元从而提高泛化能力。Dropout只作用于训练阶段预测时自动恢复所有神经元这一点在答辩或面试时经常被问到。实验四里可能不需要Dropout也能达到足够好的效果但把它加入对比实验是展示理解深度的好方法。\n\n### 5.3 shape不匹配报错的排查流程\n\n实验课上看过最多的报错就是shape类错误。报错信息一般是“logits and labels must have the same first dimension”或者“Shapes ... incompatible”。\n\n遇到这种错误先别慌。找到报错提示所对应的行再从代码里依次打印这些数据的shape模型的输入shape、模型各层输出shape、标签shape。排查顺序应该是输入数据、网络各层、输出层、标签。\n\n一个非常典型的情况标签是一维整数数组形状为(32,)而模型输出层是10个类别概率形状为(32,10)。这时损失函数如果写成了categorical_crossentropy就会报shape不匹配。因为普通categorical版本希望标签也是(32,10)的one-hot形式。解决办法是把损失函数改成sparse_categorical_crossentropy或者提前对标签做one-hot转换。另一种典型情况是输入图像保持二维形状(28,28)但Dense层期望一维输入这时多加一个Flatten层或在数据处理时reshape就能解决。总体上shape错误十有八九是层和标签的格式约定没有对齐。\n\n### 5.4 训练速度慢的优化策略\n\n实验四规模不大但有些同学会使用非常大的epochs和非常小的batch_size于是每轮训练时间越来越长最后等得心烦意乱。其实可以从几个方向优化。\n\n第一先用一小部分数据调试。比如\n\npython\nmodel.fit(x_train[:2000], y_train[:2000], epochs3)\n\n\n确认流程没问题以后再改成全量数据正式训练。第二适当增大batch_size。从32改成64或128每次参数更新消耗的迭代次数会减少训练速度会有肉眼可见的提升。第三减少不必要的验证频率。validation_split会让每个epoch结束后额外计算验证集可以先不加等调试完了再加上。第四对MNIST这种小型数据没必要一开始就用超大网络。128加64个隐藏神经元已经足够再大的网络只是增加训练时间并不能带来明显精度提升。把这些策略都用上之后整个训练过程通常可以在几分钟内完成。\n\n## 6. 实验收获与实践心得\n\n### 6.1 实验中最值得关注的三个维度\n\n带过几轮实验之后我越来越觉得实验四的评分逻辑不在于最终准确率到底多高而在于你如何描述自己的实验过程。至少要看三个维度的变化。\n\n第一个维度是loss曲线。它反映的是优化器到底有没有在有效工作。如果loss波动很大说明学习率可能偏高如果loss下降缓慢说明网络结构或数据预处理可能有隐患。第二个维度是训练集和验证集的准确率差距。训练集高、验证集低说明过拟合两者都低说明欠拟合或数据本身太难。第三个维度是错误样本分布。不要只看整体准确率统计一下模型最容易把哪类样本分错你会发现很多规律比如某些数字类别的样本量不均衡或者某些手写风格在数据集中本来就很少。这三个维度组合起来就能对模型有一个比较完整的判断。\n\n### 6.2 写实验报告的几个加分点\n\n实验报告最大的问题是大量堆代码缺少决策过程。老师真正想看到的是“为什么这么做”。比如你可以解释为什么选择sparse_categorical_crossentropy而不是categorical_crossentropy为什么中间层选择ReLU而不是Sigmoid为什么使用了两个隐藏层而不是三个。\n\n还有两个经常被问到的探索性问题值得写进报告。\n\n第一个问题如果去掉Flatten层模型怎么改答案是预处理时手动把数据reshape成(样本数, 784)同时Dense层的input_shape(784,)要和它匹配。\n\n第二个问题把激活函数从ReLU换成Sigmoid会发生什么在两层以上网络里通常训练速度明显变慢最终准确率也可能下降主要原因是Sigmoid的饱和区域会让梯度更新变得低效。如果你能真的跑一次对比实验把两个版本的结果放到报告里这份实验报告的完成度就会高很多。别小看这种探索它其实是在培养“提出假设、设计实验、验证假设”的研究习惯。\n\n### 6.3 后续可以扩展的方向\n\n实验四做完之后你其实已经摸到深度学习的门槛了。如果想继续深入我建议在同一个数据集上尝试三个扩展方向。\n\n第一个方向是增加模型深度把全连接网络从两层加到三层或四层观察准确率和训练时间的变化。第二个方向是加入卷积层比如用Conv2D、MaxPooling2D构建一个小卷积网络对比它和全连接网络在手写数字识别上的差异。第三个方向是更换数据集把MNIST换成fashion_mnist或者cifar-10考察模型在不同复杂度数据上的表现。\n\n这三个方向都不是实验要求但做过任何一个你对“模型结构、数据特征、训练效果”之间关系的理解都会上一个台阶。说实话实验四本身不可能把深度学习教完它更像一把钥匙帮你打开从“看教程”到“会动手”的那扇门。把环境、数据、模型、训练、评估这条线走通理解每一步为什么要这么做后面不管是做课程设计、参加比赛还是做更复杂的项目你都会感谢这个看似“基础”的实验给你打下的底子。}
阅读完成 · 觉得有帮助?