
简介这是一套面向深度学习者与课程大作业场景的基于TensorFlow 2的手写数字识别与猫狗分类识别项目源码涵盖两大经典视觉分类任务。项目共8个文件压缩包仅22KB主体为3个Python脚本负责模型训练、数据加载与分类推理另有Markdown说明文档和PNG网络结构图辅助理解整体轻量紧凑便于快速上手与二次修改。源码采用TensorFlow 2框架实现适用于MNIST手写数字数据集和Dogs vs Cats猫狗数据集可作为深度学习课程设计、期末大作业或入门实践的完整参考。目前已有1043人学习下载说明该项目在实际作业场景中具有一定参考价值。通过学习可直接掌握图像分类任务的数据预处理、模型搭建、训练评估与推理调用流程并能在现有基础上调整网络结构或超参数快速完成属于自己的大作业版本。1. 项目整体思路与环境准备1.1 为什么这两个任务适合作为深度学习入门项目看到基于TensorFlow 2实现手写数字识别和猫狗分类识别这个项目标题第一反应就是——这是入门深度学习最典型、最扎实的两个实战任务也是各大高校深度学习课程大作业里出现频率最高的组合。手写数字识别MNIST是计算机视觉领域的Hello World任务本身足够简单28x28的单通道灰度图一共10个类别0-9图像尺寸小、特征明显、数据干净。但麻雀虽小五脏俱全它能把图像分类的完整链路全部串起来数据加载、预处理、模型构建、训练、评估、可视化。而且MNIST在CPU上几十秒就能跑完一个epoch调试成本极低特别适合初学者理解卷积神经网络CNN的工作原理。猫狗分类则完全是另一档难度。它用的是真实场景下的彩色图像尺寸不统一、背景复杂、目标有姿态变化、类内差异大不同品种的狗长得天差地别而且是二分类问题——这些特性决定了它没办法用处理MNIST那套简单手法硬套。要在这个任务上拿到理想准确率就必须引入数据增强、迁移学习这些实战中绕不开的关键技术。把这两个任务放在同一个项目里恰好覆盖了从构造一个能跑通的模型到在复杂数据集上优化出一个好模型的完整进阶路径这也是它们能成为经典大作业组合的根本原因。1.2 环境版本选择与目录结构设计先说环境。TensorFlow 2.x现在主推的是tf.keras这套高层API写起来比TF 1.x的Session、Graph那套东西省心太多。我在实际配置时用的是Anaconda方便隔离环境。注意TensorFlow 2.10是原生支持Windows GPU环境的最后一个版本之后的版本在Windows上不再提供GPU支持。如果手头有NVIDIA显卡建议直接装2.10配合CUDA 11.2如果是纯CPU跑装新版也没问题。但考虑到大作业的兼容性和稳定性2.10是个稳妥的选择。创建环境的命令很简单conda create -n dl python3.9 conda activate dl pip install tensorflow2.10 pip install matplotlib numpy pandas scikit-learnPython版本最好选3.8-3.9太新的版本偶尔会遇到某些库没有预编译包的情况。macOS用户直接用tensorflow-macos分支Linux和Windows用户用标准包即可。项目目录结构我建议这样组织project/ ├── data/ # 数据集存放目录 │ ├── mnist/ # MNIST数据首次运行自动下载 │ └── cats_dogs/ # 猫狗图片数据 ├── models/ # 保存训练好的模型 ├── train_mnist.py # 手写数字识别训练脚本 ├── train_cats_dogs.py # 猫狗分类训练脚本 ├── predict.py # 推理预测脚本 └── utils.py # 公共工具函数绘图、评估等这样拆分的好处是职责清晰训练脚本只管训练预测脚本只管加载模型和推理公共逻辑抽取到utils里避免重复造轮子。提交大作业的时候老师看目录结构就能明白你的工程素养印象分能拉高不少。2. 手写数字识别从数据到模型的完整链路2.1 数据加载和预处理的几个关键细节MNIST数据集在tf.keras里直接集成了下载接口这段代码大家应该都见过import tensorflow as tf (x_train, y_train), (x_test, y_test) tf.keras.datasets.mnist.load_data() # 归一化到[0, 1]区间 x_train x_train.astype(float32) / 255.0 x_test x_test.astype(float32) / 255.0 # 增加通道维度从 (60000, 28, 28) - (60000, 28, 28, 1) x_train x_train[..., tf.newaxis] x_test x_test[..., tf.newaxis] # One-hot编码标签 y_train tf.keras.utils.to_categorical(y_train, 10) y_test tf.keras.utils.to_categorical(y_test, 10)有几个细节值得展开说。第一是归一化除以255这一步很多人会忽略背后的为什么。原始图像像素值范围是0-255如果不做归一化直接喂给网络在反向传播时梯度会随着层数加深被放大特别是对于未归一化的输入优化过程会非常不稳定模型容易陷入局部最优。归一化后所有特征都在0-1之间训练收敛速度明显加快这是个几乎零成本就能带来稳定提升的操作。第二是增加通道维度。MNIST是单通道灰度图但卷积层的输入格式要求是(batch, height, width, channels)所以要用tf.newaxis在最后一个维度扩展出通道维度否则会直接报错。第三是标签的编码方式。这里用了One-hot编码把数字3变成[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]配合模型最后的Softmax层和categorical_crossentropy损失函数使用。如果不想One-hot也可以让标签保持整数形式把损失函数换成sparse_categorical_crossentropy效果完全一样只是后者内部自动完成编码更省事。2.2 模型结构设计为什么小模型就够用MNIST任务上常犯的毛病是一上来就堆大网络VGG、ResNet什么深往什么来。其实完全没有必要——MNIST图像只有28x28像素目标特征非常简单一个轻量级CNN就能拿到99%以上的准确率。我在项目中采用的是经典LeNet-5思路的简化版def build_mnist_model(): model tf.keras.Sequential([ tf.keras.layers.Conv2D(32, (3, 3), activationrelu, input_shape(28, 28, 1)), tf.keras.layers.MaxPooling2D((2, 2)), tf.keras.layers.Conv2D(64, (3, 3), activationrelu), tf.keras.layers.MaxPooling2D((2, 2)), tf.keras.layers.Flatten(), tf.keras.layers.Dense(128, activationrelu), tf.keras.layers.Dropout(0.25), tf.keras.layers.Dense(10, activationsoftmax) ]) return model这个结构的选择是有讲究的。第一层卷积用32个3x3卷积核提取低级特征池化层把空间尺寸减半28x28 → 14x14第二层卷积用64个卷积核提取更抽象的特征池化后再减半14x14 → 7x7此时特征图的空间信息已经高度浓缩展平后接全连接层做分类。Dropout加在全连接层后面以25%的概率随机丢弃神经元防止过拟合。为什么第一层用32个卷积核、第二层用64个这是经验值——浅层特征比较简单不需要太多卷积核深层特征更抽象、更多样需要更多卷积核去捕捉不同的模式。参数总量大概在几十万量级CPU训练一个epoch只要十几秒GPU更是几秒钟的事。编译时选择Adam优化器初始学习率0.001损失函数用categorical_crossentropy监控指标加一个准确率model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losscategorical_crossentropy, metrics[accuracy] )Adam是自适应学习率优化器不用手动调整学习率衰减策略对新手极其友好。SGD配合合适的学习率也不是不行但调参成本高大作业场景下买Adam更省心。训练时我用batch_size128验证集从训练集中划分20%history model.fit( x_train, y_train, batch_size128, epochs10, validation_split0.2, verbose1 )跑完10个epoch训练准确率一般在99%以上测试集准确率不低于98.5%。如果准确率明显偏低优先检查数据预处理有没有问题再检查学习率是不是设置错了。2.3 评估结果与推理脚本训练完成后评估部分我习惯做三件事测试集上算整体准确率、绘制混淆矩阵、可视化几个预测错误的样本。import numpy as np from sklearn.metrics import classification_report, confusion_matrix test_loss, test_acc model.evaluate(x_test, y_test, verbose0) print(fTest accuracy: {test_acc:.4f}) y_pred np.argmax(model.predict(x_test), axis-1) y_true np.argmax(y_test, axis-1) print(classification_report(y_true, y_pred))混淆矩阵能直观看到模型在哪些数字之间容易搞混常见的坑是4和9、3和8这类形状相近的数字。可视化错误样本同样有价值——你会发现很多错题其实是图片本身就写得模糊甚至人眼都难分辨模型能到这个程度已经是很合理的表现了。模型保存用model.save(models/mnist_model.h5)加载用tf.keras.models.load_model()。推理脚本里读入一张图片归一化、扩维度、前向传播用argmax取最大概率对应的类别编号整体流程很顺畅。这里顺便提一句新版TF推荐保存为SavedModel格式不指定文件后缀即可性能和兼容性更好h5格式也仍然支持看个人习惯。3. 猫狗分类数据增强与迁移学习的实战组合3.1 数据集组织与加载的两种方式猫狗分类常用的数据集是Kaggle的Dogs vs Cats原版有25000张图片12500张猫、12500张狗大作业场景下全量训练耗时太长通常会在其中随机抽取一部分使用。典型的做法是每类各取2000-3000张作为训练集每类各取500张作为验证集。数据在磁盘上按类别建目录存放data/cats_dogs/ ├── train/ │ ├── cats/ # cat.0.jpg, cat.1.jpg ... │ └── dogs/ # dog.0.jpg, dog.1.jpg ... └── val/ ├── cats/ └── dogs/TensorFlow 2提供了非常便利的image_dataset_from_directory接口直接按目录结构自动生成带标签的数据集train_ds tf.keras.preprocessing.image_dataset_from_directory( data/cats_dogs/train, image_size(224, 224), batch_size32, label_modebinary, shuffleTrue ) val_ds tf.keras.preprocessing.image_dataset_from_directory( data/cats_dogs/val, image_size(224, 224), batch_size32, label_modebinary, shuffleFalse )image_size统一为224x224是为了配合后续迁移学习要用的预训练模型——ImageNet预训练权重对应的输入尺寸就是224x224。label_modebinary返回0/1二分类标签配合最后一层的Sigmoid输出和binary_crossentropy损失函数使用。还有一个容易被忽略的操作AUTOTUNE预取机制。数据读取要是每次从磁盘加载图片再做预处理训练速度会严重被IO卡脖子GPU要等CPU把数据送过来。加两行代码就能让数据管线在后台预取下一批数据train_ds train_ds.prefetch(buffer_sizetf.data.AUTOTUNE) val_ds val_ds.prefetch(buffer_sizetf.data.AUTOTUNE)3.2 数据增强的标配参数与适用范围猫狗分类和MNIST最大的区别在于数据复杂度。不像MNIST每个数字都是白底黑字、位置居中、大小统一猫狗照片是千变万化的有的猫在画面角落里、有的狗是斜着拍的、有的图光线特别暗。如果直接拿原始图片去训练模型很容易记住训练集中的背景、位置等无关特征泛化能力很差。数据增强就是对训练图片做随机的几何变换和颜色扰动相当于免费扩充训练集。TensorFlow 2里可以直接用tf.keras.layers层来实现把它作为模型的第一层data_augmentation tf.keras.Sequential([ tf.keras.layers.RandomFlip(horizontal), tf.keras.layers.RandomRotation(0.2), tf.keras.layers.RandomZoom(0.2), tf.keras.layers.RandomTranslation(height_factor0.1, width_factor0.1), ])参数设置上有几个心得。翻转只做水平翻转horizontal因为猫狗照片上下翻转不符合现实逻辑旋转角度0.2弧度大约11度角度太大图片边缘会露出空白区域模型学到的是旋转后的拼接痕迹而不是物体的旋转不变性缩小放大20%以内比较安全太剧烈会丢失关键特征。数据增强层只用在训练集上验证集和测试集保持原始图片不做增强否则验证结果就不真实了。image_dataset_from_directory返回的数据集在训练时已经自动做了shuffle但增强层需要放在模型内部它才能对每个batch实时生成不同的变换结果。如果数据量充足或者时间紧张可以在数据增强和迁移学习之间做一个取舍——预训练模型对增强的依赖比较小但两者叠加效果才是最好的。实测下来加了增强后验证集准确率能从92%提升到95%左右差异非常明显。3.3 迁移学习站在预训练模型肩膀上猫狗分类要拿到好成绩最有效的手段是迁移学习。简单说就是用一个在大规模数据集ImageNet1400万张图片、1000个类别上预训练好的模型去掉它的分类头保留前面所有卷积层作为特征提取器再接上自己的分类层。选择MobileNetV2作为基础模型有三个理由一是模型轻量参数量只有约350万VGG16是一个多亿CPU训练也能承受二是精度在轻量模型里属于第一梯队三是TensorFlow生态里一行代码直接调用from tensorflow.keras.applications import MobileNetV2 base_model MobileNetV2( include_topFalse, weightsimagenet, input_shape(224, 224, 3) ) base_model.trainable False # 先冻结include_topFalse表示去掉ImageNet分类的顶层weightsimagenet加载预训练权重trainableFalse冻结所有层——前几周训练时这些卷积层的参数不变模型只训练新增的分类层。整体结构拼装如下model tf.keras.Sequential([ tf.keras.layers.Rescaling(1./255), # 归一化 data_augmentation, base_model, tf.keras.layers.GlobalAveragePooling2D(), tf.keras.layers.Dropout(0.2), tf.keras.layers.Dense(1, activationsigmoid) ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.0001), lossbinary_crossentropy, metrics[accuracy] )GlobalAveragePooling2D把特征图压缩成一个向量比直接Flatten的参数更少、更不容易过拟合。最后一层Dense(1)配合Sigmoid输出一个0-1之间的概率值大于0.5判为狗小于0.5判为猫。训练分两个阶段先冻结预训练权重训练分类头等loss稳定后再解冻部分高层重新微调。# 第一阶段冻结特征提取层训练新分类层 history model.fit(train_ds, validation_dataval_ds, epochs10) # 第二阶段解冻MobileNetV2最后20层进行微调 base_model.trainable True for layer in base_model.layers[:-20]: layer.trainable False model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.00001), lossbinary_crossentropy, metrics[accuracy] ) model.fit(train_ds, validation_dataval_ds, epochs5)第二阶段的微调学习率一定要调小我用的1e-5因为预训练权重已经学得很好学习率太大会把学到的特征给冲刷掉。全程用2000张训练图片左右最终验证集准确率可以达到95%以上如果把数据扩到全量25000张并做充分的微调99%也不是不可能。4. 训练过程中的常见坑与排查实录4.1 环境配置层面的那些经典报错整个项目实操下来环境问题往往比模型问题更折磨人这里整理几个高频问题。TensorFlow在Windows上GPU支持依赖CUDA和cuDNN版本精确匹配装错版本就报Could not load dynamic library cudnn64_8.dll之类的错。解决办法是用TensorFlow 2.10自带的CUDA版本文档装CUDA 11.2、cuDNN 8.1然后确认C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2\bin在系统PATH里。如果没有NVIDIA显卡或者实在配不好GPU环境CPU版本也完全能跑完这两个任务MNIST很轻松猫狗分类慢一些但半小时内也能见到效果。还有一个高发问题是protobuf版本不兼容。安装TensorFlow时容易顺带装到过新或过旧的protobuf运行时报TypeError: Descriptors cannot not be created directly。解决办法是指定版本pip install protobuf3.20.*中文路径问题在大作业里也特别常见。数据集路径或者项目路径一旦包含中文image_dataset_from_directory在Windows环境下偶尔会读取失败或者乱码。最稳妥的做法是所有路径都用英文字符命名彻底规避这个隐患。4.2 训练不收敛和过拟合的排查方向训练准确率卡在某个值上不去或者loss震荡很剧烈先别急着改模型按这个顺序排查学习率是不是太大或太小、数据有没有做归一化、标签和损失函数是否匹配、模型最后一层激活函数对不对。MNIST项目最常见的问题是把Softmax层忘掉或者把交叉熵损失配到Sigmoid输出上这些都是一眼能看出来的低级错误。猫狗分类项目里还有一个隐蔽的坑用image_dataset_from_directory的label_modebinary时标签是[[0.], [1.]]的形状如果损失函数用了sparse_categorical_crossentropy会得到非常离谱的loss值。二分类任务就应该用binary_crossentropy加Sigmoid输出这个组合要对齐否则准确率会一直徘徊在50%附近。过拟合的典型特征是训练损失持续下降但验证损失先降后升验证准确率到某个点就不再涨。应对手段优先级排列加数据增强 加Dropout 减小模型复杂度 早停。我在两个任务里分别用了Dropout和早停效果立竿见影。早停的Keras回调写法early_stopping tf.keras.callbacks.EarlyStopping( monitorval_loss, patience3, restore_best_weightsTrue )monitor监控验证losspatience3表示连续3个epoch没有改善就停止restore_best_weightsTrue会把权重回滚到验证loss最低的那个epoch非常实用。4.3 几个提升训练体验的小技巧最后分享几个让训练过程更舒服的操作。TensorBoard的Callback记录训练日志浏览器可视化loss曲线和准确率曲线刚开始学习把这两个图看懂很重要——loss下降、acc上升的曲线形态是判断模型健不健康的最直观指标。一行代码接入tensorboard_cb tf.keras.callbacks.TensorBoard(log_dir./logs) model.fit(..., callbacks[tensorboard_cb, early_stopping])训练过程中若发现显存不足优先把batch_size从32降到16或8通常能解决问题、代价只是训练时间略微增加。CPU上跑MobileNetV2确实慢如果实在想加速可以考虑用更轻量的MobileNet替换或者减少训练数据量先验证流程跑通后再对全量数据正式训练。做这个项目最关键的经验是先跑通再调优。先拿一小部分数据比如每类500张、训练2个epoch把整个流程跑通确认没有报错、指标在合理范围内再放开数据量和epoch数做正式训练。否则拿着25000张图上来就跑跑了一小时才发现bug非常浪费时间。手写数字识别和猫狗分类这两个项目做完你就已经完整走了一遍深度学习图像分类的标准流程数据准备、模型设计、训练调优、评估部署。把源码和实验截图整理好配合这篇分析报告大作业的深度和完整度都撑得住。本文还有配套的精品资源点击获取