☰
CNN图像分类实战:猫狗识别项目从数据准备到GUI部署全流程
2026/10/1 13:35:16 网站建设 项目流程

简介:基于Python与CNN的猫狗识别项目,是一份面向高校学生期末大作业、课程设计与毕业设计的完整源码包。资源共包含6个文件,分别为5个Python脚本与1份Markdown说明文档,其中GUI脚本用于构建图形交互界面,训练脚本负责CNN模型定义与训练流程,预测脚本执行分类推理,图像处理脚本处理输入数据,说明文档则提供部署与使用指引。代码全程附有注释,结构清晰,即使是新手也能较快上手并根据需求修改模型参数或界面布局。整套资源仅7KB,压缩为zip格式,体量轻巧、便于分发,项目已通过严格调试,简单配置即可运行。目前已有244人学习,适合需要快速搭建图像识别项目或完成实践作业的学习者。项目源于高分期末作品,系统功能完善、界面美观,能帮助读者完整掌握CNN建模、数据处理与GUI整合的实践方法,具备很高的复用价值。

1. 期末大作业里那个猫狗识别项目,到底在考什么、值得做吗

期末大作业里,猫狗识别是绕不开的题目。网上能搜到一堆基于Python+CNN模型的源码,自己从头跑一遍却容易卡在三个地方:目录组织不对、验证集乱增广、GUI预测就崩。本质就是图像二分类:输入猫图或狗图,CNN提特征,最后给出是dog的概率。难的不是网络结构,而是把数据准备、训练、界面、打包串成能交付的完整链路。适合Python入门后想完整走一遍深度学习流程的在校生,也适合想复现一个可运行CNN例子的从业者。提醒一句:顺序错了,脚本报错的次数会比训练时间还多。装好Python环境后,先确认PyCharm配置的解释器和GUI运行用的是同一个,免得训练完在界面上报ModuleNotFoundError,这种低级翻车最亏。

2. 先把图片喂明白:数据集准备与ImageDataGenerator的四个参数

2.1 目录结构要按Keras的习惯摆:train、val、test一个不能少

flow_from_directory不是像plt.imread那样读单张图片,而是自动扫描train目录下每个子目录,把子目录名当作类别。所以数据集必须在磁盘上先摆成结构化目录。网上那些“把所有图片堆在一个文件夹里然后代码自动分类”的脚本不是不能跑,而是做完之后你根本没法盯着验证集调参,最后GUI里输入一张新图片时,你也不知道它和训练分布是不是一回事。

一个规范目录长这样:

data/ ├── train/ │ ├── cat/ cat.12.jpg ... │ └── dog/ dog.23.jpg ... ├── val/ │ ├── cat/ ... │ └── dog/ ... └── test/ ├── test_cat.jpg └── test_dog.jpg

如果你的原始图片文件名带前缀,比如cat.100.jpg、dog.200.jpg,可以用脚本按比例拆分,顺序是“先打乱、再切分、再复制”:

import os import random import shutil raw_dir = 'raw_images' # 原始图片所在目录 data_dir = 'data' # 目标数据集目录 train_ratio = 0.8 # 训练集占比,其余进验证集 for cls in ['cat', 'dog']: files = [f for f in os.listdir(raw_dir) if f.lower().startswith(cls + '.')] random.shuffle(files) split = int(len(files) * train_ratio) for i, name in enumerate(files): part = 'train' if i < split else 'val' dest = os.path.join(data_dir, part, cls, name) os.makedirs(os.path.dirname(dest), exist_ok=True) shutil.copy(os.path.join(raw_dir, name), dest)

逻辑说明:第一轮按类别前缀筛出cat和dog两个文件列表,分别打乱再切分,这样每个类里都有80%进训练、20%进验证。复制而不是移动,原始素材保底,万一改了代码想重新切分还有后悔药。

