Hyperparameter tuning with KerasTuner
Define a search space, run RandomSearch or Hyperband, persist results, and compare trials honestly instead of chasing the best-looking number.
Defining the search space
import keras_tuner as kt
import tensorflow as tf
def build_model(hp):
model = tf.keras.Sequential()
model.add(tf.keras.layers.Input(shape=(32,)))
for i in range(hp.Int("n_layers", 1, 3)):
model.add(tf.keras.layers.Dense(
units=hp.Int(f"units_{i}", min_value=32, max_value=256, step=32),
activation=hp.Choice(f"act_{i}", ["relu", "gelu"]),
))
model.add(tf.keras.layers.Dropout(
hp.Float(f"drop_{i}", 0.0, 0.5, step=0.1)))
model.add(tf.keras.layers.Dense(1))
model.compile(
optimizer=tf.keras.optimizers.Adam(
hp.Float("lr", 1e-4, 1e-2, sampling="log")),
loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
metrics=["accuracy"],
)
return model
tuner = kt.Hyperband(
build_model,
objective="val_accuracy",
max_epochs=20,
factor=3,
directory="tuning",
project_name="tabular_binary",
overwrite=True,
)Intwith a step,Floatwithsampling="log"for learning rates,Choicefor a fixed set,Booleanfor toggles.- Names must be unique and stable. Using
f"units_{i}"inside a loop lets the search vary depth without reusing a name. - A log-uniform range for the learning rate is essential: linear sampling spends almost all trials at the high end of a decade.
- Keep the space small. Twelve hyperparameters with wide ranges will not be explored by a few dozen trials — pick the three or four that matter and fix the rest.
Running the search with callbacks
stop_early = tf.keras.callbacks.EarlyStopping(
monitor="val_loss", patience=3, restore_best_weights=True)
tuner.search(
X_train, y_train,
epochs=20,
validation_split=0.2,
callbacks=[stop_early],
verbose=1,
)
best = tuner.get_best_hyperparameters(num_trials=1)[0]
print(best.values)
# retrain from scratch on the full training data, with a real epoch budget
final = tuner.hypermodel.build(best)
final.fit(X_train, y_train, epochs=40, validation_split=0.1)
tuner.results_summary(num_trials=5)| Algorithm | How it allocates budget | When to use |
|---|---|---|
| RandomSearch | Every trial gets the full budget | Few hyperparameters, cheap trials, a reliable baseline |
| Hyperband | Races many trials, kills the losers early | Expensive trials, a large space |
| BayesianOptimization | Models the objective, proposes promising points | Expensive trials, a small budget, continuous parameters |
Hyperband decides between trials on partial training curves, so it is only sound when the ranking is stable early. If a configuration that looks weak at epoch 3 becomes best at epoch 30, Hyperband will have already discarded it.
Reading the results honestly
import pandas as pd
results = pd.DataFrame([
{"trial": t.trial_id, "score": t.score, "values": t.hyperparameters.values}
for t in tuner.oracle.get_best_trials(num_trials=10)
])
print(results[["trial", "score"]].to_string(index=False))
print(results["values"].apply(lambda v: v.get("lr")).tolist())
# re-evaluate the top few on a split the tuner never saw
for t in tuner.oracle.get_best_trials(num_trials=3):
model = tuner.hypermodel.build(t.hyperparameters)
model.fit(X_train, y_train, epochs=30, verbose=0)
score = model.evaluate(X_holdout, y_holdout, verbose=0)
print(t.trial_id, score)- The tuner's score is measured on its validation split, which was used to make selection decisions. It is now a slightly optimistic estimate, so confirm on a third split.
- A small difference between the top few trials is noise. Pick among them for speed, simplicity or parameter count instead of pretending the top score is meaningful.
- Record the seed, the tuner configuration and the search space alongside the results; a tuning run you cannot reproduce is a number you cannot trust.
- Tune the learning rate first, then architecture, then regularisation. Doing all three at once multiplies the number of trials by an order of magnitude.
💡
Tuning finds the best configuration inside the space you defined. If your features are wrong or the labels are noisy, the search will happily return a well-tuned model that has learned the noise — check the data before spending compute on the search.
FAQ
How many trials do I need?
Fewer than you think for a small space — 20 to 50 random trials usually find the good region, and beyond that you are fitting the validation split. Spend the remaining compute on a better validation protocol.
Should the tuner train on the validation data?
No. Give the tuner a training split with an internal validation split, keep a separate holdout untouched, and use it once at the end for the number you report.
Related
TensorBoard and experiment tracking Regularisation and normalisation
Last refreshed 2026-09-18.