Estrategias para no volverse loco con la tarea de Machine leaning

He percivido los problemas que han tenido todos con la tarea de Machine Learning, donde muchos de ustedes no han podido ni siquiera correr un modelo de las cervezas. Este posteo es para darles un ejemplo mínimo reproducible que funciona. El consejo principal es que partan con un modelo mínimo y que avancen desde ahí.

Los desafíos

Hay al menos 3 desafíos importantes en esta base de datos, por un lado tenemos que la base de datos es bastante grande, lo cual hace que se nos llene rápidamente la RAM. El segundo y tercer desafío están relacionados con que nuestra base de datos tiene casi exclusivamente variables categóricas, lo cual hace que por un lado exista un potencial de que la base de datos crezca aun más, o que tengamos problemas para predecir. Veremos estos desafíos uno a uno.

Tamaño de la base de datos

Cuando nuestra base de datos es muy grande, es probablemente una mala idea el probar modelos con una base de datos gigante, lo mejor es crear un ejemplo mínimo reproducible e ir complejizandolo desde ahí. Al final de este ejercicio ustedes debieran tener un ejemplo que puedan correr en casi cualquier computador con solo 4 GB de RAM reciclado.

Pariré por cargar la base de datos:

library(tidyverse)
library(caret)

Beer <- read_rds("TrainBeer.rds")

Esta base de datos tiene 759,240 observaciones y pesa 44.5 Mb, el problema es que cuando nosotros entrenamos un modelo usando digamos k-fold-n-repeated cross validation. Dividiendo esa base de datos por la mitad para train y la mitad para test, el temaño de la base de datos de prueba quedaria con un tamaño de 2.2 Gb en el mejor de los casos, lo cual hace imposible trabajar con eso en un computador como con el que estoy realizanod este trabajo (Además de la eternidad que se demoraría en entrenar el modelo).

Una de las estrategias más sencillas para hacer un modelo inicial reproducible es partir con una base de datos mucho más pequeña y que tenga con los elementos más repetidos de la base de datos. Para esto partire por generar una base de datos resumen con los 5 usuarios que han dado su opinión sobre más cervezas.

BeerSum <- Beer %>% dplyr::group_by(review_profilename) %>% dplyr::summarise(N = n()) %>% arrange(desc(N)) %>% dplyr::top_n(5)
review_profilename N
northyorksammy 2665
BuckeyeNation 2146
mikesgroove 2130
Thorpe429 1667
brentk56 1593

Los resultados los vemos en la tabla 1, en si usamos el siguiente código dejamos solo los resultados generados por estos usuarios.

Profiles <- BeerSum$review_profilename

BeerSubset <- Beer %>% filter(review_profilename %in% Profiles)

Lo cual reduce el tamaño de mi base de datos a 10,201 observaciones y 1.1 Mb y un peso de crossvalidation de 53.9 Mb

Tamaño de la base de datos asociado a variables categoricas

Tomemos de la base de datos BerrSubset solo las primeras 12 observaciones, en particular tomemos solo los nombres de usarios, y el tipo de cerveza.

BeerSubset_20 <- BeerSubset[1:20,] %>% dplyr::select(review_profilename, beer_style)

Vemos en la Tabla 2 el resultado de este subset, el cual tiene 11 tipos de cerveza. ¿Como usa caret esa base de datos?

Tabla 2: Primeras 20 observaciones de la base de datos BeerSubset
review_profilename beer_style
northyorksammy Rauchbier
brentk56 American Pale Ale (APA)
BuckeyeNation American Pale Ale (APA)
Thorpe429 American Pale Ale (APA)
northyorksammy American IPA
mikesgroove American IPA
brentk56 American IPA
BuckeyeNation American IPA
Thorpe429 American IPA
BuckeyeNation Light Lager
Thorpe429 Light Lager
mikesgroove Light Lager
mikesgroove Russian Imperial Stout
northyorksammy American Porter
brentk56 Tripel
BuckeyeNation Scotch Ale / Wee Heavy
northyorksammy English Bitter
mikesgroove Dubbel
mikesgroove American Porter
mikesgroove American Blonde Ale

Si usaramos la variable beer_style como una variable respuesta, hay dos opciones, en algunos algoritmos caret utilizará esa variable como una variable categórica, pero en otros algoritmos lo utilizará como generará 11 dummy variables. Para ver que significa esto, tomaremos BeerSubset_20 y transformaremos beer_style en una dummy variable, para mostrar cual sería el efecto de esto en el tamaño de la base de datos.

DummyCreator <- dummyVars(~ beer_style, data = BeerSubset_20)

BeerSubset_20Dummy <- predict(DummyCreator, BeerSubset_20)

