Hyperparameter Search and Nested Cross-Validation
Master hyperparameter search and nested cross-validation. Compare grid and random search, diagnose selection bias, and evaluate tuning procedures honestly.
Machine Learning from ScratchAll chapters Hyperparameter Search and Nested Cross-ValidationFull chapter · Sections in this chapter1 / 10 ↓
Reading tools Definitions, links & notes
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Hyperparameter Search and Nested Cross-Validation
A search over 32 configurations returns one winner and a tempting number: its best validation score. That number helped choose the winner, so reporting it as though the configuration had been fixed in advance ignores selection. We need evaluation data that the search has not inspected.
A fitted model contains parameters learned from data, such as regression coefficients or neural-network weights. Many important choices are made before that fitting begins: the degree of a polynomial, the strength of a regularization penalty, the number of neighbors in a nearest-neighbor model, or the learning rate of an optimizer. These choices are hyperparameters. They define the model family, the fitting procedure, or both, and they are not updated by the inner optimization that fits the parameters.
A validation score is a noisy function of the hyperparameters, so choosing the best score is itself a learning procedure. If we use the same validation folds both to select hyperparameters and to report performance, the winner's score is usually too optimistic. The search has inspected many candidates and picked the one that happened to look best on those particular folds. Nested cross-validation separates the two jobs: an inner loop selects hyperparameters, and an outer loop estimates how well the entire selection-and-fit procedure generalizes.
In this chapter we compare grid search and random search under equal evaluation budgets, explain why selection bias appears, and build nested cross-validation from primitive loops. We then apply the ideas to a polynomial ridge regression model, assess it with outer folds, and compare equal-budget searches across repeated seeds. The chapter builds on the previous discussion of validation strategies and data leakage and on the classification metrics from the preceding chapter, where we saw that a score is only meaningful when we know exactly which data produced it.
Advantages
Recording the search space, fold rule, budget, and random seeds makes the selection procedure explicit, so it can be inspected and rerun with the same data and code. Instead of trying a few settings by hand and remembering the results, we define a search space, a validation rule, and a budget. Every candidate is evaluated on the same folds, and the selected configuration can be traced back to a specific score. This is valuable when several people collaborate on a model, when a pipeline must be rerun later, or when a regulator or reviewer asks how the final configuration was chosen.
Grid search is simple and exhaustive over a finite grid. When the search space is small and mostly discrete, it is easy to audit and easy to parallelize. Random search is often more efficient when only a few hyperparameters matter, because it spends less effort varying irrelevant dimensions. Nested cross-validation adds an honest estimate of the complete procedure, including the search step. It also exposes instability. If different outer folds select very different hyperparameters, we learn that the selection rule is not stable, which is useful information that a single best score hides.
Disadvantages
The computational cost of search is multiplicative. If we evaluate candidates with inner folds across outer folds, we fit roughly models before any final refit. Grid search grows exponentially with the number of tuned dimensions, and random search can waste draws in uninformative regions. Grid search finds the smallest observed validation loss on its finite grid, but that need not identify the lowest true risk over the wider search space. Random search may miss that configuration entirely. Neither method removes validation noise.
Nested cross-validation offers a way to assess selection when no untouched test set is available. If we already have a clean test set that was never used for development, a single outer holdout is often enough and is cheaper. Nested cross-validation is most useful when we need to use the same data for both selection and assessment, or when we want to compare complete model-selection procedures rather than one fixed model. Ordinary random-fold evaluation assumes independent observations representative of the target population. For time series, grouped data, or clustered observations, ordinary random folds can leak information and produce misleadingly optimistic estimates.
Search can overfit the inner validation folds, especially when the budget is large relative to the amount of validation data. A large search space also invites researcher degrees of freedom: if we keep expanding the space after seeing results, the reported score is no longer a clean estimate of a pre-specified procedure. Finally, preprocessing must be inside the cross-validation loop. Scaling, imputation, and data-driven feature selection learn from data and must be fitted within each training fold. A fixed polynomial feature map does not estimate population quantities, but its degree is selected inside the search, and the full pipeline keeps that selection and learned scaling within the correct splits.
Formula
It helps to view hyperparameter search as a second learning problem wrapped around the first. The inner problem fits model parameters for a fixed hyperparameter setting. The outer problem compares those settings using held-out data. The formulas below make both layers explicit, because the distinction between them is exactly what nested cross-validation is designed to protect.
Let the training data be
where is a feature vector and is a target. A learning algorithm takes a training set and a hyperparameter vector and returns a fitted model . The hyperparameters live in a search space , where:
- : a hyperparameter vector, an element of the search space
- : the search space, the set of all allowed hyperparameter values
- : the learning algorithm that maps a training set and to a fitted model
- : the fitted model returned by for hyperparameters
In a polynomial ridge pipeline, for example,
where:
- : the polynomial degree (a discrete hyperparameter)
- : the ridge penalty strength (a continuous hyperparameter)
The degree controls the feature map
where:
- : the polynomial feature map that sends a scalar input to a vector of its powers up to degree
- : the polynomial degree, the highest power included in the feature map
- : a scalar input feature value
Note that is a -dimensional vector for scalar input , with components , so the coefficient vector also has length , ensuring the inner product is well defined:
The dot product is a scalar prediction, where:
- : the -dimensional polynomial feature vector for observation , with components
- : the coefficient vector of length that weights each power
- : the coefficient associated with the -th power of the input
- : the feature value for observation (a scalar in this univariate polynomial case)
- : the polynomial degree, the highest power included in the sum
This feature map excludes a constant column. The implementation adds an unpenalized intercept and standardizes each power using training-fold means and positive scales . Write the standardized power as and the standardized feature vector as , where:
- : the standardized version of the -th power of input
- : the mean of computed on the training fold only
- : the positive scale of (typically its standard deviation) computed on the training fold only
- : the vector of standardized polynomial features for observation
For a constant column, use scale one. Ridge trades residual error against coefficient size on this standardized scale:
where:
- : the unpenalized intercept estimate, a function of the degree and penalty
- : the fitted (penalized) coefficient vector at penalty strength ; it implicitly depends on the standardized feature basis determined by
- : the coefficient vector being optimized over, of dimension (the same length as the standardized feature vector )
- : the number of training observations
- : the observed target for observation
- : the unpenalized intercept, not penalized by the ridge term
- : the polynomial features standardized using only the training observations
- : the model's predicted value for observation
- : the ridge penalty strength (controls the size of the shrinkage)
- : the squared L2 norm of the coefficients,
We will study linear regression and polynomial regression in detail later; for now, the important point is that changes the model family and changes the fitting objective, but neither is learned by the inner least-squares step. Changing changes the model family or the fitting rule. Note that the ridge objective above shows the unpenalized intercept alongside the penalized coefficients, so the prediction used in the loss includes both.
The quantity we would like to know is the generalization risk
where:
- : the expected loss of the particular model fitted on with setting , where is a hyperparameter vector in the search space
- : the training data, held fixed in this conditional risk
- : a new observation drawn from the data-generating distribution
- : the data-generating distribution
- : a loss such as squared error
- : the fitted model returned by the learning algorithm on training data with setting
- : a new input feature vector drawn from the data-generating distribution
- : the fitted model's prediction at the new input
and the squared-error loss is
where:
- : the squared-error loss for a true target and prediction
- : the observed target
- : the model's predicted value
The new observation must be independent of the training data. In this conditional expression, fitting randomness is fixed; the expectation averages over new evaluation observations. A procedure-level risk additionally averages this conditional risk over fresh training samples of a specified size and over any fitting randomness. These are different targets: cross-validation evaluates fits trained on fewer rows than a fit on all observations. Below, abbreviates the procedure-level risk at the training size being evaluated, with that size held fixed across candidates. We estimate risk with held-out data, once per candidate.
For -fold cross-validation, partition the training indices into folds . Let be the data with fold removed, and let
be the model fitted without fold . The cross-validation estimate averages the loss over all folds and, within each fold, over the held-out observations, so that every observation contributes once to the estimate:
where:
- : the -fold cross-validated estimate of the risk at hyperparameters
- : the number of folds
- : the index of the fold currently held out
- : the index set of observations in fold
- : the number of observations in fold
- : the model fitted on all folds except , with hyperparameters
- : the per-observation loss
- : the observed target for observation
- : the feature vector for observation
The inner sum averages loss within held-out fold , and the outer sum gives each fold equal weight. If fold sizes differ, this need not equal the pooled per-observation mean loss. The superscript is important: the model used to predict observations in never saw those observations during fitting. For a fixed , this is a standard validation estimate. The subtlety arises because we will compare many such estimates and keep the smallest one.
Parameters, hyperparameters, and search spaces
Parameters are the numbers that the fitting algorithm optimizes. In a linear model, they are the regression coefficients; in a neural network, they are the weights and biases. Hyperparameters are the numbers or structural choices that control how that optimization is set up. They include the polynomial degree, the ridge penalty, the number of folds in cross-validation, the learning rate of gradient descent, the number of trees in a forest, and the kernel width in a kernel method. The distinction is practical: parameters are estimated by minimizing a training objective, while hyperparameters are chosen by comparing validation performance or by using prior knowledge.
A useful way to keep the distinction straight is to ask what changes when a value changes:
- A parameter changes during fitting on a fixed training set. The fitting rule itself stays the same.
- A hyperparameter changes the fitting rule, the model family, or the search protocol. It is fixed while the parameters are optimized.
- A search space specifies which hyperparameter values are allowed. It is part of the procedure and must be fixed before the final evaluation.
The search space can contain discrete values, continuous intervals, categorical choices, or conditional choices. For a polynomial ridge model we might use the degree and the penalty:
where:
- : the polynomial degree (a discrete hyperparameter)
- : the ridge penalty strength (a continuous hyperparameter)
For a positive penalty spanning several orders of magnitude, a logarithmic search allocates resolution to relative changes in . Moving from to changes the penalty by a factor of ten, just as moving from to does. The interval occupies only 1% of the linear range . A coarse linear grid can therefore miss substantial relative differences among small positive penalties. Search spaces should be chosen before looking at the final test result, and they should be wide enough to include reasonable models but not so wide that the budget is spread too thin.
Grid search and random search under equal budgets
Once the search space is defined, the next question is how to spend the evaluation budget. Grid search evaluates a Cartesian product of candidate values. If the search space has hyperparameters and the -th hyperparameter has candidate values, the grid is
where:
- : the -th candidate value for the -th hyperparameter
- : the number of candidate values for the -th hyperparameter
- : the number of hyperparameters being tuned
- : the index selecting the candidate value along the -th hyperparameter dimension
Each element of is a single combination, chosen by fixing one candidate index per dimension. The number of candidates is the size of the Cartesian product, obtained by multiplying the number of candidate values along each hyperparameter dimension:
where:
- : the total number of grid candidates
- : the number of hyperparameters being tuned
- : the number of candidate values for the -th hyperparameter
- : the product over all hyperparameters
- : the index identifying the hyperparameter whose candidate count is multiplied
If we tune eight polynomial degrees and four ridge penalties, the budget is candidate configurations. Each configuration is evaluated on the same inner folds. Grid candidate enumeration is deterministic and exhaustive over the specified grid. Randomness in model fitting or fold construction still needs to be controlled. Its weakness is the product: five hyperparameters with ten values each require candidates, which is often impractical. The grid also evaluates every combination, including combinations whose differences may be irrelevant to performance.
Random search draws candidates independently from a distribution :
where:
- : the -th candidate hyperparameter vector drawn by the search
- : the search distribution over the hyperparameter space
- : the total number of candidates (the budget)
- : the index of the candidate draw
We then select
where:
- : the selected candidate index, using the first index to break an exact tie
- : the selected candidate vector at that index
- : the -fold cross-validation estimate of the risk for the -th candidate
For the positive ridge penalty in this chapter, we choose a log-uniform distribution because it gives equal probability to equal-width intervals on the logarithmic scale, including each full order of magnitude within its support. Random search does not enumerate combinations. Independent draws often produce many different values along continuous dimensions; discrete values can repeat, and distinctness is not guaranteed. If only one or two hyperparameters strongly affect performance, random search can cover their important ranges much faster than a grid that also varies irrelevant dimensions. It is stochastic, however, so the result depends on the seed, and two runs with the same budget can select different configurations.
An equal-budget comparison must hold the data, inner folds, loss, and number of candidate evaluations fixed. Grid search and random search should not be compared when one method receives more fits or different validation splits. For random search, we should report the seed or repeat the search across several seeds, because the candidate draw is part of the procedure. The fair question is not "which method found a lower score once?" but "how does each complete search procedure behave under the same budget and protocol?"
Selection bias and nested cross-validation
Let denote the finite set of evaluated candidates within the larger allowed space . After evaluating those candidates, we choose the one with the smallest validation loss:
The reported score of the winner is therefore
This is a minimum of noisy estimates. Even if all candidates have the same true risk, the one that happens to receive a favorable validation sample will look better than the others. To see the mechanism, write the validation error of candidate as
where:
- : the cross-validated estimate of the risk at hyperparameters
- : the true risk of the model fitted with hyperparameters
- : the estimation error on these particular folds (the gap between the validation estimate and the true risk)
The selected candidate has
where:
- : the validation estimate of the selected candidate's risk
- : the true risk of the selected candidate
- : the estimation error of the selected candidate on these folds, the gap between its validation estimate and its true risk (defined as evaluated at )
Selection favors candidates with favorable estimation errors. Under the unbiased-candidate assumptions stated below, the resulting optimism is nonnegative in expectation; this is not a claim about every realized score. Defining optimism as the expected amount by which the selected model's validation estimate falls below its true risk (equivalently, the expected overstatement of performance), we have
where:
- : the expected overstatement of performance caused by selecting the best candidate
- : the selected hyperparameters (a random quantity, depending on the validation folds)
- : the true risk of the model chosen by the search
- : the validation estimate of the selected model's risk
- : the expectation over the randomness of the validation folds
For a fixed finite candidate set, suppose each candidate estimate is unbiased for its fixed target risk: . The minimum is a concave function of its arguments. Applying Jensen's inequality to this concave function gives , and substituting the unbiasedness yields . Also, over the fixed finite candidate set , for every realization (since ), so . Combining these gives , so the expected optimism is nonnegative. Independence across candidates is unnecessary for this inequality. Actual CV estimates need not be unbiased for the risk of a model refitted on all data; the training-size difference is another source of discrepancy. The inequality is not a guarantee that a realized outer score exceeds its inner score.
A simple calculation makes the size of the effect concrete. Suppose two candidates have the same true risk, and their validation estimates are independent normal random variables with the same mean and positive standard deviation . The normality and independence assumptions are stated explicitly because the exact value below depends on them; normality is an illustrative model assumption rather than a general property of validation estimates, and shared folds can make candidate estimates correlated. Write the standardized estimates as , independently. The expected minimum is
where:
- : the value at which we evaluate whether one draw is the smaller
- : the standard normal density
- : the standard normal cumulative distribution function, the probability that a standard normal draw is at most ; its exceedance probability is
- the factor : the density that is the smaller of the two draws (one draw at , the other above it, in either order)
To evaluate the integral, use the identity . With , we have and for . This follows from the half-normal expectation
where:
- : the integration variable running over nonnegative values
- : the standard deviation of the zero-mean normal (equal to here, since )
- : the Gaussian kernel evaluated at , with the normalizing factor outside
- the factor : accounts for the two symmetric halves of the normal density when integrating only over
Therefore
Rescaling back to the candidates' units, the winner's validation score is about below the common mean even though neither candidate is better. With more candidates, the minimum is pushed even lower. More precisely, if candidates share the same true risk and their validation estimates have standard deviation , the expected minimum score is
where:
- : the common true risk shared by all candidates
- : the number of candidates sharing that common true risk
- : the standard deviation of each candidate's validation estimate
- : the standardized validation error of candidate , defined as , an independent standard normal draw
This expression is obtained by writing each estimate as with standard normal, so the minimum is and linearity of expectation gives the expression above. Since for under these assumptions, the expected minimum lies below the common risk .
This expression decreases as grows because the standardized minimum decreases with . This is the same "winner's curse" that can arise when we select the maximum of noisy measurements.
The winner's curse can be quantified without fitting a model. For independent candidate estimates with the same true risk, the expected best observed score is plus times the expected minimum of standard normal draws. The next cell approximates that expected minimum by numerical integration over , and the figure plots the negative value as expected selection optimism in standard-deviation units.
PythonIn [3]Expand
import numpy as np
from scipy.integrate import quad
from scipy.stats import norm
candidate_counts = np.arange(2, 31)
# Computation of expected minima is intentionally kept outside the plot cell.
expected_minima = np.array(
[
quad(
lambda z, m=m: z * m * norm.pdf(z) * norm.cdf(-z) ** (m - 1),
-8.0,
8.0,
limit=200,
)[0]
for m in candidate_counts
]
)
selection_optimism = -expected_minimaVisualizationOut [4]Expand

