Keras深度学习框架第十二讲:迁移学习与微调

80 篇文章 0 订阅
50 篇文章 0 订阅

1、绪论

1.1 迁移学习的定义

深度学习的迁移学习是一种技术,它允许将一个任务上学到的知识或模型应用到另一个任务中。其核心思想是将一种任务中学习的特征或模型权重用于另一种任务,以实现知识的迁移和模型的优化。

迁移学习在深度学习中具有广泛的应用,特别是在数据量较少的情况下。通过利用在源领域(source domain)上学习到的知识,迁移学习可以帮助目标领域(target domain)上的学习任务。迁移学习的主要类型包括特征提取、微调和共享参数。

  • 特征提取:将源领域上训练好的模型的中间层输出作为特征提取器,然后在目标领域上训练新的分类器。这种方法允许我们重用已训练模型中的特征表示,以加速新任务的训练过程。
  • 微调(Fine-tuning):将源领域上训练好的模型的参数作为初始参数,在目标领域上继续训练模型。微调可以进一步调整模型的参数以适应新任务,从而提高模型在新任务上的性能。
  • 共享参数:将源领域和目标领域的数据同时输入模型,共享部分参数进行训练。这种方法允许模型同时学习两个任务的知识,并通过共享参数来实现知识的迁移。

迁移学习在深度学习中的应用场景非常广泛,包括图像分类、自然语言处理、语音识别和游戏AI等领域。通过迁移学习,我们可以减少训练数据量、节省训练时间并提高模型性能。

例如,在图像分类任务中,当源领域上有大量标注数据而目标领域上的数据较少时,我们可以使用迁移学习将源领域上的模型应用到目标领域上,从而提升目标领域的分类性能。同样地,在自然语言处理任务中,迁移学习也可以帮助我们利用在大规模语料库上预训练的模型来加速新任务的训练过程并提高性能。

1.2 迁移学习的流程

迁移学习通常用于那些数据集太小、无法从头开始训练一个完整模型的任务。

在深度学习的背景下,迁移学习的最常见形式是以下工作流程:

  • 从一个预先训练好的模型中取出层。
  • 冻结这些层,以避免在未来的训练轮次中破坏它们所包含的信息。
  • 在冻结的层之上添加一些新的、可训练的层。这些新层将学习如何将旧的特征转化为新数据集上的预测。
  • 在你的数据集上训练新添加的层。
  • 最后,一个可选的步骤是微调,它包括解冻你上面得到的整个模型(或其部分),并使用非常小的学习率在新数据上重新训练它。这可以通过逐步调整预训练特征以适应新数据来实现有意义的改进。

1.3 本文的主要内容

首先,我们将详细介绍Keras的可训练API,它是大多数迁移学习和微调工作流程的基础。

然后,我们将通过在一个预训练于ImageNet数据集的模型上,并在Kaggle的“猫狗分类”数据集上重新训练它来展示典型的工作流程。

1.4 可训练属性与不可训练属性

层和模型具有三个权重属性:

  • weights 是层中所有权重变量的列表。
  • trainable_weights 是那些意图在训练过程中通过梯度下降来更新以最小化损失的权重列表。
  • non_trainable_weights 是那些不打算训练的权重列表。通常,它们在模型的前向传播过程中被模型更新。

Dense 层是深度学习模型中的一个常见层,用于实现全连接层(也称为密集层或线性层)。它包含两个主要的可训练权重:

  • kernel:这是一个二维数组(或称为矩阵),用于存储该层中神经元的权重。这些权重在训练过程中通过反向传播和梯度下降来更新,以最小化损失函数。
  • bias:这是一个一维数组,用于存储每个神经元的偏置项。同样,这些偏置项也在训练过程中通过梯度下降来更新。

示例:Dense 层有两个可训练的权重(kernel(核)和 bias(偏置))。

layer = keras.layers.Dense(3)
layer.build((None, 4))  # Create the weights

print("weights:", len(layer.weights))
print("trainable_weights:", len(layer.trainable_weights))
print("non_trainable_weights:", len(layer.non_trainable_weights))
weights: 2
trainable_weights: 2
non_trainable_weights: 0

在深度学习中,大多数层的权重默认都是可训练的(trainable),这意味着在训练过程中,这些权重会通过梯度下降等优化算法进行更新,以最小化损失函数。然而,也有一些特殊的层,如BatchNormalization层,它包含非训练权重(non-trainable weights)。

