Machine Learning en R con caret

El Machine Learning es uno de los usos principales del lenguaje de programación R. Existen muchos modelos dentro del mundo del Machine Learning y aún hay muchos más paquetes de R para poder usar esos algoritmos y caret es uno de los principales.

Y es que, aunque el hecho de que haya muchas librerías implementando modelos es algo buen es algo muy bueno, también tiene sus puntos negativos, ya que hay cuestiones (como encontrar el valor óptimio de los hiperparámetros) que hacerlo cambia según el modelo… Y es un enrollo.

Por suerte, en R contamos con caret, en mi opinión, un fantástico paquete que te permitirá aplicar más de 230 modelos de Machine Learning, todos ellos siguiendo la misma estructura y facilitándote mucho el trabajo. Además, el paquete caret de R cuenta con un montón de funciones fantásticas que te van a facilitar mucho el trabajo en las diferentes etapas del proceso de Machine Learning: feature selection, data splitting, validación del modelo, etc.

Como ves, el paquete caret de R es fantástico y sin duda, si programas en R es uno de los paquetes que deberías conocer en profundidad. Y eso es justamente lo que vas a hacer en este post. Así que, ¡vamos a ello

Cómo preparar los datos con Caret

Antes de empezar lanzar un modelo de Machine Learning, primero debemos realizar varios pasos, como Feature Selection imputación de valores perdidos, dumificación de datos, etc.

Estos son muchos pasos que, generalmente, tenemos que programar a mano. Por suerte, caret ofrece funciones para ayudarnos en gran parte de los pasos del proceso de preparación de datos. ¡Empecemos con Feature Selection!

Cómo realizar Feature Selection

Uno de los aspectos fundamentales en la selección de variables es comprobar si su varianza es cero o cercana a cero. Esto tiene mucho sentido: si la varianza es cercana a cero, eso significa que no hay mucha variación dentro de los datos, es decir, que casi todas las observaciones tienen valores similares.

Por tanto, las variables con varianza 0 suelen ser descartadas, puesto que es muy probable que solo añadan ruido a nuestro modelo.

Así pues, comprobar con caret si las observaciones tienen, o no, varianza cero es muy sencillo y lo podemos hacer con la función nearZeroVar.

Veámos cómo funciona con un ejemplo del dataset Sacramento, el cual incluye información sobre los precios de las casas en Sacramento. Primero de todo, por si no estás familiarizado con el dataset, vamos a visualizarlo:

library(caret)
data(Sacramento)
str(Sacramento[1:3,])
'data.frame':   3 obs. of  9 variables:
 $ city     : Factor w/ 37 levels "ANTELOPE","AUBURN",..: 34 34 34
 $ zip      : Factor w/ 68 levels "z95603","z95608",..: 64 52 44
 $ beds     : int  2 3 2
 $ baths    : num  1 1 1
 $ sqft     : int  836 1167 796
 $ type     : Factor w/ 3 levels "Condo","Multi_Family",..: 3 3 3
 $ price    : int  59222 68212 68880
 $ latitude : num  38.6 38.5 38.6
 $ longitude: num  -121 -121 -121

Como vemos, tenemos varias variables numéricas (número de baños, número de camas, precio, latitud y longitud). Veámos si tienen o no varianza cero.

numeric_cols = sapply(Sacramento, is.numeric)
variance = nearZeroVar(Sacramento[numeric_cols], saveMetrics = T)
variance

Como vemos, si pasamos el argumento saveMetrics, guarda los valores que ha utilizado para los cálculos. Así pues, uno de los valores que devuelve es nzv (near-zero-variance), que en todos los casos es falso.

Así pues, podemos usar todas nuestras variables numéricas en la predicción de nuestro modelo, al menos de momento.

Otras de las cuestiones importantes, es la correlación entre variables. Existen varios modelos como la regresión lineal y la regresión logística que una de las bases del modelo es la no colinealidad o multicolinealidad. Es decir, que en varios modelos no podemos meter variables correlacionadas.

