KerasTuner:超参数调优 / KerasTuner 入门

KerasTuner 入门

作者: Luca Invernizzi, James Long, Francois Chollet, Tom O'Malley, Haifeng Jin
创建日期 2019/05/31
最后修改日期 2021/10/27
描述: 使用 KerasTuner 调整模型超参数的基础知识。

在 Colab 中查看 GitHub 源代码

!pip install keras-tuner -q

简介

KerasTuner 是一个通用的超参数调优库。它与 Keras 工作流程有很强的集成,但并不局限于 Keras:你可以用它来调整 scikit-learn 模型,或者任何其他东西。在本教程中,你将看到如何使用 KerasTuner 调整模型架构、训练过程和数据预处理步骤。让我们从一个简单的例子开始。


调整模型架构

我们需要做的第一件事是编写一个函数,该函数返回一个编译后的 Keras 模型。它接受一个参数 hp,用于在构建模型时定义超参数。

定义搜索空间

在下面的代码示例中,我们定义了一个具有两个 Dense 层的 Keras 模型。我们想要调整第一个 Dense 层中的单元数。我们只需使用 hp.Int('units', min_value=32, max_value=512, step=32) 定义一个整数超参数,其范围从 32 到 512(包括 32 和 512)。当从中采样时,遍历该区间的最小步长为 32。

import keras
from keras import layers


def build_model(hp):
    model = keras.Sequential()
    model.add(layers.Flatten())
    model.add(
        layers.Dense(
            # Define the hyperparameter.
            units=hp.Int("units", min_value=32, max_value=512, step=32),
            activation="relu",
        )
    )
    model.add(layers.Dense(10, activation="softmax"))
    model.compile(
        optimizer="adam",
        loss="categorical_crossentropy",
        metrics=["accuracy"],
    )
    return model

你可以快速测试模型是否成功构建。

import keras_tuner

build_model(keras_tuner.HyperParameters())
<Sequential name=sequential, built=False>

还有许多其他类型的超参数。我们可以在函数中定义多个超参数。在下面的代码中,我们使用 hp.Boolean() 调整是否使用 Dropout 层,使用 hp.Choice() 调整要使用的激活函数,使用 hp.Float() 调整优化器的学习率。

def build_model(hp):
    model = keras.Sequential()
    model.add(layers.Flatten())
    model.add(
        layers.Dense(
            # Tune number of units.
            units=hp.Int("units", min_value=32, max_value=512, step=32),
            # Tune the activation function to use.
            activation=hp.Choice("activation", ["relu", "tanh"]),
        )
    )
    # Tune whether to use dropout.
    if hp.Boolean("dropout"):
        model.add(layers.Dropout(rate=0.25))
    model.add(layers.Dense(10, activation="softmax"))
    # Define the optimizer learning rate as a hyperparameter.
    learning_rate = hp.Float("lr", min_value=1e-4, max_value=1e-2, sampling="log")
    model.compile(
        optimizer=keras.optimizers.Adam(learning_rate=learning_rate),
        loss="categorical_crossentropy",
        metrics=["accuracy"],
    )
    return model


build_model(keras_tuner.HyperParameters())
<Sequential name=sequential_1, built=False>

如下所示,超参数是实际值。实际上,它们只是返回实际值的函数。例如,hp.Int() 返回一个 int 值。因此,你可以将它们放入变量、for 循环或 if 条件中。

hp = keras_tuner.HyperParameters()
print(hp.Int("units", min_value=32, max_value=512, step=32))
32

你也可以提前定义超参数,并将 Keras 代码保存在单独的函数中。

def call_existing_code(units, activation, dropout, lr):
    model = keras.Sequential()
    model.add(layers.Flatten())
    model.add(layers.Dense(units=units, activation=activation))
    if dropout:
        model.add(layers.Dropout(rate=0.25))
    model.add(layers.Dense(10, activation="softmax"))
    model.compile(
        optimizer=keras.optimizers.Adam(learning_rate=lr),
        loss="categorical_crossentropy",
        metrics=["accuracy"],
    )
    return model


def build_model(hp):
    units = hp.Int("units", min_value=32, max_value=512, step=32)
    activation = hp.Choice("activation", ["relu", "tanh"])
    dropout = hp.Boolean("dropout")
    lr = hp.Float("lr", min_value=1e-4, max_value=1e-2, sampling="log")
    # call existing model-building code with the hyperparameter values.
    model = call_existing_code(
        units=units, activation=activation, dropout=dropout, lr=lr
    )
    return model


build_model(keras_tuner.HyperParameters())
<Sequential name=sequential_2, built=False>

每个超参数都由其名称(第一个参数)唯一标识。为了分别调整不同 Dense 层中的单元数作为不同的超参数,我们将它们赋予不同的名称,如 f"units_{i}"

值得注意的是,这也是创建条件超参数的一个示例。有许多超参数指定 Dense 层中的单元数。这些超参数的数量由层数决定,而层数也是一个超参数。因此,使用的超参数总数可能因试验而异。某些超参数仅在满足特定条件时使用。例如,只有当 num_layers 大于 3 时才使用 units_3。使用 KerasTuner,你可以在创建模型时轻松动态地定义此类超参数。

