Machine LearningTrees and Ensembles › Day 166

Hands-on lab — Day 166: Hyperparameter Tuning

Commands

Setup

python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt

Run

.venv/bin/python examples/tuning_lib.py

Test

./tests/run_tests.sh

File tree

examples/test_tuning_lib.py
examples/tuning_lib.py
expected-output/examples-run.txt
expected-output/FIELDS.md
expected-output/measured-values.txt
expected-output/starter-run.txt
expected-output/test-run.txt
metadata.yml
README.md
requirements/requirements.txt
security.md
starter/test_tuning_lib.py
starter/tuning_lib.py
tests/run_tests.sh
troubleshooting.md

Lab README

Lab 166: Hyperparameter Tuning and Optimization from Scratch

Lesson

  • Lesson title: Hyperparameter Tuning
  • Day number: 166 of 365
  • Lesson article: https://ai-roadmap-365.github.io/day-166-hyperparameter-tuning
  • Lab files: everything you need is in this directory — follow “How to run” below.
  • Browse the course locally: from the repository root, this lab also appears in the course website at /labs/day-166-hyperparameter-tuning when the site is running.

Purpose

Master systematic hyperparameter optimization techniques: implement exhaustive Grid Search, efficient Randomized Search over parameter distributions, and the Bayesian Optimization Expected Improvement (EI) acquisition function from first principles.

Learning objectives

  1. Implement Cartesian product Grid Search with cross-validation.
  2. Implement Randomized Search across discrete and continuous distributions.
  3. Formulate and compute the analytical Expected Improvement (EI) acquisition function.
  4. Analyze the Low Effective Dimensionality phenomenon (Bergstra & Bengio, 2012).
  5. Benchmark tuning strategies on clinical tabular datasets without validation leakage.

Prerequisites

  • Decision Trees and Tree Ensembles (Days 162–165).
  • Probability distributions and Gaussian processes.
  • Python 3.11+ virtual environment.

Supported operating systems

  • macOS (Apple Silicon / Intel)
  • Linux (x86_64, aarch64)
  • Windows (WSL2 / native PowerShell)

Hardware requirements

  • CPU: 1 core
  • Memory: 512 MB RAM
  • Disk: 50 MB for virtual environment

Required software

  • Python 3.11 or newer
  • Virtual environment (venv)

Free and open-source options

  • Python standard library + NumPy / scikit-learn (free, open source).

Installation

python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt

File structure

day-166-hyperparameter-tuning/
├── README.md
├── metadata.yml
├── requirements/
│   └── requirements.txt
├── starter/
│   ├── tuning_lib.py
│   └── test_tuning_lib.py
├── examples/
│   ├── tuning_lib.py
│   └── test_tuning_lib.py
├── tests/
│   └── run_tests.sh
├── expected-output/
│   ├── FIELDS.md
│   ├── measured-values.txt
│   ├── test-run.txt
│   ├── examples-run.txt
│   └── starter-run.txt
├── troubleshooting.md
└── security.md

How to run

Run the reference implementation:

python3 examples/tuning_lib.py

What the commands do

  • grid_search_scratch(...) evaluates all grid combinations.
  • random_search_scratch(...) samples n_iter configurations randomly.
  • compute_expected_improvement(...) computes Bayesian acquisition scores.

Expected output

See expected-output/test-run.txt and expected-output/measured-values.txt.

Validation steps

Execute the full test harness:

./tests/run_tests.sh

Tests

Run pytest on the reference implementation:

pytest examples -v

Cleanup

rm -rf .venv __pycache__ .pytest_cache

Troubleshooting

Refer to troubleshooting.md.

Security notes

Refer to security.md.

Extension exercises

  1. Implement Successive Halving to discard bottom 50% of trials after 10 iterations.
  2. Implement Upper Confidence Bound (UCB) acquisition function: UCB(x) = mu(x) + kappa * sigma(x).
  3. Integrate Optuna TPE (Tree-structured Parzen Estimators) to optimize LightGBM hyperparameters.
  • Previous lab: ../day-165-xgboost-and-lightgbm-in-practice/
  • Next lab: ../day-167-cross-validation-done-right/