Así pues, veámos cómo comprobar la correlación entre variables en R con caret.

Cómo encontrar variables correlacionadas con caret

Encontrar variables correlacionadas en R usando caret es muy sencillo. Para ello solo hay que pasar a la función findCorrelation una matriz de correlaciones. Con esto, caret nos dirá qué variables hay que eliminar (si es que hay alguna).

Veámos cómo funciona:

sacramento_cor = cor(Sacramento[numeric_cols])
findCorrelation(sacramento_cor)
integer(0)

Como vemos, en este caso no hay variables correlacionadas, por lo que caret nos dice que no hay ninguna variable a eliminar. Sin embargo, si creáramos una nueva variable correlacionada, veremos como sí nos diría que hay problemas. Veamos:

fake_data = data.frame(
  variable1 = 1:20,
  variable2 = (1:20)*2,
  variable3 = runif(20),
  variable4 = runif(20) * runif(20)
)

findCorrelation(cor(fake_data), 
                verbose = T,
                names = T)
Compare row 1  and column  2 with corr  1 
  Means:  0.438 vs 0.269 so flagging column 1 
All correlations <= 0.9 
[1] "variable1"

Como vemos, la función findCorrelation identifica que variable1 está correlacionada con variable2, e indica que esta debería eliminarse. Pero, ¿qué pasaría si la variable fuera una transformación lineal de otras variables? Es decir, si tuviéramos una variable5, por ejemplo, que sea la suma de variable1 y variable3. Esto seguiría siendo un problema, a pesar de que las variables no estén correlacionadas.

Pues, precisamente para detectar estos casos, caret incluye la función findLinearCombos. Veámos cómo funciona:

# Creo una nueva variable fake
fake_data$variable5 = fake_data$variable1 + 2*fake_data$variable3

# Busco si hay combinaciones lineales
findLinearCombos(fake_data)
$linearCombos
$linearCombos[[1]]
[1] 2 1

$linearCombos[[2]]
[1] 5 1 3


$remove
[1] 2 5

Como vemos, la función findLinearCombos nos indica que las columnas 1 y 2 son combinaciones lineales y que también lo son las columnas 5,1 y 3. Es por ello que nos recomienda eliminar las columnas 2 y la columna 5.

Como vemos, podemos realizar cuestiones muy importantes del proceso de Feature Selection en R gracias al paquete caret y, además, de una forma muy sencilla.

Pero eso no es todo. Y es que el paquete caret de R también ayuda mucho en la transformación de los datos. ¡Veámos cómo lo hace!

Cómo transformar datos con Caret

Dentro de lo que son las transformaciones a la hora de afrontar un problema de Machine Learning tenemos:

  • Creación de variables dumi: muchos modelos no pueden trabajar con variables categóricas. En su lugar, la dumifican, es decir crean n-1 columnas (donde n es el número de categorías), y que cada una de estas columnas indica la presencia (1) o ausencia (0) de ese valor en concreto. Aunque muchos modelos (como la regresión logística) lo hagan ellos solos, otros modelos (como xgboost) requieren que lo hagas de forma manual.
  • Escalado de datos: consiste en normalizar la escala de los datos, ya que esta es muy importante en algoritmos como los modelos de regularización (Ridge, Lasso y Elastic Net) o kNN, entre otros.
  • Imputación de valores perdidos: la gran mayoría de modelos (salvo los basados en árboles) no pueden trabajar con valores perdidos. Es por ello que, cuando tenemos valores perdidos o los imputamos o eliminamos esas observaciones o variables. Por suerte, caret permite imputar valores perdidos usando varios tipos de modelos de una forma muy sencilla.
  • Reducción de la dimensionalidad. Cuando trabajamos en un problema con un alto nivel de dimensionalidad, es decir, con muchas variables, suele ser interesante reducir el número de variables manteniendo la mayor cantidad de variabilidad posible. Este proceso se suele realizar con un análisis de componentes principales o PCA.

