Tuning and Pipelines

library(tidylearn)
library(dplyr)

Overview

tl_model() fits one model with the hyperparameters you name. Two families build on that:

The two compose: tune to find the settings, then put the winning settings in a pipeline so the whole recipe is reproducible.

Pipelines

A pipeline records preprocessing, the models to fit, and how to evaluate them. Building it does no work; tl_run_pipeline() does.

split <- tl_split(iris, prop = 0.7, stratify = "Species", seed = 42)

pipe <- tl_pipeline(
  split$train, Species ~ .,
  preprocessing = list(standardize = TRUE, dummy_encode = FALSE),
  models = list(
    tree = list(method = "tree"),
    forest = list(method = "forest", ntree = 300)
  ),
  evaluation = list(
    validation = "cv",
    cv_folds = 3,
    metrics = c("accuracy", "f1"),
    best_metric = "accuracy"
  )
)

print(pipe)
#> Tidylearn Pipeline
#> =================
#> Formula: Species ~ . 
#> Data: 105 observations, 5 variables
#> Preprocessing: impute_missing, standardize 
#> Models: tree, forest 
#> Evaluation:  cv (3 folds)
#> Metrics: accuracy, f1 
#> Best metric: accuracy

Anything you leave out of preprocessing or evaluation takes its default, so a partial list is fine. An unrecognised name is an error rather than a step that quietly does nothing.

tl_pipeline(split$train, Species ~ .,
            preprocessing = list(scale_method = "standardize"))
#> Error:
#> ! Unknown preprocessing step(s): scale_method. Available steps: impute_missing, standardize, dummy_encode.

Running it

run <- tl_run_pipeline(pipe, verbose = FALSE)

names(run$models)
#> [1] "tree"   "forest"
print(run)
#> Tidylearn Pipeline
#> =================
#> Formula: Species ~ . 
#> Data: 105 observations, 5 variables
#> Preprocessing: impute_missing, standardize 
#> Models: tree, forest 
#> Evaluation:  cv (3 folds)
#> Metrics: accuracy, f1 
#> Best metric: accuracy 
#> 
#> Results
#> =======
#> Best model: forest 
#> Performance:
#>   tree: accuracy = 0.9524
#>   forest: accuracy = 0.9619 (best)

tl_get_best_model() returns the model that won on best_metric:

best <- tl_get_best_model(run)
best$spec$method
#> [1] "forest"

Predicting through the pipeline

This is the reason to use a pipeline rather than a bare model. Predicting on raw new data replays the preprocessing the pipeline learned during the run, applying the training centre and scale rather than recomputing them from the new rows.

preds <- tl_predict_pipeline(run, new_data = split$test, model_name = "forest")
head(preds)
#> # A tibble: 6 × 1
#>   .pred 
#>   <fct> 
#> 1 setosa
#> 2 setosa
#> 3 setosa
#> 4 setosa
#> 5 setosa
#> 6 setosa
mean(preds$.pred == split$test$Species)
#> [1] 0.9333333

Omit model_name to predict with the best model.

Saving and reloading

path <- tempfile(fileext = ".rds")
tl_save_pipeline(run, path)

reloaded <- tl_load_pipeline(path)
names(reloaded$models)
#> [1] "tree"   "forest"
# Predictions survive the round trip, preprocessing included
reloaded_preds <- tl_predict_pipeline(
  reloaded, new_data = split$test, model_name = "forest"
)
identical(reloaded_preds$.pred, preds$.pred)
#> [1] TRUE

Tuning into a Pipeline

Tuning tells you the settings; the pipeline holds them alongside the preprocessing that produced them.

tuned <- tl_tune_grid(
  split$train, Species ~ .,
  method = "forest",
  param_grid = list(mtry = c(2, 3), ntree = c(100, 300)),
  folds = 3,
  verbose = FALSE
)

best_params <- attr(tuned, "tuning_results")$best_params
best_params
#> $mtry
#> [1] 2
#> 
#> $ntree
#> [1] 100
final <- tl_pipeline(
  split$train, Species ~ .,
  models = list(
    forest = c(list(method = "forest"), best_params)
  ),
  evaluation = list(cv_folds = 3, metrics = "accuracy",
                    best_metric = "accuracy")
)

final_run <- tl_run_pipeline(final, verbose = FALSE)
final_preds <- tl_predict_pipeline(final_run, new_data = split$test)

mean(final_preds$.pred == split$test$Species)
#> [1] 0.9333333

Cost

Tuning multiplies fits. A grid of g combinations at k folds is g × k fits, plus one more to build the final model. The tree grid at the top of this vignette is 6 combinations × 3 folds = 18 fits, 19 with the final model, of a method that takes milliseconds. The same grid on method = "xgboost" with 1000 rounds is the same 19 fits of something much slower.

Two levers, in the order worth pulling:

  1. Fewer folds. Going from 5 to 3 removes 40% of the work and still gives out-of-sample estimates.
  2. Random over grid. tl_tune_random(n_iter = 10) costs a fixed 10 points regardless of how many parameters you are searching, where a grid over the same parameters costs their product.

tl_compute_advisor() estimates the cost of a single fit before you multiply it by the search — worth a look before starting a long grid.

Where to Go Next

mirror server hosted at Truenetwork, Russian Federation.