介绍

使用MobileNetV3(预训练好的)在120-breedDog数据集迁移学习,再微调。

迁移学习流程:去头,冻结权重,增加新层和分类头,在目的数据上进行新增层(包含分类头)的训练,再进行微调。

理解:原有权重已经学会了很多关于图像的基础知识,新训练的层只是告诉model,具体应用分类中哪些组合是是什么。

这就好像已经参加it工作的应届生,会写代码,但是不懂公司内部的具体业务,我们冻结权重(毕业了)+训练新层(入职培训)。

代码

去分类头、冻结参数、增加新层(包含分类头)、训练新层(包含分类头)

# 1.构建base模型
base_model = tf.keras.applications.MobileNetV3Large(
    input_shape=(IMG_SIZE, IMG_SIZE, 3),
    include_top=False,#去分类头(原有1000种分类)
    weights='imagenet',
    minimalistic=False # 标准版,包含高级激活函数,精度更高
)
base_model.trainable = False # 冻结,模型非新增层部分不参与训练

# 2.增加新层(包含匹配当前任务的分类头)
model = tf.keras.Sequential([
    base_model,
    tf.keras.layers.GlobalAveragePooling2D(),
    tf.keras.layers.Dropout(0.2), # 防止过拟合
    tf.keras.layers.Dense(NUM_CLASSES, activation='softmax')
])

model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), # 初始大学习率
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

# 3.训练 Epochs
history_phase1 = model.fit(
    train_ds,
    validation_data=val_ds,
    epochs=10  # 快速训练头部
)

微调

解冻基准模型(训练的梯度可以传递到基准模型来了),加入极小的学习率(原有的权重不宜大幅度修改,毕竟微调嘛)。

如果学习率太大(比如 $0.001$),一步迈得太快,就会瞬间把这些精细的特征提取能力冲垮,导致模型“失忆”。

目的:让基准模型更关注当前数据集的微小细节。

用术语来说,微调阶段完成以下两种任务:

任务描述
特征对齐让预训练的卷积核微调其参数,使其产生的特征图(Feature Map)能更清晰地传达给你新加的分类头。
适应特定分布比如你的数据是在特殊光照、特定医疗设备(如你研究的 WSI 图像)下拍摄的,微调能让模型学会过滤掉这些特定环境带来的干扰。
base_model.trainable = True

# 保持底层的 BatchNorm 层冻结(非常重要,否则会破坏预训练特征的统计分布)
# 在 TF2 MobileNet 中,通常只要 set trainable=True,BN层的 inference mode 需注意
# 简单的做法是全解冻,但 LR 要极小

model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5), # 极小学习率
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

history_fine = model.fit(
    train_ds,
    validation_data=val_ds,
    epochs=20, # 根据 Val Loss 决定是否停止
    callbacks=[
#只要验证集损失(Val Loss)连续 3 个 Epoch 不下降,就立刻停止训练并回退到表现最好的那个版本。这能完美防止过拟合。
        tf.keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True),
#如果发现精度不再提升,就把学习率再打 2 折(变成 2e-6)。这就像是在登山接近顶峰时,步子迈得更小、更稳
        tf.keras.callbacks.ReduceLROnPlateau(factor=0.2, patience=2)
    ]
)

整体代码

#对上面的代码进行总结,此代码就是完成的迁移学习:预训练+微调
import tensorflow as tf

# ===========================
# 1. 定义自动保存机制 (关键补充)
# ===========================
checkpoint_cb = tf.keras.callbacks.ModelCheckpoint(
    filepath='best_dog_model.h5',  # 保存的文件名
    save_best_only=True,           # True = 只保存最好的模型;False = 每轮都覆盖
    monitor='val_accuracy',        # 监控指标:验证集准确率
    mode='max',                    # 准确率越高越好
    verbose=1                      # 打印保存日志
)

# 其他回调
early_stopping_cb = tf.keras.callbacks.EarlyStopping(
    patience=5, restore_best_weights=True, monitor='val_loss'
)
reduce_lr_cb = tf.keras.callbacks.ReduceLROnPlateau(
    factor=0.2, patience=2, monitor='val_loss'
)

