tune: optimizar hiperparámetros sin trampas
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 combinacionesLatin 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
fitcuesta 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_Model14select_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: rangermtry 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)comoresamples(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
penaltyen lasso, el rango razonable es10^(-5)a10^(0). Si pasaspenalty = c(0.001, 100)sin escala log, el grid concentra todas las muestras cerca de 100 y no explora valores pequeños. Usagrid_*(size = N)condials::penalty()que ya viene en escala log. select_best()con muchos hiperparámetros y muestra pequeña. Con grid grande ynpequeñ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 deltune_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.