Así pues, veámos cómo hacer todos estos tipos de transformaciones en nuestros modelos de machine learning en R con caret, la gran mayoría de elloscon una misma función: preProcess.

Cómo crear variables dummy con caret

Crear variables dummy con caret es muy sencillo, simplemente tenemos que usar la función dummyVars y aplicar un predict para obtener los datos resultantes.

head(sacramento_dummy)
  type.Condo type.Multi_Family type.Residential
1          0                 0                1
2          0                 0                1
3          0                 0                1
4          0                 0                1
5          0                 0                1
6          1                 0                0

Como vemos, caret ha convertido una única columna (type) en tres columnas (una por categoría), cada una de ellas siendo binaria. Sin embargo, no ha eliminado una de las categorías, generando redundancia. Al fin y al cabo: Condo = 0 &Multi_Family = 0 –> Residential = 1.

Por suerte esto lo podemos indicar con el parámetro drop2nd == TRUE .

pre_dummy = dummyVars(price ~ type, data = Sacramento,
                      drop2nd = T)
sacramento_dummy = predict(pre_dummy, Sacramento)

head(sacramento_dummy)
  type.Condo type.Multi_Family type.Residential
1          0                 0                1
2          0                 0                1
3          0                 0                1
4          0                 0                1
5          0                 0                1
6          1                 0                0

Cómo escalar datos

Para escalar los datos, simplemente tenemos que pasar argumentos al parámetro method a la función preProcess de caret. Esta función, acepta dos tipos principales:

  • center: resta el promedio a los valores, de tal forma que todos tengan promedio 0.
  • scale: divide los valores entre la desviación estándar. De esta forma, los datos tendrán desviación típica 1.
  • range: normaliza los datos, haciendo que estos tengan un rango de 0 a 1.
preProcess(Sacramento, method = "center")
Created from 932 samples and 9 variables

Pre-processing:
  - centered (6)
  - ignored (3)

Como vemos, caret ha centrado los datos de 6 variables, correspondientes a las variables numéricas, ignorando 3 variables. Vemos el mensaje, pero no los datos. ¿Por qué?

La razón es que, la función preProcess de no está pensada para transformar los datos en el momento, sino para hacer la transformación en el proceso de entrenamiento (o de inferencia).

Sin embargo, podemos ver cómo quedan nuestros datos tras aplicar el preprocesamiento. Para ello, tenemos que pasar el preprocesamiento y nuestros datos a la función predict. Veámos cómo funciona.

preprocess = preProcess(Sacramento, method = "center")
predict(preprocess, Sacramento)[1:10,]

Como vemos, ahora sí que caret nos devuelve todos los datos con el procesamiento ya aplicado (en este caso, habiendo restado la media). Veámos, por ejemplo, cómo normalizaríamos los datos.

preprocess = preProcess(Sacramento, method = "range")
Sacramento_processed = predict(preprocess, Sacramento)

cat("--- Datos sin procesar ---","\n",
    "Min:", min(Sacramento$sqft),"\n",
    "Max:", max(Sacramento$sqft), "\n","\n",
    "--- Datos procesados ---","\n",
    "Min:", min(Sacramento_processed$sqft),"\n",
    "Max:", max(Sacramento_processed$sqft)
    )
--- Datos sin procesar --- 
 Min: 484 
 Max: 4878 

 --- Datos procesados --- 
 Min: 0 
 Max: 1

Como vemos, hemos normalizado los datos simplemente con una línea de código. Pero esto no es todo, puesto que la función preProcess permite hacer mucho más, como imputar valores perdidos. Veamos.

Cómo imputar valores perdidos con caret

Para imputar valores perdidos con caret, usaremos la función preProcess. En este caso, hay diferentes valores que le podemos pasar al parámetro method:

  • knnImpute: permite utilizar el algoritmo kNN para imputar valores perdidos. Como sabes (sino, lo explico en este post), el algoritmo kNN requiere que le indiques el número de vecinos a utilizar en la predicción. Es por ello que, si usamos el método knnImpute, también tendremos que indicar el parámetro k.
  • bagImpute: con este valor utilizaremos varios árboles de decisión para hacer la imputación de nuestro valor perdido.
  • medianImpute: como su nombre indica, imputa la mediana (en caso de una variable numérica). Esto suele ser preferible a imputar la media, puesto que el promedio puede verse afectado por outliers.

