Unidad 3(Random Forest): Determinación de Sistemas de Aprendizaje automático

Random Forest

Code
library(tidyverse)
# library(rpart)
# library(rpart.plot)
# library(bayesQR)
library(broom)
library(corrplot)
# library(yardstick)
library(pROC)
# library(ggrepel)
# library(purrr)
library(plotly)
library(randomForest)
library(skimr)
library(MASS)

Algoritmos de Machine Learning

  • Regresión Lineal

  • Regresión Logística

  • Árboles de Decisión

  • Random Forest

  • k Nearest Neighbor (KNN)

  • SVM

  • Naive Bayes

  • Clustering jerárquico

  • K-Means

  • PCA

  • Redes Neuronales

  • Aprendizaje profundo

Bibliografía

  • Bagnato (2022)

  • Fávero, Belfiore, and de Freitas Souza (2023)

  • Rodrigo (2020)

Aproximación a la idea de “Bosque aleatorio”

Imagínate que tienes un grupo de amigos que te ayudan a tomar decisiones. Cada uno de ellos te da su opinión basada en sus experiencias y conocimientos. Al final, decides basándote en la mayoría de sus opiniones. Random Forest funciona de manera similar, pero en lugar de amigos, tienes un conjunto de árboles de decisión.

Cada árbol se entrena con una muestra aleatoria de los datos (con reemplazo) y al hacer una predicción, cada árbol vota. La predicción final es el resultado de la mayoría de los votos de todos los árboles. Esta técnica mejora la precisión de la predicción y controla el sobreajuste, que es un problema común cuando se usa un solo árbol de decisión.

Ensemble

Las técnicas de ensemble combinan múltiples modelos en uno nuevo con el objetivo equilibrar sesgo vs varianza, consiguiendo así mejores predicciones que cualquiera de los modelos individuales originales. Dos de los tipos de ensemble más utilizados son:

Bagging: Se ajustan múltiples modelos, cada uno con un subconjunto distinto de los datos de entrenamiento. Para predecir, todos los modelos que forman el agregado participan aportando su predicción. Como valor final, se toma la media de todas las predicciones (variables continuas) o la clase más frecuente (variables categóricas). Random Forest está dentro de esta categoría.

Boosting: Se ajustan secuencialmente múltiples modelos sencillos, llamados weak learners, de forma que cada modelo aprende de los errores del anterior. Como valor final, al igual que en bagging, se toma la media de todas las predicciones (variables continuas) o la clase más frecuente (variables cualitativas). Tres de los métodos de boosting más empleados son AdaBoost, Gradient Boosting y Stochastic Gradient Boosting. Veremos alguno de ellos en la unidad siguiente.

Fundamentos de Random Forest

Random Forest es un algoritmo de aprendizaje ensemble, lo que significa que combina las predicciones de varios modelos (en este caso, árboles de decisión) para generar una salida más precisa y robusta. Se basa en dos conceptos clave del ML: el bagging (bootstrap aggregating) y la selección aleatoria de variables predictivas.

Dado que su origen son los árboles de decisión, se utiliza tanto para problemas de clasificación como de regresión. En clasificación, el resultado se obtiene con la moda de la clase en el conjunto de árboles o en probabilidades como la media de las probabilidades de cada clase del conjunto de árboles. En regresión se usa la media obtenida en cada árbol.

Bagging

Este método mejora la estabilidad y precisión de los algoritmos de machine learning. Consiste en generar múltiples conjuntos de datos de entrenamiento mediante muestreo aleatorio con reemplazo (bootstrap), entrenar un modelo en cada uno de estos conjuntos y luego combinar sus predicciones. En el caso de Random Forest, cada conjunto de datos de entrenamiento se usa para entrenar un árbol de decisión diferente.

Selección aleatoria de predictores

Al construir cada árbol, en cada división o nodo, en lugar de buscar la mejor característica entre todas las posibles para dividir el conjunto de datos, Random Forest selecciona un subconjunto aleatorio de características y busca la mejor entre este subconjunto. Esto añade diversidad a los modelos, aumentando la robustez del conjunto total y haciéndolo menos sensible a outliers.

Antes de cada división, se seleccionan aleatoriamente m predictores de un total de p. La diferencia en el resultado dependerá del valor m escogido. Si m=p, los resultados de random forest y bagging son equivalentes.

Algunas recomendaciones son:

  • La raíz cuadrada del número total de predictores para problemas de clasificación. \(m \approx \sqrt{p}\)

  • Un tercio del número de predictores para problemas de regresión. \(m\approx \frac{p}3\)

  • Si los predictores están muy correlacionados, valores pequeños de m consiguen mejores resultados.

Aumentar el número de árboles, no produce overfit y mejora el modelo hasta cierto número en el que se estabiliza. No tiene sentido seguir aumentando, pues se debe buscar reducir el coste computacional.

Paquete randomForest e Hiperparámetros

Code
# install.packages("randomForest)
# install.packages("ranger")

En R, el paquete que tradicionalmente se ha usado para este algoritmo es randomForest. En la documentación de la función con el mismo nombre podemos ver que algunos de los atributos son hiperparámetros.

