简介:这是一套面向深度学习者与课程大作业场景的基于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 python=3.9 conda activate dl pip install tensorflow==2.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), activation='relu', input_shape=(28, 28, 1)), tf.keras.layers.MaxPooling2D((2, 2)), tf.keras.layers.Conv2D(64, (3, 3), activation='relu'), tf.keras.layers.MaxPooling2D((2, 2)), tf.keras.layers.Flatten(), tf.keras.layers.Dense(128, activation='relu'), tf.keras.layers.Dropout(0.25), tf.keras.layers.Dense(10, activation='softmax') ]) return model这个结构的选择是有讲究的。第一层卷积用32个3x3卷积核提取低级特征,池化层把空间尺寸减半(28x28 → 14x14),第二层卷积用64个卷积核提取更抽象的特征,池化后再减半(14x14 → 7x7),此时特征图的空间信息已经高度浓缩,展平后接全连接层做分类。Dropout加在全连接层后面,以25%的概率随机丢弃神经元,防止过拟合。
为什么第一层用32个卷积核、第二层用64个?这是经验值——浅层特征比较简单,不需要太多卷积核;深层特征更抽象、更多样,需要更多卷积核去捕捉不同的模式。参数总量大概在几十万量级,CPU训练一个epoch只要十几秒,GPU更是几秒钟的事。
编译时选择Adam优化器,初始学习率0.001,损失函数用categorical_crossentropy,监控指标加一个准确率:
model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='categorical_crossentropy', metrics=['accuracy'] )Adam是自适应学习率优化器,不用手动调整学习率衰减策略,对新手极其友好。SGD配合合适的学习率也不是不行,但调参成本高,大作业场景下买Adam更省心。
训练时我用batch_size=128,验证集从训练集中划分20%:
history = model.fit( x_train, y_train, batch_size=128, epochs=10, validation_split=0.2, verbose=1 )跑完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, verbose=0) print(f'Test 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_size=32, label_mode='binary', shuffle=True ) val_ds = tf.keras.preprocessing.image_dataset_from_directory( 'data/cats_dogs/val', image_size=(224, 224), batch_size=32, label_mode='binary', shuffle=False )image_size统一为224x224是为了配合后续迁移学习要用的预训练模型——ImageNet预训练权重对应的输入尺寸就是224x224。label_mode='binary'返回0/1二分类标签,配合最后一层的Sigmoid输出和binary_crossentropy损失函数使用。
还有一个容易被忽略的操作:AUTOTUNE预取机制。数据读取要是每次从磁盘加载图片再做预处理,训练速度会严重被IO卡脖子,GPU要等CPU把数据送过来。加两行代码就能让数据管线在后台预取下一批数据:
train_ds = train_ds.prefetch(buffer_size=tf.data.AUTOTUNE) val_ds = val_ds.prefetch(buffer_size=tf.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_factor=0.1, width_factor=0.1), ])参数设置上有几个心得。翻转只做水平翻转('horizontal'),因为猫狗照片上下翻转不符合现实逻辑;旋转角度0.2弧度大约11度,角度太大图片边缘会露出空白区域,模型学到的是"旋转后的拼接痕迹"而不是"物体的旋转不变性";缩小放大20%以内比较安全,太剧烈会丢失关键特征。
数据增强层只用在训练集上,验证集和测试集保持原始图片不做增强,否则验证结果就不真实了。image_dataset_from_directory返回的数据集在训练时已经自动做了shuffle,但增强层需要放在模型内部,它才能对每个batch实时生成不同的变换结果。
如果数据量充足或者时间紧张,可以在数据增强和迁移学习之间做一个取舍——预训练模型对增强的依赖比较小,但两者叠加效果才是最好的。实测下来加了增强后,验证集准确率能从92%提升到95%左右,差异非常明显。
3.3 迁移学习:站在预训练模型肩膀上
猫狗分类要拿到好成绩,最有效的手段是迁移学习。简单说就是用一个在大规模数据集(ImageNet,1400万张图片、1000个类别)上预训练好的模型,去掉它的分类头,保留前面所有卷积层作为特征提取器,再接上自己的分类层。
选择MobileNetV2作为基础模型有三个理由:一是模型轻量(参数量只有约350万,VGG16是一个多亿),CPU训练也能承受;二是精度在轻量模型里属于第一梯队;三是TensorFlow生态里一行代码直接调用:
from tensorflow.keras.applications import MobileNetV2 base_model = MobileNetV2( include_top=False, weights='imagenet', input_shape=(224, 224, 3) ) base_model.trainable = False # 先冻结include_top=False表示去掉ImageNet分类的顶层,weights='imagenet'加载预训练权重,trainable=False冻结所有层——前几周训练时这些卷积层的参数不变,模型只训练新增的分类层。整体结构拼装如下:
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, activation='sigmoid') ]) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001), loss='binary_crossentropy', metrics=['accuracy'] )GlobalAveragePooling2D把特征图压缩成一个向量,比直接Flatten的参数更少、更不容易过拟合。最后一层Dense(1)配合Sigmoid输出一个0-1之间的概率值,大于0.5判为狗,小于0.5判为猫。
训练分两个阶段:先冻结预训练权重训练分类头,等loss稳定后再解冻部分高层重新微调。
# 第一阶段:冻结特征提取层,训练新分类层 history = model.fit(train_ds, validation_data=val_ds, epochs=10) # 第二阶段:解冻MobileNetV2最后20层,进行微调 base_model.trainable = True for layer in base_model.layers[:-20]: layer.trainable = False model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.00001), loss='binary_crossentropy', metrics=['accuracy'] ) model.fit(train_ds, validation_data=val_ds, epochs=5)第二阶段的微调学习率一定要调小(我用的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 protobuf==3.20.*中文路径问题在大作业里也特别常见。数据集路径或者项目路径一旦包含中文,image_dataset_from_directory在Windows环境下偶尔会读取失败或者乱码。最稳妥的做法是所有路径都用英文字符命名,彻底规避这个隐患。
4.2 训练不收敛和过拟合的排查方向
训练准确率卡在某个值上不去,或者loss震荡很剧烈,先别急着改模型,按这个顺序排查:学习率是不是太大或太小、数据有没有做归一化、标签和损失函数是否匹配、模型最后一层激活函数对不对。MNIST项目最常见的问题是把Softmax层忘掉或者把交叉熵损失配到Sigmoid输出上,这些都是一眼能看出来的低级错误。
猫狗分类项目里还有一个隐蔽的坑:用image_dataset_from_directory的label_mode='binary'时,标签是[[0.], [1.]]的形状,如果损失函数用了sparse_categorical_crossentropy,会得到非常离谱的loss值。二分类任务就应该用binary_crossentropy加Sigmoid输出,这个组合要对齐,否则准确率会一直徘徊在50%附近。
过拟合的典型特征是训练损失持续下降但验证损失先降后升,验证准确率到某个点就不再涨。应对手段优先级排列:加数据增强 > 加Dropout > 减小模型复杂度 > 早停。我在两个任务里分别用了Dropout和早停,效果立竿见影。早停的Keras回调写法:
early_stopping = tf.keras.callbacks.EarlyStopping( monitor='val_loss', patience=3, restore_best_weights=True )monitor监控验证loss,patience=3表示连续3个epoch没有改善就停止,restore_best_weights=True会把权重回滚到验证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,非常浪费时间。
手写数字识别和猫狗分类这两个项目做完,你就已经完整走了一遍深度学习图像分类的标准流程:数据准备、模型设计、训练调优、评估部署。把源码和实验截图整理好,配合这篇分析报告,大作业的深度和完整度都撑得住。
本文还有配套的精品资源,点击获取