Veámos cómo funciona la imputación de valores perdidos con caret en la práctica. Para ello, lo primero de todo, vamos a “eliminar” algunos datos de nuestro dataset para simular que tenemos valores perdidos.

colSums(is.na(sacramento_missing))
     city       zip      beds     baths      sqft      type     price  latitude longitude 
        3         0         3         3         3         0         0         0         0 

Como vemos, ahora tenemos 4 variables con 3 valores perdidos cada uno. Veámos a ver cómo funcionan cada método de imputación:

# Realizamos la imputación
pre_knn = preProcess(sacramento_missing, 
                     method = "knnImpute", k = 2)

pre_bag = preProcess(sacramento_missing, 
                     method = "bagImpute")

pre_median = preProcess(sacramento_missing, 
                        method = "medianImpute")

# Obtenemos los datos
imputed_knn = predict(pre_knn, sacramento_missing)
imputed_bag = predict(pre_bag, sacramento_missing)
imputed_median = predict(pre_median, sacramento_missing)

# Comprobamos con el valor real
print(Sacramento[c(1,4,5), c(1,3,4,5)])
print(imputed_knn[c(1,4,5), c(1,3,4,5)]) # Uses normalized data
print(imputed_bag[c(1,4,5), c(1,3,4,5)])
print(imputed_median[c(1,4,5), c(1,3,4,5)])

Como vemos, hemos podido realizar la imputación de los valores perdidos de una forma muy sencilla. De momento ya hemos visto muchas cosas para el preprocesamiento de datos con caret: selección de variables, transformación de datos, imputación de NAs… ¡Pero aún hay más! Con caret puedes hacer cosas tan chulas como usar un PCA. ¡Veámos!

Cómo reducir la dimensionalidad

Cuando trabajamos en problemas de Machine Learning con muchas variables, muchas veces solemos tener problemas. Y es que, la gran mayoría de modelos no funcionan bien con muchas variables predictoras y, si lo hace, requieren de muchos datos.

En estos casos una buena opción suele ser aplicar un método de reducción de la dimensionalidad, como es el análisis de componentes principales o PCA.

Por suerte, aplicar un PCA en nuestros dataset en R es muy sencillo gracias a caret. Para ello simplemente debemos indicar el valor pca al parámetro method de la función preProcess. Asimismo, con el parámetro thresh podemos indicar el porcentaje de la variabilidad con la que nos queremos quedar.

pre_pca = preProcess(Sacramento, method = "pca", thresh = 0.8)
predict(pre_pca, Sacramento)

Como vemos, ahora el dataset cuenta con 6 columnas en vez de 9. Sí, lo sé, este no es el mejor ejemplo en el cual aplicar un PCA aporta mucho valor, pero, como vemos, podemos hacerlo y de una forma muy sencilla gracias a caret.

Con esto, ya hemos visto todas las opciones que ofrece la librería caret de cara a la transformación de datos. Pero las opciones van mucho, mucho más allá, sobre todo en la creación de modelos. Véamos qué ofrece.

Cómo crear modelos de machine learning con caret

Elegir el modelo de machine learning a utilizar

Cuando queremos crear un modelo de Machine Learning en R, generalmente cargamos una librería que contenga el algoritmo que a nosotros nos interesa. Por ejemplo, si queremos usar un Random Forest, cargaremos el paquete randomForest, mientras que si queremos usar AdaBoost, cargaremos el paquete ada.

Y aquí surge el primer problema, y es que cada paquete es diferente y tiene su propia implementación: algunos requieren que pases una fórmula, otros que pases los predictores y la variable dependiente por separado, algunos gestionan la dumificación, pero otros no…

Además, cada modelo tiene sus propios hiperparámetros y la forma de tunearlos cambia de paquete a paquete.

