Skip to content

RandomizedSearchCV for Large Parameter Spaces

What you’ll learn

  • why random sampling beats a grid at the same budget, with the geometric argument
  • the probability formula behind “59 draws”, derived
  • sampling from distributions rather than lists, and which distribution to use
  • a head-to-head on real data where random search wins with identical cost
  • when a grid is still the right choice
  • successive halving, which beats both when fits are expensive

Intuition

A grid search spends its budget on the product of the hyperparameter values. With 6 values of CC and 6 of gammagamma you get 36 fits — but only 6 distinct values of each. If gammagamma turns out not to matter, you paid 36 fits to try 6 values of the one parameter that did.

Random search samples each combination independently. Thirty-six draws give you 36 distinct values of every hyperparameter. If only one matters, you explored it six times more thoroughly for the same money.

figureTwenty-five fits eachmatplotlib
Two panels, each with 25 sampled points. The grid panel shows a regular five by five lattice with only five distinct horizontal positions; the random panel shows 25 scattered points with 25 distinct horizontal positions. A green curve along the bottom marks where the important parameter matters.Two panels, each with 25 sampled points. The grid panel shows a regular five by five lattice with only five distinct horizontal positions; the random panel shows 25 scattered points with 25 distinct horizontal positions. A green curve along the bottom marks where the important parameter matters.
The green curve is the score as a function of the parameter that matters; the vertical axis is a parameter that does not. The grid reaches 0.67 of the useful peak; random search reaches 0.97.

Reading the plot

  1. Both panels cost exactly 25 fits. This is not a more-compute argument.
  2. The grid tries 5 distinct values of the useful parameter. The other 20 fits are repeats along an axis that changes nothing.
  3. Random search tries 25 distinct values. Its best landed at 0.97 of the peak against the grid’s 0.67.
  4. The grid’s failure is structural, not unlucky. Any grid wastes resolution on parameters that do not matter, and you rarely know in advance which those are.

The math

Suppose the top 5% of the hyperparameter space would be “good enough”. A single random draw misses it with probability 0.95. Draws are independent, so nn draws all miss with probability 0.95n0.95^n, and:

P(at least one good draw)=1(1p)nP(\text{at least one good draw}) = 1 - (1 - p)^{n}

Solving for the number of draws needed to reach confidence cc:

nlog(1c)log(1p)n \geq \frac{\log(1 - c)}{\log(1 - p)}

With p=0.05p = 0.05 and c=0.95c = 0.95:

nlog(0.05)log(0.95)=2.99570.0513=58.4    59n \geq \frac{\log(0.05)}{\log(0.95)} = \frac{-2.9957}{-0.0513} = 58.4 \;\Rightarrow\; \boxed{59}
DrawsP(hit the top 5%)P(\text{hit the top 5\%})
100.4013
200.6415
300.7854
590.9509
600.9539
1000.9941

The result does not depend on the number of hyperparameters. Fifty-nine draws give 95% confidence of landing in the top 5% whether you are tuning 2 hyperparameters or 20. A grid covering 20 hyperparameters at even 3 values each is 3203.53^{20} \approx 3.5 billion combinations.

figureDiminishing returns, quantifiedmatplotlib
Median quality percentile reached plotted against number of random draws, rising steeply then flattening, with markers at the top-5% line and at 59 draws.Median quality percentile reached plotted against number of random draws, rising steeply then flattening, with markers at the top-5% line and at 59 draws.
The curve is steep to about 30 draws and flat after 60. Doubling from 60 to 120 moves the confidence from 0.954 to 0.998 — usually not worth twice the compute.

Worked example by hand

You have 6 hyperparameters and want 5 values of each.

Step 1 — the grid.

56=15,625 combinations×5 folds=78,125 fits5^6 = 15{,}625 \text{ combinations} \times 5 \text{ folds} = 78{,}125 \text{ fits}

At 2 seconds per fit that is 43 hours.

Step 2 — random search at 60 draws.

60×5=300 fits=10 minutes60 \times 5 = 300 \text{ fits} = 10 \text{ minutes}

Step 3 — what each buys. The grid guarantees the best point in the grid. Random search gives 95% confidence of landing in the top 5% of the continuous space — including values no grid contained.

Step 4 — the ratio. 260 times less compute, for a probabilistic rather than exhaustive guarantee. On any real problem where the score surface is smooth, that is the better trade.

Sampling from distributions

Random search’s real advantage over a grid is not randomness, it is continuity. Give it a distribution instead of a list and it can propose values no grid contained:

