Bayesian Optimization

Production engineering guide for bayesian optimization covering patterns, implementation strategies, and operational best practices.

Bayesian Optimization is a sequential strategy for finding the global minimum of a black-box objective function, especially when function evaluations are expensive. It matters when tuning hyperparameters of machine learning models, optimizing experimental procedures, or configuring complex systems where each evaluation takes minutes to hours.

Selecting the Hyperparameters of a Machine Learning Model

When training a model with a large number of hyperparameters—such as learning rate, batch size, number of layers, dropout rate, and optimizer type—each configuration requires a full training run. Bayesian Optimization automates this process by selecting the most promising configurations to evaluate next, based on prior belief about the objective function and evidence from past evaluations.

The core of the method is a probabilistic surrogate model—typically a Gaussian Process—that encodes belief about the objective function. At each iteration, it computes an acquisition function to balance exploration (trying new regions) and exploitation (refining known good regions), and selects the hyperparameter configuration that maximizes the acquisition score.

To run a Bayesian Optimization loop, you define:

Example: Optimizing a Neural Network with Scikit-learn and GPy

import numpy as np
from sklearn.model_selection import cross_val_score
from sklearn.neural_network import MLPClassifier
from gpyt import GaussianProcessRegressor
from gpyt.acquisition import ExpectedImprovement


param_space = {
    'learning_rate_init': (1e-4, 1e-1),       # continuous
    'batch_size': (16, 128),                   # discrete
    'hidden_layer_sizes': [(32,), (64,), (128,), (64, 32)],  # categorical
    'alpha': (1e-6, 1e-1),                     # continuous
    'max_iter': (50, 500),                     # continuous
}

# Objective function: returns validation score
def objective(params):
    model = MLPClassifier(
        learning_rate_init=params['learning_rate_init'],
        batch_size=params['batch_size'],
        hidden_layer_sizes=params['hidden_layer_sizes'],
        alpha=params['alpha'],
        max_iter=params['max_iter'],
        random_state=42
    )
    scores = cross_val_score(model, X_train, y_train, cv=5, scoring='accuracy')
    return -np.mean(scores)  # minimize negative accuracy

# Initialize Bayesian Optimization loop
gp = GaussianProcessRegressor(kernel='rbf', alpha=1e-3)
acq_func = ExpectedImprovement(gp)

# Start with a few random configurations
initial_configs = [
    {'learning_rate_init': 0.01, 'batch_size': 32, 'hidden_layer_sizes': (64,), 'alpha': 0.01, 'max_iter': 200},
    {'learning_rate_init': 0.001, 'batch_size': 64, 'hidden_layer_sizes': (128,), 'alpha': 0.001, 'max_iter': 300},
    {'learning_rate_init': 0.005, 'batch_size': 16, 'hidden_layer_sizes': (32,), 'alpha': 0.1, 'max_iter': 100},
]

# Evaluate initial points
results = []
for config in initial_configs:
    loss = objective(config)
    results.append((config, loss))

# Iteratively suggest and evaluate new configurations
for _ in range(20):
    # Fit GP to current results
    X = np.array([list(r[0].values()) for r in results])
    y = np.array([r[1] for r in results])
    gp.fit(X, y)

    # Suggest next point
    next_config = acq_func.suggest(X, y, param_space)
    next_loss = objective(next_config)
    results.append((next_config, next_loss))

The param_space defines the search domain. Continuous parameters are specified as intervals, discrete ones as lists, and categorical ones as choices. The objective function must accept a dictionary of hyperparameters and return a scalar loss.

Common Pitfalls

Failure Modes in Practice

Optimizing Experimental Procedures

In physical or biological experiments—e.g., optimizing the growth conditions of a cell culture or the settings of a chemical reactor—Bayesian Optimization automates the process of adjusting multiple control parameters to maximize yield, purity, or throughput.

The objective is the outcome of a full experiment, which may take hours or days. Each configuration is a vector of parameters: temperature, pressure, flow rate, nutrient concentration, and duration.

Example: Optimizing a Protein Expression Experiment

The experiment is controlled by a lab automation platform that accepts a JSON configuration and returns a result object.

{
  "temperature": 37.5,
  "pressure": 1.2,
  "flow_rate": 0.8,
  "nutrient_concentration": 5.0,
  "duration": 24.0
}

The platform exposes an HTTP API:

curl -X POST https://lab-api.example.com/experiments \
  -H 'Content-Type: application/json' \
  -d @experiment.json \
  -o experiment-result.json

The response includes:

{
  "experiment_id": "exp-123",
  "status": "completed",
  "yield": 83.2,
  "purity": 0.91,
  "timestamp": "2024-04-05T12:00:00Z"
}

The objective function extracts the yield or a composite score.

import requests
import json

def run_experiment(config):
    url = "https://lab-api.example.com/experiments"
    response = requests.post(url, json=config, timeout=3600)
    result = response.json()
    return -result["yield"]  # minimize negative yield

The Bayesian Optimization loop runs as before, using a GP surrogate and Expected Improvement acquisition.

Sharp Edges

Configuration of Complex Systems

For systems with many tunable parameters—e.g., a web service with caching, load balancing, database indexing, and API routing—Bayesian Optimization can find the configuration that minimizes latency, maximizes throughput, or reduces cost.

The objective is measured by running a stress test or by monitoring a production deployment over time.

Example: Optimizing a Web Service with a Custom Load Test

A system is deployed with a set of configuration keys:

# config.yaml
server:
  max_connections: 1024
  worker_threads: 8
  cache_ttl: 300
  buffer_size: 1024
  compression: true
  load_balancer:
    strategy: "round_robin"
    health_check_interval: 60
    timeout: 120

A load test is run via a command-line tool:

./run-stress-test \
  --config config.yaml \
  --duration 300 \
  --concurrency 100 \
  --output results.json

The output includes:

{
  "avg_latency_ms": 124.3,
  "throughput_rps": 450.1,
  "error_rate": 0.023,
  "total_cost": 142.8
}

The objective function computes a composite score:

def objective(config):
    # Write config to file
    with open("config.yaml", "w") as f:
        yaml.dump(config, f)

    # Run stress test
    subprocess.run([
        "./run-stress-test",
        "--config", "config.yaml",
        "--duration", "300",
        "--concurrency", "100",
        "--output", "results.json"
    ], check=True)

    # Read results
    with open("results.json") as f:
        results = json.load(f)

    # Compute objective: minimize latency + 2 * error rate + 0.1 * cost
    score = (
        results["avg_latency_ms"]
        + 2 * results["error_rate"]
        + 0.1 * results["total_cost"]
    )
    return score

The hyperparameter space includes:

The optimization loop selects configurations to evaluate, based on the GP and acquisition function.

Critical Considerations

Summary

Bayesian Optimization is not a one-size-fits-all tool. It requires careful design of the objective function, precise definition of the hyperparameter space, and attention to the details of the acquisition and surrogate models. Its power lies in its ability to learn from each evaluation and guide future choices, but its success depends on avoiding common pitfalls: misaligned parameter types, poor scaling, noisy observations, and stale acquisition. With attention to these details, it becomes a robust engine for automating the tuning of complex systems.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.