迁移学习实战教程:从理论到实践入门

23次阅读
没有评论

共计 17820 个字符,预计需要花费 45 分钟才能阅读完成。

在深度学习的浪潮中,我们常常惊叹于那些在图像识别、自然语言处理等领域取得卓越成就的庞大模型。然而,训练这些动辄拥有数亿参数的“巨兽”,需要海量的标注数据、强大的计算资源以及漫长的训练时间,这对于许多企业和研究者来说是难以企及的。幸运的是,迁移学习(Transfer Learning)为我们打开了一扇新的大门,它允许我们将一个在大型数据集上预训练好的模型“迁移”到我们自己的任务中,从而在数据和资源有限的情况下,快速构建出高性能的深度学习应用。本篇教程将以超过两万字的篇幅,带你深入探索迁移学习的理论精髓,并手把手地通过一个完整的图像分类实战项目,让你彻底掌握这项强大的技术。

第一章:迁移学习的理论基石

1.1 什么是迁移学习?

迁移学习,顾名思义,就是将从一个任务中学到的知识“迁移”到另一个相关但不同的任务中。这个概念并非深度学习所独有,在人类的学习过程中,我们无时无刻不在运用迁移学习。例如,学会了骑自行车的人,学习骑摩托车就会变得相对容易,因为他们已经掌握了平衡、方向控制等核心技能。类似地,一个在庞大的 ImageNet 数据集(包含超过 1400 万张图片,涵盖 1000 多个类别)上训练好的图像识别模型,已经学会了如何识别图像的底层特征(如边缘、纹理、颜色)和高层特征(如物体的形状、部件的组合)。当我们面临一个新的、数据量较少的图像分类任务时,比如识别不同种类的花卉,我们就不需要从零开始训练一个模型去学习这些基础特征,而是可以借助这个预训练模型的“慧眼”,让它在我们的小数据集上进行微调,从而快速达到理想的效果。

迁移学习的核心思想:利用在一个大规模数据集上训练好的模型所学习到的通用特征表示,来帮助解决另一个相关领域中数据量不足的问题。

1.2 为什么需要迁移学习?

在深度学习的实践中,我们经常会遇到以下挑战,而迁移学习正是应对这些挑战的有力武器:

  • 数据稀缺:在许多专业领域,如医疗影像分析、工业瑕疵检测等,获取大量标注好的数据是非常困难且昂贵的。从零开始训练一个深度神经网络,很容易因为数据不足而导致过拟合,即模型仅仅记住了训练数据中的噪声和细节,而无法泛化到新的数据上。
  • 计算资源限制:训练一个顶级的深度学习模型,往往需要数周甚至数月的 GPU/TPU 训练时间,这对于大多数个人开发者和中小型企业来说是一笔巨大的开销。
  • 时间成本高昂:除了计算成本,模型的设计、训练、调试和调优也需要耗费大量的时间和人力。

迁移学习通过以下方式有效地解决了这些问题:

  • 降低对数据的依赖:由于预训练模型已经学习到了通用的特征,我们只需要少量的数据就能让模型适应新的任务。
  • 节省计算资源和时间:我们无需从零开始训练模型,大大缩短了训练周期,降低了计算成本。
  • 提升模型性能:预训练模型在一个巨大的数据集上进行了充分的学习,其学到的特征表示通常比在小数据集上从零开始训练的模型更为鲁棒和泛化,从而能够在新任务上取得更好的性能。

1.3 迁移学习的分类

根据源任务和目标任务之间的数据和领域关系,迁移学习可以分为多种类型。在深度学习的背景下,我们主要关注以下两种最常见的策略:

1.3.1 特征提取(Feature Extraction)

特征提取是一种相对简单但非常有效的迁移学习方法。其核心思想是:我们将预训练模型作为一个固定的特征提取器。具体来说,我们加载一个预训练模型(例如 VGG16、ResNet50),去掉其顶部的全连接分类层(因为这些层通常是针对原始任务的特定类别进行分类的),然后将我们的数据输入到这个“残缺”的模型中,得到每个输入的特征向量。这些特征向量可以看作是原始数据在预训练模型学到的高维特征空间中的表示。最后,我们在这些提取出的特征之上,训练一个新的、通常是较简单的分类器(如逻辑回归、支持向量机,或者一个小型的全连接神经网络)。

