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í.
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.
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
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?
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.
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 |
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.
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