猫狗识别在高校毕业设计和深度学习入门里一直是很稳的选题。它不像目标检测那样需要处理复杂的边框回归,也不需要像语义分割那样逐像素标注;它只有一个核心任务:把输入图片判断成猫或狗。难度适中,又覆盖了 TensorFlow 的安装、数据集加载、图像预处理、CNN 模型搭建、训练参数调整、模型评估和单图预测这一整套闭环。很多同学卡住的地方其实不是“看不懂网络结构”,而是环境装不上、数据集加载报错、训练完不会保存模型、预测结果和标签对不上。这篇文章面向准备做毕设或刚开始接触 TensorFlow 的读者,从环境准备讲到最终能跑通训练和预测,并把每一步代码拆开解释。
整个项目的技术主线很明确:使用 TensorFlow 2.x 的 Keras API,基于卷积神经网络(CNN)完成猫狗二分类。文章会先解释 CNN 的关键机制,再搭建数据集目录结构,然后用image_dataset_from_directory完成数据加载,接着编写并训练一个小型 CNN 模型,最后给出模型评估、单图预测、报错排查和扩展方向。代码量不大,核心训练脚本保持在 100 行以内,但每一步都不只是一段代码,还包括“为什么要这么写”和“运行后应该看到什么结果”。
1. 理解项目本质:猫狗识别为什么能成为一个完整毕设
1.1 任务定义和技术选型
猫狗二分类的本质是一个监督学习问题。输入是形状为(height, width, channels)的图像张量,输出是一个二分类标签,一般用 0 表示猫、1 表示狗(或者反过来)。在 TensorFlow 中,有两种常见的建模方式:
- 使用
Dense层把图片展开成一维向量,再接全连接层分类,这种方法对图像的空间特征利用非常差。 - 使用卷积层
Conv2D提取局部特征,再通过池化层压缩特征图,最后连接全连接层分类,这就是 CNN 的常规结构。
第二种方式更适合图像任务,因为卷积操作能够通过卷积核捕捉相邻像素之间的局部模式,比如边缘、纹理、弧线,这些特征叠加起来就能区分猫和狗在耳朵、脸型、毛发纹理上的差异。
1.2 为什么 TensorFlow 适合作为毕设项目框架
TensorFlow 是完成猫狗识别最常用的框架之一,在本科毕设中使用有如下几个实际理由:
- Keras 高层 API 封装完善,用
Sequential模型就可以快速搭建 CNN,不用手动实现反向传播。 - 官方和开源社区的猫狗数据集、预训练权重、教程材料非常丰富,遇到问题容易搜到解决方案。
- 支持 CPU 和 GPU 两种训练方式。数据集规模不大时,用 CPU 也能完成训练;有 NVIDIA GPU 的同学可以显著缩短训练时间。
- 模型导出、保存和后续扩展方便,
model.save之后可以直接用 TensorFlow Serving 或转成 TFLite 做移动端推理。
PyTorch 同样是优秀选择,但如果你完全没接触过深度学习框架,TensorFlow 的 Keras API 学习曲线相对平缓,适合在有限时间内跑通毕设主线。
1.3 本文的完整实现流程
整个项目可以拆成 6 个阶段,每个阶段都有明确的交付物:
- 环境准备:安装 TensorFlow 2.x,确认 Python、CUDA、cuDNN 版本匹配。
- 数据集准备:下载猫狗数据集,整理成 train 和 validation 两个目录。
- 数据加载:使用
tf.keras.utils.image_dataset_from_directory读取图片并自动打标签。 - 模型搭建:编写
Sequential结构的 CNN 模型。 - 训练与回调:配置优化器、损失函数、评估指标和 Checkpoint 回调。
- 评估与预测:在验证集上计算准确率,对单张图片进行预测并输出置信度。
学完本文后,你不仅能跑通猫狗识别,还能把这套流程迁移到其他二分类任务上,比如口罩检测、垃圾分类、零件缺陷识别。
2. CNN 核心机制:术语和计算流程必须理解
2.1 图片在神经网络里到底是什么
一张彩色图片在程序里是一个三维数组,shape 是(height, width, channels)。猫狗数据集里的常见尺寸是宽高不统一、通道数为 3(RGB)。比如一张 128x128 的彩色图片,它的 shape 就是(128, 128, 3)。
模型不能直接处理不同尺寸的图片,所以数据加载阶段必须统一所有图片的尺寸。后续代码中统一使用(128, 128, 3)作为输入形状,这个选择不是唯一的,但尺寸越小训练越快,尺寸越大特征越充分。对于猫狗识别,128 或 150 是常见的折中值。
2.2 Conv2D、MaxPooling2D、Flatten、Dense 各负责什么
CNN 模型由几种核心层组合而成,它们的职责完全不同:
Conv2D是卷积层,通过一组可学习的卷积核在图片上滑动,输出多个特征图。每个卷积核负责检测一种局部特征,初始权重是随机的,训练过程中会根据损失函数的梯度不断更新。
MaxPooling2D是最大池化层,作用是在一个小窗口内取最大值,从而降低特征图尺寸,保留最显著的特征信息,同时减少参数量,能够缓解过拟合。
Flatten是把多维特征图展平成一维向量,目的是把卷积层输出的特征“拉直”后交给全连接层。
Dense是全连接层,把前面提取到的特征组合起来做分类决策。最后一层如果是 2 个神经元配合softmax,就是多分类输出;如果是 1 个神经元配合sigmoid,就是二分类输出。
2.3 二分类的损失函数和输出层设计
二分类有两个常用方案:
- 输出层 1 个神经元,激活函数为
sigmoid,损失函数为binary_crossentropy。输出值代表“是狗”的概率,数值越接近 1 越可能是狗。 - 输出层 2 个神经元,激活函数为
softmax,损失函数为sparse_categorical_crossentropy,输出一个长度为 2 的概率分布。
本文采用第一种方案,因为逻辑更直观,而且预测时只需要取一个概率值即可完成判断。需要特别注意的是,sigmoid输出必须配合binary_crossentropy,softmax输出必须配合sparse_categorical_crossentropy或categorical_crossentropy,混用会导致训练曲线异常。
3. 环境准备与数据集组织
3.1 TensorFlow 版本与依赖关系
TensorFlow 2.x 已经迭代了多个版本,不同版本对 Python 和 CUDA 的要求不同。在实际项目中,装环境出错的原因大多是 Python 版本过高或过低、显卡驱动不匹配、CUDA 和 cuDNN 版本对不上。
| 环境项 | 学习环境建议 | 说明 |
|---|---|---|
| Python | 3.9 - 3.11 | 版本过高时部分 TF 版本没有对应 wheel |
| TensorFlow | 2.10 - 2.18 | 以官方 PyPI 支持为准,落地前先确认 |
| CUDA | 视 TensorFlow 版本而定 | 不是必须,CPU 也能跑通本项目 |
| NVIDIA 驱动 | 推荐较新稳定版 | 仅 GPU 训练需要 |
如果你的机器没有 NVIDIA 显卡,可以直接安装 CPU 版 TensorFlow,依然可以完成本项目的全部代码,只是训练时间会变长。
安装命令如下(任选其一):
# CPU 版,适合学生电脑和没有独显的环境 pip install tensorflow # 如果需要指定版本,比如 2.18 pip install tensorflow==2.18.0安装完成后,在 Python 中执行下面代码确认版本:
import tensorflow as tf print(tf.__version__) print(tf.config.list_physical_devices('GPU'))注意:不要只确认能 import,还要看打印出的版本号是否为 2.x。如果输出 1.x,说明环境变量或安装源有问题。
3.2 数据集获取和目录结构设计
猫狗识别常用的数据集是 Kaggle 上的 Dogs vs. Cats。原始数据集包含 25000 张图片,训练毕设时不需要全部使用,可以只取一部分。如果无法访问数据集,也可以先用少量图片搭建完整代码,验证流程没问题后再换成完整数据集。
在项目目录下,建议按下面结构组织数据:
cat_dog_project/ ├── data/ │ ├── train/ │ │ ├── cats/ # 猫图片 │ │ └── dogs/ # 狗图片 │ └── validation/ │ ├── cats/ │ └── dogs/ ├── checkpoints/ # 模型保存目录 ├── train.py # 训练脚本 ├── predict.py # 预测脚本 └── requirements.txt这里要求训练集和验证集都按照“类名作为子目录名”的方式组织,因为image_dataset_from_directory会自动读取子目录名作为标签。需要注意,类名不要用中文,否则后续标签映射会麻烦。
3.3 使用脚本划分训练集
如果下载的数据集是单一目录中混杂所有图片,可以用脚本按比例划分。下面这段代码会把每张图片根据文件名中的 cat 或 dog 关键字复制到目标目录:
import os import shutil import random source_dir = "raw_images" # 原始图片目录 train_dir = "data/train" val_dir = "data/validation" val_ratio = 0.2 for label in ["cat", "dog"]: os.makedirs(os.path.join(train_dir, label), exist_ok=True) os.makedirs(os.path.join(val_dir, label), exist_ok=True) for filename in os.listdir(source_dir): if "cat" in filename: label = "cat" elif "dog" in filename: label = "dog" else: continue src = os.path.join(source_dir, filename) if random.random() < val_ratio: dst = os.path.join(val_dir, label, filename) else: dst = os.path.join(train_dir, label, filename) shutil.copy(src, dst) print("数据集划分完成")这段代码的关键点是按文件名关键字判断类别,真实项目中如果图片文件名是纯数字,就需要从标注文件里获取标签,而不能靠关键字。
4. 数据加载与预处理
4.1 使用 image_dataset_from_directory 加载图片
有了目录结构之后,数据加载可以直接交给 Keras 工具,不需要手动写读取图片的循环。下面代码放在训练脚本中:
import tensorflow as tf IMG_SIZE = (128, 128) BATCH_SIZE = 32 train_ds = tf.keras.utils.image_dataset_from_directory( "data/train", validation_split=0.2, subset="training", seed=123, image_size=IMG_SIZE, batch_size=BATCH_SIZE, label_mode="binary", ) val_ds = tf.keras.utils.image_dataset_from_directory( "data/validation", label_mode="binary", image_size=IMG_SIZE, batch_size=BATCH_SIZE, )这里有几个参数需要解释:
validation_split和subset:在训练目录内部再留一部分做验证集,适合训练数据没有单独划分验证集的情况。seed:固定随机划分顺序,保证多次运行结果可比。label_mode="binary":直接输出 0/1 标签,和一层神经元加sigmoid匹配。image_size:加载时自动缩放图片,不需要自己写 resize。
4.2 归一化处理
图片像素值范围是 0 到 255,把数据缩放到 0 到 1 之间可以让网络更稳定地训练。常见做法是使用Rescaling层:
from tensorflow.keras import layers normalization_layer = layers.Rescaling(1.0 / 255) train_ds = train_ds.map(lambda x, y: (normalization_layer(x), y)) val_ds = val_ds.map(lambda x, y: (normalization_layer(x), y))也可以直接把Rescaling(1.0 / 255)作为模型的第一层。两种方式效果类似,放在模型里更直观,放在数据管道里可以缩短单个 epoch 的预处理时间。
4.3 数据增强的延迟使用策略
数据增强是防止过拟合的常用手段,包括随机翻转、旋转、缩放等。需要注意的是,增强操作只应用在训练集,不能应用于验证集,否则验证集每次评估都看到不同图片,指标就不稳定。可以在模型内部加入增强层,也可以只在实际训练时开启增强。
data_augmentation = tf.keras.Sequential([ layers.RandomFlip("horizontal"), layers.RandomRotation(0.1), layers.RandomZoom(0.1), ])先用cache和prefetch优化数据管道,让数据读取尽量不阻塞模型计算:
train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=tf.data.AUTOTUNE) val_ds = val_ds.cache().prefetch(buffer_size=tf.data.AUTOTUNE)其中prefetch的作用是在 GPU 或 CPU 训练的同时预取下一批数据,减少等待时间。
5. 构建 CNN 模型并理解每层参数
5.1 基础 CNN 模型结构
下面是一个适合猫狗二分类的 CNN 模型,结构简洁,参数量适中,CPU 也能训练:
from tensorflow.keras import layers, models model = models.Sequential([ data_augmentation, layers.Conv2D(32, (3, 3), activation="relu", input_shape=(128, 128, 3)), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation="relu"), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activation="relu"), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(128, activation="relu"), layers.Dropout(0.5), layers.Dense(1, activation="sigmoid"), ])模型前半部分是卷积特征提取,后半部分是全连接分类。input_shape必须和image_size以及通道数保持一致。如果前面使用Rescaling在模型中,就要把它放在input_shape之后的第一层。
5.2 编译模型时的关键选择
model.compile( optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"], )三个配置项都很关键:
optimizer="adam":Adam 优化器对学习率的敏感度较低,通用性强,适合大多数图像分类任务。loss="binary_crossentropy":损失函数必须和最后一层的激活函数匹配。metrics=["accuracy"]:用于观察每一轮训练和验证的准确率。
5.3 打印模型摘要并确认参数量
model.summary()运行后会看到每一层的输出 shape 和参数量。最后一层全连接之前的特征图维度取决于图片尺寸和池化次数,这是判断网络结构是否合理的重要依据。如果参数远超预期,说明深层卷积核过多或全连接层过大,需要调整。
6. 模型训练与回调配置
6.1 训练脚本主体
训练部分的核心代码如下:
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping checkpoint = ModelCheckpoint( "checkpoints/best_model.keras", monitor="val_accuracy", save_best_only=True, verbose=1, ) early_stop = EarlyStopping( monitor="val_loss", patience=5, restore_best_weights=True, ) history = model.fit( train_ds, validation_data=val_ds, epochs=30, callbacks=[checkpoint, early_stop], )ModelCheckpoint会根据验证集准确率自动保存最优模型,训练中断或过拟合后也不用担心丢失最佳权重。EarlyStopping在验证损失连续多轮不下降时提前结束训练,防止浪费时间。
6.2 训练日志解读
正常训练时,输出大致如下:
Epoch 1/30 125/125 [==============================] - 12s 95ms/step - loss: 0.6932 - accuracy: 0.5130 - val_loss: 0.6820 - val_accuracy: 0.5550 Epoch 2/30 125/125 [==============================] - 11s 88ms/step - loss: 0.6765 - accuracy: 0.5720 - val_loss: 0.6640 - val_accuracy: 0.6020前几轮准确率接近 0.5 是正常现象,因为初始权重是随机的。随着 epoch 增加,训练准确率和验证准确率都应该逐步上升。如果训练准确率长时间不动,很可能需要调整学习率,或者数据增强过强导致模型难以拟合。
6.3 绘制训练曲线
训练结束后,用 Matplotlib 绘制准确率和损失曲线,可以直观判断是否过拟合:
import matplotlib.pyplot as plt acc = history.history["accuracy"] val_acc = history.history["val_accuracy"] loss = history.history["loss"] val_loss = history.history["val_loss"] plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(acc, label="train acc") plt.plot(val_acc, label="val acc") plt.legend() plt.title("Accuracy") plt.subplot(1, 2, 2) plt.plot(loss, label="train loss") plt.plot(val_loss, label="val loss") plt.legend() plt.title("Loss") plt.show()如果训练准确率很高而验证准确率偏低,说明过拟合;如果两边都很低,说明模型欠拟合,需要增加模型容量或减少数据增强强度。
7. 模型评估与单图预测
7.1 在验证集上评估模型
训练完成后,使用evaluate得到模型在验证集上的最终表现:
val_loss, val_acc = model.evaluate(val_ds) print(f"验证集损失: {val_loss:.4f}") print(f"验证集准确率: {val_acc:.4f}")7.2 对单张图片进行预测
单图预测是毕设答辩里最常演示的部分。核心代码:
import numpy as np from tensorflow.keras.preprocessing import image def predict_image(img_path): img = image.load_img(img_path, target_size=(128, 128)) img_array = image.img_to_array(img) img_array = np.expand_dims(img_array, axis=0) img_array = img_array / 255.0 pred = model.predict(img_array, verbose=0)[0][0] class_name = "狗" if pred > 0.5 else "猫" print(f"图片: {img_path}") print(f"预测分数(狗的概率): {pred:.4f}") print(f"预测结果: {class_name}")这段代码中有几个常见出错点:
- 训练时用了
Rescaling(1/255),预测时也要手动做同样的归一化,否则输入分布不一致,结果会异常。 target_size必须和训练时的image_size一致。np.expand_dims是为了把单张图片变成 shape 为(1, 128, 128, 3)的批次数据,因为模型要求输入带 batch 维度。
7.3 保存和加载模型
ModelCheckpoint已经保存了最佳模型,也可以单独保存整个模型:
model.save("cat_dog_model.keras")之后在预测脚本里加载:
loaded_model = tf.keras.models.load_model("cat_dog_model.keras")Keras 3 推荐使用.keras后缀,使用旧版.h5在 TensorFlow 2.18 中依然可用,但新项目建议统一用.keras格式。
8. 常见问题与排查链路
8.1 显存不足或训练崩溃
现象:启动训练后程序报错,提示CUDA_ERROR_OUT_OF_MEMORY或Resource exhausted。
可能原因:批量大小太大、图片尺寸太大、同时打开了多个模型。
处理方式:
- 把
BATCH_SIZE从 32 降到 16 或 8。 - 把
IMG_SIZE从 256 降到 128。 - 关闭其他占用 GPU 显存的程序。
nvidia-smi用上述命令确认 GPU 占用情况。
8.2 训练准确率一直不上升
现象:多轮训练后准确率停留在 0.5 附近。
排查顺序:
- 检查标签是否错乱。
image_dataset_from_directory的标签顺序是按字母序排列的,cat 在前为 0,dog 在后为 1。 - 检查归一化是否重复或遗漏。如果加载数据时已经归一化,模型中又添加了
Rescaling层,相当于输入被除了两次 255。 - 检查损失函数是否和输出层匹配。
sigmoid + binary_crossentropy搭配正确,其他组合会出现训练异常。 - 减少数据增强强度,过强的翻转和缩放可能让模型难以学习。
8.3 预测结果总是某一类
现象:无论输入什么图片,预测结果都是猫或都是狗。
排查重点:
- 确认预测脚本中的归一化方式和训练时一致。
- 确认加载的是
ModelCheckpoint保存的最优模型,而不是最后一个 epoch 的权重。 - 打印
pred原始值。如果一直接近 0 或一直接近 1,大概率是模型或数据问题;如果接近 0.5,说明模型没有学到有效特征。
8.4 训练和验证准确率差异巨大
现象:训练准确率 95% 以上,验证准确率只有 70% 左右。
这是过拟合。处理思路:
- 降低全连接层参数或添加更多
Dropout。 - 增加
RandomFlip、RandomRotation等数据增强。 - 缩小训练轮次,让
EarlyStopping发挥作用。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 训练卡在 50% 准确率 | 归一化重复或损失函数不匹配 | 查看数据管道和模型输入 | 统一归一化方式,检查 loss 与激活函数 |
| 验证准确率低 | 数据量不足或过拟合 | 对比 train/val acc | 增加增强、Dropout、早停 |
| 预测全为同一类 | 预测预处理不一致或加载错误权重 | 打印预测分数 | 统一 target_size 和归一化 |
| 显存不足 | batch 或图片尺寸过大 | nvidia-smi 查看显存 | 调小 batch 或图片尺寸 |
9. 提升识别效果的方向与毕设扩展建议
9.1 数据增强要合理,不能盲目加
数据增强是提升模型泛化能力的有效手段,但增强强度过大会导致模型无法学习关键特征。对猫狗识别来说,水平翻转比较安全,垂直翻转则不适合,因为真实照片中猫狗很少倒立出现。旋转角度也可以控制在 10 度以内。
9.2 迁移学习通常是提分最快的方案
从头训练一个小型 CNN,最终准确率可能达到 85% 左右,如果追求更高的效果,使用预训练模型是性价比更高的选择。在 TensorFlow 中用迁移学习的思路是:使用预训练模型(如 MobileNetV2、ResNet50)的卷积基,冻结前置层的权重,只训练全连接分类器。
from tensorflow.keras.applications import MobileNetV2 base_model = MobileNetV2( input_shape=(128, 128, 3), include_top=False, weights="imagenet", ) base_model.trainable = False model = models.Sequential([ layers.Rescaling(1.0 / 255), base_model, layers.GlobalAveragePooling2D(), layers.Dense(128, activation="relu"), layers.Dropout(0.5), layers.Dense(1, activation="sigmoid"), ])迁移学习的思路同样适用于毕设论文中的“基于预训练模型的猫狗识别”选题。
9.3 特征可视化和 Grad-CAM 是论文加分项
如果毕设需要展示“模型到底看哪里”,可以在模型中输出最后一层卷积层,然后结合梯度计算 Grad-CAM 热力图。这部分能在论文中大幅提升工作量展示效果,但要注意把每个步骤讲清楚,避免只贴图不解释。
9.4 扩展方向和使用场景
一个猫狗二分类项目可以扩展成多种形式:
- 使用 OpenCV 读取摄像头画面,实时判断画面中的猫狗。
- 把模型转为 TFLite,部署到 Android 或嵌入式设备。
- 把二分类扩展到多分类,比如猫、狗、鸟、兔子四类,只需要改数据集目录结构和最后一层神经元数量。
- 在前端做一个上传图片的 Web 页面,后端调用 TensorFlow 推理,形成完整系统演示。
对于毕设来说,从“能跑通”到“能讲明白”还有一段距离。建议做以下几件事:
- 记录每一次训练的超参数和结果,形成实验记录表。
- 比较基础 CNN 和迁移学习两种方案在相同数据集上的效果差异。
- 把训练过程保存的 checkpoints 整理好,便于答辩时现场演示。
- 在项目 README 中写明环境版本、运行命令和遇到的问题,方便自己复盘也让老师快速了解项目。
猫狗识别本身不难,但它是理解图像分类完整链路的最佳入门项目之一。从环境准备到最终预测,每一步踩过的坑都对应着深度学习实践中真实会遇到的问题。建议先按照本文完整跑通一遍,再尝试调整模型深度、图片尺寸、数据增强和优化器参数,运行对比实验。这样交出来的毕设不只是“代码能跑”,而是你真正理解每个环节为什么这么设计。