操作步骤

  1. 加载预训练模型:选择一个在大型数据集(如 ImageNet)上预训练好的模型。
  2. 冻结权重:将预训练模型的所有层的权重设置为不可训练(冻结),以防止在训练新分类器时破坏其已经学到的通用特征。
  3. 提取特征:将我们的训练数据和测试数据通过预训练模型,获取其输出的特征图。
  4. 训练新分类器:使用提取出的特征来训练一个新的分类器,以完成我们的特定任务。

适用场景

  • 目标任务的数据集非常小。
  • 目标任务与预训练模型的原始任务非常相似。

1.3.2 微调(Fine-Tuning)

微调是一种更为深入的迁移学习方法。与特征提取不同,微调不仅会替换掉预训练模型的分类头,还会“解冻”一部分或全部的预训练模型的卷积层,并在新的数据集上以一个较小的学习率继续进行训练。这样做的目的是让预训练模型学到的通用特征能够更好地适应新任务的数据分布。

操作步骤

  1. 加载预训练模型:同特征提取。
  2. 替换分类头:将预训练模型的顶部全连接分类层替换为适合我们新任务的分类层(例如,如果我们的任务是 10 分类,就替换为一个输出为 10 个神经元的全连接层)。
  3. 冻结部分底层:通常,我们会选择冻结预训练模型靠近输入的部分卷积层(底层)。因为这些底层学习到的是非常通用的特征(如边缘、颜色),这些特征对于大多数视觉任务都是有用的。而靠近输出的高层则学习到的是更抽象、更接近特定任务的特征,这些特征可能需要针对新任务进行调整。
  4. 进行微调训练:使用我们的数据集,以一个非常小的学习率对整个模型(包括解冻的卷积层和新的分类头)进行端到端的训练。使用小学习率是为了避免在训练初期,由于随机初始化的新分类头产生较大的梯度,从而剧烈地改变预训练模型已经学好的权重,导致“灾难性遗忘”(Catastrophic Forgetting)。

适用场景

  • 目标任务的数据集虽然不大,但也有一定的规模。
  • 目标任务与预训练模型的原始任务有一定相似性,但又不完全相同。
  • 希望获得比特征提取更好的性能。

如何选择冻结的层数? 这是一个经验性的问题,通常取决于新任务与预训练任务的相似性以及新数据集的大小。

  • 新数据集小,与原数据集相似:这种情况风险较高,容易过拟合。建议使用特征提取的方式,或者只微调最后的分类头。
  • 新数据集小,与原数据集不相似:这种情况比较棘手。可以尝试微调更多的高层,但需要非常小心地使用正则化技术(如 Dropout)来防止过拟合。
  • 新数据集大,与原数据集相似:这是最理想的情况。可以自信地微调整个模型,或者只冻结非常底层的少数几层。
  • 新数据集大,与原数据集不相似:由于数据量充足,可以考虑从头开始训练一个模型。但通常情况下,使用预训练模型的权重作为初始化,然后微调整个模型,仍然会比随机初始化收敛得更快、效果更好。

1.4 经典的预训练模型

在计算机视觉领域,有许多在 ImageNet 数据集上预训练好的经典模型可供我们使用。这些模型在网络结构上各有特点,性能也略有差异。

  • VGGNet (VGG16, VGG19):由牛津大学的视觉几何组(Visual Geometry Group)提出。其特点是结构简单,整个网络都使用了同样大小的卷积核(3×3)和最大池化层(2×2)。通过不断加深网络结构,VGGNet 在 ImageNet 竞赛中取得了优异的成绩。VGG16 和 VGG19 分别代表其拥有 16 和 19 个含权重的层。其缺点是参数量巨大,对计算资源要求较高。
  • ResNet (ResNet50, ResNet101):由微软研究院的何恺明等人提出,是深度学习发展史上的一个里程碑。它创造性地引入了“残差连接”(Residual Connection)或“快捷连接”(Shortcut Connection),有效地解决了深度神经网络中的梯度消失和网络退化问题,使得训练数百层甚至上千层的网络成为可能。ResNet50 是其一个经典的 50 层版本,在性能和计算效率之间取得了很好的平衡。
  • Inception (GoogLeNet, InceptionV3):由谷歌团队提出。其核心是 Inception 模块,该模块在一个层中并行地使用不同大小的卷积核(1×1, 3×3, 5×5)和池化操作,然后将它们的输出拼接在一起。这样做的好处是可以在不同的尺度上提取特征,并增加了网络的宽度,从而在不显著增加计算量的情况下提升了模型的表达能力。
  • MobileNet:由谷歌团队为移动和嵌入式设备设计的轻量级网络。它采用了深度可分离卷积(Depthwise Separable Convolutions)来大幅减少模型的参数量和计算量,同时保持了较高的准确率。
  • EfficientNet:由谷歌团队提出的一个模型家族。它通过一种复合缩放方法(Compound Scaling),系统地、同时地对网络的深度、宽度和分辨率进行缩放,从而在极高的效率下实现了 SOTA(State-of-the-art)的性能。

