
这个系列写到第三部分前面两篇已经带大家把 TensorFlow 2.0 的基础姿势摆顺了张量怎么玩、Keras 模型怎么搭、全连接网络怎么训练、分类和回归的小任务怎么跑通。到了这一篇你再停留在“能跑通”就不太够意思了。深度学习真正有杀伤力的地方在于它能处理图像、文本、语音这些高维非结构化数据所以第三部分我打算直接拉你上手两个最重要的实战方向CNN 做图像分类RNN/LSTM 做文本情感分类最后再补上迁移学习和模型落地那点事让模型从“训练完就扔”变成“真的能用”。这篇内容我按零基础能跟上的节奏来写每段代码都会解释“为什么这么写”每个环节都会告诉你“实操时会踩什么坑”。你不需要数学功底多深但最好已经会写基本的 Python并且把 TensorFlow 2.0 装好了。如果你是从第一篇开始一路看完的这篇基本就是给你准备的升级包。1. 第三部分的内容地图学完你能做什么1.1 先盘一下前两篇你该有的底子在动笔之前我建议你先花十分钟自查一下基础。深度学习这东西最怕的就是地基没打牢就往上盖楼。前两篇的内容你可以对照下面这几条快速过一遍会用 TensorFlow 2.0 的tf.keras.Sequential搭建全连接网络并且理解Dense、Dropout、Flatten这几个层在干什么。知道compile里面的loss、optimizer、metrics分别承担什么角色不会一报错就懵。能自己写一个数据加载循环至少用过model.fit训练过几次模型看到 loss 下降不会觉得诡异。理解训练集、验证集、测试集为什么要分开而不是把所有数据一把梭。如果以上四点你都能拍胸脯说“没问题”那第三部分的内容你完全可以放心往下读。如果有些模糊我建议你先回头翻翻前两篇的训练部分因为第三部分所有的内容都是建立在这些基础上的你不需要记什么复杂的数学公式但你要对“训练一个模型”这个过程的整体感觉还在。1.2 三条主线CNN、RNN、迁移学习这一篇我规划了三条主线对应深度学习工程里最常用的三种类型第一CNN 卷积神经网络用来处理图像类数据。你需要理解卷积层为什么比全连接层更适合图像池化层到底在干嘛以及怎么把图像数据喂给模型。第二RNN 循环神经网络重点用 LSTM用来处理文本序列数据。你需要理解“序列”这个概念理解词嵌入Embedding做了什么以及为什么有了全连接层我们还要搞出一套循环结构来。第三迁移学习与模型落地这是从“模型跑通”到“模型能用”的关键一步。现实中你很少会从零训练一个超大网络更多是站在预训练模型的肩膀上做微调然后把训练好的模型保存下来在新数据上做预测。这条路线走完你就可以完成两个相对完整的实践项目一个是手写数字识别图像分类一个是影评情感分类文本分类。两个项目都会涉及完整的数据处理、模型构建、训练评估和预测流程做完之后你对 TensorFlow 的整体使用脉络会清晰很多。2. 图像分类实战手写数字识别完整拆解2.1 数据加载与预处理别急着喂模型图像分类的第一个坑往往不在模型而在数据怎么进模型。我们用的数据集是 Keras 内置的手写数字数据集它在深度学习入门里的地位相当于编程界的“Hello World”。先用代码把它加载出来import tensorflow as tf from tensorflow.keras import layers, models # 加载内置数据集 (x_train, y_train), (x_test, y_test) tf.keras.datasets.mnist.load_data() print(训练集形状:, x_train.shape) # (60000, 28, 28) print(测试集形状:, x_test.shape) # (10000, 28, 28) print(标签示例:, y_train[:10])加载完之后你会发现每张图片是一个 28×28 的二维数组表示灰度图。像素值的范围是 0 到 2550 是纯黑255 是纯白。如果你直接把 0 到 255 的原始数值丢进网络数值范围过大会让梯度更新不稳定收敛变慢。所以预处理最关键的一步就是归一化x_train x_train.astype(float32) / 255.0 x_test x_test.astype(float32) / 255.0 # 增加通道维度(28, 28) - (28, 28, 1) x_train x_train[..., tf.newaxis] x_test x_test[..., tf.newaxis]这里有两个细节值得说。第一为什么要除以 255本质是把像素值压到 0 到 1 之间让网络的输入分布更均匀梯度下降时更容易收敛。第二为什么要加一个通道维度TensorFlow 的卷积层要求输入是四维的格式是(batch_size, height, width, channels)。灰度图只有一个通道所以我们要在最后补一个1代码里的tf.newaxis就是干这个的。如果你忘了这一步后面Conv2D层一定会报维度错误这个错我见新手踩过无数回。2.2 从全连接到卷积卷积层的设计逻辑很多人学 CNN 的时候会问图像不就是像素点吗那我用上一章的Dense层堆一个网络把 28×28 个像素全拼成一维数组不也能做分类吗理论上可以但效果很差。原因在于全连接层把每个像素位置都看成独立的特征完全忽略了像素之间的空间关系。一张图里相邻的像素往往构成边缘、纹理、形状这些局部特征是识别数字的关键。卷积层就是为了捕捉这种局部空间特征而设计的。核心思想是“用一个小窗口扫过整张图”窗口里的数字叫卷积核。比如一个 3×3 的卷积核每次滑动时看的是当前像素周围 3×3 的一个小邻域把这个邻域内的信息提炼成一个新值。窗口扫过整张图就得到了一张新的“特征图”。如果我用多个卷积核就能提取出多种不同的局部特征比如一个核偏向提取横线另一个核偏向提取竖线再深层的卷积核甚至会组合出更抽象的形状。下面是我建议初学者搭的第一个 CNN 架构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.Flatten(), layers.Dense(64, activationrelu), layers.Dense(10, activationsoftmax) ])拆开解释一下。第一层Conv2D(32, (3, 3))意思是使用 32 个 3×3 的卷积核输入单通道 28×28 图像输出 32 张特征图。MaxPooling2D((2, 2))是池化层原理很简单把 2×2 的区域压缩成一个最大值。为什么要这么做因为图像里往往包含很多冗余信息我们只需要知道某个局部有没有出现某种特征不需要知道它的精确位置。池化还能把特征图尺寸减半参数总量大幅下降计算更快也不容易过拟合。Flatten层则是把多维特征图“拉平”成一维向量方便接后面的全连接层。最后两个Dense层做的事和上一章一样64 个神经元先做一次非线性变换然后 10 个神经元输出每个数字类别0 到 9的预测概率softmax保证 10 个概率加起来等于 1。这里我强烈建议你上手后把每一层的输出形状打印出来用model.summary()就能看到。你会看到 28×28 的图经过第一层卷积变成 26×263×3 卷积核会让宽高各减 2经过 2×2 池化变成 13×13经过第二层卷积变 11×11再池化变成 5×5。这个尺寸变化过程理解了CNN 的原理你就算真正懂了。2.3 训练配置与回调函数跑一次像样的训练模型搭好后训练配置其实有一套固定套路model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] )sparse_categorical_crossentropy这个损失函数名字长原理却很直白我们的标签是整数 0 到 9属于多分类问题这个损失函数会直接把整数标签和模型输出的概率分布做交叉熵比较。如果你把标签变成 one-hot 编码比如数字 3 变成[0,0,0,1,0,0,0,0,0,0]那就要改成categorical_crossentropy。这俩怎么选取决于你的标签是整数还是 one-hot 向量二选一别搞混。接下来我们用回调函数来做训练过程的进阶控制callbacks [ tf.keras.callbacks.EarlyStopping(monitorval_loss, patience3, restore_best_weightsTrue), tf.keras.callbacks.ReduceLROnPlateau(monitorval_loss, factor0.5, patience2) ] history model.fit( x_train, y_train, epochs20, batch_size64, validation_data(x_test, y_test), callbackscallbacks )EarlyStopping的意思是如果连续 3 个 epoch 验证集 loss 都没有下降就提前终止训练同时把权重恢复成验证集 loss 最好的那一次。这能帮你省下大量试错时间不至于傻乎乎跑完 20 个 epoch 才发现早就过拟合了。ReduceLROnPlateau则是当 loss 连续 2 个 epoch 不下降时学习率自动减半。深度学习里学习率太高会导致 loss 震荡太低会导致收敛极慢这种动态调整策略算是一种“自动挡”的调参思路。训练结束后你可以用model.evaluate(x_test, y_test)看测试集准确率正常情况能做到 99% 左右。这是因为手写数字数据集本身比较“友好”任务简单CNN 对这种任务已经很成熟。你可以试着在测试集里随机抽几张图打印预测结果感受一下模型实际输出的样子。2.4 第一个小项目扩展练习基础跑通之后我建议你做这样一个扩展练习把数据换成 Keras 内置的 FashionMNIST 数据集也就是服装图片分类。它同样是 28×28 灰度图包含 T 恤、裤子、外套等 10 个类别但识别难度明显比手写数字高。你只需要把加载数据的mnist换成fashion_mnist其他代码几乎不用改跑一遍你就会发现准确率比手写数字低好几个百分点。原因在于服装类别之间的视觉差异更微妙模型需要捕捉更细的特征。这个练习能让你直观体会到“同一个模型在不同任务上的表现差异”也会让你理解为什么深度学习项目里数据质量的地位不亚于模型结构。3. 文本情感分类实战Embedding LSTM 玩转影评3.1 文本数据怎么变成张量三种向量化方案文本分类和图像分类最大的区别在于数据类型。图像天然是数值矩阵文本却是变长的字符串计算器根本不知道“这部电影太棒了”是什么意思。所以第一步必须把文本变成数值这个过程叫文本向量化。我总结一下最常见的三种方案第一种词袋模型Bag of Words。统计每个词在文本中出现的次数形成一个和词典等长的向量。优点是简单缺点是丢失了词序信息“我很喜欢这部电影”和“这部电影很喜欢我”在词袋模型里几乎一样句子含义却完全不同。第二种TF-IDF。它不是简单数词频而是降低那些在所有文本里都出现的常见词比如“的”“是”“很”的权重突出有区分度的词。这个方案在很多传统机器学习任务里很好用但它同样不关心词序。第三种词嵌入Embedding。这也是深度学习做 NLP 的主流方式。它的核心思路是把每个词映射到一个固定长度的密集向量这个向量能捕捉词与词之间的语义关系。比如“高兴”和“开心”在向量空间里距离很近“高兴”和“愤怒”距离就远。和前面两种方案相比词嵌入最大的优势是能利用词序信息因为后面接的 RNN/LSTM 天然就是按顺序处理序列的。在 TensorFlow 里最标准的流程是先做一个文本分词器from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences # 假设 reviews 是原始评论文本列表 tokenizer Tokenizer(num_words10000) # 只保留最常见的 10000 个词 tokenizer.fit_on_texts(reviews) # 文本 - 整数序列 sequences tokenizer.texts_to_sequences(reviews) # 把长度不一的序列统一填充/截断为固定长度 data pad_sequences(sequences, maxlen100)这一步做完每条影评就从一段字符串变成了一个长度 100 的整数数组。数组里每个数字代表一个词在词典中的编号。pad_sequences相当于把所有序列统一成长度 100短的在前面补 0长的截掉超出部分。为什么要固定长度因为模型训练需要 batch 内的数据形状一致你不可能让一个 batch 里同时出现长度 80 和长度 200 的样本。3.2 搭建模型Embedding 层到底做了什么数据准备好之后就可以搭模型了。文本分类的入门级模型长这样model models.Sequential([ layers.Embedding(10000, 128, input_length100), layers.LSTM(64, dropout0.2, recurrent_dropout0.2), layers.Dense(1, activationsigmoid) ])第一层Embedding(10000, 128)参数含义是词典里一共有 10000 个词每个词被映射成一个 128 维的向量。这一层内部本质上是一个可训练的大矩阵形状是(10000, 128)训练过程中这个矩阵会不断更新让每个词的向量逐渐带上语义信息。你可以把它理解成一本不断被修订的“词典”在训练中逐步调整每个词的“含义”。第二层LSTM(64)是循环神经网络的一种升级版。它和全连接层最大的不同在于它会“记忆”前面看到的信息并把它传递给后面的位置。比如“电影虽然开头平淡但结局非常震撼”这句话要理解“震撼”为什么好光看结尾这几个词不够还得联系前面的“虽然”“但”这种转折关系。LSTM 内部的记忆机制就是为了捕捉这种长距离依赖。dropout0.2和recurrent_dropout0.2是两个不同位置的随机丢弃一个作用在输入数据上一个作用在 LSTM 内部传递的记忆状态上都能有效防止过拟合。最后一层Dense(1, activationsigmoid)输出一个 0 到 1 之间的概率因为这里做的是“好评还是差评”的二分类。训练配置要注意损失函数和前面图像分类不一样model.compile( optimizeradam, lossbinary_crossentropy, metrics[accuracy] )二分类问题用binary_crossentropy多分类用sparse_categorical_crossentropy这个对应关系你多写几次就熟了。3.3 训练过程与过拟合控制训练文本模型时要特别注意过拟合因为文本数据往往维度高、样本量小模型很容易“背下”训练集而不是真正理解语义。我建议你的训练过程拆成两步走第一步先用较少 epoch 快速跑通流程比如只训练 5 个 epoch观察训练集和验证集的 loss 变化。如果训练集 loss 一直下降但验证集 loss 在某个 epoch 之后开始回升这就是过拟合的典型信号。第二步加上EarlyStopping和Dropout再重新训练让模型在验证集 loss 开始回升前就停下来。还有一个很实用的技巧不要只盯 accuracy要关注 loss 曲线。准确率在类别不平衡时会有迷惑性比如 95% 的样本都是好评你全预测成好评也有 95% 准确率。但 loss 不会骗人它反映的是模型概率预测和真实标签之间的差距趋势更可信。3.4 文本项目练习题这个项目我建议你自己动手做一遍完整的流程加载 Keras 内置的影评数据集调用方式和 MNIST 一样一行代码就能下载完成分词、填充、建模、训练、评估全流程。进阶练习有两个方向一个是把 LSTM 换成layers.GRU二者结构相似但 GRU 参数更少、训练更快你可以对比一下训练速度和准确率另一个是尝试双向 LSTMBidirectional LSTM它同时从前往后和从后往前读句子能捕捉到更丰富的上下文信息但参数量和训练时间也会翻倍。这种对比实验做多了你对不同结构的适用场景会形成直觉这比背一百条经验帖都有用。4. 迁移学习与模型落地从“会跑”到“能用”4.1 为什么要用预训练模型很多初学者有个误区觉得深度学习就是“从零搭一个网络开始训练”。实际工作里遇到图像识别任务的第一反应不应该是“自己从头训”而是“能不能用现成的预训练模型”。预训练模型就是已经在超大数据集上训练好的网络比如图像分类领域的 VGG、ResNet、EfficientNet 系列。这些模型为了在几十万甚至上百万张图上取得好效果已经学会了大量通用的图像特征提取能力比如识别边缘、纹理、形状、物体部件。迁移学习的思路很简单复用别人已经学好的“特征提取器”只训练最后面的分类部分。这就像你学做菜不用从怎么切菜开始学直接看大厨已经处理好的食材自己只负责掌握火候。在 TensorFlow 里用迁移学习搭图像分类模型代码其实很简洁base_model tf.keras.applications.ResNet50( weightsimagenet, include_topFalse, input_shape(224, 224, 3) ) # 冻结前面的卷积层 base_model.trainable False model models.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.Dense(256, activationrelu), layers.Dropout(0.5), layers.Dense(10, activationsoftmax) ])解释几个关键点。include_topFalse的意思是去掉预训练网络顶部的原始分类层因为原始的分类层是给 1000 类设计的我们要换成自己的分类层。base_model.trainable False是把整个预训练网络冻结让它保持已有参数不动。GlobalAveragePooling2D是一种把整个特征图压缩成一个向量的方式相比Flatten它参数更少也不太容易过拟合是迁移学习里的标配操作。为什么input_shape要用(224, 224, 3)因为预训练模型在训练时有固定的输入尺寸你至少要满足它在数据格式上的基本要求。如果你自己的图片分辨率不是 224×224需要用tf.image.resize预处理一下。这也是迁移学习第一个常见的坑输入尺寸不匹配。训练时有一点要特别提醒既然base_model.trainable False那model.fit时更新参数的量级会小很多但你不能忘记数据增强还是该做就做。比如随机翻转、旋转、缩放这些操作能显著提升小数据集的泛化能力用tf.keras.layers.RandomFlip和RandomRotation就能在模型里直接加非常方便。4.2 模型保存的三种方式别再只会model.save模型训练好之后下一步就是保存和加载。初学者经常用model.save(model.h5)一把梭但实际使用场景不同应该选不同的方式。我整理了一个表格你可以在实操时对照选择保存方式代码示例适用场景注意点完整模型model.save(model.keras)想把模型完完整整给别人用文件较大几百MB到几GB包含网络结构和权重HDF5 格式model.save(model.h5)兼容老代码同样是完整模型但有些自定义层加载时容易出问题仅保存权重model.save_weights(weights.h5)只保存训练好的参数加载时必须先用相同代码重新定义模型结构SavedModelmodel.export(saved_model_dir)部署到服务端或 TensorFlow Serving目录格式包含签名和推理图生产环境首选我推荐你在练习阶段就习惯“权重和结构分离”的思维用代码定义好模型结构用save_weights保存训练参数加载时先重新搭建结构再load_weights。这样做的好处是你想修改网络结构时不会被旧文件绑住也方便你对比不同结构的效果。真正上生产环境再用model.export导出成 SavedModel 格式。加载模型的代码# 重新定义结构和训练时完全一致 model models.Sequential([ layers.Embedding(10000, 128, input_length100), layers.LSTM(64), layers.Dense(1, activationsigmoid) ]) # 加载权重 model.load_weights(weights.h5)这里有个容易踩的坑load_weights之前模型结构必须和保存权重时完全一致包括层的顺序、参数数量、每一层的名称。如果你改了任何一层结构系统会直接报错提示形状不匹配。这个坑在实操里特别常见尤其是在你反复修改模型结构的时候我建议给每个版本的模型起个有意义的文件名别全叫model_final。4.3 完整实践项目从训练脚本到预测脚本最后这个项目我建议你做一件非常“工程化”的事把前面的代码整理成两个文件——训练脚本和预测脚本。训练脚本train.py负责数据加载、模型构建、训练、保存。预测脚本predict.py负责加载保存好的模型对新的图片或文本输出预测结果。这样拆分的意义在于训练和推理是两种完全不同的场景。训练时你要的是批量数据、调整参数、观察曲线推理时你要的是快速拿到单条样本的预测结果不能每次预测都重新训练一遍。预测脚本的核心逻辑很简单# 加载完整模型 model tf.keras.models.load_model(model.keras) # 对新图片做和训练时完全相同的预处理 new_image load_and_preprocess(test.jpg) # 归一化、缩放、加batch维度 pred model.predict(new_image) print(预测类别:, tf.argmax(pred, axis-1).numpy()[0])这里我特别提醒一个新手很容易忽略的细节预测时的预处理必须和训练时完全一致。比如训练时分图像素除以了 255那预测时也必须除以 255训练时做了裁剪和缩放预测时也要按同样方式处理。如果训练时是 224×224 的输入预测时却直接丢进去一张 320×480 的图绝对会报错。这个坑我在带人做项目时几乎每个新手都会踩一次记住预处理和模型绑定不是随意调整的。如果你想让项目更有挑战性可以试着自己搭建一个简单的命令行工具比如运行python predict.py --image test.jpg就能输出预测类别和置信度。这一步不难但能帮你把“训练”和“部署”的边界彻底想清楚。5. 常见问题排查与避坑实录5.1 维度和形状相关的报错这是初学者遇到最多的错误类型典型报错长这样ValueError: Input 0 of layer conv2d is incompatible with the layer: expected min_ndim4, found ndim3.这个报错的意思很简单Conv2D层期望输入是 4 维批量大小、高度、宽度、通道数但你的数据只有 3 维。最常见的原因就是忘了给灰度图补充通道维度或者数据加载时本身就不带 batch 维度。解决办法也很直白在数据上调用x[..., tf.newaxis]增加一个维度或者用tf.expand_dims(x, axis-1)。另一个高频报错是ValueError: Shapes (None, 1) and (None, 10) are incompatible这个报错通常出现在model.compile或model.fit阶段关键信息是“形状不兼容”。它说的是你的损失函数和标签形式不匹配。比如你用了sparse_categorical_crossentropy但标签是 one-hot 编码的 10 维向量或者你用了binary_crossentropy但模型输出维度是 10。排查思路就盯着一个模型最后一层的输出维度 损失函数类型 标签维度三者必须是一个闭环。5.2 训练不收敛的排查思路训练时 loss 不降或直接 NaN这种情况谁都会遇到我建议你按下面的顺序一步步排查第一步检查数据预处理。输入里有没有 NaN 或无穷大有没有忘记归一化导致数值范围过大文本序列里有没有填充值 0 被当成真实的词 ID 参与了计算第二步检查学习率。学习率太大是 loss 直接变成 NaN 的常见原因。你可以把优化器从adam换成显式设置学习率optimizer tf.keras.optimizers.Adam(learning_rate0.0001)第三步检查损失函数和标签是否匹配。多分类问题用了binary_crossentropy二分类又用了categorical_crossentropy这种错误很难一眼看出来因为代码能跑但 loss 表现会很奇怪。第四步如果模型能收敛但准确率上不去先别急着换模型结构可以尝试增加训练轮数、减小 batch size、调整 dropout 比例。深度学习的调参是一个系统性工程不要只改一个参数就期待质变。5.3 环境与版本相关的坑TensorFlow 2.0 这个标题里的版本号其实已经有些年代感了很多读者现在装的可能是更新版本。如果你发现代码里某些 API 报“找不到”或“已弃用”大概率是版本差异。我建议你在项目根目录放一个requirements.txt把关键依赖锁死比如tensorflow2.10,2.16这样团队协作或自己换机器时都能复现出相同结果。深度学习项目里环境不一致导致的“明明代码一样结果却不同”是非常消耗精力的。如果你没有 GPU纯 CPU 跑也不会影响本篇的学习但有几个体验层面的技巧一是尽量用小的数据集和小的模型结构做练习比如把训练样本限制在几千条以内二是用model.fit时设置verbose1至少能看清每个 epoch 的进度三是把不必要的tf.debugging检查和中间张量打印删掉减少不必要的计算。等你真正觉得 CPU 训练已经慢到影响实验效率了再考虑 GPU 环境那会顺很多。5.4 我踩过的一些坑总结写到最后分享几个我带项目时经常提醒别人的经验都是我真实踩过的。第一个是关于model.fit里的validation_split。这个参数可以帮你自动划出一部分训练数据做验证集非常方便但前提是你的数据本身是按随机顺序排列的。如果数据本身是按类别排序的比如前面全是数字 0后面全是数字 1你用validation_split随机取尾部 20% 做验证集那验证集里可能根本看不到某些类别loss 曲线会很诡异。解决办法是先手动打乱数据或者直接用validation_data显式指定验证集。第二个是关于“验证集效果很好但实际使用很拉胯”的情况。这通常意味着过拟合了验证集或者验证集的分布和真实场景不太一样。比如你用手写数字数据集训练模型结果拿到手机拍照的数字上测试效果大概率会明显下降因为手机照片的字体、光照、背景都和训练数据不同。这不是模型代码的问题而是数据分布偏移。面对这种情况最好的办法是尽量收集接近真实场景的数据或者用数据增强模拟真实变化。第三个是关于随机种子。深度学习中很多操作引入随机性包括权重初始化、数据打乱顺序、dropout 等。如果你想让实验结果可复现在代码最开头加上import random import numpy as np import tensorflow as tf random.seed(42) np.random.seed(42) tf.random.set_seed(42)这样设置之后同一份代码在不同时间运行结果能保持基本一致让你调参时能确定改动到底来源于参数还是来源于随机波动。如果你不做这个设置你很可能在某次调参时被随机性欺骗以为参数有效实际上只是运气好。第四个是写代码习惯上的建议多用model.summary()查看模型结构多保存history.history到变量里然后画 loss 曲线。很多人训练完就只看一眼准确率丢掉了最宝贵的诊断数据。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()画完这张图你对“训练得怎么样”的判断会靠谱很多。我个人在实际操作中最深的体会是深度学习的入门并不难难的是把每一个环节都弄透彻。很多人卡住不是因为某个模型结构多难理解而是因为报错看不懂、数据不会预处理、版本不一致这些问题来回反复消耗耐心。希望你学完这篇之后能对着报错信息第一反应是“我也许漏了一个维度”而不是“这个模型对我太难了”。如果你把上面两个项目完整跑通恭喜你你已经具备了独立完成一个小型深度学习任务的底气后面的路就可以跑起来了。