参数说明:train_ratio=0.8对一两千张的数据够用;数据量超过一万张时,0.9训练比例更常见,因为验证集只需要保证每类几百张就能看出趋势。exist_ok=True保证目标目录不存在时自动创建,避免自己在data/train/cat外再多包一层目录——这是新手最高频的目录坑,多套一层就会让flow_from_directory打印出0张图片。

2.2 ImageDataGenerator:训练集做增广,验证集只做归一化

图像进CNN之前要统一尺寸、统一数值范围。常见做法是写一个ImageDataGenerator,把“读图、增广、归一化、成batch”整个过程串起来,训练时每次拿到一批新变换后的图,验证时不加任何随机变换。

from tensorflow.keras.preprocessing.image import ImageDataGenerator IMG_W, IMG_H = 150, 150 BATCH_SIZE = 32 train_datagen = ImageDataGenerator( rescale=1.0 / 255, rotation_range=20, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True ) val_datagen = ImageDataGenerator(rescale=1.0 / 255) train_gen = train_datagen.flow_from_directory( 'data/train', target_size=(IMG_W, IMG_H), batch_size=BATCH_SIZE, class_mode='binary' ) val_gen = val_datagen.flow_from_directory( 'data/val', target_size=(IMG_W, IMG_H), batch_size=BATCH_SIZE, class_mode='binary' ) print(train_gen.class_indices)

逻辑说明:rescale=1.0/255把0到255的像素值压到0到1,避免大数值让激活函数直接饱和。rotation_range=20是在±20度范围内随机旋转,width_shift_range和height_shift_range都是随机平移20%,shear_range做错切,zoom_range=0.2做缩放,horizontal_flip做水平翻转。猫狗图片有左右对称性,水平翻转非常安全。

参数说明:验证集只写rescale,不做任何翻转裁剪。验证集做增广会让validation准确率忽高忽低,你还会误以为模型没过拟合,实际是验证分布被改了。class_mode='binary'告诉Keras标签是一维0/1,对应二分类sigmoid输出;print(train_gen.class_indices)打印出来通常是{'cat': 0, 'dog': 1},这个顺序后面GUI里要对照用。

提示:如果flow_from_directory打印Found 0 images,第一反应去检查目录层数,train/cat下面必须直接是jpg,不能是train/cat/2024/xxx.jpg。

还有人会问,为什么不把图片提前批量转成numpy数组直接塞给模型?全量load到内存对两千张图确实没问题,但CNN训练指望每个epoch都看到随机增广后的新样本,一次性存好的数组就失去了在线增广的意义。保留生成器这种写法,本质是让模型每轮都在和“数据采集器”交互,这是这个项目里最值得保留的习惯。

3. 搭一个够用的CNN:卷积层、池化层与二分类输出

3.1 CNN基本结构就三件事:提特征、压维度、出概率

核心就是CNN卷积神经网络最基本的结构组合。第一层卷积找边缘和纹理,第二层卷积组合出五官、耳朵轮廓,第三层卷积能抓住更全局的形状。对猫狗这种小数据集来说,不需要上ResNet或VGG那么深的预训练网络,三层卷积加两个全连接已经能把准确率稳定撑到90%上下,训练速度快很多,参数也更容易解释。

各层作用用一个表说清楚:

层类型做的事常用参数
Conv2D用卷积核扫过图像,提取局部特征kernel_size=(3,3), filters=32/64/128
MaxPooling2D取2x2区域最大值,压缩尺寸、增强平移不变性pool_size=(2,2)
Flatten把三维特征图拉平成一维向量无参数
Dropout训练时随机丢弃部分神经元,防止全连接过拟合rate=0.5
Dense对特征做加权组合,输出类别概率units=1或2

对应代码:

from tensorflow.keras import Sequential, layers model = Sequential([ layers.Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 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(512, activation='relu'), layers.Dropout(0.5), layers.Dense(1, activation='sigmoid') ]) model.summary()

逻辑说明:输入是150x150x3的RGB图。第一层32个3x3卷积核输出150x150x32的特征图,池化之后变75x75x32;第二层64个卷积核到38x38x64;第三层128个卷积核到18x18x128。Flatten后约41万个特征值,经过Dense(512)映射到512维,最后由1个节点输出dog的概率。

