tune: optimizar hiperparámetros sin trampas

r
machine-learning
tidymodels
Marcar hiperparámetros con tune(), búsqueda en grid con tune_grid(), tipos de grid (regular, latin hypercube, irregular), tune_bayes() para búsqueda inteligente, racing methods para acelerar y finalize_workflow para cerrar el ciclo.

Hiperparámetros vs parámetros

Antes de tunear, conviene distinguir:

  • Parámetros del modelo: se aprenden de los datos durante el fit(). Los coeficientes de una regresión lineal, los pesos de una red neuronal, los splits de un árbol.
  • Hiperparámetros: configuraciones del algoritmo de aprendizaje que no se aprenden, las eliges tú. La penalización en lasso, el número de árboles en random forest, la profundidad máxima en xgboost.

Los hiperparámetros condicionan qué modelo aprende. Cambiar mtry en random forest cambia drásticamente el resultado. Encontrar los valores correctos es tuning.

tune orquesta esto sobre la maquinaria de parsnip + workflows + rsample que ya conoces.

Marcar hiperparámetros con tune()

library(tidymodels)

modelo_spec <- rand_forest(
  trees = 500,
  mtry  = tune(),
  min_n = tune()
) |>
  set_engine("ranger") |>
  set_mode("classification")

tune() no es un valor, es un marcador. Dice “este hiperparámetro se decide después, no lo fijes ahora”. mtry y min_n se quedan abiertos. trees = 500 se fija.

Para ver qué está marcado a tunear:

extract_parameter_set_dials(modelo_spec)
#> Collection of 2 parameters for tuning
#>
#>  identifier  type    object
#>        mtry  mtry nparam[?]
#>       min_n min_n nparam[+]

El nparam[?] en mtry significa que tune no conoce el rango por defecto (porque depende del número de predictores). Hay que decírselo (lo veremos abajo).

tune_grid(): búsqueda exhaustiva en cuadrícula

El método más simple es probar combinaciones en una cuadrícula:

folds <- vfold_cv(train, v = 5, strata = outcome)

wf <- workflow() |>
  add_recipe(receta) |>
  add_model(modelo_spec)

set.seed(123)
res <- tune_grid(
  wf,
  resamples = folds,
  grid      = 20,
  metrics   = metric_set(roc_auc)
)

grid = 20 significa “prueba 20 combinaciones”. tune genera automáticamente un latin hypercube (cobertura uniforme del espacio de búsqueda).

Pero hay un problema: mtry no tiene rango por defecto. Hay que especificarlo:

parametros <- extract_parameter_set_dials(modelo_spec) |>
  update(mtry = mtry(range = c(2, 10)))

res <- tune_grid(
  wf,
  resamples = folds,
  grid      = parametros |> grid_latin_hypercube(size = 20),
  metrics   = metric_set(roc_auc)
)

update(mtry = mtry(range = c(2, 10))) declara que mtry se buscará entre 2 y 10.

Tipos de grid

Tipo Cuándo usarlo
grid_regular() Cuadrícula uniforme. Útil para visualizar superficies con pocos hiperparámetros (≤ 3).
grid_latin_hypercube() Muestreo cuasi-aleatorio que cubre el espacio uniformemente. El default sensato.
grid_random() Aleatorio puro. Más barato pero menos eficiente que latin hypercube.
grid_max_entropy() Optimiza diversidad. Útil con muchos hiperparámetros.

Ejemplo regular para 2 hiperparámetros:

parametros |> grid_regular(levels = 5)   # 5x5 = 25 combinaciones

Latin hypercube es lo recomendado para 3+ hiperparámetros. Cubre el espacio mejor que regular sin el coste exponencial.

tune_bayes(): búsqueda inteligente

tune_grid() prueba combinaciones predefinidas sin aprender de las anteriores. tune_bayes() aprende: cada evaluación informa a la siguiente, concentrando búsqueda en regiones prometedoras.

set.seed(123)
res_bayes <- tune_bayes(
  wf,
  resamples = folds,
  initial   = 5,           # 5 combinaciones aleatorias para arrancar
  iter      = 25,          # 25 iteraciones bayesianas
  metrics   = metric_set(roc_auc),
  control   = control_bayes(no_improve = 10, verbose = TRUE)
)

Cuándo usar Bayes:

  • Modelos lentos donde cada fit cuesta minutos, quieres maximizar información por intento.
  • Espacios de búsqueda grandes (≥ 4 hiperparámetros).
  • Tienes idea aproximada del rango pero no del punto óptimo.

Cuándo NO: si los hiperparámetros son discretos con pocos niveles (ej. min_n ∈ {1, 5, 10}), grid simple es más eficiente.