En la actualidad, el paquete ranger, mejora la implementación original e incluye más opciones de configuración.

  • ntree: Número de árboles (n_estimators). Más árboles aumentan la precisión pero también el costo computacional. Por defecto, suele estar configurado en 500

  • Profundidad máxima del árbol (max_depth): La profundidad máxima de cada árbol. Limitar la profundidad puede ayudar a prevenir el sobreajuste. maxnodes

  • mtry: Número máximo de variables predictoras (max_features). El número de características a considerar cuando se busca la mejor división.

  • nodesize: Mínimo número de muestras para dividir (min_samples_split): El número mínimo de muestras que debe tener un nodo antes de que pueda ser dividido.

Regresión con Random Forest

Code
data("Boston")
Code
glimpse(Boston)
Rows: 506
Columns: 14
$ crim    <dbl> 0.00632, 0.02731, 0.02729, 0.03237, 0.06905, 0.02985, 0.08829,…
$ zn      <dbl> 18.0, 0.0, 0.0, 0.0, 0.0, 0.0, 12.5, 12.5, 12.5, 12.5, 12.5, 1…
$ indus   <dbl> 2.31, 7.07, 7.07, 2.18, 2.18, 2.18, 7.87, 7.87, 7.87, 7.87, 7.…
$ chas    <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ nox     <dbl> 0.538, 0.469, 0.469, 0.458, 0.458, 0.458, 0.524, 0.524, 0.524,…
$ rm      <dbl> 6.575, 6.421, 7.185, 6.998, 7.147, 6.430, 6.012, 6.172, 5.631,…
$ age     <dbl> 65.2, 78.9, 61.1, 45.8, 54.2, 58.7, 66.6, 96.1, 100.0, 85.9, 9…
$ dis     <dbl> 4.0900, 4.9671, 4.9671, 6.0622, 6.0622, 6.0622, 5.5605, 5.9505…
$ rad     <int> 1, 2, 2, 3, 3, 3, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4,…
$ tax     <dbl> 296, 242, 242, 222, 222, 222, 311, 311, 311, 311, 311, 311, 31…
$ ptratio <dbl> 15.3, 17.8, 17.8, 18.7, 18.7, 18.7, 15.2, 15.2, 15.2, 15.2, 15…
$ black   <dbl> 396.90, 396.90, 392.83, 394.63, 396.90, 394.12, 395.60, 396.90…
$ lstat   <dbl> 4.98, 9.14, 4.03, 2.94, 5.33, 5.21, 12.43, 19.15, 29.93, 17.10…
$ medv    <dbl> 24.0, 21.6, 34.7, 33.4, 36.2, 28.7, 22.9, 27.1, 16.5, 18.9, 15…
Code
set.seed(131)
boston.rf <- randomForest(medv ~ ., data=Boston)
print(boston.rf)

Call:
 randomForest(formula = medv ~ ., data = Boston) 
               Type of random forest: regression
                     Number of trees: 500
No. of variables tried at each split: 4

          Mean of squared residuals: 10.02171
                    % Var explained: 88.13
Code
residuos <- Boston$medv - boston.rf$predicted
Code
MSE <- sum(residuos**2)/(nrow(Boston))
RMSE <- sqrt(MSE)
RMSE
[1] 3.165708

Clasificación con Random Forest

Code
bank <- read.csv("data/bank.csv")
Code
glimpse(bank)
Rows: 11,162
Columns: 17
$ age       <int> 59, 56, 41, 55, 54, 42, 56, 60, 37, 28, 38, 30, 29, 46, 31, …
$ job       <chr> "admin.", "admin.", "technician", "services", "admin.", "man…
$ marital   <chr> "married", "married", "married", "married", "married", "sing…
$ education <chr> "secondary", "secondary", "secondary", "secondary", "tertiar…
$ default   <chr> "no", "no", "no", "no", "no", "no", "no", "no", "no", "no", …
$ balance   <int> 2343, 45, 1270, 2476, 184, 0, 830, 545, 1, 5090, 100, 309, 1…
$ housing   <chr> "yes", "no", "yes", "yes", "no", "yes", "yes", "yes", "yes",…
$ loan      <chr> "no", "no", "no", "no", "no", "yes", "yes", "no", "no", "no"…
$ contact   <chr> "unknown", "unknown", "unknown", "unknown", "unknown", "unkn…
$ day       <int> 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, …
$ month     <chr> "may", "may", "may", "may", "may", "may", "may", "may", "may…
$ duration  <int> 1042, 1467, 1389, 579, 673, 562, 1201, 1030, 608, 1297, 786,…
$ campaign  <int> 1, 1, 1, 1, 2, 2, 1, 1, 1, 3, 1, 2, 4, 2, 2, 1, 3, 1, 2, 1, …
$ pdays     <int> -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, …
$ previous  <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ poutcome  <chr> "unknown", "unknown", "unknown", "unknown", "unknown", "unkn…
$ deposit   <chr> "yes", "yes", "yes", "yes", "yes", "yes", "yes", "yes", "yes…
Code
bank_pp <- bank %>%
  mutate_if(is.character, as.factor) 
