This whole module — and a good part of the course — has been running up a tab: alpha in regularization, C in logistic regression and SVMs, K in K-NN, max_depth in trees, learning rate and n_estimators in boosting, layers and dropout in networks… So far we've chosen them by eye or with manual loops like those of 06-05. This lesson closes the module by automating that search: you'll learn to distinguish parameters from hyperparameters, to use GridSearchCV and RandomizedSearchCV on the full MercaFresh churn pipeline, to understand Bayesian search, to avoid the trap of scoring with the same validation that picked the winner, and to manage your compute budget wisely.
Contents
- Parameters vs. hyperparameters
- Manual search and its limits
- GridSearchCV in detail
- Grid search on the full churn pipeline
- RandomizedSearchCV: when sampling is better
- Bayesian search: searching with memory
- Nested validation: don't score with the judge who chose
- Good search practices
- Closing the module: the full arsenal
Parameters vs. hyperparameters
The distinction underpins everything:
- Parameters: the algorithm learns them from the data during
fit. You don't choose them. - Hyperparameters: you set them before training; they configure how the algorithm learns.
| Model (lesson) | Parameters (learned by fit) |
Hyperparameters (chosen by you) |
|---|---|---|
| Linear regression (04-01) | Coefficients and intercept | — (that's why we started with it) |
| Ridge/Lasso (07-01) | Coefficients | alpha, l1_ratio |
| Logistic regression (04-02) | Coefficients | C, penalty |
| SVM (04-04) | Support vectors and their weights | C, kernel, gamma |
| K-NN (04-05) | — (it memorizes the train set) | n_neighbors (the K), weights |
| Tree (04-03) | The tree's splits | max_depth, min_samples_leaf |
| Random Forest (07-02) | All the trees | n_estimators, max_features |
| Gradient boosting (07-03) | The sequence of trees | learning_rate, n_estimators, max_depth |
| Neural network (04-07, 07-04) | Weights and biases | Number of layers and neurons, dropout, learning rate, batch size |
The mnemonic rule: if it appears in the constructor (Ridge(alpha=...)), it's a hyperparameter; if it appears as an attribute with a trailing underscore after training (ridge.coef_), it's a learned parameter. And the methodological principle that governs the lesson: parameters are fitted with the train set; hyperparameters are chosen with validation — never with the test set, which stays under lock and key for the final verdict (06-01).
Manual search and its limits
We've already done hyperparameter search three times without naming it: the validation curves over max_depth in 06-05, the alpha loop in 07-01 and the η/n_estimators combinations in 07-03. The manual method — loop, CV, jot down results — works, but scales terribly:
- Combinatorial explosion: with 4 hyperparameters and 5 values each you get 625 combinations; by hand, impossible.
- Interactions: the best
learning_ratedepends onn_estimators(we saw it in 07-03); exploring each knob separately misses the cross combinations. - Discipline slips: in hand-rolled loops it's easy to accidentally tune with test data, or to scale outside the CV (the leak of 06-01).
- Irreproducibility: "I tried stuff and kept this one" is not an auditable procedure.
sklearn packages the solution into two tools that do exactly what we were doing by hand, but exhaustively, in parallel and leak-free.
GridSearchCV in detail
GridSearchCV takes an estimator, a grid of values per hyperparameter, and a CV strategy; it trains and evaluates every combination with cross-validation and keeps the best one.
Its pieces:
param_grid: a dictionary{hyperparameter: list of values}. The Cartesian product defines the combinations.cv: the cross-validation strategy — for churn, theStratifiedKFoldof 06-03.scoring: the metric that decides (06-02):"f1","roc_auc","neg_root_mean_squared_error"… Choosing this metric well is choosing what "better" means; for MercaFresh's imbalanced churn, F1 or AUC, not accuracy.refit=True(the default): after the search, it automatically retrains the best combination on the whole train set — the resulting object is already a model ready to predict.n_jobs=-1: parallelizes across all cores; the combinations are independent of each other.
And its outputs:
best_params_: the winning combination.best_score_: its mean CV score.cv_results_: the complete table of the search (all combinations, means, deviations, timings) — turn it into a DataFrame and study it: it's worth more than the winner alone.best_estimator_: the retrained model (ifrefit=True).
Grid search on the full churn pipeline
The golden rule of 06-03 still applies: what you search over is the full pipeline, not the bare model, so the preprocessing is refitted inside each fold without leaks. The hyperparameters of a pipeline step are named with the step__parameter notation (double underscore) we met back then — which even lets you treat preprocessing decisions as just more hyperparameters.
import numpy as np
import pandas as pd
from sklearn.model_selection import StratifiedKFold, GridSearchCV, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
# MercaFresh churn dataset (RFM features, as in 07-02/07-03)
rng = np.random.default_rng(42)
n = 1000
recency = rng.gamma(2, 15, n)
frequency = rng.poisson(5, n) + 1
monetary = rng.gamma(3, 40, n)
tenure = rng.uniform(1, 60, n)
incidents = rng.poisson(0.5, n)
logits = 0.05 * recency - 0.4 * frequency + 0.6 * incidents - 0.01 * tenure - 0.5
y = (rng.random(n) < 1 / (1 + np.exp(-logits))).astype(int)
X = pd.DataFrame({"recency": recency, "frequency": frequency,
"monetary": monetary, "tenure": tenure,
"incidents": incidents})
# Test set carved out BEFORE any search (06-01)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42)
pipe = Pipeline([
("scaler", StandardScaler()),
("clf", LogisticRegression(max_iter=5000)),
])
param_grid = {
"clf__C": np.logspace(-3, 2, 6), # 0.001 ... 100, log scale (07-01)
"clf__penalty": ["l1", "l2"], # Lasso or Ridge on the logistic model
"clf__solver": ["liblinear"], # solver compatible with both
}
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
search = GridSearchCV(pipe, param_grid, cv=cv, scoring="f1",
n_jobs=-1, refit=True)
search.fit(X_train, y_train) # 6 x 2 = 12 combinations x 5 folds = 60 fits
print("Best combination:", search.best_params_)
print(f"Best F1 (CV): {search.best_score_:.3f}")
# The full table, to understand the landscape and not just the peak
results = pd.DataFrame(search.cv_results_)
print(results[["param_clf__C", "param_clf__penalty",
"mean_test_score", "std_test_score"]]
.sort_values("mean_test_score", ascending=False).head())
# Final verdict: the untouched test set, once
from sklearn.metrics import f1_score
print(f"Test F1: {f1_score(y_test, search.predict(X_test)):.3f}")Points worth rereading:
- The real cost is
combinations × foldstraining runs (60 here): the grid grows multiplicatively and the CV multiplies it again. Budget before launching. searchis used afterwards like a normal model (predict,predict_proba): thanks torefit, it carries inside the best pipeline retrained on the whole train set.- Always look at
std_test_score: a "winner" by 0.002 of mean with deviations of 0.03 is a technical tie; in that case pick the simplest/most regularized combination, not the first row of the table. - The test F1 usually lands somewhat below
best_score_— and that gap has a name and its own section further down.
RandomizedSearchCV: when sampling is better
The exhaustive grid dies of combinatorics: with the gradient boosting of 07-03 we'd want to explore learning_rate, max_iter, max_depth, min_samples_leaf, subsample… 5 values of each = 3,125 combinations × 5 folds = 15,625 training runs.
RandomizedSearchCV changes strategy: instead of trying everything, it samples n_iter combinations at random from distributions you define. Decisive advantages:
- Fixed budget: you decide how many training runs you pay for (
n_iter=50), no matter how big the space is. - Continuous distributions instead of lists:
loguniform(0.001, 0.3)for the learning rate explores the whole range, not 5 scattered points. - Better coverage of what matters: in practice only a few hyperparameters are influential; random sampling tries many different values of each hyperparameter, while the grid wastes budget repeating the same values of the influential one for every value of the irrelevant one.
from sklearn.model_selection import RandomizedSearchCV
from sklearn.ensemble import HistGradientBoostingClassifier
from scipy.stats import loguniform, randint
pipe_gb = Pipeline([("clf", HistGradientBoostingClassifier(random_state=42))])
param_dist = {
"clf__learning_rate": loguniform(0.005, 0.3), # continuous, log scale
"clf__max_iter": randint(100, 1000),
"clf__max_depth": randint(2, 8),
"clf__min_samples_leaf": randint(10, 60),
}
random_search = RandomizedSearchCV(pipe_gb, param_dist, n_iter=50, cv=cv,
scoring="f1", n_jobs=-1, random_state=42)
random_search.fit(X_train, y_train) # 50 x 5 = 250 fits, decided by you
print(random_search.best_params_, f"CV F1: {random_search.best_score_:.3f}")Rule of thumb: grid for small, discrete spaces (2-3 hyperparameters with few reasonable values, like the logistic example); randomized for everything else, and especially for boosting and networks.
Bayesian search: searching with memory
Grid and randomized share a naivety: each combination is chosen without looking at previous results. Bayesian search (or Bayesian optimization) is the natural evolution: it builds a probabilistic model of the "hyperparameters → score" function — yes, an ML model to optimize ML models — and uses it to decide which combination to try next, balancing exploiting the zones that look good and exploring the uncertain ones. It's the same belief-updating-with-evidence logic of Bayes' theorem (02-05), applied to the search.
On tight budgets (dozens of evaluations of an expensive model), it usually finds better combinations than pure chance. The reference library is Optuna (also scikit-optimize); its API integrates well with sklearn and adds extras like discarding mid-training the combinations that are going badly (pruning). We won't develop it here: conceptually you already have the essentials, and its syntax can be learned in an afternoon when you need it.
| Strategy | How it chooses | When to use it |
|---|---|---|
| Manual (06-05) | Your intuition | Initial exploration, learning |
| GridSearchCV | Every combination | Small, discrete spaces |
| RandomizedSearchCV | Random sampling on a budget | Large spaces; the default option |
| Bayesian (Optuna) | A model that learns from previous attempts | Expensive-to-train models, scarce budget |
Nested validation: don't score with the judge who chose
An important subtlety that already peeked out in the grid example: best_score_ is an optimistic estimate of real performance. Why? Because you tried many combinations on the same CV and kept the one that scored best on that particular CV: part of its edge is merit and part is luck with those folds. It's the same selection bias that in 06-01 led us to separate validation and test — resurfacing one level up.
The clean solution is nested validation (nested CV): an outer CV loop to estimate performance, and inside each outer fold, a complete inner search loop that chooses the hyperparameters. That way, the data doing the scoring never took part in the choosing:
from sklearn.model_selection import cross_val_score
# The whole search (inner loop) is treated as just another estimator
# and evaluated with an outer CV it never sees
nested_f1 = cross_val_score(search, X_train, y_train, cv=5, scoring="f1")
print(f"Honest (nested) F1: {nested_f1.mean():.3f} ± {nested_f1.std():.3f}")The cost multiplies (5 × 60 = 300 fits in our example), so in practice it's reserved for when the performance figure must be rigorous — comparing algorithms for a report, or estimating how the model will perform before committing to the business. For day-to-day work, this lesson's scheme is enough: search with CV on the train set and deliver the final verdict a single time on the untouched test set.
Good search practices
- Coarse to fine. First pass with wide ranges and few points (or a moderate
n_iter); look atcv_results_, locate the good zone and launch a second, refined pass around it. Two cheap searches beat one very expensive one. - Log scale for the multiplicative ones.
alpha,C,learning_rate,gammaact by orders of magnitude: usenp.logspace/loguniform. For the structural ones (max_depth,n_neighbors), linear scale. - Budget before launching. Compute
combinations × folds × time_per_fitand decide whether you can afford it; if not, shrink the grid, lower the folds (5 → 3) or switch to randomized. Time a single fit first with%timeitortime.perf_counter. - Search less where it matters less. Not every hyperparameter deserves a grid: in Random Forest,
n_estimatorsis set high and that's that (07-02); in boosting, early stopping picksn_estimatorsfor you (07-03) — remove them from the search space. - Fix
random_stateeverywhere (CV, models, search) so the search is reproducible and comparisons are fair. - Save
cv_results_. The full landscape says things the winner keeps quiet: which hyperparameters truly matter, where the model is robust and where fragile.
Closing the module: the full arsenal
This module answered the question we opened it with — "can I do even better?" — with five complementary techniques:
| Technique (lesson) | What it improves | When to apply it |
|---|---|---|
| L1/L2/EN regularization (07-01) | Tames the variance of linear models; L1 additionally selects features | Linear models with many or correlated features; always with scaling |
| Ensembles: bagging/RF, voting, stacking (07-02) | Cancels decorrelated errors; RF as a universal strong baseline | Almost always on tabular data: start here |
| Gradient boosting (07-03) | The performance ceiling on tabular data | When every metric point is worth money and you can pay for the tuning |
| Deep learning (07-04) | Learns hierarchical representations | Images, audio, text; tabular only with massive data |
| Hyperparameter optimization (07-05) | Squeezes any of the above systematically and leak-free | Always, with a budget proportional to what's at stake |
And the workflow that ties them together for a tabular problem like MercaFresh's churn: Dummy baseline (06-02) → simple interpretable model → Random Forest → gradient boosting with early stopping → hyperparameter search on the finalist → a single verdict on the test set.
Common Mistakes and Tips
- Searching hyperparameters using the test set. The capital sin. If the test set influences any decision — hyperparameters, features, threshold — it stops measuring generalization. Test set untouched until the end, a single evaluation.
- Passing the bare model instead of the pipeline. If you scale beforehand and outside the search, every CV fold sees statistics from the rest: the leak of 06-01 in its subtle version. Always search over the
Pipelinewith thestep__parameternotation. - Linear grids for multiplicative hyperparameters.
Cin[1, 2, 3, 4, 5]explores a tiny corner of the useful range;np.logspace(-3, 2, 6)explores five orders of magnitude at the same cost. - Taking
best_score_as the expected performance. It's optimistic due to selection bias; the honest number comes from the final test or from nested validation. - Crowning winners by differences within the noise. Compare
mean_test_scorealongsidestd_test_score; in a technical tie, the simpler model wins. - Tip: treat the search like a scientific experiment: hypotheses (reasoned ranges, not arbitrary ones), a reproducible procedure (fixed seeds), archived results (
cv_results_to CSV). Your three-months-from-now self — or the model's auditor — will thank you.
Exercises
- Grid on the churn K-NN. Build a
StandardScaler+KNeighborsClassifierpipeline and search withGridSearchCV(scoring="f1", the lesson's stratified CV) overclf__n_neighborsin[3, 5, 9, 15, 25, 41]andclf__weightsin["uniform", "distance"]. Reportbest_params_,best_score_and the test F1. How many training runs did the search execute? - Grid vs. randomized on the same budget. On the example's
HistGradientBoostingClassifier, compare: (a) aGridSearchCVwith 3 values oflearning_rate× 3 ofmax_depth(9 combinations) and (b) aRandomizedSearchCVwithn_iter=9over the example's continuous distributions. Samecv, samescoring. Repeat (b) with three differentrandom_statevalues. Who wins and what stability do you observe? - The size of the optimism. For the example's logistic-regression search, compute: (a)
best_score_, (b) the nested-validation F1 withcross_val_score(search, ...), and (c) the test F1. Order the three figures and explain each difference using the lesson's concepts.
Solutions
- Structure:
Pipeline([("scaler", StandardScaler()), ("clf", KNeighborsClassifier())])and the grid with theclf__notation. With these data, the winner tends to sit aroundn_neighborsbetween 15 and 41 withweights="distance"(the churn data is noisy and wide neighborhoods average better), with a CV F1 close to the logistic model's. Training runs: 6 × 2 = 12 combinations × 5 folds = 60 fits (plus 1 final one from therefit). The test F1 should land in the vicinity ofbest_score_, slightly below. - With only 2 well-bounded hyperparameters, the grid is competitive and sometimes wins; the randomized search comes very close and, depending on the seed, beats it — its 9 points cover learning-rate values the grid never even tries. Across seeds, the randomized
best_score_fluctuates (typically in the second-third decimal place): with such a lown_iter, the luck of which combinations get drawn matters. A double moral: in small spaces the grid is defensible; as soon as the space grows, the randomized search's continuous coverage wins — and with a largern_iter, its variance across seeds shrinks. - The expected order is
best_score_≥ nested F1 ≈ test F1. (a) > (b):best_score_carries the selection bias — the winning combination is a winner partly through luck with those specific folds, and the outer CV of the nested scheme, which played no part in the choice, discounts it. (b) ≈ (c): both are honest estimates on data used in no decision; they differ only by sampling noise (the test is a single split; the nested scheme averages five). If in your run (c) comes out above (a), that's a lesson too: with datasets this size, the noise between splits can exceed the biases we measure.
Conclusion
You've closed the circle module 6 opened: parameters are learned by fit, hyperparameters are chosen by a disciplined search — GridSearchCV for small spaces, RandomizedSearchCV with distributions and a budget for large ones, Bayesian search when each training run is expensive —, always over the full pipeline, always with CV on the train set, with nested validation as the referee when the figure must be beyond reproach and the test set untouched for the final verdict. With that, module 7 is complete: regularization to restrain, ensembles and boosting to combine, deep networks for unstructured data and systematic optimization to squeeze it all. The MercaFresh team finally has its best churn model, tuned and evaluated honestly; but a model that lives in a notebook retains no customer. The best model in the world is worth nothing if it doesn't reach production, and that is what module 8 is about: frameworks, deployment, monitoring and the ethical considerations of putting machine learning in front of real people.
Machine Learning Course
Module 1: Introduction to Machine Learning
- What is Machine Learning?
- History and evolution of Machine Learning
- Types of Machine Learning
- Applications of Machine Learning
- The Machine Learning project workflow
Module 2: Foundations of Statistics and Probability
- Basic statistics concepts
- Probability distributions
- Correlation and covariance
- Statistical inference
- Bayes' theorem
Module 3: Data Preprocessing
- Data cleaning
- Handling missing data
- Data transformation
- Encoding categorical variables
- Normalization and standardization
- Feature engineering
Module 4: Supervised Machine Learning Algorithms
- Linear regression
- Logistic regression
- Decision trees
- Support Vector Machines (SVM)
- K-Nearest Neighbors (K-NN)
- Naive Bayes
- Neural networks
Module 5: Unsupervised Machine Learning Algorithms
- Clustering: K-means
- Hierarchical clustering
- Principal Component Analysis (PCA)
- DBSCAN clustering
- Data visualization with t-SNE and UMAP
Module 6: Model Evaluation and Validation
- Data splitting: training, validation and test
- Evaluation metrics
- Cross-validation
- ROC curve and AUC
- Overfitting and underfitting
Module 7: Advanced Techniques and Optimization
- Regularization: Ridge, Lasso and Elastic Net
- Ensemble Learning
- Gradient Boosting
- Deep neural networks (Deep Learning)
- Hyperparameter optimization
Module 8: Model Implementation and Deployment
- Popular frameworks and libraries
- Deploying models to production
- Model maintenance and monitoring
- Ethical and privacy considerations
Module 9: Hands-On Projects
- Project 1: Housing price prediction
- Project 2: Image classification
- Project 3: Sentiment analysis on social media
- Project 4: Fraud detection
- Project 5: Customer segmentation
