简介:《基于Keras的猫狗分类识别实验报告》共22页,是作者原创的机器学习期末大作业,面向需要完成图像分类实验报告的高校学生。报告以Kaggle经典猫狗赛题为背景,结构完整,涵盖摘要、引言、算法原理分析、数据组成、实验与结果分析、结论及参考文献,并包含算法流程图、运行结果截图和核心代码,便于读者对照复现。实现上,报告基于CNN和AlexNet网络结构,利用ImageNet预训练权重进行迁移学习;针对训练集标签异常问题,设计了一套创新预处理流程:用预训练模型预测训练集图片,分别微调猫和狗的top参数来筛查异常样本,得到24964张猫狗数量均衡的图像,并重新编号排序。资源包内只有1个docx文档,大小5.16MB。目前已有7899人学习下载,是机器学习课程设计、期末报告撰写的实用参考资料。 又到期末季,机器学习课程的期末大作业,我选了基于Keras的猫狗分类识别——经典到不能再经典的图片分类任务,却恰好能把卷积神经网络、数据增强、训练验证这些核心知识点全部串起来。这篇文章相当于一份完整的实验记录,从环境搭建、数据准备、模型设计、训练调参到结果分析都拆开讲,代码和参数都给出可复现的写法,适合正在做机器学习课程作业、或者刚入门深度学习想跑通第一个图像分类项目的同学参考。
1. 为什么选猫狗分类这个小项目,以及环境怎么搭
1.1 经典任务背后的完整学习链路
很多人觉得猫狗分类太“入门”了,不愿意选。但恰恰是这个题目,把深度学习的几个关键环节全部覆盖了:数据集怎么组织、图片怎么预处理、网络结构怎么设计、训练过程怎么监控、结果怎么评估。换一个更复杂的任务,比如细粒度分类或目标检测,课程报告反而容易变成“调参流水账”,因为核心知识点全被预训练模型和复杂框架遮住了。
另外,猫狗分类的数据集非常容易获取,Kaggle上的Dogs vs Cats数据集,25000张训练图片,猫狗各半,够训练一个从零搭建的小型卷积神经网络。这个数据量对CPU训练比较友好,对显存要求也不高,不至于让没有GPU的同学卡在第一步。
1.2 环境配置与版本依赖避坑
我的实验环境如下,直接给出来供参考:
| 项目 | 版本/配置 |
|---|---|
| 操作系统 | Ubuntu 20.04(Windows 10也可以,命令基本一致) |
| Python | 3.9 |
| TensorFlow | 2.10.0 |
| Keras | 使用TensorFlow内置的tf.keras |
| Jupyter Notebook | 用于逐步记录和可视化 |
| CPU/GPU | 有NVIDIA显卡更好,没有也可以跑 |
安装命令很简单:
pip install tensorflow==2.10.0 pip install jupyter这里有个容易踩的版本坑:从TensorFlow 2.x开始,Keras已经内置为tf.keras,直接安装TensorFlow就能用,不需要再单独pip install keras。如果你单独装了新版Keras 3.x,反而可能和TensorFlow自带版本产生冲突。建议代码里统一用from tensorflow import keras这种写法,保证环境干净。
数据准备和模型训练全在Jupyter Notebook里完成,单元格逐段执行,方便截图表、写注释,交报告时直接导出PDF或HTML,非常省事。硬件方面,我的机器有8GB显存,训练一个epoch大约60秒,如果你的CPU训练,每个epoch可能要3到5分钟,也能接受,只是耐心一点。
2. 数据集的获取、整理和预处理细节
2.1 数据划分与目录结构
Kaggle原始数据解压之后是PetImages目录,里面有Cat和Dog两个文件夹,总共25000张图。但实验报告里不能直接拿全部数据训练,必须划分训练集和验证集。我的划分策略是:训练集20000张,验证集5000张,猫狗各占一半。这个比例对这个小模型来说足够,验证集留大一点,评估结果更稳定。
目录结构如下:
data/ ├── train/ │ ├── cats/ │ └── dogs/ └── validation/ ├── cats/ └── dogs/划分数据的脚本不复杂,核心就是按文件列表复制图片:
import os import shutil source_dir = 'PetImages' # 原始解压目录 base_dir = 'data' train_dir = os.path.join(base_dir, 'train') val_dir = os.path.join(base_dir, 'validation') cats = os.listdir(os.path.join(source_dir, 'Cat')) dogs = os.listdir(os.path.join(source_dir, 'Dog')) def copy_files(filenames, source_class, target_class, target_dir): os.makedirs(os.path.join(target_dir, target_class), exist_ok=True) for fname in filenames: src = os.path.join(source_dir, source_class, fname) dst = os.path.join(target_dir, target_class, fname) shutil.copyfile(src, dst) # 每个类取前10000张进训练集,后2500张进验证集 copy_files(cats[:10000], 'Cat', 'cats', train_dir) copy_files(dogs[:10000], 'Dog', 'dogs', train_dir) copy_files(cats[10000:12500], 'Cat', 'cats', val_dir) copy_files(dogs[10000:12500], 'Dog', 'dogs', val_dir)注意一个真实存在的坑:Kaggle原始数据里有些jpg文件其实是损坏的,直接喂给模型训练会报错。最稳妥的做法是训练前用PIL检查一遍,删掉坏图:
from PIL import Image for root, dirs, files in os.walk(base_dir): for file in files: path = os.path.join(root, file) try: img = Image.open(path) img.verify() except Exception: print('删除损坏文件:', path) os.remove(path)这个步骤看起来多余,但真的能帮你省下后面排查“莫名其妙训练中断”的时间,我记得我当时删掉了三十多张坏图。
2.2 ImageDataGenerator:不做增强就亏了
图片数据不能直接灌进网络,需要统一尺寸,同时做归一化。我用的是Keras的ImageDataGenerator,它最方便的地方是把图片读取、缩放、归一化、batch生成全部封装好了,配合flow_from_directory直接从文件夹读取数据,不用自己写数据加载器。
关键参数说明:
rescale=1./255:把像素值从0到255缩放到0到1。神经网络对这样的小数值收敛更快,这是所有图像任务的第一步。validation_split:如果不想手动划目录,也可以直接用这个参数按比例切分,但我更推荐显式划分,逻辑更清楚。- 训练集加了数据增强,验证集不做增强,只做rescale。因为验证集要模拟“真实场景”,不能人为扰动。
数据增强的配置:
train_datagen = ImageDataGenerator( rescale=1./255, rotation_range=40, 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./255) train_generator = train_datagen.flow_from_directory( train_dir, target_size=(150, 150), batch_size=32, class_mode='categorical' ) val_generator = val_datagen.flow_from_directory( val_dir, target_size=(150, 150), batch_size=32, class_mode='categorical' )数据增强的作用在网上被很多人一句带过,但我觉得值得说透一点:猫的照片不一定是正着的,可能有旋转、有缩放、有左右翻转。如果模型只能在“规规矩矩”的正方形图片上识别猫,换一张角度刁钻的图就废了。rotation_range、width_shift_range这些参数相当于在训练时不断给模型出“变形题”,强迫它学习到“猫的本质特征”,而不是死记硬背某一张图的像素分布。这招对抑制过拟合非常有效,尤其在这个两万张图的小数据集上。
目标尺寸我选的150x150,没有用224x224。原因很简单:模型小、参数少,150x150足够表达猫狗分类所需的空间特征,CPU训练速度也能快不少。后文如果换用迁移学习,才需要把输入尺寸提到224x224去匹配预训练模型。
class_mode='categorical'生成One-hot标签,比如cat对应[1, 0],dog对应[0, 1];如果只想做二分类,也可以改成'binary'配合sigmoid输出,效果差别不大。我做实验报告时用了categorical,因为One-hot编码配合softmax在报告里讲起来更通用,方便扩展到多分类。
3. 卷积神经网络结构设计:小模型也有讲究
3.1 结构设计思路
模型结构我参考了经典VGG的思路,没有做得太深,因为两万张训练图对深层网络来说还是偏少。网络太深容易出现两个问题:一是过拟合,模型把训练集“背”下来了;二是训练时间太长,不利于反复实验。
我的结构是“卷积+池化”重复四组,最后接全连接层和Dropout:
from tensorflow import keras from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dropout, Dense model = keras.Sequential([ Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3)), MaxPooling2D(2, 2), Conv2D(64, (3, 3), activation='relu'), MaxPooling2D(2, 2), Conv2D(128, (3, 3), activation='relu'), MaxPooling2D(2, 2), Conv2D(128, (3, 3), activation='relu'), MaxPooling2D(2, 2), Flatten(), Dropout(0.5), Dense(512, activation='relu'), Dense(2, activation='softmax') ])几个结构上的考量值得写进报告:
第一组卷积核从32开始,逐层翻倍到64、128、128,这个做法在多数CNN里很常见。浅层卷积捕捉边缘、颜色块这些低级特征,卷积核可以少一些;深层卷积组合出更抽象的形状特征,需要更多通道来容纳信息。
三层卷积核全部用3x3,而不是5x5或7x7。原因在于两个3x3卷积堆叠的感受野等于一个5x5卷积,但参数量只有后者的18分之25左右,非线性表达还更强。这段在报告中写出来很加分,能体现你确实理解了结构背后的原理。
最后连上512维全连接层前,先加了一个Dropout(0.5)。Dropout在训练时随机“关闭”一半神经元,逼着网络不依赖某几个特定节点,等于做了模型集成,是抑制过拟合的一大杀器。我用的是0.5,这是经验值,太小没效果,太大模型学不动。
输入尺寸150x150x3,这里的3是RGB三个通道。输出层2个神经元用softmax,把输出变成“猫概率+狗概率=1”的分布,取概率大的那个作为预测类别。
3.2 参数量计算与可视化
用model.summary()可以直接看到每层参数量:
Total params: 2,905,154 Trainable params: 2,905,154 Non-trainable params: 0290万参数,放在现在的眼光看非常轻量。全连接层占了其中大头,卷积层主要计算量在浮点运算而非参数量。做实验报告时,我建议把summary的输出截图放进附录,再结合参数量解释:“卷积层通过权值共享大幅减少了参数数量,比如一个3x3卷积核在整张图上滑动,同一组参数被重复使用,而不是每个像素位置都存一套参数。”这句解释比贴一堆公式更容易让老师看出你真的理解。
3.3 为什么不用迁移学习当主力
很多人交作业喜欢直接上VGG16、ResNet50做迁移学习,准确率动辄95%以上。但我这次故意没把迁移学习作为主模型,只作为最后的加分尝试。原因很简单:期末大作业考察的是你是否掌握卷积网络的基本构造和训练流程。如果直接加载预训练权重,核心工作变成“特征提取+微调”,对卷积层本身的原理理解反而被跳过了。先让从零训练的小模型跑到80%多准确率,再谈迁移学习,报告的逻辑链条才完整。
4. 训练过程与参数调优
4.1 编译参数选择
模型编译时我用了Adam优化器,学习率默认0.001。SGD加动量在这个任务上也能收敛,但Adam自适应调整每个参数的学习率,省去了手动调学习率表的大量时间,对小模型来说非常省心。损失函数用categorical_crossentropy,和One-hot标签配对。
model.compile( loss='categorical_crossentropy', optimizer=keras.optimizers.Adam(learning_rate=0.001), metrics=['accuracy'] )4.2 回调函数:训练不用时刻盯着
训练过程里我挂了三个回调:
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau checkpoint = ModelCheckpoint( 'cats_dogs_model.h5', monitor='val_accuracy', save_best_only=True ) early_stop = EarlyStopping( monitor='val_loss', patience=5, restore_best_weights=True ) reduce_lr = ReduceLROnPlateau( monitor='val_loss', factor=0.2, patience=3, min_lr=1e-6 )这三个回调的功能分别说清楚:
ModelCheckpoint会在每个epoch结束后检查验证集准确率,只有比之前最好的一轮更好才保存模型。这样就算后面过拟合了,磁盘上也留着一个最佳模型。
EarlyStopping是防过拟合的保险丝。验证集loss连续5个epoch不下降就自动停止训练,并恢复到验证集最好的那一次权重。我设置epochs=30,但实际跑下来往往只训练十几轮就停了,原因就是验证loss不再下降。这在报告里属于正常现象,不是bug。
ReduceLROnPlateau配合EarlyStopping使用,验证loss连续3轮不下降时,学习率缩小为原来的五分之一。训练后期损失曲线会进入平台期,固定的大学习率会在最优点附近来回震荡,减小学习率可以让loss继续优化到更深处。
训练代码:
history = model.fit( train_generator, steps_per_epoch=20000 // 32, epochs=30, validation_data=val_generator, validation_steps=5000 // 32, callbacks=[checkpoint, early_stop, reduce_lr] )steps_per_epoch填的是每个epoch用多少个batch。我这里显式写了20000除以32,等于625个batch走完一遍训练集。如果不填,Keras会自动推断,但填上能让报告里的参数关系更清楚。
4.3 训练日志解读
训练时的输出大致长这样:
Epoch 1/30 625/625 [==============================] - 60s 96ms/step - loss: 0.6895 - accuracy: 0.5261 - val_loss: 0.6501 - val_accuracy: 0.6200 Epoch 2/30 625/625 [==============================] - 58s 93ms/step - loss: 0.6214 - accuracy: 0.6470 - val_loss: 0.5802 - val_accuracy: 0.6861 Epoch 3/30 625/625 [==============================] - 58s 92ms/step - loss: 0.5554 - accuracy: 0.7151 - val_loss: 0.5318 - val_accuracy: 0.7358 ... Epoch 12/30 625/625 [==============================] - 58s 93ms/step - loss: 0.3547 - accuracy: 0.8350 - val_loss: 0.3407 - val_accuracy: 0.8630第一个epoch准确率只有0.52左右,跟随机猜差不多,正常。到第三个epoch就明显涨了,这说明模型确实在学,而不是卡在欠拟合。最终验证集准确率停在86%上下,是一个从零训练小模型的合理水平。
训练8GB显存大约每epoch一分钟,CPU的话大约五分钟,时间成本可以接受。我开头说过没有GPU也能跑到这个准确率,只是需要耐心,亲测可行。
5. 结果评估:曲线、混淆矩阵和单张图预测
5.1 训练曲线怎么判读
训练结束后,把history里的数据画成曲线是实验报告必备环节:
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'] epochs = range(1, len(acc) + 1) plt.plot(epochs, acc, 'b', label='Training acc') plt.plot(epochs, val_acc, 'r', label='Validation acc') plt.title('Training and validation accuracy') plt.legend() plt.show() plt.plot(epochs, loss, 'b', label='Training loss') plt.plot(epochs, val_loss, 'r', label='Validation loss') plt.title('Training and validation loss') plt.legend() plt.show()图上最值得关注的不是最终数值,而是两条曲线的距离。理想状态是训练曲线和验证曲线贴得比较近,说明模型在“举一反三”。如果训练准确率坐到95%,验证准确率还趴在80%,那就是典型的过拟合——模型把训练图背下来了,遇到没见过的图就露馅。
我的实验里两条曲线始终比较贴近,这说明数据增强加Dropout的组合拳打对了。在报告里你可以重点描述这个现象,再补充一句:“数据增强让同一条数据在不同epoch以不同形态出现,相当于暗中扩大了训练集规模,这是缓解过拟合的关键。”
5.2 混淆矩阵算清楚每种错误
准确率只是一个粗粒度指标,想要知道模型具体错在哪里,要画混淆矩阵:
import numpy as np from sklearn.metrics import confusion_matrix val_generator.reset() pred = model.predict(val_generator) pred_classes = np.argmax(pred, axis=1) true_classes = val_generator.classes cm = confusion_matrix(true_classes, pred_classes) print(cm)得到的矩阵类似:
[[1201 49] [ 74 1176]]第一行是真实猫,第二行是真实狗。对角线是预测正确的数量,上三角是“把猫认成狗”,下三角是“把狗认成猫”。从数字上看,模型把狗认成猫的次数(74)比把猫认成狗(49)多一些。这说明模型学到的“猫特征”泛化得更好一点,可能因为猫脸轮廓和耳朵形状的共性更强。这个细节写进报告,老师会觉得你不只是跑了个模型,而是真的在分析结果。
5.3 单张图片预测展示
最后在报告里放几张真实照片,跑一下预测,肉眼确认效果:
from tensorflow.keras.preprocessing import image def predict_image(img_path): img = image.load_img(img_path, target_size=(150, 150)) img_tensor = image.img_to_array(img) / 255.0 img_tensor = np.expand_dims(img_tensor, axis=0) pred = model.predict(img_tensor, verbose=0) class_names = ['cat', 'dog'] label = class_names[np.argmax(pred)] confidence = np.max(pred) print(f'图片路径: {img_path}, 预测类别: {label}, 置信度: {confidence:.4f}') return label, confidence我自己试了几张网络上找的猫狗图,大部分预测正确,置信度在0.8到0.99之间。偶尔有一张黑白老照片里的猫被认错,这很合理,因为训练集里彩色图片占绝大多数,灰度图像特征分布偏离了训练分布。遇到这种情况,可以在报告里如实写出来,然后分析原因,比只展示成功案例更有说服力。
6. 整个实验里踩过的坑和可以加分的改进方向
6.1 我实际遇到过的几个问题
训练过程中我先后遇到过四个比较有代表性的问题,逐个说下原因和解决办法。
第一个是第一次训练没用数据增强,验证准确率卡在78%上下,训练准确率却一路飙到93%。这就是典型的过拟合。加了增强后,验证准确率直接跳到85%以上,效果立竿见影。如果你的模型也出现“训练高、验证低”的剪刀差,优先怀疑数据量不够,而不是模型太弱。
第二个坑是flow_from_directory的shuffle参数。训练生成器默认shuffle=True,但验证生成器默认不shuffle。在预测和算混淆矩阵时,验证生成器的顺序固定,但predict返回的预测顺序和generator.classes的顺序是对应的,前提是你不要在中途reset两次导致顺序错乱。我建议在预测前先执行一次val_generator.reset(),保证从头开始算。
第三个问题是学习率设太大,loss变成NaN。这是新手最容易懵的情况。解决办法是先把学习率降到0.0001重新训练,或者加梯度裁剪。如果发现某一步loss突然变成nan,不用重头再来,把学习率调小后再编译、再fit几轮通常就能恢复。
第四个问题是保存模型后加载报错。如果你在模型里用了自定义层或自定义函数,model.save()默认的.h5格式可能没法直接load_model。这次实验都是标准层,没遇到,但我后来做迁移学习时自定义了层,踩过这个坑,印象很深。标准模型的保存用model.save('文件名.h5')即可,加载用keras.models.load_model,不用踩别的坑。
6.2 把报告从80分提到90分的几个方向
如果时间允许,下面几个方向对分数提升非常明显:
- 迁移学习对比实验:用VGG16预训练权重做特征提取,冻结卷积层只训练顶部分类器,准确率基本能到95%以上。在报告里加一节“从零训练与迁移学习的对比”,体现你对两种方案的权衡有认识。
- 数据清洗和统计:统计训练集里猫图、狗图的尺寸分布和通道分布,分析模型可能对哪些图片效果差,比如低亮度、模糊、多只动物同时出现。这些分析不需要额外代码,写好就是亮点。
- 模型结构可视化:用
keras.utils.plot_model画出网络结构图,配上model.summary()输出,图文并茂,符合“图文报告”的要求。 - 超参数敏感性实验:固定其他变量,只修改batch_size或dropout比例,看准确率怎么变化。哪怕只做两个设置,报告也会显得有实验设计能力。
最后再分享一点我个人的体会:深度学习课程作业最重要的不是准确率数字好看,而是把每一个环节的“为什么”讲清楚。猫狗分类听起来老套,但当我真的从数据准备、模型搭建、训练调参一路走下来,才明白课程里那些概念在真实项目里是怎么咬合在一起的。整个过程给我最大的收获是“训练深度学习模型不是一个一键完成的黑盒操作,而是一条需要在每个环节做决策的流水线”。希望你做完这个实验后,也能有同样的感受,那这篇报告的价值就真正达到了。
本文还有配套的精品资源,点击获取