开发者指南 / 超参数调优 / 在自定义训练循环中调整超参数

在自定义训练循环中调整超参数

作者: Tom O'Malley, Haifeng Jin
创建日期: 2019/10/28
最后修改: 2022/01/12
描述: 使用 HyperModel.fit() 来调整训练超参数(例如批大小)。

在 Colab 中查看 GitHub 源代码

!pip install keras-tuner -q

介绍

KerasTuner 中的 HyperModel 类提供了一种方便的方式,将搜索空间定义为可重用的对象。您可以重写 HyperModel.build() 来定义和调整模型本身。要调整训练过程(例如通过选择合适的批大小、训练的轮次或数据增强设置),可以重写 HyperModel.fit(),在其中您可以访问:

一个基本示例显示在 KerasTuner 入门 的“调整模型训练”部分。


调整自定义训练循环

在本指南中,我们将子类化 HyperModel 类,并通过重写 HyperModel.fit() 来编写一个自定义训练循环。有关如何使用 Keras 编写自定义训练循环的信息,请参考指南 从头开始编写训练循环

首先,我们导入所需的库,并创建用于训练和验证的数据集。在这里,我们只是使用一些随机数据来演示。

import keras_tuner
import tensorflow as tf
import keras
import numpy as np


x_train = np.random.rand(1000, 28, 28, 1)
y_train = np.random.randint(0, 10, (1000, 1))
x_val = np.random.rand(1000, 28, 28, 1)
y_val = np.random.randint(0, 10, (1000, 1))

随后,我们将 HyperModel 类子类化为 MyHyperModel。在 MyHyperModel.build() 中,我们构建一个简单的 Keras 模型来进行 10 个不同类别的图像分类。MyHyperModel.fit() 接受多个参数。它的签名如下所示:

def fit(self, hp, model, x, y, validation_data, callbacks=None, **kwargs):
  • hp 参数用于定义超参数。
  • model 参数是由 MyHyperModel.build() 返回的模型。
  • xyvalidation_data 是所有自定义定义的参数。稍后我们将通过调用 tuner.search(x=x, y=y, validation_data=(x_val, y_val)) 将数据传递给它们。您可以定义任意数量的参数并为其指定自定义名称。
  • callbacks 参数旨在与 model.fit() 一起使用。KerasTuner 在其中放置了一些有用的 Keras 回调,例如在其最佳轮次对模型进行检查点的回调。

我们将在自定义训练循环中手动调用回调。在调用它们之前,我们需要用以下代码将模型分配给它们,以便它们可以访问模型进行检查点操作。

for callback in callbacks:
    callback.model = model

在这个例子中,我们只是调用了回调的 on_epoch_end() 方法来帮助我们进行模型检查点。如果需要,您也可以调用其他回调方法。如果您不需要保存模型,则无需使用回调。

在自定义训练循环中,我们调整数据集的批大小,因为我们将 NumPy 数据包装到 tf.data.Dataset 中。请注意,您也可以在这里调整任何预处理步骤。我们还调整了优化器的学习率。

我们将使用验证损失作为模型的评估指标。为了计算平均验证损失,我们将使用 keras.metrics.Mean(),它对每个批次的验证损失进行平均。我们需要返回验证损失,以便调整器记录。