在选择预训练模型时,我们需要在模型的性能(准确率)和效率(模型大小、推理速度)之间进行权衡。对于资源受限的应用,MobileNet 和 EfficientNet 的轻量级版本是很好的选择。而对于追求极致性能的应用,ResNet 或 EfficientNet 的更深版本则更为合适。

第二章:实战环境准备

在本教程的实战部分,我们将使用 Python 语言和强大的深度学习框架 TensorFlow 及其高级 API Keras 来完成一个完整的迁移学习项目。

2.1 安装必要的库

首先,请确保你的 Python 环境中已经安装了以下库:

  • TensorFlow:谷歌开源的深度学习框架。
  • NumPy:Python 中用于科学计算的基础库,用于处理多维数组。
  • Matplotlib:一个用于数据可视化的库,我们将用它来展示图片和训练过程中的图表。
  • Scikit-learn:一个机器学习库,我们将用它来评估模型的性能。

你可以使用 pip 来安装这些库:

Bash

pip install tensorflow numpy matplotlib scikit-learn

建议在一个独立的虚拟环境中进行安装,以避免与系统中其他的 Python 项目产生依赖冲突。

2.2 数据集介绍与下载

为了更好地演示迁移学习的效果,我们选择了一个经典且直观的数据集:“猫狗大战”(Dogs vs. Cats)。这个数据集由微软和 Kaggle 联合提供,包含了 25000 张猫和狗的图片(训练集),以及 12500 张用于测试的图片。我们的任务是训练一个模型,能够准确地区分一张图片里的是猫还是狗。

这个数据集的特点是:

  • 类别简单:只有两个类别,便于理解和实现。
  • 数据量适中:对于从零开始训练一个模型来说,这个数据量可能略显不足,但对于迁移学习来说,这是一个非常合适的规模。
  • 数据多样性:图片中的猫和狗姿态各异、背景复杂,对模型的泛化能力提出了一定的挑战。

你可以从 Kaggle 官方网站下载该数据集:https://www.kaggle.com/c/dogs-vs-cats/data

下载后,你会得到一个 dogs-vs-cats.zip 文件。解压后,会看到 train.ziptest1.zip。请将它们也一并解压。

2.3 数据集整理

原始的数据集结构并不直接适用于 Keras 的数据加载器。Keras 的 ImageDataGenerator 要求数据按照以下的目录结构进行组织:

<base_directory>/
    train/
        class_a/
            image_1.jpg
            image_2.jpg
            ...
        class_b/
            image_1.jpg
            image_2.jpg
            ...
    validation/
        class_a/
            image_1.jpg
            image_2.jpg
            ...
        class_b/
            image_1.jpg
            image_2.jpg
            ...

因此,我们需要编写一个脚本来将原始的 train 文件夹中的图片分门别类地整理到新的目录结构中。我们还会从训练集中划分出一部分作为验证集,用于在训练过程中监控模型的性能,防止过拟合。

以下是一个 Python 脚本,用于完成数据集的整理工作:

Python

import os
import shutil
from sklearn.model_selection import train_test_split

# 原始数据集路径
original_dataset_dir = './train'

# 新的数据集基础路径
base_dir = './cats_and_dogs_small'
os.makedirs(base_dir, exist_ok=True)

# 创建训练、验证、测试目录
train_dir = os.path.join(base_dir, 'train')
os.makedirs(train_dir, exist_ok=True)
validation_dir = os.path.join(base_dir, 'validation')
os.makedirs(validation_dir, exist_ok=True)

# 创建猫和狗的子目录
train_cats_dir = os.path.join(train_dir, 'cats')
os.makedirs(train_cats_dir, exist_ok=True)
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.makedirs(train_dogs_dir, exist_ok=True)

validation_cats_dir = os.path.join(validation_dir, 'cats')
os.makedirs(validation_cats_dir, exist_ok=True)
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.makedirs(validation_dogs_dir, exist_ok=True)

# 获取所有猫和狗的文件名
cat_fnames = [f'cat.{i}.jpg' for i in range(12500)]
dog_fnames = [f'dog.{i}.jpg' for i in range(12500)]

