El proyecto consiste en la competencia de Kaggle (www.kaggle.com) “What’s cooking?”. El objetivo es , a partir de una lista de ingredientes, crear un modelo que pueda predecir el tipo de comida, entre varias recetas de comida del mundo entero : Italiana, Mexicana, Hindu, …,etc.
Para poder trabajar este proyecto se necesitan los paquetes de R orientados a mineria de texto (text mining). Con la diferentes herramientas de mineria de texto se puede establecer una base de datos de los ingredientes, su ocurrencia en cada comida y “entrenar” un modelo que permita categorizar las diferentes comidas.
Cargamos las librerias necesarias. Las librerias jsonlite, dplyr, ggplot2 y tm nos sirven para manipular los datos. Las librerias caret, rpart y randomForest nos sirven para la creacion de los modelos de prediccion.
##
## Attaching package: 'dplyr'
##
## The following objects are masked from 'package:stats':
##
## filter, lag
##
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
##
## Loading required package: NLP
##
## Attaching package: 'NLP'
##
## The following object is masked from 'package:ggplot2':
##
## annotate
##
## Loading required package: lattice
## randomForest 4.6-12
## Type rfNews() to see new features/changes/bug fixes.
##
## Attaching package: 'randomForest'
##
## The following object is masked from 'package:dplyr':
##
## combine
El paso inicial es la carga de datos. Para esta competencia los archivos de “train” y “test” estan dados en formato JSON en la URL: https://www.kaggle.com/c/whats-cooking/data.
library(jsonlite)
setwd("~/Documents/R folder/Cooking")
train <- fromJSON("train.json", flatten = TRUE)
test <- fromJSON("test.json", flatten = TRUE)
Existen un total de 39774 recetas en el archivo “train” con 6716 ingredientes en total. Cada receta corresponde a una comida o “cuisine” y tiene un identificador (id) y un conjunto de ingredientes (ingredients). Los ingredientes son una lista de texto:
head(train)[1:4,1:3]
## id cuisine
## 1 10259 greek
## 2 25693 southern_us
## 3 20130 filipino
## 4 22213 indian
## ingredients
## 1 romaine lettuce, black olives, grape tomatoes, garlic, pepper, purple onion, seasoning, garbanzo beans, feta cheese crumbles
## 2 plain flour, ground pepper, salt, tomatoes, ground black pepper, thyme, eggs, green tomatoes, yellow corn meal, milk, vegetable oil
## 3 eggs, pepper, salt, mayonaise, cooking oil, green chilies, grilled chicken breasts, garlic powder, yellow onion, soy sauce, butter, chicken livers
## 4 water, vegetable oil, wheat, salt
Un cuerpo o “Corpus” es basicamente una coleccion de listas de texto. En este caso las listas contienen todos los ingredientes de cada una de las 39774 comidas. Esto nos sirve para analizar los ingredientes que conforman cada una de las recetas:
ingredients <- Corpus(VectorSource(train$ingredients))
ingredients_test <- Corpus(VectorSource(test$ingredients))
ingredients
## <<VCorpus>>
## Metadata: corpus specific: 0, document level (indexed): 0
## Content: documents: 39774
La etapa de pre-procesamiento nos permite hacer un poco de limpieza a los datos correspondientes a los ingredientes. En este caso nos interesa remover razgos comunes de las palabras de los ingredientes (e.g., thigh, thighs). Es decir removemos palabras que corresponden al mismo ingrediente: pechuga = pechugas, dejando uno solo de los ingredientes necesarios.
ingredients <- tm_map(ingredients, stemDocument)
ingredients_test <- tm_map(ingredients_test,stemDocument)
La matrix de terminos es basicamente una matriz de ingredientes que corresponden a cada una de las recetas. Nos indica si el ingrediente aparece o no en cada receta, es decir podemos ver un “1” en la columna si el ingrediente se incluye en la receta correspondiente. La matriz tiene 39774 lineas y 249 columnas que corresponden a ingredientes. A continuacion una muestra de la matriz de ingredientes:
ingredientsDTM <- DocumentTermMatrix(ingredients)
ingredientsDTM_test <- DocumentTermMatrix(ingredients_test)
inspect(ingredientsDTM[1:20, 319:330])
## <<DocumentTermMatrix (documents: 20, terms: 12)>>
## Non-/sparse entries: 4/236
## Sparsity : 98%
## Maximal term length: 11
## Weighting : term frequency (tf)
##
## Terms
## Docs broccoli broccolini broil broiler broiler-fry broth brown browni
## 1 0 0 0 0 0 0 0 0
## 2 0 0 0 0 0 0 0 0
## 3 0 0 0 0 0 0 0 0
## 4 0 0 0 0 0 0 0 0
## 5 0 0 0 0 0 0 0 0
## 6 0 0 0 0 0 0 0 0
## 7 0 0 0 0 0 0 0 0
## 8 0 0 0 0 0 0 0 0
## 9 0 0 0 0 0 0 0 0
## 10 0 0 0 0 0 0 0 0
## 11 0 0 0 0 0 0 0 0
## 12 0 0 0 0 0 0 0 0
## 13 0 0 0 0 0 0 0 0
## 14 0 0 0 0 0 0 0 0
## 15 0 0 0 0 0 1 0 0
## 16 0 0 0 0 0 0 0 0
## 17 0 0 0 0 0 0 0 0
## 18 0 0 0 0 1 0 0 0
## 19 0 0 0 0 0 0 0 0
## 20 0 1 0 0 0 1 0 0
## Terms
## Docs brussel bucatini buckwheat bud
## 1 0 0 0 0
## 2 0 0 0 0
## 3 0 0 0 0
## 4 0 0 0 0
## 5 0 0 0 0
## 6 0 0 0 0
## 7 0 0 0 0
## 8 0 0 0 0
## 9 0 0 0 0
## 10 0 0 0 0
## 11 0 0 0 0
## 12 0 0 0 0
## 13 0 0 0 0
## 14 0 0 0 0
## 15 0 0 0 0
## 16 0 0 0 0
## 17 0 0 0 0
## 18 0 0 0 0
## 19 0 0 0 0
## 20 0 0 0 0
La matriz de ingredientes tiene varias columnas que corresponden a ingredientes que tienen baja ocurrencia, es decir ingredientes que se encuentran muy “dispersos”. Reducimos la dispersion de manera que mantenemos los ingredientes que estan incluidos en el 99% de las recetas.
sparse <- removeSparseTerms(ingredientsDTM, 0.99)
sparse_test <- removeSparseTerms(ingredientsDTM_test, 0.99)
sparse
## <<DocumentTermMatrix (documents: 39774, terms: 249)>>
## Non-/sparse entries: 646139/9257587
## Sparsity : 93%
## Maximal term length: 13
## Weighting : term frequency (tf)
Convertimos los datos a un data frame antes de poder correr los modelos de prediccion. El set de pruebas “test” puede tener algunos ingredientes que no estan en el set de “train” y por lo tanto comparamos sus columnas e incluimos solo las que se intersectan.
ingredientsDTM <- as.data.frame(as.matrix(sparse))
ingredientsDTM_test <- as.data.frame(as.matrix(sparse_test))
# Hay ingredientes en el train data set , que no estan en el test data set. Por lo tanto hay que
# dejar los que son comunes a ambos sets:
train_columns <- names(ingredientsDTM)
test_columns <- names(ingredientsDTM_test)
intersection <- intersect(train_columns,test_columns)
ingredientsDTM <- ingredientsDTM[,c(intersection)]
ingredientsDTM_test <- ingredientsDTM_test[,c(intersection)]
ingredientsDTM$cuisine <- as.factor(train$cuisine)
El primer modelo de prediccion es un modelo de arbol de decision CART (Classification and regression trees) usando la libreria rpart de R. Nos interesa poder predecir las 20 categorias, basados en el contenido de los ingredientes. La variable a predecir es “cuisine” y el metodo a utilizar es clasificacion:
set.seed(9347)
cartModelFit <- rpart(cuisine ~ ., data = ingredientsDTM, method = "class")
prp(cartModelFit)
Corremos la prediccion sobre el “test” data set y generamos el archivo .csv para subir a Kaggle:
# Correr prediccion sobre "test""
cartPredict <- predict(cartModelFit, newdata = ingredientsDTM_test, type = "class")
# Generar archivo CSV
submit <- data.frame(id=test$id,cuisine=cartPredict)
write.csv(submit, file = "project_LD_CART.csv", row.names = FALSE)
La primera entrega genero un valor de 0.40 en Kaggle. La segunda entrega genero un valor de 0.68. Basicamente esta diferencia se obtuvo jugando con los valores de dispersion “Sparse” variando entre 0.995 y 0.99 es decir tratando de modificar la dispersion incluyendo mas ingredientes.
Para la tercera entrega usamos un modelo de random forest. Este modelo utiliza multiples arboles de decision en lugar de un solo arbol. Una particularidad de este modelo es que genera una medicion de “importancia” la cual puede servir para ajustes de las variables.
La generacion del modelo, la prediccion y el resultado obtenido en Kaggle se muestra a continuacion:
# El modelo random forest da error al detectar "-"
ingredientsDTM_RF <- ingredientsDTM
names(ingredientsDTM_RF) <- gsub("-","",names(ingredientsDTM_RF))
ingredientsDTM_test_RF <- ingredientsDTM_test
names(ingredientsDTM_test_RF) <- gsub("-","",names(ingredientsDTM_test_RF))
# Modelo con 10 arboles y prediccion sobre "test"
ForestModel <- randomForest(cuisine ~., data = ingredientsDTM_RF, importance = TRUE, ntree = 10)
ForestPredict <- predict(ForestModel, newdata = ingredientsDTM_test_RF, type = "class")
submit_2 <- data.frame(id=test$id,cuisine=ForestPredict)
write.csv(submit_2,file = "project_LD_RF.csv", row.names = FALSE)
Este ultimo modelo genero una mejora significativa llegando a 0.72395 ubicandome en el puesto 709 de 1013 participantes:
Es posible aun mejorar estos resultados. Algunas opciones son: 1) Jugar con valores de dispersion o “sparsity” para incluir mas ingredientes 2) En lugar de suprimir ingredientes no comunes a “test” y “train” tratar de incluirlos en el modelo o 3) Utilizar tecnicas mas avanzadas de clasificacion.