Nested cross-validation addresses this by adding an outer loop. Partition the data into outer folds , and let denote the data with outer fold removed. For each outer fold , run the entire inner search on and obtain
where:
- : the hyperparameters selected by the inner search when outer fold is held out
- : the -fold inner cross-validation estimate of candidate computed only on
For random search, here means the candidates drawn for this outer training run. Candidate generation must also exclude the outer test targets.
Then fit the selected model on the outer training set,
and evaluate it on the outer test fold. The nested estimate averages the outer-fold losses, each computed with the model whose hyperparameters were selected by that fold's inner search:
where:
- : the nested cross-validation estimate of the procedure's risk
- : the number of outer folds
- : the data with outer fold removed, used to run the inner search and select
- : the index set of observations in outer fold
- : the number of observations in outer fold
- : the model refitted on the outer training set using the selected hyperparameters
- : the per-observation loss
- : the observed target for observation
- : the feature vector for observation
- : the index running over observations within an outer test fold
- : the index of the outer fold
The outer test fold was not used to choose , so it gives a cleaner view of how the selection procedure performs on unseen data. Notice that can differ from fold to fold. The nested estimator is not the cross-validation score of one fixed model; it is an estimate of the risk of the complete procedure that takes a training set, runs a search, and returns a fitted model. With independent identically distributed observations and a procedure fixed before inspecting outer results, outer held-out losses evaluate that procedure at the outer-training sample size, approximately . They do not directly estimate the conditional risk of the final model refitted on all rows. Finite data and overlapping outer training sets still create variability. To see why the distinction matters, note that averages over outer folds each of which selected its own ; keeping each outer test fold out of its entire selection process is what avoids reusing its scores for selection, but it also means the nested score is not the score of any single deployable model.
Refitting, final testing, and reporting variability
After nested cross-validation, we usually want a final model for deployment. The standard recipe is to run the search on all available development data, obtain , and refit
The nested estimate describes the procedure; the final model is the procedure applied to all development data. If an untouched test set exists, we evaluate on it once. That test score is often the cleanest estimate of final performance, and in that situation nested cross-validation is not mandatory. Nested cross-validation is the appropriate tool when we need to estimate the procedure using the same data that were available for development, or when we want to compare several complete procedures under a common protocol.
Reporting should include the search method, the search space, the budget, the inner and outer fold counts, the metric, the random seeds, and the selected hyperparameters across outer folds. A single number hides too much. It is useful to report the mean outer score, the standard deviation across outer folds, and a standard error across repeated nested runs. If we repeat the entire nested procedure times with different outer splits, we can summarize the repetition-level scores with a mean and an estimated standard error. The mean of the per-repetition nested estimates is
and the estimated standard error of that mean is
where:
- : the number of repeated nested runs (each with a fresh outer split)
- : the index of the repetition
- : the nested estimate produced by the -th repetition
- : the mean nested estimate across repetitions
- : the estimated standard error of across repetitions
- : the squared deviation of repetition 's estimate from the mean
- : the sample standard deviation of the repetition-level scores
The formula is the sample standard deviation of the repetition-level scores divided by : the inner square root is the sample standard deviation across repetitions, and dividing by converts that spread into a standard error for their mean.
With independently drawn partition and search seeds, this is a Monte Carlo standard error for the mean over that randomization, conditional on this fixed dataset. It can decrease as we add repetitions without adding observations. The repetition-level standard deviation describes split/search variability; neither quantity measures how results vary over new datasets or supplies a population-risk confidence interval. If the selected degree or penalty changes drastically from fold to fold, the procedure is unstable, and a simpler model or more data may be preferable.
Mathematical properties
Grid and random search treat validation loss as a black-box objective and do not use derivatives. A discrete degree cannot be updated by an ordinary continuous gradient. Some continuous hyperparameters, including ridge penalties, admit differentiable optimization under suitable conditions, but that is a different method from the searches implemented here. A validation surface need not be convex, and finite-data noise can affect which candidate wins.
Random search has a simple coverage property. The following formula makes the probability of covering the good region explicit for an independent-draw search. Suppose the good region has probability mass under the chosen search distribution. Each independent draw then has probability of producing an acceptable candidate; the value of is simply the probability mass the search distribution assigns to the good region, and a uniform distribution over the search region makes it equal to the good region's volume (or length) divided by the total volume (or length). The probability that independent draws contain at least one good candidate is
where:
- : the probability that at least one of the candidates falls in the good region
- : the per-draw probability of an acceptable candidate under the search distribution
- : the number of independent candidate draws
- : the probability that all independent draws miss the good region, since each draw misses with probability and draws are independent
For a target hit probability with and per-draw hit probability with , the smallest integer budget giving hit probability at least is
where:
- : the smallest integer number of independent draws needed to reach hit probability at least , obtained by rounding the real-valued lower bound up to the next integer
- : the target lower bound on the probability of at least one hit (with )
- : the probability mass of the good region under the search distribution (with )
- : the ceiling function, which rounds its argument up to the smallest integer greater than or equal to it
To derive the budget bound, start from the requirement that the hit probability reach and rearrange the steps one at a time:
Because , we have , so the division used in the last step reverses the inequality direction. Because as well, , and the ratio of two negative quantities is positive, so the lower bound on is a positive number. The ceiling rounds the real-valued lower bound upward. The limiting cases confirm the bound: if , no finite positive budget can hit; if , one draw already suffices since for any . This formula explains why random search can be effective when the good region is not too small. It also shows the limits: if is tiny, the required budget becomes enormous.
The coverage formula can be read as a budget-planning curve. The next cells evaluate over a range of budgets for three per-draw hit probabilities, then plot the resulting hit probabilities.
PythonIn [5]Expand
import numpy as np
coverage_budgets = np.arange(0, 201)
coverage_qs = [0.01, 0.02, 0.05]
coverage_curves = {q: 1.0 - (1.0 - q) ** coverage_budgets for q in coverage_qs}VisualizationOut [6]Expand

