“【自编教材】TensorFlow 2 教材”这个标题,我在第一眼看到时就知道这不是一次简单的资料整理。市面上讲 TensorFlow 的书多到能堆满半面墙,但“自编教材”这四个字,意味着你面对的是一群真实的学生,一条清晰的课程主线,一份经过反复斟酌的学习路径。我连续做过几个学期的机器学习课程,深知拿着一本通识书直接上课有多痛苦:知识点分布和学生的基础对不上,案例不是太玩具就是太跳跃,习题要么抄书要么无从下手。真正管用的方案,恰恰是围绕授课对象,自己动手编一版 TensorFlow 2 教材。
这篇博文不会去教你如何“出版”一本教材,也不会给你一个纯理论的知识大纲。我会分享把 TensorFlow 2 教材从零设计、逐单元编写、到配套实验跑通的完整思路,附带我在课堂上反复验证过的案例、代码和踩坑记录。如果你准备在高校带一门深度学习的入门课,或者在企业内部做 AI 应用培训,这篇内容可以直接参考。
1. 自编教材的初衷:为什么我在人工智能教育中坚持“动手编”
1.1 从“读教材”到“编教材”的一次转变
第一次带深度学习课程时,我直接选了一本行业公认的权威书籍当主教材,结果第一周就出了问题。书里面的数学推导很严谨,但学生连 numpy 的广播机制都还没吃透,更别提理解反向传播的矩阵运算。我在课上花了大把时间补齐前置知识,真正讲 TensorFlow 的地方反而被不断压缩。期中反馈时,不少学生说“书是好书,但不知道哪一章该配哪一次实验”,我才意识到问题不在于学生的态度,而在于教材与学情严重脱节。
后来我换了个思路,把书当作参考,自己按课时重排内容。最初只是想做个精简讲义,结果越写越发现:自编教材的本质不是降低难度,而是重构知识序列。比如讲线性回归,我不再一上来就扔出梯度下降公式,而是先让学生跑通一个最小二乘拟合,再引入 TensorFlow 的自动求导做对比,让他们亲眼看到“手算不方便的地方,框架是怎么替我们完成的”。学生的反馈立刻好了很多,因为每一步都能解释上一节课留下的疑问。
从那次以后,我每学期都会更新自编讲义,逐渐沉淀成一套完整的 TensorFlow 2 教材。它不追求覆盖所有前沿模型,而是追求“学了就能用、用了就能改、改了就能讲清楚”。对老师来说,这才是教材真正的价值。
1.2 教材与市面书的差异:教育才是核心目标
市面上的 TensorFlow 书大多默认读者已有 Python 基础和一定机器学习知识,适合自学者补全盲区,却不适合作为课程主线。因为课堂教学有明确的周次、实验学时和考核目标,教材必须和这些进度一一对应。我在编写之初就给自己定了三条原则:
第一,课程中出现的每一个知识点,都必须和实验任务严格绑定。我不会单独讲“什么是卷积”,而是在 LeNet 手写数字识别的实验里让学生自己观察卷积核提取的特征,再反推原理。
第二,难度曲线要“缓坡爬升”。第一周只写数据管线,第二周只改模型结构,第三周再上训练调参,每节课的新概念不超过三个。学生只要跟住节奏,就不会因为某个步骤卡住而放弃整门课。
第三,容错设计比追求正确更重要。教材里专门加入“错误示例”章节,列出数据类型不匹配、批次维度错乱、梯度爆炸等典型错误,让学生学会看报错信息。这个能力在未来工作中比会调参还值钱。
我见过太多老师手里攥着厚厚一本公认教材,却依然讲不好一节课,原因就是教材与课程目标两张皮。自编教材最大的优势,就是可以随时把学生的问题、课堂上的突发情况写进下一版讲义,让教学内容像软件一样迭代。
2. 教材的核心设计:先理解,再复现,后创新
2.1 三条主线:全流程、基础原理、调试能力
我最终确定的教材结构不是按模型类型排布的,而是按“做项目的流程”排布的。整本教材贯穿三条主线,每一条都对应一类能力。
第一条主线是“从数据到部署的完整闭环”。教材的前几章并不是直接写模型,而是先用大量篇幅处理数据:读取图片、拆分训练集与测试集、数据标准化、构建 tf.data 管道。因为教学实践中最常见的问题不是模型不会写,而是数据格式不对导致训练不了。这条线在后面每章都会重复出现:图像任务用 ImageDataGenerator,文本任务用 TextVectorization,表格任务用 pandas 预处理后转成 Dataset。
第二条主线是“核心原理与框架机制的对照”。教材坚持每个算法模型配一个“底层实现小实验”,比如自己写一个不使用 keras 接口的梯度下降循环,让学生感知框架不过是把数学过程自动化。这个设计很大程度消除了 TensorFlow 被当成“黑魔法”的问题,学生能够明白每一步操作的数学含义,也就不会再犯乱调整超参数的毛病。
第三条主线是“错误诊断与调试”。教材里每章末尾都有一块“常见报错自查表”,按照报错类型、出现场景、解决方案三条列出来。一开始我觉得这有点像说明书,结果发现这恰恰是学生使用频率最高的章节。对学生来说,能解决眼前红字的人,才是真正学会框架的人。
2.2 单元结构:一个单元解决一个真实问题
整本教材共分了 8 个单元,每单元对应 2 到 3 周课时,每个单元围绕一个完整的实战任务展开。我特别避免使用所谓“教学案例”的虚构数据,而是优先选择学生能感知到真实性的数据集。
第一单元,用 TensorFlow 完成线性回归与房价预测,目标不是精度,而是让学生理解张量、变量和自动求导。第二单元,用多层感知机做 MNIST 手写数字分类,引入 Sequential API 和训练流程。第三单元,用 CNN 做猫咪与狗狗的图像分类,引入卷积层、池化层和数据增强。第四单元,用预训练网络做迁移学习,处理一个几百张图片的小型数据集,解决数据不足的痛点。第五单元,用 RNN 或者 LSTM 做简单的文本情感分类,让学生接触序列数据。第六单元,用 AutoMPG 这类回归数据集做完整的特征工程与模型调优。第七单元,把自己训练的模型导出成 SavedModel 并用 TensorFlow Serving 部署,我只要求学生做到本地能跑通接口即可。第八单元,把前面所有内容串成一个小组项目,学生自选数据集,从数据探索到模型部署走完整个流程。
这样的单元结构,目标非常清楚。每学完一个单元,学生都能拿出一个能演示的作品,而不是只收获一堆碎片化知识点。到了期末,学生手上自然有一个完整项目,比任何笔试都能更真实地反映能力。
2.3 练习体系设计:从会到熟再到变
练习并不只是课后习题,我把它分为三个层次。第一层叫“复现练习”,直接把教材代码原样运行一遍,或者做微小改动,目的是让学生熟悉代码路径。第二层叫“改造练习”,我会改掉任务条件,比如换数据集、改网络层数、调整损失函数,要求学生判断哪些逻辑需要跟着变。第三层叫“挑战练习”,只给任务描述和评测指标,不给任何代码框架,学生在没有指引的情况下自行设计实现。
这三个层次对应布鲁姆认知目标的记忆、应用与创造。实际操作中,我每章至少配两种练习:必做的“复现练习”和选做的“改造练习”。“挑战练习”则主要放在期末项目里。通过练习体系分层,每个基础层次的学生都能找到适合自己的进度,而不是被统一的作业难度劝退。
3. 实操环节一:用 TensorFlow 2 搭建你的第一个图像分类 Demo
3.1 第一步:手写数字识别任务
我带学生做的第一个完整图像项目,就是 MNIST 手写数字分类。选择这个任务的原因很现实:数据集小、单张图片 28x28 灰度、类别只有 10 个,即使只用 CPU,训练一轮也只要几十秒。学生可以在课堂时间内看到完整的结果反馈,这是课程设计中一个非常关键的因素。
教材里的代码路径是这样的:先用 keras.datasets 读取数据,再对像素值做归一化处理。几乎所有学生一开始都会忘记归一化,把 0 到 255 的像素直接丢给网络,结果就是 loss 降不下去或者梯度异常。我在教材里重点强调了这一步背后的原因:神经网络的初始权重通常在小数值附近,如果输入数值范围过大,会让激活函数很早进入饱和区,梯度更新失效。
在模型结构上,我用了三层结构:一个 Flatten 层把二维图片展平,两个 Dense 层作为隐藏层,输出层用 softmax。核心代码如下:
import tensorflow as tf from tensorflow import keras # 加载 MNIST 数据 (x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data() # 归一化:把像素值压缩到 [0, 1] x_train = x_train.astype('float32') / 255.0 x_test = x_test.astype('float32') / 255.0 # 构建模型 model = keras.Sequential([ keras.layers.Flatten(input_shape=(28, 28)), keras.layers.Dense(128, activation='relu'), keras.layers.Dense(64, activation='relu'), keras.layers.Dense(10, activation='softmax') ]) # 编译 model.compile( optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) # 训练 history = model.fit( x_train, y_train, batch_size=32, epochs=5, validation_split=0.2 )这段代码最大的价值是“简单到没有地方可以出错”。学生第一次接触模型搭建,不应该被复杂的回调函数、自定义训练循环干扰。先用最简单的接口跑通,再逐层拆开解释,是我一贯的授课节奏。
3.2 关键配置与训练参数拆解
很多学生会照着代码原样运行一遍,然后发现自己什么都没学会,原因就在于不理解那几个关键参数。我在教材里专门花了两页篇幅做参数说明,这里挑重要的讲。
第一个是损失函数的选择。MNIST 的标签是整数 0 到 9,所以用 sparse_categorical_crossentropy。如果当初把标签做成了 one-hot 编码,那就要换成 categorical_crossentropy。这两个损失函数听起来很像,实际上只是对标签编码方式的要求不同。学生在这里如果没搞懂,以后换一个数据集就会频繁报错。
第二个是 batch_size。我常用 32 这个值,不是因为它最优,而是因为它经过大量实践验证,在多数任务上计算效率和稳定性都比较均衡。batch_size 过小,梯度更新频繁,训练震荡大;batch_size 过大,单次迭代时间太长,还可能占用过多显存。教学阶段没必要做精细调优,但一定要让学生理解这个参数的存在意义。
第三个是 epochs。我用 5 是因为 MNIST 数据量小、任务简单,5 轮已经足够看到收敛趋势。我会让学生观察每一轮训练后的准确率变化,引导他们发现:前两轮提升最快,后面逐渐变缓。这个现象引出了学习率、收敛速度的概念,顺势为后面讲模型优化埋下伏笔。
还有一个细节经常被忽略,就是我使用了 validation_split=0.2。这意味着从训练集中再划分出 20% 作为验证集,模型每训练一轮就在验证集上计算一次指标。学生可以从 history 对象里对比训练准确率和验证准确率,这为后续识别过拟合提供了最简单直观的证据。
3.3 模型评估与保存:不只看准确率
训练完模型后,教材让学生完成三个动作:评估测试集、查看预测结果、保存模型。测试集评估很简单,一行 model.evaluate(x_test, y_test) 就能返回 loss 和准确率。但我会额外强调:学生需要手动检查一些预测错误的样本,而不是只看整体数字。比如打印出测试集的真实标签、模型预测标签,再把几张判错的图片画出来,让学生直观感受哪些手写数字容易混淆。这种“人眼看错误”的训练,能帮助学生建立对模型性能的判断力。
保存模型的代码也很简单:
model.save('mnist_model.keras')保存的这个文件既包含模型结构,也包含权重。需要时可以直接通过 keras.models.load_model 加载。我要求每个学生在上交作业时,必须附带保存好的模型文件和推理脚本,这为后续部署实验做了准备。
跑完这个 Demo,整本教材的第一个完整闭环就成立了:准备数据、构建模型、训练、评估、保存、推理。学生带着这个闭环进入下一单元,再逐步替换其中的模块,思维就会跟着训练走。
4. 实操环节二:迁移学习与自定义模型的血泪复盘
4.1 使用预训练模型,别重复造轮子
到了迁移学习单元,数据集变成了一个小的猫狗分类图片集,规模大概只有 2000 张,比这更小的数据集在真实业务里非常常见。如果从头训练一个卷积网络,效果往往很差,因为数据量撑不起深度模型的参数量。迁移学习就是为了解决这种“数据饥饿”问题。
我在教材里让学生使用 keras.applications 中现成的预训练模型,比如 MobileNetV2。这个模型在大型图像数据集上预训练过,已经学会提取通用视觉特征,我们只替换最后的分类层,在小型数据集上微调。基本实现路径如下:
base_model = tf.keras.applications.MobileNetV2( input_shape=(128, 128, 3), include_top=False, weights='imagenet' ) # 冻结预训练模型的参数 base_model.trainable = False model = keras.Sequential([ base_model, keras.layers.GlobalAveragePooling2D(), keras.layers.Dense(1, activation='sigmoid') ]) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='binary_crossentropy', metrics=['accuracy'] )这里与 MNIST 任务有很明显的不同:输入图片是 128x128x3 的彩色图像,标签变成了二分类,输出层激活函数从 softmax 换成了 sigmoid。我在教材里着重比较了这两类任务的差异,因为很多学生把上一章的代码复制过来,只改几个数字就开跑,结果完全行不通。
冻结 base_model 是迁移学习的关键步骤。初次训练时,我们不希望预训练权重被新数据的梯度大幅破坏,所以让基础网络只做特征提取,不参与权重更新。等到训练稳定后,可以再解冻部分层,设置很小学习率微调。这一策略在数据量有限时非常有效。
4.2 自定义层与自定义训练循环:揭开黑盒
很多初学者会误以为 TensorFlow 只能搭积木,其实自定义能力才是进阶关键。我在教材的迁移学习单元之后,专门插入了一节“自定义层与训练循环”,目的在于让学生理解框架内部机制。
自定义层继承自 keras.layers.Layer,需要实现 call 方法。教材里的示例是一个简单的自定义全连接层:
class MyDenseLayer(tf.keras.layers.Layer): def __init__(self, units): super().__init__() self.units = units def build(self, input_shape): self.w = self.add_weight( shape=(input_shape[-1], self.units), initializer='random_normal', trainable=True ) self.b = self.add_weight( shape=(self.units,), initializer='zeros', trainable=True ) def call(self, inputs): return tf.matmul(inputs, self.w) + self.b这段代码把 tf.keras.layers.Dense 的实现从黑盒变成了白盒。学生能看到权重和偏置的定义、矩阵乘法的调用、以及参数如何注册到可训练集合里。有了这个基础,他们就能理解为什么有时候模型加载后报“参数数量不对”之类的错误。
自定义训练循环则更进一步。我用 tf.GradientTape 手动实现了一次梯度更新:
optimizer = tf.keras.optimizers.Adam() for epoch in range(epochs): for batch_x, batch_y in train_dataset: with tf.GradientTape() as tape: predictions = model(batch_x, training=True) loss = loss_fn(batch_y, predictions) gradients = tape.gradient(loss, model.trainable_variables) optimizer.apply_gradients(zip(gradients, model.trainable_variables))这个过程就是训练的本质:前向传播计算损失,反向传播求梯度,优化器更新参数。把它写一遍之后,学生再看 model.fit 会觉得异常通透。这不是要求学生以后都写底层循环,而是让他们在出问题时能定位到训练流程的哪一步。比如 loss 变成 NaN,就会想到是不是梯度爆炸;参数一直不变,就会想到是不是变量没有设置为可训练。
4.3 踩坑记录:数据类型、批次维度、过拟合
这一节是我最想分享给后来者的内容,全部来自真实课堂。第一坑是数据增强放在错误的位置。一开始有学生把 ImageDataGenerator 的增强参数直接用于验证集,导致验证集每轮都变,准确率剧烈波动。正确的做法是只对训练集增强,验证集保持原样。
第二坑是自定义层 build 方法里的 input_shape 理解偏差。keras 传给 build 的参数是一个包含 batch 维度的元组,比如 (None, 64)。如果学生写成 (64,),维度对齐时就会报错或者出现莫名其妙的矩阵乘法错误。我建议他们在写任何自定义层时,先在 build 里打印 input_shape,比猜代码可靠十倍。
第三坑是过拟合被误认为训练失败。在数据量小的任务里,训练准确率可能到 99%,验证准确率却停在 85%,有不少学生认为模型“坏了”。实际上这是典型的过拟合。教材引导学生观察训练与验证损失曲线:两条曲线开始分离的那一刻,就是过拟合信号。对策包括增加数据增强、加 Dropout 层、提前停止训练。课程里要求学生至少尝试两种过拟合缓解手段,并在报告中对比前后曲线变化,这个实验是书面知识给不了的。
迁移学习单元是整个教材中实验报告写得最密集的部分,因为学生在每个环节都可能遇到问题。这些问题恰好是教学的最佳素材。我会把这些真实报错收集起来,不定期更新到教材附录里,让后面的学生少走弯路。
5. 常见问题速查表与备课小工具
5.1 高频问题排查表
整理一份常见问题排查表,对教学帮助极大。以下是我根据多学期数据总结的高频问题,读者可以直接抄进自己的教材里。
| 现象 | 常见原因 | 解决方案 |
|---|---|---|
| 加载图片时维度报错 | 图片不是 RGB 三通道,或尺寸与输入层不匹配 | 统一图像尺寸和通道数,检查数据读取函数参数 |
| 训练时 loss 为 NaN | 学习率过大、数据未归一化、存在缺失值 | 调低学习率,检查数据集是否有异常样本 |
| 验证准确率远低于训练准确率 | 过拟合 | 增加 Dropout、数据增强,或提前停止训练 |
| 迁移学习准确率很低 | 预训练模型被解冻且学习率过大 | 冻结基础网络,只训练顶层,或设置极低学习率微调 |
| SavedModel 加载失败 | 模型与 tf 版本不一致 | 确认序列化与加载时的 TensorFlow 版本一致 |
| 自定义层参数不更新 | 变量没有注册到 trainable_variables | 使用 add_weight 或确保变量属性被 keras 追踪 |
每个学期我都会在课程答疑群里收集报错截图,定期更新这张表。这个过程不需要太高深的技术,却能让教材时刻处于“活”的状态。
5.2 备课与实验的小工具清单
讲完排查,再说说我用来提高备课效率的一些小工具。第一是 Jupyter Notebook,这不是新鲜事物,但真正关键的是模板化。我把每个单元的实验都预先拆成“数据观察、基线模型、改进实验、结果汇总”四段,学生再将自己的操作填进去,作业质量明显提升。
第二是 TensorFlow 官方文档自带的代码示例,我并不会直接照搬,而是会抽出其中与教学主线相关的部分改写。官方示例追求展示功能,教材追求循序渐进,改写时必须调整顺序和上下文。
第三是 Google Colab,这个工具最大价值是零配置,学生不需要在本地折腾 CUDA 和驱动就能跑通实验。同时我会在教材里提供一段检测 TensorFlow 版本和 GPU 是否可用的代码,学生运行一下就能知道自己环境是否就绪。
第四是 Weights & Biases 这类实验跟踪工具,适合期末项目阶段使用。学生把每次实验的参数、loss、准确率记录下来,最后分析哪些改动真正有效。这不只是一个工具教程,更是培养实验素养的手段。
5.3 教案迭代建议
教材写完不等于结束,真正有价值的是迭代。我每学期结课后都会做三件事:先收集学生反馈,标注出“最容易卡住”的点;再对照期末项目的完成情况,反推哪些章节铺垫不足;最后结合技术社区的新变化,补充新的实用内容。
迭代时不要贪多,每次只改一个单元。比如上一学期图像分类讲得吃力,下一学期就把数据增强内容提前,并增加一次小实验专门比较增强前后的效果。这种小幅修正,比推倒重写更可持续。我自己的教材从第一版到现在已经改了六轮,里面有不少内容是最初完全没有的,比如 TensorFlow 2 对 Keras 的整合、SavedModel 部署细节,都是后来才补进去。
这里我想专门提一点:保持教材版本一致很重要。我曾经同时使用多个版本,导致有的学生跟新教材走、有的学生跟旧讲义走,课程整体非常混乱。现在我每次开课都锁定一个版本,并把环境固定下来,最大程度减少因版本差异带来的额外问题。
6. 结语:自编教材的持续演进
这套自编教材走到今天,早就超出了“讲义”的范畴,更像是一套有生命的学习资源体系。我最大的感受是:教学设计和工程技术是两件事。你可能是很优秀的工程师,能写复杂的分布式训练代码,但不一定能设计出学生易懂的起步路径。反过来也一样。真正落地一套课程,需要把“理解学生”和“吃透工具”结合起来,而自编教材恰好是这两者的交汇点。
如果你也想自己动手编一本 TensorFlow 2 教材,我的建议是别从写大纲开始,而是从一次你亲手跑通的实验开始。先把实验代码写顺,再把所有可能卡住学生的地方记录下来,最后按教学节奏组织成章节。这样写出来的教材,每个环节都有依据,学生学起来也会顺畅很多。
最后一句话送给准备入坑的同学:自编教材没有“完成”的那一天,它永远在改。我的第 7 版已经在路上了,希望你也能享受这个反复打磨的过程。