# 为了快速演示,我们只使用一部分数据
# 实际项目中,你可以使用全部数据
# 我们随机抽取 2000 张猫和 2000 张狗的图片
# 其中 1500 张用于训练,500 张用于验证

# 划分猫的数据
train_cat_fnames, val_cat_fnames = train_test_split(cat_fnames, test_size=0.25, random_state=42) # 12500*0.25 = 3125 for val
train_cat_fnames = train_cat_fnames[:1500]
val_cat_fnames = val_cat_fnames[:500]

# 划分狗的数据
train_dog_fnames, val_dog_fnames = train_test_split(dog_fnames, test_size=0.25, random_state=42)
train_dog_fnames = train_dog_fnames[:1500]
val_dog_fnames = val_dog_fnames[:500]


print(f'Total training cat images: {len(train_cat_fnames)}')
print(f'Total validation cat images: {len(val_cat_fnames)}')
print(f'Total training dog images: {len(train_dog_fnames)}')
print(f'Total validation dog images: {len(val_dog_fnames)}')


# 复制文件到新的目录
def copy_files(fnames, source_dir, dest_dir):
    for fname in fnames:
        src = os.path.join(source_dir, fname)
        dst = os.path.join(dest_dir, fname)
        shutil.copyfile(src, dst)

copy_files(train_cat_fnames, original_dataset_dir, train_cats_dir)
copy_files(val_cat_fnames, original_dataset_dir, validation_cats_dir)
copy_files(train_dog_fnames, original_dataset_dir, train_dogs_dir)
copy_files(val_dog_fnames, original_dataset_dir, validation_dogs_dir)

print("Dataset reorganization complete!")

运行这个脚本后,你将得到一个新的 cats_and_dogs_small 文件夹,其中包含了我们整理好的训练集和验证集。我们特意选择了一个较小的数据子集(训练集 3000 张,验证集 1000 张),以模拟真实场景中数据量不足的情况,从而更好地凸显迁移学习的优势。

第三章:实战一:使用预训练模型作为特征提取器

在第一个实战项目中,我们将采用特征提取的方法。我们会加载在 ImageNet 上预训练好的 VGG16 模型,利用其卷积基(convolutional base)来提取猫狗图片的特征,然后在此之上训练一个新的分类器。

3.1 加载预训练的 VGG16 模型

Keras 的 applications 模块中内置了多种经典的预训练模型,我们可以非常方便地加载它们。

Python

from tensorflow.keras.applications import VGG16

# 加载 VGG16 模型,权重为在 ImageNet 上预训练好的权重
# include_top=False 表示不包含顶部的全连接分类层
# input_shape 是输入图片的大小,这里我们统一设置为 150x150 像素,3 个颜色通道
conv_base = VGG16(weights='imagenet',
                  include_top=False,
                  input_shape=(150, 150, 3))

# 打印模型结构,观察其层次
conv_base.summary()

conv_base.summary()的输出会显示 VGG16 的卷积基结构。可以看到,最后一层的输出形状是(None, 4, 4, 512)。这意味着对于一个 150×150 的输入图片,VGG16 会将其转换为一个 4x4x512 的特征图。

3.2 数据预处理与特征提取

接下来,我们需要将我们的猫狗图片转换成 VGG16 模型可以处理的格式,并通过模型来提取特征。我们将使用 Keras 的 ImageDataGenerator 来完成这项工作。这个工具可以方便地从硬盘上的图片目录中生成批量的数据,并且可以进行数据增强(data augmentation),我们将在后续的微调部分详细介绍。

Python

import numpy as np
from tensorflow.keras.preprocessing.image import ImageDataGenerator

base_dir = './cats_and_dogs_small'
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')

# ImageDataGenerator 会将像素值从 0 -255 缩放到 0 - 1 之间,这是神经网络训练的常规操作
datagen = ImageDataGenerator(rescale=1./255)
batch_size = 20