Each outer fold performs candidates times inner fits, plus one refit on the outer training set, giving
where:
- : the number of fits for nested assessment, including one outer refit per fold, given by
- : the approximate fitting cost of the nested procedure,
- : the number of outer folds
- : the number of candidate configurations searched per outer fold
- : the number of inner folds used to score each candidate
- : the average time to fit one model
- : the per-outer-fold fit count, namely candidates times inner fits plus one refit
The search fits and outer refits make up this count. A final search and refit on all development data add fits. With , and , nested assessment uses fits per repetition and a final search/refit adds . Different candidates can have different fit times, so equal fit counts need not mean equal wall-clock time. Whether this cost is practical depends on the candidate budget, fold counts, per-fit cost, available compute, and the value of assessing the complete selection procedure. Parallelizing over candidates or folds can reduce wall-clock time, but it does not reduce the total number of fits.
Example
We first import the libraries used for the arithmetic and the later search implementation. The hidden setup seeds the shared plotting environment once.
PythonIn [7]Expand
import numpy as np
import pandas as pd
from scipy.stats import loguniform
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import GridSearchCV, KFold, RandomizedSearchCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScalerTo see the nested-cross-validation accounting without the distraction of model fitting, strip the problem down to a table of scores. Imagine five outer folds and three candidate polynomial degrees. For each outer fold, inner cross-validation gives one score per degree. The inner loop selects the degree with the smallest score. Then the outer loop measures that selected degree on the held-out outer fold. The code below records this bookkeeping, performs the selection, and computes the summary. The numbers are a small arithmetic illustration, not a claim about the synthetic dataset used later.
PythonIn [8]Expand
example_scores = pd.DataFrame(
{
"outer_fold": [1, 2, 3, 4, 5],
"inner_degree_1": [0.48, 0.51, 0.50, 0.47, 0.49],
"inner_degree_2": [0.42, 0.36, 0.46, 0.39, 0.45],
"inner_degree_3": [0.41, 0.39, 0.44, 0.40, 0.43],
"outer_mse": [0.52, 0.49, 0.60, 0.46, 0.57],
}
)
inner_cols = ["inner_degree_1", "inner_degree_2", "inner_degree_3"]
example_scores["selected_degree"] = (
example_scores[inner_cols].to_numpy().argmin(axis=1) + 1
)
example_scores["best_inner_mse"] = (
example_scores[inner_cols].to_numpy().min(axis=1)
)PythonIn [9]Expand
inner_mean = example_scores["best_inner_mse"].mean()
outer_mean = example_scores["outer_mse"].mean()
example_gap = outer_mean - inner_mean
outer_sd = example_scores["outer_mse"].std(ddof=1)
outer_se = outer_sd / np.sqrt(len(example_scores))ConsoleOut [10]Expand
outer_fold inner_degree_1 inner_degree_2 inner_degree_3 outer_mse selected_degree best_inner_mse
1 0.480 0.420 0.410 0.520 3 0.410
2 0.510 0.360 0.390 0.490 2 0.360
3 0.500 0.460 0.440 0.600 3 0.440
4 0.470 0.390 0.400 0.460 2 0.390
5 0.490 0.450 0.430 0.570 3 0.430
mean best inner MSE = 0.406
mean outer MSE = 0.528
inner-outer gap = 0.122
outer SD = 0.057
outer SE = 0.026The first block builds one row per outer fold. Each row contains the three inner scores, the selected degree, the best inner score, and the outer score. The selection step is an argmin: in each row, we compare the three inner scores and keep the smallest. For example, if the inner scores are , , and , the selected degree is the third one and the best inner score is . That is the choice the inner loop would make.
The second step records, for each outer fold, the outer MSE of the selected configuration refitted on the outer training set and scored on the outer test fold; those values are supplied in the outer_mse column. The key distinction is summarized here:
- The inner score answers: "Among these candidates, which one looks best on the inner validation folds?"
- The outer score answers: "After choosing that candidate, how well does the fitted model perform on data that were not used for the choice?"
The printed table lets us compare those two answers fold by fold. For each outer fold , let be the inner cross-validation score of candidate computed on the training data for outer fold , and let be the model's score on outer test fold . The inner mean is the average of the best inner scores, where the minimum is over the evaluated candidate set and the inner cross-validation is computed only on the training portion of outer fold :
where:
- : the mean of the best inner cross-validation scores across outer folds
- : the number of outer folds
- : the finite set of evaluated candidate hyperparameters
- : the inner cross-validation score for candidate on the training portion of outer fold
The outer mean is the average of the outer test scores:
where:
- : the mean of the outer test-fold scores across outer folds
- : the number of outer folds
- : the model's score on outer test fold
The difference between the outer and inner means estimates the inner-outer gap for this table:
where the outer mean uses each fold's held-out score while the inner mean uses that fold's best-selected candidate, so their difference is a descriptive inner-outer gap:
- : the estimated gap between the outer and inner means
- : the mean of the best inner-fold scores across outer folds
- : the mean of the outer-fold scores across outer folds
- : the number of outer folds
- : the inner cross-validation score for candidate on outer fold , computed only on the training portion of outer fold
- : the model's score on outer test fold
The difference is positive when the outer mean exceeds the selected inner mean. Selection, different fitting sample sizes, and finite-sample variation all contribute; this gap does not isolate selection bias. The output reports this gap. It also reports the outer standard deviation and the standard error
where:
- : the independence-based standard-error benchmark for this illustrative table
- : the sample standard deviation of the outer-fold scores
- : the number of outer folds
The fold standard deviation describes spread among these five values. Dividing it by would be a standard-error estimate for independent identically distributed fold scores, but overlapping training sets invalidate that justification for ordinary nested CV. Treat the printed outer SE as an independence benchmark, not a calibrated uncertainty interval.
Implementation in Scikit-learn
With the bookkeeping clear, we can implement the complete procedure on a real synthetic dataset. We generate 180 observations from a cubic polynomial with Gaussian noise, then fit a polynomial ridge regression model. The pipeline is
Standardization is inside the pipeline, so it is fit only on the training portion of each split. This is not a cosmetic detail: if we standardized the full dataset before splitting, the validation folds would influence the training preprocessing through the mean and scale. Keeping the scaler inside the pipeline makes the entire preprocessing step part of the model that is refit within every fold.
The hyperparameters are the polynomial degree and the ridge penalty . The grid therefore has candidates. We use 5 outer folds, 4 inner folds, and 10 repeated outer splits. The functions below implement the search from primitive loops so that every selection step is visible: candidate generation, inner cross-validation, selection, outer refitting, and outer scoring.
We use a SeedSequence hierarchy to give each repetition its own child sequence, then separate outer splits, inner splits, primitive candidate draws, and library candidate draws within that repetition. NumPy designs these child streams to be independent with very high probability; they are reproducible pseudorandom streams, not a proof of physical independence. We rebuild the same hierarchy when comparing methods, so grid and random search share outer and inner folds. Candidate samplers have separate streams. We pass derived 32-bit integer seeds to the scikit-learn splitters and random search. The finite seed protocol is checked for collisions; distinct hierarchy paths alone would not guarantee distinct 32-bit integers.
PythonIn [11]Expand
def make_polynomial_data(n=180, noise=0.35, seed=7):
rng = np.random.default_rng(seed)
x = rng.uniform(-2.2, 2.2, size=n)
y = 0.7 * x**3 - 1.1 * x**2 + 0.6 * x + 0.8
y = y + rng.normal(0.0, noise, size=n)
return x.reshape(-1, 1), y
DEGREES = list(range(1, 9))
ALPHAS = [1e-3, 1e-2, 1e-1, 1.0]
BUDGET = len(DEGREES) * len(ALPHAS)
def grid_candidates():
return [{"degree": d, "alpha": a} for d in DEGREES for a in ALPHAS]
def random_candidates(rng, budget=BUDGET):
candidates = []
for _ in range(budget):
degree = int(rng.integers(1, 9))
alpha = float(np.exp(rng.uniform(np.log(1e-3), np.log(1.0))))
candidates.append({"degree": degree, "alpha": alpha})
return candidates
def make_model(candidate):
return make_pipeline(
PolynomialFeatures(degree=candidate["degree"], include_bias=False),
StandardScaler(),
Ridge(alpha=candidate["alpha"]),
)
def inner_cv_mse(candidate, X_train, y_train, inner_splits):
fold_scores = []
for tr, va in inner_splits:
model = make_model(candidate)
model.fit(X_train[tr], y_train[tr])
pred = model.predict(X_train[va])
fold_scores.append(mean_squared_error(y_train[va], pred))
return float(np.mean(fold_scores))
def best_so_far(scores):
return np.minimum.accumulate(np.asarray(scores))
def splitter_seed(sequence):
return int(sequence.generate_state(1, dtype=np.uint32)[0])
def nested_streams(seed):
# Rebuild, rather than mutate, a supplied repetition's spawn counter.
if isinstance(seed, np.random.SeedSequence):
root = np.random.SeedSequence(
seed.entropy, spawn_key=seed.spawn_key, pool_size=seed.pool_size
)
else:
root = np.random.SeedSequence(seed)
outer, inner, candidates, library = root.spawn(4)
return {
"outer": splitter_seed(outer),
"inner": [splitter_seed(s) for s in inner.spawn(5)],
"candidates": candidates.spawn(5),
"library": [splitter_seed(s) for s in library.spawn(5)],
}
def run_nested_cv(method, X, y, seed, budget=BUDGET):
if method not in {"grid", "random"}:
raise ValueError("method must be 'grid' or 'random'")
if not isinstance(budget, (int, np.integer)) or budget < 1:
raise ValueError("budget must be a positive integer")
if method == "grid" and budget != BUDGET:
raise ValueError("the defined grid contains exactly BUDGET candidates")
streams = nested_streams(seed)
outer = KFold(n_splits=5, shuffle=True, random_state=streams["outer"])
outer_scores = []
inner_scores = []
chosen = []
for fold, (tr, te) in enumerate(outer.split(X)):
if method == "grid":
candidates = grid_candidates()
else:
rng = np.random.default_rng(streams["candidates"][fold])
candidates = random_candidates(rng, budget)
inner = KFold(
n_splits=4, shuffle=True, random_state=streams["inner"][fold]
)
inner_splits = list(inner.split(X[tr]))
scores = [
inner_cv_mse(c, X[tr], y[tr], inner_splits) for c in candidates
]
best_idx = int(np.argmin(scores))
best = candidates[best_idx]
model = make_model(best)
model.fit(X[tr], y[tr])
pred = model.predict(X[te])
outer_scores.append(mean_squared_error(y[te], pred))
inner_scores.append(scores[best_idx])
chosen.append(best)
return np.array(outer_scores), np.array(inner_scores), chosenPythonIn [12]Expand
X, y = make_polynomial_data(seed=7)
experiment_root = np.random.SeedSequence(1000)
repetition_seeds = experiment_root.spawn(10)
final_seed, trace_seed, jitter_seed = experiment_root.spawn(3)
# Check the finite integer interfaces, including the demonstration hierarchy.
integer_seeds = [splitter_seed(final_seed)]
for sequence in repetition_seeds + [trace_seed]:
streams = nested_streams(sequence)
integer_seeds.extend(
[streams["outer"], *streams["inner"], *streams["library"]]
)
assert len(integer_seeds) == len(set(integer_seeds))
rows = []
fold_rows = []
for rep, seed in enumerate(repetition_seeds):
for method in ["grid", "random"]:
outer_scores, inner_scores, chosen = run_nested_cv(method, X, y, seed)
for fold, (outer_loss, inner_loss, candidate) in enumerate(
zip(outer_scores, inner_scores, chosen), start=1
):
fold_rows.append(
{
"method": method,
"rep": rep,
"outer_fold": fold,
"degree": candidate["degree"],
"alpha": candidate["alpha"],
"inner_mse": float(inner_loss),
"outer_mse": float(outer_loss),
}
)
rows.append(
{
"method": method,
"rep": rep,
"outer_mse": float(np.mean(outer_scores)),
"inner_mse": float(np.mean(inner_scores)),
"inner_outer_gap": float(
np.mean(outer_scores) - np.mean(inner_scores)
),
"outer_sd": float(np.std(outer_scores, ddof=1)),
}
)
nested_results = pd.DataFrame(rows)
fold_results = pd.DataFrame(fold_rows)PythonIn [13]Expand
summary = (
nested_results.groupby("method")
.agg(
outer_mean=("outer_mse", "mean"),
outer_split_sd=("outer_mse", lambda s: s.std(ddof=1)),
outer_mcse=("outer_mse", lambda s: s.std(ddof=1) / np.sqrt(len(s))),
inner_mean=("inner_mse", "mean"),
gap_mean=("inner_outer_gap", "mean"),
outer_sd_mean=("outer_sd", "mean"),
)
.reset_index()
)ConsoleOut [14]Expand
Nested CV summary across repeated outer splits method outer_mean outer_split_sd outer_mcse inner_mean gap_mean outer_sd_mean grid 0.1129 0.0023 0.0007 0.1127 0.0003 0.0296 random 0.1130 0.0026 0.0008 0.1127 0.0004 0.0296
The first repetition's fold-level table shows the configurations selected independently inside each outer training set. Different selections need not imply a large predictive difference: nearby candidates can have nearly equal validation losses.
ConsoleOut [15]Expand
method rep outer_fold degree alpha inner_mse outer_mse grid 0 1 3 0.100000 0.108641 0.125123 grid 0 2 3 0.001000 0.105756 0.147204 grid 0 3 3 0.001000 0.115480 0.100014 grid 0 4 3 0.100000 0.119183 0.083471 grid 0 5 3 0.001000 0.113433 0.090897 random 0 1 3 0.067673 0.108666 0.125193 random 0 2 3 0.001858 0.105757 0.147204 random 0 3 3 0.001584 0.115481 0.100015 random 0 4 3 0.206168 0.119137 0.083856 random 0 5 3 0.004695 0.113434 0.090889
Because both methods use the same outer partitions within each repetition, we also inspect their paired MSE differences. Positive differences below mean random search had higher outer MSE on that repetition.
PythonIn [16]Expand
paired_outer = nested_results.pivot(
index="rep", columns="method", values="outer_mse"
)
paired_differences = paired_outer["random"] - paired_outer["grid"]
paired_mean = float(paired_differences.mean())
paired_sd = float(paired_differences.std(ddof=1))ConsoleOut [17]Expand
mean paired random-minus-grid MSE: 0.00010156 SD of paired differences: 0.00039750
These paired summaries describe partition and search randomization on one fixed dataset. A small difference relative to the marginal spread of either method is not, by itself, a significance test; pairing can make differences much less variable than individual scores.
The summary table separates three quantities. outer_split_sd describes repetition-to-repetition variation on this fixed dataset; outer_mcse is that spread divided by the square root of the repetition count, describing Monte Carlo precision of the mean under the independent-stream randomization model, conditional on this dataset. Neither is uncertainty over new datasets. inner_mean averages selected inner scores, gap_mean is outer minus inner, and outer_sd_mean summarizes the within-run fold spread. Here the grid outer mean is 0.1129 and its selected inner mean is 0.1127; the corresponding random-search means are 0.1130 and 0.1127. The realized outer-minus-inner gaps are 0.00029 for grid search and 0.00037 for random search. These executed results do not contradict expected selection optimism under its stated assumptions; inner fits use fewer rows, and realized validation differences also contain sample variation.
Scikit-learn provides GridSearchCV and RandomizedSearchCV, which automate the inner search. We can use them inside the same outer loop. The grid search should match our primitive implementation closely, because both use the same folds, candidates, and scoring rule; any residual difference comes from tie-breaking order or numerical round-off in the fitted coefficients. A truly exact match requires the same regularization convention and identical preprocessing. The random search uses a different sampler, so we compare budgets and distributions rather than expecting identical candidate draws.
PythonIn [18]Expand
param_grid = {
"polynomialfeatures__degree": DEGREES,
"ridge__alpha": ALPHAS,
}
param_distributions = {
"polynomialfeatures__degree": DEGREES,
"ridge__alpha": loguniform(1e-3, 1.0),
}
def sklearn_nested_once(method, X, y, seed):
streams = nested_streams(seed)
outer = KFold(n_splits=5, shuffle=True, random_state=streams["outer"])
outer_scores = []
chosen = []
for fold, (tr, te) in enumerate(outer.split(X)):
inner = KFold(
n_splits=4, shuffle=True, random_state=streams["inner"][fold]
)
base = make_pipeline(
PolynomialFeatures(include_bias=False),
StandardScaler(),
Ridge(),
)
if method == "grid":
search = GridSearchCV(
base,
param_grid,
cv=inner,
scoring="neg_mean_squared_error",
n_jobs=1,
)
else:
search = RandomizedSearchCV(
base,
param_distributions,
n_iter=BUDGET,
cv=inner,
scoring="neg_mean_squared_error",
n_jobs=1,
random_state=streams["library"][fold],
)
search.fit(X[tr], y[tr])
pred = search.predict(X[te])
outer_scores.append(mean_squared_error(y[te], pred))
chosen.append(search.best_params_)
return np.array(outer_scores), chosenPythonIn [19]Expand
seed_compare = repetition_seeds[0]
sk_grid_scores, sk_grid_chosen = sklearn_nested_once("grid", X, y, seed_compare)
sk_rand_scores, sk_rand_chosen = sklearn_nested_once(
"random", X, y, seed_compare
)
our_grid_scores, our_grid_inner, our_grid_chosen = run_nested_cv(
"grid", X, y, seed_compare
)
our_rand_scores, our_rand_inner, our_rand_chosen = run_nested_cv(
"random", X, y, seed_compare
)
assert np.allclose(our_grid_scores, sk_grid_scores)
assert int(np.argmin([1.0, 1.0, 2.0])) == 0PythonIn [20]Expand
comparison = pd.DataFrame(
{
"implementation": [
"ours-grid",
"sklearn-grid",
"ours-random",
"sklearn-random",
],
"mean_outer_mse": [
our_grid_scores.mean(),
sk_grid_scores.mean(),
our_rand_scores.mean(),
sk_rand_scores.mean(),
],
}
)ConsoleOut [21]Expand
implementation mean_outer_mse
ours-grid 0.1093
sklearn-grid 0.1093
ours-random 0.1094
sklearn-random 0.1095
Grid outer scores match: TrueThe comparison serves two purposes. First, it checks that the primitive grid loop and GridSearchCV agree when they receive the same folds and candidate set. Second, it places the random-search results beside the grid results without pretending that the candidate draws are identical. Matching grid scores are a correctness check on the implementation, not evidence that grid search is generally better. The random-search comparison is about the behavior of a stochastic procedure under a fixed budget.
After assessment, we run the chosen grid-search procedure once more on all development observations. refit=True fits the selected configuration on all 180 rows after its inner search. We retain that estimator for future predictions; its training fit is not a new held-out performance estimate.
PythonIn [22]Expand
final_search = GridSearchCV(
make_model({"degree": 1, "alpha": 1e-3}),
param_grid,
cv=KFold(n_splits=4, shuffle=True, random_state=splitter_seed(final_seed)),
scoring="neg_mean_squared_error",
n_jobs=1,
refit=True,
)
final_search.fit(X, y)
final_model = final_search.best_estimator_ConsoleOut [23]Expand
Final selected configuration: {'polynomialfeatures__degree': 4, 'ridge__alpha': 0.001}
Final refit observations: 180The outer assessment used 144 training observations per fold, whereas this final refit uses 180. The nested score evaluates the pre-specified procedure at the smaller training size, not the conditional risk of this exact final model. We chose the grid procedure in advance for this demonstration; choosing between procedures after inspecting outer results would itself need independent assessment.
Key Parameters
Below are the main configuration choices that control the nested search shown above:
degree: Polynomial degree in the feature map, searched over1through8in the primitive loops. Higher degrees expand the feature family; variance also depends on regularization and data. Specify the degree range before outer evaluation and choose within it using inner folds. If outer results lead you to redesign the search, assess that redesigned procedure with new independent evaluation data.alpha: Ridge penalty strength, searched over1e-3through1. Larger values increase shrinkage; a log-spaced range covers several orders of magnitude efficiently.param_grid: Defines the grid search space. Keys use the pipeline step name, a double underscore, and the parameter name, such aspolynomialfeatures__degreeandridge__alpha. If the pipeline step names change, the search keys must change with them.param_distributions: Defines the random search space. Continuous distributions such asloguniform(1e-3, 1.0)are sampled on a logarithmic scale.n_iter: Sets the random-search budget. To compare grid and random search fairly, it should equal the number of grid candidates, with both searches using the same inner folds and scoring rule.cv: Controls the inner validation splitter. The explicitly seededKFoldobjects used here give reproducible partitions for unchanged row ordering.scoring: Set to"neg_mean_squared_error"because scikit-learn maximizes scores by convention; negative MSE makes a larger value correspond to a smaller error.random_state: Controls the random candidate draws inRandomizedSearchCV.n_jobs: Controls fit parallelism. We use1to keep this CPU example modest;-1requests all available processors. Reproducibility also depends on the estimator, splitters, and random seeds.refit: With the single-metric searches here,Truerefits the selected configuration on the full input passed tofit, after inner cross-validation.best_estimator_holds that fitted estimator andpredictuses it;best_params_,best_score_, andcv_results_expose the configuration, selected inner score, and candidate results.
Visualizing Hyperparameter Search and Nested Cross-Validation
For each candidate in evaluation order, we compute the inner cross-validation MSE and then take the running minimum. That running minimum is the best score the search would report after that many evaluations. Each trace changes only when a newly evaluated candidate improves the best score so far. Grid search uses a fixed Cartesian order. Random search draws candidates from the same degree and log-alpha space, so its curve can drop earlier or later depending on the draw. The figure is not a claim that one method always wins; it shows how budget and candidate order affect the internal score.
PythonIn [24]Expand
demo_streams = nested_streams(trace_seed)
outer_demo = KFold(n_splits=5, shuffle=True, random_state=demo_streams["outer"])
tr_demo, te_demo = next(iter(outer_demo.split(X)))
inner_demo = KFold(
n_splits=4, shuffle=True, random_state=demo_streams["inner"][0]
)
inner_splits_demo = list(inner_demo.split(X[tr_demo]))
grid_scores = [
inner_cv_mse(c, X[tr_demo], y[tr_demo], inner_splits_demo)
for c in grid_candidates()
]
rng_demo = np.random.default_rng(demo_streams["candidates"][0])
rand_scores = [
inner_cv_mse(c, X[tr_demo], y[tr_demo], inner_splits_demo)
for c in random_candidates(rng_demo)
]
trace_grid = best_so_far(grid_scores)
trace_random = best_so_far(rand_scores)
budget_axis = np.arange(1, BUDGET + 1)VisualizationOut [25]Expand

