
简介基于Python语言与卷积神经网络CNN的猫狗图片分类项目是一份面向计算机专业毕业设计、期末大作业和项目实战学习者的高质量源代码资源。该项目由作者在导师指导下自主完成评审得分98分所有源码均经过本地编译与严格调试能够稳定运行。资源压缩包中共有2000个文件以近2000张jpg格式的猫狗图片数据集为主辅以7个Python脚本和1个Markdown说明文档包体约87.79MB整体结构清晰便于直接解压后进行数据加载、模型构建与训练测试。目前已有75人学习下载适合需要快速掌握卷积神经网络建模全流程的初学者和进阶者。借助这份资源读者可以完整看到从图片预处理到卷积层、池化层、全连接层设计再到损失计算与准确率评估的代码实现还能借助说明文档梳理项目思路严格调试过的源码让复现过程顺畅既可用于课程设计或毕业答辩也可作为迁移到其他图像识别任务的基础框架。1. 从一份猫狗图片分类源码说起CNN 入门项目为什么值得反复拆期末大作业和毕业设计里猫狗图片分类是出现频率最高的 CNN 实战题之一。这份基于 Python CNN 卷积神经网络实现的猫狗图片分类项目源代码核心就两件事把图片读进来用卷积网络区分出猫和狗。它不是什么炫技项目但训练流程完整、模型可跑、指标看得见正好适合正在找 Python CNN 方向课设源码的人也适合想通过一个完整项目把卷积神经网络从“概念黑匣子”变成“自己能调的代码”的学习者。我拆过不少这类资源这套代码的目录组织、训练入口和预测脚本都比较规整难度适中是那种拿到手能直接复现、跑通之后再改着玩的项目。2. 猫狗图片分类的数据准备目录划分与图片预处理细节2.1 数据集的目录结构为什么要按类别分文件夹猫狗分类项目的第一步不是写模型而是把图片按照约定好的目录结构放好。项目正文里出现的 cat.835.jpg、dog.157.jpg 这类命名说明数据源本身就是按类别.序号.jpg的规则命名的。Keras 的ImageDataGenerator.flow_from_directory读取数据集时依赖路径中的类别名官方推荐的目录结构是每个类别一个子文件夹这样不需要手动写标签。常见做法是把数据拆成 train、val、test 三份。训练集用来更新权重验证集用来监控过拟合测试集用来做最终评估。我一般按 7:1.5:1.5 拆分。拆分时要注意一个容易翻车的点不能只按文件名前几个字机械地分需要先打乱再切否则前 70% 全是猫、后 30% 全是狗训练直接废掉。下面是推荐目录结构和对应的拆分脚本。import os import random import shutil src_dir images_all # 原始图片目录 splits {train: 0.7, val: 0.15, test: 0.15} # 先为每个拆分目录建好 cat/dog 子目录 for split in splits.keys(): for cls in [cat, dog]: os.makedirs(os.path.join(data, split, cls), exist_okTrue) for cls in [cat, dog]: files [f for f in os.listdir(src_dir) if f.startswith(cls)] random.shuffle(files) # 必须先打乱否则类别顺序不均衡 n_total len(files) n_train int(n_total * splits[train]) n_val int(n_total * splits[val]) for f in files[:n_train]: shutil.copy(os.path.join(src_dir, f), os.path.join(data, train, cls, f)) for f in files[n_train:n_train n_val]: shutil.copy(os.path.join(src_dir, f), os.path.join(data, val, cls, f)) for f in files[n_train n_val:]: shutil.copy(os.path.join(src_dir, f), os.path.join(data, test, cls, f))这段脚本的核心逻辑是random.shuffle先把文件顺序打乱再按比例切片。split字典里定义了三份数据的占比startswith(cls)从文件名里识别类别shutil.copy负责复制而不是移动保留原始数据做备份。如果你把n_train和n_val的边界算错最典型的后果是验证集和测试集出现类别缺失flow_from_directory会直接报 Found 0 images belonging to 2 classes。2.2 图片预处理与数据增强参数设置直接影响收敛速度CNN 对输入图片有固定尺寸要求。这个项目里输入统一缩放为 150×150原因很实际尺寸越大显存占用越高150×150 对猫狗这种粗粒度分类任务足够训练速度也快。另一个必须做的预处理是归一化把 0-255 的像素值除以 255 变成 0-1 区间。不做归一化时梯度更新会非常不稳定损失值来回震荡精确率上不去。数据增强是防止过拟合的重要手段。猫狗数据集通常有几万张图但如果你的数据集只有几千张模型很快就能“背”下训练集的纹理。增强的本质是让模型每次看到的是略微变形的图片迫使它学习“猫”和“狗”的本质特征而不是具体某张图的像素排列。我用ImageDataGenerator做增强代码如下。from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1./255, # 像素归一化到 0~1 rotation_range20, # 随机旋转 ±20 度 width_shift_range0.2, # 水平平移 20% height_shift_range0.2, # 垂直平移 20% shear_range0.15, # 错切变换强度 zoom_range0.2, # 随机缩放 ±20% horizontal_flipTrue # 随机水平翻转 ) val_datagen ImageDataGenerator(rescale1./255)关键一点验证集和测试集只做rescale不做任何增强。原因是验证集需要反映真实分布如果验证集也被旋转缩放评估指标就失真了。rotation_range20意味着图片随机旋转 20 度以内width_shift_range0.2表示图片宽度的 20% 作为平移量。水平翻转对猫狗分类是合理增强因为猫狗照片的朝向没有语义差别。下面是两个生成器的加载参数对照。参数作用训练集建议值验证集/测试集rescale像素归一化1./2551./255rotation_range随机旋转角度20不设置width_shift_range水平平移比例0.2不设置height_shift_range垂直平移比例0.2不设置zoom_range随机缩放0.2不设置horizontal_flip水平翻转True不设置flow_from_directory配合target_size(150, 150)会自动把图片读成所需尺寸batch_size一般取 32 或 64。批量越小梯度更新越频繁训练震荡越明显批量太大则显存不够。32 在这个项目里是稳妥的选择。3. 模型结构拆解从卷积层堆叠到全连接分类3.1 卷积块设计三层 ConvPooling 结构的选型理由猫狗分类属于图像分类里的入门难度不需要 ResNet 这种深层结构。这个项目采用的是经典的堆叠式 CNN三个卷积块每个块由卷积层、激活函数、池化层组成最后接全连接层。这种结构脱胎于 LeNet-5 和 AlexNet 的设计思路通道数从 32 逐步翻倍到 128空间尺寸从 150×150 逐步缩小到 17×17符合“空间维度递减、通道维度递增”的通用设计准则。第一层卷积核数量少是因为浅层学的是边缘、颜色块等低级特征不需要太多通道越往后通道数越多是为了容纳更丰富的抽象特征。卷积核统一用 3×3这是 VGG 系列验证过的经验两个 3×3 堆叠的感受野等于一个 5×5但参数量更少、非线性更强。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout model Sequential([ Conv2D(32, (3, 3), activationrelu, input_shape(150, 150, 3)), MaxPooling2D(2, 2), Conv2D(64, (3, 3), activationrelu), MaxPooling2D(2, 2), Conv2D(128, (3, 3), activationrelu), MaxPooling2D(2, 2), Flatten(), Dropout(0.5), Dense(512, activationrelu), Dense(2, activationsoftmax) ])Conv2D(32, (3, 3), activationrelu, input_shape(150, 150, 3))里的 32 是输出通道数3×3 是卷积核尺寸input_shape只在第一层声明。MaxPooling2D(2, 2)把特征图尺寸缩减一半同时保留最显著的特征这比平均池化更适合提取边缘和纹理。Dropout(0.5)在全连接层之前随机丢弃一半神经元是抑制过拟合最有效的手段之一训练时生效、预测时自动关闭。3.2 张量形状推导从 150×150×3 到 2 个分类概率理解模型的关键是跟着张量 shape 走一遍。input_shape(150, 150, 3)表示高 150、宽 150、通道 3RGB。第一层卷积用 3×3 卷积核、默认 paddingvalid输出尺寸是 148×148×32池化后减半变成 74×74×32。后续两层同理经过三层卷积池化后变成 17×17×128。Flatten 把这 36992 个数拉成一维向量最后经过 512 维全连接层和 2 维 softmax 输出层得到“猫概率”和“狗概率”。层输出形状参数量级输入图片(150, 150, 3)-Conv2D 323×3(148, 148, 32)约 896MaxPooling2D(74, 74, 32)0Conv2D 643×3(72, 72, 64)约 18496MaxPooling2D(36, 36, 64)0Conv2D 1283×3(34, 34, 128)约 73856MaxPooling2D(17, 17, 128)0Flatten369920Dense 512512约 1894 万Dense 2 softmax2约 1026Flatten 之后参数量突然暴增到 1894 万这是因为 36992 维向量和 512 个神经元全连接。这也是 Dropout 必须放在 Flatten 和 Dense 之间的原因——如果不丢全连接层会记住训练集里大量无关的高频噪声。训练时如果发现验证集准确率停滞而训练集准确率逼近 1优先看这一层。3.3 编译参数优化器、损失函数和评估指标的搭配模型搭好之后编译参数直接决定训练能否收敛。这个项目里优化器用 Adam损失函数用 categorical_crossentropy分类交叉熵评估指标用 accuracy。Adam 是自适应学习率优化器它对学习率的初始值不像 SGD 那么敏感适合快速验证模型能不能学起来。损失函数的选择有个前提标签必须是 one-hot 编码。categorical_crossentropy要求标签形如[1, 0]表示猫、[0, 1]表示狗。如果标签是整数0和1应该用sparse_categorical_crossentropy。很多人在这一步踩坑报错信息通常是 Shape mismatch 或者 Received a label value of 1 which is outside the valid range。from tensorflow.keras.optimizers import Adam model.compile( optimizerAdam(learning_rate0.001), losscategorical_crossentropy, metrics[accuracy] )learning_rate0.001是 Adam 的默认值对这个项目合适。如果你发现损失在训练初期震荡剧烈可以把学习率降到 0.0003如果收敛太慢再调到 0.003。flow_from_directory默认的class_modecategorical正好输出 one-hot 编码和categorical_crossentropy是配套的不要随意改其中一个而不改另一个。4. 训练与评估回调函数、准确率解读与模型保存4.1 训练流程steps_per_epoch 的计算与回调配置数据生成器是流式的模型不知道数据集总共有多少张图所以必须指定steps_per_epoch和validation_steps。计算公式很简单样本总数除以 batch_size。比如训练集有 14000 张图、batch_size 为 32steps_per_epoch就是 14000÷32 约等于 437。如果不设置Keras 会默认用 len(generator)部分旧版本会报错新版本会警告。训练时我习惯配置三个回调ModelCheckpoint 保存最优模型、EarlyStopping 防止过拟合、ReduceLROnPlateau 在验证损失停滞时自动降低学习率。from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau checkpoint ModelCheckpoint( best_model.h5, monitorval_accuracy, modemax, save_best_onlyTrue, verbose1 ) early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) reduce_lr ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-6 ) history model.fit( train_generator, steps_per_epoch437, epochs30, validation_dataval_generator, validation_steps94, callbacks[checkpoint, early_stop, reduce_lr] )save_best_onlyTrue只在验证集准确率创新高时保存模型防止最后一轮过拟合的权重覆盖最优结果。patience10表示验证集损失连续 10 个 epoch 不下降就停止训练。ReduceLROnPlateau的factor0.5意味着验证集损失停滞 5 个 epoch 后学习率直接减半这种“先快后慢”的策略比固定学习率更容易收敛到好的局部最优点。4.2 评估指标验证集准确率、混淆矩阵与分类报告训练结束后不要只看终端打出的 accuracy。验证集准确率只能粗略衡量模型好坏要真正评估分类能力需要看每一类的精确率、召回率和混淆矩阵。比如如果模型对猫的召回率是 0.95对狗的召回率只有 0.80说明模型偏向把狗预测成猫这在实际应用里是严重的问题。import numpy as np from sklearn.metrics import confusion_matrix, classification_report val_generator val_datagen.flow_from_directory( data/val, target_size(150, 150), batch_size32, class_modecategorical, shuffleFalse # 评估时必须关闭 shuffle否则标签对不上 ) y_true val_generator.classes y_pred np.argmax(model.predict(val_generator), axis1) print(confusion_matrix(y_true, y_pred)) print(classification_report(y_true, y_pred, target_names[cat, dog]))shuffleFalse必须显式设置。因为flow_from_directory默认会打乱顺序如果开启 shufflemodel.predict的输出顺序就和val_generator.classes的真实标签顺序对不上了混淆矩阵完全失真。np.argmax(..., axis1)把 softmax 输出的两个概率转成预测类别索引。分类报告里的 precision精确率、recall召回率、f1-score 是比 accuracy 更能反映模型短板的三项指标。4.3 模型保存与加载两种方式的适用场景训练完成后有两种保存方式。model.save(cat_dog_model.h5)保存完整模型包括网络结构和权重加载后可以直接预测适合“训练完就不再改结构”的情况。另一种是只保存权重model.save_weights(weights.h5)加载前需要先用代码重新构建一模一样的模型结构适合你还要继续调整结构、做迁移学习的情况。# 方式一保存完整模型 model.save(cat_dog_model.h5) # 方式二只保存权重加载时需先重建结构 model.save_weights(cat_dog_weights.h5) # 加载完整模型 from tensorflow.keras.models import load_model loaded_model load_model(cat_dog_model.h5) # 加载权重前提当前 model 结构和原模型完全一致 model.load_weights(cat_dog_weights.h5)推荐训练时用ModelCheckpoint保存.h5完整模型这样即使脚本中途崩溃或者你换了机器直接用load_model就能恢复。只保存权重的坑在于如果你改动了任何一层结构load_weights就会因为 shape 不匹配而报错而报错信息并不会告诉你是哪一层不匹配排查很费时间。加载模型之后建议先用一张训练集之外的图片试跑一次确认输入输出正常再进入预测环节。5. 避坑指南调参翻车与数据隐患的五个典型场景5.1 过拟合训练准确率 0.98验证准确率只有 0.72现象训练集的损失持续下降准确率逼近 1但验证集准确率在 0.7 附近来回抖动两者差距越拉越大。这是 CNN 项目里最常见的现象本质是模型把训练集的背景、纹理、拍摄水印等噪声当成了分类特征。原因模型参数太多数据集太小或者训练 epoch 过长。Dropout 比例不够、没有数据增强也会加剧这个问题。解决优先做三个调整——增大Dropout到 0.5 以上给训练集加数据增强旋转、平移、翻转至少选两样用EarlyStopping的patience控制在 8 到 10不要让它无限训练。如果还压不住最直接的办法是换成预训练模型做迁移学习用 VGG16 冻结底层只训练顶层过拟合风险会大幅降低。5.2 类别不平衡模型把 90% 的图片都预测成猫现象验证集混淆矩阵显示狗的召回率只有 0.3而猫的召回率接近 0.98整体准确率看着还行但实际“猫狗分类”变成了“什么都说是猫”。原因训练集里猫的图片数量远多于狗。flow_from_directory不会自动处理类别不平衡模型天然倾向预测样本多的类别因为这样做能让整体损失最小。解决给少数类加权重。Keras 的fit里可以直接传class_weight参数猫和狗的权重分别设为{0: 1.0, 1: 2.0}或者根据数量比计算。另一种做法是让ImageDataGenerator的flow_from_directory配合class_modecategorical时使用sampling策略但更省事的还是class_weight。total sum(train_generator.classes 0) sum(train_generator.classes 1) weight_for_cats total / (2 * sum(train_generator.classes 0)) weight_for_dogs total / (2 * sum(train_generator.classes 1)) class_weight {0: weight_for_cats, 1: weight_for_dogs}5.3 验证集和训练集数据泄漏shuffle 顺序的隐形坑现象训练时验证集准确率一直比训练集还高或者验证集准确率出现“跳变”某个 epoch 突然从 0.7 跳到 0.95下个 epoch 又跌回 0.7。原因数据集在拆分前没有打乱或者验证集生成器设置了shuffleTrue。有一个常见错误是把同一批图片既放进训练集又放进验证集模型实际上“见”过验证集评估结果虚高。解决拆分前必须random.shuffle(files)评估时flow_from_directory(..., shuffleFalse)。判断是否有泄漏的一个笨办法训练结束后随机抽 5 张验证集图片用模型预测并打印文件名如果预测置信度全部超过 0.99很可能是验证集图片混进了训练集。5.4 环境兼容性TensorFlow 版本导致的模型加载失败现象在一台机器上训练好的.h5模型换到另一台机器加载时报错比如AttributeError: module tensorflow has no attribute placeholder或者Unknown layer: Conv2D。原因训练环境是 TensorFlow 1.x加载环境是 TensorFlow 2.x或者反过来Keras 的序列化格式跨版本不兼容。Python 3.8 和 TensorFlow 2.x 的搭配比较稳定但如果从网上下载的源码是基于 Keras 独立版写的在 TensorFlow 2.x 里直接 import 也会报错。解决建议统一用 TensorFlow 2.x 训练代码里from tensorflow.keras导入不要用from keras。如果一定要用旧版代码先跑一个最简单的Sequential([Dense(1)])模型保存再加载验证环境本身没毛病再去排查项目代码。版本问题永远最先查pip list | grep tensorflow。5.5 预测阶段输入 shape 不匹配单张图片预测报错现象训练和评估都正常但用model.predict(单张图片)时报错信息类似ValueError: Input 0 of layer sequential is incompatible with the layer: expected min_ndim4, found ndim3。原因训练时输入是四维张量(batch_size, 150, 150, 3)单张图片读取后是三维(150, 150, 3)模型不接受。很多人忽略这一点把预测脚本写成了model.predict(img)而不是model.predict(img[np.newaxis, ...])。解决预测时把图片扩展一个维度变成(1, 150, 150, 3)。这也是完整项目里预测脚本的价值所在它把图片读取、缩放、维度扩展、结果解析都封装好了直接调用就能拿到类别和置信度。import numpy as np from tensorflow.keras.preprocessing import image img image.load_img(test_cat.jpg, target_size(150, 150)) img_array image.img_to_array(img) # (150, 150, 3) img_array np.expand_dims(img_array, axis0) # (1, 150, 150, 3) img_array img_array / 255.0 # 归一化和训练时保持一致 pred model.predict(img_array) print(猫的概率:, pred[0][0], 狗的概率:, pred[0][1])np.expand_dims(img_array, axis0)在索引 0 的位置插入一个新维度把三维数组扩展成四维。这里最容易漏掉的是归一化训练时ImageDataGenerator(rescale1./255)做过一次归一化预测脚本如果忘记除以 255输入像素值范围变成 0-255 而不是 0-1模型输出的置信度会完全异常。6. 进阶玩法迁移学习、特征可视化与模型导出from tensorflow.keras.applications import VGG16 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Flatten, Dense, Dropout base_model VGG16(weightsimagenet, include_topFalse, input_shape(150, 150, 3)) base_model.trainable False model Sequential([ base_model, Flatten(), Dense(256, activationrelu), Dropout(0.5), Dense(2, activationsoftmax) ]) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy])迁移学习是把猫狗分类准确率从 85% 提到 95% 的最快路径。VGG16 在 ImageNet 上预训练好的底层特征边缘、纹理、形状可以直接复用到猫狗任务base_model.trainable False冻结这些权重只训练新加的全连接层。这样做的好处是训练速度快、不容易过拟合尤其适合数据集只有几千张图的情况。微调阶段可以再解冻最后几层卷积用很小的学习率继续训练但我建议先把冻结版本跑通确认准确率上来了再动这个。特征可视化能帮你理解模型到底在“看”什么。取模型中间某层卷积的输出把特征图打印出来你会发现浅层特征图响应的是图片里的边缘和颜色块深层特征图响应的则是眼睛、耳朵这类语义区域。如果深层特征图对猫和狗的响应没有明显差异说明模型没学到有效特征这种诊断比盯着准确率数字更直观。实现上用tf.keras.Model(inputsmodel.input, outputsmodel.layers[2].output)截取中间层输出把单张图片喂进去打印特征图即可。模型导出方面如果后续要把模型接到 Web 服务或者移动端可以转换成 TFLite 格式。转换时注意输入和输出的张量维度要保持一致否则部署端推理时会出问题。转换命令很简单converter tf.lite.TFLiteConverter.from_keras_model(model)然后调用converter.convert()写出的.tflite文件体积会比.h5小不少推理速度也更快。自己训练、评估、部署走完这一轮之后我对这个项目的判断是作为课程设计和期末大作业它完整覆盖了数据集预处理、CNN 模型构建、训练调参、评估诊断、模型保存的全部环节是一个能拿得出手、经得起提问的完整作品。回忆我自己调参的那段日子每次被过拟合折磨到最后都是靠“减少模型容量、加强数据增强、尽早停止训练”这三板斧兜底。从那以后我每拿到一个分类项目都强制自己先画一遍数据流、确认每一层张量的 shape再开始写模型——这个习惯帮我少踩了很多坑。希望帮到你。本文还有配套的精品资源点击获取