赞
踩
import numpy as np
import tensorflow as tf
from tensorflow import keras
迁移学习是在一个特定的问题上学到的特征,并在新的类似问题上加以利用。例如,来自已学会识别浣熊的模型的特征可能对启动旨在识别狸小动物的模型很有用。
迁移学习通常是针对数据集数据太少而无法从头训练完整模型的任务完成的。
在深度学习中,迁移学习最常见的体现是以下问题:
首先,我们将详细介绍Keras可训练的API,它是大多数迁移学习和微调工作流的基础。
然后,我们将通过在ImageNet数据集上进行预训练的模型,然后在Kaggle“猫与狗”分类数据集上对其进行重新训练,来演示典型的工作流程。
改编自Python深度学习和2016年博客文章“使用很少的数据构建强大的图像分类模型”。
图层和模型具有三个权重属性:
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
通常,所有权重(weights) 都是可训练的权重。 唯一具有不可调整权重的内置层是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
图层和模型还具有可训练的布尔属性。 其值可以更改。 将layer.trainable设置为False会将图层的所有权重从可训练变为不可训练。 这称为“冻结”层:冻结层的状态在训练期间不会更新(无论是使用fit()进行训练,还是使用依赖于trainable_weights来应用梯度更新的任何自定义循环进行训练)。
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 [==============================] - 0s 1ms/step - loss: 0.0846
不要将layer.trainable属性与layer .__ call __()中的训练变量混淆(该参数控制该层是应该以推理模式还是训练模式运行其前向传递)。 有关更多信息,请参见Keras FAQ。
如果你在模型或具有子图层的任何图层上设置rainable = 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中实现典型的转学学习工作流程:
但是,第二个工作流程的一个问题是,它不允许您在训练期间动态修改新模型的输入数据,例如在进行数据扩充时,这是必需的。当新数据集的数据太少而无法从头开始训练完整模型时,转移学习通常用于任务,在这种情况下,数据扩充非常重要。因此,在接下来的内容中,我们将专注于第一个工作流程。
这是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=...)
一旦您的模型收敛于新数据,您就可以尝试解冻全部或部分基本模型,并以非常低的学习率端到端重新训练整个模型。
这是最后一个可选步骤,可以潜在地为您提供增量改进。它还可能会导致快速过拟合-请记住这一点。
至关重要的是只有在训练具有冻结层的模型以使其收敛之后才执行此步骤。如果将随机初始化的可训练图层与包含预训练要素的可训练图层混合使用,则随机初始化的图层将在训练过程中引起非常大的渐变更新,这将破坏您的预训练要素。
在此阶段使用非常低的学习率也很关键,因为在一个通常很小的数据集上,您要训练的模型比第一轮训练中的模型大得多。因此,如果您应用较大的重量更新,则可能会很快过度拟合。在这里,您只想以增量方式重新调整预训练的权重。
这是实现整个基本模型的微调的方法:
# 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()旨在“冻结”该模型的行为。这意味着在编译模型时,应该在该模型的整个生命周期中保留可训练的属性值,直到再次调用compile为止。因此,如果您更改了任何可训练的值,请确保再次在模型上调用compile(),以将更改考虑在内。
许多图像模型都包含BatchNormalization图层。在每一个可以想象的数量上,该层都是一个特例。这里有几件事要牢记。
如果您使用的是自己的低级训练循环而不是fit(),则工作流程基本上保持不变。 在应用渐变更新时,您应注意仅考虑列表model.trainable_weights:
# Create base model base_model = keras.applications.Xception( weights='imagenet', input_shape=(150, 150, 3), include_top=False) # Freeze base model base_model.trainable = False # Create new model on top. inputs = keras.Input(shape=(150, 150, 3)) x = base_model(inputs, training=False) x = keras.layers.GlobalAveragePooling2D()(x) outputs = keras.layers.Dense(1)(x) model = keras.Model(inputs, outputs) loss_fn = keras.losses.BinaryCrossentropy(from_logits=True) optimizer = keras.optimizers.Adam() # Iterate over the batches of a dataset. for inputs, targets in new_dataset: # Open a GradientTape. with tf.GradientTape() as tape: # Forward pass. predictions = model(inputs) # Compute the loss value for this batch. loss_value = loss_fn(targets, predictions) # Get gradients of loss wrt the *trainable* weights. gradients = tape.gradient(loss_value, model.trainable_weights) # Update the weights of the model. optimizer.apply_gradients(zip(gradients, model.trainable_weights))
同样用于微调。
资料集
为了巩固这些概念,让我们为您介绍一个具体的端到端转移学习和微调示例。 我们将加载在ImageNet上预先训练的Xception模型,并将其用于Kaggle“猫与狗”分类数据集中。
首先,让我们使用TFDS来获取猫狗数据集。 如果您拥有自己的数据集,则可能需要使用实用程序tf.keras.preprocessing.image_dataset_from_directory从磁盘上提交到特定于类的文件夹中的一组图像中生成相似的标签数据集对象。
当处理非常小的数据时,Tansfer学习最为有用。 为了使数据集保持较小,我们将使用原始训练数据的40%(25,000张图像)进行训练,将10%用于验证,将10%用于测试。
import tensorflow_datasets as tfds 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("Number of training samples: %d" % tf.data.experimental.cardinality(train_ds)) print( "Number of validation samples: %d" % tf.data.experimental.cardinality(validation_ds) ) print("Number of test samples: %d" % tf.data.experimental.cardinality(test_ds))
Number of training samples: 9305
Number of validation samples: 2326
Number of test samples: 2326
这些是训练数据集中的前9张图像-如您所见,它们都是不同的大小。
import matplotlib.pyplot as plt
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”。
我们的原始图像有各种尺寸。另外,每个像素由0到255之间的3个整数值(RGB级别值)组成。这不适合提供神经网络。我们需要做两件事:
在这里,我们将在数据管道中进行图像大小调整(因为深度神经网络只能处理连续的数据批次),并且在创建模型时将其作为模型的一部分进行输入值缩放。
让我们将图像调整为150x150:
size = (150, 150)
train_ds = train_ds.map(lambda x, y: (tf.image.resize(x, size), y))
validation_ds = validation_ds.map(lambda x, y: (tf.image.resize(x, size), y))
test_ds = test_ds.map(lambda x, y: (tf.image.resize(x, size), y))
此外,让我们分批处理数据并使用缓存和预取来优化加载速度。
batch_size = 32
train_ds = train_ds.cache().batch(batch_size).prefetch(buffer_size=10)
validation_ds = validation_ds.cache().batch(batch_size).prefetch(buffer_size=10)
test_ds = test_ds.cache().batch(batch_size).prefetch(buffer_size=10)
当您没有大型图像数据集时,通过对训练图像进行随机但逼真的变换(例如随机水平翻转或小的随机旋转)来人为引入样本多样性是一种很好的做法。 这有助于使模型暴露于训练数据的不同方面,同时减慢过度拟合的速度。
from tensorflow import keras
from tensorflow.keras import layers
data_augmentation = keras.Sequential(
[
layers.experimental.preprocessing.RandomFlip("horizontal"),
layers.experimental.preprocessing.RandomRotation(0.1),
]
)
让我们直观地看到经过各种随机转换后的第一批图像:
import numpy as np
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(
tf.expand_dims(first_image, 0), training=True
)
plt.imshow(augmented_image[0].numpy().astype("int32"))
plt.title(int(labels[i]))
plt.axis("off")
现在让我们建立一个遵循我们先前解释的蓝图的模型。
注意:
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)) x = data_augmentation(inputs) # Apply random data augmentation # Pre-trained Xception weights requires that input be normalized # from (0, 255) to a range (-1., +1.), the normalization layer # does the following, outputs = (inputs - mean) / sqrt(var) norm_layer = keras.layers.experimental.preprocessing.Normalization() mean = np.array([127.5] * 3) var = mean ** 2 # Scale inputs to [-1, +1] x = norm_layer(x) norm_layer.set_weights([mean, var]) # 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()
model.compile(
optimizer=keras.optimizers.Adam(),
loss=keras.losses.BinaryCrossentropy(from_logits=True),
metrics=[keras.metrics.BinaryAccuracy()],
)
epochs = 20
model.fit(train_ds, epochs=epochs, validation_data=validation_ds)
最后,让我们解冻基本模型并以较低的学习率端到端地训练整个模型。
重要的是,尽管基本模型变得可训练,但由于我们在构建模型时调用该模型时传递了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() model.compile( optimizer=keras.optimizers.Adam(1e-5), # Low learning rate loss=keras.losses.BinaryCrossentropy(from_logits=True), metrics=[keras.metrics.BinaryAccuracy()], ) epochs = 10 model.fit(train_ds, epochs=epochs, validation_data=validation_ds)
经过10个epoch后,微调在这里取得了很大的进步。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。