迁移学习实战:基于MobileNet V2的猫狗分类模型优化与微调
1. 迁移学习:让AI学会“举一反三”的捷径
如果你刚接触深度学习,可能会觉得训练一个能准确识别猫和狗的模型是件挺难的事儿。毕竟,这需要成千上万张标注好的图片,还得有一块不错的显卡,花上好几个小时甚至几天去训练。但今天我要跟你分享一个“作弊”般的方法——迁移学习。它能让你用很少的数据和计算资源,快速得到一个高精度的模型。这就像你已经学会了骑自行车,现在让你去学骑电动车,肯定比从零开始学要快得多。
迁移学习的核心思想就是“站在巨人的肩膀上”。我们不再从零开始训练一个模型的所有参数,而是直接使用一个在超大规模数据集(比如包含140万张图片、1000个类别的ImageNet)上预训练好的模型。这个模型已经学会了识别“边缘”、“纹理”、“形状”乃至“物体部件”等非常通用且强大的视觉特征。对于我们的猫狗分类任务来说,这些底层特征(比如毛发纹理、耳朵形状)是完全通用的宝贵知识。我们要做的,就是把这些知识“迁移”过来,只针对我们自己的小数据集,对模型进行微小的调整。
这次我们选择的“巨人”是MobileNet V2。它是一个非常高效的卷积神经网络,专为移动和嵌入式设备设计,在保持高精度的同时,模型体积和计算量都大大减小。用它来做迁移学习,速度快,效果好,特别适合我们这种想快速上手、验证想法的场景。接下来,我会手把手带你走完整个流程,从数据准备、模型构建、训练优化到最终预测,把每个环节的细节和容易踩的坑都讲清楚。
2. 实战第一步:准备你的猫狗数据集
任何机器学习项目都始于数据。好在TensorFlow为我们准备了一个现成的猫狗分类数据集,里面包含了大约2000多张已经分好类的猫和狗图片,训练集和验证集都帮我们分好了。我们直接用几行代码就能把它下载下来。
import tensorflow as tf
import os
# 数据集下载地址
_URL = 'https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip'
# 下载并解压,文件会保存在你的用户目录下的 .keras/datasets 文件夹里
path_to_zip = tf.keras.utils.get_file('cats_and_dogs.zip', origin=_URL, extract=True)
# 获取解压后的目录路径
PATH = os.path.join(os.path.dirname(path_to_zip), 'cats_and_dogs_filtered')
# 设置训练和验证集的目录
train_dir = os.path.join(PATH, 'train')
validation_dir = os.path.join(PATH, 'validation')
数据下载好后,我们需要用 image_dataset_from_directory 这个非常方便的函数来加载数据。它会自动根据文件夹结构来推断标签(猫和狗两个文件夹),并生成一个 tf.data.Dataset 对象,这是我们后续训练模型的标准输入格式。
BATCH_SIZE = 32 # 每次训练喂给模型32张图片
IMG_SIZE = (160, 160) # 将所有图片统一缩放到160x160大小
train_dataset = tf.keras.preprocessing.image_dataset_from_directory(
train_dir,
shuffle=True, # 打乱顺序
batch_size=BATCH_SIZE,
image_size=IMG_SIZE,
)
validation_dataset = tf.keras.preprocessing.image_dataset_from_directory(
validation_dir,
shuffle=True,
batch_size=BATCH_SIZE,
image_size=IMG_SIZE,
)
这里有个小技巧,原始数据没有单独的测试集,我们可以从验证集中再划出一部分作为最终的测试集,用来评估模型的真实泛化能力。
# 计算验证集有多少个batch
val_batches = tf.data.experimental.cardinality(validation_dataset)
# 取前20%作为测试集
test_dataset = validation_dataset.take(val_batches // 5)
# 剩下的80%作为真正的验证集,用于训练过程中的评估
validation_dataset = validation_dataset.skip(val_batches // 5)
print(f'验证集批次: {tf.data.experimental.cardinality(validation_dataset)}')
print(f'测试集批次: {tf.data.experimental.cardinality(test_dataset)}')
为了让模型训练得更高效,我们还需要对数据集进行性能优化。使用 prefetch 可以让数据加载和模型训练并行,避免GPU等CPU读数据的尴尬。
AUTOTUNE = tf.data.AUTOTUNE
train_dataset = train_dataset.prefetch(buffer_size=AUTOTUNE)
validation_dataset = validation_dataset.prefetch(buffer_size=AUTOTUNE)
test_dataset = test_dataset.prefetch(buffer_size=AUTOTUNE)
2.1 数据增强:用“想象力”扩充你的小数据集
我们的训练集只有2000张图片,对于深度学习来说有点少,很容易导致模型“过拟合”——就是模型把训练图片的细节甚至噪声都记住了,但遇到新图片就傻眼了。数据增强是解决这个问题的利器,它通过对训练图片进行随机的、合理的变换(如翻转、旋转),来人工创造“新”图片,增加数据的多样性。
data_augmentation = tf.keras.Sequential([
tf.keras.layers.RandomFlip("horizontal"), # 随机水平翻转
tf.keras.layers.RandomRotation(0.2), # 随机旋转±20%
# 你还可以添加更多,比如 RandomZoom, RandomContrast 等
])
我们可以直观地看一下数据增强的效果,对同一张图片应用9次增强操作,看看生成的不同变体。
import matplotlib.pyplot as plt
for images, _ in train_dataset.take(1):
plt.figure(figsize=(10, 10))
first_image = images[0]
for i in range(9):
ax = plt.subplot(3, 3, i + 1)
# 注意:augmentation层期望的输入是带batch维度的,所以要expand_dims
augmented_image = data_augmentation(tf.expand_dims(first_image, 0))
plt.imshow(augmented_image[0] / 255.0) # 显示时需要将像素值从[-1,1]转换回[0,1]
plt.axis('off')
plt.show()
重要提示:数据增强层只在模型训练时激活。当我们用 model.evaluate 或 model.predict 进行验证和预测时,这些层是自动关闭的,不会影响我们的评估结果。
3. 构建模型:请出预训练好的MobileNet V2
现在进入核心环节。我们将加载在ImageNet上预训练好的MobileNet V2模型作为我们的特征提取器。
# 定义输入形状,RGB三通道
IMG_SHAPE = IMG_SIZE + (3,)
# 加载模型,关键参数:include_top=False 表示我们不要模型顶部的全连接分类层
base_model = tf.keras.applications.MobileNetV2(
input_shape=IMG_SHAPE,
include_top=False,
weights='imagenet' # 加载在ImageNet上训练好的权重
)
这里解释一下 include_top=False 的作用。MobileNet V2原本的输出是1000个神经元,对应ImageNet的1000个类别。对于我们猫狗二分类的任务来说,这个“头部”不适用。我们只想要它前面那些卷积层提取到的通用图像特征。去掉顶部后,模型的输出是一个形状为 (batch_size, 5, 5, 1280) 的特征图,你可以理解为每张图片被编码成了1280个5x5的特征图。
3.1 冻结卷积基:保护已有的知识
在开始训练我们自己的分类器之前,必须先把 base_model 的所有层“冻结”起来。这意味着在接下来的训练中,这些层的权重不会被更新。我们只想利用它已经学好的特征,而不是用我们的小数据集去破坏它。
base_model.trainable = False
我们可以用 base_model.summary() 看一下这个庞大的模型结构,你会发现它有超过200万个参数,但此刻 Trainable params 是0。
3.2 添加我们自己的分类头
预训练模型输出了丰富的特征,我们需要在其之上构建一个简单的分类器,来学习“如何根据这些特征判断是猫还是狗”。
# 全局平均池化层:将 (5, 5, 1280) 的特征图,在每个通道上取平均值,变成 (1280,) 的向量
# 这比直接展平(Flatten)参数更少,且有一定抗过拟合作用
global_average_layer = tf.keras.layers.GlobalAveragePooling2D()
# 输出层:一个神经元,用Sigmoid激活函数输出0到1之间的概率值,代表是狗的概率
prediction_layer = tf.keras.layers.Dense(1, activation='sigmoid')
现在,我们用Keras的函数式API,把数据增强、预处理、基础模型和我们的分类头像搭积木一样组合起来。
# 注意:MobileNetV2内置的预处理要求输入像素值在[-1, 1]之间
preprocess_input = tf.keras.applications.mobilenet_v2.preprocess_input
inputs = tf.keras.Input(shape=IMG_SHAPE)
x = data_augmentation(inputs) # 数据增强(仅训练时生效)
x = preprocess_input(x) # 像素值归一化到[-1, 1]
x = base_model(x, training=False) # 注意!这里training=False很重要,确保BatchNormalization层使用推理模式
x = global_average_layer(x)
x = tf.keras.layers.Dropout(0.2)(x) # 随机丢弃20%的神经元,防止过拟合
outputs = prediction_layer(x)
model = tf.keras.Model(inputs, outputs)
这里有一个超级重要的细节:当我们将包含 BatchNormalization 层的预训练模型设置为不可训练时,在调用它时必须显式传入 training=False。这是因为 BatchNormalization 层在训练和推理(预测)时的行为是不同的(是否更新滑动均值和方差)。如果我们不指定,它可能会错误地更新统计量,破坏预训练模型学到的知识。
4. 编译与初始训练:快速得到一个基准模型
模型搭建好了,接下来就是编译和训练。由于基础模型的权重被冻结,我们只需要训练最后添加的Dense层,参数很少,所以学习率可以设得小一点,训练也会非常快。
# 设置一个较小的基础学习率
base_learning_rate = 0.0001
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=base_learning_rate),
loss='binary_crossentropy', # 二分类任务的标准损失函数
metrics=['accuracy']
)
我们先看一眼模型在训练前的初始表现(随机权重):
loss0, accuracy0 = model.evaluate(validation_dataset)
print(f"初始损失: {loss0:.2f}")
print(f"初始准确率: {accuracy0:.2f}")
# 输出大概会是:初始损失: 0.69,初始准确率: 0.50 (相当于随机猜)
现在开始第一阶段的训练,我们只训练新添加的分类头。
initial_epochs = 10
history = model.fit(
train_dataset,
epochs=initial_epochs,
validation_data=validation_dataset
)
训练10个周期后,你会发现验证准确率轻松达到了94%-96%!这简直不可思议,我们只训练了最后那1000多个参数,就获得了如此高的精度。这充分证明了迁移学习的威力——预训练模型的特征提取能力极其强大。
我们可以把训练过程中的准确率和损失变化画出来,直观感受一下。
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
plt.figure(figsize=(8, 8))
plt.subplot(2, 1, 1)
plt.plot(acc, label='训练准确率')
plt.plot(val_acc, label='验证准确率')
plt.legend(loc='lower right')
plt.ylabel('准确率')
plt.ylim([min(plt.ylim()),1])
plt.title('训练和验证准确率')
plt.subplot(2, 1, 2)
plt.plot(loss, label='训练损失')
plt.plot(val_loss, label='验证损失')
plt.legend(loc='upper right')
plt.ylabel('交叉熵')
plt.ylim([0,1.0])
plt.title('训练和验证损失')
plt.xlabel('训练周期')
plt.show()
你可能会注意到一个有趣的现象:验证准确率比训练准确率还高。这主要是因为我们使用了 Dropout 和 BatchNormalization 层。它们在训练时会引入随机性(丢弃神经元、用当前批次的统计量),从而降低了训练指标;但在验证时,它们是关闭的(不丢弃神经元、使用训练好的滑动统计量),所以指标反而更高。这是正常现象。
5. 模型微调:解锁部分预训练层以追求极致
第一阶段我们只训练了分类头,模型表现已经很好。但如果我们想让性能再上一个台阶,可以尝试“微调”。微调的意思是:解冻预训练模型的一部分高层,让它们和我们新加的分类头一起,用我们的小数据集进行训练,从而让特征更适配我们的猫狗任务。
为什么只解冻高层?因为卷积神经网络底层学习的是通用特征(边缘、颜色),这些对所有视觉任务都有用;而高层学习的是与特定任务相关的抽象特征(比如“车轮”、“狗鼻子”)。我们解冻高层,就是允许模型将这些抽象特征从“ImageNet的1000类物体”微调到“猫和狗”的差异上。
重要原则:微调必须在第一阶段训练完成之后进行。如果一开始就解冻并联合训练,分类头的随机初始权重会产生巨大的梯度,彻底破坏预训练模型精心调整好的特征。
# 1. 解冻整个基础模型
base_model.trainable = True
# 2. 查看总层数,MobileNetV2有154层
print(f"基础模型层数: {len(base_model.layers)}")
# 3. 我们决定冻结前100层,只微调后面的54层
fine_tune_at = 100
for layer in base_model.layers[:fine_tune_at]:
layer.trainable = False
5.1 重新编译模型:使用更小的学习率
微调时,我们是在已经很好的权重上进行小幅调整,所以学习率必须比第一阶段更小,通常缩小10倍,避免“大步”更新破坏了原有知识。
# 重新编译模型,使层冻结/解冻的更改生效
model.compile(
optimizer=tf.keras.optimizers.RMSprop(learning_rate=base_learning_rate/10), # 学习率缩小10倍
loss='binary_crossentropy',
metrics=['accuracy']
)
# 再次查看可训练参数,会发现从1千多变成了180多万
model.summary()
5.2 继续训练
现在,我们接着之前的训练,继续微调10个周期。
fine_tune_epochs = 10
total_epochs = initial_epochs + fine_tune_epochs
history_fine = model.fit(
train_dataset,
epochs=total_epochs,
initial_epoch=history.epoch[-1], # 从上一个阶段结束的地方开始
validation_data=validation_dataset
)
微调后,验证准确率通常会再提升1-2个百分点,达到97%-98%。我们把两个阶段的训练曲线连起来看,能清晰看到在微调开始点(第10个周期后),模型性能有一个明显的提升跳跃。
# 合并历史记录
acc += history_fine.history['accuracy']
val_acc += history_fine.history['val_accuracy']
loss += history_fine.history['loss']
val_loss += history_fine.history['val_loss']
plt.figure(figsize=(8, 8))
plt.subplot(2, 1, 1)
plt.plot(acc, label='训练准确率')
plt.plot(val_acc, label='验证准确率')
plt.axvline(x=initial_epochs-1, color='gray', linestyle='--', label='开始微调')
plt.legend(loc='lower right')
plt.ylim([0.8, 1])
plt.title('训练和验证准确率')
plt.subplot(2, 2)
plt.plot(loss, label='训练损失')
plt.plot(val_loss, label='验证损失')
plt.axvline(x=initial_epochs-1, color='gray', linestyle='--', label='开始微调')
plt.legend(loc='upper right')
plt.ylim([0, 1.0])
plt.title('训练和验证损失')
plt.xlabel('训练周期')
plt.show()
6. 模型评估与预测:看看它到底有多聪明
训练完成,是时候用我们之前预留的、模型从未见过的测试集来最终检验一下模型的泛化能力了。
loss, accuracy = model.evaluate(test_dataset)
print(f'\n测试集上的最终表现:')
print(f'损失: {loss:.4f}')
print(f'准确率: {accuracy:.4f}')
如果一切顺利,你应该能看到接近99%的测试准确率。这意味着模型在全新的图片上判断猫狗的准确率非常高。
最后,我们来点好玩的,让模型预测一批测试图片,并可视化结果。
# 获取一批测试图片和真实标签
image_batch, label_batch = test_dataset.as_numpy_iterator().next()
# 进行预测
predictions = model.predict_on_batch(image_batch).flatten()
# 将sigmoid输出转换为0或1的类别
predictions = tf.where(predictions < 0.5, 0, 1).numpy()
print('模型预测结果:\n', predictions)
print('真实标签:\n', label_batch)
# 可视化前9张图片的预测结果
class_names = ['猫', '狗']
plt.figure(figsize=(10, 10))
for i in range(9):
ax = plt.subplot(3, 3, i + 1)
plt.imshow(image_batch[i].astype("uint8"))
plt.title(f"预测: {class_names[predictions[i]]}\n真实: {class_names[label_batch[i]]}")
plt.axis("off")
plt.show()
看着模型几乎全部预测正确,是不是很有成就感?回顾整个过程,我们从零开始,借助迁移学习,用很少的代码和训练时间,就构建了一个高性能的猫狗分类器。这正是现代深度学习框架和预训练模型带给我们的便利。你可以把这个方法应用到其他类似的图像分类问题上,比如识别不同种类的花朵、区分汽车型号等等。关键就是找到合适的预训练模型,理解冻结、微调的策略,然后动手去试。我在实际项目中遇到过数据量更少的情况,通过更激进的数据增强和谨慎的微调,同样取得了不错的效果。记住,实践出真知,多调参,多观察训练曲线,你就能越来越得心应手。
更多推荐
所有评论(0)