def build_model(hp):
    model = keras.Sequential()
    model.add(layers.Flatten())
    # Tune the number of layers.
    for i in range(hp.Int("num_layers", 1, 3)):
        model.add(
            layers.Dense(
                # Tune number of units separately.
                units=hp.Int(f"units_{i}", min_value=32, max_value=512, step=32),
                activation=hp.Choice("activation", ["relu", "tanh"]),
            )
        )
    if hp.Boolean("dropout"):
        model.add(layers.Dropout(rate=0.25))
    model.add(layers.Dense(10, activation="softmax"))
    learning_rate = hp.Float("lr", min_value=1e-4, max_value=1e-2, sampling="log")
    model.compile(
        optimizer=keras.optimizers.Adam(learning_rate=learning_rate),
        loss="categorical_crossentropy",
        metrics=["accuracy"],
    )
    return model


build_model(keras_tuner.HyperParameters())
<Sequential name=sequential_3, built=False>

在定义了搜索空间后,我们需要选择一个调优器类来运行搜索。你可以从 RandomSearchBayesianOptimizationHyperband 中选择,它们对应于不同的调优算法。这里我们以 RandomSearch 为例。

要初始化调优器,我们需要在初始化器中指定几个参数。

  • hypermodel。模型构建函数,在我们的例子中是 build_model
  • objective。要优化的目标名称(是否最小化或最大化是为内置指标自动推断的)。我们将在本教程的后面介绍如何使用自定义指标。
  • max_trials。在搜索期间要运行的试验总数。
  • executions_per_trial。每个试验应该构建和拟合的模型数量。不同的试验具有不同的超参数值。同一试验内的执行具有相同的超参数值。每个试验执行多个轮次训练的目的是减少结果的差异,从而能够更准确地评估模型的性能。如果想要更快地获得结果,可以将 executions_per_trial=1(每次模型配置进行单轮训练)。
  • overwrite。控制是否覆盖同一目录中的先前结果,或者改为恢复先前的搜索。这里我们设置 overwrite=True 以开始新的搜索并忽略任何先前的结果。
  • directory。用于存储搜索结果的目录的路径。
  • project_namedirectory 中的子目录的名称。
tuner = keras_tuner.RandomSearch(
    hypermodel=build_model,
    objective="val_accuracy",
    max_trials=3,
    executions_per_trial=2,
    overwrite=True,
    directory="my_dir",
    project_name="helloworld",
)

你可以打印搜索空间的摘要

tuner.search_space_summary()
Search space summary
Default search space size: 5
num_layers (Int)
{'default': None, 'conditions': [], 'min_value': 1, 'max_value': 3, 'step': 1, 'sampling': 'linear'}
units_0 (Int)
{'default': None, 'conditions': [], 'min_value': 32, 'max_value': 512, 'step': 32, 'sampling': 'linear'}
activation (Choice)
{'default': 'relu', 'conditions': [], 'values': ['relu', 'tanh'], 'ordered': False}
dropout (Boolean)
{'default': False, 'conditions': []}
lr (Float)
{'default': 0.0001, 'conditions': [], 'min_value': 0.0001, 'max_value': 0.01, 'step': None, 'sampling': 'log'}

在开始搜索之前,让我们准备 MNIST 数据集。

import keras
import numpy as np

(x, y), (x_test, y_test) = keras.datasets.mnist.load_data()

x_train = x[:-10000]
x_val = x[-10000:]
y_train = y[:-10000]
y_val = y[-10000:]

x_train = np.expand_dims(x_train, -1).astype("float32") / 255.0
x_val = np.expand_dims(x_val, -1).astype("float32") / 255.0
x_test = np.expand_dims(x_test, -1).astype("float32") / 255.0

num_classes = 10
y_train = keras.utils.to_categorical(y_train, num_classes)
y_val = keras.utils.to_categorical(y_val, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)

然后,开始搜索最佳超参数配置。传递给 search 的所有参数都传递给每次执行中的 model.fit()。请记住传递 validation_data 来评估模型。

tuner.search(x_train, y_train, epochs=2, validation_data=(x_val, y_val))
Trial 3 Complete [00h 00m 19s]
val_accuracy: 0.9665500223636627
Best val_accuracy So Far: 0.9665500223636627
Total elapsed time: 00h 00m 40s

search 期间,模型构建函数在不同的试验中以不同的超参数值调用。在每次试验中,调优器都会生成一组新的超参数值来构建模型。然后对模型进行拟合和评估。指标被记录下来。调优器逐步探索空间,最终找到一组良好的超参数值。

查询结果

当搜索结束后,你可以检索最佳模型。模型保存在其在 validation_data 上评估的最佳性能时期。

# Get the top 2 models.
models = tuner.get_best_models(num_models=2)
best_model = models[0]
best_model.summary()
/usr/local/python/3.10.13/lib/python3.10/site-packages/keras/src/saving/saving_lib.py:388: UserWarning: Skipping variable loading for optimizer 'adam', because it has 2 variables whereas the saved optimizer has 18 variables. 
  trackable.load_own_variables(weights_store.get(inner_path))
