猫狗图像分类实战:CNN/DNN/RNN对比与训练调优全指南
简介基于Python实现猫狗图像分类的完整项目资源面向深度学习与计算机视觉入门者围绕CNN、DNN、RNN三种网络结构系统演示图像分类任务从数据预处理、模型搭建、训练评估到保存部署的完整链路。CNN部分借助卷积、池化与全连接层自动提取边缘、纹理等层次特征DNN关注深层非线性变换及激活函数、残差等调优手段RNN则探讨其在图像序列场景下的应用边界适合课程设计、竞赛实践或算法对比。压缩包共9个文件包括3个Python脚本分别对应CNN/DNN/RNN模型实现、原始与灰度两种数据集压缩包、2份作业/实验文档、README及License整体约105MB目录划分明确可按模型或数据分支独立阅读。项目已有782人学习作者为s1t16内容既有可运行脚本也有配套数据和文档说明能帮助读者快速跑通实验并理解不同网络在图像分类中的表现差异同时掌握图像缩放、归一化、增强等预处理技巧。1. 猫狗图像分类CNN、DNN、RNN 三个脚本检验同一份数据的成色猫狗图像分类几乎是我见过入门深度学习时撞上的第一个完整项目也是把 CNN、DNN、RNN 三种网络放到同一批数据上做横向对比最合适的场景。这份资源解压后就是一组脚本加两份压缩包data.rar是原始彩色图data_gray.rar是灰度图核心代码就三个文件dog-or-cat_CNN.py、dog-or-cat_DNN.py、dog-or-cat_RNN.py。跑一遍会发现同一个分类任务换一种网络结构收敛速度和最终精度差得非常明显——卷积网络在图像上天然占优RNN 硬套图片则处处透着别扭。适合刚学完神经网络基础、想用代码把理论和实际对齐的开发者也适合课程作业需要做三类模型对比实验的人。2. 数据预处理与数据集读取灰度图、缩放、归一化各司其职2.1 原始图和灰度图两份数据集分别何时用压缩包里有data.rar和data_gray.rar两份数据集第一次打开容易懵。实际上这是项目有意为之的设计彩色图保留了 RGB 三个通道适合 CNN 在完整信息下提取特征灰度图把通道压成 1 个数据量直接缩到原来的三分之一。我一般这样分配CNN 用彩色图做完整实验DNN 用灰度图省内存RNN 也优先吃灰度图——因为 RNN 在图像上的表现本来就勉强输入维度越小越容易把序列模型训起来。如果机器配置一般灰度图配合 64×64 的分辨率单轮训练时间能缩短一半以上。数据集内部的目录结构常见的做法是按类别分文件夹data/ train/ cat/ cat_001.jpg ... dog/ dog_001.jpg ... val/ cat/ ... dog/ ... test/ cat/ ... dog/ ...这个结构的好处是ImageDataGenerator可以直接按目录自动打标签省去手写标签映射的麻烦。实践时注意train、val、test三份数据不要有重叠尤其要从同一个原始数据集里随机划分避免验证集和训练集出现相同图片导致评估虚高。2.2 图像读取、灰度转换与缩放的标准写法项目里的图像数据需要统一尺寸才能进模型。直接用 PIL 就能完成读取、转灰度、缩放和归一化的完整链路from PIL import Image import numpy as np def load_image(path, target_size(64, 64), as_grayTrue): img Image.open(path) if as_gray: img img.convert(L) # 转灰度单通道 else: img img.convert(RGB) # 保留三通道 img img.resize(target_size) # 统一尺寸 arr np.array(img, dtypenp.float32) arr arr / 255.0 # 归一化到 [0, 1] return arr这段代码里的每一行都有讲究。convert(L)是把 RGB 三个通道加权合并成一个亮度通道得到的数组形状是(64, 64)比彩色图的(64, 64, 3)少了三分之二的数据。resize这里直接用 PIL 默认的双线性插值对猫狗分类这种粗粒度任务足够。最后除以 255.0 是关键——深度学习对输入数值范围非常敏感0-255 的整数像素值会让梯度更新幅度不稳定归一化之后模型收敛速度会明显改善。2.3 手写批次生成器避免一次性把数据全塞进内存如果数据集比较大一次性把所有图片读进内存很容易把机器搞挂。这里用一个简单的生成器每次只取一批数据送入模型import os import numpy as np def batch_generator(folder, batch_size32, target_size(64, 64)): class_names sorted(os.listdir(folder)) # [cat, dog] file_paths [] labels [] for idx, cls in enumerate(class_names): class_dir os.path.join(folder, cls) for fname in os.listdir(class_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): file_paths.append(os.path.join(class_dir, fname)) labels.append(idx) # 打乱顺序避免模型学到样本顺序 indices np.arange(len(file_paths)) np.random.shuffle(indices) while True: for start in range(0, len(indices), batch_size): batch_idx indices[start:start batch_size] batch_x [] batch_y [] for i in batch_idx: batch_x.append(load_image(file_paths[i])) batch_y.append(labels[i]) yield np.array(batch_x), np.array(batch_y)生成器用while True无限循环这样model.fit才能持续取到数据。批大小batch_size32是一个比较稳妥的起点显存不够就降到 16。np.random.shuffle每次 epoch 之前重新洗牌防止模型记住样本的先后顺序。注意如果训练后期精度波动特别大先检查是不是生成器每次返回的批次顺序没真正打乱或者batch_size设得太大导致单批内类别不均衡。3. CNN/DNN/RNN 三个模型脚本架构设计、关键参数与训练入口3.1 CNN卷积加池化的层次特征提取dog-or-cat_CNN.py用的就是最经典的卷积结构。CNN 在图像上之所以有效是因为卷积核只在局部窗口内滑动参数量远小于全连接同时能捕捉到边缘、纹理这些空间特征。以 64×64 灰度图为输入一个能跑通的基础结构是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(64, 64, 1)), MaxPooling2D(pool_size(2, 2)), Conv2D(64, (3, 3), activationrelu), MaxPooling2D(pool_size(2, 2)), Flatten(), Dense(128, activationrelu), Dropout(0.5), Dense(1, activationsigmoid) ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy])这里两个卷积层分别用了 32 和 64 个卷积核感受野都是 3×3。第一个卷积层把原始图像从 1 个通道映射到 32 个特征图第二个卷积层再进一步抽象池化层每次把特征图尺寸减半(64,64)经过两次池化变成(16,16)这样后续 Flatten 后进入全连接层的参数量就小很多。Dropout 放在全连接前以 0.5 的概率随机丢掉一半神经元是防过拟合的关键一招。3.2 DNN把像素拉平后的全连接暴力拟合dog-or-cat_DNN.py里的模型没有卷积、池化这些结构直接把所有像素展开成一维向量再用多层全连接去拟合。如果输入是灰度图 64×64展平后就是 4096 维第一层全连接就可能有百万级参数from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout, Flatten model Sequential([ Flatten(input_shape(64, 64, 1)), Dense(512, activationrelu), Dropout(0.3), Dense(256, activationrelu), Dropout(0.3), Dense(128, activationrelu), Dense(1, activationsigmoid) ])DNN 在图像上的局限在于它完全没有空间先验——猫耳朵在图片左下角还是右上角对 DNN 来说是完全不同的输入模式。因此同一份数据下 DNN 往往需要更深更宽的网络才能逼近 CNN 的效果训练耗时和过拟合风险也同步上升。这里看到三个Dense层逐渐从 512 缩到 128这种逐步降维的结构比直接跳到输出层更容易学。3.3 RNN把图像按行切成序列的另类尝试dog-or-cat_RNN.py是全项目最让人眼前一亮的文件。RNN 原本处理的是序列数据比如文本里的 token 或时间序列上的采样点但这里把一张 64×64 的灰度图像按行切开把每一行像素看作一个时间步的输入64 行就是 64 个时间步每个时间步是一个 64 维的向量from tensorflow.keras.models import Sequential from tensorflow.keras.layers import SimpleRNN, Dense # 输入形状64 个时间步每个时间步 64 维 model Sequential([ SimpleRNN(64, input_shape(64, 64), activationtanh), Dense(32, activationrelu), Dense(1, activationsigmoid) ])这个思路的结构逻辑是模型按顺序读完图片的每一行通过隐藏状态记住之前行的重要信息最后汇总识别猫狗特征。从摘要里的描述也能看出RNN 在静态图像分类上并不占优因为图像的行与行之间虽然有空间关联但这种关联更适合卷积去捕捉而不是靠序列记忆。实际跑起来 RNN 的收敛速度通常是最慢的损失可能会长时间停留在 0.69 附近。3.4 三个模型的编译、训练与评估入口对比三个脚本除了网络结构不同训练部分的代码高度相似差异集中在输入形状和优化器参数上。一个可复用的训练入口像这样BATCH_SIZE 32 EPOCHS 30 train_gen batch_generator(data_gray/train, batch_sizeBATCH_SIZE) val_gen batch_generator(data_gray/val, batch_sizeBATCH_SIZE) history model.fit( train_gen, steps_per_epoch200, epochsEPOCHS, validation_dataval_gen, validation_steps50, verbose1 )steps_per_epoch200表示每个 epoch 从生成器取 200 批数据也就是 6400 张训练图validation_steps50对应 1600 张验证图。这两个数字要跟数据集大小匹配如果数据量没这么大可以设小一点。verbose1是为了能看到每一轮的 loss 和 accuracy 变化方便判断训练状态。4. 训练避坑把常见跑崩、不收敛、过拟合一次说清4.1 输入形状对不上卷积层连第一行代码都跑不过现象model.fit刚执行就报错提示expected min_ndim4, found ndim3或者ValueError: Input 0 of layer conv2d is incompatible。原因卷积层的输入要求是四维张量(batch, height, width, channels)但数据预处理时只把单张图处理成了(64, 64)喂给模型时少了通道维和批次维。灰度图常犯这个错因为convert(L)出来的数组天然是二维的。解决在数据进模型之前补上维度img np.expand_dims(img, axis-1) # (64, 64) - (64, 64, 1) batch_x np.expand_dims(batch_x, axis0) # 单张预测时 (1, 64, 64, 1)4.2 训练损失卡在 0.69模型根本没开始学习现象RNN 脚本训练了十几个 epochloss 一直稳定在 0.69 左右accuracy 始终是 50% 上下怎么调学习率都不动。原因二分类交叉熵在这个案例里0.69 恰好是模型输出概率恒为 0.5 时对应的损失值说明梯度基本没起作用。RNN 用 tanh 激活处理 64 维的行向量输入信息经过这么多时间步传递很容易被稀释前向传播的梯度幅度本来就小。加上图像行与行之间的相关性弱于文本词与词之间的相关性模型抓不到可记忆的规律。解决换个思路。把损失函数检查一遍确认用的是binary_crossentropy而不是categorical_crossentropy然后把SimpleRNN换成LSTM或GRU这两个结构有门控机制能更好地保留长距离信息最后把学习率从默认的0.001降到0.0003试几个 epoch。4.3 训练精度九成多、验证精度七成过拟合的典型信号现象CNN 模型在训练集上精度接近 0.95但验证集精度始终在 0.75 上下徘徊而且两者差距越来越大损失曲线呈喇叭口。原因训练轮数超过模型容量所需的上限模型开始把训练样本的噪声也记下来了。这是小数据集上最常见的问题——猫狗数据集通常只有几千张图CNN 动辄几十万参数量完全有容量“背”下所有训练样本。解决优先往模型里加 Dropout 层把Dropout(0.5)放到全连接层之前同时用EarlyStopping回调验证损失连续 3 个 epoch 不下降就停from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping(monitorval_loss, patience3, restore_best_weightsTrue) model.fit(..., callbacks[early_stop])4.4 忘记归一化损失曲线像心电图一样上下跳现象DNN 脚本的 loss 在训练过程中剧烈震荡从 0.8 掉到 0.4 又弹回 0.9accuracy 也极不稳定但并不是数据问题。原因输入像素值保持 0-255 的整数范围经过多层全连接后每一层的输出数值范围被不断放大梯度计算容易出现极端值。这和 4.2 里 RNN 损失不动是相反的极端——数值范围太大导致梯度爆炸。解决在数据预处理阶段统一除以 255.0把像素值压到[0, 1]区间如果做完归一化后仍然震荡可以再加一步标准化把均值归零、方差归一。建议在load_image函数里就处理掉避免每个脚本重复改。5. 参数调优与模型评估把训练曲线、混淆矩阵和分类报告用起来5.1 批大小、学习率、训练轮数怎么搭配三个脚本里的超参数不是随便写的它们之间有明显的联动关系。常用的起步配置如下参数建议值调整逻辑批大小32显存不够降到 16数据集太小降到 8学习率0.001损失震荡时降到 0.0003收敛慢时可试 0.003训练轮数30配合 EarlyStopping实际轮数以早停为准优化器Adam自适应学习率省去手动调学习率衰减的麻烦批大小和学习率的联动关系值得留意批大小从 32 提到 128 时梯度计算用的样本更多方向更稳定可以尝试把学习率同步调大反过来批大小减小学习率也要相应降低否则单批的噪声会让参数更新跳来跳去。训练轮数不要死守 30用早停机制比硬编码轮数更靠谱。5.2 从训练曲线判断欠拟合与过拟合而不是只看最终精度只看最后一个 epoch 的 accuracy 很容易被误导。更稳妥的做法是把整个history对象里的损失和精度画出来看趋势区分三种状态训练损失和验证损失都高说明欠拟合需要加模型容量训练损失降、验证损失不降反升说明过拟合需要加 Dropout 或减少轮数两者一起缓慢下降且差距不大才是健康状态。import matplotlib.pyplot as plt plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.savefig(loss_curve.png)图中如果验证损失在某个 epoch 之后开始陡峭回升那个转折点就是早停应该发挥作用的位置。实践中我常发现模型在第 15 轮左右已经开始过拟合但最终精度看起来还挺高——这就是只看数字不看曲线的坑。5.3 用混淆矩阵和分类报告量化模型短板accuracy 对类别不均衡的数据集有天然局限。猫狗数据集如果猫图比狗图多模型把狗全认成猫accuracy 可能还有 60% 以上但实际已经废了。用sklearn的分类报告能更清楚地看出两个类别各自的精准率、召回率和 F1 值from sklearn.metrics import classification_report, confusion_matrix y_true [] # 真实标签 y_pred [] # 模型预测标签 model load_model(cat_dog_cnn.h5) for img_path, label in test_samples: prob predict_image(img_path, model) y_true.append(label) y_pred.append(1 if prob 0.5 else 0) print(classification_report(y_true, y_pred, target_names[cat, dog])) print(confusion_matrix(y_true, y_pred))分类报告里如果 cat 的召回率明显低于 dog说明模型把一部分猫误判成了狗结合混淆矩阵能看到具体数量。这个步骤一般在三份模型评估对比时都会用CNN、DNN、RNN 各出一份报告优劣一目了然。6. 模型保存、加载与预测实战把训练好的权重变成可用工具6.1 保存 H5 与 SavedModel 两种格式的取舍模型训练完不是关掉脚本就结束了。把模型权重存下来后续做预测、对比实验、甚至部署都能直接复用。TensorFlow 提供两种主流保存方式H5 文件适合单机复用一个文件打包了结构和权重SavedModel 格式则适合服务化部署。model.save(cat_dog_cnn.h5)保存重点是别混淆save和save_weights——save_weights只存权重不存结构加载的时候必须重新构建一模一样的网络非常容易出错。.h5文件则完整保留了网络结构和训练好的参数加载时不需要再定义模型结构。6.2 加载模型对单张图像做预测的完整函数预测阶段要复用数据预处理时的规则输入尺寸、灰度转换、归一化必须和训练时完全一致否则模型会收到无法识别的输入分布from tensorflow.keras.models import load_model from PIL import Image import numpy as np model load_model(cat_dog_cnn.h5) def predict_single_image(model, img_path, target_size(64, 64)): img Image.open(img_path).convert(L) img img.resize(target_size) arr np.array(img, dtypenp.float32).reshape(1, 64, 64, 1) / 255.0 prob model.predict(arr, verbose0)[0][0] return prob prob predict_single_image(model, test_dog.jpg) print(是狗的概率: {:.2f}%.format(prob * 100))reshape(1, 64, 64, 1)这一步是从单张二维灰度图拼出四维张量批次维度是 1通道维度是 1。predict返回的[0][0]是批次第一张图在输出层的值经过sigmoid后的范围是 0 到 1大于 0.5 判定为狗小于 0.5 判定为猫。6.3 顺手做一个批量预测的脚本验证鲁棒性单张预测跑通之后可以再写一个批量预测脚本对测试集里所有图片跑一遍统计准确率。这样既能验证模型的稳定性也能发现某些特定图像角度的误判import os def predict_folder(model, folder_path, threshold0.5): correct 0 total 0 for cls, label in [(cat, 0), (dog, 1)]: cls_dir os.path.join(folder_path, cls) for fname in os.listdir(cls_dir): file_path os.path.join(cls_dir, fname) prob predict_single_image(model, file_path) pred 1 if prob threshold else 0 if pred label: correct 1 total 1 print(f准确率: {correct / total:.2%} ({correct}/{total}))批量预测的价值在于它能暴露训练时看不到的问题。比如模型对白底照片里的白猫识别率很高但对暗光环境下的黑猫准确率骤降——这类问题在单张测试里看不出来批量统计后一目了然。从那以后我每次跑完模型都会先过一遍批量预测脚本确认准确率和训练时的验证集数字没有大幅偏差再考虑是否进入部署环节这个习惯帮我避免了好几次在测试集上翻车。希望帮到你。本文还有配套的精品资源点击获取