Machine Learning course menu
Hyperparameter Tuning: GridSearchCV and RandomizedSearchCV
Tune machine learning models in scikit-learn: parameters vs hyperparameters, grid search and randomized search with cross-validation, reading cv_results_, searching on a log scale, nested cross-validation, and avoiding overfitting the validation data.
On this page
Parameters are what a model learns from data (the coefficients of a linear regression, the splits of a tree). Hyperparameters are settings you choose before training: the depth of a tree, the number of neighbours in KNN, the regularisation strength C. Tuning means finding hyperparameters that give the best performance on unseen data.
Grid search#
GridSearchCV tries every combination in a grid and scores each with cross-validation:
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV, train_test_split
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0, stratify=y)
grid = {"max_depth": [3, 6, None], "min_samples_leaf": [1, 5]}
search = GridSearchCV(RandomForestClassifier(n_estimators=150, random_state=0), grid, cv=5, n_jobs=-1)
search.fit(X_train, y_train)
print("best settings:", search.best_params_)
print("best cross-validated accuracy:", round(search.best_score_, 3))
print("test accuracy of the refitted best model:", round(search.score(X_test, y_test), 3))best settings: {'max_depth': 6, 'min_samples_leaf': 1}
best cross-validated accuracy: 0.96
test accuracy of the refitted best model: 0.944That grid has 3 × 2 = 6 combinations, each trained 5 times: 30 fits. After the search, GridSearchCV refits the best combination on all the training data, so search can be used directly as a model.
Reading the results#
cv_results_ holds the score of every combination, which shows how sensitive the model is to each setting:
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV
X, y = load_breast_cancer(return_X_y=True)
grid = {"max_depth": [3, 6, None], "min_samples_leaf": [1, 5]}
search = GridSearchCV(RandomForestClassifier(n_estimators=150, random_state=0), grid, cv=5, n_jobs=-1).fit(X, y)
results = pd.DataFrame(search.cv_results_)[["param_max_depth", "param_min_samples_leaf", "mean_test_score", "std_test_score"]]
print(results.sort_values("mean_test_score", ascending=False).round(3).to_string(index=False))param_max_depth param_min_samples_leaf mean_test_score std_test_score
6 1 0.963 0.021
None 1 0.960 0.024
6 5 0.958 0.024
None 5 0.958 0.024
3 1 0.953 0.025
3 5 0.953 0.025When the top scores are all within one standard deviation of each other, the setting barely matters; prefer the simpler or faster option.
Searching on a log scale#
Strength-like hyperparameters (C, alpha, gamma, learning rates) matter by order of magnitude, so search them on a log scale:
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
X, y = load_breast_cancer(return_X_y=True)
pipe = make_pipeline(StandardScaler(), SVC())
grid = {"svc__C": np.logspace(-2, 2, 5), "svc__gamma": np.logspace(-3, 0, 4)}
print("C values:", grid["svc__C"])
search = GridSearchCV(pipe, grid, cv=5, n_jobs=-1).fit(X, y)
print({k: float(v) for k, v in search.best_params_.items()}, round(search.best_score_, 3))C values: [1.e-02 1.e-01 1.e+00 1.e+01 1.e+02]
{'svc__C': 10.0, 'svc__gamma': 0.01} 0.979np.logspace(-2, 2, 5) gives 0.01, 0.1, 1, 10, 100. Pipeline step parameters use step__name (here make_pipeline named the SVM step svc).
Randomized search#
A grid grows very quickly: 5 values for each of 4 hyperparameters is 625 combinations. RandomizedSearchCV samples a fixed number of random combinations instead, usually finding nearly as good settings much faster, and it can sample from continuous distributions:
from scipy.stats import loguniform, randint
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.model_selection import RandomizedSearchCV
X, y = load_breast_cancer(return_X_y=True)
distributions = {
"learning_rate": loguniform(0.01, 0.3),
"max_leaf_nodes": randint(8, 64),
"l2_regularization": loguniform(1e-4, 1.0),
}
search = RandomizedSearchCV(HistGradientBoostingClassifier(random_state=0), distributions,
n_iter=15, cv=5, random_state=0, n_jobs=-1)
search.fit(X, y)
print({k: v if isinstance(v, int) else round(float(v), 4) for k, v in search.best_params_.items()})
print(round(search.best_score_, 3)){'l2_regularization': 0.0006, 'learning_rate': 0.1226, 'max_leaf_nodes': 36}
0.96815 combinations × 5 folds = 75 fits, no matter how many hyperparameters there are.
Don't overfit the validation data#
Trying many combinations and picking the best cross-validation score slightly overestimates how well the winner will do, because some of its advantage is luck. Two defences:
- Keep a test set outside the search and score the final model on it once (as in the first example).
- For a fully honest estimate on small data, use nested cross-validation: an outer cross-validation loop around the whole search.
from sklearn.datasets import load_wine
from sklearn.model_selection import GridSearchCV, cross_val_score
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X, y = load_wine(return_X_y=True)
inner = GridSearchCV(make_pipeline(StandardScaler(), KNeighborsClassifier()),
{"kneighborsclassifier__n_neighbors": [1, 3, 5, 9, 15]}, cv=5)
outer_scores = cross_val_score(inner, X, y, cv=5)
print(outer_scores.round(3), round(outer_scores.mean(), 3))[0.944 0.944 0.972 1. 0.943] 0.961Common mistakes#
Practice#
Practice exercise
Tune a DecisionTreeClassifier(random_state=0) on the wine dataset with GridSearchCV (5 folds) over max_depth in [2, 3, 4, 6, None] and min_samples_leaf in [1, 3, 5]. Print the best parameters, the best score (3 decimals) and how many fits were run.
Show hint
The number of fits is the number of combinations times the number of folds; len(search.cv_results_["params"]) gives the combinations.
Show solution
from sklearn.datasets import load_wine
from sklearn.model_selection import GridSearchCV
from sklearn.tree import DecisionTreeClassifier
X, y = load_wine(return_X_y=True)
grid = {"max_depth": [2, 3, 4, 6, None], "min_samples_leaf": [1, 3, 5]}
search = GridSearchCV(DecisionTreeClassifier(random_state=0), grid, cv=5).fit(X, y)
print(search.best_params_, round(search.best_score_, 3))
print(len(search.cv_results_["params"]) * 5, "fits"){'max_depth': 4, 'min_samples_leaf': 1} 0.916
75 fitsKey takeaway#
Finished this lesson?