Pues bien, crear modelos de machine learning en R con caret es muy sencillo, ya que caret unifica la forma de crear y optimizar los hiperparámetros de 238 modelos diferentes.

Así pues, si queremos crear un modelo de machine learning con caret, lo primero es saber cómo se llama ese modelo dentro de caret. Esto lo podemos descubrir en esta página. Por ejemplo, ahí veremos que podemos llamar al modelo randomForest de la librería randomForest mediante el nombre rf.

Modelos de Random Forest disponibles en caret
Modelos de Random Forest disponibles en caret

Modelos de Random Forest disponibles en caret

Cómo hacer la partición de datos en train y test

Una vez hemos elegido nuestro modelo, tendremos que dividir los datos en train y test. Para ello, caret ofrece una función muy útil, llamada createDataPartition, la cual sirve para realizar esta partición.

La función es muy sencilla, simplemente hay que pasar nuestra variable dependiente y la proporción de datos que queremos que vayan a entrenamiento (generalmente entre el 0.7 y el 0.8 del total).

Con esto, la función createDataPartition devuelve los índices de las observaciones que deben ir a cada partición. Por defecto, la información la devuelve como lista, cosa que a mi, personalmente, no me gusta. Por suerte, podemos evitarlo indicando el parámetro list=FALSE.

Veámos cómo partir nuestros datos entre train y test con caret:

cat('Train rows: ', nrow(train),"\n",
    'Test rows: ', nrow(test),
    sep="")
Train rows: 747
Test rows: 185

Como vemos, hemos podido crear la partición de datos de una forma súper sencilla en caret. Visto esto, veámos cómo entrenar un modelo de machine learning en R con caret.

Cómo entrenar un modelo de machine learning con caret

Una vez hayamos definido el modelo, podemos crearlo de forma muy sencilla con la función train. Simplemente tendremos que pasar las variables independientes por un lado y la variable dependiente por otro e indicar el modelo en el parámetro method.

Sacramento$zip = NULL
Sacramento$city = NULL

indep_var = colnames(Sacramento) != "price"
model_rf = train(x = Sacramento[indep_var], 
                 y = Sacramento$price,
                 method = 'rf'
                 )

model_rf
Random Forest 

932 samples
  6 predictor

No pre-processing
Resampling: Bootstrapped (25 reps) 
Summary of sample sizes: 932, 932, 932, 932, 932, 932, ... 
Resampling results across tuning parameters:

  mtry  RMSE      Rsquared   MAE     
  2     76275.05  0.6534992  54482.84
  4     77632.80  0.6416936  55744.72
  6     78933.34  0.6310063  56864.50

RMSE was used to select the optimal model using the
 smallest value.
The final value used for the model was mtry = 2.

Como vemos, con la función train no solo hemos creado el modelo (en este caso un randomForest), sino que además ha hecho un pequeño tuning del parámetro mtry (que indica el número de variables aleatorias a elegir en cada árbol creado) y nos indica las principales medidas de ajuste del modelo (RMSE y MAE).

Pero aún hay más, a la hora de crear nuestro modelo, podemos indicar a caret que haga una transformación de nuestros datos, como las que hemos visto anteriormente. Para eso simplemente debemos pasar el valor preProcess a la función train.

Por ejemplo, supongamos que vamos a usar el algortimo kNN, el cual requiere que los datos estén normalizados. Veámos cómo podemos procesar los datos en el propio entrenamiento:

# Me quedo con las variables numéricas
num_cols = sapply(Sacramento, is.numeric)
Sacramento_num = Sacramento[num_cols]

# Separo entre variables dependientes e independientes
indep_var = colnames(Sacramento_num) != "price"
model_knn = train(x = Sacramento[indep_var], 
                 y = Sacramento$price,
                 preProcess = "range",
                 method = 'knn'
                 )

model_knn
k-Nearest Neighbors 

932 samples
  6 predictor

