ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

猫狗分类实战:CNN搭建、数据增强与97%识别率调参全解析

猫狗分类实战:CNN搭建、数据增强与97%识别率调参全解析 简介这是一份基于Python卷积神经网络CNN的猫狗图像分类毕业设计项目源码面向计算机相关专业学生、毕业设计者以及图像识别初学者。项目采用数据增强技术提升模型泛化能力以卷积神经网络作为分类器在猫狗二分类任务上识别率达到97%。压缩包共含2000个文件主要包括1992张jpg图像样本、7个py源码脚本和1个md说明文档图像数据划分为训练集、验证集与测试集可依据源码自行扩展至全量数据集重新训练。资源包大小约87.85MB目前已有1132人学习浏览。从中能够获取完整可运行的CNN分类项目包括数据预处理、模型搭建、训练验证与预测脚本以及数据集划分思路和调整训练规模的改写方法便于快速上手图像分类任务或在此基础上完成毕业设计。1. 为什么一个“猫狗分类”还能成为毕业设计很多人看到“猫狗分类”第一反应是 Keras 官方教程里那个 30 行代码的入门示例觉得这东西撑不起一个毕业设计。但真正动手做过的人会告诉你官方示例跑通容易要稳定达到 97% 的识别率并讲清楚每个参数为什么这么设是另一回事。这个项目的价值不在于“猫 vs 狗”这个任务本身而在于它把图像分类的完整流程走了一遍——从原始图片目录到数据划分再到数据增强、CNN 结构设计、训练策略调整最后落到识别率指标上。适合正在做深度学习的课程设计、毕业设计或者刚接触 CNN、想知道图像分类工程细节的 Python 开发者。这篇文会把项目里的核心逻辑拆开讲包括数据怎么分、网络怎么搭、97% 是怎么调出来的以及想换自己的数据集时改哪里。2. 数据准备阶段从零散的 JPG 目录到结构化数据集2.1 先看清楚原始数据的组织方式这个项目收到的原始数据是散装的图片文件名字形如cat.835.jpg、dog.157.jpg、dog.815.jpg也就是说图片本身没有放在分类子目录里类别信息编码在文件名前缀中。Keras 的ImageDataGenerator.flow_from_directory默认要求数据按子目录组织data/ train/ cats/ dogs/ validation/ cats/ dogs/ test/ cats/ dogs/所以在跑任何模型之前第一步是写一个脚本把散装图片按规则归类。常见做法是直接用 Python 的shutil和os模块按文件名前缀分拣到对应目录。核心逻辑如下import os import shutil source_dir ./raw_images # 原始图片所在目录 target_root ./data # 目标根目录 train_ratio 0.5 # 训练集比例 val_ratio 0.25 # 验证集比例 # 剩余 0.25 作为测试集 os.makedirs(target_root, exist_okTrue) # 先按类别收集所有文件路径 files_by_class {cat: [], dog: []} for fname in os.listdir(source_dir): if fname.lower().startswith(cat.) and fname.endswith(.jpg): files_by_class[cat].append(fname) elif fname.lower().startswith(dog.) and fname.endswith(.jpg): files_by_class[dog].append(fname) for cls, files in files_by_class.items(): files.sort() # 保证顺序稳定 n len(files) n_train int(n * train_ratio) n_val int(n * val_ratio) splits { train: files[:n_train], validation: files[n_train:n_train n_val], test: files[n_train n_val:], } for split_name, split_files in splits.items(): dest_dir os.path.join(target_root, split_name, cls) os.makedirs(dest_dir, exist_okTrue) for fname in split_files: shutil.copy2( os.path.join(source_dir, fname), os.path.join(dest_dir, fname) ) print(f{cls}: total{n}, ftrain{len(splits[train])}, fval{len(splits[validation])}, ftest{len(splits[test])})这段代码按 50%/25%/25% 把每个类别的图片分别切分到三个子集避免某个类别在某个子集中占比失衡。如果你拿到的是完整数据集而不是已经切好的 2000/1000/1000就把train_ratio改成 0.7、val_ratio改成 0.15 之类凑够你要的数量。注意这里用的是copy2保留原始文件不污染源目录如果磁盘紧张可以换成move。2.2 训练集/验证集/测试集为什么要分开初学者最容易犯的错是只用训练集和测试集或者拿测试集反复调参。这个项目的划分方式是教科书标准训练集用来更新网络权重验证集用来在每个 epoch 结束后评估模型并决定是否调整超参数比如学习率、Dropout 比例测试集只在全部训练完成后用一次模拟真实场景下的表现。如果验证集和训练集有重叠模型会“记住”而不是“学会”验证 loss 会虚低换到新图片上立刻现原形。这个项目提取了 2000 张作为训练集1000 张作为验证集1000 张作为测试集。对二分类任务来说2000 张训练图并不算多后面能跑到 97%一部分功劳要记在数据增强上。3. CNN 模型架构三层卷积 Dropout 是如何拟合猫狗问题的3.1 为什么不用预训练模型既然是毕业设计很多人第一反应是上 VGG16、ResNet50 做迁移学习。但这里必须说清楚一个矛盾点迁移学习虽然能更快收敛但网络被冻结的部分不可训练能改的只有最后的全连接层和分类层。评委如果问“卷积层为什么选 3×3 卷积核、为什么池化要用 2×2”用迁移学习的学生很容易答不上来因为那些层压根不是自己设计的。这个项目从头搭建 CNN虽然结构不深但每个组件的选型逻辑都是可控的、能讲清楚的。3.2 模型结构逐层拆解核心模型是典型的“卷积 池化 Dropout 全连接”套件结构大致如下from tensorflow.keras import layers, models model models.Sequential([ # 第一层卷积提取低级特征边缘、颜色块 layers.Conv2D(32, (3, 3), activationrelu, input_shape(128, 128, 3)), layers.MaxPooling2D(2, 2), # 第二层卷积组合边缘为纹理、局部形状 layers.Conv2D(64, (3, 3), activationrelu), layers.MaxPooling2D(2, 2), # 第三层卷积组合纹理为语义部件耳朵、眼睛、尾巴形态 layers.Conv2D(128, (3, 3), activationrelu), layers.MaxPooling2D(2, 2), # 展平后接全连接加 Dropout 抑制过拟合 layers.Flatten(), layers.Dense(512, activationrelu), layers.Dropout(0.5), layers.Dense(1, activationsigmoid) # 二分类输出 ]) model.compile( optimizeradam, lossbinary_crossentropy, metrics[accuracy] )网络结构不复杂但每个参数都有讲究。Conv2D(32, (3, 3))表示第一层用 32 个 3×3 卷积核输入图是 128×128×3RGB 三通道。为什么从 32 开始而不是直接上 128因为浅层特征边缘、颜色渐变比较简单少量卷积核就够随着深度增加特征越来越抽象通道数才翻倍到 64、128。MaxPooling2D(2, 2)把 128×128 降到 64×64再降到 32×32、16×16一方面减少计算量另一方面扩大感受野——后面的卷积层能看到更大范围的像素关系。Flatten把 16×16×128 的三维特征图拉平成 32768 维向量Dense(512)做高层语义组合Dropout(0.5)随机丢弃一半神经元让网络不依赖某些特定神经元从而缓解过拟合。3.3 输出层和损失函数的选择逻辑二分类任务输出层用Dense(1, activationsigmoid)配合lossbinary_crossentropy二分类交叉熵这组搭配直接输出一个 0 到 1 之间的概率值大于 0.5 判为狗小于 0.5 判为猫。如果你把输出层写成Dense(2, activationsoftmax)损失函数就得换成categorical_crossentropy标签也要改成 one-hot 编码。两种方案在这个规模的任务上精度差别极小但 sigmoid 版本的模型文件更小、预测时少一次 softmax 计算部署上更轻量。下表是两者的对应关系改造多分类时会用到任务类型输出层激活函数输出节点数损失函数标签编码二分类sigmoid1binary_crossentropy0/1 整数多分类softmax类别数categorical_crossentropyone-hot多分类整数标签softmax类别数sparse_categorical_crossentropy整数4. 训练策略数据增强、EarlyStopping 与 97% 识别率的来源4.1 ImageDataGenerator 做数据增强2000 张训练图对 CNN 来说偏少直接硬训很容易在第一轮就出现过拟合训练 accuracy 快速逼近 100%验证 accuracy 卡在 80% 上下。这个项目的解法是数据增强——在训练过程中对每张图片做随机变换相当于变相扩大数据集。关键代码是from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1.0/255, # 像素归一化到 0~1加速收敛 rotation_range20, # 随机旋转 ±20 度 width_shift_range0.2, # 水平平移 20% height_shift_range0.2, # 垂直平移 20% shear_range0.2, # 错切变换 zoom_range0.2, # 随机缩放 horizontal_flipTrue, # 水平翻转猫狗图片镜像仍是猫狗语义不变 fill_modenearest # 变换后空白区域用最近邻像素填充 ) # 验证集和测试集只做归一化不做增强 val_datagen ImageDataGenerator(rescale1.0/255) train_generator train_datagen.flow_from_directory( ./data/train, target_size(128, 128), batch_size32, class_modebinary ) val_generator val_datagen.flow_from_directory( ./data/validation, target_size(128, 128), batch_size32, class_modebinary )每个参数对应一类“噪声”rotation_range和width_shift_range解决猫狗在图片中位置不居中的问题zoom_range解决拍摄距离远近不一的问题horizontal_flip增加样本多样性。注意验证集不能用增强否则不同 epoch 的验证集数据不一致loss 曲线会抖动得没法看。fill_modenearest很关键不做填充的话旋转后边缘会出现黑色像素块相当于给训练集注入了一种原数据中不存在的分布。4.2 回调函数EarlyStopping 与学习率衰减训练部分的另一个关键设计是回调函数。如果没有 EarlyStopping你会发现训练到第 30 个 epoch 时验证 accuracy 已经在 95% 附近震荡继续训练不仅浪费时间还可能让模型过拟合到验证集上。标准配置如下from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint callbacks [ EarlyStopping( monitorval_loss, patience8, restore_best_weightsTrue ), ReduceLROnPlateau( monitorval_loss, factor0.5, patience3, min_lr1e-6 ), ModelCheckpoint( best_cat_dog_model.keras, monitorval_accuracy, save_best_onlyTrue ) ] history model.fit( train_generator, steps_per_epoch2000 // 32, epochs50, validation_dataval_generator, validation_steps1000 // 32, callbackscallbacks )steps_per_epoch2000 // 32等于 62意思是每个 epoch 从训练生成器中取 62 个 batch每个 batch 32 张共约 1984 张剩余的零头由生成器随机补足。patience8表示验证 loss 连续 8 个 epoch 不下降就停止训练restore_best_weightsTrue保证模型回滚到验证集上表现最好的那一次权重而不是最后一次可能已经过拟合的权重。ReduceLROnPlateau在验证 loss 连续 3 个 epoch 不降时把学习率减半让模型在小步长下越过局部最优。4.3 97% 是怎么来的组合策略而非单一技巧如果只跑一个裸 CNN 不做增强、不加回调这个数据集上的典型结果在 90% 到 93% 之间。加上数据增强能到 95% 左右。再配合 EarlyStopping 在最佳权重处截断以及验证集上的多次小步微调97% 是一个合理的数字。值得注意class_modebinary时flow_from_directory会自动按字母序分配标签cats是 0dogs是 1所以预测结果里np.round(pred)等于 1 时是狗等于 0 时是猫这个映射关系后面会用到。训练过程中要盯两个指标训练 accuracy 和验证 accuracy 的差距。如果差距持续超过 5 个百分点说明 Dropout 比例不够或数据增强强度不够如果两者都卡在 90% 上不去说明网络的容量不够需要加卷积层通道数或增加一层卷积。5. 源码改造实战换全量数据集、改预测脚本、扩展多分类5.1 把 2000 张训练图改成全量数据项目摘要里特意说“如果想用全部数据集作为训练可以更改源码”。改法其实很直接把第 1 章的分类脚本里train_ratio调高或者干脆不切验证集和测试集把全部图片都放训练集然后单独留出一小部分比如 10% 到 15%做验证。但这里有一个隐蔽的问题如果原本给的 2000 张训练图是从全量数据里抽出来的直接全量训练后模型性能会更好如果全量数据本身就是全部图片全量训练时验证集和测试集的划分比例就得重新算不能沿用 2000/1000/1000 的切法。改法推荐这样做# 重新划分数据集全部图片按 70% / 15% / 15% 切分 raw_root ./all_images target_root ./full_data # 统计每个类别的总图片数 cat_count len([f for f in os.listdir(raw_root) if f.startswith(cat.)]) dog_count len([f for f in os.listdir(raw_root) if f.startswith(dog.)]) # 然后按 0.7 / 0.15 / 0.15 的比例分配 # 核心逻辑与 1.1 节相同只改 train_ratio 和 val_ratio切分完成后把flow_from_directory的路径从./data/train改成./full_data/train其余训练代码完全不用动。全量数据下同一个模型结构可以适当调大——比如把第三层通道数从 128 加到 256——因为样本量大了更大的模型容量不容易过拟合。训练时长也会相应增加建议先用 20 个 epoch 快速看趋势确认模型在下降而不是发散后再放开到 50 个 epoch 跑完整版。5.2 写一个独立的预测脚本训练完成后predict 阶段有一个高频踩坑点model.predict的输入必须和训练时一样经过归一化除以 255否则预测结果会偏。标准预测脚本如下import numpy as np from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing import image model load_model(./best_cat_dog_model.keras) def predict_image(img_path): img image.load_img(img_path, target_size(128, 128)) img_array image.img_to_array(img) # 形状: (128, 128, 3) img_array np.expand_dims(img_array, axis0) # 加 batch 维度 - (1, 128, 128, 3) img_array img_array / 255.0 # 归一化必须与训练一致 pred model.predict(img_array, verbose0)[0][0] label dog if pred 0.5 else cat return label, float(pred) # 用法示例 label, prob predict_image(./test_images/cat.100.jpg) print(f预测类别: {label}, 置信度: {prob:.4f})np.expand_dims这一步很容易漏。模型训练时输入形状是(batch_size, 128, 128, 3)单张图片只有三维不补一维会直接报ValueError。预测结果是一个二维数组[0][0]取出 batch 第一条数据的 sigmoid 输出值。边界值 0.5 可以按需调整——如果你更在意“把猫误判成狗”还是“把狗误判成猫”的代价就把阈值往对应方向移比如调成 0.6 会减少猫被误判为狗的概率但会增加漏判真狗的风险。5.3 从二分类扩展到多分类的思路如果想把猫狗二分类改成猫狗鸟三分类需要改四个地方目录下加birds文件夹、flow_from_directory的class_mode改为categorical、输出层改成Dense(3, activationsoftmax)、损失函数换成categorical_crossentropy。标签的读取方式也会变化原来binary模式下生成整数标签categorical模式下标签是 one-hot 向量model.predict的输出从单个概率值变成长度为 3 的概率分布取argmax得到类别下标。需要注意flow_from_directory的类别映射是基于文件夹名称的字母顺序birds排在cats和dogs之间这个顺序在写分类报告时要对齐。6. 验证模型没跑偏混淆矩阵、单张图和 batch 预测训练完别急着交差花十分钟做三个验证能发现很多隐藏问题。第一打印混淆矩阵。二分类只有两个类别混淆矩阵一眼就能看出模型更混淆哪个方向from sklearn.metrics import confusion_matrix, classification_report test_generator val_datagen.flow_from_directory( ./data/test, target_size(128, 128), batch_size32, class_modebinary, shuffleFalse # 必须关掉否则预测结果和标签对不上 ) test_preds model.predict(test_generator, verbose1) test_preds_binary (test_preds 0.5).astype(int) cm confusion_matrix(test_generator.classes, test_preds_binary) report classification_report( test_generator.classes, test_preds_binary, target_nameslist(test_generator.class_indices.keys()) ) print(cm) print(report)shuffleFalse是这里最容易踩的坑。flow_from_directory默认会打乱数据顺序如果保持打乱test_generator.classes和预测结果就不是一一对应关系混淆矩阵会完全错乱。test_generator.class_indices是一个字典比如{cats: 0, dogs: 1}用它来对齐类别名。第二随机抽三张单图走一遍 5.2 节的预测脚本和测试集整体指标做对比。单图预测能暴露预处理不一致的问题——比如某张图是 RGBA 四通道 PNGload_img默认会转成 RGB但如果你的数据集里有灰度图又没做灰度统一这里就会出偏差。一致性检查是在测试集 accuracy 之外的第二道保险。第三统计每个 batch 的预测耗时。如果模型要在嵌入式设备或者后端服务中上线单张预测耗时要控制在可接受范围。测法很简单import time start time.time() for _ in range(50): model.predict(img_array, verbose0) end time.time() print(f50 次单图预测耗时: {end - start:.2f}s, f平均每次: {(end - start) / 50 * 1000:.1f}ms)如果单图预测超过 100ms考虑把输入尺寸从 128×128 降到 96×96或者用 TensorFlow Lite 转换模型。97% 的识别率是在测试集 1000 张图片上验证出来的前提条件是测试集和训练集来自同一分布——如果你把手机拍的、带水印的、光照极端的猫狗图片丢进去准确率会明显下滑这不是模型 bug而是数据分布偏移。本文还有配套的精品资源点击获取
返回列表