参数说明:filters从32翻倍到128是常见模式,每次翻倍伴随特征图尺寸减半、通道数翻倍,信息容量不降。Dropout(0.5)放在唯一的大全连接层前面,是这里最关键的防过拟合参数。model.summary()用来核对参数量,如果训练集只有几千张,总参数量控制在1000万以内比较稳妥,超了就先删一层或把128改64。

3.2 输入尺寸怎么定,最后为什么输出1个节点而不是2个

为什么是150x150而不是原始图片大小?原始图片可能一张500x500、一张640x480,CNN需要固定输入尺寸才能成batch。150x150是速度和精度的折中点,再大到224x224可以更清晰,但训练时间基本翻倍,期末作业没必要。尺寸由ImageDataGenerator的target_size决定,模型里的input_shape=(150,150,3)必须和它一致,不一致时predict会直接报维度不匹配。

最后一个全连接层,二分类有两种写法,很多人在这里抄错:

# 写法A:sigmoid单节点,配套binary_crossentropy,推荐 layers.Dense(1, activation='sigmoid') # model.compile(loss='binary_crossentropy', ...) # 写法B:softmax双节点,配套categorical_crossentropy # layers.Dense(2, activation='softmax') # model.compile(loss='categorical_crossentropy', ...) # flow_from_directory 的 class_mode 要改成 'categorical'

逻辑说明:猫和狗互斥,一张图不是猫就是狗,sigmoid用一个浮点数表示是dog的概率,接近0是猫、接近1是狗,简单直接。softmax双节点是针对互斥多分类的通用形态,之后改成三分类(猫狗鸟)时要用它。

参数说明:用了写法B,class_mode必须同步改成categorical,predict返回的是二维向量,argmax之后才能拿到类别。到底是0代表猫还是1代表猫,以train_gen.class_indices打印结果为准,别靠“我觉得”,直接看字典。

4. 训练、调参与模型保存:把准确率从0.5拉进0.9的每个细节

4.1 fit还是fit_generator:旧教程的坑与新写法

很多旧教程会在训练时写model.fit_generator(train_gen, ...),TensorFlow 2.x里这个方法已经移除,直接换成model.fit即可。fit能自动识别生成器作为输入,不需要关心底层线程处理。这里要先确认框架版本,不要拿老代码硬试。