Pre-processing: re-scaling to [0, 1] (6) 
Resampling: Bootstrapped (25 reps) 
Summary of sample sizes: 932, 932, 932, 932, 932, 932, ... 
Resampling results across tuning parameters:

  k  RMSE      Rsquared   MAE     
  5  39670.84  0.9157169  26896.41
  7  39718.85  0.9183391  27052.33
  9  40274.35  0.9188837  27325.56

RMSE was used to select the optimal model using the
 smallest value.
The final value used for the model was k = 5.

Como vemos, el modelo ha normalizado las 6 variables, ha creado un algoritmo kNN con diferentes valores de k y ha decidido que el valor de k óptimo es k=5.

Y, por si parece poco, sigue habiendo más: caret facilita mucho el tuning de un modelo mediante grid search. Veámos cómo.

Cómo optimizar los hiperparámetros de un modelo con caret

Como hemos visto, cuando hacemos un modelo en caret, directamente aplica un tuning por defecto. Sin embargo, puede que a nosotros nos interese controlar qué valores toman esos hiperparámetros. Pues bien, hacer esto con caret es muy sencillo.

Para poder probar diferentes parámetros, primero deberemos crear nuestro propio Grid Search. Cuando hacemos una optimización por Grid Search básicamente probamos todas las posibles combinaciones de todos los hiperparémtros que indiquemos.

Por ejemplo, supongamos que queremos crear una regresión Lasso basado en reglas y queremos tunear el parámetro lambda, que indica el nivel de penalización que se realizará.

Importante: los parámetros que podemos tunear de cada modelo aparecen en el listado de modelos disponibles.

Para ello, simplemente hay que pasarle a la función expand.grid cada valor de cada parámetro que queremos que prueba. Importante, si hay parámetros que solo queremos que tengan un valor, también tenemos que incluirlos. Veámos cómo se hace:

model_lasso = train(x = Sacramento[indep_var], 
                 y = Sacramento$price,
                 method = "glmnet",
                 family = "gaussian",
                 tuneGrid = tunegrid
                 )
model_lasso = train(x = Sacramento[indep_var], 
                 y = Sacramento$price,
                 method = "glmnet",
                 family = "gaussian",
                 tuneGrid = tunegrid
                 )
model_lasso
glmnet 

932 samples
  3 predictor

No pre-processing
Resampling: Bootstrapped (25 reps) 
Summary of sample sizes: 932, 932, 932, 932, 932, 932, ... 
Resampling results across tuning parameters:

  lambda  RMSE      Rsquared   MAE     
      0   83549.32  0.5893103  61258.44
      1   83549.32  0.5893103  61258.44
    100   83549.32  0.5893103  61258.44
    200   83549.08  0.5893124  61258.51
    500   83524.09  0.5894426  61275.17
   1000   83508.27  0.5894367  61319.27
   2000   83566.59  0.5887109  61473.51
   5000   84354.89  0.5812886  62298.71
  10000   85149.90  0.5764404  63031.99
  50000   97271.45  0.5764554  72190.38

Tuning parameter 'alpha' was held constant at a value of 1
RMSE was used to select the optimal model using the smallest value.
The final values used for the model were alpha = 1 and lambda = 1000.

Como ves, realizar un Grid Search con caret es muy sencillo. Y sí, aunque ya sea mucho, aún hay más. Y es que caret permite otra cosa también: hacer validación cruzada o cross validation. Veámos cómo hacerlo.

Cómo hacer Cross Validation con caret

Para realizar cross validation en R con caret simplemente tenemos que llamar a la función trainControl y pasar este resultado a nuestra función de entrenamiento.

Dentro de la función trainControl podemos indicar muchas de las cuestiones que nos interesan, como el método de resampling a utilizar o cuántas veces lo debemos utilizar.

Lo más típico suele ser fijar el método como cv o repeatedcv lo cual permiten realizar cross validation, aunque también podemos realizar bootstrapping si fijamos el valor en boot, boot632, optimism_boot o boot_all.

