猫狗分类这个项目,几乎每个接触深度学习的人都会做一遍。但真把它从“跑通示例代码”变成“自己动手从头实现一遍”,并且连数据处理、模型训练、结果评估整个链路都搞清楚,很多人是在这个项目上完成的第一次完整闭环。我最初接触TensorFlow/Keras时,就是用类似的二分类图像任务练手,当时在Jupyter Notebook里反复改参数、重跑训练、观察损失曲线,踩了不少坑,也正因为这些坑,反而把图像分类的底层逻辑吃透了。
这篇文章就把我当时实现猫狗分类模型的全过程整理出来,包括为什么选这套技术栈、数据怎么处理、网络结构怎么定、训练时怎么观察状态,以及最容易被忽视的那些小问题。无论你是刚入门深度学习,还是已经在别的框架上写过代码想补一下TensorFlow的流程,这份实践笔记应该都能给你省点时间。
1. 为什么拿猫狗分类练手是件“正确的小事”
1.1 这个项目到底覆盖了哪些能力
图像分类任务看起来只是“给一张图判断是猫还是狗”,但它本质上覆盖了深度学习应用里几乎全部的标准环节:数据准备、数据预处理、模型构建、损失函数设计、训练迭代、性能评估。把这个项目的全流程走通一遍,以后再接触目标检测、图像分割、甚至文本分类,思路都能很快迁移过去。
从问题难度来看,猫狗二分类又足够简单。类别只有两个,不需要处理复杂的多标签逻辑;图片内容结构清晰,特征差异明显;公开数据集规模适中,单张图片尺寸不大,在普通笔记本上几分钟内就能完成一轮训练。这意味着你可以快速验证自己的改动,不用担心一次实验就等好几个小时。
此外,这个项目还隐藏了一个重要的学习点——过拟合。样本量不大、模型容量又足够时,训练集准确率一路飙升、验证集却上不去的现象非常典型。你能亲眼看到正则化、数据增强等手段是如何把泛化能力拉回来的,这种直观感受比看任何理论都深刻。
1.2 为什么选择TensorFlow/Keras与Jupyter Notebook
深度学习框架选型上,TensorFlow配合Keras是我当时最顺手的选择,原因有三点。第一,Keras的高层API足够简洁,模型定义、编译、训练几步就能完成,不需要手动推导反向传播,对初学者非常友好;第二,TensorFlow的生态完善,数据加载、图像预处理、模型可视化都有现成组件,不用反复造轮子;第三,相关教程和踩坑资料极其丰富,遇到问题很容易找到参考思路。
Jupyter Notebook对于这类实验性项目也很合适。它允许你按单元格分步执行,数据加载、预处理、模型定义、训练过程都可以独立运行和修改,这种交互式体验非常贴近“探索”的工作方式。训练中途发现某个参数不合理,直接改掉再重启对应单元格即可,而不用像写脚本那样从头执行,调试效率高很多。
提示:如果你之后要跑大规模模型或部署到生产环境,脚本化、模块化是必然趋势。但在学习阶段,Notebook的灵活性和可视化优势非常突出,这也是我坚持用它做实验的原因。
2. 动手前的准备:数据、环境与目录设计
2.1 数据集长什么样,目录怎么组织
猫狗分类最常用的数据集是包含25000张图片的经典版本,猫狗各约12500张。不过实际使用中没必要一次全上,我通常会按8:2或者7:2:1把训练集、验证集、测试集分开。培养一个良好的工程习惯:数据集的目录结构从一开始就按用途划分,而不是把所有图片堆在一起再靠文件名去区分。
我当时采用的目录结构是这样的:
data/ ├── train/ │ ├── cats/ │ │ ├── cat.0.jpg │ │ └── ... │ └── dogs/ │ ├── dog.0.jpg │ └── ... ├── val/ │ ├── cats/ │ │ └── ... │ └── dogs/ │ └── ... └── test/ ├── cats/ └── dogs/这种按类别分目录的方式,配合数据加载工具能省去大量手动标注工作。比如TensorFlow的image_dataset_from_directory方法会自动把子目录名作为类别标签,这个设计非常实用,合理组织目录结构后几乎不需要额外写标签映射代码。
2.2 环境准备与依赖版本
我用的环境是纯CPU版本的TensorFlow,因为猫狗分类任务对算力要求并不苛刻。建议安装时明确指定版本,避免因API变动导致代码跑不通。我的环境大致如下:
- Python 3.9或3.10
- TensorFlow 2.10以上(CPU版即可,NVIDIA GPU环境可安装GPU版)
- Jupyter Notebook(Anaconda自带)
- Matplotlib用于画训练曲线
- NumPy用于数组操作
安装命令很简单:
pip install tensorflow-cpu matplotlib numpy jupyter如果想用GPU加速,需要提前配置CUDA和cuDNN,这部分步骤比较繁琐,建议新手先在CPU环境跑通流程,再考虑GPU优化。
2.3 如果本地没有现成数据集
如果没有现成的数据集,可以通过代码从网上获取公开数据。我这里假设你已经把数据下载好并整理成上面的目录结构。要注意的是,很多公开数据集的图片尺寸不一,后面处理时需要统一缩放到固定尺寸,这一步在数据加载阶段完成。
3. 数据加载与预处理:从文件夹到训练张量
3.1 用image_dataset_from_directory一步到位
TensorFlow的image_dataset_from_directory是加载图像分类数据的利器,它直接从目录结构生成数据集对象,自动完成图片读取、标签匹配、shuffle和批次划分。关键参数是validation_split,它可以从同一目录中按比例自动拆分训练集和验证集,省去手动切分的麻烦。
from tensorflow.keras.preprocessing import image_dataset_from_directory train_ds = image_dataset_from_directory( 'data/train', validation_split=0.2, subset='training', seed=123, image_size=(150, 150), batch_size=32 ) val_ds = image_dataset_from_directory( 'data/train', validation_split=0.2, subset='validation', seed=123, image_size=(150, 150), batch_size=32 )这里有个细节容易踩坑:两次调用必须使用相同的seed,否则训练集和验证集会出现图片重叠。我第一次写的时候没加seed,结果训练效果虚高,当时还以为是模型调参调得好,后来排查半天才发现是这个低级失误。
如果原始数据量大,加载时还需要考虑内存占用。image_dataset_from_directory默认采用懒加载方式,只在被读取时才把图片载入内存,16GB内存的机器跑2.5万张150×150图片完全够用。
3.2 预处理:归一化与缓存
图像数据输入神经网络前,通常要做像素值归一化。原始图片的像素值范围是0到255,而神经网络倾向于处理0到1或-1到1范围的数据,这不是可选项,而是训练稳定性的基本要求。
normalization_layer = tf.keras.layers.Rescaling(1./255) train_ds = train_ds.map(lambda x, y: (normalization_layer(x), y)) val_ds = val_ds.map(lambda x, y: (normalization_layer(x), y))还可以利用缓存和预取机制提升训练速度:
train_ds = train_ds.cache().prefetch(buffer_size=tf.data.AUTOTUNE) val_ds = val_ds.cache().prefetch(buffer_size=tf.data.AUTOTUNE)prefetch的作用是在当前批次训练时提前准备好下一批次的数据,让数据读取和模型计算重叠进行,CPU和GPU/内存的利用率都会更高。
3.3 数据增强:把有限数据用出更多花样
在数据量不太大的情况下,数据增强是提升模型泛化能力的有效手段。它的核心在于对训练图片做随机变换,比如翻转、旋转、缩放、亮度调整,让模型看到更多样化的输入,不容易把某些无关特征当作判断依据。
data_augmentation = tf.keras.Sequential([ tf.keras.layers.RandomFlip('horizontal'), tf.keras.layers.RandomRotation(0.1), tf.keras.layers.RandomZoom(0.1), ])注意数据增强一般只作用于训练集,验证集和测试集不应该做随机增强,否则会导致评估结果不稳定。我当时想测试增强的效果,给验证集也加了一点随机翻转,结果验证损失曲线剧烈抖动,完全看不出真实水平。这也是一个频繁出现的误区。
提示:如果模型在训练集上准确率很高,说明模型容量足够,甚至过拟合了,这时增强的正向收益最明显。如果模型本身连训练集都拟合不好,先排查数据和模型结构问题,不要急着加增强。
4. 模型设计与训练方案:从简单CNN到迁移学习
4.1 先搭一个最简单但有效的CNN基线
模型结构设计不需要一开始就搞得很复杂。我的习惯是先从一个小型卷积神经网络做起,确认整个数据管线没有bug,然后逐步调整结构。这里给出一个可运行的基线模型,只有三个卷积块加一个全连接层,参数量不大,但在CPU上几分钟就能看到初步效果。
from tensorflow import keras from tensorflow.keras import layers model = keras.Sequential([ layers.Input(shape=(150, 150, 3)), layers.Conv2D(32, (3, 3), activation='relu'), 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.Conv2D(128, (3, 3), activation='relu'), layers.MaxPooling2D(2, 2), layers.Flatten(), layers.Dropout(0.5), layers.Dense(512, activation='relu'), layers.Dense(1, activation='sigmoid') ]) model.compile( optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'] ) model.summary()输出的model.summary()能直观看到每层张量形状变化:输入是(150, 150, 3),经过卷积和池化后空间尺寸不断缩小、通道数逐渐增加,最后通过全连接层输出一个概率值。
这里的关键设计点有两个。第一,最后的全连接层用了sigmoid激活函数,输出范围在0到1之间,配合binary_crossentropy损失函数完成二分类;第二,在第一个全连接层之前加了Dropout(0.5),随机丢弃一半神经元,防止过拟合,这是小数据集上非常常用且有效的正则化手段。
4.2 直接使用预训练模型做迁移学习
如果想让准确率更上一层楼,不用自己从零训练网络,而是借用预训练模型提取特征。迁移学习的思路是:已经在大规模数据集上训练好的网络,学到了通用的边缘、纹理、形状特征,你只需要它的前置部分作为“特征提取器”,再在顶部接上自己的分类层。
base_model = tf.keras.applications.MobileNetV2( input_shape=(150, 150, 3), include_top=False, weights='imagenet' ) base_model.trainable = False model = keras.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.Dropout(0.2), layers.Dense(1, activation='sigmoid') ]) model.compile( optimizer=keras.optimizers.Adam(learning_rate=0.001), loss='binary_crossentropy', metrics=['accuracy'] )这里把base_model.trainable=False,意味着预训练部分的权重在训练过程中保持不变,只训练新加的池化层和全连接层。好处是训练速度极快、不容易过拟合、准确率往往比从头训练高不少。等基础流程稳定后,再解冻部分底层权重做微调,还能进一步小幅提升效果。
4.3 训练超参数怎么定
训练超参数中,最核心的是学习率、批次大小、轮数。学习率我习惯从0.001开始,用Adam优化器时这个值一般不会出大问题。批次大小影响梯度估计的稳定性和内存占用,CPU环境用32或64就够;轮数不要一开始就定死,配合早期停止策略让模型自己决定什么时候停。
history = model.fit( train_ds, validation_data=val_ds, epochs=30, )这样写的含义是最多训练30轮,但实际效果要结合训练曲线判断,如果验证准确率连续几轮不再提升,就应当提前停止。这个场景正是早期停止回调发挥作用的地方。
5. 训练过程观察、评估与调优
5.1 看训练日志:别被单次数字骗了
训练每个epoch结束后,控制台会输出本轮的训练损失、训练准确率、验证损失、验证准确率。这四个数字的组合能反映不少信息。如果训练loss持续下降、验证loss也跟着下降,说明模型在正常学习;如果训练loss下降、验证loss先降后升,那就出现了过拟合。
我在练习时走过弯路:只看训练准确率,觉得95%已经很高了,直到验证集上测试才发现只有70%多,才意识到模型已经把训练集背下来了。训练曲线的可视化非常重要:
import matplotlib.pyplot as plt def plot_history(history): acc = history.history['accuracy'] val_acc = history.history['val_accuracy'] loss = history.history['loss'] val_loss = history.history['val_loss'] epochs_range = range(1, len(acc) + 1) plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(epochs_range, acc, label='训练准确率') plt.plot(epochs_range, val_acc, label='验证准确率') plt.legend() plt.title('准确率曲线') plt.subplot(1, 2, 2) plt.plot(epochs_range, loss, label='训练损失') plt.plot(epochs_range, val_loss, label='验证损失') plt.legend() plt.title('损失曲线') plt.show()这条曲线基本能反映训练的全部状态:收敛速度、震荡程度、过拟合信号。我每次调参后都会看一眼这张图,再决定下一步方向。
5.2 用混淆矩阵看模型的“犯错方式”
准确率只是整体指标,它掩盖了模型在猫狗各自类别上的表现差异。比如模型可能对狗更敏感,导致把很多猫误判成狗,只是因为猫的样本稍少,整体准确率看起来还不算差。混淆矩阵能把这种偏科情况暴露出来。
import numpy as np from tensorflow.keras.preprocessing import image_dataset_from_directory test_ds = image_dataset_from_directory( 'data/test', image_size=(150, 150), batch_size=32, shuffle=False ) test_ds = test_ds.map(lambda x, y: (normalization_layer(x), y)) y_true = [] y_pred = [] for images, labels in test_ds: preds = model.predict(images) y_pred.extend((preds > 0.5).astype(int).flatten()) y_true.extend(labels.numpy()) # 手动计算混淆矩阵 TP = sum(1 for a, b in zip(y_true, y_pred) if a == 1 and b == 1) FP = sum(1 for a, b in zip(y_true, y_pred) if a == 0 and b == 1) TN = sum(1 for a, b in zip(y_true, y_pred) if a == 0 and b == 0) FN = sum(1 for a, b in zip(y_true, y_pred) if a == 1 and b == 0) print('混淆矩阵:') print(f'真阳性(TP)={TP} 假阳性(FP)={FP}') print(f'假阴性(FN)={FN} 真阴性(TN)={TN}')数据加载部分要注意:测试集的shuffle=False必须显式设置,否则预测顺序和真实标签的顺序会对不上。
5.3 过拟合的应对思路:早停、Dropout与微调
小数据集上做图像分类,过拟合几乎不可避免,关键是如何压制。我常用的三板斧是:早期停止、Dropout和数据增强,前面已经提到过Dropout和增强,这里重点讲早期停止。
early_stopping = tf.keras.callbacks.EarlyStopping( monitor='val_loss', patience=5, restore_best_weights=True ) model.fit( train_ds, validation_data=val_ds, epochs=30, callbacks=[early_stopping] )patience=5的含义是,如果连续5轮验证损失都没有改善,就停止训练。restore_best_weights=True会让模型回滚到验证损失最优时的权重,而不是用最后一轮可能已经过拟合的权重。这个回调等于给训练过程加了保险,既防止训练过度,又省时间。
等到模型用固定特征提取方式训练到位后,还可以试着解冻预训练模型的后几层,用较小的学习率比如0.0001继续训练几轮,这个过程叫微调。它能帮助预训练特征更好地适配猫狗分类这个特定任务,但需要小心控制学习率和训练轮数,否则很容易过拟合。
6. 常见问题与排查实录
6.1 报错“Failed to find data adapter”或目录找不到
这个错误九成是路径写错了或目录结构不对。排查思路很简单:先确认当前工作目录,再用绝对路径或者os.path.join拼接路径,尽量避免手打路径。另外注意目录名和代码中写的一致,大小写、空格都不要忽略。
6.2 训练太慢或内存不足
CPU环境下,大尺寸图片和大的批次都会拖慢训练速度。可以采取几个措施:把图片尺寸从224×224降到150×150甚至128×128;把批次大小从32降到16;启用前面提到的cache().prefetch()。如果数据集很大,还可以用.cache()把预处理后的数据缓存到磁盘。
6.3 标签错乱导致准确率崩盘
image_dataset_from_directory会按字母顺序或目录顺序为类别分配标签,比如cats是0、dogs是1。如果之后需要手动处理标签,务必先检查类别映射关系:
print(train_ds.class_names)不看这个直接写标签逻辑,很容易把猫当狗、狗当猫,表现就是训练准确率只有50%左右,反复调模型结构也没用。
6.4 数据增强过度导致训练集准确率上不去
增强不是越多越好。我曾经把旋转和缩放范围设置得很大,结果连训练集准确率都只有60%左右,因为图片已经被变换得面目全非。合理的做法是从小范围的增强开始,比如水平翻转就够了,等到确实需要更强泛化时再逐步增加旋转角度。
6.5 验证集和训练集数据泄露
这个问题遇到过两次。第一次是数据增强错误地应用到了验证集,导致验证集不固定,曲线剧烈抖动;第二次是调用image_dataset_from_directory两次时忘了加相同的seed,导致训练集和验证集有重叠,验证准确率虚高。建议在切换验证集时先打印几张图片及标签,肉眼确认数据的合理性。
6.6 预测结果全偏向某一类
如果模型的预测概率总是偏向0或偏向1,先检查数据集的类别比例。虽然猫狗数量大致均衡,但如果某个子集读取出错,比例可能失衡。其次,检查模型最后一层的输出分布,如果所有预测概率都非常接近0.5,说明模型区分能力不足,需要增强特征提取能力或者调整网络结构。
7. 完整训练流程与备用方案参考
7.1 阶段一:快速验证管线
无论最终计划用什么模型,我都建议先跑一个极小的子集验证流程正确性。做法是取每个类别的前200张图片组成快速实验集,用5个epoch跑一轮。这一步重点不是看准确率,而是确认从数据加载、预处理、模型构建到训练回调的整条链路没有bug,同时熟悉Notebook的操作节奏。
7.2 阶段二:完整训练
管线验证通过后,再切换到完整数据集,设置合适的超参数,加上早期停止和数据增强,开始正式训练。训练过程中可以在Notebook里用plot_history函数定期查看曲线,而不是等到全部训练结束才发现问题。
7.3 阶段三:用回调机制自动保存最优模型
手动记录哪个epoch的模型最好太不靠谱,更专业的做法是用模型检查点回调把最优权重保存下来:
checkpoint = tf.keras.callbacks.ModelCheckpoint( 'best_model.h5', monitor='val_accuracy', save_best_only=True, mode='max' ) model.fit( train_ds, validation_data=val_ds, epochs=30, callbacks=[early_stopping, checkpoint] )训练结束后,直接加载最优模型进行测试:
best_model = tf.keras.models.load_model('best_model.h5')加载后最好再做一次预测验证,确认模型状态正常,再交给下游使用。
8. 实验后的思考与记录习惯
每次实验结束后,我都会在同一个Notebook末尾用Markdown单元格记录当次实验的数据修改点、超参数、最终指标和踩坑问题。比如:“本次改用MobileNetV2迁移学习,去掉数据增强,增大Dropout到0.3,验证准确率从82%升到91%,验证损失的过拟合拐点出现在第7轮。”这种记录习惯能帮你迅速回顾哪个改动有效、哪个无效,避免重复走老路。
从最开始跑通一个简单CNN,到后来引入预训练模型,再到逐步调整数据增强和正则化策略,我前后做了十几次实验。印象最深的一次修改是把图片尺寸从224减到150,训练速度快了近一倍,而验证准确率只下降了不到1个百分点。这个经验说明,很多时候不一定非得追求最高指标,还要考虑实验周期和资源消耗的平衡。
猫狗分类这个项目虽然简单,但我在它身上练出来的数据加载能力、训练曲线判断能力和常见问题排查能力,后面在更多复杂项目中反复受益。如果环境有限,就从CPU版本跑一个小模型开始;如果只想快速看到不错的效果,迁移学习是性价比很高的选择。核心是亲手把每一步走一遍,让代码和数据流在脑子里建立清晰的图像。这样遇到新数据集时,你就不必从零摸索了。