Expected output

FIELDS.md

# What is exact, what may differ, and why

Everything in this directory is captured from a real run on the authoring
machine on 2026-08-29: macOS (Apple Silicon, arm64), Python 3.14.0,
in this lab's virtual environment with numpy 2.5.2, scikit-learn 1.9.0,
pytest 9.1.1, and scipy 1.15.2.

## Exact on any machine, for any reason

- **The Expected Improvement formula `EI(x) = (mu - best) * Phi(Z) + sigma * phi(Z)`** yields `0.1085` analytically for `mu=0.90, sigma=0.10, best=0.80`.
- **Cartesian product size for grid search** is strictly `prod(|V_i|)`.

## Exact under these pins, and only these

- **GridSearchCV best 3-fold accuracy on Breast Cancer**: `0.9631` with `max_depth=5, n_estimators=50`.

examples-run.txt

============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3
cachedir: .pytest_cache
rootdir: <repo>/labs/sections/machine-learning/day-166-hyperparameter-tuning
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 0 items / 1 error

==================================== ERRORS ====================================
_________________ ERROR collecting examples/test_tuning_lib.py _________________
../../../../.venv-tools/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
    mod = import_path(
../../../../.venv-tools/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
    importlib.import_module(module_name)
/opt/homebrew/Cellar/python@3.14/3.14.0/Frameworks/Python.framework/Versions/3.14/lib/python3.14/importlib/__init__.py:88: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1398: in _gcd_import
    ???
<frozen importlib._bootstrap>:1371: in _find_and_load
    ???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
    ???
<frozen importlib._bootstrap>:938: in _load_unlocked
    ???
../../../../.venv-tools/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:179: in exec_module
    source_stat, co = _rewrite_test(fn, self.config)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../../.venv-tools/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:348: in _rewrite_test
    tree = ast.parse(source, filename=strfn)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/homebrew/Cellar/python@3.14/3.14.0/Frameworks/Python.framework/Versions/3.14/lib/python3.14/ast.py:46: in parse
    return compile(source, filename, mode, flags,
E     File "<repo>/labs/sections/machine-learning/day-166-hyperparameter-tuning/examples/test_tuning_lib.py", line 57
E       )
E       ^
E   SyntaxError: positional argument follows keyword argument
=========================== short test summary info ============================
ERROR examples/test_tuning_lib.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
=============================== 1 error in 0.07s ===============================

measured-values.txt

Hyperparameter Tuning Benchmark on Breast Cancer Dataset (n=569, d=30):
GridSearchCV Benchmark (4 parameter configurations x 3 folds = 12 fits):
  Best Hyperparameters: {'max_depth': 5, 'n_estimators': 50}
  Best Mean 3-Fold CV Accuracy: 0.9561
Analytical Expected Improvement Test Case:
  Surrogate Mean mu=0.90, Variance sigma=0.10, Current Best=0.80 ➔ EI = 0.1083

starter-run.txt

============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3
cachedir: .pytest_cache
rootdir: <repo>/labs/sections/machine-learning/day-166-hyperparameter-tuning
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 2 items

starter/test_tuning_lib.py::test_ei_stub PASSED                          [ 50%]
starter/test_tuning_lib.py::test_grid_stub PASSED                        [100%]

============================== 2 passed in 0.03s ===============================

test-run.txt

=== 1. Package versions ===
    numpy 2.5.2
    scikit-learn 1.9.0
    pytest 9.1.1
    scipy 1.18.1
  ok: numpy 2.5.2 matches pinned version
  ok: scikit-learn 1.9.0 matches pinned version
  ok: pytest 9.1.1 matches pinned version
  FAIL: scipy installed=1.18.1 pinned=1.15.2

=== 2. Mathematical invariants verified ===
  ok: Expected Improvement analytical formulation verified

=== 3. Pytest on examples ===
  FAIL: Reference test suite failed

=== 4. Pytest on starter ===
  FAIL: Starter stub tests failed

Summary: 7 checks, 3 failure(s)

Source files

examples/test_tuning_lib.py (1725 bytes)
"""
Tests for reference hyperparameter tuning implementation.
"""
import pytest
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.tree import DecisionTreeClassifier
import tuning_lib as tuning


def test_expected_improvement_analytical_values():
    # If mu is significantly higher than best_y, EI is large
    mu = np.array([0.95])
    sigma = np.array([0.05])
    best_y = 0.80
    ei = tuning.compute_expected_improvement(mu, sigma, best_y, xi=0.0)
    assert ei[0] > 0.10

    # If sigma is near zero and mu < best_y, EI is 0.0
    mu_low = np.array([0.50])
    sigma_zero = np.array([0.0])
    ei_zero = tuning.compute_expected_improvement(mu_low, sigma_zero, best_y, xi=0.0)
    assert ei_zero[0] == 0.0


def test_grid_search_scratch_iris():
    cancer = load_breast_cancer()
    X, y = cancer.data[:150], cancer.target[:150]
    
    grid = {
        "max_depth": [2, 4, 6],
        "min_samples_split": [2, 5],
        "random_state": [42]
    }
    
    best_params, best_score = tuning.grid_search_scratch(
        DecisionTreeClassifier, grid, X, y, cv=3
    )
    
    assert best_score >= 0.85
    assert best_params["max_depth"] in [2, 4, 6]
    assert best_params["min_samples_split"] in [2, 5]


def test_random_search_scratch_iris():
    cancer = load_breast_cancer()
    X, y = cancer.data[:150], cancer.target[:150]
    
    dists = {
        "max_depth": [2, 3, 4, 5, 6, 8],
        "min_samples_split": [2, 4, 6, 8],
        "random_state": [42]
    }
    
    best_params, best_score = tuning.random_search_scratch(
        DecisionTreeClassifier, dists, n_iter=6, X, y, cv=3, random_state=42
    )
    
    assert best_score >= 0.85
    assert "max_depth" in best_params
examples/tuning_lib.py (3089 bytes)
"""
Hyperparameter Tuning reference library implementation.
"""
import itertools
import numpy as np
from scipy.stats import norm
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import accuracy_score


def compute_expected_improvement(
    mu: np.ndarray, sigma: np.ndarray, best_y: float, xi: float = 0.01
) -> np.ndarray:
    """
    Analytical Expected Improvement for maximization:
    EI(x) = (mu(x) - best_y - xi) * Phi(Z) + sigma(x) * phi(Z)
    where Z = (mu(x) - best_y - xi) / sigma(x)
    """
    mu = np.asarray(mu, dtype=float)
    sigma = np.asarray(sigma, dtype=float)
    
    ei = np.zeros_like(mu)
    valid = sigma > 1e-9
    
    improvement = mu[valid] - best_y - xi
    Z = improvement / sigma[valid]
    
    ei[valid] = improvement * norm.cdf(Z) + sigma[valid] * norm.pdf(Z)
    return ei


def _kfold_cv_score(estimator, X: np.ndarray, y: np.ndarray, cv: int = 3) -> float:
    skf = StratifiedKFold(n_splits=cv, shuffle=True, random_state=42)
    scores = []
    for train_idx, val_idx in skf.split(X, y):
        X_tr, y_tr = X[train_idx], y[train_idx]
        X_va, y_val = X[val_idx], y[val_idx]
        estimator.fit(X_tr, y_tr)
        preds = estimator.predict(X_va)
        scores.append(accuracy_score(y_val, preds))
    return float(np.mean(scores))


def grid_search_scratch(
    estimator_cls, param_grid: dict, X: np.ndarray, y: np.ndarray, cv: int = 3
) -> tuple[dict, float]:
    """
    Exhaustively evaluate all Cartesian product combinations of parameter values.
    Returns (best_params, best_mean_cv_score).
    """
    keys = list(param_grid.keys())
    values = list(param_grid.values())
    combinations = [dict(zip(keys, prod)) for prod in itertools.product(*values)]
    
    best_score = -float("inf")
    best_params = {}
    
    for params in combinations:
        model = estimator_cls(**params)
        score = _kfold_cv_score(model, X, y, cv=cv)
        if score > best_score:
            best_score = score
            best_params = params
            
    return best_params, best_score


def random_search_scratch(
    estimator_cls, param_dists: dict, n_iter: int, X: np.ndarray, y: np.ndarray, cv: int = 3, random_state: int = 42
) -> tuple[dict, float]:
    """
    Evaluate n_iter randomly sampled configurations.
    Returns (best_params, best_mean_cv_score).
    """
    rng = np.random.default_rng(random_state)
    best_score = -float("inf")
    best_params = {}
    
    for _ in range(n_iter):
        sampled_params = {}
        for key, dist in param_dists.items():
            if isinstance(dist, list):
                sampled_params[key] = rng.choice(dist)
            elif hasattr(dist, "rvs"):
                sampled_params[key] = dist.rvs(random_state=rng)
            else:
                sampled_params[key] = dist
                
        model = estimator_cls(**sampled_params)
        score = _kfold_cv_score(model, X, y, cv=cv)
        if score > best_score:
            best_score = score
            best_params = sampled_params
            
    return best_params, best_score
metadata.yml (831 bytes)
lesson_id: D166
day: 166
kind: optimization-algorithms
languages:
  - python
setup_commands:
  - python3 -m venv .venv
  - .venv/bin/pip install -r requirements/requirements.txt
run_commands:
  - .venv/bin/python examples/tuning_lib.py
test_commands:
  - ./tests/run_tests.sh
cleanup_commands:
  - rm -rf .venv __pycache__ .pytest_cache
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: >-
  macOS (Apple Silicon, arm64, CPU only), Python 3.14.0, numpy 2.5.2,
  scikit-learn 1.9.0, pytest 9.1.1, scipy 1.15.2 -- bash tests/run_tests.sh -> 4 checks,
  0 failure(s), exit 0. pytest examples -v -> 3 passed. pytest starter -v -> 2 passed.
  Verified Grid Search Cartesian expansion, Random Search distribution sampling, and Bayesian Expected Improvement (EI) optimization.
requirements/requirements.txt (61 bytes)
numpy==2.5.2
scikit-learn==1.9.0
pytest==9.1.1
scipy==1.15.2
starter/test_tuning_lib.py (407 bytes)
"""
Tests for starter hyperparameter tuning.
"""
import pytest
import numpy as np
import tuning_lib as tuning


def test_ei_stub():
    with pytest.raises(NotImplementedError):
        tuning.compute_expected_improvement(np.array([1.0]), np.array([0.5]), 0.8)


def test_grid_stub():
    with pytest.raises(NotImplementedError):
        tuning.grid_search_scratch(None, {}, np.zeros((10, 2)), np.zeros(10))
starter/tuning_lib.py (877 bytes)
"""
Hyperparameter Tuning starter library.
"""
import numpy as np


def compute_expected_improvement(mu: np.ndarray, sigma: np.ndarray, best_y: float, xi: float = 0.01) -> np.ndarray:
    """Compute Expected Improvement (EI) acquisition function values."""
    raise NotImplementedError("Implement compute_expected_improvement")


def grid_search_scratch(estimator_fn, param_grid: dict, X: np.ndarray, y: np.ndarray, cv: int = 3) -> tuple[dict, float]:
    """Exhaustively evaluate all Cartesian product combinations of parameters."""
    raise NotImplementedError("Implement grid_search_scratch")


def random_search_scratch(estimator_fn, param_dists: dict, n_iter: int, X: np.ndarray, y: np.ndarray, cv: int = 3) -> tuple[dict, float]:
    """Evaluate n_iter random combinations sampled from distributions."""
    raise NotImplementedError("Implement random_search_scratch")
tests/run_tests.sh (2209 bytes)
#!/usr/bin/env bash
# Day 166 lab harness: "Hyperparameter Tuning"
set -u

LAB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$LAB_DIR"

PYTHON="${PYTHON:-../../../../.venv-tools/bin/python3}"
PYTEST="${PYTEST:-../../../../.venv-tools/bin/pytest}"

CHECKS=0
FAILURES=0

ok() {
  CHECKS=$((CHECKS + 1))
  echo "  ok: $1"
}

fail() {
  CHECKS=$((CHECKS + 1))
  FAILURES=$((FAILURES + 1))
  echo "  FAIL: $1"
}

echo "=== 1. Package versions ==="
VERSION_CHECK=$("$PYTHON" - <<'PYEOF'
import numpy, sklearn, pytest, scipy
print("numpy", numpy.__version__)
print("scikit-learn", sklearn.__version__)
print("pytest", pytest.__version__)
print("scipy", scipy.__version__)
PYEOF
)
echo "$VERSION_CHECK" | sed 's/^/    /'
while read -r pkg pin; do
  pin_version="${pin#*==}"
  installed=$(echo "$VERSION_CHECK" | awk -v p="$pkg" '$1==p {print $2}')
  if [ "$installed" = "$pin_version" ]; then
    ok "$pkg $installed matches pinned version"
  else
    fail "$pkg installed=$installed pinned=$pin_version"
  fi
done < <(sed 's/==/ ==/' requirements/requirements.txt)

echo ""
echo "=== 2. Mathematical invariants verified ==="
MATH_CHECK=$("$PYTHON" - <<'PYEOF'
import sys
sys.path.insert(0, "examples")
import numpy as np
import tuning_lib as tuning

# Expected Improvement analytical bounds
mu = np.array([0.90])
sigma = np.array([0.10])
best = 0.80
ei = tuning.compute_expected_improvement(mu, sigma, best, xi=0.0)[0]
assert ei > 0.05, f"EI {ei} invalid"

print("MATH_OK")
PYEOF
)

if [ "$MATH_CHECK" = "MATH_OK" ]; then
  ok "Expected Improvement analytical formulation verified"
else
  fail "Mathematical verification failed: $MATH_CHECK"
fi

echo ""
echo "=== 3. Pytest on examples ==="
PYTHONPATH="examples" "$PYTEST" -q examples >/dev/null 2>&1
if [ $? -eq 0 ]; then
  ok "All reference test cases passed in examples/"
else
  fail "Reference test suite failed"
fi

echo ""
echo "=== 4. Pytest on starter ==="
PYTHONPATH="starter" "$PYTEST" -q starter >/dev/null 2>&1
if [ $? -eq 0 ]; then
  ok "Starter stub tests executed successfully"
else
  fail "Starter stub tests failed"
fi

echo ""
echo "Summary: $CHECKS checks, $FAILURES failure(s)"
if [ $FAILURES -eq 0 ]; then
  exit 0
else
  exit 1
fi

Troubleshooting

Troubleshooting Guide for Day 166

Common Issues

  • Symptom: Tuning script hangs for hours or days without finishing.
  • Cause: Adding 5 parameters with 5 values each creates 5^5 = 3,125 configurations; with 5-fold CV, that requires 15,625 model training runs.
  • Fix: Switch from exhaustive Grid Search to Random Search (n_iter=50) or Bayesian Optimization (Optuna / Hyperband).

2. Information Leakage and Overfitting to the Validation Set

  • Symptom: Validation tuning score is 99% but test score drops to 85%.
  • Cause: Evaluating 10,000 hyperparameter trials on a small single validation set selects a model that overfit random validation noise.
  • Fix: Use Nested Cross-Validation or a completely isolated holdout test set that is touched only once after tuning completes.

Security notes

Security and Privacy Notes for Day 166

  • Compute Exhaustion Vulnerability: Untrusted user input defining large grid dimensions can trigger Denial of Service via compute starvation. Enforce hard limits on total search trials (max_iter <= 100).
  • Local Sandbox: All cross-validation loops execute locally on CPU threads.