def extract_features(directory, sample_count):
    features = np.zeros(shape=(sample_count, 4, 4, 512))
    labels = np.zeros(shape=(sample_count))
    
    # flow_from_directory 会根据子目录的名称自动生成标签
    # 例如,'cats' 目录下的图片标签为 0,'dogs' 目录下的为 1
    generator = datagen.flow_from_directory(
        directory,
        target_size=(150, 150),
        batch_size=batch_size,
        class_mode='binary' # 因为是二分类
    )
    
    i = 0
    for inputs_batch, labels_batch in generator:
        # 使用 VGG16 的卷积基预测(提取)特征
        features_batch = conv_base.predict(inputs_batch)
        features[i * batch_size : (i + 1) * batch_size] = features_batch
        labels[i * batch_size : (i + 1) * batch_size] = labels_batch
        i += 1
        # 因为生成器是无限循环的,我们需要在处理完所有图片后手动跳出
        if i * batch_size >= sample_count:
            break
            
    return features, labels

# 提取训练集和验证集的特征
# 训练集有 3000 张图片,验证集有 1000 张
train_features, train_labels = extract_features(train_dir, 3000)
validation_features, validation_labels = extract_features(validation_dir, 1000)

# 将提取的特征展平,以便输入到全连接层
train_features = np.reshape(train_features, (3000, 4 * 4 * 512))
validation_features = np.reshape(validation_features, (1000, 4 * 4 * 512))

代码解释

  • 我们定义了一个 extract_features 函数,它接收一个目录路径和样本数量作为输入。
  • 在函数内部,我们使用 ImageDataGenerator 创建了一个数据生成器。target_size参数确保了所有的图片都被调整到 150×150 的大小。class_mode='binary'指定了这是一个二分类问题。
  • 我们遍历这个生成器,它会一批一批地(每批 20 张)产生图片数据和对应的标签。
  • 对于每一批图片,我们调用 conv_base.predict() 来提取特征。
  • 最后,我们将提取出的 3D 特征图(4x4x512)展平成 1D 的向量,以便后续输入到我们的分类器中。

3.3 构建并训练分类器

现在我们已经有了从 VGG16 提取出的特征,接下来我们可以在这些特征之上构建一个简单的全连接神经网络作为我们的分类器。

Python

from tensorflow.keras import models
from tensorflow.keras import layers
from tensorflow.keras import optimizers

# 定义模型
model = models.Sequential()
model.add(layers.Dense(256, activation='relu', input_dim=4 * 4 * 512))
model.add(layers.Dropout(0.5)) # 使用 Dropout 防止过拟合
model.add(layers.Dense(1, activation='sigmoid')) # 输出层,使用 sigmoid 激活函数进行二分类

# 编译模型
model.compile(optimizer=optimizers.RMSprop(learning_rate=2e-5),
              loss='binary_crossentropy',
              metrics=['acc'])

# 训练模型
history = model.fit(train_features, train_labels,
                    epochs=30,
                    batch_size=20,
                    validation_data=(validation_features, validation_labels))

代码解释

  • 我们使用Sequential API 构建了一个简单的两层神经网络。
  • 第一层是一个包含 256 个神经元的 Dense 层,使用 ReLU 作为激活函数。
  • 为了防止在小数据集上过拟合,我们加入了一个 Dropout 层,它会在训练过程中随机地将 50% 的神经元输出置为 0。
  • 输出层是一个 Dense 层,只有一个神经元,并使用 sigmoid 激活函数。sigmoid函数的输出在 0 到 1 之间,可以被解释为图片是“狗”的概率(假设“狗”的标签是 1)。
  • 我们使用 RMSprop 优化器,并选择了一个较小的学习率2e-5
  • 损失函数使用binary_crossentropy,这是二分类问题中最常用的损失函数。
  • 我们训练了 30 个周期(epochs),并使用验证集来监控模型的性能。

3.4 评估模型性能

训练完成后,我们可以通过绘制训练过程中的准确率和损失曲线来直观地评估模型的性能。

Python

import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

# 绘制准确率曲线
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

# 绘制损失曲线
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

运行这段代码,你会看到两张图表。通常情况下,你会观察到验证集准确率可以轻松达到 90% 左右,这是一个非常不错的结果,远好于从零开始训练一个卷积神经网络。这充分证明了即使是作为静态的特征提取器,预训练模型也能提供非常强大的特征表示能力。

然而,你可能也会注意到训练准确率和验证准确率之间存在一定的差距,并且验证损失在训练后期可能会有上升的趋势,这表明模型仍然存在一定的过拟合。

特征提取方法的优缺点

  • 优点:计算速度快。因为我们只需要对数据进行一次前向传播来提取特征,之后只需要训练一个非常小的分类器,这个过程非常迅速。
  • 缺点:性能可能不是最优的。因为我们没有对预训练模型的权重进行任何调整,它学到的特征可能不是最适合我们新任务的。此外,这种方法无法使用数据增强,因为数据增强需要在模型训练的每一步动态地对输入图片进行变换。

