《Keras 3 :开发人员指南 / 迁移学习和微调》
迁移学习和微调
作者:fchollet
创建日期:2020/04/15
最后修改时间:2023/06/25
描述:Keras迁移学习和微调的完整指南。
在 Colab 中查看
GitHub 源
设置
import numpy as np import keras from keras import layers import tensorflow_datasets as tfds import matplotlib.pyplot as plt
介绍
迁移学习包括利用在一个问题上学到的特征,以及 利用它们来解决一个新的、类似的问题。例如,来自具有 学会识别浣熊可能有助于启动旨在识别的模型 狸猫。
迁移学习通常用于数据集数据太少而无法完成的任务 从头开始训练全尺寸模型。
在深度学习的背景下,迁移学习最常见的化身是 以下工作流程:
- 从以前训练的模型中提取层。
- 冻结它们,以避免在 未来的训练轮次。
- 在冻结的层之上添加一些新的可训练层。他们将学会转弯 旧特征转化为新数据集上的预测。
- 在数据集上训练新层。
最后一个可选步骤是微调,包括解冻整个 模型,并使用 非常低的学习率。这有可能实现有意义的改进,方法是 逐步使预训练特征适应新数据。
首先,我们将详细介绍 Keras API,它是大多数基础 迁移学习和微调工作流程。trainable
然后,我们将通过在 ImageNet 数据集,并根据 Kaggle “cats vs dogs” 分类对其进行重新训练 数据。
这改编自使用 Python 进行深度学习和 2016 年的博客文章“使用非常少的数据构建强大的图像分类模型”。
冻结图层:了解属性trainable
层和模型有三个重量属性:
weights
是层的所有权重变量的列表。trainable_weights
是要更新的 (通过 Gradient descent) 以最大限度地减少训练期间的损失。non_trainable_weights
是那些不打算接受训练的人的列表。 通常,它们在前向传递期间由模型更新。
示例:Dense
层有 2 个可训练的权重(内核和偏差)
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
一般来说,所有权重都是可训练的权重。唯一具有 Non-trainable weights 是 layer。它使用不可训练的权重 在训练期间跟踪其输入的均值和方差。 要了解如何在您自己的自定义层中使用不可训练的权重,请参阅从头开始编写新层的指南。BatchNormalization
示例:BatchNormalization
层有 2 个可训练的权重和 2 个不可训练的权重 权重
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 移动到 不可训练。这称为 “冻结” 层:冻结层的状态不会 在训练期间进行更新(与 一起训练时或使用 任何依赖于应用渐变更新的自定义循环)。trainable
layer.trainable
False
fit()
trainable_weights
示例:将 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
不要将属性与参数 in 混淆(该参数控制层是否应运行其前向传递 inference mode 或 training mode) 的 Pod Pod 的 Quin S T有关更多信息,请参阅 Keras 常见问题解答。layer.trainable
training
layer.__call__()
属性的递归设置trainable
如果您在模型或任何具有子层的层上设置, 所有 Children 图层也变得不可训练。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
典型的迁移学习工作流程
这让我们想到了如何在 Keras 中实现典型的迁移学习工作流程:
- 实例化基础模型并将预训练的权重加载到其中。
- 通过设置 冻结基础模型中的所有图层。
trainable = False
- 在基础的一层(或几层)输出之上创建新模型 型。
- 在新数据集上训练新模型。
请注意,另一种更轻量级的工作流也可以是:
- 实例化基础模型并将预训练的权重加载到其中。
- 通过它运行新数据集并记录一个(或多个)图层的输出 从基本模型。这称为特征提取。
- 将该输出用作新的较小模型的输入数据。
第二个工作流的一个关键优势是,您只需在 您的数据,而不是每个训练纪元一次。所以它更快也更便宜。
但是,第二个工作流的一个问题是,它不允许您动态地 在训练期间修改新模型的输入数据,这在执行 例如,数据增强。迁移学习通常用于 您的新数据集数据太少,无法从头开始训练全尺寸模型,并且在 此类场景 数据增强非常重要。因此,在接下来的内容中,我们将重点关注 在第一个工作流上。
以下是 Keras 中的第一个工作流:
首先,使用预先训练的权重实例化基础模型。
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=...)
微调
一旦您的模型收敛到新数据上,您可以尝试解冻全部或部分 base model 的 base 模型,并以非常低的学习率端到端地重新训练整个模型。
这是可选的最后一步,可能会为您提供增量改进。 它还可能导致快速过拟合 - 请记住这一点。
只有在具有冻结层的模型完成后才执行此步骤至关重要 训练收敛。如果您将随机初始化的可训练层与 包含预训练特征的可训练层,随机初始化的层将 在训练过程中导致非常大的梯度更新,这将破坏您的预训练 特征。
在这个阶段使用非常低的学习率也很重要,因为 您正在 dataset 上训练的模型比第一轮训练要大得多 这通常非常小。 因此,如果您施加较大的权重,您很快就会面临过度拟合的风险 更新。在这里,您只想以增量方式重新调整预训练的权重。
这是实现整个 base model 的微调的方法:
# 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=...)
关于 compile()
和 trainable
的重要说明
调用 model 是为了 “冻结” 该 model 的行为。这 意味着编译模型时的属性值应在整个 该模型的生命周期, until 再次调用。因此,如果您更改任何值,请确保 再次调用 model 来考虑您的更改。compile()
trainable
compile
trainable
compile()
关于 BatchNormalization
层的重要说明
许多图像模型都包含图层。该层是 上的 特例 每一个可以想象到的计数。以下是一些需要记住的事项。BatchNormalization
BatchNormalization
包含 2 个不可训练的权重,这些权重在 训练。这些是跟踪输入的均值和方差的变量。- 设置 时,图层将 在推理模式下运行,并且不会更新其均值和方差统计数据。这不是 其他层的情况通常,因为体重训练性和推理/训练模式是两个正交的概念。 但在层的情况下,两者是并列的。
bn_layer.trainable = False
BatchNormalization
BatchNormalization
- 当您解冻包含层的模型时,为了执行 微调,您应该通过以下方式将层保持在推理模式 在调用基本模型时传递。 否则,应用于不可训练权重的更新将突然销毁 模型学到了什么。
BatchNormalization
BatchNormalization
training=False
您将在本指南末尾的端到端示例中看到此模式的实际效果。
端到端示例:在 cats vs. dogs 数据集上微调图像分类模型
为了巩固这些概念,让我们引导您完成具体的端到端传输 学习和微调示例。我们将加载 Xception 模型,在 ImageNet 的 Git 示例,并将其用于 Kaggle “cats vs. dogs” 分类数据集。
获取数据
首先,让我们使用 TFDS 获取猫与狗的数据集。如果您有自己的数据集, 您可能希望使用实用程序 keras.utils.image_dataset_from_directory 来生成类似的 Labeled 数据集对象,这些对象来自磁盘上归档到特定于类的文件夹中的一组图像。
迁移学习在处理非常小的数据集时最有用。为了保持我们的 数据集较小,我们将使用 40% 的原始训练数据(25000 张图像)进行 培训,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()}")
Downloading and preparing dataset 786.68 MiB (download: 786.68 MiB, generated: Unknown size, total: 786.68 MiB) to /home/mattdangerw/tensorflow_datasets/cats_vs_dogs/4.0.0... WARNING:absl:1738 images were corrupted and were skipped Dataset cats_vs_dogs downloaded and prepared to /home/mattdangerw/tensorflow_datasets/cats_vs_dogs/4.0.0. Subsequent calls will reuse this data. Number of training samples: 9305 Number of validation samples: 2326 Number of test samples: 2326
这些是训练数据集中的前 9 张图像 – 如您所见,它们都是 不同的尺寸。
plt.figure(figsize=(10, 10)) for i, (image, label) in enumerate(train_ds.take(9)): ax = plt.subplot(3, 3, i + 1) plt.imshow(image) plt.title(int(label)) plt.axis("off")
我们还可以看到,标签 1 是 “dog” ,标签 0 是 “cat”。
标准化数据
我们的 Raw 图像有多种尺寸。此外,每个像素由 3 个整数组成 介于 0 和 255 之间的值(RGB 级别值)。这不太适合喂养 神经网络。我们需要做两件事:
- 标准化为固定的图像大小。我们选择 150x150。
- 标准化 -1 和 1 之间的像素值。我们将使用一个层来执行此作,例如 模型本身的一部分。
Normalization
一般来说,最好开发将原始数据作为输入的模型,例如 与采用已预处理数据的模型相反。原因是,如果您的 model 需要预处理的数据,只要您导出模型以在其他地方使用它 (在 Web 浏览器、移动应用程序中),您需要重新实现完全相同的作 预处理管道。这很快就会变得非常棘手。所以我们应该最少地做 在到达模型之前可能的预处理量。
在这里,我们将在数据管道中调整图像大小(因为深度神经网络可以 仅处理连续批次的数据),我们将作为 input value 缩放的一部分进行 当我们创建模型时。
让我们将图像大小调整为 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))
使用随机数据增强
当您没有大型图像数据集时,最好人工进行 通过应用随机但真实的变换来引入样本多样性 训练图像,例如随机水平翻转或小的随机旋转。这 有助于在减慢速度的同时将模型暴露给训练数据的不同方面 过拟合。
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()
让我们想象一下,在各种随机之后,第一批的第一张图像是什么样子的 转换:
for images, labels in train_ds.take(1): plt.figure(figsize=(10, 10)) first_image = images[0] for i in range(9): ax = plt.subplot(3, 3, i + 1) augmented_image = data_augmentation(np.expand_dims(first_image, 0)) plt.imshow(np.array(augmented_image[0]).astype("int32")) plt.title(int(labels[0])) plt.axis("off")
构建模型
现在,让我们构建一个遵循我们之前解释的蓝图的模型。
请注意:
- 我们添加一个层以将输入值(最初在范围内)缩放到该范围。
Rescaling
[0, 255]
[-1, 1]
- 我们在分类层之前添加一个层,用于正则化。
Dropout
- 我们确保在调用基本模型时传递,以便 它在推理模式下运行,因此 BatchNorm 统计数据不会更新 即使在我们解冻基本模型进行微调之后。
training=False
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)
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/xception/xception_weights_tf_dim_ordering_tf_kernels_notop.h5 83683744/83683744 ━━━━━━━━━━━━━━━━━━━━ 0s 0us/step
Model: "functional_4"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃ Trai… ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━┩
│ input_layer_4 (InputLayer) │ (None, 150, 150, 3) │ 0 │ - │
├─────────────────────────────┼──────────────────────────┼─────────┼───────┤
│ rescaling (Rescaling) │ (None, 150, 150, 3) │ 0 │ - │
├─────────────────────────────┼──────────────────────────┼─────────┼───────┤
│ xception (Functional) │ (None, 5, 5, 2048) │ 20,861… │ N │
├─────────────────────────────┼──────────────────────────┼─────────┼───────┤
│ global_average_pooling2d │ (None, 2048) │ 0 │ - │
│ (GlobalAveragePooling2D) │ │ │ │
├─────────────────────────────┼──────────────────────────┼─────────┼───────┤
│ dropout (Dropout) │ (None, 2048) │ 0 │ - │
├─────────────────────────────┼──────────────────────────┼─────────┼───────┤
│ dense_7 (Dense) │ (None, 1) │ 2,049 │ Y │
└─────────────────────────────┴──────────────────────────┴─────────┴───────┘
Total params: 20,863,529 (79.59 MB)
Trainable params: 2,049 (8.00 KB)
Non-trainable params: 20,861,480 (79.58 MB)
训练顶层
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)
Fitting the top layer of the model Epoch 1/2 78/146 ━━━━━━━━━━[37m━━━━━━━━━━ 15s 226ms/step - binary_accuracy: 0.7995 - loss: 0.4088 Corrupt JPEG data: 65 extraneous bytes before marker 0xd9 136/146 ━━━━━━━━━━━━━━━━━━[37m━━ 2s 231ms/step - binary_accuracy: 0.8430 - loss: 0.3298 Corrupt JPEG data: 239 extraneous bytes before marker 0xd9 143/146 ━━━━━━━━━━━━━━━━━━━[37m━ 0s 231ms/step - binary_accuracy: 0.8464 - loss: 0.3235 Corrupt JPEG data: 1153 extraneous bytes before marker 0xd9 144/146 ━━━━━━━━━━━━━━━━━━━[37m━ 0s 231ms/step - binary_accuracy: 0.8468 - loss: 0.3226 Corrupt JPEG data: 228 extraneous bytes before marker 0xd9 146/146 ━━━━━━━━━━━━━━━━━━━━ 0s 260ms/step - binary_accuracy: 0.8478 - loss: 0.3209 Corrupt JPEG data: 2226 extraneous bytes before marker 0xd9 146/146 ━━━━━━━━━━━━━━━━━━━━ 54s 317ms/step - binary_accuracy: 0.8482 - loss: 0.3200 - val_binary_accuracy: 0.9667 - val_loss: 0.0877 Epoch 2/2 146/146 ━━━━━━━━━━━━━━━━━━━━ 7s 51ms/step - binary_accuracy: 0.9483 - loss: 0.1232 - val_binary_accuracy: 0.9705 - val_loss: 0.0786 <keras.src.callbacks.history.History at 0x7fc8b7f1db70>
对整个模型进行一轮微调
最后,让我们解冻基础模型,并使用低 学习率。
重要的是,尽管基础模型变得可训练,但它仍在 推理模式,因为我们在构建 型。这意味着其中的批量归一化图层不会更新其批量 统计学。如果他们这样做了,他们将对 模型。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)
Model: "functional_4"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃ Trai… ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━┩
│ input_layer_4 (InputLayer) │ (None, 150, 150, 3) │ 0 │ - │
├─────────────────────────────┼──────────────────────────┼─────────┼───────┤
│ rescaling (Rescaling) │ (None, 150, 150, 3) │ 0 │ - │
├─────────────────────────────┼──────────────────────────┼─────────┼───────┤
│ xception (Functional) │ (None, 5, 5, 2048) │ 20,861… │ Y │
├─────────────────────────────┼──────────────────────────┼─────────┼───────┤
│ global_average_pooling2d │ (None, 2048) │ 0 │ - │
│ (GlobalAveragePooling2D) │ │ │ │
├─────────────────────────────┼──────────────────────────┼─────────┼───────┤
│ dropout (Dropout) │ (None, 2048) │ 0 │ - │
├─────────────────────────────┼──────────────────────────┼─────────┼───────┤
│ dense_7 (Dense) │ (None, 1) │ 2,049 │ Y │
└─────────────────────────────┴──────────────────────────┴─────────┴───────┘
Total params: 20,867,629 (79.60 MB)
Trainable params: 20,809,001 (79.38 MB)
Non-trainable params: 54,528 (213.00 KB)
Optimizer params: 4,100 (16.02 KB)
Fitting the end-to-end model 146/146 ━━━━━━━━━━━━━━━━━━━━ 75s 327ms/step - binary_accuracy: 0.8487 - loss: 0.3760 - val_binary_accuracy: 0.9494 - val_loss: 0.1160 <keras.src.callbacks.history.History at 0x7fcd1c755090>
在 10 个 epoch 之后,微调在这里为我们带来了不错的改进。 让我们在测试数据集上评估模型:
print("Test dataset evaluation") model.evaluate(test_ds)
Test dataset evaluation 11/37 ━━━━━[37m━━━━━━━━━━━━━━━ 1s 52ms/step - binary_accuracy: 0.9407 - loss: 0.1155 Corrupt JPEG data: 99 extraneous bytes before marker 0xd9 37/37 ━━━━━━━━━━━━━━━━━━━━ 2s 47ms/step - binary_accuracy: 0.9427 - loss: 0.1259 [0.13755160570144653, 0.941300630569458]