简介:本资源面向希望上手深度学习图像分类的开发者与算法学习者,聚焦TensorFlow 2.X环境下MobileNetV2模型的实战应用。内容基于植物幼苗数据集中的部分样本,覆盖12个类别,帮助读者理解轻量级网络在移动端场景中的落地方式。压缩包共约2000个文件,以png图片数据为主,另含py脚本、h5模型权重与pdf说明文档,整体约961.42MB,便于直接复现训练流程。已有753人学习下载。通过配套代码与数据,读者可掌握图片数据加载与预处理、标签onehot编码、数据增强、mixup策略、数据集切分以及预训练模型加载等关键环节,并借助MobileNetV2的线性瓶颈与倒残差结构,完成小数据集上的分类任务实践,适合作为图像分类入门与模型迁移学习的参考案例。
1. 小数据集上跑 MobileNetV2:为什么它比你想的更值得投入
手里只有几百上千张图,想做一个能落地的图像分类任务,这是很多一线工程师的真实处境。公开数据集动辄几十万张,自己标注的成本又高得离谱,于是「小数据集 + 轻量模型」成了最务实的组合。MobileNetV2 在这个场景里出镜率极高:它用深度可分离卷积把参数量和计算量压到很低,ImageNet 预训练权重又能直接迁移,几百张图微调几轮就能出一个能用的分类器。TensorFlow 2.x 把tf.keras.applications里的模型封装得很干净,加载、冻结、微调、导出基本是一条流水线。这篇笔记就围绕「MobileNetV2 图像分类任务(小数据集)」这个具体目标,把数据组织、迁移学习策略、训练参数、评估和踩坑讲透,让你照着能复现,也能判断这条路值不值得走。
2. 数据准备与迁移学习策略:小数据集到底该怎么喂给 MobileNetV2
小数据集做图像分类,成败往往不在模型结构,而在数据怎么组织、增强怎么做、预训练权重怎么用。MobileNetV2 在 ImageNet 上学到的特征对自然图像有很强的通用性,但你的类别如果偏门(比如森林图像分类、红外小目标检测数据集里的目标),直接全量微调很容易过拟合。这一章先把数据管线和迁移学习的分层策略讲清楚,再落到可执行的代码。
2.1 目录结构与 tf.data 管线的搭建
TensorFlow 2.x 最省事的方式是image_dataset_from_directory,它要求按类别分文件夹。假设你的数据长这样:
dataset/ train/ cat/ a001.jpg a002.jpg dog/ b001.jpg val/ cat/ dog/每个类别一个子目录,目录名就是标签。小数据集常见问题是类别不平衡,某个类只有几十张,另一个类有几百张。这时候不要直接按文件数采样,要么在增强上给少样本类加权,要么用class_weight补偿。下面是最小可跑的加载代码:
import tensorflow as tf IMG_SIZE = (224, 224) BATCH_SIZE = 32 AUTOTUNE = tf.data.AUTOTUNE train_ds = tf.keras.utils.image_dataset_from_directory( "dataset/train", image_size=IMG_SIZE, batch_size=BATCH_SIZE, label_mode="categorical", # 多分类用 categorical,二分类可换 binary shuffle=True, seed=42 ) val_ds = tf.keras.utils.image_dataset_from_directory( "dataset/val", image_size=IMG_SIZE, batch_size=BATCH_SIZE, label_mode="categorical", shuffle=False ) # 缓存 + 预取,小数据集能全部塞进内存时 cache 收益明显 train_ds = train_ds.cache().prefetch(buffer_size=AUTOTUNE) val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)逻辑说明:image_dataset_from_directory会自动按子目录名生成类别索引,label_mode="categorical"输出 one-hot,配合categorical_crossentropy。cache()把解码后的张量缓存到内存,小数据集(几千张 224×224 的图)通常放得下,能省掉每个 epoch 重复解码的开销。prefetch让数据准备和 GPU 计算重叠。参数上,IMG_SIZE必须和后面模型输入一致,MobileNetV2 默认 224,但include_top=False时可以用 128 或 160 来提速,代价是精度略降。
2.2 数据增强:小数据集的“后悔药”
小数据集最怕过拟合,增强就是最便宜的后悔药。TensorFlow 2.x 推荐把增强层直接写进模型,这样导出 SavedModel 时增强逻辑一起带走,推理时自动关闭。常见组合是随机翻转、旋转、缩放、对比度抖动:
data_augmentation = tf.keras.Sequential([ tf.keras.layers.RandomFlip("horizontal"), tf.keras.layers.RandomRotation(0.1), tf.keras.layers.RandomZoom(0.1), tf.keras.layers.RandomContrast(0.1), ], name="augment")逻辑说明:这些层只在training=True时生效,推理时是恒等映射。RandomRotation(0.1)表示旋转范围是 ±10%,RandomZoom(0.1)是 ±10% 缩放。注意不要加垂直翻转,除非你的类别本身上下对称(比如某些遥感图像),否则会把语义翻乱。对于红外小目标检测数据集这类偏灰度的图,颜色抖动意义不大,反而应该加噪声或亮度扰动。
2.3 迁移学习的两段式策略:先冻结,再解冻
MobileNetV2 的迁移学习我一般分两段。第一段冻结整个骨干,只训练新加的分类头,让随机初始化的头先收敛,避免大梯度把预训练权重冲坏。第二段解冻骨干的顶部若干层做小学习率微调。这是小数据集上最稳的套路。
base_model = tf.keras.applications.MobileNetV2( input_shape=(224, 224, 3), include_top=False, weights="imagenet" ) base_model.trainable = False # 第一段:冻结骨干 inputs = tf.keras.Input(shape=(224, 224, 3)) x = data_augmentation(inputs) # MobileNetV2 自带预处理,把像素从 [0,255] 映射到 [-1,1] x = tf.keras.applications.mobilenet_v2.preprocess_input(x) x = base_model(x, training=False) x = tf.keras.layers.GlobalAveragePooling2D()(x) x = tf.keras.layers.Dropout(0.2)(x) outputs = tf.keras.layers.Dense(num_classes, activation="softmax")(x) model = tf.keras.Model(inputs, outputs) model.compile( optimizer=tf.keras.optimizers.Adam(1e-3), loss="categorical_crossentropy", metrics=["accuracy"] )逻辑说明:include_top=False去掉 ImageNet 的 1000 类头,GlobalAveragePooling2D把特征图压成向量,比 Flatten 参数少、更抗过拟合。Dropout(0.2)在小数据集上是必要的正则。preprocess_input很关键,MobileNetV2 期望输入在 [-1,1],如果你自己归一化到 [0,1] 再喂进去,精度会莫名其妙掉一截,这是血泪经验。第一段训练一般 10~20 个 epoch,学习率 1e-3。
第二段解冻:
base_model.trainable = True # 只解冻最后 30 层左右,前面的底层特征保持冻结 fine_tune_at = len(base_model.layers) - 30 for layer in base_model.layers[:fine_tune_at]: layer.trainable = False model.compile( optimizer=tf.keras.optimizers.Adam(1e-5), # 微调学习率要小两个量级 loss="categorical_crossentropy", metrics=["accuracy"] )逻辑说明:解冻后必须重新compile,否则trainable的改动不生效,这是 TensorFlow 2.x 的经典坑。微调学习率用 1e-5 甚至更低,因为预训练权重已经很好,大学习率会把它们破坏掉。解冻层数没有固定答案,小数据集一般解冻 20~50 层,数据越少解冻越少。
3. 训练、评估与导出:把 MobileNetV2 跑成能用的模型
数据管线搭好、模型结构定下来,接下来就是训练循环、回调、评估和导出。这一章把每个环节的参数和判断标准讲清楚,尤其是小数据集上怎么判断模型是欠拟合还是过拟合,以及怎么把模型导出成能部署的格式。
3.1 回调函数:早停、学习率衰减与模型保存
小数据集训练最容易出现的情况是验证集精度震荡,训练集精度一路涨。回调是控制这个过程的阀门。
callbacks = [ tf.keras.callbacks.EarlyStopping( monitor="val_accuracy", patience=8, restore_best_weights=True ), tf.keras.callbacks.ReduceLROnPlateau( monitor="val_loss", factor=0.5, patience=4, min_lr=1e-7 ), tf.keras.callbacks.ModelCheckpoint( "best_mobilenetv2.keras", monitor="val_accuracy", save_best_only=True ) ]逻辑说明:EarlyStopping的patience=8表示验证精度连续 8 个 epoch 不提升就停,restore_best_weights=True保证回到最好的那轮权重,避免最后几轮过拟合的权重被留下。ReduceLROnPlateau在验证损失停滞时把学习率减半,min_lr防止学习率降到 0。ModelCheckpoint保存验证精度最高的模型,格式用.keras(TF 2.15+ 推荐),老版本可以用.h5。
训练调用:
history = model.fit( train_ds, validation_data=val_ds, epochs=50, callbacks=callbacks )参数说明:epochs=50是上限,实际会被早停截断。小数据集第一段通常 15~25 轮就收敛,第二段微调 10~20 轮。如果验证精度一直上不去,先检查数据增强是不是太狠、学习率是不是太大、类别是不是标错了。
3.2 评估:别只看 accuracy
小数据集上 accuracy 会骗人,尤其是类别不平衡时。必须看混淆矩阵和每类的 precision/recall。
import numpy as np from sklearn.metrics import classification_report, confusion_matrix y_true = [] y_pred = [] for images, labels in val_ds: preds = model.predict(images, verbose=0) y_true.extend(np.argmax(labels.numpy(), axis=1)) y_pred.extend(np.argmax(preds, axis=1)) print(classification_report(y_true, y_pred, target_names=class_names)) print(confusion_matrix(y_true, y_pred))逻辑说明:val_ds在加载时设了shuffle=False,所以顺序和标签一致,可以直接收集。classification_report给出每类的 precision、recall、f1,能看出模型是不是只学会了多数类。混淆矩阵能定位具体哪两类在互相误判,比如森林图像分类里“松林”和“混交林”容易混,这时候要么补这类样本,要么在增强上做针对性处理。
3.3 导出:SavedModel 与 TFLite 两条路
训练完的模型要落地,常见两种导出。服务器端用 SavedModel,移动端或边缘设备用 TFLite。
# SavedModel model.export("saved_model/mobilenetv2_cls") # TFLite(带动态范围量化,体积小、推理快) converter = tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations = [tf.lite.Optimize.DEFAULT] tflite_model = converter.convert() with open("mobilenetv2_cls.tflite", "wb") as f: f.write(tflite_model)逻辑说明:model.export导出的是包含计算图的 SavedModel,适合 TF Serving。TFLite 转换时Optimize.DEFAULT做动态范围量化,权重从 float32 压到 int8,体积大约缩到四分之一,精度损失通常 1% 以内。如果要做全整型量化,需要提供代表性数据集,小数据集上直接用动态范围就够。注意导出前确认增强层在推理时是关闭的,SavedModel 和 TFLite 都会正确处理training=False。
4. 避坑与排查:小数据集训练 MobileNetV2 最容易翻车的地方
这一章是我自己在小数据集上反复踩过的坑,按「现象 → 原因 → 解决」写,每条都对应真实会遇到的报错或精度异常。
4.1 验证精度卡在随机水平不动
现象:训练几十轮,验证精度一直在 1/类别数 附近,训练精度也不涨。 原因:最常见是标签和目录不匹配,或者preprocess_input用错。MobileNetV2 期望 [-1,1],如果你手动除了 255 又没做后续映射,输入分布和预训练时差太多,骨干输出接近噪声。 解决:确认image_dataset_from_directory打印的class_names顺序和你的预期一致;确认预处理只用mobilenet_v2.preprocess_input,不要叠加自己的归一化。可以在加载后打印一个 batch 的 min/max,正常应该在 [-1,1]。
4.2 训练精度 99%,验证精度 60%
现象:训练集很快到接近满分,验证集远低,且差距越来越大。 原因:过拟合。小数据集 + 全量微调 + 增强不足是典型组合。 解决:先冻结骨干只训头,确认验证精度能到合理水平再解冻;增强加狠一点(旋转、缩放、对比度);加 Dropout 和权重衰减;减少解冻层数。如果数据只有几百张,考虑用class_weight或对少样本类做额外增强。
4.3 解冻后精度反而下降
现象:第一段冻结训练验证精度 80%,解冻微调后掉到 70% 甚至更低。 原因:微调学习率太大,把预训练权重冲坏了;或者 BatchNormalization 层在解冻后被重新训练,小 batch 下统计量不稳。 解决:微调学习率降到 1e-5 或更低;解冻时保持 BN 层为推理模式(layer.trainable = False对 BN 层单独处理),或者用较大的 batch size。TensorFlow 2.x 里 BN 层解冻后默认会更新 moving mean/variance,小数据集上这很危险。
4.4 输入尺寸改了但精度崩了
现象:为了提速把输入从 224 改成 128,重新训练后精度掉很多。 原因:MobileNetV2 的预训练权重是在 224 上学的,特征图的感受野和统计特性和 128 不匹配,尤其是深层。 解决:如果一定要用小尺寸,要么从头训练(不推荐小数据集),要么在 128 上微调足够多轮让 BN 统计量适应。更稳的做法是保持 224,用 TFLite 量化来提速,而不是改输入尺寸。
4.5 类别不平衡导致少数类全错
现象:混淆矩阵里少数类几乎全被预测成多数类。 原因:损失函数被多数类主导,模型学到“全猜多数类”就能降低总损失。 解决:用class_weight给少数类加权,或者在增强时对少数类做更多变换。model.fit支持class_weight参数,按类别频率的倒数计算即可。如果少数类样本实在太少(每类不到 50 张),考虑先做二分类或合并相似类。
5. 进阶技巧:用混淆矩阵反推数据问题,把 MobileNetV2 调到能上线
模型训完不是终点,能不能上线取决于你对错误的定位能力。我一般会做一件事:把验证集里所有错分的样本按置信度排序,挑出置信度高但分错的那些,逐张看。这些样本往往不是模型的问题,而是标注错了、或者类别定义本身有歧义。小数据集上,标注噪声的杀伤力比模型结构大得多。
具体做法是导出错分样本清单:
import numpy as np wrong = [] for images, labels in val_ds: preds = model.predict(images, verbose=0) pred_idx = np.argmax(preds, axis=1) true_idx = np.argmax(labels.numpy(), axis=1) for i in range(len(true_idx)): if pred_idx[i] != true_idx[i]: wrong.append({ "true": class_names[true_idx[i]], "pred": class_names[pred_idx[i]], "conf": float(preds[i][pred_idx[i]]) }) wrong.sort(key=lambda x: x["conf"], reverse=True) for w in wrong[:20]: print(w)逻辑说明:按预测置信度降序排列,最上面的是“模型很自信但错了”的样本,这类最值得人工复核。如果发现某类大量出现在错分里,要么补样本,要么检查这类和其他类的边界是不是清晰。我做过一个森林图像分类的任务,模型总把“火烧迹地”和“裸地”搞混,后来发现标注时这两类的边界本身就没统一,重新定义后精度直接涨了 8 个点。
另一个技巧是用 MobileNetV2 的中间层特征做可视化,确认模型关注的是目标区域而不是背景。小数据集上模型很容易学到背景捷径,比如所有“猫”的图都在沙发上,模型可能学的是沙发而不是猫。用 Grad-CAM 看一眼热力图就能发现。TensorFlow 2.x 里可以用tf.keras的 GradientTape 自己实现,核心是取最后一个卷积层的输出对输入求梯度,加权求和得到热力图。这一步不复杂,但能帮你判断模型是不是在“作弊”。
最后说一个部署上的习惯:导出 TFLite 后,一定要在真实设备或模拟环境里跑一遍验证集,确认量化后的精度和 Keras 模型差距在可接受范围。我见过量化后精度掉 5 个点的情况,原因是某些层的激活值范围太宽,动态量化压不住。这时候要么换全整型量化加代表性数据集,要么对敏感层跳过量化。MobileNetV2 整体对量化很友好,但小数据集上微调后的模型权重分布可能和 ImageNet 预训练时不同,多验证一步不吃亏。
这套流程我在几个几百到几千张图的项目里反复用过,MobileNetV2 + TensorFlow 2.x 的组合在小数据集上性价比很高,但前提是数据管线干净、迁移学习分两段走、评估不只看 accuracy。希望帮到你。
本文还有配套的精品资源,点击获取