nested_tune_grid() estimates how well a model tuned with
tune::tune_grid() performs on new data. Each outer fold tunes on its own
inner resamples, fits the chosen candidate on its analysis set, and scores
it on the rows it held out.
Usage
nested_tune_grid(object, ...)
# Default S3 method
nested_tune_grid(object, ...)
# S3 method for class 'model_spec'
nested_tune_grid(
object,
preprocessor,
resamples,
...,
param_info = NULL,
grid = 10,
metrics = NULL,
event_level = "first",
eval_time = NULL,
select = selection_rule()
)
# S3 method for class 'workflow'
nested_tune_grid(
object,
resamples,
...,
param_info = NULL,
grid = 10,
metrics = NULL,
event_level = "first",
eval_time = NULL,
select = selection_rule()
)Arguments
- object
A
workflows::workflow()with at least one parameter marked withtune::tune(), or a parsnip model specification.- ...
On the generic, the arguments of the method. On a method, pass
control = tune::control_grid()here to set the control. Name every argument after....- preprocessor
A formula or recipe, when
objectis a model specification.- resamples
A nested resampling design from
nested_resamples()orrsample::nested_cv().- param_info
A
dials::parameters()object orNULL. If none is given, a parameters set is derived from other arguments. Passing this argument can be useful when parameter ranges need to be customized.- grid
A data frame of tuning combinations or a positive integer. The data frame should have columns for each parameter being tuned and rows for tuning parameter candidates. An integer denotes the number of candidate parameter sets to be created automatically.
- metrics
A
yardstick::metric_set(), orNULLfor tune's default set. Under every rule but"desirability", the first metric selects each fold's candidate, so put the metric you report first. The Bayes, race and annealing searches also steer on it. Every metric is scored on the outer folds.- event_level
"first"(the default) or"second". It names which level of a two-class outcome is the event. It applies to the inner tuning run and the outer scoring fit alike.- eval_time
A numeric vector of evaluation times for a censored regression model, or
NULL(the default) to leave the choice to tune. Times that are not numeric, empty, missing, negative or not finite are an error. Zero, repeated and unordered times are passed on to tune.- select
A
selection_rule()that each outer fold uses to pick its candidate. The default istune::select_best()on the first metric.
Value
A tibble of class nested_results with one row per outer fold.
Beside the fold's split and labels, each row holds:
.metrics, the metrics scored on the fold's assessment set..selected, the candidate the fold's inner tuning chose..inner_metrics, the inner run's metrics for every candidate that scored. A fold that scored no candidate holds a zero-row table, neverNULL. tune can reach fewer candidates than agridsize asks for, soattr(res, "grid"), the argument as given, can differ from these rows..completed, whether the fold finished, and.notes, what went wrong..tuning_seedand.outer_fit_seed, the fold's two seeds.
Use collect_metrics() for the estimate. summary(),
collect_selections(), agreement() and
autoplot() show it in other ways.
arrange(), mutate() adding a column, and a join that matches one row
per fold keep the class. Removing a fold or a column above
returns a plain tibble, because the result no longer describes the
design that ran. So do slice(), bind_rows() of two results, and
vctrs::vec_rbind(x) alone. bind_cols() keeps the class only with the
result as its first argument. group_by(), rowwise() and
tibble::as_tibble() return their own classes and keep the attributes.
A column you add is read as a fold label only when the design itself
carries a column of that name.
Details
For each outer fold, the function calls tune::tune_grid() on the fold's
inner resamples and picks a candidate with select. It finalizes the
workflow with that candidate. Then tune::last_fit() fits the workflow on
the fold's analysis set and scores it on the assessment set.
The estimate from collect_metrics() describes this whole procedure (tune,
select, fit) and not one fitted model. To get a model to
deploy, call nested_final_fit(), which runs the same procedure on all
the data. The nested estimate is the number to report for that model,
because the model comes from the same procedure.
nested_tune_bayes(), nested_tune_race_anova(),
nested_tune_race_win_loss() and nested_tune_sim_anneal() run the same
loop with another search. nested_fit_resamples() runs it for a workflow
with nothing to tune. This page documents what they share.
object can also be a parsnip model specification, given with a
preprocessor, as in nested_tune_grid(spec, preprocessor, resamples).
Nested designs
resamples has one row per outer fold: a splits column of outer splits,
an inner_resamples column of inner rsets, and id columns. The function
checks the design before it fits anything. An outer bootstrap is an
error, and so are rsample::loo_cv(), rsample::apparent() and
rsample::permutations() in either loop. An inner split that reaches a
row its outer fold holds out is an error too. Each error names the design,
or the rows, splits or folds at fault.
Time-series designs work as well. nested_tune_grid() is tested with an
outer rsample::rolling_origin(), rsample::sliding_window(),
rsample::sliding_index() or rsample::sliding_period() design and an
inner design of any of those four kinds. Leave the lag of
rsample::rolling_origin() at 0, because a larger lag is an error.
nested_resamples() says which of these designs the other functions are
tested on.
Finalizing a parameter range
A range that depends on the data, such as mtry(), is finalized on each
outer fold's analysis set, never on the rows that fold holds out.
nested_final_fit() finalizes it on all the data. A range set in
param_info applies to every fold.
Evaluation times
The inner tuning and the outer scoring both use the times in eval_time.
If the metric set has no dynamic or integrated survival metric, tune
ignores the times with a warning. Each fold selects at the first of these
times.
Weighting the outer folds
Weights set on the design with tune::add_resample_weights() give a
weighted average of the outer folds' estimates, as tune computes it. A
fold that fails or scores NA is left out, and the other weights are
scaled to sum to one.
Reproducibility
Call set.seed() before the function, as elsewhere in tidymodels. Each
fold takes its seeds from that one seed by its position, so the result is
the same serially and in parallel. The seeds are kept as .tuning_seed
and .outer_fit_seed. The function restores your random state on exit,
even after an error. So two calls with no set.seed() between them give
the same result, as repeated tune::tune_grid() calls do.
This covers randomness that flows through R's generator. Engines that randomize outside it (kernlab's SVMs, the deep-learning engines) cannot be pinned by any R-side scheme, here or in tune.
Reproducing one fold
Fold i runs the code below. On a nested_resamples() design, inner
is the fold's inner rset re-pointed at
rsample::analysis(resamples$splits[[i]]). Under another select, the
selection line uses tune::select_by_one_std_err() or
tune::select_by_pct_loss(). A Bayesian fold also sets control$seed to
the fold's tuning seed.
set.seed(res$.tuning_seed[[i]], kind = "Mersenne-Twister",
normal.kind = "Inversion", sample.kind = "Rejection")
tuned <- tune_grid(object, inner, grid = grid,
param_info = param_info, metrics = metrics,
eval_time = eval_time,
control = extract_procedure(res)$control)
final <- finalize_workflow(object, select_best(tuned, metric = <first metric>))
set.seed(res$.outer_fit_seed[[i]], kind = "Mersenne-Twister",
normal.kind = "Inversion", sample.kind = "Rejection")
last_fit(final, resamples$splits[[i]], metrics = metrics,
eval_time = eval_time,
control = control_last_fit(event_level = event_level))When a fold fails
A fold that fails does not stop the run. Its .completed is FALSE and
its .notes says which stage failed and why. The run warns at the end,
and collect_metrics() summarizes only the folds that completed. If no
fold completed, collect_metrics() gives an error.
Parallel execution
The outer folds run in parallel when two or more mirai daemons are running, and serially otherwise. Inner tuning always runs serially.
The daemons load nestedtune from an installed library, and they do not
inherit your session's options. Set what a fold needs with
mirai::everywhere(). Under devtools::load_all(), the daemons cannot
see your development code, so run
mirai::everywhere(pkgload::load_all("<path>")) first. A running daemon
keeps the version it loaded, so restart the pool after you reinstall.
Before it sends the folds out, the function checks that every daemon can
load the packages the run needs. The check waits up to 30 seconds, and
options(nestedtune.preflight_timeout = <ms>) changes that limit. The
folds themselves have no time limit. If every daemon dies after dispatch,
the call waits until you interrupt it.
Each fold is sent one copy of the data. A workflow built inside a
function carries that function's environment to every fold, so build it
at the top level. On a pool with mirai's default dispatcher, an interrupt
cancels the folds that are still running. A pool started with
dispatcher = FALSE cannot be cancelled, and the call warns about that.
Differences from calling tune directly
A tune::control_grid() passed through ... as control reaches the
inner tune_grid() in every fold. The outer fit runs under its own
control_last_fit(), and takes only event_level, save_pred and
extract from the control. The result keeps the control that ran as
extract_procedure(res)$control, and nested_final_fit() runs again
with it. Its settings differ from a direct tune_grid() call as
follows.
Forced: allow_par. The inner tuning run and the outer fit run with
allow_par = FALSE, because the outer folds carry the parallelism.
Settable as its own argument: event_level. Set the level with the
event_level argument, which reaches the inner run and the outer fit. A
control that names a level other than tune's default or the argument's is
an error. eval_time is an argument of tune_grid() too, not a control
setting.
Refused: none. No setting is refused on its own. A control of another
class is an error, even a control_bayes() that tune itself accepts.
tune gives tune::control_resamples() and tune::control_last_fit() the
control_grid class, so both are accepted here.
Passed through: verbose, pkgs, parallel_over, workflow_size.
Each reaches tune_grid() as given. parallel_over still sets the seed
each model fit starts from, so a stochastic engine's numbers depend on
it.
Kept from the outer fit: save_pred, extract. With
save_pred = TRUE, the result has a .predictions column of each
fold's outer predictions. With an extract function, it has an
.extracts column. The inner run's predictions and extracts are still
discarded.
collect_predictions() and
collect_extracts() read the two
columns.
Not returned: save_workflow. The inner tuning results are not kept,
so this setting returns nothing. Use extract = function(x) x to keep
each fold's fitted workflow.
Inert: backend_options. Options for a parallel backend, with no
backend to reach at allow_par = FALSE.
Examples
data(mtcars)
rec <- recipes::recipe(mpg ~ ., data = mtcars) |>
recipes::step_pca(recipes::all_predictors(), num_comp = tune::tune())
wf <- workflows::workflow(rec, parsnip::linear_reg())
set.seed(1)
folds <- nested_resamples(
mtcars,
outside = rsample::vfold_cv(v = 2),
inside = rsample::vfold_cv(v = 2)
)
set.seed(2)
res <- nested_tune_grid(wf, folds, grid = data.frame(num_comp = 1:2))
collect_metrics(res)
#> # A tibble: 2 × 5
#> .metric .estimator mean n std_err
#> <chr> <chr> <dbl> <int> <dbl>
#> 1 rmse standard 2.98 2 0.459
#> 2 rsq standard 0.747 2 0.0691
# What each fold chose. Folds that disagree show the choice is unstable.
res$.selected
#> [[1]]
#> # A tibble: 1 × 2
#> num_comp .config
#> <int> <chr>
#> 1 1 pre1_mod0_post0
#>
#> [[2]]
#> # A tibble: 1 × 2
#> num_comp .config
#> <int> <chr>
#> 1 1 pre1_mod0_post0
#>