第四章:实战二:微调预训练模型

为了获得更好的性能,并利用数据增强来进一步抑制过拟合,我们将采用第二种迁移学习策略:微调(Fine-Tuning)。

在微调中,我们将预训练的 VGG16 模型与我们自定义的分类器连接起来,形成一个更大的模型,并对这个大模型进行端到端的训练。

4.1 构建模型

我们将 VGG16 的卷积基作为我们模型的一部分,并在其之上添加我们的分类器。

Python

from tensorflow.keras import models
from tensorflow.keras import layers

# 重新加载 VGG16 模型,这次它将作为我们模型的一个层
conv_base = VGG16(weights='imagenet',
                  include_top=False,
                  input_shape=(150, 150, 3))

# 构建我们的模型
model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.summary()

model.summary()的输出会显示,现在我们的模型总参数量非常巨大(超过 1400 万),这些参数绝大部分来自于 VGG16 的卷积基。如果我们从零开始在 3000 张图片上训练这样一个模型,几乎肯定会发生严重的过拟合。

4.2 冻结卷积基

在开始微调之前,一个非常重要的步骤是 冻结 卷积基。这意味着我们将它的权重设置为在训练过程中不可改变。我们只训练我们自己添加的分类器部分。如果我们不这样做,那么在训练的初始阶段,由于我们新添加的分类器是随机初始化的,它会产生非常大的误差梯度。这些梯度会通过反向传播传回到底层的卷积基,从而严重破坏 VGG16 在 ImageNet 上学到的那些宝贵的特征。1

Python

print('This is the number of trainable weights'
      'before freezing the conv base:', len(model.trainable_weights))

# 冻结卷积基
conv_base.trainable = False

print('This is the number of trainable weights'
      'after freezing the conv base:', len(model.trainable_weights))

运行这段代码,你会看到可训练的权重数量大幅减少。现在,只有我们添加的两个 Dense 层的权重是可训练的。

4.3 数据预处理与数据增强

微调方法的一大优势是我们可以使用数据增强(Data Augmentation)。数据增强是一种通过对训练图片进行一系列随机变换(如旋转、缩放、平移、翻转等)来生成新的、可信的训练样本的技术。这相当于人为地增加了训练集的多样性,是一种非常有效的正则化手段,可以有效地减轻模型的过拟合。

Keras 的 ImageDataGenerator 提供了丰富的数据增强功能。

Python

from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras import optimizers

# 训练数据生成器,进行数据增强
train_datagen = ImageDataGenerator(
      rescale=1./255,
      rotation_range=40,      # 随机旋转的角度范围
      width_shift_range=0.2,  # 随机水平平移的范围
      height_shift_range=0.2, # 随机竖直平移的范围
      shear_range=0.2,        # 随机错切变换的范围
      zoom_range=0.2,         # 随机缩放的范围
      horizontal_flip=True,   # 随机水平翻转
      fill_mode='nearest')    # 填充新创建像素的方式

# 注意:验证数据不应该被增强!# 我们只需要将其像素值缩放到 0 -1
validation_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(
        train_dir,
        target_size=(150, 150),
        batch_size=20,
        class_mode='binary')

validation_generator = validation_datagen.flow_from_directory(
        validation_dir,
        target_size=(150, 150),
        batch_size=20,
        class_mode='binary')

# 编译模型
model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(learning_rate=2e-5),
              metrics=['acc'])

# 训练模型
# steps_per_epoch = 训练样本数 / batch_size
# validation_steps = 验证样本数 / batch_size
history = model.fit(
      train_generator,
      steps_per_epoch=150, # 3000 / 20
      epochs=30,
      validation_data=validation_generator,
      validation_steps=50) # 1000 / 20

# 保存模型
model.save('cats_and_dogs_small_3.h5')

代码解释

  • 我们创建了一个train_datagen,并设置了多种数据增强的参数。
  • validation_datagen则保持简单,只进行像素值的缩放。
  • 然后我们使用 .flow_from_directory() 方法来创建数据生成器。
  • 在调用 model.fit() 时,我们传入的是数据生成器,而不是像之前那样直接传入 Numpy 数组。
  • steps_per_epochvalidation_steps 参数是必需的,因为数据生成器会无限地生成数据。我们需要告诉 fit 方法在一个 epoch 中从生成器中抽取多少批次的数据。

