第一次接触深度学习实验课的人很多不是倒在数学公式上而是倒在环境装好了但模型跑不起来和模型跑起来了但准确率很诡异这两堵墙上。实验四Keras基础与简单应用听起来简单但真正做起来几乎每一步都有隐藏的小坑库装错版本、数据没归一化、标签没用one-hot、训练了好几轮损失纹丝不动。这篇文章就把这个实验从头到尾拆开讲一遍不只是给可以直接抄的代码更会解释每一行代码背后的原理以及那些教材不会写、但实际做的时候几乎每个人都会踩的坑。内容适合两类人一类是刚学完深度学习基础理论、准备第一次动手写代码的同学另一类是已经把模型跑通、但想回头弄明白为什么这么写的人。我会按一条完整做实验的路来组织先讲为什么选Keras再讲环境准备然后是MNIST数据集的加载、预处理、模型训练、评估最后把常见问题和延伸玩法一起端上来。1. 为什么实验课选Keras而不是直接上PyTorch1.1 实验课的真正目标先建立模型数据流的直觉实验课选什么框架不是看哪个框架在业界更流行而是看这节课要教会你什么。深度学习入门阶段核心目标是理解数据经过一层层计算变成预测结果然后用损失函数的梯度反向更新参数这个过程。Keras的最大优势就是把这套直觉直接翻译成了近乎白话的代码。你写的每一层就是数据流图上的一个节点模型的编译和训练过程与理论课讲的概念一一对应基本没有中间翻译层。PyTorch当然也是很优秀的框架但它给了太多控制权你需要自己写训练循环、自己管理梯度、自己处理设备调度。这些能力对做研究的人来说是必须的但对第一次接触深度学习的人就是额外认知负担。不是说学不会而是没有必要在第一节课同时学深度学习概念和灵活框架的复杂用法这两件事。Keras则把大部分细节封装掉了让你把注意力集中在数据、网络结构、损失函数、训练结果这些核心概念上。另外实验四的场景是简单应用课题本身就很适合Keras这种高层API。比如最常见的分类任务模型定义可以浓缩成几行from tensorflow import keras model keras.Sequential([ keras.layers.Flatten(input_shape(28, 28)), keras.layers.Dense(128, activationrelu), keras.layers.Dense(10, activationsoftmax) ])就算完全没看过文档也能大概猜出来把28×28的图像拉平接一个128个神经元的全连接层最后输出10个类别的概率。这种可读性是入门阶段非常难得的东西。如果换用需要手写矩阵乘法、手动定义前向传播的写法很容易把新手绕晕在实现细节里反而忘了自己在学什么。1.2 几个框架在实验场景里的真实对比我经常在实验课上遇到同学问到底该学Keras还是PyTorch我的回答一直是先搞清楚你现在处在哪个阶段。下面这个表格是我个人体会的总结不是标准答案但参考价值很直观。框架代码风格学习曲线灵活度最适合的场景Kerastf.keras高层API搭积木式代码量少平缓易上手中低快速验证、教学实验、中小型模型PyTorch显式控制训练循环手写先陡后平需要补基础高研究、复杂网络、自定义训练逻辑底层TensorFlow接口直接用底层算子堆模型陡峭高但繁琐框架开发、特殊情况性能调优还有一点需要说清楚现在使用Keras绝大多数情况是从TensorFlow引入的就是from tensorflow import keras。Keras本身也可以独立安装但实验场景里我建议直接用TensorFlow自带的版本原因很简单少一个版本兼容维度少很多莫名其妙的报错。这种选型思路本身就是工程经验的一部分——能用默认方案跑通就不额外引入变量。2. 环境搭建的隐藏难点版本匹配与第一个能跑的模型2.1 一套稳妥的环境准备步骤实验的第一道坎不是模型是环境。我见过太多人在这一步卡了一晚上最后发现只是某个库版本不对。下面这套步骤是我反复验证过、对新手最稳妥的流程建议按顺序操作。用Conda创建独立环境。不建议直接在base环境里装因为后面还要做其他实验依赖很容易互相打架。conda create -n dl-lab python3.9 conda activate dl-lab安装TensorFlow。没有NVIDIA显卡的机器直接装CPU版本就够了MNIST这种数据集在CPU上训练每个epoch也就几十秒到几分钟完全不是瓶颈。即使有显卡第一次做实验也用CPU版更省心因为少一个CUDA版本匹配的变量。pip install tensorflow验证环境是否能用。import tensorflow as tf from tensorflow import keras print(tf.__version__) print(keras.__version__)跑一个最小的训练循环确认训练流程没有硬件或底层库的问题。import numpy as np model keras.Sequential([ keras.layers.Dense(4, activationrelu), keras.layers.Dense(1) ]) model.compile(optimizeradam, lossmse) x np.random.rand(16, 8).astype(float32) y np.random.rand(16, 1).astype(float32) model.fit(x, y, epochs2)这一步如果没报错说明环境基本通了可以开始正式实验。很多人跳过这个最小测试直接跑完整项目结果环境报错和代码报错混在一起排查起来非常痛苦。2.2 最容易踩的版本坑与解决办法环境装好后最常见的几个坑集中在版本匹配上我把它们列出来方便你遇到了直接对照。第一个坑在网上找到的很多教程是老文章第一行写着import keras。如果你只装了TensorFlow没单独装Keras这一行会直接报ModuleNotFoundError。这不是你的问题是教程版本太老了。正确写法是from tensorflow import keras。如果你为了迁就旧教程单独pip install keras又会引入独立Keras版本与TensorFlow内置版本不一致的隐患建议不要混用。第二个坑numpy版本冲突。TensorFlow的某些版本与较新的numpy不兼容导入时会报类似module compiled using NumPy 1.x, cannot run with NumPy 2.x的错误。这时候不用慌把它降成兼容版本就行比如pip install numpy2。具体版本以报错提示为准核心思路是让numpy大版本和TensorFlow预期一致。第三个坑找不到对应版本的wheel。这通常发生在Python版本太新的时候比如用Python 3.12去装某些旧版TensorFlow找不到预编译包然后报错Could not find a version that satisfies the requirement tensorflow。解决办法要么换Python 3.9/3.10要么升级TensorFlow版本。第四个坑数据集下载卡住。keras.datasets.mnist.load_data()会自动从外网下载数据集网络不太好的时候会一直卡住或者报连接错误。这个问题的解决方法是手动下载数据集放到本地缓存目录~/.keras/datasets/下代码会自动优先读取本地文件。下载文件的命名和目录结构需要注意建议先从出错的机器上找到文件名再按同样名字存放。这些坑里的绝大多数都可以通过看错误栈最后一行快速定位。环境问题一般发生在import阶段代码问题一般发生在某个具体操作调用时。把报错信息复制到搜索框里搜英文原样比搜中文翻译更容易找到有效答案。3. 手写数字识别从数据加载到模型训练的一条完整链路3.1 MNIST数据集的加载与本地缓存处理MNIST就是深度学习的Hello World28×28的灰度图内容是0到9的手写数字训练集6万张测试集1万张。虽然现在看来这个数据集很简单但它非常适合用来理解整个训练流程而且Keras直接内置了加载接口两行就能拿数据。(x_train, y_train), (x_test, y_test) keras.datasets.mnist.load_data()拿到数据后第一件事不是马上塞进模型而是看一眼它的形状和取值范围。这一步看似简单但能帮你过滤掉很大一部分后续问题。print(x_train.shape) # (60000, 28, 28) print(y_train.shape) # (60000,) print(x_train.dtype) # uint8 print(x_train.min(), x_train.max()) # 0 255x_train的dtype是uint8像素值在0到255之间y_train是整数标签从0到9。这些信息都记住预处理部分全都要用到。如果load_data()一直卡住或者报网络错误就回到2.2说的手动下载方案问题是完全可以绕过的。3.2 预处理归一化、形状调整与one-hot编码预处理是整个实验里最容易被轻视、但对结果影响最大的环节。很多同学模型结构抄的一模一样准确率却差一截问题往往就出在这里。第一个操作是归一化。把像素值从0到255变成0到1x_train x_train.astype(float32) / 255.0 x_test x_test.astype(float32) / 255.0为什么要归一化因为神经网络本质上是数值计算系统激活函数对输入范围非常敏感。如果输入是0到255的大数值加权求和的结果很容易落到激活函数的饱和区梯度变得很小训练速度会明显变慢甚至停滞。把数据压缩到0到1附近后数值计算更稳定收敛也更快。第二个操作是调整形状。全连接网络要求输入是一维向量所以要把每张28×28的图像展平成784维向量。可以手动做也可以在模型第一层放Flatten自动做。手动做的代码是x_train x_train.reshape(-1, 784) x_test x_test.reshape(-1, 784)这里的-1表示自动推导维度比如训练集6万张图每张784维reshape结果就是(60000, 784)。如果后面要用卷积网络则要reshape成(60000, 28, 28, 1)最后一个1是通道数灰度图只有一个通道。第三个操作是标签的one-hot编码。把数字7从标量变成向量[0,0,0,0,0,0,0,1,0,0]num_classes 10 y_train keras.utils.to_categorical(y_train, num_classes) y_test keras.utils.to_categorical(y_test, num_classes)为什么需要one-hot因为我接下来用的损失函数是categorical_crossentropy它期望真实标签是概率分布形式。如果保持整数标签也可以用sparse_categorical_crossentropy这种情况下就不用one-hot。实验课一般用前者因为多分类交叉熵的计算方式和概率分布更直观。两种方式都能跑通但不要混用模型输出和标签格式不匹配的时候loss会算出来一个很大的数然后梯度爆炸模型直接废掉。3.3 模型定义、编译和训练每一步都在干什么模型结构继续用1.1里的Sequential定义。三层网络一个Flatten层把图像展平一个128神经元的Dense层用relu激活一个10神经元的输出层用softmax激活。relu在这里的作用是给网络引入非线性让模型能学到更复杂的决策边界输出层的softmax把10个神经元的值变成10个类别的概率所有概率加起来等于1最大值对应的类别就是预测结果。编译这一步很多新手只知道照着抄不知道这三个参数在做什么model.compile( optimizeradam, losscategorical_crossentropy, metrics[accuracy] )optimizeradam是优化器负责根据梯度更新网络参数。Adam自带自适应学习率机制对学习率不敏感入门阶段选它基本不会出大问题省去了手动调学习率的麻烦。losscategorical_crossentropy是损失函数量化模型预测和真实标签之间的差距训练的目标就是让这个值越来越小。metrics[accuracy]是评估指标不影响训练过程只是训练时打印出来方便你看效果。训练循环调用fithistory model.fit( x_train, y_train, batch_size32, epochs5, validation_split0.2 )解释两个关键参数。batch_size32表示每32个样本计算一次梯度并更新一次参数不是把所有6万张图一起算。这样做的原因是显存和内存有限且小批量更新能让训练更稳定。epochs5表示把全部训练数据迭代5遍每遍结束都会在验证集上计算一次损失和准确率。validation_split0.2表示在训练集里拿出最后20%当验证集不参与参数更新只用于观察模型是否过拟合。训练完成后在真正没参与过训练的测试集上做最终评估test_loss, test_acc model.evaluate(x_test, y_test) print(测试集准确率:, test_acc)这里有个特别容易犯的错测试集数据也要做和训练集一样的预处理流程归一化、reshape、one-hot一个都不能少。很多人训练集上表现不错一换测试集准确率暴跌然后怀疑模型结构实际上只是忘记对测试集做归一化了。原始0到255的数据扔进去模型看到的分布完全变了效果不可能好。3.4 预测与可视化让实验结果看得见模型训完除了看准确率数字还可以拿几个测试样本实际预测一下用图直观看看效果。在课程报告里一张手写数字图片预测结果真实标签的对比图比写一百字都有说服力。import numpy as np import matplotlib.pyplot as plt predictions model.predict(x_test[:5]) predicted_classes np.argmax(predictions, axis1) true_classes np.argmax(y_test[:5], axis1) for i in range(5): plt.subplot(1, 5, i 1) plt.imshow(x_test[i].reshape(28, 28), cmapgray) plt.title(fpred{predicted_classes[i]} true{true_classes[i]}) plt.axis(off) plt.show()model.predict返回每个样本每种类别的概率np.argmax取概率最大的位置就是模型预测的类别。这里再次印证了前面one-hot处理的重要性因为y_test已经变成one-hot向量所以要argmax才能还原成数字标签。4. 训练曲线里的学问损失不下降、过拟合与显存问题4.1 训练损失不下降时的系统排查思路实验里最打击人的现象之一训练了好几个epoch损失值纹丝不动准确率一直低迷。很多人的第一反应是网络不够深加层但根据我的经验绝大多数时候问题根本不在网络结构而在数据或超参数设置上。我建议按下面这个顺序排查数据预处理。归一化做了吗测试集和训练集的预处理一致吗标签有没有配对错误这是出现频率最高的问题。网络输出和损失函数是否匹配。多分类用softmax输出加categorical_crossentropy二分类用sigmoid输出加binary_crossentropy回归问题不加激活函数用mse。匹配错了训练根本学不到东西。标签的shape和格式。如果数据是整数标签模型输出是10维概率但loss用的是sparse_categorical_crossentropy一般没问题但如果混用了categorical_crossentropy又不做one-hotloss会直接算错。模型参数有没有在更新。看每个epoch的loss如果完全不变说明梯度可能是零或者太大。可以临时把学习率调大一点测试但不要长期依赖这个调整。是不是数据量太少模型学不到有效规律。MLP在MNIST上有几万张训练图一般不会遇到这种情况但如果自己造了20个样本那模型再复杂也没用。这个排查顺序的核心逻辑是先数据后模型、先简单后复杂。每一层都有对应的检查手段逐层排除比盲目改网络结构高效得多。4.2 用history对象画曲线识别过拟合的入门方法fit函数返回的history对象记录了训练过程中每个epoch的各项指标。不要忽略它这是判断模型是否过拟合的最直接材料。plt.figure(figsize(10, 4)) plt.subplot(1, 2, 1) plt.plot(history.history[accuracy], labeltrain_acc) plt.plot(history.history[val_accuracy], labelval_acc) plt.title(Accuracy) plt.legend() plt.subplot(1, 2, 2) plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.title(Loss) plt.legend() plt.show()判断过拟合的方法也很直观如果训练准确率一直上涨但验证准确率在某轮后停滞甚至下降同时验证损失开始回升那就说明模型开始背训练集了而不是在学通用的规律。应对过拟合的几个常用手段在Dense层后面加Dropout比如keras.layers.Dropout(0.5)训练时随机让一半神经元失活强制网络学到更鲁棒的特征。加L2正则化在Dense层设置kernel_regularizerkeras.regularizers.l2(0.001)让权重保持较小抑制对单个特征的过度依赖。使用EarlyStopping回调监控验证损失连续几个epoch不下降就提前停防止浪费时间继续训练。callbacks [ keras.callbacks.EarlyStopping( monitorval_loss, patience3, restore_best_weightsTrue ) ] model.fit(x_train, y_train, epochs20, validation_split0.2, callbackscallbacks)这些手段在实验四里不一定都需要因为MNIST分类任务相对简单但理解它们是什么、怎么用是后面做更复杂数据集的必要储备。4.3 显存不足与CPU训练太慢的实用方案实验课机器配置往往参差不齐。如果训练时报OOM显存不足最直接的办法是把batch_size调小比如从32调到16甚至8。单次迭代的数据量小了显存占用会明显降下来。有的同学担心batch_size变小会影响最终准确率。在MNIST这种简单任务上影响通常很小完全可以接受。如果机器没有GPU、纯CPU训练体验会慢一些。这时可以把epochs减少或者从训练集里抽样一部分做快速验证比如只取1万张图。代码逻辑完全不用改只是数据量变了先确认整个流程能通再放开跑完整数据。训练日志也不用每行都盯着看损失、准确率、验证损失、验证准确率这几个指标的变化趋势比单个数字重要得多。还有一个很多人忽略的点训练过程的记录。我当时做实验的习惯是把每次修改的结构、参数、epoch对应的准确率记在一个小表格里。这样如果某次改动让结果变差了可以很快回退到之前的配置而不是陷入乱调参数然后忘记原来是什么的困境。5. 实验之外函数式API、模型保存与下一步延伸5.1 从Sequential到函数式API模型结构的表达能力升级Sequential模型适合层的顺序堆叠这是它最直观的优点也是局限无法表达分支、多输入、多输出这些更复杂的结构。以后看实际项目的代码尤其是多任务模型或者带特征共享的结构几乎都是函数式API写的。函数式API的写法区别在于显式连接层的输入输出inputs keras.Input(shape(784,)) x keras.layers.Dense(128, activationrelu)(inputs) outputs keras.layers.Dense(10, activationsoftmax)(x) model keras.Model(inputsinputs, outputsoutputs)看起来只是写法变了本质是把数据流图显式化inputs作为入口经过Dense层处理得到outputs最后用keras.Model组装起来。理解这个模式之后再看多输入场景比如同时输入文本和图像做分类就只是定义两个输入分支再合并逻辑上是顺着的。5.2 模型的保存与加载让实验成果可以复用实验做完模型不能关电脑就没了。保存和加载都很简单model.save(mnist_model.keras)之后随时可以加载回来做预测loaded_model keras.models.load_model(mnist_model.keras) preds loaded_model.predict(x_test[:10])保存模型在实验报告里也很有用可以对比不同网络结构保存下来的模型在同一测试集上的表现。新版推荐用.keras格式老代码里常见的.h5也能用但要确保运行时Keras版本支持。建议训练结束后把测试准确率和历史训练曲线一起存到文件里方便后面复盘。这个习惯在后续更复杂的项目里会反复受益。5.3 下一步延伸从全连接网络到卷积网络实验四做完还有余力的话最自然的下一站是卷积网络。全连接网络把整张图展平成784维向量丢掉了像素的空间位置关系CNN用卷积核在图上滑动天然保留局部结构所以准确率会有明显提升。可以把前面的模型改成CNN结构model keras.Sequential([ keras.layers.Reshape((28, 28, 1), input_shape(784,)), keras.layers.Conv2D(32, (3, 3), activationrelu), keras.layers.MaxPooling2D((2, 2)), keras.layers.Conv2D(64, (3, 3), activationrelu), keras.layers.MaxPooling2D((2, 2)), keras.layers.Flatten(), keras.layers.Dense(128, activationrelu), keras.layers.Dropout(0.5), keras.layers.Dense(10, activationsoftmax) ])注意这里第一层用Reshape把784维向量恢复成28×28×1的图片结构。如果用CNN预处理阶段也不需要手动reshape成784直接保持(60000, 28, 28, 1)就行。在MNIST基础分类上CNN在不做任何数据增强的情况下测试准确率普遍能到99%以上这个提升会直观地让你感受到特征提取层的价值。5.4 用回调函数让训练过程更省心训练大模型的时候不可能一直盯着终端看每个epoch的输出。回调函数是Keras里非常实用的机制下面是我每次训练都会配置的基础组合callbacks [ keras.callbacks.EarlyStopping( monitorval_loss, patience3, restore_best_weightsTrue ), keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.2, patience2, min_lr1e-5 ) ] model.fit(x_train, y_train, epochs20, validation_split0.2, callbackscallbacks)EarlyStopping监控验证损失连续3轮不下降就自动停止省时间也避免过拟合。ReduceLROnPlateau在验证损失不再下降时自动把学习率乘上0.2帮助训练在后期更好地收敛。训练结束打印history看曲线确认训练过程正常再根据结果决定下一步调整方向。我做这个实验时最大的体会是深度学习入门阶段真正难的往往不是网络结构而是数据在进入网络之前有没有处理对。很多看起来玄学的现象排查到最后都是预处理或者维度问题。另一个特别深的体会是实验记录的价值。我把每次改的结构、参数、每个epoch的结果都记在表格里写实验报告、跟同学讨论、复现自己的结果都变得非常快。实验四只是第一步但这套搭模型-训练-看曲线-调整-记录的流程一旦跑顺后面学CNN、学RNN、做实际项目都会顺畅得多。