Code
glimpse(bank_pp)
Rows: 11,162
Columns: 17
$ age       <int> 59, 56, 41, 55, 54, 42, 56, 60, 37, 28, 38, 30, 29, 46, 31, …
$ job       <fct> admin., admin., technician, services, admin., management, ma…
$ marital   <fct> married, married, married, married, married, single, married…
$ education <fct> secondary, secondary, secondary, secondary, tertiary, tertia…
$ default   <fct> no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, …
$ balance   <int> 2343, 45, 1270, 2476, 184, 0, 830, 545, 1, 5090, 100, 309, 1…
$ housing   <fct> yes, no, yes, yes, no, yes, yes, yes, yes, yes, yes, yes, ye…
$ loan      <fct> no, no, no, no, no, yes, yes, no, no, no, no, no, yes, no, n…
$ contact   <fct> unknown, unknown, unknown, unknown, unknown, unknown, unknow…
$ day       <int> 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, …
$ month     <fct> may, may, may, may, may, may, may, may, may, may, may, may, …
$ duration  <int> 1042, 1467, 1389, 579, 673, 562, 1201, 1030, 608, 1297, 786,…
$ campaign  <int> 1, 1, 1, 1, 2, 2, 1, 1, 1, 3, 1, 2, 4, 2, 2, 1, 3, 1, 2, 1, …
$ pdays     <int> -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, …
$ previous  <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ poutcome  <fct> unknown, unknown, unknown, unknown, unknown, unknown, unknow…
$ deposit   <fct> yes, yes, yes, yes, yes, yes, yes, yes, yes, yes, yes, yes, …
Code
set.seed(123)
bank.rfmdl <- randomForest(deposit ~ ., data = bank_pp, ntree = 250 )
Code
print(bank.rfmdl)

Call:
 randomForest(formula = deposit ~ ., data = bank_pp, ntree = 250) 
               Type of random forest: classification
                     Number of trees: 250
No. of variables tried at each split: 4

        OOB estimate of  error rate: 13.99%
Confusion matrix:
      no  yes class.error
no  4829 1044  0.17776264
yes  518 4771  0.09793912
Code
deposits <- bank_pp$deposit == "yes"
Code
predicted_probs <- predict(bank.rfmdl, data = bank_pp, type = "prob")
predicted_probs <- predicted_probs[,2]
Code
ROC <- roc(deposits, predicted_probs)
plot(ROC, col = "blue")    
text(0.5, 0.2, paste("AUC =", round(auc(ROC), 3)))

Ejercicios

1.- Repite los ejercicios de regresión y clasificación usando el paquete ranger y/o h2o. Busca información y explica las diferencias principales en los flujos de trabajo (funciones y parámetros).

2.- Dado el siguiente código:

Code
num_trees <- seq(50, 600, by=50) # Evaluar modelos con diferentes números de árboles
oob_error <- numeric(length(num_trees)) # Para almacenar el error OOB de cada modelo

# Bucle para entrenar modelos y recoger errores OOB
for (i in seq_along(num_trees)) {
  set.seed(123) # Para reproducibilidad
  rf_model <- randomForest(deposit ~ ., data=bank_pp, ntree=num_trees[i], mtry=4, keep.forest=FALSE)
  # Acceder al último valor del error OOB
  oob_error[i] <- rf_model$err.rate[length(rf_model$err.rate)]
}

# Creación de un dataframe para los resultados
results <- data.frame(num_trees, oob_error)

# Gráfico de cómo converge el error OOB con diferentes números de árboles
ggplot(results, aes(x=num_trees, y=oob_error)) +
  geom_line() +
  geom_point() +
  theme_minimal() +
  labs(title="Convergencia del Error OOB en función del Número de Árboles",
       x="Número de Árboles",
       y="Error OOB")

Encuentra el número óptimo de árboles a partir del cual los resultados convergen y no se mejora por incrementar el número de árboles. Repite el código para el algoritmo de regresión.

3.- Con la ayuda de chatGPT, intenta optimizar el parámetro ntree, usando el paquete ranger y/o h2o. ¿Mejora el rendimiento?

References

Bagnato, Juan Ignacio. 2022. Aprende Machine Learning En Español: Teoría + Práctica Python. Leanpub. http://leanpub.com/aprendeml.
Fávero, Luiz Paulo, Patrícia Belfiore, and Rafael de Freitas Souza. 2023. Chapter 15 - Binary and Multinomial Logistic Regression Models. Edited by Luiz Paulo Fávero, Patrícia Belfiore, and Rafael de Freitas Souza. Academic Press. https://doi.org/https://doi.org/10.1016/B978-0-12-824271-1.00008-1.
Rodrigo, Joaquín Amat. 2020. “Árboles de Decisión, Random Forest, Gradient Boosting y C5.0.” cienciadedatos.net. https://cienciadedatos.net/documentos/33_arboles_de_prediccion_bagging_random_forest_boosting#Random_Forest.