Machine Learning › Trees and Ensembles › Day 166
Hands-on lab — Day 166: Hyperparameter Tuning
- ← Back to the Day 166 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/machine-learning/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-tuningwhen 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
- Implement Cartesian product Grid Search with cross-validation.
- Implement Randomized Search across discrete and continuous distributions.
- Formulate and compute the analytical Expected Improvement (EI) acquisition function.
- Analyze the Low Effective Dimensionality phenomenon (Bergstra & Bengio, 2012).
- 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(...)samplesn_iterconfigurations 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
- Implement Successive Halving to discard bottom 50% of trials after 10 iterations.
- Implement Upper Confidence Bound (UCB) acquisition function:
UCB(x) = mu(x) + kappa * sigma(x). - Integrate Optuna TPE (Tree-structured Parzen Estimators) to optimize LightGBM hyperparameters.
Navigation
- 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
1. Exponential Computational Explosion in Grid Search
- Symptom: Tuning script hangs for hours or days without finishing.
- Cause: Adding 5 parameters with 5 values each creates
5^5 = 3,125configurations; with 5-fold CV, that requires15,625model 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.