BatchNormalization层在训练过程中执行两个主要操作:标准化(normalization)和缩放/平移(scaling/shifting)。标准化是通过对输入数据进行变换,使其具有零均值和单位方差来实现的。这个变换的参数(即均值和方差)是在每个训练批次上计算的,并用于标准化当前批次的输入。这些均值和方差在训练过程中是不断更新的,但它们不是通过梯度下降来优化的,因此它们被视为非训练权重。

另一方面,BatchNormalization层还有另外两个可训练权重:缩放因子(gamma)和平移项(beta)。这些权重在训练过程中通过梯度下降进行更新,用于控制标准化后的数据应该如何被缩放和平移。这些权重对于调整模型的输出分布非常重要,可以帮助模型更好地拟合训练数据。

因此,BatchNormalization层通常包含两个可训练权重(gamma和beta)和两个非训练权重(输入数据的均值和方差)。

如果你想在自己的自定义层中使用非训练权重,你可以通过在定义层时创建tf.Variable对象并设置其trainable属性为False来实现。这样,这些权重就不会在训练过程中被优化算法更新。但是,请注意,在大多数情况下,你不需要直接使用非训练权重,除非你的层具有类似于BatchNormalization层的特殊需求。

layer = keras.layers.BatchNormalization()
layer.build((None, 4))  # Create the weights

print("weights:", len(layer.weights))
print("trainable_weights:", len(layer.trainable_weights))
print("non_trainable_weights:", len(layer.non_trainable_weights))
weights: 4
trainable_weights: 2
non_trainable_weights: 2

可训练属性设置为不可训练
将trainable设置为False

layer = keras.layers.Dense(3)
layer.build((None, 4))  # Create the weights
layer.trainable = False  # Freeze the layer

print("weights:", len(layer.weights))
print("trainable_weights:", len(layer.trainable_weights))
print("non_trainable_weights:", len(layer.non_trainable_weights))
weights: 2
trainable_weights: 0
non_trainable_weights: 2

不可训练属性的更新
当一个可训练权重变为非训练权重时,其值在训练过程中将不再被更新。

# Make a model with 2 layers
layer1 = keras.layers.Dense(3, activation="relu")
layer2 = keras.layers.Dense(3, activation="sigmoid")
model = keras.Sequential([keras.Input(shape=(3,)), layer1, layer2])

# Freeze the first layer
layer1.trainable = False

# Keep a copy of the weights of layer1 for later reference
initial_layer1_weights_values = layer1.get_weights()

# Train the model
model.compile(optimizer="adam", loss="mse")
model.fit(np.random.random((2, 3)), np.random.random((2, 3)))