The second figure summarizes the repeated nested cross-validation runs. Each point is the mean outer-fold MSE from one repetition. The horizontal line marks the mean across repetitions for that search method. The vertical spread comes from different outer splits and, for random search, from different candidate draws. The two point clouds overlap in this realized example. The important comparison is not which point is lowest in one repetition, but whether the distributions differ systematically and how large the split-to-split variability is.
PythonIn [26]Expand
rng_jitter = np.random.default_rng(jitter_seed)
distribution_data = {}
for method in ["grid", "random"]:
values = nested_results.loc[
nested_results["method"] == method, "outer_mse"
].to_numpy()
distribution_data[method] = {
"values": values,
"jitter": rng_jitter.uniform(-0.08, 0.08, size=len(values)),
"mean": float(values.mean()),
}VisualizationOut [27]Expand

On this generated dataset, the grid search has a positive outer-minus-inner gap and random search has a positive gap. The gap combines selection, different training sizes, and random variation; it cannot identify selection bias by itself. The two outer means differ by much less than the observed marginal split-to-split spread; the paired summaries above describe the within-repetition differences. This is a descriptive comparison on one dataset, with paired outer partitions, and does not establish a general ranking of search methods. The outer scores evaluate the entire pre-specified procedure on data excluded from each fold's search.
Practical Implications
Grid search is a reasonable default when the search space is small, mostly discrete, and needs to be audited by reviewers; exhaustive enumeration makes the candidate set easy to explain and reproduce. Random search is often preferable when the space contains continuous hyperparameters, when several dimensions are expected to be irrelevant, or when a fixed budget must cover a broad range. In either case, the search space, budget, metric, and inner-fold rule should be fixed before results are inspected, and every candidate should be scored on the same inner folds. When the budget is large relative to the amount of validation data, the inner search can fit fold-specific noise; reducing unnecessary candidates or using more validation data can reduce this problem. Changing the search method alone does not supply independent evaluation.
Nested cross-validation is warranted when no untouched test set is available and the quantity of interest is the performance of the complete selection-and-fit procedure, rather than the performance of one fixed configuration. It is unnecessary when a clean holdout can answer the evaluation question directly, and it is not a substitute for leakage control. Scaling, imputation, feature selection, target encoding, and other data-dependent transformations must be fitted inside each training fold. For grouped, clustered, or time-dependent observations, the outer and inner splitters should respect that structure; ordinary random folds can share information across splits and make the nested estimate optimistic.
The useful diagnostics are the inner-outer gap, the variability of outer scores, and the stability of the selected hyperparameters across outer folds. A large gap can arise from selection over many noisy candidates, from different fitting sample sizes, or from finite-sample variation; it should be interpreted as a warning about the selection procedure, not as a precise decomposition of bias. Compare methods using their paired fold or repetition outcomes when the protocol pairs them. Marginal outer-score standard deviations alone do not determine the uncertainty of a difference, and repeated partitions of one dataset do not establish reproducibility over new datasets. For expensive models, reduce the candidate budget, parallelize folds, cache preprocessing, or use a cheaper surrogate during exploration, but preserve the same protocol for the final comparison. Reporting should include the search space, budget, inner and outer fold counts, metric, seeds, outer mean with a measure of spread, and the configurations selected across outer folds; the best inner score should not be presented as final generalization performance.
Summary
Hyperparameter search is part of the modeling procedure. Grid search enumerates a finite Cartesian product and is easy to audit, while random search samples candidates from a distribution and often covers important dimensions more efficiently under a fixed budget. For a fixed candidate set with unbiased validation estimates, selecting the minimum produces optimistic validation loss in expectation. Actual inner-outer gaps also reflect training-size differences and sample variation. Nested cross-validation separates selection from assessment: inner folds choose hyperparameters, and outer folds estimate the risk of the entire selection-and-fit procedure.
We built the procedure from primitive loops, compared it with scikit-learn's search objects, and used repeated outer splits to report variability. The practical recipe is to pre-specify the search space and budget, keep preprocessing inside the pipeline, report the outer estimate with its spread, and refit the selected configuration on all development data. When an untouched test set exists, it can answer the final evaluation question directly; nested cross-validation is the tool for the situations where it does not.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about hyperparameter search and nested cross-validation.
Hyperparameter Search and Nested Cross-Validation
Join the discussionAsk a question or share what you learned.
Keep exploring
Reference
Citation details
Cite or share this article.


Comments
Add to the conversation
A question, a correction, or an example from your own work.
No comments yet. Be the first to share your thoughts!