Racing methods: acelerar el grid

Cuando muchas combinaciones son claramente peores que otras, no tiene sentido evaluarlas en todos los folds. Racing methods descartan candidatos pronto:

library(finetune)

set.seed(123)
res_race <- tune_race_anova(
  wf,
  resamples = folds,
  grid      = 50,
  metrics   = metric_set(roc_auc),
  control   = control_race(verbose_elim = TRUE)
)

Cómo funciona: tras evaluar todas las combinaciones en el primer fold, hace un ANOVA contra el mejor candidato. Las que son significativamente peores se eliminan. En folds siguientes, solo entran los supervivientes.

Resultado: en lugar de combinaciones × folds fits, solo combinaciones × algunos_folds + supervivientes × resto_folds. Speedup típico: 3-5× sin pérdida notable de calidad.

tune_race_anova es del paquete finetune (no tune directamente). tune_race_win_loss es una variante.

Seleccionar el mejor candidato

Tras el tuning, eliges:

mejor <- select_best(res, metric = "roc_auc")
mejor
#> # A tibble: 1 × 3
#>    mtry min_n .config
#>   <int> <int> <chr>
#> 1     5     8 Preprocessor1_Model14

select_best() da el candidato con mejor métrica.

Alternativa más conservadora: select_by_one_std_err():

mejor_robusto <- select_by_one_std_err(res, metric = "roc_auc", -mtry)

Esto elige el candidato más simple (menor mtry) cuyo rendimiento esté dentro de un error estándar del mejor absoluto. La filosofía: si dos candidatos están estadísticamente empatados, prefiere el más simple, menos sobreajuste, mejor generalización fuera de muestra.

Esta heurística (introducida por Breiman) suele ser preferible al mejor absoluto cuando el espacio de hiperparámetros tiene mesetas planas.

finalize_workflow: cerrar el ciclo

Hasta ahora, tu workflow tenía tune() como marcador. Para entrenar el modelo final, hay que sustituir los marcadores por los valores ganadores:

wf_final <- wf |>
  finalize_workflow(mejor)

wf_final
#> ══ Workflow ════════════════════════════════════════════════════════════════════
#> Preprocessor: Recipe
#> Model: rand_forest()
#>
#> ── Preprocessor ────────────────────────────────────────────────────────────────
#> 3 Recipe Steps
#>
#> ...
#>
#> ── Model ───────────────────────────────────────────────────────────────────────
#> Random Forest Model Specification (classification)
#>
#> Main Arguments:
#>   mtry = 5
#>   trees = 500
#>   min_n = 8
#>
#> Computational engine: ranger

mtry y min_n ya no son tune(), son los valores concretos. Ahora puedes entrenar sobre todo el train set (no solo folds) y evaluar en test una sola vez:

# Entrenar en todo el train
wf_fit_final <- fit(wf_final, data = train)

# Evaluar en test (la última y única vez)
predict(wf_fit_final, new_data = test, type = "prob") |>
  bind_cols(test |> select(outcome)) |>
  roc_auc(truth = outcome, .pred_positivo)

O en una sola llamada con last_fit():

final <- last_fit(wf_final, split)
collect_metrics(final)
collect_predictions(final)

last_fit() toma el split original, entrena con training(split) y evalúa con testing(split). Es la operación canónica de “evaluación final”.

Trampas habituales

  • Tunear sobre el test set. Pasar testing(split) como resamples (en vez de los folds del train) es el error más caro. El “mejor” elegido será específico a ese test, y la evaluación final ya no será válida. Test es intocable durante tuning.
  • Espacios de búsqueda mal escalados. Para penalty en lasso, el rango razonable es 10^(-5) a 10^(0). Si pasas penalty = c(0.001, 100) sin escala log, el grid concentra todas las muestras cerca de 100 y no explora valores pequeños. Usa grid_*(size = N) con dials::penalty() que ya viene en escala log.
  • select_best() con muchos hiperparámetros y muestra pequeña. Con grid grande y n pequeño, el “mejor” puede ser ruido. select_by_one_std_err() mitiga esto eligiendo el modelo más simple en empate estadístico.
  • No usar paralelismo. Tuning sin paralelismo es lento. future::plan(multisession, workers = 4) antes del tune_grid() paraleliza folds y combinaciones automáticamente, 4× speedup gratis.

En la siguiente entrega

Has aprendido a optimizar hiperparámetros honestamente. La siguiente pieza son las métricas, qué se calcula en cada evaluación, y cómo elegir la métrica correcta para tu problema. Clasificación, regresión, balanceo, calibración. Lo siguiente.