# Check that the weights of layer1 have not changed during training
final_layer1_weights_values = layer1.get_weights()
np.testing.assert_allclose(
    initial_layer1_weights_values[0], final_layer1_weights_values[0]
)
np.testing.assert_allclose(
    initial_layer1_weights_values[1], final_layer1_weights_values[1]
1/1 ━━━━━━━━━━━━━━━━━━━━ 1s 766ms/step - loss: 0.0615

递归设置可训练属性
如果你在一个模型或任何具有子层的层上设置trainable = False,那么所有的子层也将变得不可训练。

inner_model = keras.Sequential(
    [
        keras.Input(shape=(3,)),
        keras.layers.Dense(3, activation="relu"),
        keras.layers.Dense(3, activation="relu"),
    ]
)

model = keras.Sequential(
    [
        keras.Input(shape=(3,)),
        inner_model,
        keras.layers.Dense(3, activation="sigmoid"),
    ]
)

model.trainable = False  # Freeze the outer model

assert inner_model.trainable == False  # All layers in `model` are now frozen
assert inner_model.layers[0].trainable == False  # `trainable` is propagated recursively

3、典型的迁移学习工作流程

本节我们将讨论如何在Keras中实现典型的迁移学习工作流程:

  • 实例化一个基础模型,并将预训练的权重加载到该模型中。
  • 通过设置trainable = False来冻结基础模型中的所有层。
  • 在基础模型的一个(或几个)层的输出之上创建一个新模型。
  • 在你的新数据集上训练你的新模型。

注意,还有一个更轻量级的替代工作流程可能是:

  • 实例化一个基础模型,并将预训练的权重加载到该模型中。
  • 将你的新数据集输入到基础模型中,并记录一个(或几个)层的输出。这被称为特征提取。
  • 将这些输出作为新的小型模型的输入数据。

第二种工作流程的一个关键优势是,只需要在数据集上运行一次基础模型,而不是在训练的每个周期中都运行。因此,它更快也更便宜。

然而,第二种工作流程的一个问题是,它不允许程序员在训练过程中动态修改新模型的输入数据,但这在执行数据增强时是必需的。迁移学习通常用于当程序员的新数据集数据量太小,无法从头开始训练一个全尺寸模型的任务,而在这种场景下,数据增强非常重要。因此,接下来我们将重点关注第一种工作流程。
设置

import numpy as np
import keras
from keras import layers
import tensorflow_datasets as tfds
import matplotlib.pyplot as plt

基础模型的实例化

首先,使用预训练的权重实例化一个基础模型。

base_model = keras.applications.Xception(
    weights='imagenet',  # Load weights pre-trained on ImageNet.
    input_shape=(150, 150, 3),
    include_top=False)  # Do not include the ImageNet classifier at the top.

冻结基础模型

base_model.trainable = False

建立新的模型

inputs = keras.Input(shape=(150, 150, 3))
# We make sure that the base_model is running in inference mode here,
# by passing `training=False`. This is important for fine-tuning, as you will
# learn in a few paragraphs.
x = base_model(inputs, training=False)
# Convert features of shape `base_model.output_shape[1:]` to vectors
x = keras.layers.GlobalAveragePooling2D()(x)
# A Dense classifier with a single unit (binary classification)
outputs = keras.layers.Dense(1)(x)
model = keras.Model(inputs, outputs)

训练模型
使用数据集对新建立的模型进行训练

model.compile(optimizer=keras.optimizers.Adam(),
              loss=keras.losses.BinaryCrossentropy(from_logits=True),
              metrics=[keras.metrics.BinaryAccuracy()])
model.fit(new_dataset, epochs=20, callbacks=..., validation_data=...)

4、微调(Fine-tuning)

如果新建立的模型在新数据上收敛,程序员就可以尝试解冻基础模型的全部或部分层,并使用非常低的学习率对整个模型进行端到端的重新训练。

这是一个可选的最后步骤,可能会给程序员带来一些额外的改进。但这也可能导致快速过拟合。

重要的是,只有在模型带有冻结的层已经训练到收敛之后,才执行这一步。如果程序员将随机初始化的可训练层与包含预训练特征的可训练层混合在一起,那么随机初始化的层在训练过程中会产生非常大的梯度更新,这会破坏预训练特征。

同样重要的是,在这个阶段使用非常低的学习率,因为此时程序员通常正在一个通常非常小的数据集上训练一个比第一轮训练时大得多的模型。因此,如果应用大的权重更新,将会很快面临过拟合的风险。在这里,程序员只想以增量的方式重新调整预训练的权重。

以下是实现整个基础模型微调的方法:

  • 加载已训练到收敛的带有冻结层的基础模型。
  • 解冻你想要微调的基础模型的部分或全部层,设置这些层的trainable属性为True
  • 编译模型,选择一个适合微调的学习率(通常比初始训练时小得多)。
  • 在你的新数据集上重新训练模型,直到再次收敛或达到你的目标性能。

通过这种方法,程序员可以利用预训练的模型权重,同时调整这些权重以适应程序员的特定任务,从而可能获得更好的性能。但是,请注意监控过拟合的迹象,并考虑使用如早停(early stopping)、正则化(regularization)等技术来防止过拟合。

# Unfreeze the base model
base_model.trainable = True

# It's important to recompile your model after you make any changes
# to the `trainable` attribute of any inner layer, so that your changes
# are take into account
model.compile(optimizer=keras.optimizers.Adam(1e-5),  # Very low learning rate
              loss=keras.losses.BinaryCrossentropy(from_logits=True),
              metrics=[keras.metrics.BinaryAccuracy()])

# Train end-to-end. Be careful to stop before you overfit!
model.fit(new_dataset, epochs=10, callbacks=..., validation_data=...)

4.1 关于compile()和trainable的重要说明

在模型上调用compile()方法意味着“冻结”该模型的行为。这意味着在模型编译时设置的trainable属性值应该在该模型的整个生命周期中被保留,直到再次调用compile()。因此,如果程序员改变了任何trainable值,请确保再次在模型上调用compile()以使更改生效。

4.2 关于BatchNormalization层的重要说明

许多图像模型都包含BatchNormalization层。这个层在可想象的每个方面都是一个特殊情况。以下是需要注意的几点:

  • BatchNormalization包含两个在训练过程中更新的非可训练权重。这些变量跟踪输入的均值和方差。

  • 当设置bn_layer.trainable = False时,BatchNormalization层将在推理模式下运行,并且不会更新其均值和方差统计信息。这通常不是其他层的情况,因为权重可训练性和推理/训练模式是两个正交的概念。但在BatchNormalization层的情况下,两者是相关的。

  • 当程序员解冻包含BatchNormalization层的模型以进行微调时,应该在调用基础模型时传递training=False来保持BatchNormalization层在推理模式下运行。否则,对非可训练权重的更新会突然破坏模型所学到的东西。

在实际操作中,当程序员从冻结的模型过渡到微调阶段时,你通常会保持基础模型的BatchNormalization层在推理模式下(即training=False),同时只让新添加的层的权重可训练。这有助于保持预训练特征的稳定性,同时允许程序员微调新添加的层以更好地适应新任务。

5 迁移学习的示例

为了加深对前面讨论概念的理解,本节通过一个具体的迁移学习和微调操作的示例来展示相应的知识点。我们将加载在ImageNet上预训练的Xception模型,并将其应用于Kaggle的“猫狗”分类数据集。

5.1 获取数据

首先,让我们使用TFDS(TensorFlow数据集)来获取猫狗数据集。如果你有自己的数据集,你可能会想使用keras.utils.image_dataset_from_directory这个工具来从磁盘上按类别文件夹组织的图像集中生成类似的带标签数据集对象。

迁移学习在处理非常小的数据集时最为有用。为了保持数据集的小规模,我们将使用原始训练数据的40%(25,000张图片)进行训练,10%用于验证,10%用于测试。

tfds.disable_progress_bar()

train_ds, validation_ds, test_ds = tfds.load(
    "cats_vs_dogs",
    # Reserve 10% for validation and 10% for test
    split=["train[:40%]", "train[40%:50%]", "train[50%:60%]"],
    as_supervised=True,  # Include labels
)

print(f"Number of training samples: {train_ds.cardinality()}")
print(f"Number of validation samples: {validation_ds.cardinality()}")
print(f"Number of test samples: {test_ds.cardinality()}")

5.2 标准化数据

我们的原始图像具有各种大小。此外,每个像素由0到255之间的3个整数值(RGB级别值)组成。这不太适合直接输入到神经网络中。我们需要做两件事:

  1. 将图像标准化为固定大小。我们选择150x150。
  2. 将像素值归一化到-1和1之间。我们将使用Normalization层作为模型本身的一部分来实现这一点。

通常,开发以原始数据作为输入的模型是一个好习惯,而不是以已经预处理过的数据作为输入的模型。原因是,如果你的模型期望预处理过的数据,那么每次你将模型导出到其他地方使用(如在浏览器中、在移动应用中)时,你都需要重新实现完全相同的预处理流程。这很快就会变得非常棘手。因此,我们应该在模型处理之前尽可能少地进行预处理。

在这里,我们将在数据管道中调整图像大小(因为深度神经网络只能处理连续的数据批次),并且当我们创建模型时,我们将输入值缩放作为模型的一部分来实现。

让我们将图像大小调整为150x150:

resize_fn = keras.layers.Resizing(150, 150)

train_ds = train_ds.map(lambda x, y: (resize_fn(x), y))
validation_ds = validation_ds.map(lambda x, y: (resize_fn(x), y))
test_ds = test_ds.map(lambda x, y: (resize_fn(x), y))

5.3 使用随机数据增强

当你没有大型图像数据集时,一个很好的做法是对训练图像应用随机但现实的变换来人为地引入样本多样性,例如随机水平翻转或小随机旋转。这有助于让模型接触到训练数据的不同方面,同时减缓过拟合。

augmentation_layers = [
    layers.RandomFlip("horizontal"),
    layers.RandomRotation(0.1),
]


def data_augmentation(x):
    for layer in augmentation_layers:
        x = layer(x)
    return x


train_ds = train_ds.map(lambda x, y: (data_augmentation(x), y))

对数据进行批处理并使用预取功能来优化加载速度。

from tensorflow import data as tf_data

batch_size = 64

train_ds = train_ds.batch(batch_size).prefetch(tf_data.AUTOTUNE).cache()
validation_ds = validation_ds.batch(batch_size).prefetch(tf_data.AUTOTUNE).cache()
test_ds = test_ds.batch(batch_size).prefetch(tf_data.AUTOTUNE).cache()

5.4 构建模型

按照之前解释的蓝图来构建一个模型:

  • 我们添加一个重新缩放层(Rescaling layer),将输入值(最初在[0, 255]范围内)缩放到[-1, 1]范围。
  • 我们在分类层之前添加一个Dropout层,用于正则化。
  • 当我们调用基础模型时,确保传递training=False,以便它在推理模式下运行,这样即使在微调时解冻基础模型,批归一化(batch normalization)的统计信息也不会被更新。
base_model = keras.applications.Xception(
    weights="imagenet",  # Load weights pre-trained on ImageNet.
    input_shape=(150, 150, 3),
    include_top=False,
)  # Do not include the ImageNet classifier at the top.

# Freeze the base_model
base_model.trainable = False

# Create new model on top
inputs = keras.Input(shape=(150, 150, 3))

# Pre-trained Xception weights requires that input be scaled
# from (0, 255) to a range of (-1., +1.), the rescaling layer
# outputs: `(inputs * scale) + offset`
scale_layer = keras.layers.Rescaling(scale=1 / 127.5, offset=-1)
x = scale_layer(inputs)

# The base model contains batchnorm layers. We want to keep them in inference mode
# when we unfreeze the base model for fine-tuning, so we make sure that the
# base_model is running in inference mode here.
x = base_model(x, training=False)
x = keras.layers.GlobalAveragePooling2D()(x)
x = keras.layers.Dropout(0.2)(x)  # Regularize with dropout
outputs = keras.layers.Dense(1)(x)
model = keras.Model(inputs, outputs)

model.summary(show_trainable=True)

5.5 训练顶层模型

model.compile(
    optimizer=keras.optimizers.Adam(),
    loss=keras.losses.BinaryCrossentropy(from_logits=True),
    metrics=[keras.metrics.BinaryAccuracy()],
)

epochs = 2
print("Fitting the top layer of the model")
model.fit(train_ds, epochs=epochs, validation_data=validation_ds)

5.6 模型微调

最后,解冻基础模型并使用较低的学习率对整个模型进行端到端的微调。

重要的是,尽管基础模型变得可训练,但由于我们在构建模型时调用它时传递了training=False,因此它仍然处于推理模式。这意味着其中的批归一化层不会更新其批统计信息。如果它们更新了,那么它们会破坏模型到目前为止学习的特征表示。

# Unfreeze the base_model. Note that it keeps running in inference mode
# since we passed `training=False` when calling it. This means that
# the batchnorm layers will not update their batch statistics.
# This prevents the batchnorm layers from undoing all the training
# we've done so far.
base_model.trainable = True
model.summary(show_trainable=True)

model.compile(
    optimizer=keras.optimizers.Adam(1e-5),  # Low learning rate
    loss=keras.losses.BinaryCrossentropy(from_logits=True),
    metrics=[keras.metrics.BinaryAccuracy()],
)

epochs = 1
print("Fitting the end-to-end model")
model.fit(train_ds, epochs=epochs, validation_data=validation_ds)

6、总结

迁移学习和微调(也称为精细调整)是深度学习中两种强大的技术,它们允许模型在特定任务上快速有效地学习,特别是当目标数据集相对较小或标注数据稀缺时。以下是关于迁移学习和微调的总结:

迁移学习

迁移学习是一种机器学习方法,其中在一个任务上学到的知识被用来改进另一个不同但相关的任务上的学习。在深度学习中,这通常意味着使用在一个大型数据集上预训练的模型(如ImageNet上的模型)作为起点,然后将这些学到的特征或模型权重迁移到一个新的、较小或特定的数据集上。这种方法的关键在于,预训练模型已经学会了识别图像中的通用特征,这些特征在新任务上也可能是有用的。

微调

微调是迁移学习中的一种特殊技术,其中预训练模型的部分或全部层被解冻(变得可训练),然后使用新数据集上的数据重新训练这些层。在微调过程中,通常会使用比预训练时更低的学习率,以防止破坏预训练模型中学到的有用特征。微调允许模型学习针对新任务进行特定优化的特征,从而在新数据集上获得更好的性能。

迁移学习和微调是深度学习实践中广泛使用的技术,特别是在处理标注数据稀缺或计算资源有限的情况时。通过利用预训练模型中的通用特征,迁移学习能够显著加速模型在新任务上的训练过程。而微调则进一步允许模型针对新任务进行特定优化,从而提高其在新数据集上的性能。这两种技术的结合使用,使得深度学习模型能够更有效地处理各种复杂任务,并在各种应用场景中取得优异的结果。

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MUKAMO

你的鼓励是我们创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值