/usr/local/python/3.10.13/lib/python3.10/site-packages/keras/src/saving/saving_lib.py:388: UserWarning: Skipping variable loading for optimizer 'adam', because it has 2 variables whereas the saved optimizer has 10 variables. 
  trackable.load_own_variables(weights_store.get(inner_path))
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ flatten (Flatten)               │ (32, 784)                 │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dense (Dense)                   │ (32, 416)                 │    326,560 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dense_1 (Dense)                 │ (32, 512)                 │    213,504 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dense_2 (Dense)                 │ (32, 32)                  │     16,416 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dropout (Dropout)               │ (32, 32)                  │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dense_3 (Dense)                 │ (32, 10)                  │        330 │
└─────────────────────────────────┴───────────────────────────┴────────────┘
 Total params: 556,810 (2.12 MB)
 Trainable params: 556,810 (2.12 MB)
 Non-trainable params: 0 (0.00 B)

你也可以打印搜索结果的摘要。

tuner.results_summary()
Results summary
Results in my_dir/helloworld
Showing 10 best trials
Objective(name="val_accuracy", direction="max")
Trial 2 summary
Hyperparameters:
num_layers: 3
units_0: 416
activation: relu
dropout: True
lr: 0.0001324166048504802
units_1: 512
units_2: 32
Score: 0.9665500223636627
Trial 0 summary
Hyperparameters:
num_layers: 1
units_0: 128
activation: tanh
dropout: False
lr: 0.001425162921397599
Score: 0.9623999893665314
Trial 1 summary
Hyperparameters:
num_layers: 2
units_0: 512
activation: tanh
dropout: True
lr: 0.0010584293918512798
units_1: 32
Score: 0.9606499969959259

你将在文件夹 my_dir/helloworld 中找到详细的日志、检查点等,即 directory/project_name

你还可以使用 TensorBoard 和 HParams 插件可视化调优结果。有关更多信息,请访问 此链接

重新训练模型

如果你想使用整个数据集训练模型,你可以检索最佳超参数并自行重新训练模型。

# Get the top 2 hyperparameters.
best_hps = tuner.get_best_hyperparameters(5)
# Build the model with the best hp.
model = build_model(best_hps[0])
# Fit with the entire dataset.
x_all = np.concatenate((x_train, x_val))
y_all = np.concatenate((y_train, y_val))
model.fit(x=x_all, y=y_all, epochs=1)
1/1875 ━━━━━━━━━━━━━━━━━━━━  17:57 575ms/step - accuracy: 0.1250 - loss: 2.3113


29/1875 ━━━━━━━━━━━━━━━━━━━━ 3 秒 2 毫秒/步 - 准确率:0.1753 - 损失:2.2296



63/1875 ━━━━━━━━━━━━━━━━━━━━ 3 秒 2 毫秒/步 - 准确率:0.2626 - 损失:2.1206



96/1875 ━━━━━━━━━━━━━━━━━━━━ 2 秒 2 毫秒/步 - 准确率:0.3252 - 损失:2.0103



130/1875 ━━━━━━━━━━━━━━━━━━━━ 2 秒 2 毫秒/步 - 准确率:0.3745 - 损失:1.9041



164/1875 ━━━━━━━━━━━━━━━━━━━━ 2 秒 2 毫秒/步 - 准确率:0.4139 - 损失:1.8094



199/1875 ━━━━━━━━━━━━━━━━━━━━ 2 秒 2 毫秒/步 - 准确率:0.4470 - 损失:1.7246



235/1875 ━━━━━━━━━━━━━━━━━━━━ 2 秒 2 毫秒/步 - 准确率:0.4752 - 损失:1.6493



270/1875 ━━━━━━━━━━━━━━━━━━━━ 2 秒 2 毫秒/步 - 准确率:0.4982 - 损失:1.5857



305/1875 ━━━━━━━━━━━━━━━━━━━━ 2 秒 2 毫秒/步 - 准确率:0.5182 - 损失:1.5293



339/1875 ━━━━━━━━━━━━━━━━━━━━ 2 秒 2 毫秒/步 - 准确率:0.5354 - 损失:1.4800



374/1875 ━━━━━━━━━━━━━━━━━━━━ 2 秒 1 毫秒/步 - 准确率:0.5513 - 损失:1.4340



409/1875 ━━━━━━━━━━━━━━━━━━━━ 2 秒 1 毫秒/步 - 准确率:0.5656 - 损失:1.3924



444/1875 ━━━━━━━━━━━━━━━━━━━━ 2 秒 1 毫秒/步 - 准确率:0.5785 - 损失:1.3545



478/1875 ━━━━━━━━━━━━━━━━━━━━ 2 秒 1 毫秒/步 - 准确率:0.5899 - 损失:1.3208



513/1875 ━━━━━━━━━━━━━━━━━━━━ 2 秒 1 毫秒/步 - 准确率:0.6006 - 损失:1.2887



548/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6104 - 损失:1.2592



583/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6195 - 损失:1.2318



618/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6279 - 损失:1.2063



653/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6358 - 损失:1.1823



688/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6431 - 损失:1.1598



723/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6500 - 损失:1.1387



758/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6564 - 损失:1.1189



793/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6625 - 损失:1.1002



828/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6682 - 损失:1.0826



863/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6736 - 损失:1.0658



899/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6788 - 损失:1.0495



935/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6838 - 损失:1.0339



970/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6885 - 损失:1.0195



1005/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6929 - 损失:1.0058



1041/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.6972 - 损失:0.9923



1076/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.7012 - 损失:0.9798



1111/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.7051 - 损失:0.9677



1146/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.7088 - 损失:0.9561



1182/1875 ━━━━━━━━━━━━━━━━━━━━ 1 秒 1 毫秒/步 - 准确率:0.7124 - 损失:0.9446



