
1. 为什么猫狗识别是深度学习入门的一道必做题先说说我自己的感受。MNIST手写数字识别几乎是所有人的第一站但做完之后往往会有一种“好像学会了又好像什么都没学会”的错觉。因为MNIST图片太小、太干净、类别太简单整个训练过程顺畅得不像真实世界的问题。当你真正想做点跟图像有关的东西时猫狗识别往往就是那个“跳一跳才够得着”的台阶。这个练习的价值在于它几乎集齐了图像分类项目的全部核心要素真实拍摄的图片、背景杂乱、光照不稳定、目标大小不一、类别只有两个但在语义上非常接近。猫和狗不是“数字0和1”那种机械差异而是毛发纹理、耳朵形状、面部结构、身体姿态的综合判断。神经网络要处理的不是“有没有某种固定模式”而是“一组抽象特征的组合更偏向哪一侧”这跟真实业务里的分类任务是同构的。另一个非常现实的理由是数据量有限。公开的猫狗数据集看起来有几万张图片但真要训练一个从零开始的深度网络这点数据还是不够。于是你不得不面对过拟合、数据增强、迁移学习这些问题。而这三个东西恰好是工业场景里每天都在用的核心技巧。换句话说猫狗识别练的不是“怎么调通一个模型”而是“在数据不够的情况下怎么把模型做好”这一点才是真正值钱的经验。所以这篇文章不打算只贴一份能跑通的代码而是把我反复试过、踩过坑之后觉得最值得记录的东西写下来。包括数据预处理怎么选型、基线网络怎么搭、训练到什么程度可以判断过拟合、以及迁移学习到底该怎么“迁移”才有效。适合正在学TensorFlow、已经跑通MNIST但想更进一步的朋友参考。2. 数据准备阶段目录划分和ImageDataGenerator的取舍2.1 先按目录把数据整理清楚这是后面所有流程的地基很多人拿到数据集之后第一件事就是写代码这其实是个坑。图像分类任务里数据结构混乱会导致你在排查问题时根本分不清是代码写错了、路径加载错了还是模型本身有问题。我习惯的划分方式是data/ ├── train/ │ ├── cats/ │ │ ├── cat_0001.jpg │ │ └── ... │ └── dogs/ │ ├── dog_0001.jpg │ └── ... ├── validation/ │ ├── cats/ │ └── dogs/ └── test/ ├── cats/ └── dogs/训练集负责拟合验证集负责监控训练过程中的泛化能力测试集只在全部调优结束之后跑一次。这个规矩建议一开始就遵守不要嫌麻烦。尤其是验证集很多人直接把训练集里抽出一小部分当验证集虽然也能用但如果你后面做了数据增强验证集和训练集同源会污染你的判断——你看到的“提升”可能只是模型记住了训练样本的增强版本。关于划分比例我的建议是数据总量不大时训练集、验证集、测试集按8:1:1切。如果原始数据集已经分好了train和validation那就直接把validation当成测试集也没问题练手项目不需要那么严格。2.2 ImageDataGenerator和tf.data怎么选别迷信“更高级”TensorFlow里做图像数据流水线有两条主流路线老的ImageDataGenerator和新的tf.data管道。很多人一上来就学tf.data觉得这是未来方向结果被map、batch、prefetch这些概念搞得一头雾水训练代码半天写不出来。我的观点很直接练手阶段用ImageDataGenerator就够了而且它自带flow_from_directory你只要把目录结构摆好它会自动根据子文件夹名生成标签。配合rescale做归一化、validation_split切验证集十几行代码就能把训练数据和验证数据都准备好。from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1./255, rotation_range20, width_shift_range0.2, height_shift_range0.2, shear_range0.2, zoom_range0.2, horizontal_flipTrue, fill_modenearest ) val_datagen ImageDataGenerator(rescale1./255) train_generator train_datagen.flow_from_directory( data/train/, target_size(150, 150), batch_size32, class_modebinary ) validation_generator val_datagen.flow_from_directory( data/validation/, target_size(150, 150), batch_size32, class_modebinary )class_modebinary是因为猫狗二分类输出层用1个神经元加sigmoid就可以了。如果换成多分类这里是categorical。那tf.data什么时候用当你要处理的数据集大到无法一次性读入内存或者需要做非常复杂的预处理比如从TFRecord读取、多进程并行解码、动态batch再去研究tf.data不迟。先学会跑通流程再考虑性能优化这是我一贯的做法。3. 第一版基线网络从零开始训练一个小型CNN3.1 网络结构怎么设计才能兼顾效果和训练速度第一次跑猫狗识别时我犯过一个典型错误直接上了个很深很宽的网络光参数就有上千万结果在一张消费级显卡上每个epoch都要跑十几分钟而且训练集上准确率很快冲到99%验证集上却一直在75%左右晃荡。后来才意识到对这种几万张图片的二分类任务基线模型用一个小型CNN就足够了关键在结构合理不在层数多。我当时最终采用的基线结构是这样的from tensorflow.keras import layers, models model models.Sequential([ layers.Conv2D(32, (3, 3), activationrelu, input_shape(150, 150, 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), layers.Conv2D(128, (3, 3), activationrelu), layers.MaxPooling2D(2, 2), layers.Flatten(), layers.Dropout(0.5), layers.Dense(512, activationrelu), layers.Dense(1, activationsigmoid) ])这个结构有几个刻意为之的设计点。第一卷积核数量从32到128逐层递增因为浅层提取的是边缘、颜色块这类低级特征不需要太多通道深层要学习“猫脸”“狗耳朵”这种语义特征需要更丰富的表达空间。第二每隔一个卷积层加一个最大池化把特征图尺寸逐步压缩到原来的1/16这样既降低了计算量又让小目标的位置偏移对最终判断的影响变小。第三全连接层之前加了Dropout这是针对小数据集过拟合问题的标准预防手段测试时Dropout自动关闭不会影响推理。3.2 训练参数优化器、学习率、batch大小之间的搭配选优化器时别来回折腾Adam是默认选择但学习率不要用默认的0.001一竿子捅到底。我自己的经验是先用0.001跑前几个epoch观察loss下降速度如果下降太慢改用0.0005如果loss震荡剧烈降到一个更小的值。模型结构和数据规模不同最优学习率差异很大只能实测。model.compile( optimizeradam, lossbinary_crossentropy, metrics[accuracy] )binary_crossentropy对应单神经元sigmoid输出如果输出层用了两个神经元加softmax就要换成categorical_crossentropy。很多新手在这里混用导致loss曲线诡异其实根源就是输出层设计和损失函数不匹配。batch size我习惯先用32。太小的batch比如8、16会让梯度估计噪声大训练不稳定太大的batch比如128以上虽然稳定但每个epoch需要更多步而且在小数据集上容易收敛到比较差的局部最优。32是一个公认的稳妥起点。训练时用ModelCheckpoint保存最佳模型不要在训练结束之后才手动保存因为中途可能已经过拟合了最后的那个权重往往不是验证集上最好的那个。from tensorflow.keras.callbacks import ModelCheckpoint checkpoint ModelCheckpoint( best_model.h5, monitorval_accuracy, save_best_onlyTrue, modemax ) history model.fit( train_generator, steps_per_epochtrain_generator.samples // 32, epochs30, validation_datavalidation_generator, validation_stepsvalidation_generator.samples // 32, callbacks[checkpoint] )这里steps_per_epoch不能省略尤其是在ImageDataGenerator配合flow_from_directory时如果不传它会默认用样本总数除以batch size手动写出来的好处是你清楚知道每一步到底在做什么。4. 训练过程中的过拟合观察当训练准确率和验证准确率开始分道扬镳4.1 判断过拟合的三个信号别等到训练完了才看曲线初学者最容易犯的错是epoch设得很大然后盯着最终准确率看完全忽略中间的曲线形态。实际上过拟合从某个epoch开始就悄悄发生了晚期看到的结果只是“已经坏掉很久之后的状态”。我判断过拟合主要看三个信号第一训练准确率持续上升但验证准确率在一个平台期反复横跳甚至开始掉头向下。这是最直观的信号。第二训练loss一路走低但验证loss先降后升形成一个“U型曲线再上翘”的形态。从这一瞬间开始模型就已经在记住训练样本的噪声了。第三验证准确率和训练准确率之间的差距拉大到5个百分点以上并且差距还在持续扩大。正常训练中两者有差异是正常的但差距不断扩大说明泛化正在恶化。以下是我第一次用基线模型跑出来的典型结果阶段训练准确率验证准确率状态epoch 50.820.78正常epoch 100.910.83轻微过拟合epoch 200.980.79明显过拟合epoch 300.990.75严重过拟合可以看到epoch 10之后验证准确率就再也没有实质提升训练却还在继续往100%逼近。此时再训练下去没有意义不如把精力放在正则化和数据增强上。4.2 数据增强的力度怎么调随机参数不是越多越好数据增强是对抗过拟合最有效的手段之一。但这里有个常见误区增强的强度越大越好。实际上过度增强会让模型看到失真严重、甚至人类都难以辨认的图片导致欠拟合。我当时对训练数据用的增强参数在上文代码里已经写了旋转20度、水平和垂直平移0.2、剪切0.2、缩放0.2、水平翻转。这些数值是怎么定出来的并不是拍脑袋。我建议把增强参数和你的数据特点对应起来。猫狗图片里物体基本是水平摆放的极少出现倒立的情况所以rotation_range设20度就够设到180度反而会引入大量不合理姿态。猫和狗可能在画面中的位置有偏移所以width_shift_range和height_shift_range可以适当放宽。水平翻转对猫狗识别是完全合理的增强因为一张猫的照片左右翻转之后仍然是猫但垂直翻转就不能开地上跑的动物倒过来看就违背常理了。train_datagen ImageDataGenerator( rescale1./255, rotation_range20, width_shift_range0.2, height_shift_range0.2, shear_range0.2, zoom_range0.2, horizontal_flipTrue, fill_modenearest )增强之后我还遇到了一个新的意外情况训练集和验证集的表现都提升了但验证集提升幅度明显小于训练集。这是因为数据增强只作用于训练流验证集始终是原始图片。后来我做了个简单实验把验证集也接入一个只有rescale的ImageDataGenerator不加强化结果曲线形态就正常了。这算是个小经验验证集的预处理只需与训练集保持同样的归一化方式不要做任何增强。5. 迁移学习实战冻结、微调、替换分类头5.1 为什么不应该从零开始训练一个深层网络迁移学习到底迁移了什么基线模型再优化在小数据集上从零训练的天花板也就85%左右。想继续往上冲就要用迁移学习。先从原理上理解这件事。一个在ImageNet上预训练好的模型它的前几层已经学会了边缘、纹理、颜色块这些通用的图像特征。这些特征对绝大多数图像任务都有效不管你是识别猫狗、识别汽车还是识别医疗影像低层特征都是相通的。真正需要重新学的是任务专属的高层语义特征也就是“什么样的特征组合能代表猫”和“什么样的特征组合能代表狗”。所以迁移学习的做法是把预训练模型的底层保留替换掉顶部分类器只训练新分类器或者只微调很少一部分高层卷积层。一开始我犯了个错把预训练模型整个设置为trainableTrue然后用小学习率全局微调。结果因为数据量不够底层特征被破坏验证准确率反而下降。后来我把底层全部冻结只训练自己新加的全连接层效果立刻好了很多。5.2 用一个预训练模型做特征提取器的具体操作我用的是MobileNetV2。选它的理由很实际参数少、推理快在一台普通CPU机器上也能跑得动而且它在ImageNet上学到的特征对猫狗识别来说完全够用。from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras import layers, models base_model MobileNetV2( input_shape(150, 150, 3), include_topFalse, weightsimagenet ) base_model.trainable False model models.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.Dropout(0.2), layers.Dense(1, activationsigmoid) ])include_topFalse表示不要预训练模型的原始分类头。GlobalAveragePooling2D代替Flatten把每个特征图压缩成一个值能大幅减少参数数量也降低过拟合风险。训练时我先把模型的输入尺寸调整到与预训练模型一致。很多预训练模型默认输入是224×224但我为了跟前面的基线模型保持一致用了150×150。这里要注意MobileNetV2对输入尺寸有一定容忍度但太小会掉精度太大又浪费算力150×150对猫狗识别足够了。model.compile( optimizeradam, lossbinary_crossentropy, metrics[accuracy] ) history model.fit( train_generator, steps_per_epochtrain_generator.samples // 32, epochs20, validation_datavalidation_generator, validation_stepsvalidation_generator.samples // 32, callbacks[checkpoint] )冻结底层之后模型参数数量骤降训练速度比从零开始还要快。我这边跑下来仅仅十几分钟就收敛了验证准确率直接到了95%以上。对比从零训练的基线模型效果差距非常明显。如果你有GPU并且想再进一步可以在最后几个epoch把base_model.trainable设为True用一个很小的学习率比如1e-5微调高层卷积层。但这步操作要非常谨慎如果发现验证准确率掉头向下说明已经被破坏特征了立即收敛回去。6. 预测阶段的坑图片预处理和模型输入要求的对齐6.1 你以为训练没问题就能顺利预测结果第一个坑就在尺寸和归一化模型训练好之后很多人拿一张自己的照片去预测发现结果离谱然后开始怀疑模型是不是坏了。实际上八成原因是图片预处理没对齐。训练时ImageDataGenerator做了rescale1./255但预测时如果直接加载原始图片像素值0到255送入模型特征分布完全不同模型当然给出奇怪的结果。我在代码里提前准备好了预测函数建议你也照着封装一个import numpy as np from tensorflow.keras.preprocessing import image def predict_image(model, img_path): img image.load_img(img_path, target_size(150, 150)) img_array image.img_to_array(img) img_array np.expand_dims(img_array, axis0) img_array / 255.0 pred model.predict(img_array)[0][0] if pred 0.5: return dog, pred else: return cat, 1 - pred这里的target_size(150, 150)必须和训练时保持一致。如果训练用的是224×224预测时却用150×150模型输入维度和训练时不一致轻则lower精度重则直接报错。np.expand_dims是给图片补一个batch维度因为模型要求的输入是四维张量(batch_size, height, width, channels)。6.2 导出和部署时容易忽略的模型格式问题训练时自动保存下来的best_model.h5可以直接用但如果你想换到服务端推理建议导出为SavedModel格式这是TensorFlow部署时最通用的格式。model.save(cats_dogs_model, save_formattf)导出之后再怎么用用tf.keras.models.load_model加载即可。loaded_model tf.keras.models.load_model(cats_dogs_model)有几个我踩过的细节值得说。第一如果你的模型用了自定义层导出时可能会带上额外的对象加载时需要注册但本文用的都是内置层没有这个问题。第二导出前要把整个模型重新跑一次预测确认和h5文件的结果一致避免出现“训练时好好的部署时居然报错”的事情。6.3 批次预测和单张预测之间的性能差异如果你要对一批图片做预测千万不要写个循环一张一张调model.predict。这样很慢因为每次调用都重新走一遍前向传播的Python开销。正确做法是把所有图片堆叠成一个NumPy数组一次性预测。def predict_batch(model, img_paths): images [] for path in img_paths: img image.load_img(path, target_size(150, 150)) img_array image.img_to_array(img) / 255.0 images.append(img_array) batch_array np.stack(images, axis0) preds model.predict(batch_array) return [dog if p 0.5 else cat for p in preds]我测过一个小场景500张图片循环单张预测大概要40秒批量预测不到10秒。这几乎是四倍的差距。平时做实验无所谓但如果你要把模型接进一个实时接口这个优化就非常关键了。回顾这个猫狗识别的练习真正有用的不是最终那一点准确率数字而是整个过程里反复出现的“训练、观察、调整、验证”循环。基线模型让我学会了怎么看曲线判断过拟合数据增强让我理解了样本分布对模型的影响迁移学习则让我直观感受到特征复用的威力。如果说MNIST教你的是“跑通深度学习代码”那么猫狗识别教你的就是“怎么像一个工程师一样去调模型、排问题”。我建议你在跑完这篇笔记的完整流程之后自己换一个二分类数据集比如区分苹果和橘子、区分白天和夜晚的路况图片重新走一遍从数据准备到预测部署的流程。过程中你一定会踩到上文提过的某个坑那种时候再回来看笔记才会真正理解我写这些注释的用意。