Asismismo, si nuestros datos sufren de imbalanceo, podemos balancearlos de diferentes maneras usando el parámetro sampling. Los tipos de sampling que permite son: down para realizar downsampling, up para upsampling o aplicar modelos específicos de sampling con smote o rose.

Veámos cómo funciona aplicándolo al ejemplo de la regresión Lasso que hemos creado previamente:

fitControl = trainControl(method = "repeatedcv",
                          number = 10,
                          repeats = 10)

cv_model_lasso = train(x = Sacramento[indep_var], 
                 y = Sacramento$price,
                 method = 'glmnet',
                 family = 'gaussian',
                 tuneGrid = tunegrid,
                 trControl = fitControl
                 )
cv_model_lasso
glmnet 

932 samples
  3 predictor

No pre-processing
Resampling: Cross-Validated (10 fold, repeated 10 times) 
Summary of sample sizes: 839, 840, 838, 839, 839, 839, ... 
Resampling results across tuning parameters:

  lambda  RMSE      Rsquared   MAE     
      0   82431.41  0.6050358  60544.17
      1   82431.41  0.6050358  60544.17
    100   82431.41  0.6050358  60544.17
    200   82431.36  0.6050361  60544.15
    500   82428.08  0.6050931  60566.41
   1000   82445.83  0.6050029  60618.18
   2000   82539.04  0.6043956  60790.61
   5000   83361.05  0.5979379  61635.24
  10000   84335.30  0.5926398  62447.44
  50000   97474.98  0.5926398  72309.98

Tuning parameter 'alpha' was held constant at a value of 1
RMSE was used to select the optimal model using the smallest value.
The final values used for the model were alpha = 1 and lambda = 500.

Como vemos, cada uno de los modelos ha realizado un 10-fold cross validation, que ha repetido 10 veces. Y, cómo no, el error del modelo para los diferentes valores de lambda han cambiado.

Por último, quedaría comentar una cuestión importante para el entrenamiento de nuestros modelos de machine learning en R con caret: el entrenamiento en paralelo.

Cómo entrenar modelos de Machine Learning en R en paralelo

Cuando creamos modelos, estos pueden tardar en ejecutarse, sobre todo si realizamos un Grid Search muy extenso (además de que, todo hay que decirlo, caret no es muy rápido).

Por suerte, caret nos ofrece la opción de paralelizar los modelos, de tal forma que conseguimos realizar muchos más modelos en menos tiempo.

Para comprobarlo, veámos cuánto tarda en crearse una regresión Lasso con muchos hiperparámetros si no paralelizamos el modelo:

tic = Sys.time()

tunegrid = expand.grid(
  alpha  = seq(0,1,0.1),
  lambda = c(0,1,100,200,500,1000,2000,5000,10000,50000)
) 

fitControl = trainControl(method = "repeatedcv",
                          number = 10,
                          repeats = 10)

cv_model_lasso = train(x = Sacramento[indep_var], 
                 y = Sacramento$price,
                 method = 'glmnet',
                 family = 'gaussian',
                 tuneGrid = tunegrid,
                 trControl = fitControl
                 )

toc = Sys.time()

cat("Total time:",toc-tic)
Total time: 20.35262

Como vemos, ha tardado algo más de 20 segundos en realizar todo el proceso. Pero, ¿y si lo paralelizamos?

Paralelizar un modelo en R con caret es muy sencillo, simplemente hay que crear un cluster con la librería doParallel y parar el cluster una vez hayamos entrenado.

El cluster se puede crear de la siguiente manera:

library(doParallel)
cl = makePSOCKcluster(5)
registerDoParallel(cl)

Ahora que hemos creado el cluster, podemos ejecutar el mismo código que antes, que ahora se paralelizará automáticamente.

tic = Sys.time()

tunegrid = expand.grid(
  alpha  = seq(0,1,0.1),
  lambda = c(0,1,100,200,500,1000,2000,5000,10000,50000)
) 

fitControl = trainControl(method = "repeatedcv",
                          number = 10,
                          repeats = 10)