distributions.py
from scipy.stats import loguniform, randint, uniform
 
param_distributions = {
    # multiplicative parameters: log-uniform, so each decade is equally likely
    "svc__C": loguniform(1e-2, 1e3),
    "svc__gamma": loguniform(1e-4, 1e1),
    # counts: integer uniform
    "rf__n_estimators": randint(50, 500),
    "rf__max_depth": randint(3, 30),
    # genuinely linear parameters: plain uniform
    "rf__max_features": uniform(0.1, 0.8),      # loc=0.1, scale=0.8 -> [0.1, 0.9]
    # categorical: a plain list still works
    "svc__kernel": ["rbf", "poly", "sigmoid"],
}
distributions.py
from scipy.stats import loguniform, randint, uniform
 
param_distributions = {
    # multiplicative parameters: log-uniform, so each decade is equally likely
    "svc__C": loguniform(1e-2, 1e3),
    "svc__gamma": loguniform(1e-4, 1e1),
    # counts: integer uniform
    "rf__n_estimators": randint(50, 500),
    "rf__max_depth": randint(3, 30),
    # genuinely linear parameters: plain uniform
    "rf__max_features": uniform(0.1, 0.8),      # loc=0.1, scale=0.8 -> [0.1, 0.9]
    # categorical: a plain list still works
    "svc__kernel": ["rbf", "poly", "sigmoid"],
}
Parameter typeDistributionWhy
Regularisation, learning rate, gammaloguniform(low, high)loguniform(low, high)Acts multiplicatively; 0.001→0.01 matters as much as 0.1→1
Counts (n_estimatorsn_estimators, max_depthmax_depth)randint(low, high)randint(low, high)Integers, and roughly linear in effect
Proportions (max_featuresmax_features, subsamplesubsample)uniform(loc, scale)uniform(loc, scale)Bounded and linear
Choices (kernelkernel, solversolver)A plain listNothing to interpolate

Head to head

Same model, same data, same budget of 36 combinations:

head_to_head.py
import numpy as np
from scipy.stats import loguniform
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
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 = GridSearchCV(
    pipe,
    {"svc__C": np.logspace(-2, 3, 6), "svc__gamma": np.logspace(-4, 1, 6)},
    cv=5, n_jobs=-1,
).fit(X, y)
 
random = RandomizedSearchCV(
    pipe,
    {"svc__C": loguniform(1e-2, 1e3), "svc__gamma": loguniform(1e-4, 1e1)},
    n_iter=36, cv=5, random_state=0, n_jobs=-1,
).fit(X, y)
 
print(f"grid    {grid.best_score_:.4f}  {grid.best_params_}")
# grid    0.9789  {'svc__C': 10.0, 'svc__gamma': 0.01}
print(f"random  {random.best_score_:.4f}  "
      f"{ {k: round(v, 5) for k, v in random.best_params_.items()} }")
# random  0.9807  {'svc__C': 7.09551, 'svc__gamma': 0.0156}
head_to_head.py
import numpy as np
from scipy.stats import loguniform
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
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 = GridSearchCV(
    pipe,
    {"svc__C": np.logspace(-2, 3, 6), "svc__gamma": np.logspace(-4, 1, 6)},
    cv=5, n_jobs=-1,
).fit(X, y)
 
random = RandomizedSearchCV(
    pipe,
    {"svc__C": loguniform(1e-2, 1e3), "svc__gamma": loguniform(1e-4, 1e1)},
    n_iter=36, cv=5, random_state=0, n_jobs=-1,
).fit(X, y)
 
print(f"grid    {grid.best_score_:.4f}  {grid.best_params_}")
# grid    0.9789  {'svc__C': 10.0, 'svc__gamma': 0.01}
print(f"random  {random.best_score_:.4f}  "
      f"{ {k: round(v, 5) for k, v in random.best_params_.items()} }")
# random  0.9807  {'svc__C': 7.09551, 'svc__gamma': 0.0156}

Random search wins, at identical cost. Not because randomness is magic, but because it found C=7.096C = 7.096 and γ=0.0156\gamma = 0.0156 — values that were not on the grid and could not have been. The grid’s best cell was the nearest lattice point to a better answer sitting between the lines.

This is a single comparison and would not always go this way. The general claim is weaker and still useful: at equal budget, random search is at least competitive, and its advantage grows with the number of hyperparameters.

See it move

Both searchers get the same budget on the same hidden score surface. Watch which one finds the bright region first.

sketch Grid against random on the same budget p5.js
Two identical score surfaces, one probed by a regular grid and one by random draws, with the same number of evaluations. The running best is tracked beneath each.