# ===========================
# 2. 构建与编译模型
# ===========================
base_model = tf.keras.applications.MobileNetV3Large(
    input_shape=(IMG_SIZE, IMG_SIZE, 3),
    include_top=False,
    weights='imagenet',
    minimalistic=False 
)
base_model.trainable = False # 冻结

model = tf.keras.Sequential([
    base_model,
    tf.keras.layers.GlobalAveragePooling2D(),
    tf.keras.layers.Dropout(0.2), 
    tf.keras.layers.Dense(NUM_CLASSES, activation='softmax')
])

model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), 
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

# ===========================
# 3. 第一阶段:冻结训练 (必须加入 callbacks)
# ===========================
print("🔥 [Phase 1] Start Training Head...")
history_phase1 = model.fit(
    train_ds,
    validation_data=val_ds,
    epochs=10,
    callbacks=[checkpoint_cb]  # <--- 这里加上!
)

# ===========================
# 4. 第二阶段:微调训练
# ===========================
print("🧊 [Phase 2] Start Fine-tuning...")
base_model.trainable = True

model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5), # 极小学习率
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

history_fine = model.fit(
    train_ds,
    validation_data=val_ds,
    initial_epoch=history_phase1.epoch[-1] + 1, # 接续之前的轮次
    epochs=30, # 总轮次
    callbacks=[checkpoint_cb, early_stopping_cb, reduce_lr_cb] # <--- 这里也要加上!
)

# ===========================
# 5. (可选) 手动保存最终版
# ===========================
# 如果你想不管效果好坏,把最后一步训练完的模型也存一份:
model.save('final_dog_model.h5')
print("✅ 训练结束,最佳模型已保存为 best_dog_model.h5")

过程

服务器

客户端

实践中遇到的问题

  • 训练好的模型部署到客户端,效果差异巨大

    原因:训练预处理和部署时预处理不一致(客户端推理有问题)。

    具体分析:

    TensorFlow 的 Resize:TF 的 tf.image.resize(尤其是旧版本或默认配置)在处理像素坐标偏移(Coordinate Offset)时,与 OpenCV 或 PIL 的实现逻辑不同(例如 half_pixel_centers 的取值)。

    解决方案:

    推理侧对齐

    在推理代码中,复刻训练时的 TF 逻辑。具体是在包装模型时,内置 resize(与训练完全一致的 bilinear, antialias=False)

理论中的问题

  • 为什么不直接解冻、加头、然后一步到位开始训练呢?

    新加的层和分类头的权重是随机的,如此时解冻基准模型,则计算梯度,会对原有预训练权重造成大的波动。

    其次,让新加入的层理解基准模型提供的特征,eg:基准模型输出1280维视觉信号,新加入的层要正确的解读这些信号,来映射到分类上。说白了就是新增的层执行的是翻译的逻辑。

    ⚠️注意:等到它能给出 70% 或 80% 的准确率时,接下来的微调才是有意义的(会比较温和),这里也不必关注具体数值,下面给出两个参考。

    1. 新层的训练进入了瓶颈,即 Val Accuracy 不再上升,Val Loss 也不再下降时,说明新层已经把基座现有的特征理解到位了。

    2. 准确率稳住不动。

后续

扩展猫猫分类,现有数据集Kaggle Cat Breeds;

对与新增分类,可采取在现有狗狗分类最佳权重的基础上进行迁移学习,在新数据集(狗+猫)上进行。

若分类的物种持续增多?

提出

A. 层次化分类 (Hierarchical Classification)

B. 向量检索架构 (Metric Learning / Embedding)

C. 持续学习 (Continual Learning / Incremental Learning)

类别规模推荐方案核心技术点适用场景
< 200 类合并微调 (方案一)加权采样 (Weighted Sampler)、微调你的 Android 比赛、小型 App
200 - 1000 类分阶段微调知识蒸馏、学习率衰减中型商业识别软件
> 1000 类特征向量检索Triplet Loss、向量数据库 (Faiss)拍照识花、万物识别、人脸库