简介本资源面向希望上手深度学习图像分类的开发者与算法学习者聚焦TensorFlow 2.X环境下MobileNetV2模型的实战落地。内容基于植物幼苗数据集中的部分样本覆盖12个分类类别适合作为小数据集迁移学习的练手项目。压缩包共约2000个文件以png图像数据为主另含4个py脚本、1个h5模型权重与1份pdf说明文档整体约961.42MB可直接用于训练与验证。已有753人学习该资源。通过配套代码与数据读者可掌握图片数据加载与预处理、标签onehot编码、数据增强、mixup策略、数据集切分以及预训练模型加载等关键环节理解MobileNetV2线性瓶颈与倒残差结构在移动端场景下的轻量化优势并据此搭建可复用的图像分类流程。1. 小数据集也能跑 MobileNetV2这份 TensorFlow2.X 图像分类包到底能解决什么手里只有几百张图却要做一个 12 分类的识别任务这是很多做图像分类的工程师真实遇到的处境。公开数据集动辄几十万张自己标注的数据往往只有几百到几千张直接上 ResNet 或者 EfficientNet 大概率过拟合训练几轮验证集准确率就卡住不动了。这份资源针对的正是这个场景用 TensorFlow2.X 加载植物幼苗数据集中的部分数据共 12 个类别模型选 MobileNetV2配合数据增强和 mixup把一个小数据集的图像分类任务完整跑通。包里包含my_model.h5训练好的权重文件、一份 PDF 说明文档以及若干张训练过程截图适合想快速上手迁移学习、又不想从零搭训练框架的从业者。MobileNetV2 在 V1 基础上引入线性瓶颈和倒残差结构参数量小、推理快是移动端图像分类的常见选择小数据集上微调它比从头训一个大模型务实得多。2. 数据管道与标签编码把 12 类幼苗图片喂进 MobileNetV22.1 为什么小数据集要先把加载和切分做对小数据集做图像分类翻车往往不是出在模型结构上而是出在数据管道。图片数量少如果切分不合理比如某一类全落在训练集、验证集里一张都没有那验证准确率就是玄学。常见做法是按类别分层抽样保证每个类别在训练集、验证集、测试集里的比例一致。这份资源用的是植物幼苗数据集的一个子集12 个类别每类样本量不大所以切分时更要按类别来分而不是把所有图片打乱后一刀切。TensorFlow2.X 里加载图片有两条路一条是tf.keras.preprocessing.image.ImageDataGenerator配合flow_from_directory直接从目录读另一条是tf.data.Dataset配合image_dataset_from_directory。前者老派但稳后者更贴近 TF2 的原生写法。这份资源走的是 ImageDataGenerator 这条路原因是它内置了数据增强和归一化代码量少适合快速验证。目录结构上每个类别一个文件夹文件夹名就是类别名这是 Keras 图像分类的默认约定不用额外写标签映射。import tensorflow as tf from tensorflow.keras.preprocessing.image import ImageDataGenerator # 训练集目录每个子文件夹是一个类别共 12 类 train_dir data/train val_dir data/val # 训练集生成器带数据增强 train_datagen ImageDataGenerator( rescale1./255, # 像素归一化到 [0,1] rotation_range20, # 随机旋转 ±20 度 width_shift_range0.1, # 水平平移 10% height_shift_range0.1, # 垂直平移 10% horizontal_flipTrue, # 水平翻转 zoom_range0.1, # 随机缩放 fill_modenearest # 填充方式 ) # 验证集只做归一化不做增强 val_datagen ImageDataGenerator(rescale1./255) train_generator train_datagen.flow_from_directory( train_dir, target_size(224, 224), # MobileNetV2 默认输入尺寸 batch_size32, class_modecategorical # 多分类标签转 onehot ) val_generator val_datagen.flow_from_directory( val_dir, target_size(224, 224), batch_size32, class_modecategorical )这段代码里几个参数值得说清楚。target_size(224,224)是 MobileNetV2 在 ImageNet 上预训练时的输入尺寸虽然 MobileNetV2 支持任意尺寸输入但用预训练权重时最好对齐否则第一层卷积的统计分布对不上微调效果会打折。class_modecategorical会自动把类别标签转成 onehot 编码12 类就是 12 维向量配合categorical_crossentropy损失函数使用。如果类别是二分类改成binary如果是多标签用raw或自己写生成器。rescale1./255把像素从 0-255 压到 0-1这是 MobileNetV2 预训练时的预处理方式。注意MobileNetV2 在tf.keras.applications里有一个preprocess_input函数它做的不是简单的除以 255而是把像素映射到 [-1,1]。如果你用preprocess_input就不要在 ImageDataGenerator 里再写rescale1./255两者叠加会把数据分布搞乱这是很常见的踩坑点。这份资源用的是rescale1./255那加载模型时就要注意别再加preprocess_input保持一致。2.2 标签 onehot 与 mixup 的实现细节标签转 onehot 在上面class_modecategorical已经自动做了但如果你用tf.data或者自己写生成器就需要手动转。TF2.X 里用tf.keras.utils.to_categoricalimport numpy as np from tensorflow.keras.utils import to_categorical # 假设 labels 是整数标签列表共 12 类 labels np.array([0, 3, 7, 11, 5, 2]) onehot to_categorical(labels, num_classes12) print(onehot.shape) # (6, 12)num_classes12必须和实际类别数一致写少了会报错写多了会多出全零列训练时那些列永远不激活浪费计算。小数据集上类别数不多手动核对一遍类别名和文件夹数量比训练完发现少一类要省事。mixup 是这份资源里比较有亮点的部分。它的做法是把两张图片按比例线性混合标签也按同样比例混合让模型学到更平滑的决策边界。小数据集上 mixup 能明显抑制过拟合。实现上可以在生成器里做也可以写一个自定义的tf.data映射函数。下面是一个在 batch 级别做 mixup 的写法import tensorflow as tf def mixup_batch(images, labels, alpha0.2): 对一个 batch 做 mixupalpha 控制混合强度 batch_size tf.shape(images)[0] # 从 Beta 分布采样 lambda lam tf.compat.v1.distributions.Beta(alpha, alpha).sample() # 打乱索引 indices tf.random.shuffle(tf.range(batch_size)) shuffled_images tf.gather(images, indices) shuffled_labels tf.gather(labels, indices) # 线性混合 mixed_images lam * images (1 - lam) * shuffled_images mixed_labels lam * labels (1 - lam) * shuffled_labels return mixed_images, mixed_labelsalpha0.2是常见取值越小混合越接近原图越大混合越激进。小数据集上建议从 0.2 开始试太大可能欠拟合。tf.gather按打乱后的索引取图片和标签保证图片和标签的对应关系不乱。混合后的标签不再是 onehot而是概率分布所以损失函数要用categorical_crossentropy它本身支持软标签。注意 mixup 只在训练时用验证和测试不能用否则评估结果没有意义。3. 迁移学习与模型微调MobileNetV2 的冻结层怎么设3.1 加载预训练模型与冻结策略MobileNetV2 在tf.keras.applications里可以直接加载 ImageNet 预训练权重。小数据集上标准做法是先把卷积基冻结只训练顶部分类层等分类层收敛后再解冻一部分卷积层做微调。冻结的目的是保护预训练学到的通用特征避免小数据集上的梯度把权重带偏。from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras import layers, models # 加载预训练模型去掉顶部分类层 base_model MobileNetV2( input_shape(224, 224, 3), include_topFalse, # 不要 ImageNet 的 1000 类分类头 weightsimagenet # 加载预训练权重 ) # 冻结卷积基 base_model.trainable False # 搭建分类头 model models.Sequential([ base_model, layers.GlobalAveragePooling2D(), # 全局平均池化替代 Flatten layers.Dense(128, activationrelu), layers.Dropout(0.5), # 小数据集上 dropout 很关键 layers.Dense(12, activationsoftmax) # 12 类输出 ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy] )include_topFalse去掉 ImageNet 的 1000 类分类头因为我们的任务是 12 类。weightsimagenet加载预训练权重第一次运行会下载之后会缓存到本地。base_model.trainable False冻结整个卷积基此时只有顶部的 Dense 层参与训练参数量从几百万降到十几万训练很快。GlobalAveragePooling2D把特征图从(7,7,1280)压成(1280,)比Flatten参数量少得多也不容易过拟合。Dropout(0.5)在小数据集上是标配如果发现训练准确率远高于验证准确率可以把 dropout 调到 0.6 甚至 0.7。最后一层Dense(12, activationsoftmax)对应 12 类激活函数用 softmax损失用categorical_crossentropy。3.2 两阶段训练与学习率调整第一阶段冻结卷积基训练分类头通常跑 10-20 个 epoch 就能收敛。然后解冻卷积基的后半部分做微调学习率要调小否则会把预训练权重冲掉。# 第一阶段只训练分类头 history1 model.fit( train_generator, epochs15, validation_dataval_generator ) # 第二阶段解冻后 30 层做微调 base_model.trainable True for layer in base_model.layers[:-30]: layer.trainable False model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-5), # 学习率调小 losscategorical_crossentropy, metrics[accuracy] ) history2 model.fit( train_generator, epochs10, validation_dataval_generator )微调阶段学习率从1e-3降到1e-5这是血泪经验学习率不降预训练权重几轮就被改得面目全非验证准确率反而下降。解冻层数从后往前数 30 层MobileNetV2 的卷积基大约 150 多层解冻最后 30 层覆盖了高层语义特征低层的边缘、纹理特征保持冻结这样既适配新任务又不破坏通用特征。如果验证准确率在微调后不升反降说明解冻太多或者学习率太大。可以先把解冻层数减到 10 层学习率再降一个数量级试试。小数据集上微调是把双刃剑用好了涨点用不好还不如只训分类头。4. 训练过程排查小数据集图像分类的五个常见坑4.1 验证准确率不动或震荡现象训练准确率稳步上升验证准确率卡在某个值附近震荡甚至下降。原因通常有三个一是数据增强太激进训练集分布和验证集差异过大二是学习率太大权重更新步子迈太大三是类别不平衡某些类样本太少模型偏向多数类。解决先把增强参数调温和rotation_range从 20 降到 10zoom_range从 0.1 降到 0.05学习率从1e-3降到1e-4检查每个类别的样本数如果差距超过 5 倍考虑对少数类做额外增强或者用class_weight加权。4.2 损失变成 NaN现象训练几个 batch 后 loss 变成 NaN。原因学习率过大导致梯度爆炸或者 mixup 的alpha设得太小导致数值不稳定或者输入数据没有归一化。解决先确认rescale1./255有没有生效打印一个 batch 的像素范围看看是不是在 0-1 之间学习率降到1e-4mixup 的alpha不要低于 0.1。如果用了preprocess_input检查是不是和rescale叠加了。4.3 模型保存后加载报错现象训练完保存my_model.h5下次加载时提示未知层或者形状不匹配。原因MobileNetV2 作为自定义层嵌套在 Sequential 里保存时如果没写custom_objects加载会找不到。解决保存时用model.save(my_model.h5)加载时用tf.keras.models.load_model(my_model.h5)TF2.X 对tf.keras.applications里的模型有内置支持一般不会报错。如果报错检查保存和加载的 TF 版本是否一致跨版本加载 h5 经常出问题建议保存为 SavedModel 格式。4.4 显存不够或训练太慢现象batch_size 设到 32 就 OOM或者一个 epoch 跑十几分钟。原因图片尺寸太大或者没有用tf.data的预取机制。解决把target_size从 224 降到 192 或 160MobileNetV2 支持任意尺寸小数据集上降尺寸对精度影响不大速度提升明显用train_generator时加workers和use_multiprocessing参数或者改用tf.data.Dataset的.prefetch(tf.data.AUTOTUNE)。4.5 测试集准确率远低于验证集现象验证集准确率 90%测试集只有 70%。原因验证集和测试集分布不一致或者验证集被间接用于调参导致过拟合。解决切分数据时确保验证集和测试集都按类别分层抽样调参只看验证集测试集只在最后评估一次如果测试集准确率低很多说明模型对某些类别泛化差打印混淆矩阵看看哪些类被混淆了针对性补充数据。5. 从 h5 权重到实际推理验证模型与导出技巧训练完拿到my_model.h5下一步是验证它到底能不能用。很多人训完模型就结束了其实推理阶段的坑不比训练少。下面这段代码加载保存的模型对单张图片做预测并输出每个类别的概率import numpy as np import tensorflow as tf from tensorflow.keras.preprocessing import image # 加载模型 model tf.keras.models.load_model(my_model.h5) # 类别名按训练时的文件夹顺序 class_names [Black-grass, Charlock, Cleavers, Common Chickweed, Common wheat, Fat Hen, Loose Silky-bent, Maize, Scentless Mayweed, Shepherds Purse, Small-flowered Cranesbill, Sugar beet] def predict(img_path): img image.load_img(img_path, target_size(224, 224)) x image.img_to_array(img) / 255.0 # 和训练时一致的归一化 x np.expand_dims(x, axis0) # 加 batch 维度 preds model.predict(x)[0] top_idx np.argmax(preds) print(f预测类别: {class_names[top_idx]}, 置信度: {preds[top_idx]:.4f}) return preds predict(test.jpg)这里的关键是归一化方式必须和训练时一致。训练用rescale1./255推理也要除以 255。如果训练用了preprocess_input推理也要用否则预测结果会偏。np.expand_dims(x, axis0)把(224,224,3)变成(1,224,224,3)因为model.predict期望输入带 batch 维度。如果想看模型对哪些类别容易混淆可以跑一遍测试集画混淆矩阵from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt # 假设 test_generator 是测试集生成器 preds model.predict(test_generator) pred_labels np.argmax(preds, axis1) true_labels test_generator.classes cm confusion_matrix(true_labels, pred_labels) plt.figure(figsize(12, 10)) sns.heatmap(cm, annotTrue, fmtd, xticklabelsclass_names, yticklabelsclass_names) plt.xlabel(Predicted) plt.ylabel(True) plt.show()混淆矩阵能直接告诉你哪些类被分错了。比如Loose Silky-bent和Black-grass在幼苗期长得很像混淆很正常这时候要么补充这两类的区分性样本要么在损失函数里给这两类加权。小数据集上混淆矩阵比准确率更有诊断价值。还有一个实用技巧如果推理时发现模型对某些图片置信度普遍偏低可以在 softmax 前加一个温度系数把 logits 除以一个小于 1 的数让概率分布更尖锐。这个技巧在模型部署时常用但要注意温度系数是在验证集上调的不能拍脑袋定。从那以后我每次训完模型都会先跑一遍混淆矩阵再挑几张错分图片肉眼看看确认不是数据标注问题。这个习惯帮我省了很多次返工。希望帮到你。本文还有配套的精品资源点击获取