训练这个模型会比之前的特征提取方法慢一些,因为每一次梯度更新都需要通过整个 VGG16 模型进行前向和反向传播。训练完成后,我们可以像之前一样绘制准确率和损失曲线。你会发现,由于数据增强的作用,模型的过拟合现象得到了明显的缓解,验证集准确率也会有进一步的提升,可能会达到 92%-94%。

4.4 微调卷积基的高层

到目前为止,我们只训练了我们自己添加的分类器,而 VGG16 的卷积基仍然是冻结的。为了获得更好的性能,我们可以进入微调的第二阶段:解冻卷积基的一部分高层,并以一个非常低的学习率对它们进行训练。

为什么只微调高层?因为在卷积神经网络中,底层学习到的特征是比较通用的(如边缘、纹理),而高层学习到的特征则更加抽象和专业化(如“狗耳朵”、“猫眼睛”)。对于我们的猫狗分类任务,这些高层特征可能需要进行一些调整以更好地适应。而底层的通用特征则可以直接复用。

Python

# 查看 VGG16 卷积基的层结构
conv_base.summary()

summary 的输出中,我们可以看到 VGG16 的卷积基由多个卷积块(block1_conv1, block2_conv1, …)组成。我们决定微调最后的三个卷积层,即从 block5_conv1 开始。

Python

# 解冻 `block5`
conv_base.trainable = True

set_trainable = False
for layer in conv_base.layers:
    if layer.name == 'block5_conv1':
        set_trainable = True
    if set_trainable:
        layer.trainable = True
    else:
        layer.trainable = False

# 重新编译模型,这次使用一个更低的学习率
model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(learning_rate=1e-5),
              metrics=['acc'])

# 继续训练模型
history_fine_tune = model.fit(
      train_generator,
      steps_per_epoch=150,
      epochs=30, # 通常微调阶段的 epoch 数可以更多
      validation_data=validation_generator,
      validation_steps=50)

代码解释

  • 我们首先将整个 conv_base 设置为可训练。
  • 然后,我们遍历 conv_base 的所有层,当遇到 block5_conv1 时,将一个标志位 set_trainable 设为True。此后的所有层都将被设置为可训练,而之前的层则保持冻结。
  • 非常重要:在解冻了部分层之后,我们必须重新编译模型,这样新的可训练设置才会生效。
  • 在重新编译时,我们使用了一个比之前更低的学习率(1e-5)。这是微调的关键。我们希望对预训练的权重只进行微小的、渐进式的调整,以避免破坏它们已经学好的特征。一个大的学习率会使得权重更新的步子太大,可能会导致灾难性的后果。
  • 我们继续训练模型 30 个周期。

训练完成后,再次绘制学习曲线。你很可能会看到验证集准确率又有了新的提升,可能会达到 95% 甚至更高。这就是微调的力量!

4.5 合并学习曲线

为了更清晰地看到微调带来的效果,我们可以将两个训练阶段的学习曲线绘制在同一张图上。

Python

acc += history_fine_tune.history['acc']
val_acc += history_fine_tune.history['val_acc']
loss += history_fine_tune.history['loss']
val_loss += history_fine_tune.history['val_loss']