class MyHyperModel(keras_tuner.HyperModel):
    def build(self, hp):
        """构建一个卷积模型。"""
        inputs = keras.Input(shape=(28, 28, 1))
        x = keras.layers.Flatten()(inputs)
        x = keras.layers.Dense(
            units=hp.Choice("units", [32, 64, 128]), activation="relu"
        )(x)
        outputs = keras.layers.Dense(10)(x)
        return keras.Model(inputs=inputs, outputs=outputs)

    def fit(self, hp, model, x, y, validation_data, callbacks=None, **kwargs):
        # 将数据集转换为 tf.data.Dataset。
        batch_size = hp.Int("batch_size", 32, 128, step=32, default=64)
        train_ds = tf.data.Dataset.from_tensor_slices((x_train, y_train)).batch(
            batch_size
        )
        validation_data = tf.data.Dataset.from_tensor_slices(validation_data).batch(
            batch_size
        )

        # 定义优化器。
        optimizer = keras.optimizers.Adam(
            hp.Float("learning_rate", 1e-4, 1e-2, sampling="log", default=1e-3)
        )
        loss_fn = keras.losses.SparseCategoricalCrossentropy(from_logits=True)

        # 跟踪验证损失的指标。
        epoch_loss_metric = keras.metrics.Mean()

        # 运行训练步骤的函数。
        @tf.function
        def run_train_step(images, labels):
            with tf.GradientTape() as tape:
                logits = model(images)
                loss = loss_fn(labels, logits)
                # 添加任何正则化损失。
                if model.losses:
                    loss += tf.math.add_n(model.losses)
            gradients = tape.gradient(loss, model.trainable_variables)
            optimizer.apply_gradients(zip(gradients, model.trainable_variables))

        # 运行验证步骤的函数。
        @tf.function
        def run_val_step(images, labels):
            logits = model(images)
            loss = loss_fn(labels, logits)
            # 更新指标。
            epoch_loss_metric.update_state(loss)

        # 将模型分配给回调函数。
        for callback in callbacks:
            callback.set_model(model)

        # 记录最佳验证损失值
        best_epoch_loss = float("inf")

        # 自定义训练循环。
        for epoch in range(2):
            print(f"Epoch: {epoch}")

            # 遍历训练数据以运行训练步骤。
            for images, labels in train_ds:
                run_train_step(images, labels)

            # 遍历验证数据以运行验证步骤。
            for images, labels in validation_data:
                run_val_step(images, labels)

            # 训练轮次结束后调用回调函数。
            epoch_loss = float(epoch_loss_metric.result().numpy())
            for callback in callbacks:
                # “my_metric”是传递给调优器的目标。
                callback.on_epoch_end(epoch, logs={"my_metric": epoch_loss})
            epoch_loss_metric.reset_state()

            print(f"Epoch loss: {epoch_loss}")
            best_epoch_loss = min(best_epoch_loss, epoch_loss)

        # 返回评估指标值。
        return best_epoch_loss

现在,我们可以初始化调优器。在这里,我们将 Objective("my_metric", "min") 作为我们需要最小化的指标。指标名称应与您在回调的 on_epoch_end() 方法中传递的 logs 中使用的键保持一致。回调需要在 logs 中使用此值来找到最佳的检查点。

tuner = keras_tuner.RandomSearch(
    objective=keras_tuner.Objective("my_metric", "min"),
    max_trials=2,
    hypermodel=MyHyperModel(),
    directory="results",
    project_name="custom_training",
    overwrite=True,
)

我们通过将 MyHyperModel.fit() 的签名中定义的参数传递给 tuner.search() 开始搜索。

tuner.search(x=x_train, y=y_train, validation_data=(x_val, y_val))
实验 2 完成 [00h 00m 02s]
my_metric: 2.3025283813476562
迄今为止最佳 my_metric: 2.3025283813476562
总耗时: 00h 00m 04s

最后,我们可以检索结果。

best_hps = tuner.get_best_hyperparameters()[0]
print(best_hps.values)

best_model = tuner.get_best_models()[0]
best_model.summary()
{'units': 128, 'batch_size': 32, 'learning_rate': 0.0034272591820215972}
模型: "functional_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ 层 (类型)                     输出形状                  参数数量 ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 输入层 (InputLayer)        │ (None, 28, 28, 1)         │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ 展平层 (Flatten)               │ (None, 784)               │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ 全连接层 (Dense)                   │ (None, 128)               │    100,480 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ 全连接层_1 (Dense)                 │ (None, 10)                │      1,290 │
└─────────────────────────────────┴───────────────────────────┴────────────┘
 总参数: 101,770 (397.54 KB)
 可训练参数: 101,770 (397.54 KB)
 不可训练参数: 0 (0.00 B)

总之,要在自定义训练循环中调整超参数,您只需重写 HyperModel.fit() 以训练模型并返回评估结果。使用提供的回调,您可以轻松保存已训练的模型。 他们的最佳周期,并在之后加载最佳模型。

要了解有关 KerasTuner 的基本知识,请参见 开始使用 KerasTuner