Model Selection And Hyperparameter Tuning
Grid Search & Random Search
A distinction worth making precise before tuning anything.
Jr Codex ML Notes
Level: Intermediate–Advanced Prerequisites: Chapter 1: Cross-Validation Strategies Time to complete: ~25 minutes
Table of Contents
- Hyperparameters vs Parameters
- Manual Tuning's Limits
- Grid Search
- The Combinatorial Explosion Problem
- Random Search
- Why Random Search Often Outperforms Grid Search
- A Complete Tuning Workflow
- Summary & Next Steps
1. Hyperparameters vs Parameters
A distinction worth making precise before tuning anything.
Parameters Hyperparameters
───────────────────────────── ─────────────────────────────
LEARNED from data during SET before training begins
training (Module 3, Ch.1's — control HOW the learning
coefficients, Module 4, Ch.3's happens
tree splits)
e.g. linear regression's β e.g. Ridge's alpha (Module
coefficients 3, Ch.2), a tree's
max_depth (Module 4, Ch.3),
K in KNN (Module 4, Ch.2)
───────────────────────────── ─────────────────────────────
Every algorithm from Modules 3-6 has hyperparameters — this module is specifically about finding good values for them systematically, rather than the ad-hoc guessing used in earlier chapters' examples.
2. Manual Tuning's Limits
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
# Manually trying a FEW combinations — tedious, unsystematic, easy to miss the best one
for n_estimators in [50, 100, 200]:
for max_depth in [5, 10, None]:
model = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth, random_state=42)
scores = cross_val_score(model, X_train, y_train, cv=5)
print(f"n_estimators={n_estimators}, max_depth={max_depth}: {scores.mean():.3f}")Why This Doesn't Scale
─────────────────────────────────────────
With just 2 hyperparameters and 3 values each, that's already
9 combinations to manually write and track. Real models often
have 5+ tunable hyperparameters (Module 5, Ch.3's XGBoost) —
manual nested loops become unwieldy and error-prone FAST.
─────────────────────────────────────────
3. Grid Search
Automates exactly the nested-loop pattern above — define a grid of values for each hyperparameter, and exhaustively try every combination.
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier
param_grid = {
"n_estimators": [50, 100, 200],
"max_depth": [5, 10, None],
"min_samples_leaf": [1, 5, 10],
}
grid_search = GridSearchCV(
RandomForestClassifier(random_state=42),
param_grid,
cv=5, # Chapter 1's cross-validation, used to evaluate EACH combination
scoring="f1", # Module 4, Ch.6's metrics — choose based on the actual problem
n_jobs=-1, # use ALL available CPU cores in parallel
)
grid_search.fit(X_train, y_train)
print(f"Best parameters: {grid_search.best_params_}")
print(f"Best cross-validation score: {grid_search.best_score_:.3f}")
best_model = grid_search.best_estimator_ # the REFIT model, using the best combination foundWhat GridSearchCV Actually Does
─────────────────────────────────────────
For EVERY combination in the grid (3×3×3 = 27 combinations here):
Run FULL cross-validation (Chapter 1) using that combination
Record the average CV score
Pick the combination with the BEST average score, then REFIT
a final model on ALL training data using those best
hyperparameters.
─────────────────────────────────────────
4. The Combinatorial Explosion Problem
Grid search's exhaustive nature is exactly its weakness — the number of combinations grows multiplicatively, not additively, with each new hyperparameter.
# 3 hyperparameters, 3 values each: 3×3×3 = 27 combinations — manageable
# 5 hyperparameters, 5 values each: 5^5 = 3,125 combinations — expensive
# 6 hyperparameters, 10 values each: 10^6 = 1,000,000 combinations — infeasible!
n_hyperparameters = 6
n_values_each = 10
total_combinations = n_values_each ** n_hyperparameters
print(f"Total combinations: {total_combinations:,}") # 1,000,000
# Each combination requires a FULL cross-validation run (Chapter 1) —
# with 5-fold CV, that's 5,000,000 total model trainings!Directly Connects to the AI Notes' Search Chapter
─────────────────────────────────────────
This is EXACTLY the same combinatorial explosion the AI
Notes' search algorithms confront — an exhaustive search over
a large space becomes intractable as the space grows.
Grid search is essentially BREADTH-FIRST search (AI Notes,
Module 2, Ch.2) over the hyperparameter space — thorough,
but exponentially expensive.
─────────────────────────────────────────
5. Random Search
Instead of trying every combination, random search samples a fixed number of random combinations from the hyperparameter space — trading exhaustiveness for tractability.
from sklearn.model_selection import RandomizedSearchCV
from sklearn.ensemble import RandomForestClassifier
from scipy.stats import randint
param_distributions = {
"n_estimators": randint(50, 500), # a RANGE, not a fixed list
"max_depth": randint(3, 20),
"min_samples_leaf": randint(1, 20),
}
random_search = RandomizedSearchCV(
RandomForestClassifier(random_state=42),
param_distributions,
n_iter=50, # try only 50 RANDOM combinations, regardless of the grid's true size
cv=5,
scoring="f1",
random_state=42,
n_jobs=-1,
)
random_search.fit(X_train, y_train)
print(f"Best parameters: {random_search.best_params_}")
print(f"Best score: {random_search.best_score_:.3f}")Grid Search vs Random Search
─────────────────────────────────────────
Grid Search: tries EVERY combination in a FIXED, discrete
grid — guaranteed to find the best combo
WITHIN that grid, but the grid itself may
be too coarse, and cost grows exponentially
Random Search: samples a FIXED NUMBER of combinations
from a (possibly CONTINUOUS) range —
cost is controlled directly by n_iter,
regardless of how many hyperparameters
or values are involved
─────────────────────────────────────────
6. Why Random Search Often Outperforms Grid Search
A genuinely counter-intuitive, well-established result: for a fixed compute budget, random search frequently finds better hyperparameters than grid search.
The Key Insight — Not Every Hyperparameter Matters Equally
─────────────────────────────────────────
In practice, often only ONE OR TWO hyperparameters have a
LARGE effect on performance — the rest matter much less.
Grid search WASTES effort testing every combination of the
UNIMPORTANT hyperparameters, at EVERY value of the important
ones — effectively duplicating the SAME few important-
hyperparameter values repeatedly.
Random search, by SAMPLING continuously, tries MANY MORE
DISTINCT values for the truly important hyperparameters,
within the SAME total number of trials.
─────────────────────────────────────────
Visualizing Why (2 Hyperparameters: One Important, One Not)
─────────────────────────────────────────
Grid Search (9 trials, 3×3 grid): Random Search (9 trials):
● ● ● ← only 3 DISTINCT ● ● ●
● ● ● values tried for ● ●
● ● ● the IMPORTANT axis ● ● ●
(important) (important axis: 9
DISTINCT values tried!)
─────────────────────────────────────────
Practical guidance: for a large hyperparameter space or limited compute budget, prefer random search over grid search — reserve grid search for a FINAL, narrow refinement once random search (or domain knowledge) has already identified a promising region.
7. A Complete Tuning Workflow
from sklearn.model_selection import train_test_split, RandomizedSearchCV, GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from scipy.stats import randint
# 1. Split off a FINAL test set — never touched during tuning (Module 1, Ch.5)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)
# 2. BROAD random search FIRST — explore the space efficiently
broad_search = RandomizedSearchCV(
RandomForestClassifier(random_state=42),
{"n_estimators": randint(50, 500), "max_depth": randint(3, 30), "min_samples_leaf": randint(1, 20)},
n_iter=100, cv=5, random_state=42, n_jobs=-1,
)
broad_search.fit(X_train, y_train)
print(f"Promising region found: {broad_search.best_params_}")
# 3. NARROW grid search around the promising region found — final refinement
narrow_grid = {
"n_estimators": [broad_search.best_params_["n_estimators"] - 20,
broad_search.best_params_["n_estimators"],
broad_search.best_params_["n_estimators"] + 20],
"max_depth": [broad_search.best_params_["max_depth"] - 2,
broad_search.best_params_["max_depth"],
broad_search.best_params_["max_depth"] + 2],
}
final_search = GridSearchCV(RandomForestClassifier(random_state=42), narrow_grid, cv=5, n_jobs=-1)
final_search.fit(X_train, y_train)
# 4. Evaluate the FINAL model on the untouched test set — ONCE
final_model = final_search.best_estimator_
final_test_score = final_model.score(X_test, y_test)
print(f"Final, honest test score: {final_test_score:.3f}")8. Summary & Next Steps
Key Takeaways
- Hyperparameters are set before training and control how learning happens (unlike parameters, which are learned from data) — this entire module is about finding good hyperparameter values systematically.
- Grid search exhaustively tries every combination in a defined grid, guaranteed to find the best within that grid, but its cost grows exponentially with each added hyperparameter.
- Random search samples a fixed number of random combinations, controlling cost directly and often outperforming grid search on a fixed budget, since it explores more distinct values of the truly important hyperparameters.
- A practical workflow combines both: broad random search to find a promising region, then a narrow grid search to refine within it, evaluating the final model on an untouched test set exactly once.
Concept Check
- Why does grid search's cost grow exponentially rather than linearly as more hyperparameters are added?
- Why can random search outperform grid search on a fixed compute budget, even though it doesn't try every combination?
- In the combined workflow, why does the narrow grid search happen after the broad random search, not before?
Next Chapter
→ Chapter 3: Bayesian Optimization & Advanced Tuning
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index