parsnip: especificar modelos agnóstico al engine

r
machine-learning
tidymodels
La interfaz unificada para modelos de ML en R. linear_reg(), rand_forest(), set_engine() y set_mode(). Intercambiar ranger ↔︎ xgboost ↔︎ glmnet sin reescribir el pipeline.

¿Por qué desacoplar modelo de implementación?

Antes de parsnip, cada paquete de ML en R tenía su propia API. Entrenar un random forest con randomForest::randomForest() requería argumentos distintos a ranger::ranger(), que a su vez eran distintos a randomForestSRC::rfsrc(). Cambiar de implementación implicaba reescribir el código.

parsnip resuelve esto con una interfaz única: tú declaras “quiero un random forest con 500 árboles” en sintaxis estándar, y parsnip traduce a la API del paquete que elijas. Cambiar de ranger a xgboost es una sola línea.

library(tidymodels)

Especificar un modelo: forma básica

Una spec de parsnip tiene tres componentes:

  1. El tipo de modelo: qué hace conceptualmente (regresión lineal, random forest, etc.).
  2. El engine: qué paquete implementa el cálculo.
  3. El modo: "classification" o "regression".
modelo_rf <- rand_forest(trees = 500, mtry = 4, min_n = 10) |>
  set_engine("ranger") |>
  set_mode("classification")

Léelo: “un random forest con 500 árboles, mtry = 4 y nodos hoja con al menos 10 observaciones, implementado con ranger, en modo clasificación”.

Los argumentos del modelo se llaman main arguments, son los que tienen sentido independientemente del engine. Para random forest son trees, mtry, min_n. Para regresión lineal son penalty y mixture. Cada tipo de modelo tiene los suyos.

set_engine y set_mode

Los modelos en parsnip se separan en estas dos decisiones explícitas:

set_engine(), elige el paquete subyacente:

rand_forest() |> set_engine("ranger")            # paquete ranger
rand_forest() |> set_engine("randomForest")      # paquete randomForest legacy
rand_forest() |> set_engine("xgboost")           # XGBoost (sí, también puede)
rand_forest() |> set_engine("spark")             # H2O via sparklyr

set_mode(), "classification" o "regression". No es opcional, muchos modelos admiten ambos modos y la spec se construye distinta en cada caso.

modelo <- rand_forest()                           # error: falta mode
modelo <- rand_forest() |> set_mode("regression") # OK

Intercambiar engines: la prueba real

Donde parsnip brilla es cuando quieres comparar implementaciones. La spec no cambia. Solo set_engine:

# Mismo modelo conceptual, 3 implementaciones distintas
modelo_ranger <- rand_forest(trees = 500, mtry = 4, min_n = 10) |>
  set_engine("ranger") |>
  set_mode("classification")

modelo_rf_legacy <- rand_forest(trees = 500, mtry = 4, min_n = 10) |>
  set_engine("randomForest") |>
  set_mode("classification")

modelo_xgb <- rand_forest(trees = 500, mtry = 4, min_n = 10) |>
  set_engine("xgboost") |>
  set_mode("classification")

El resto del pipeline (recipe, workflow, métricas) no cambia. Esto te permite tener un experimento sistemático cambiando una sola línea.

Pasar argumentos al engine

Los main arguments cubren lo común. Para argumentos específicos del engine (que no tienen sentido en todos los implementadores), se pasan dentro de set_engine():

modelo <- rand_forest(trees = 500, mtry = 4) |>
  set_engine("ranger",
             importance = "impurity",          # específico de ranger
             num.threads = 4,                  # específico de ranger
             splitrule = "extratrees") |>
  set_mode("classification")

Estos argumentos pasan tal cual a ranger::ranger(). Si cambias el engine a xgboost, estos argumentos ya no aplican.

Esta separación es deliberada: parsnip no abstrae todas las opciones (sería imposible). Abstrae las comunes y deja una vía limpia para personalización específica.

Catálogo de modelos disponibles

# Ver todos los modelos disponibles
show_engines("rand_forest")
#> # A tibble: 7 × 2
#>   engine       mode
#>   <chr>        <chr>
#> 1 ranger       classification
#> 2 ranger       regression
#> 3 randomForest classification
#> 4 randomForest regression
#> 5 spark        classification
#> 6 spark        regression
#> 7 xgboost      classification
# Ver detalles de un modelo + engine concretos
show_model_info("rand_forest")

El catálogo completo está en la web de tidymodels. Los más usados:

Modelo Función parsnip Engines típicos
Regresión lineal linear_reg() lm, glmnet, stan
Regresión logística logistic_reg() glm, glmnet, keras
Random forest rand_forest() ranger, randomForest, xgboost
Boosted trees boost_tree() xgboost, lightgbm, C5.0
SVM svm_rbf(), svm_linear() kernlab, liquidSVM
Red neuronal (simple) mlp() nnet, keras, brulee
K-NN nearest_neighbor() kknn
Naive Bayes naive_Bayes() klaR, naivebayes

Más allá del modelo: tune() como placeholder

Una funcionalidad clave de parsnip es declarar hiperparámetros sin valor, marcándolos como a tunear:

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

tune() no es un valor, es un marcador que dice “este hiperparámetro se decide más tarde, en la fase de tuning”. Lo veremos en detalle en el tutorial de tune. Por ahora basta con saber que es así como se declara la intención de optimizar.

Inspeccionar la spec

Antes de fit(), puedes ver qué se va a ejecutar:

translate(modelo_ranger)
#> Random Forest Model Specification (classification)
#>
#> Main Arguments:
#>   mtry = 4
#>   trees = 500
#>   min_n = 10
#>
#> Computational engine: ranger
#>
#> Model fit template:
#> ranger::ranger(x = missing_arg(), y = missing_arg(),
#>     mtry = min_cols(~4, x), num.trees = ~500, min.node.size = ~10,
#>     num.threads = 1, verbose = FALSE, seed = sample.int(10^5, 1))

translate() te muestra la llamada exacta que parsnip va a hacer al engine. Útil para depurar, si el modelo se comporta extraño, lee qué se está ejecutando realmente.

Trampas habituales

  • Olvidar set_mode(). Modelos que admiten ambos modos (random forest, boost_tree, neural net) requieren set_mode() explícito. Si lo omites, fit() falla con un error que no siempre apunta al problema real.
  • Engine no instalado. parsnip no instala los paquetes subyacentes. Si declaras set_engine("xgboost") sin tener xgboost instalado, el fit() falla. install.packages("xgboost") antes.
  • Confundir main arguments con engine-specific. mtry = 4 va en rand_forest(). num.threads = 4 va en set_engine() (específico de ranger). Si pones todo en rand_forest(), parsnip se queja.
  • Cambiar trees = 500 en parsnip esperando que ntree = 500 también cambie en randomForest. La traducción la hace parsnip, tú usas trees, siempre. Si pasas ntree directamente, parsnip no lo reconoce.

En la siguiente entrega

Tienes recipe (preprocesado) y model spec (modelo). El siguiente paquete los combina en un único objeto entrenable y serializable: workflows. Es lo que hace que el pipeline sea un objeto coherente en lugar de dos piezas sueltas. Lo siguiente.