En la tabla 3 vemos el resultado de transformar beer_style en una variable dummy, seguimos teniendo 20 filas, pero pasamos de tener 2 columnas a tener 11, y de un peso de entrenamiento de 107 Kb a un tamaño de entrenamiento de 209.8 Kb, esto es casi el doble y esto es solo considerando 11 tipos de cerveza, cuando la BeerSubset tiene 103 estilos de cerveza, lo que equivale a la misma cantidad de columnas, a esto sumemosle, y esto pasaría para cada variable categórica.

Tabla 3: Formato dummy de la tabla 2
beer_styleAmerican Blonde Ale beer_styleAmerican IPA beer_styleAmerican Pale Ale (APA) beer_styleAmerican Porter beer_styleDubbel beer_styleEnglish Bitter beer_styleLight Lager beer_styleRauchbier beer_styleRussian Imperial Stout beer_styleScotch Ale / Wee Heavy beer_styleTripel
0 0 0 0 0 0 0 1 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 1 0 0 0 0 0 0 0 0 0
0 1 0 0 0 0 0 0 0 0 0
0 1 0 0 0 0 0 0 0 0 0
0 1 0 0 0 0 0 0 0 0 0
0 1 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 1 0 0 0 0
0 0 0 0 0 0 1 0 0 0 0
0 0 0 0 0 0 1 0 0 0 0
0 0 0 0 0 0 0 0 1 0 0
0 0 0 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 0 0 0 1 0
0 0 0 0 0 1 0 0 0 0 0
0 0 0 0 1 0 0 0 0 0 0
0 0 0 1 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0 0

Solucionando el problema de las variables categoricas el tamaño de Train

para solucionar este problema hay dos opciones, por un lado es seguir subseteando el tamaño de la base de datos original para tomar en cuenta a las otras variables categóricas, por ejemplo podemos fijarnos en cuantas cervezas combinando estilos y nombres se repiten al menos 50 veces, para esto podemos correr el siguiente código:

BeerSum2 <- Beer %>% dplyr::group_by(beer_name, beer_style) %>% dplyr::summarise(N = n()) %>% dplyr::arrange(desc(N)) %>% dplyr::filter(N >= 50)

Esto nos dá una base de datos de 3103 cervezas con estas características, pero si combinamos los filtros que teníamos de usuarios con estos utilizando el siguiente código:

Profiles <- BeerSum$review_profilename

BeerSubset <- Beer %>% filter(review_profilename %in% Profiles & beer_name %in% BeerSum2$beer_name &  beer_style %in% BeerSum2$beer_style)

Quedamos con una base de datos de 5,379 observaciones y 520.1 Kb, y un peso de entrenamiento de 25.4 Mb con lo cual ya podríamos empezar a trabajar y generar un modelo.

set.seed(2018)
Index <- createDataPartition(BeerSubset$review_overall, p = 0.5, list = FALSE)

Train <- BeerSubset[Index,] %>% dplyr::select(review_overall, brewery_name, beer_style, beer_abv, review_profilename)

Train2 <- Train %>% mutate_if(is.character, as.factor)

Test <- BeerSubset[-Index,] %>% dplyr::select(review_overall, brewery_name, beer_style, beer_abv, review_profilename)
Test2 <- Test %>% mutate_if(is.character, as.factor)


Demora <- system.time(Model <- train(x = Train2[,c(2,3,4,5)], y = Train2$review_overall, method = "rpart"))

Al usar la función system.time puedo ver que este modelo se demoró 2.708 segundos en entrenarse, desde ahí puedo empezar a complejizar el modelo por ejemplo cambiando el algoritmo o agrandando el subset de datos y/o haciendo un crossvalidation más grande, en este caso usé el default.

Potenciales problemas con el postResample

Cuando tenemos variables categóricas, no podemos hacer predicciones sobre variables nuevas, esto es:

NotInStyle <- unique(Test2$beer_style)[!(unique(Test2$beer_style) %in% unique(Train2$beer_style))]

En este caso hay 4 estilos de cerveza presentes en Test2 que no se encuentran en Train2, estos son: Japanese Rice Lager, English Dark Mild Ale, Faro, Scottish Gruit / Ancient Herbed Ale, debido a esto si intentamos hacer un postResample tendermos un error, por lo cual es necesario quitar de Test2 esas observaciones (lo mismo con todas las otras veriables CATEGORICAS), con esto ya podemos tener un resultado.

NotInBrewery <- unique(Test2$brewery_name)[!(unique(Test2$brewery_name) %in% unique(Train2$brewery_name))]


NewTest2 <- Test2 %>% dplyr::filter(!(beer_style %in% NotInStyle) & !(brewery_name %in% NotInBrewery))

postResample(pred = predict(Model, NewTest2), obs = NewTest2$review_overall)
##      RMSE  Rsquared       MAE 
## 0.6225641 0.1719183 0.4507511