1218/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7159 - 损失:0.9336



1254/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7193 - 损失:0.9230



1289/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7225 - 损失:0.9131



1324/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7255 - 损失:0.9035



1359/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7284 - 损失:0.8943



1394/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7313 - 损失:0.8853



1429/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7341 - 损失:0.8767



1465/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7368 - 损失:0.8680



1500/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7394 - 损失:0.8599



1535/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7419 - 损失:0.8520



1570/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7443 - 损失:0.8444



1605/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7467 - 损失:0.8370



1639/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7489 - 损失:0.8299



1674/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7511 - 损失:0.8229



1707/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7532 - 损失:0.8164



1741/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7552 - 损失:0.8099



1774/1875 ━━━━━━━━━━━━━━━━━━━━ 0 秒 1 毫秒/步 - 准确率:0.7572 - 损失:0.8038



1809/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 1毫秒/步 - 准确率: 0.7592 - 损失: 0.7975



1843/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 1毫秒/步 - 准确率: 0.7611 - 损失: 0.7915



1875/1875 ━━━━━━━━━━━━━━━━━━━━ 3秒 1毫秒/步 - 准确率: 0.7629 - 损失: 0.7858

<keras.src.callbacks.history.History at 0x7f31883d9e10>

调整模型训练

要调整模型构建过程,我们需要继承 HyperModel 类,这也有助于共享和重用超模型。

我们需要重写 HyperModel.build()HyperModel.fit() 分别来调整模型构建和训练过程。HyperModel.build() 方法与模型构建函数相同,它使用超参数创建一个 Keras 模型并返回它。

HyperModel.fit() 中,您可以访问 HyperModel.build() 返回的模型、hp 以及传递给 search() 的所有参数。您需要训练模型并返回训练历史记录。

在以下代码中,我们将调整 model.fit() 中的 shuffle 参数。

通常不需要调整 epoch 的数量,因为会传递一个内置的回调函数给 model.fit(),以便在 validation_data 评估的最佳 epoch 时保存模型。

注意**kwargs 应该始终传递给 model.fit(),因为它包含模型保存和 tensorboard 插件的回调函数。

class MyHyperModel(keras_tuner.HyperModel):
    def build(self, hp):
        model = keras.Sequential()
        model.add(layers.Flatten())
        model.add(
            layers.Dense(
                units=hp.Int("units", min_value=32, max_value=512, step=32),
                activation="relu",
            )
        )
        model.add(layers.Dense(10, activation="softmax"))
        model.compile(
            optimizer="adam",
            loss="categorical_crossentropy",
            metrics=["accuracy"],
        )
        return model

    def fit(self, hp, model, *args, **kwargs):
        return model.fit(
            *args,
            # Tune whether to shuffle the data in each epoch.
            shuffle=hp.Boolean("shuffle"),
            **kwargs,
        )

同样,我们可以快速检查代码是否正常工作。

hp = keras_tuner.HyperParameters()
hypermodel = MyHyperModel()
model = hypermodel.build(hp)
hypermodel.fit(hp, model, np.random.rand(100, 28, 28), np.random.rand(100, 10))

1/4 ━━━━━━━━━━━━━━━━━━━━ 0秒 279毫秒/步 - 准确率: 0.0000e+00 - 损失: 12.2230



4/4 ━━━━━━━━━━━━━━━━━━━━ 0秒 108毫秒/步 - 准确率: 0.0679 - 损失: 11.9568



4/4 ━━━━━━━━━━━━━━━━━━━━ 1秒 109毫秒/步 - 准确率: 0.0763 - 损失: 11.8941

<keras.src.callbacks.history.History at 0x7f318865c100>

调整数据预处理

要调整数据预处理,我们只需在 HyperModel.fit() 中添加一个额外的步骤,我们可以在其中访问来自参数的数据集。在以下代码中,我们调整是否在训练模型之前规范化数据。这次我们在函数签名中显式地放入 xy,因为我们需要使用它们。

class MyHyperModel(keras_tuner.HyperModel):
    def build(self, hp):
        model = keras.Sequential()
        model.add(layers.Flatten())
        model.add(
            layers.Dense(
                units=hp.Int("units", min_value=32, max_value=512, step=32),
                activation="relu",
            )
        )
        model.add(layers.Dense(10, activation="softmax"))
        model.compile(
            optimizer="adam",
            loss="categorical_crossentropy",
            metrics=["accuracy"],
        )
        return model

    def fit(self, hp, model, x, y, **kwargs):
        if hp.Boolean("normalize"):
            x = layers.Normalization()(x)
        return model.fit(
            x,
            y,
            # Tune whether to shuffle the data in each epoch.
            shuffle=hp.Boolean("shuffle"),
            **kwargs,
        )


hp = keras_tuner.HyperParameters()
hypermodel = MyHyperModel()
model = hypermodel.build(hp)
hypermodel.fit(hp, model, np.random.rand(100, 28, 28), np.random.rand(100, 10))

1/4 ━━━━━━━━━━━━━━━━━━━━ 0秒 276毫秒/步 - 准确率: 0.1250 - 损失: 12.0090



4/4 ━━━━━━━━━━━━━━━━━━━━ 0秒 94毫秒/步 - 准确率: 0.0994 - 损失: 12.1242