EPOCHS = 30 history = model.fit( train_gen, steps_per_epoch=train_gen.samples // BATCH_SIZE, epochs=EPOCHS, validation_data=val_gen, validation_steps=val_gen.samples // BATCH_SIZE, callbacks=callbacks, verbose=1 )

逻辑说明:train_gen.samples是生成器统计到的训练图片总数,除以BATCH_SIZE得到每个epoch需要跑几步。模型每走完一个epoch会看到一遍所有训练图,但每张图是随机增广后的新版本,所以本质上训练集被“放大”了好几倍。

参数说明:epochs=30只是上限,后面配了EarlyStopping会提前收,不要傻跑30个。validation_steps也显式指定,不然它会尝试把验证集完整扫一遍,在小数据集上拖时间。verbose=1让每轮打印loss和准确率,训练时能实时看到曲线有没有翻车。

4.2 三个回调把“玄学调参”变成机制:早停、检查点与学习率衰减

训练过程最怕两种状态:loss降不下去卡在0.5附近,或者降得太狠直接过拟合。两个问题都不靠肉眼盯着解决,靠回调。

from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau callbacks = [ EarlyStopping( monitor='val_loss', patience=5, restore_best_weights=True ), ModelCheckpoint( 'models/best_cat_dog.h5', monitor='val_loss', save_best_only=True ), ReduceLROnPlateau( monitor='val_loss', factor=0.5, patience=3, min_lr=1e-6 ) ]

逻辑说明:三个回调都盯着val_loss。EarlyStopping发现验证集loss连续5个epoch不创新低就停,restore_best_weights把模型权重回滚到历史最优,等于自带后悔药;ModelCheckpoint把最优状态存成h5文件;ReduceLROnPlateau在loss停滞3个epoch后把学习率减半,给模型一次“慢下来继续找”的机会,让卡在0.5的模型有机会脱困。

参数说明:patience=5比较温和,验证集噪声大时太小的patience会误停。factor=0.5是减半,min_lr设1e-6避免降到0。训练结束时,models/best_cat_dog.h5就是GUI要用的模型文件。

训练完顺手看一眼准确率曲线,是最快的验证方式:

import matplotlib.pyplot as plt plt.plot(history.history['accuracy'], label='train_acc') plt.plot(history.history['val_accuracy'], label='val_acc') plt.legend() plt.show()

如果val_acc曲线在后期明显掉头向下,说明过拟合,回到3.1把Dense改小或Dropout调高;如果train_acc和val_acc一起横盘在0.5附近,先看ReduceLROnPlateau有没有触发、验证集是不是从正确目录读进来的,别急着改网络。

5. 猫狗识别避坑手册:跑通之前必看的4个坎

这几条是学生交作业前最容易翻车的点,按命中概率从高到低排,每一条都见过不止一次。

5.1 训练集准确率接近100%,验证集是65%,过拟合怎么收

现象:history曲线里train_acc一路冲到0.98,val_acc在0.65附近震荡,两者之间差了一条鸿沟。

原因:模型参数远多于样本,Dense(512)对两千多张训练图来说过于奢侈;或者val_datagen里写进了rotation_range、horizontal_flip,验证分布被弄脏。Dropout没加或加在错误位置,也会让全连接层记住图片噪声。

解决:把Dense从512降到256,Dropout提高到0.5;检查val_datagen只保留rescale;EarlyStopping的patience调成5。这三步做完,val_acc通常能回到0.85以上。如果还不行,就把Conv第三层的128改64,减少整体参数。我给学生的建议是:先看模型参数量,参数量超过训练图片数就有过拟合风险,别急着堆层数。

5.2 GUI里点“识别”按钮卡死或无响应

现象:界面能打开,选完图片后程序直接转圈,点哪都没反应,过几分钟弹“无响应”。

原因:model.predict在Tkinter的主线程里同步执行,模型加载和预处理占用了消息循环,界面事件无法被处理。另一个常见问题是用了cv2.imread读中文路径,返回None,界面拿不到图片数据。

解决:把预测放到daemon线程里,主线程只负责更新Label。读图一律用PIL的Image.open,对中文路径更宽容,核心写法是threading.Thread(target=..., daemon=True).start()和root.after(0, update_ui)两行。这条坎几乎每届都有人踩,交作业前一定要用文件对话框多选几张图实测一遍。

5.3 模型训练很准,GUI预测却永远输出同一个类

现象:训练acc 0.98,但界面预测猫图输出dog,狗图也输出dog,感觉模型没生效。

原因:预测预处理和训练不一致。训练时图片被resize到150并除以255,而GUI预测时直接读取原图矩阵,尺寸、数值范围都不对。CNN对输入分布极其敏感,0-255和0-1的分布差100倍,输出大概率被压到某一侧。

解决:把所有预处理收敛到一个函数里,训练和预测共用同一套逻辑,别再各写各的:

import numpy as np from PIL import Image def preprocess_image(path): img = Image.open(path).convert('RGB').resize((150, 150)) arr = np.asarray(img, dtype=np.float32) / 255.0 return arr[np.newaxis, ...] # 变成 (1, 150, 150, 3)

逻辑说明:convert('RGB')防止四通道PNG报错,resize和训练target_size一致,/255.0对应训练时的rescale,newaxis补上batch维度。训练和预测同一段代码,至少能消灭一半问题。

参数说明:如果predict时报shape mismatch,多半是input_shape这和resize的尺寸没对齐,统一改成150即可。

5.4 找不到图片或类别目录:0 images与标签顺序反了

现象:flow_from_directory打印Found 0 images,或打印出的class_indices是{'cat': 1, 'dog': 0}而不是期待的{'cat': 0, 'dog': 1}。

原因:数据目录多包了一层,data/train/cat/2024/xxx.jpg会让Keras认为2024才是类别目录;类别顺序按目录名的字母序排,cat先读到就是0,和你的直觉无关。

解决:重新整理目录,让train/cat下面直接放jpg。标签顺序以打印出的train_gen.class_indices为准,GUI判断概率时也读这个映射。最稳的办法是拿到class_indices后,拿一张猫图、一张狗图分别验证一次,不要假设顺序。

提示:路径里不要用中文文件夹名,Windows下OpenCV的imread会直接失败,TensorFlow扫描也会有各种诡异问题。项目从建目录开始就全用英文。

6. 给期末大作业做一个能交的GUI:Tkinter布局与打包成exe

6.1 文件选择、图片预览与结果展示:12行核心逻辑

GUI部分用Tkinter,不依赖额外界面库,Python自带的就能交差。先加载第4章保存的模型,再写一个独立的recognize方法;预测放子线程,UI更新放主循环。

import threading import tkinter as tk from tkinter import filedialog from PIL import Image, ImageTk import numpy as np from tensorflow.keras.models import load_model model = load_model('models/best_cat_dog.h5') class App: def __init__(self): self.root = tk.Tk() self.root.title('猫狗识别 - 期末大作业') tk.Button(self.root, text='选择图片', command=self.select).pack(pady=10) self.lbl_img = tk.Label(self.root, text='暂无图片') self.lbl_img.pack(pady=10) self.lbl_res = tk.Label(self.root, text='预测结果:') self.lbl_res.pack(pady=10) def select(self): path = filedialog.askopenfilename( filetypes=[('图片', '*.jpg *.jpeg *.png')]) if not path: return img = Image.open(path).resize((200, 200)) self.tk_img = ImageTk.PhotoImage(img) self.lbl_img.config(image=self.tk_img) threading.Thread(target=self.recognize, args=(path,), daemon=True).start() def recognize(self, path): arr = preprocess_image(path) prob = float(model.predict(arr)[0][0]) label = 'dog' if prob > 0.5 else 'cat' self.root.after(0, lambda: self.lbl_res.config( text=f'预测结果:{label}(dog概率 {prob:.2f})')) app = App() app.root.mainloop()

逻辑说明:select里先选文件、再预览、最后启动子线程推理。ImageTk.PhotoImage必须赋值给self.tk_img,否则局部变量被回收后图片不显示。预测结果通过after(0, ...)排回主线程更新,避免直接在子线程改Tkinter控件。

参数说明:daemon=True保证窗口关闭时子线程立即终止,不会拖住进程;prob > 0.5对应sigmoid单节点的阈值,如果改了模型输出结构,这里也要同步改。

6.2 用PyInstaller打包成exe:一条命令,但留出两个耐心点

交作业通常要求能演示,不能只给.py和.h5。常见做法是用PyInstaller打包:

pip install pyinstaller pyinstaller -F -w --name cat_dog_gui gui.py

-F把程序压成单个exe,-w不出现命令行黑窗,--name指定输出名。打包后把models/best_cat_dog.h5放到exe同目录,代码里写相对路径就能找到。

要留两个耐心点:第一,TensorFlow被打进去后exe体积很大,首次启动要解压临时文件,等待十几秒是正常现象;第二,如果exe一闪而过,先用去掉-w的命令重新打包,在终端里看Traceback,多半是缺了某个数据文件,用--add-data补进去。如果还要交一份文档说明,建议只写三样:训练曲线截图、三个核心参数为什么这么设、GUI使用步骤,老师看的是思路,不是代码量。我自己的习惯是先把GUI在解释器里完整跑通一遍,确认模型加载路径和预测结果,再打包;顺序反了,很多时候“打包的锅”实际上是“预测预处理不一致”的锅。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询