When to prefer which

SituationUse
1–2 hyperparameters, few values eachGrid — exhaustive and cheap
3+ hyperparametersRandom — the product explodes
Continuous hyperparametersRandom — a grid cannot express between-values
You need reproducible exhaustive coverageGrid
Fixed compute budgetRandomn_itern_iter is the budget
Refining after a coarse searchGrid, narrowed around the winner
Fits are very expensiveSuccessive halving or Bayesian optimisation

The standard workflow uses both: a wide random search to find the promising region, then a small grid around the winner to refine it.

Successive halving

When each fit is expensive, most of the budget goes on combinations that were obviously bad after a tenth of the data:

halving.py
from sklearn.experimental import enable_halving_search_cv  # noqa: F401
from sklearn.model_selection import HalvingRandomSearchCV
 
search = HalvingRandomSearchCV(
    pipe, param_distributions,
    factor=3,             # keep the best third at each round
    resource="n_samples", # the resource being increased is training rows
    random_state=0, n_jobs=-1,
).fit(X, y)
halving.py
from sklearn.experimental import enable_halving_search_cv  # noqa: F401
from sklearn.model_selection import HalvingRandomSearchCV
 
search = HalvingRandomSearchCV(
    pipe, param_distributions,
    factor=3,             # keep the best third at each round
    resource="n_samples", # the resource being increased is training rows
    random_state=0, n_jobs=-1,
).fit(X, y)

Round 1 evaluates many candidates on a small subset; the best third survive to round 2 with three times the data, and so on. Most candidates are eliminated cheaply, and the survivors get evaluated properly. Note the enable_halving_search_cvenable_halving_search_cv import — the estimator is still experimental.

Picking a search strategy

diagram Diagram mermaid

The edge check in that flow is the step people skip. A random search whose best CC is the largest value it sampled has not found an optimum, it has found the boundary of the box you drew — and the fix is to redraw the box, not to accept the number.

Pitfalls

quizCheck yourself
  1. At the same budget of 25 fits, why does random search often beat a grid?

    Show answer

    B — A 5x5 grid tries only 5 distinct values of each parameter, while 25 random draws try 25 distinct values of each — and usually only one or two parameters matter — A grid spends its budget on the product of the values. If one parameter is irrelevant, every fit that only varies it is wasted resolution.

  2. How many random draws give 95% confidence of landing in the top 5% of the space?

    Show answer

    B — 59 — n >= log(0.05)/log(0.95) = 58.4, so 59. Remarkably, the answer does not depend on the dimensionality of the space at all.

  3. Which distribution should you use for the regularisation strength C?

    Show answer

    B — loguniform(1e-3, 1e3), because C acts multiplicatively and each decade deserves equal weight — uniform over that range puts 99.9% of its mass above 1, so the heavily regularised end is essentially never sampled.

  4. In the head-to-head, random search found C = 7.096 while the grid's best was C = 10. What does that show?

    Show answer

    B — A grid can only return lattice points; the better value lay between the lines and only a continuous distribution could propose it — The grid offered 0.01, 0.1, 1, 10, 100, 1000. Nothing it could have returned was closer to 7.096 than 10. Continuity, not randomness, is the advantage.

🧪 Try It Yourself

Exercise 1 – Grid size against search budget

Exercise 2 – Derive the 59

Exercise 3 – Sample from distributions

Exercise 4 – Run the head-to-head

Exercise 5 – Choose the strategy

Recap

  • A 5×55\times5 grid tries 5 distinct values per parameter; 25 random draws try 25. When only one parameter matters, that is five times the resolution where it counts.
  • P(hit top p)=1(1p)nP(\text{hit top } p) = 1 - (1-p)^n, so 59 draws give 95% confidence of the top 5% — independent of how many hyperparameters there are.
  • Sample multiplicative parameters from loguniformloguniform, counts from randintrandint, proportions from uniformuniform, choices from a list.
  • Head to head at 36 combinations: grid 0.9789, random 0.9807 — because random could propose C=7.096C = 7.096 and the grid could not.
  • Grid for one or two discrete hyperparameters; random for three or more or anything continuous; successive halving when fits are expensive.
  • best_score_best_score_ is still optimistic. Nested CV or a held-out test set remains the honest number.

Exercise 6 – How many draws do you actually need?

Next

Continue to The ML Pipeline - Automating the Workflow — everything in this phase assumed a pipeline; this page makes it the reproducible unit you tune, save and ship.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did