4/4 ━━━━━━━━━━━━━━━━━━━━ 1秒 95毫秒/步 - 准确率: 0.0955 - 损失: 12.1594

<keras.src.callbacks.history.History at 0x7f31ba836200>

如果一个超参数在 build()fit() 中都使用,您可以在 build() 中定义它,并在 fit() 中使用 hp.get(hp_name) 来检索它。我们以图像大小为例。它既用作 build() 中的输入形状,又用于 fit() 中数据预处理步骤来裁剪图像。

class MyHyperModel(keras_tuner.HyperModel):
    def build(self, hp):
        image_size = hp.Int("image_size", 10, 28)
        inputs = keras.Input(shape=(image_size, image_size))
        outputs = layers.Flatten()(inputs)
        outputs = layers.Dense(
            units=hp.Int("units", min_value=32, max_value=512, step=32),
            activation="relu",
        )(outputs)
        outputs = layers.Dense(10, activation="softmax")(outputs)
        model = keras.Model(inputs, outputs)
        model.compile(
            optimizer="adam",
            loss="categorical_crossentropy",
            metrics=["accuracy"],
        )
        return model

    def fit(self, hp, model, x, y, validation_data=None, **kwargs):
        if hp.Boolean("normalize"):
            x = layers.Normalization()(x)
        image_size = hp.get("image_size")
        cropped_x = x[:, :image_size, :image_size, :]
        if validation_data:
            x_val, y_val = validation_data
            cropped_x_val = x_val[:, :image_size, :image_size, :]
            validation_data = (cropped_x_val, y_val)
        return model.fit(
            cropped_x,
            y,
            # Tune whether to shuffle the data in each epoch.
            shuffle=hp.Boolean("shuffle"),
            validation_data=validation_data,
            **kwargs,
        )


tuner = keras_tuner.RandomSearch(
    MyHyperModel(),
    objective="val_accuracy",
    max_trials=3,
    overwrite=True,
    directory="my_dir",
    project_name="tune_hypermodel",
)

tuner.search(x_train, y_train, epochs=2, validation_data=(x_val, y_val))
Trial 3 Complete [00h 00m 04s]
val_accuracy: 0.9567000269889832
Best val_accuracy So Far: 0.9685999751091003
Total elapsed time: 00h 00m 13s

重新训练模型

使用 HyperModel 还允许您自己重新训练最佳模型。

hypermodel = MyHyperModel()
best_hp = tuner.get_best_hyperparameters()[0]
model = hypermodel.build(best_hp)
hypermodel.fit(best_hp, model, x_all, y_all, epochs=1)
1/1875 ━━━━━━━━━━━━━━━━━━━━  9:00 289ms/step - accuracy: 0.0000e+00 - loss: 2.4352


52/1875 ━━━━━━━━━━━━━━━━━━━━ 1秒 996微秒/步 - 准确率: 0.6035 - 损失: 1.3521



110/1875 ━━━━━━━━━━━━━━━━━━━━ 1秒 925微秒/步 - 准确率: 0.7037 - 损失: 1.0231



171/1875 ━━━━━━━━━━━━━━━━━━━━ 1秒 890微秒/步 - 准确率: 0.7522 - 损失: 0.8572



231/1875 ━━━━━━━━━━━━━━━━━━━━ 1秒 877微秒/步 - 准确率: 0.7804 - 损失: 0.7590



291/1875 ━━━━━━━━━━━━━━━━━━━━ 1秒 870微秒/步 - 准确率: 0.7993 - 损失: 0.6932



350/1875 ━━━━━━━━━━━━━━━━━━━━ 1秒 867微秒/步 - 准确率: 0.8127 - 损失: 0.6467



413/1875 ━━━━━━━━━━━━━━━━━━━━ 1秒 856微秒/步 - 准确率: 0.8238 - 损失: 0.6079



476/1875 ━━━━━━━━━━━━━━━━━━━━ 1秒 848微秒/步 - 准确率: 0.8326 - 损失: 0.5774



535/1875 ━━━━━━━━━━━━━━━━━━━━ 1秒 849微秒/步 - 准确率: 0.8394 - 损失: 0.5536



600/1875 ━━━━━━━━━━━━━━━━━━━━ 1秒 841微秒/步 - 准确率: 0.8458 - 损失: 0.5309



661/1875 ━━━━━━━━━━━━━━━━━━━━ 1秒 840微秒/步 - 准确率: 0.8511 - 损失: 0.5123



723/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 837微秒/步 - 准确率: 0.8559 - 损失: 0.4955



783/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 838微秒/步 - 准确率: 0.8600 - 损失: 0.4811



847/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 834微秒/步 - 准确率: 0.8640 - 损失: 0.4671



912/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 830微秒/步 - 准确率: 0.8677 - 损失: 0.4544



976/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 827微秒/步 - 准确率: 0.8709 - 损失: 0.4429



1040/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 825微秒/步 - 准确率: 0.8738 - 损失: 0.4325



1104/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 822微秒/步 - 准确率: 0.8766 - 损失: 0.4229



1168/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 821微秒/步 - 准确率: 0.8791 - 损失: 0.4140



1233/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 818微秒/步 - 准确率: 0.8815 - 损失: 0.4056



1296/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 817微秒/步 - 准确率: 0.8837 - 损失: 0.3980



1361/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 815微秒/步 - 准确率: 0.8858 - 损失: 0.3907