plt.plot(range(1, len(acc) + 1), acc, 'bo', label='Training acc')
plt.plot(range(1, len(val_acc) + 1), val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()

plt.plot(range(1, len(loss) + 1), loss, 'bo', label='Training loss')
plt.plot(range(1, len(val_loss) + 1), val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()

从合并后的图表中,你可以清晰地看到在第 30 个 epoch 之后,由于我们开始微调卷积基,模型的准确率和损失曲线都发生了明显的变化,验证集准确率通常会有一个显著的跃升。

第五章:进阶技巧与最佳实践

掌握了基本的特征提取和微调之后,我们还可以探索一些进阶的技巧,来进一步提升迁移学习的效果。

5.1 选择合适的预训练模型

我们之前使用了 VGG16,但它是一个相对较老的模型。现在有许多更先进、性能更好、效率更高的模型可供选择,如 ResNet50, InceptionV3, EfficientNet 等。

  • ResNet50:由于其残差连接的设计,通常比 VGG16 更容易训练,性能也更好。
  • EfficientNet:在准确率和效率方面都达到了 SOTA 水平,是目前许多任务的首选。

尝试使用不同的预训练模型作为你的基础,看看哪个模型在你的特定任务上表现最好,这是一个值得探索的方向。在 Keras 中切换模型非常简单,只需要改变一行代码即可:

Python

from tensorflow.keras.applications import ResNet50

# base_model = ResNet50(weights='imagenet', include_top=False, input_shape=(224, 224, 3))

需要注意的是,不同的模型可能需要不同的输入尺寸(例如,ResNet 和 Inception 通常使用 224×224),并且它们的预处理方式也可能不同(例如,有些模型要求输入像素值在 - 1 到 1 之间,而不是 0 到 1)。在使用新的预训练模型时,请务必查阅其官方文档。Keras 的 applications 模块为每个模型都提供了相应的预处理函数(如tensorflow.keras.applications.resnet50.preprocess_input)。

5.2 差异化学习率(Differential Learning Rates)

在微调时,我们对整个模型(包括解冻的卷积层和新添加的分类器)使用了同一个非常低的学习率。一个更精细的策略是使用 差异化学习率

其思想是:对于靠近输入的底层(例如block1, block2),我们使用一个极低的学习率,因为它们学习到的通用特征几乎不需要改变。对于中间层(例如block3, block4),我们使用一个中等的学习率。而对于我们新添加的、从零开始训练的分类器,我们使用一个相对较高的学习率。

这种策略在 fast.ai 库中得到了广泛的应用和推广。在 TensorFlow/Keras 中实现差异化学习率需要一些额外的工作,通常需要为模型的不同部分定义不同的优化器,或者自定义训练循环。

5.3 渐进式解冻(Gradual Unfreezing)

另一个有效的微调策略是 渐进式解冻。我们不是一次性地解冻所有我们想微调的层,而是分阶段进行:

  1. 阶段一:冻结整个卷积基,只训练新添加的分类器,直到验证集损失不再下降。
  2. 阶段二:解冻最高的一个卷积块(例如block5),并以一个较低的学习率进行微调。
  3. 阶段三:再解冻下一个卷积块(例如block4),并以一个更低的学习率进行微调。
  4. … 依此类推。

这种由上至下、由浅入深的微调方式,可以更稳定地让模型的特征适应新任务,减少“灾难性遗忘”的风险。

5.4 评估与测试

在整个教程中,我们都使用验证集来监控和指导我们的训练过程。但是,验证集的性能并不能完全代表模型在真实世界中的泛化能力,因为我们可能会在不经意间根据验证集的表现来调整我们的模型和超参数,这实际上是一种间接的“信息泄露”。

因此,在完成所有训练和调优之后,我们还需要一个独立的 测试集(Test Set)来对最终的模型进行一次性的、公正的评估。这个测试集在整个模型开发过程中都不能被用来做任何决策。

对于我们的猫狗大战项目,你可以将原始的 test1.zip 解压,并用我们最终训练好的模型对这些图片进行预测,然后将结果提交到 Keras 平台,以获得最终的测试分数。

Python

# 加载最终训练好的模型
model = models.load_model('cats_and_dogs_final.h5')

# 假设你已经准备好了测试数据生成器 test_generator
# test_generator = ...

# 进行预测
predictions = model.predict(test_generator)

# 处理预测结果...

第六章:总结与展望

本篇教程从迁移学习的基本概念出发,深入探讨了其背后的原理、优势以及两种核心的实战策略:特征提取和微调。通过一个完整的猫狗分类项目,我们手把手地演示了如何利用 Keras 框架,加载预训练的 VGG16 模型,并一步步地通过冻结、训练分类器、解冻高层、微调等步骤,构建出一个高性能的图像分类器。我们最终达到的超过 95% 的准确率,充分展示了迁移学习在数据和资源有限的情况下,所能爆发出的巨大威力。

核心要点回顾

  • 迁移学习 是将在一个任务上学到的知识应用于另一个相关任务的技术,是解决深度学习中数据稀缺、计算资源昂贵问题的关键。
  • 特征提取 是一种快速、简单的迁移学习方法,它将预训练模型作为固定的特征提取器,适用于数据集极小的情况。
  • 微调 是一种更强大、更灵活的方法,它通过解冻并继续训练预训练模型的部分或全部权重,使模型更好地适应新任务。
  • 数据增强 是微调中不可或缺的伙伴,它能有效地抑制过拟合,提升模型的泛化能力。
  • 冻结 是微调前的关键步骤,可以保护预训练权重不被初始的大学习率所破坏。
  • 使用一个 非常低的学习率 进行微调,是成功调整预训练权重的核心秘诀。

正文完
 0
一诺
版权声明:本站原创文章,由 一诺 于2023-01-18发表,共计17820字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码