cv_model_lasso_par = train(x = Sacramento[indep_var], 
                           y = Sacramento$price,
                           method = 'glmnet',
                           family = 'gaussian',
                           tuneGrid = tunegrid,
                           trControl = fitControl
                           )

toc = Sys.time()

cat("Total time:",toc-tic)
Total time: 9.741221

Como vemos, ahora la creación del modelo únicamente ha llevado 9 segundos, es decir, menos de la mitad del tiempo que sin paralelizar. Todo con 2 líneas de código muy simples. Y ojo, esto es aplicable a todos los 238 modelos que incluye caret.

Por último, tenemos que detener el cluster, lo cual podemos realizar con la siguietne función:

stopCluster(cl)

Como ves, caret ofrece ventajas muy muy interesantes. Por último, llegamos a la recta final de este post, donde veremos cómo hacer predicciones con caret, así como evaluar el rendimiento de un modelo de ML. ¡Vamos allá!

Cómo hacer predicciones y medir capacidad predictiva del modelo con caret en R

Para realizar predicciones con R debemos pasar nuevos datos y nuestro modelo a la función predict, como cualquier otro modelo normal en R.

head(pred)
       1        2        3        4        5        6 
141811.1 168743.7 135557.5 144312.5 135713.9 161708.4 

Asimismo, caret también ofrece funciones de cara a calcular la capacidad predictiva de los modelos. Esto dependerá de los tipos de datos que tengamos. Para variables numéricas, podemos usar las funciones RMSE y la función defaultSummary, la cual devuelve las principales métricas (RMSE, R2 y MAE).

A mi personalmente me suele gustar más la función RMSE, básicamente porque suele ser la forma que (en general) utilizo para medir la capacidad predictiva de los modelos. Además, es más sencillo de realizar que la función defaultSummary, ya que esta última requiere que crees un dataframe para que funciones. Veámos cómo funcionan:

print("Use of defaultSummary")
defaultSummary(
  data = data.frame(obs = Sacramento$price[1:100], 
                    pred = pred))

print("Use of RMSE")
RMSE(pred, Sacramento$price[1:100])
[1] "Use of defaultSummary"
        RMSE     Rsquared          MAE 
5.615570e+04 3.388848e-01 4.676571e+04 

[1] "Use of RMSE"
[1] 56155.7

Asimismo, en el caso de variables categóricas caret ofrece la función confusionMatrix, la cual calcula la matriz de confusión, así como todas las métricas asociadas a ella.

pred_fake = factor(round(runif(100)))
real_fake = factor(round(runif(100)))

confusionMatrix(pred_fake, real_fake)
Confusion Matrix and Statistics

          Reference
Prediction  0  1
         0 25 28
         1 21 26

               Accuracy : 0.51           
                 95% CI : (0.408, 0.6114)
    No Information Rate : 0.54           
    P-Value [Acc > NIR] : 0.7591         

                  Kappa : 0.0247         

 Mcnemar's Test P-Value : 0.3914         

            Sensitivity : 0.5435         
            Specificity : 0.4815         
         Pos Pred Value : 0.4717         
         Neg Pred Value : 0.5532         
             Prevalence : 0.4600         
         Detection Rate : 0.2500         
   Detection Prevalence : 0.5300         
      Balanced Accuracy : 0.5125         

       'Positive' Class : 0              
                                         

Aunque no se trate de un caso real, vemos que caret ofrece muchísima información con tan solo una línea de código.

Resumen

En definitiva, si vas a hacer machine learning con R, caret es un paquete que debes conocer. No solo unifica muchísimos modelos en un mismo paquete, sino que estandariza cosas super interesantes como la optimización de hiperparámetros o la realización de cross validation. Además, permite entrenar todos los modelos de una forma súper sencilla.

Por si fuera poco, cuenta con varias funciones con las que, de una forma muy sencilla, podemos ver cómo de bueno ha sido nuestro modelo.

En definitiva, caret es un muy buen paquete y espero que este post te haya servido para todo el potencial que tiene. ¡Nos vemos en el siguiente post!