1424/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 814微秒/步 - 准确率: 0.8877 - 损失: 0.3840



1488/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 813微秒/步 - 准确率: 0.8895 - 损失: 0.3776



1550/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 813微秒/步 - 准确率: 0.8912 - 损失: 0.3718



1613/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 813微秒/步 - 准确率: 0.8928 - 损失: 0.3662



1678/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 811微秒/步 - 准确率: 0.8944 - 损失: 0.3607



1744/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 809微秒/步 - 准确率: 0.8959 - 损失: 0.3555



1810/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 808微秒/步 - 准确率: 0.8973 - 损失: 0.3504



1874/1875 ━━━━━━━━━━━━━━━━━━━━ 0秒 807微秒/步 - 准确率: 0.8987 - 损失: 0.3457



1875/1875 ━━━━━━━━━━━━━━━━━━━━ 2秒 808微秒/步 - 准确率: 0.8987 - 损失: 0.3456

<keras.src.callbacks.history.History at 0x7f31884b3070>

指定调整目标

在所有前面的示例中,我们都只使用验证准确率 ("val_accuracy") 作为调整目标来选择最佳模型。实际上,您可以使用任何指标作为目标。最常用的指标是 "val_loss",它是验证损失。

内置指标作为目标

Keras 中还有许多其他内置指标可以用作目标。这是 内置指标的列表

要使用内置指标作为目标,您需要执行以下步骤

  • 使用内置指标编译模型。例如,您想使用 MeanAbsoluteError()。您需要使用 metrics=[MeanAbsoluteError()] 编译模型。您也可以使用其名称字符串代替:metrics=["mean_absolute_error"]。指标的名称字符串始终是类名称的蛇形大小写。
  • 确定目标名称字符串。目标的名称字符串始终采用 f"val_{metric_name_string}" 的格式。例如,在验证数据上评估的均方误差的目标名称字符串应为 "val_mean_absolute_error"
  • 将其包装到 keras_tuner.Objective 中。我们通常需要将目标包装到 keras_tuner.Objective 对象中,以指定优化目标的方向。例如,我们要最小化均方误差,我们可以使用 keras_tuner.Objective("val_mean_absolute_error", "min")。方向应为 "min""max"
  • 将包装后的目标传递给调谐器。

您可以查看以下基本代码示例。

def build_regressor(hp):
    model = keras.Sequential(
        [
            layers.Dense(units=hp.Int("units", 32, 128, 32), activation="relu"),
            layers.Dense(units=1),
        ]
    )
    model.compile(
        optimizer="adam",
        loss="mean_squared_error",
        # Objective is one of the metrics.
        metrics=[keras.metrics.MeanAbsoluteError()],
    )
    return model


tuner = keras_tuner.RandomSearch(
    hypermodel=build_regressor,
    # The objective name and direction.
    # Name is the f"val_{snake_case_metric_class_name}".
    objective=keras_tuner.Objective("val_mean_absolute_error", direction="min"),
    max_trials=3,
    overwrite=True,
    directory="my_dir",
    project_name="built_in_metrics",
)

tuner.search(
    x=np.random.rand(100, 10),
    y=np.random.rand(100, 1),
    validation_data=(np.random.rand(20, 10), np.random.rand(20, 1)),
)

tuner.results_summary()
Trial 3 Complete [00h 00m 01s]
val_mean_absolute_error: 0.39589792490005493
Best val_mean_absolute_error So Far: 0.34321871399879456
Total elapsed time: 00h 00m 03s
Results summary
Results in my_dir/built_in_metrics
Showing 10 best trials
Objective(name="val_mean_absolute_error", direction="min")
Trial 1 summary
Hyperparameters:
units: 32
Score: 0.34321871399879456
Trial 2 summary
Hyperparameters:
units: 128
Score: 0.39589792490005493
Trial 0 summary
Hyperparameters:
units: 96
Score: 0.5005304217338562

自定义指标作为目标

您可以实现自己的指标并将其用作超参数搜索目标。在这里,我们以均方误差 (MSE) 为例。首先,我们通过继承 keras.metrics.Metric 来实现 MSE 指标。请记住使用 super().__init__()name 参数为您的指标命名,该参数稍后将被使用。注意:MSE 实际上是一个内置指标,可以使用 keras.metrics.MeanSquaredError 导入。这只是一个示例,展示如何使用自定义指标作为超参数搜索目标。

有关实现自定义指标的更多信息,请参阅 此教程。如果您需要一个与 update_state(y_true, y_pred, sample_weight) 具有不同函数签名的指标,您可以按照 此教程 重写模型的 train_step() 方法。

from keras import ops


class CustomMetric(keras.metrics.Metric):
    def __init__(self, **kwargs):
        # Specify the name of the metric as "custom_metric".
        super().__init__(name="custom_metric", **kwargs)
        self.sum = self.add_weight(name="sum", initializer="zeros")
        self.count = self.add_weight(name="count", dtype="int32", initializer="zeros")

    def update_state(self, y_true, y_pred, sample_weight=None):
        values = ops.square(y_true - y_pred)
        count = ops.shape(y_true)[0]
        if sample_weight is not None:
            sample_weight = ops.cast(sample_weight, self.dtype)
            values *= sample_weight
            count *= sample_weight
        self.sum.assign_add(ops.sum(values))
        self.count.assign_add(count)

    def result(self):
        return self.sum / ops.cast(self.count, "float32")

    def reset_state(self):
        self.sum.assign(0)
        self.count.assign(0)

