3.5 Tuning Hyperparameters with Hyperopt's fmin
Key Takeaways
fmin(fn, space, algo, max_evals, trials)drives the search: an objective function, a search space, a search algorithm, an evaluation budget, and a trials store.The objective takes one dictionary of sampled hyperparameters and returns either a scalar loss or
{'loss': value, 'status': STATUS_OK}.fminalways minimises, so a metric that should be maximised must be returned negated —-roc_auc— or as1 - metric.hp.quniformreturns floats, so integer hyperparameters such asmax_depthmust be cast withint()inside the objective or the estimator raises a TypeError.hp.loguniform(label, np.log(lo), np.log(hi))takes log-space bounds and is the correct distribution for learning rates and regularisation strengths.
3.5 Tuning Hyperparameters with Hyperopt's fmin
fmin is Hyperopt's entry point, and its signature encodes the whole workflow:
from hyperopt import fmin, tpe, hp, STATUS_OK, Trials
best = fmin(
fn=objective_function, # callable: params dict -> loss (or result dict)
space=search_space, # dict of hp.* distributions
algo=tpe.suggest, # search algorithm: tpe.suggest, rand.suggest, anneal.suggest
max_evals=50, # evaluation budget
trials=Trials(), # where results are stored (Trials or SparkTrials)
rstate=np.random.default_rng(42), # reproducibility
)
| Argument | Purpose | Common values |
|---|---|---|
fn | The objective to minimise | Your training-and-scoring function |
space | Hyperparameter distributions | A dict of hp.* expressions |
algo | How candidates are proposed | tpe.suggest (Bayesian), rand.suggest (random), anneal.suggest |
max_evals | Total trials to run | 20–200 depending on budget |
trials | Result store and execution engine | Trials() (sequential) or SparkTrials() (distributed — Section 3.7) |
rstate | Random seed | np.random.default_rng(seed) |
fmin returns a dictionary of the best hyperparameters found. Note the sharp edge:
for hp.choice parameters it returns the index into the option list, not the
value. hyperopt.space_eval(space, best) converts the returned dictionary back into
actual parameter values, which is what you want before retraining a final model.
Defining Hyperopt Search Spaces (hyperopt.hp)
Hyperopt provides a comprehensive suite of stochastic distribution primitives in the hyperopt.hp module:
| Expression | Description | Common Machine Learning Use Case |
|---|---|---|
hp.choice(label, options) | Returns one element from a list/tuple of categorical options. | Loss functions (['logloss', 'hinge']), optimizers (['adam', 'sgd']), tree splitters. |
hp.uniform(label, low, high) | Uniformly distributed continuous float in . | Subsampling ratios (subsample, colsample_bytree) in . |
hp.quniform(label, low, high, q) | Continuous float drawn uniformly, rounded to step size : . | Number of trees (n_estimators), tree depth (max_depth). Must cast to int in objective! |
hp.loguniform(label, low, high) | Value drawn such that is uniform in . | Learning rate : hp.loguniform('lr', np.log(1e-4), np.log(1e-1)). |
hp.lognormal(label, mu, sigma) | Value drawn from log-normal distribution with mean and std . | Positive regularization penalties with skewed distribution priors. |
Important
Integer Casting in hp.quniform:
hp.quniform returns a floating-point number (e.g. 6.0). Passing floats to integer parameters like max_depth or n_estimators in Scikit-learn or XGBoost causes runtime exceptions. Always cast integer hyperparameters: int(params['max_depth']).
Objective Function Construction & Optimization Loop
The objective function is the evaluation harness executed on every trial. It must satisfy three strict rules:
- Input: Accepts a single dictionary containing candidate hyperparameters sampled from the search space.
- Optimization Direction: Hyperopt strictly minimizes the objective. If optimizing a metric where higher is better (e.g., ROC-AUC, Accuracy, ), you must return the negative metric (e.g.,
-roc_auc) or . - Return Format: Returns either a single scalar loss (float) or a dictionary containing at minimum
{'loss': loss_value, 'status': STATUS_OK}.
+-----------------------------------------------------------------------------+
| HYPEROPT OBJECTIVE FUNCTION CONTRACT |
| |
| def objective(params): |
| # 1. Cast integer hyperparameters |
| max_depth = int(params['max_depth']) |
| |
| # 2. Train model on training data |
| model = XGBClassifier(max_depth=max_depth, lr=params['lr'], ...) |
| model.fit(X_train, y_train) |
| |
| # 3. Evaluate validation metric |
| val_auc = roc_auc_score(y_val, model.predict_proba(X_val)[:, 1]) |
| |
| # 4. Return negated loss for minimization |
| return {'loss': -val_auc, 'status': STATUS_OK} |
+-----------------------------------------------------------------------------+
Complete Production Code Example
import numpy as np
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from hyperopt import fmin, tpe, hp, STATUS_OK, Trials
import mlflow
import mlflow.xgboost
# 1. Prepare Data
pdf = spark.table("lakehouse_gold.churn_features").toPandas()
X = pdf.drop(columns=["customer_id", "churned"])
y = pdf["churned"]
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
# 2. Define Hyperopt Search Space
search_space = {
"max_depth": hp.quniform("max_depth", 3, 10, 1),
"learning_rate": hp.loguniform("learning_rate", np.log(0.005), np.log(0.3)),
"n_estimators": hp.quniform("n_estimators", 50, 400, 25),
"subsample": hp.uniform("subsample", 0.6, 1.0),
"colsample_bytree": hp.uniform("colsample_bytree", 0.6, 1.0),
"reg_alpha": hp.loguniform("reg_alpha", np.log(1e-3), np.log(10.0)),
"reg_lambda": hp.loguniform("reg_lambda", np.log(1e-3), np.log(10.0))
}
# 3. Define Objective Function
def objective_function(params):
# Cast discrete hyperparameters to integers
params["max_depth"] = int(params["max_depth"])
params["n_estimators"] = int(params["n_estimators"])
# Train candidate model
model = xgb.XGBClassifier(
**params,
random_state=42,
eval_metric="logloss",
use_label_encoder=False
)
model.fit(X_train, y_train, eval_set=[(X_val, y_val)], verbose=False)
# Predict and evaluate validation metric
val_probs = model.predict_proba(X_val)[:, 1]
val_auc = roc_auc_score(y_val, val_probs)
# Return negated metric because fmin minimizes loss
return {
"loss": -val_auc,
"status": STATUS_OK,
"roc_auc": val_auc
}
# 4. Execute Optimization with MLflow Tracking
mlflow.xgboost.autolog(log_models=True)
trials = Trials()
with mlflow.start_run(run_name="xgboost_hyperopt_parent"):
best_hyperparameters = fmin(
fn=objective_function,
space=search_space,
algo=tpe.suggest,
max_evals=50,
trials=trials,
rstate=np.random.default_rng(42)
)
print("Optimal Hyperparameters:", best_hyperparameters)
MLflow Automatic Experiment Tracking Integration
Databricks provides seamless, automatic integration between Hyperopt and MLflow. When mlflow.autolog() is activated before calling fmin(), MLflow automatically constructs a hierarchical nested experiment run structure:
+-----------------------------------------------------------------------------+
| MLFLOW NESTED RUN HIERARCHY FOR HYPEROPT |
| |
| PARENT RUN: "hyperopt_tpe_optimization" |
| +-- Best Parameters: {learning_rate: 0.038, max_depth: 6, subsample: 0.85}|
| +-- Best Validation Loss: -0.9240 (ROC-AUC: 0.9240) |
| | |
| +---> CHILD RUN 1: Trial 001 | lr=0.120 | depth=3 | loss=-0.8650 | 2.1s |
| +---> CHILD RUN 2: Trial 002 | lr=0.005 | depth=8 | loss=-0.8120 | 4.3s |
| +---> CHILD RUN 3: Trial 003 | lr=0.045 | depth=5 | loss=-0.9100 | 3.0s |
| +---> CHILD RUN 4: Trial 004 | lr=0.038 | depth=6 | loss=-0.9240 | 3.5s * |
| +---> CHILD RUN N: Trial ... |
+-----------------------------------------------------------------------------+
Each nested child run captures:
- The exact hyperparameter parameters evaluated in that trial.
- Training metrics, validation metrics, and epoch loss curves.
- Model artifacts and the environment dependencies needed to reload them.
- Trial execution start time, duration, and status.
When defining an objective function for Hyperopt to tune a binary classification model where the primary target performance metric is F1-Score, what must the function return to ensure proper optimization?
A dictionary containing {'loss': -f1_score, 'status': STATUS_OK} (or scalar -f1_score) because Hyperopt always minimizes the returned loss value.
A dictionary containing {'score': f1_score, 'status': STATUS_MAXIMIZE} because Hyperopt automatically detects classification tasks.
The raw confusion matrix array [TP, FP, TN, FN] so Hyperopt can calculate ROC-AUC internally.
A boolean flag indicating whether the current trial surpassed the baseline model accuracy.
An engineer specifies "max_depth": hp.quniform("max_depth", 3, 10, 1) in their Hyperopt search space. When passing params["max_depth"] into Scikit-learn's RandomForestClassifier, the code throws a TypeError. What is the cause and resolution?
hp.quniform is only supported for PySpark ML estimators; the engineer must switch to ParamGridBuilder.
hp.quniform generates floating-point values (e.g. 5.0); the parameter must be explicitly cast to an integer using int(params["max_depth"]) before model initialization.
The step size of 1 is invalid for tree depth; it must be specified as 0.1.
Scikit-learn models cannot be executed inside a Hyperopt objective function.
When mlflow.autolog() is enabled prior to running Hyperopt's fmin() function on Databricks, how does MLflow structure the resulting experiment runs?
It creates a single flattened run that overwrites parameters on every iteration, saving only the final trial.
It generates a distinct unlinked experiment for every individual trial without recording parent lineage.
It creates a parent experiment run for the overall fmin() optimization session, with each individual Hyperopt trial logged as a nested child run containing its specific parameters, metrics, and artifacts.
It disables MLflow tracking entirely to prevent distributed cluster logging conflicts.
Sections you finish are checked off in the contents.