使用自定义目标运行搜索。

def build_regressor(hp):
    model = keras.Sequential(
        [
            layers.Dense(units=hp.Int("units", 32, 128, 32), activation="relu"),
            layers.Dense(units=1),
        ]
    )
    model.compile(
        optimizer="adam",
        loss="mean_squared_error",
        # Put custom metric into the metrics.
        metrics=[CustomMetric()],
    )
    return model


tuner = keras_tuner.RandomSearch(
    hypermodel=build_regressor,
    # Specify the name and direction of the objective.
    objective=keras_tuner.Objective("val_custom_metric", direction="min"),
    max_trials=3,
    overwrite=True,
    directory="my_dir",
    project_name="custom_metrics",
)

tuner.search(
    x=np.random.rand(100, 10),
    y=np.random.rand(100, 1),
    validation_data=(np.random.rand(20, 10), np.random.rand(20, 1)),
)

tuner.results_summary()
Trial 3 Complete [00h 00m 01s]
val_custom_metric: 0.2830956280231476
Best val_custom_metric So Far: 0.2529197633266449
Total elapsed time: 00h 00m 02s
Results summary
Results in my_dir/custom_metrics
Showing 10 best trials
Objective(name="val_custom_metric", direction="min")
Trial 0 summary
Hyperparameters:
units: 32
Score: 0.2529197633266449
Trial 2 summary
Hyperparameters:
units: 128
Score: 0.2830956280231476
Trial 1 summary
Hyperparameters:
units: 96
Score: 0.4656866192817688

如果您的自定义目标很难放入自定义指标中,您也可以在 HyperModel.fit() 中自行评估模型并返回目标值。默认情况下,目标值将被最小化。在这种情况下,您在初始化调谐器时不需要指定 objective。但是,在这种情况下,指标值将不会仅通过 KerasTuner 日志在 Keras 日志中跟踪。因此,这些值不会显示在任何使用 Keras 指标的 TensorBoard 视图中。

class HyperRegressor(keras_tuner.HyperModel):
    def build(self, hp):
        model = keras.Sequential(
            [
                layers.Dense(units=hp.Int("units", 32, 128, 32), activation="relu"),
                layers.Dense(units=1),
            ]
        )
        model.compile(
            optimizer="adam",
            loss="mean_squared_error",
        )
        return model

    def fit(self, hp, model, x, y, validation_data, **kwargs):
        model.fit(x, y, **kwargs)
        x_val, y_val = validation_data
        y_pred = model.predict(x_val)
        # Return a single float to minimize.
        return np.mean(np.abs(y_pred - y_val))


tuner = keras_tuner.RandomSearch(
    hypermodel=HyperRegressor(),
    # No objective to specify.
    # Objective is the return value of `HyperModel.fit()`.
    max_trials=3,
    overwrite=True,
    directory="my_dir",
    project_name="custom_eval",
)
tuner.search(
    x=np.random.rand(100, 10),
    y=np.random.rand(100, 1),
    validation_data=(np.random.rand(20, 10), np.random.rand(20, 1)),
)

tuner.results_summary()
Trial 3 Complete [00h 00m 01s]
default_objective: 0.6571611521766413
Best default_objective So Far: 0.40719249752993525
Total elapsed time: 00h 00m 02s
Results summary
Results in my_dir/custom_eval
Showing 10 best trials
Objective(name="default_objective", direction="min")
Trial 1 summary
Hyperparameters:
units: 128
Score: 0.40719249752993525
Trial 0 summary
Hyperparameters:
units: 96
Score: 0.4992297225533352
Trial 2 summary
Hyperparameters:
units: 32
Score: 0.6571611521766413

如果您在 KerasTuner 中有多个指标要跟踪,但只使用其中一个作为目标,您可以返回一个字典,其键是指标名称,值是指标值,例如,返回 {"metric_a": 1.0, "metric_b", 2.0}。使用其中一个键作为目标名称,例如 keras_tuner.Objective("metric_a", "min")

class HyperRegressor(keras_tuner.HyperModel):
    def build(self, hp):
        model = keras.Sequential(
            [
                layers.Dense(units=hp.Int("units", 32, 128, 32), activation="relu"),
                layers.Dense(units=1),
            ]
        )
        model.compile(
            optimizer="adam",
            loss="mean_squared_error",
        )
        return model

    def fit(self, hp, model, x, y, validation_data, **kwargs):
        model.fit(x, y, **kwargs)
        x_val, y_val = validation_data
        y_pred = model.predict(x_val)
        # Return a dictionary of metrics for KerasTuner to track.
        return {
            "metric_a": -np.mean(np.abs(y_pred - y_val)),
            "metric_b": np.mean(np.square(y_pred - y_val)),
        }


tuner = keras_tuner.RandomSearch(
    hypermodel=HyperRegressor(),
    # Objective is one of the keys.
    # Maximize the negative MAE, equivalent to minimize MAE.
    objective=keras_tuner.Objective("metric_a", "max"),
    max_trials=3,
    overwrite=True,
    directory="my_dir",
    project_name="custom_eval_dict",
)
tuner.search(
    x=np.random.rand(100, 10),
    y=np.random.rand(100, 1),
    validation_data=(np.random.rand(20, 10), np.random.rand(20, 1)),
)

tuner.results_summary()
Trial 3 Complete [00h 00m 01s]
metric_a: -0.39470441501524833
Best metric_a So Far: -0.3836997988261662
Total elapsed time: 00h 00m 02s
Results summary
Results in my_dir/custom_eval_dict
Showing 10 best trials
Objective(name="metric_a", direction="max")
Trial 1 summary
Hyperparameters:
units: 64
Score: -0.3836997988261662
Trial 2 summary
Hyperparameters:
units: 32
Score: -0.39470441501524833
Trial 0 summary
Hyperparameters:
units: 96
Score: -0.46081380465766364

调整端到端工作流程

在某些情况下,很难将您的代码对齐到构建和拟合函数中。您还可以通过重写 Tuner.run_trial() 将您的端到端工作流程保留在一个地方,这使您可以完全控制一个试验。您可以将其视为任何事物的黑盒优化器。

调整任何函数

例如,您可以找到一个 x 值,该值可以最小化 f(x)=x*x+1。在以下代码中,我们只将 x 定义为超参数,并返回 f(x) 作为目标值。可以省略用于初始化调谐器的 hypermodelobjective 参数。

class MyTuner(keras_tuner.RandomSearch):
    def run_trial(self, trial, *args, **kwargs):
        # Get the hp from trial.
        hp = trial.hyperparameters
        # Define "x" as a hyperparameter.
        x = hp.Float("x", min_value=-1.0, max_value=1.0)
        # Return the objective value to minimize.
        return x * x + 1


tuner = MyTuner(
    # No hypermodel or objective specified.
    max_trials=20,
    overwrite=True,
    directory="my_dir",
    project_name="tune_anything",
)

# No need to pass anything to search()
# unless you use them in run_trial().
tuner.search()
print(tuner.get_best_hyperparameters()[0].get("x"))
Trial 20 Complete [00h 00m 00s]
default_objective: 1.6547719581194267
Best default_objective So Far: 1.0013236767905302
Total elapsed time: 00h 00m 00s
0.03638236922645777

保持 Keras 代码分离

您可以保持所有 Keras 代码不变,并使用 KerasTuner 进行调整。如果由于某些原因您无法修改 Keras 代码,这将非常有用。

它还为您提供了更大的灵活性。您不必将模型构建和训练代码分开。但是,此工作流程不会帮助您保存模型或连接 TensorBoard 插件。

要保存模型,您可以使用 trial.trial_id,它是一个唯一标识试验的字符串,来构建不同的路径,以保存来自不同试验的模型。

import os


def keras_code(units, optimizer, saving_path):
    # Build model
    model = keras.Sequential(
        [
            layers.Dense(units=units, activation="relu"),
            layers.Dense(units=1),
        ]
    )
    model.compile(
        optimizer=optimizer,
        loss="mean_squared_error",
    )

    # Prepare data
    x_train = np.random.rand(100, 10)
    y_train = np.random.rand(100, 1)
    x_val = np.random.rand(20, 10)
    y_val = np.random.rand(20, 1)

    # Train & eval model
    model.fit(x_train, y_train)

    # Save model
    model.save(saving_path)

    # Return a single float as the objective value.
    # You may also return a dictionary
    # of {metric_name: metric_value}.
    y_pred = model.predict(x_val)
    return np.mean(np.abs(y_pred - y_val))


class MyTuner(keras_tuner.RandomSearch):
    def run_trial(self, trial, **kwargs):
        hp = trial.hyperparameters
        return keras_code(
            units=hp.Int("units", 32, 128, 32),
            optimizer=hp.Choice("optimizer", ["adam", "adadelta"]),
            saving_path=os.path.join("/tmp", f"{trial.trial_id}.keras"),
        )


tuner = MyTuner(
    max_trials=3,
    overwrite=True,
    directory="my_dir",
    project_name="keep_code_separate",
)
tuner.search()
# Retraining the model
best_hp = tuner.get_best_hyperparameters()[0]
keras_code(**best_hp.values, saving_path="/tmp/best_model.keras")
Trial 3 Complete [00h 00m 00s]
default_objective: 0.18014027375230962
Best default_objective So Far: 0.18014027375230962
Total elapsed time: 00h 00m 03s

1/4 ━━━━━━━━━━━━━━━━━━━━ 0秒 172毫秒/步 - 损失: 0.5030



4/4 ━━━━━━━━━━━━━━━━━━━━ 0秒 60毫秒/步 - 损失: 0.5288



4/4 ━━━━━━━━━━━━━━━━━━━━ 0秒 61毫秒/步 - 损失: 0.5367

1/1 ━━━━━━━━━━━━━━━━━━━━ 0秒 27毫秒/步



1/1 ━━━━━━━━━━━━━━━━━━━━ 0秒 28毫秒/步

0.5918120126201316

KerasTuner 包括预先制作的可调整应用程序:HyperResNet 和 HyperXception

这些是用于计算机视觉的即用型超模型。

它们预先编译了 loss="categorical_crossentropy"metrics=["accuracy"]

from keras_tuner.applications import HyperResNet

hypermodel = HyperResNet(input_shape=(28, 28, 1), classes=10)

tuner = keras_tuner.RandomSearch(
    hypermodel,
    objective="val_accuracy",
    max_trials=2,
    overwrite=True,
    directory="my_dir",
    project_name="built_in_hypermodel",
)