Naïve Bayes con R para clasificación de texto

En este artículo revisaremos como implementar el Naïve Bayes (clasificador Bayesiano ingenuo) para clasificar texto usando R. Naïve Bayes es un algoritmo de aprendizaje automático basado en el teorema de Bayes que aunque es sencillo de implementar, tiende a dar buenos resultados.

Usaremos un conjunto de datos sencillo, obtenido con la API de Twitter, que consta de 1349 tuits, acompañados de su nombre de usuario e identificador de tuit. Estos tuits fueron obtenidos el 9 de Abril del 2018.

Nuestro objetivo será determinar si un tuit en particular fue hecho por un usuario específico o no, a partir de su contenido. Los datos que usaremos contienen tuits que pertenecen a cuatro cuentas, mezclados con tuits de multiples usuarios.

  • @lopezobrador — Andrés Manuel Lopez Obrador, candidato a la presidencia de México.
  • @UNAM_MX — Universidad Nacional Autónoma de México, cuenta institucional.
  • @CMLL_OFICIAL — Consejo Mundial de Lucha Libre, promoción de lucha libre de México.
  • @MSFTMexico — Microsoft México, cuenta corporativa.

Además, veremos cómo podemos sistematizar la implementación de este algoritmo en R.

Pero antes de empezar…

Una explicación informal de Naïve Bayes

La idea de Naïve Bayes es sencilla, pero efectiva. Usamos las probabilidades condicionales de las palabras en un texto para determinar a qué categoría pertenece, estas calculadas con el teorema de Bayes.

Por ejemplo, si deseamos clasificar reseñas de un servicio en dos categorías, “positiva” y “negativa”, tenemos que determinar qué palabras es más probable encontrar en cada una de ellas. Podemos imaginar que es más probable que una reseña pertenezca a la categoría “positiva” si contiene palabras como “bueno” o “excelente”, y menos probable si contiene palabras como “malo” o “deficiente.

Entonces podemos decir: ¿cuál es la probabilidad de que una reseña pertenezca a la categoría “positiva”, dado que contiene la palabra “bueno”? De manera sencilla: p(positiva|bueno)

Este algoritmo es llamado “ingenuo” porque calcula las probabilidades condicionales de cada palabra por separado, como si fueran independientes una de otra. En lugar de calcular la probabilidad condicional de que una reseña pertenezca a la categoría “positiva”, dado que contiene la palabra “bueno”, y dado que contiene la palabra “servicio”, y dado que contiene la palabra “familia”, y así sucesivamente para todas las palabras de la reseña; lo que hacemos es calcular la probabilidad condicional de cada palabra, asumiendo de manera “ingenua” que en esta probabilidad no importa cuales palabras le acompañan.

Una vez que tenemos las probabilidades condicionales de cada palabra en una reseña, calculamos la probabilidad conjunta de todas ellas, mediante un producto, para determinar la probabilidad de que pertenezca a la categoría “positiva”. Luego hacemos lo mismo para cada reseña que tengamos hasta clasificarlas todas.

Una explicación formal se encuentra en el siguiente enlace

Ahora sí, comencemos.

Preparando nuestro entorno.

Estos son los paquetes que usaremos en esta ocasión. Si no los tienes instalados, ejecuta primero install.packages(), como de costumbre. La implementación de Naïve Bayes que usaremos será la del paquete naivebayes.

library(tidyverse)
library(tidytext)
library(naivebayes)
library(tm)
library(caret)

Descarga y lectura de datos

Descargamos los datos de la siguiente dirección:

download.file(url = "https://raw.githubusercontent.com/jboscomendoza/rpubs/master/bayes_twitter/tuits_bayes.csv", destfile = "tuits.csv")

Usamos read.csv() para leer nuestros datos. Podríamos usar read_csv() de readr, pero esa función no tiene la opcion para leer texto usando una codificación específica. Para nuestros datos, necesitamos que las tildes, la ñ y otros caracteres especiales propios del español sean mostrados correctamente, por tanto es importante usar la codificación de texto correcta, que definimos con el argumento fileencoding.

tuits_df <-
  read.csv("tuits_bayes.csv", stringsAsFactors = F, fileEncoding = "latin1") %>%
  tbl_df

Procesamiento de los datos

Vamos a definir una función para quitar URLs. En nuestros datos todos los URLs han sido acortados, por lo que difícilmente obtendremos información relevante de de ellos por ser series de caracteres sin mucho significado.

La función siguiente usa regexp para detectar palabras que empiecen con “http” y las eliminará.

quitar_url <- function(texto) {
    gsub("\\<http\\S*\\>|[0-9]", " ", texto)
}

Creación de matriz dispersa

Para clasificar texto usando Naïve Bayes necesitamos que nuestros datos tengan estructura específica:

  • Cada renglón debe corresponder a un texto específico.
  • Cada columna debe corresponder a una palabra.
  • En las celdas debe indicarse si una palabra aparece en un texto específico.

Para ilustrar esta estructura, veamos un ejemplo sencillo. Partimos de un data frame, muy parecido a nuestros datos:

# A tibble: 3 x 2
     id texto                             
  <int> <chr>                             
1     1 este es un texto de ejemplo       
2     2 texto distinto, distinto contenido
3     3 conjunto de palabras nuevo

Al convertir lo anterior a una matriz dispersa, luce así;

## # A tibble: 3 x 12
##      id conjunto contenido    de distinto ejemplo    es  este nuevo
##   <int>    <int>     <int> <int>    <int>   <int> <int> <int> <int>
## 1     1       NA        NA     1       NA       1     1     1    NA
## 2     2       NA         1    NA        2      NA    NA    NA    NA
## 3     3        1        NA     1       NA      NA    NA    NA     1
## # ... with 3 more variables: palabras <int>, texto <int>, un <int>

En sentido estricto, para que esta fuera una matriz dispersa, en lugar de NA en las celdas deberían aparecer ceros. El nombre de dispersa se refiere a que es una matriz con pocas celdas llenas con datos distintos a 0. Sin embargo, para nuestros fines, una matriz como la anterior funcionará.

Nuestros datos lucen así:

tuits_df
## # A tibble: 1,349 x 3
##    status_id screen_name   text                                           
##        <dbl> <chr>         <chr>                                          
##  1   8.32e17 lopezobrador_ Josefina Vázquez Mota debe informar qué hizo c~
##  2   8.33e17 lopezobrador_ Ya ni la burla perdonan: bajaron 2 centavos la~
##  3   8.34e17 lopezobrador_ Martín Moreno no votará por mí. Comprendo. Es ~
##  4   8.34e17 lopezobrador_ En Chicago dije que iremos a Nueva York (ONU) ~
##  5   8.34e17 lopezobrador_ Entérate y apoya con tu firma la denuncia cont~
##  6   8.35e17 lopezobrador_ Qué república ni qué ocho cuartos, es la monar~
##  7   8.35e17 lopezobrador_ Salieron a defender a Yunes Linares, Calderón,~
##  8   8.36e17 lopezobrador_ En 2010, la trasnacional Odebrecht entregó sob~
##  9   8.36e17 lopezobrador_ Calderón dice donará su sueldo como expresiden~
## 10   8.37e17 lopezobrador_ ¡Las pensiones de los expresidentes de México ~
## # ... with 1,339 more rows

Entonces para convertir nuestros datos a una matriz dispersa necesitamos:

  1. Segmentar cada tuit por palabras.
  2. Contar cuantas veces aparece cada palabra por tuit.
  3. Dar formato de matriz “ancha”.

Lo anterior lo realizamos con las siguientes funciones:

  1. unnest_tokens() del paquete tidytext. Segmentamos una variable por palabras, creando una nueva columna con ellas.
  2. count() de dplyr. Ya que tenemos las palabras en una columna, contamos cuántas veces aparecen por tuit.
  3. spread() de tidyr. Los pasos anteriores nos dejan con datos “altos”, pues tendremos tantos renglones como palabras, pero buscamos tener tantas columnas como palabras. Con esta función pasamos de un formato “alto” de datos a uno “ancho”.

Veamos lo anterior en acción:

tuits_df %>%
  unnest_tokens(input = "text", output = "palabra") %>%
  count(screen_name, status_id, palabra) %>%
  spread(key = palabra, value = n)
## # A tibble: 1,348 x 8,571
##    screen_name     status_id `_mx` `_paolimeow` `_siqure`  `00`  `03`
##    <chr>               <dbl> <int>        <int>     <int> <int> <int>
##  1 __dianapatricia   9.83e17    NA           NA        NA    NA    NA
##  2 _vichoo1          9.83e17    NA           NA        NA    NA    NA
##  3 _victoriacetina   9.83e17    NA           NA        NA    NA    NA
##  4 09osuna           9.83e17    NA           NA        NA    NA    NA
##  5 1javierespinoza   9.83e17    NA           NA        NA    NA    NA
##  6 Accountant_Job    9.83e17    NA           NA        NA    NA    NA
##  7 AcuarioJac        9.83e17    NA           NA        NA    NA    NA
##  8 agenciasanluis    9.83e17    NA           NA        NA    NA    NA
##  9 Alexvaldesp       9.83e17    NA           NA        NA    NA    NA
## 10 Allisonband       9.83e17    NA           NA        NA    NA    NA
## # ... with 1,338 more rows, and 8,564 more variables: `04npkn59na` <int>,
## #   `04p2fdlagz` <int>, `08ij203qie` <int>, `0bdkourrq8` <int>,
## #   `0c7gf4c6gv` <int>, `0czmlndtlm` <int>, `0dzdwampsf` <int>,
## #   `0e3n7zqary` <int>, `0fw7jpwszc` <int>, `0hnvu8zqlr` <int>,
## #   `0inwe4hy6r` <int>, `0isstzejcq` <int>, `0klsvcwom6` <int>,
## #   `0m8bwywwfu` <int>, `0nmq8kp6eh` <int>, `0o6roeseit` <int>,
## #   `0ojk2dvn6q` <int>, `0qaqf3rw99` <int>, `0qib0v59gl` <int>,
## #   `0rh26gzg0d` <int>, `0s9kt25syc` <int>, `0w7wplwjde` <int>,
## #   `0wz5h1ngnj` <int>, `0xygafiglx` <int>, `1` <int>, `10` <int>,
## #   `10.9` <int>, `100` <int>, `10ahm8gaas` <int>, `11` <int>,
## #   `118` <int>, `11wln5qpvn` <int>, `12` <int>, `121765` <int>,
## #   `124` <int>, `1250` <int>, `13` <int>, `133` <int>, `135` <int>,
## #   `14` <int>, `140` <int>, `141` <int>, `14xk8kfn4r` <int>, `15` <int>,
## #   `1588` <int>, `1596` <int>, `16` <int>, `1614` <int>, `1626` <int>,
## #   `16uyveupwe` <int>, `17` <int>, `1727` <int>, `1746` <int>,
## #   `17vkpahpfg` <int>, `18` <int>, `1805` <int>, `1827` <int>,
## #   `1845` <int>, `1848` <int>, `1853` <int>, `18msfoez6t` <int>,
## #   `19` <int>, `1900` <int>, `1903` <int>, `1904` <int>, `1905` <int>,
## #   `1906` <int>, `1910` <int>, `1911` <int>, `1913` <int>, `1914` <int>,
## #   `1915` <int>, `1917` <int>, `1918` <int>, `1924` <int>, `1928` <int>,
## #   `1929` <int>, `1932` <int>, `1934` <int>, `1938` <int>, `1939` <int>,
## #   `1941` <int>, `1942` <int>, `1943` <int>, `1945` <int>, `1946` <int>,
## #   `1947` <int>, `1949` <int>, `1950` <int>, `1952` <int>, `1955` <int>,
## #   `1956` <int>, `1959` <int>, `1962` <int>, `1963` <int>, `1966` <int>,
## #   `1968` <int>, `1969` <int>, `197` <int>, `1972` <int>, ...

Obtenemos un objeto con 1348 renglones (uno por tuit) y 8 571 columnas (una por palabra en nuestros datos).

Este proceso lo realizaremos varias veces, así que nos conviene definir una función. De paso, aprovechamos para introducir en ella el proceso el quitar los URLs y la columna status_id, que no es necesaria para el resto del análisis.

crear_matriz <- function(tabla) {
  tabla %>%
    mutate(text = quitar_url(text)) %>%
    unnest_tokens(input = "text", output = "palabra") %>%
    count(screen_name, status_id, palabra) %>%
    spread(key = palabra, value = n) %>%
    select(-status_id)
}

Con esto tenemos nuestros datos listos para el análisis

Ajustando Naïve Bayes

Crearemos un modelo de predicción para determinar si un tuit pertenece a un usuario específico. Para esta prueba, intentaremos predecir si un tuit fue hecho por la cuenta @MSFTMexico o no. Dado que no nos interesa a qué categoría pertenecen los demás tuits, etiquetaremos todos los tuits que no pertenecen a esta cuenta como “Otro”.

Aunque Naïve Bayes puede hacer clasificaciones con múltiples categorías, conviene empezar con un ejemplo de clasificación binaria.

Recodificamos nuestra variable objetivo, screen_name, y creamos nuestra matriz dispersa con las funciones mutate() de dplyr e ifelse() de base. Aprovechamos para convertir nuestra variable objetivo a factor, que es el tipo de datos más compatible con la implementación de Naïve Bayes que usaremos.

ejemplo_matriz <-
  tuits_df %>%
  mutate(screen_name = ifelse(screen_name == "MSFTMexico", screen_name, "Otro"),
         screen_name = as.factor(screen_name)) %>%
  crear_matriz

Como haremos varias clasificaciones más adelante, es definimos una función para realizar la recodificación fácilmente.

elegir_usuario <- function(nombres, usuario) {
  as.factor(ifelse(nombres %in% usuario, nombres, "Otro"))
}

Sets de entrenamiento y prueba (training y test)

Cuando creamos un modelo de clasificación necesitamos un diagnóstico de qué tan bien está haciendo su trabajo. Para este fin dividiremos nuestros datos en dos sets (conjuntos), uno de entrenamiento (train) y uno de de prueba (test).

Con el set de entrenamiento ajustaremos nuestro modelo, en este caso, determinando las probabilidades condicionales de cada palabra, para cada categoría. Después, aplicamos este modelo en nuestro set de prueba para analizar cuántos de nuestros casos fueron clasificados correctamente.

Dividiremos nuestros datos de modo que tengamos 70% de ellos en el set de entrenamiento y el resto en el set de prueba. Usaremos la función sample_frac() de dplyr para obtener una muestra al azar de nuestros datos y después setdiff() del mismo paquete para obtener su complemento. Para que el ejemplo sea reproducible, usaremos set.seed() de base antes de obtener el primer set.

set.seed(2001)
ejemplo_entrenamiento <- sample_frac(ejemplo_matriz, .7)
ejemplo_prueba <- setdiff(ejemplo_matriz, ejemplo_entrenamiento)

También definimos una función para crear sets de entrenamiento y prueba, que nos serán devueltos en forma de lista.

crear_sets <- function(tabla, prop = .7) {
  lista_sets <- list()
  lista_sets$train <- sample_frac(tabla, prop)
  lista_sets$test  <- setdiff(tabla, lista_sets[["train"]])
  
  lista_sets
}

Ha llegado la hora de la verdad.

Usando la función naive_bayes

Para ajustar nuestro modelo usamos la función naive_bayes() del paquete naivebayes con nuestro set de entrenamiento. Esta función nos pide como argumentos la variable objetivo para clasificar y los datos que serán usados.

Especificamos la variable objetivo como una formula: screen_name ~ .

De esta manera estamos expresando que la variable screen_name será el objetivo o variable dependiente, y todas las demás variables (.) serán los predictores o variables independientes. No ajustaremos ningún otro parámetro de la función naive_bayes() para este ejemplo.

ejemplo_modelo <- naive_bayes(formula = screen_name ~ .,  data = ejemplo_entrenamiento)

Esperamos un poco en lo que hace su trabajo ¡Y eso es todo! Con esto ya tenemos un objeto que contiene nuestro modelo de predicción de Naïve Bayes, el cual podemos usar para hacer predicciones.

Haciendo predicciones con nuestro modelo

Para hacer predicciones con nuestro modelo usamos la función predict() de base. Esta función nos pide un modelo y datos nuevos, que en nuestro caso son el set de prueba.

ejemplo_prediccion <- predict(ejemplo_modelo, ejemplo_prueba)

Como resultado obtenemos un vector con los valores de screen_name que han sido predichos por nuestro modelo.

head(ejemplo_prediccion, 25)
[1] MSFTMexico MSFTMexico MSFTMexico MSFTMexico MSFTMexico
[6] MSFTMexico Otro       MSFTMexico MSFTMexico Otro      
[11] Otro       MSFTMexico MSFTMexico MSFTMexico MSFTMexico
[16] Otro       MSFTMexico Otro       MSFTMexico MSFTMexico
[21] Otro       MSFTMexico MSFTMexico MSFTMexico MSFTMexico
Levels: MSFTMexico Otro

Para analizar qué tanto éxito hemos tenido, creamos una matriz de confusión usando la función confusionMatrix() de caret, que es muy similar en su sintaxis a table() de base. No pide dos argumentos, el vector con las predicciones y los valores reales de screen_name.

Con esta matriz podremos analizar la precisión de nuestras predicciones y algunas medidas de ajuste.

confusionMatrix(ejemplo_prediccion, ejemplo_prueba[["screen_name"]])

Confusion Matrix and Statistics
 
             Reference
 Prediction   MSFTMexico Otro
   MSFTMexico         40   16
   Otro               13  319
                                           
                Accuracy : 0.9253          
                  95% CI : (0.8944, 0.9494)
     No Information Rate : 0.8634          
     P-Value [Acc > NIR] : 9.685e-05       
                                           
                   Kappa : 0.6905          
  Mcnemar's Test P-Value : 0.7103          
                                           
             Sensitivity : 0.7547          
             Specificity : 0.9522          
          Pos Pred Value : 0.7143          
          Neg Pred Value : 0.9608          
              Prevalence : 0.1366          
          Detection Rate : 0.1031          
    Detection Prevalence : 0.1443          
       Balanced Accuracy : 0.8535          
                                           
        'Positive' Class : MSFTMexico

¡Excelente! 92% de precisión (Accuracy) no está nada mal para un modelo al que no hemos hecho ningún ajuste particular. Clasificamos correctamente cerca de nueve de cada diez casos.

Sin embargo, nos conviene dar un vistazo rápido a algunas de las medidas que nos ofrece confusionMatrix(), pues la medida de precisión por sí misma puede ser engañosa. Veamos con más detalle la información que nos da.

Interpretando la matriz de confusión

Primero, tenemos tabla de confusión, propiamente dicha. En ella lo primero que nos interesa observar son las celdas en las que se cruzan los valores predichos (MSFTMExico) contra los de referencia. Es decir, el número en la celda en las que cruza el renglón MSFTMexico y la columna MSFTMexico corresponde a la cantidad de casos clasificados correctamente en esa categoría.

De 53 casos que eran MSFTMexico en el set de prueba, clasificamos correctamente 40, es decir, tuvimos una Sensibilidad (Sensitivity) de 75.47%. De manera complementaria, de 335 casos que eran Otro, clasificamos correctamente 319, esto es, 95.22% de Especificidad (Specificity). En otras palabras, tuvimos más éxito clasificando a la categoría Otro que MSFTMexico.

Otra medida útil es el estadístico Kappa. Este nos da una medida de qué tanto mejora nuestro modelo una predicción, contra las probabilidades observadas.

Por ejemplo, supongamos que tenemos un conjunto de datos donde 50% de ellos pertenecen a la clase A y el otro 50% a la B. Esto quiere decir que, por azar, clasificaríamos correctamente 50% de los casos en nuestros datos como A, pues esta es su probabilidad esperada. Un modelo que tenga 50% de clasificaciones correctas, no estaría mejorando nuestra capacidad de predicción más allá del azar. Un modelo tendría que clasificar correctamente más del 50% de los casos para considerarse una mejora sobre la probabilidad esperada.

Entre más cercano a 1 es el valor de Kappa, nuestro modelo es mejor para predecir que la probabilidad esperada. Qué valor de Kappa consideremos ideal depende del contexto de nuestro análisis, pero en general, valores arriba de 0.6 se consideran “buenos”. Nosotros tenemos 0.69.

El valor predictivo positivo (Pos Pred Value) indica la probabilidad de que un dato que ha sido predicho como perteneciente a nuestra categoría “positiva”, realmente pertenezca a ella (‘Positive’ Class : MSFTMexico, en este ejemplo). En este caso, la probabilidad es de 71.43%. Por complemento, el valor predictivo negativo (Neg Pred Value) indica la probabilidad de que un dato predicho como perteneciente a la categoría negativa (“Otro”), en efecto pertenezca a ella. Esta fue de 96.08%.

Finalmente, la precisión balanceada, indica qué tan bien predice nuestro modelo tanto a la categoría positiva, como a la negativa. Esto es muy importante con datos como los nuestros, en los que tenemos clases no balanceadas, es decir, que una es más abundante y tiene más probabilidades de aparecer que la otra. En conjuntos de datos como estos, es fácil obtener una precisión alta para la clase más probable, aunque tengamos poca para la clase menos probable.

Nuestra precisión balanceada es de 85.35% lo cual no está mal, aunque podría mejorar.

Resultados

Considerando todo lo anterior, podemos concluir que tenemos una buena precisión en nuestras predicciones, con más éxito para clasificar “Otro” que “MSFTMExico” y que nuestro modelo en efecto mejora la predicción con respecto a la probabilidad esperada.

Pero aún no hemos terminado.

Funciones para facilitar el análisis

El paso anterior puede simplificarse, de modo que sea más fácil realizar análisis posteriores.

Para ello definimos una función para ajustar Naïve Bayes y obtener predicciones, a partir de un lista con datos de entrenamiento y de prueba. Esta función nos devolvera una lista con el modelo y sus predicción.

obtener_bayes <- function(lista_sets, objetivo = "screen_name") {
  bayes_formula<- as.formula(paste0(objetivo, "~ .") )
  
  bayes <- list()
  
  bayes$modelo <- naive_bayes(formula = bayes_formula, data = lista_sets[["train"]])
  bayes$prediccion   <- predict(object = bayes$modelo, newdata = lista_sets[["test"]])
  
  bayes
}

También definimos una función para obtener matrices de confusión, a partir de la lista que devuelve la función anterior.

mat_conf <- function(resultado, set_test) {
  confusionMatrix(resultado[["prediccion"]], set_test[["test"]][["screen_name"]])
}

También es posible crear gráficas a partir de las matrices de confusión, usando el elemento table que devuelve la función confusionMatrix.

ejemplo_conf <- confusionMatrix(ejemplo_prediccion, ejemplo_prueba[["screen_name"]])
plot(ejemplo_conf[["table"]])

Así que tambien definimos una función para graficar matrices de confusión a partir de la lista de resultados que nos devuelve la función obtener_bayes.

plot_conf <- function(resultados_bayes) {
  plot(resultados_bayes[["confusion"]][["table"]],
       col = c("#00BBFF", "#FF6A00"),
       main = resultados_bayes[["confusion"]][["positive"]])
}

Ahora, ha llegado el momento de integrar lo que hemos hecho hasta ahora.

Sistematizando nuestro análisis

Integrando los pasos anteriores y las funciones que hemos definido para realizarlos, podemos definir una función para implementar Naïve Bayes.

hacer_bayes <- function(tabla, usuario) {
  ingenuo <- list()
  ingenuo[["matriz"]] <-
    tabla %>%
    mutate(screen_name = elegir_usuario(screen_name, usuario)) %>%
    crear_matriz()

  ingenuo[["sets"]] <- crear_sets(ingenuo[["matriz"]])
  ingenuo[["resultado"]] <- obtener_bayes(ingenuo[["sets"]])
  ingenuo[["confusion"]] <- list()
  ingenuo[["confusion"]] <- mat_conf(ingenuo[["resultado"]],  
  ingenuo[["sets"]])

  ingenuo
}

Veamos como funcionaría nuestra función. Intentaremos clasificar los tuits de la cuenta @CMLL_OFICIAL. Una vez más usamos set.seed() para hacer reproducible este ejemplo.

set.seed(1988)
bayes_cmll <- hacer_bayes(tuits_df, "CMLL_OFICIAL")

De lo anterior obtenemos una lista con:

  • Un data frame con la matriz dispersa.
bayes_cmll[["matriz"]]
# A tibble: 1,328 x 6,727
   screen_name  `_mx` `_paolimeow` `_siqure`     a abajo
   <fct>        <int>        <int>     <int> <int> <int>
 1 CMLL_OFICIAL    NA           NA        NA     1    NA
 2 CMLL_OFICIAL    NA           NA        NA     2    NA
 3 CMLL_OFICIAL    NA           NA        NA     2    NA
 4 CMLL_OFICIAL    NA           NA        NA     4    NA
 5 CMLL_OFICIAL    NA           NA        NA     2    NA
 6 CMLL_OFICIAL    NA           NA        NA     1    NA
 7 CMLL_OFICIAL    NA           NA        NA    NA    NA
 8 CMLL_OFICIAL    NA           NA        NA     1    NA
 9 CMLL_OFICIAL    NA           NA        NA     1    NA
10 CMLL_OFICIAL    NA           NA        NA     2    NA
# ... with 1,318 more rows, and 6,721 more variables:
#   abastecimiento <int>, abiertas <int>, abiertos <int>,
#   abogado <int>, abogados <int>, abordar <int>, abordo <int>,
#   about <int>, abracemos <int>, abrazo <int>, abre <int>,
#   abren <int>, abreviar <int>, abrieron <int>, abril <int>,
#   abrir <int>, abrirá <int>, absolutismo <int>, abusaron <int>,
#   abusó <int>, acá <int>, acaba <int>, acabar <int>,
#   acabará <int>, acabarán <int>, acabo <int>, acabó <int>,
#   académica <int>, académicas <int>, académico <int>,
#   académicos <int>, acala <int>, acatitla <int>, acayucan <int>,
#   accede <int>, acceso <int>, accidente <int>, acción <int>,
#   acciones <int>, account <int>, accounting <int>, acdc <int>,
#   acelera <int>, acento <int>, aceptado <int>, aceptar <int>,
#   acepto <int>, aceptó <int>, acerca <int>, acertijo <int>,
#   ácido <int>, aclamada <int>, aclare <int>, aclaren <int>,
#   acompañada <int>, acompañados <int>, acompañan <int>,
#   acompañando <int>, acompañantes <int>, acompañar <int>,
#   acompañarte <int>, acompañé <int>, aconseja <int>,
#   aconsejable <int>, acoplamiento <int>, acordamos <int>,
#   acordar <int>, acordé <int>, acoso <int>, acostumbraba <int>,
#   activa <int>, activado <int>, activas <int>, actividad <int>,
#   actividades <int>, activista <int>, acto <int>, actor <int>,
#   actos <int>, actriz <int>, actuación <int>, actual <int>,
#   actualidad <int>, actualizaciones <int>, actualizadas <int>,
#   actualizar <int>, actualmente <int>, actuando <int>,
#   actuar <int>, acude <int>, acudes <int>, acuerdo <int>,
#   acuerdos <int>, acuíferos <int>, acusa <int>, acusan <int>,
#   acusen <int>, ad <int>, adán <int>, adaptación <int>, ...
  • Una lista con los sets de entrenamiento y prueba.
bayes_cmll[["sets"]][["train"]]
## # A tibble: 930 x 6,727
##    screen_name `_mx` `_paolimeow` `_siqure`     a abajo abastecimiento
##    <fct>       <int>        <int>     <int> <int> <int>          <int>
##  1 Otro           NA           NA        NA    NA    NA             NA
##  2 Otro           NA           NA        NA    NA    NA             NA
##  3 Otro           NA           NA        NA    NA    NA             NA
##  4 Otro           NA           NA        NA    NA    NA             NA
##  5 Otro           NA           NA        NA    NA    NA             NA
##  6 Otro           NA           NA        NA    NA    NA             NA
##  7 Otro           NA           NA        NA     1    NA             NA
##  8 Otro           NA           NA        NA     2    NA             NA
##  9 Otro           NA           NA        NA     2    NA             NA
## 10 Otro           NA           NA        NA     1    NA             NA
## # ... with 920 more rows, and 6,720 more variables: abiertas <int>,
## #   abiertos <int>, abogado <int>, abogados <int>, abordar <int>,
## #   abordo <int>, about <int>, abracemos <int>, abrazo <int>, abre <int>,
## #   abren <int>, abreviar <int>, abrieron <int>, abril <int>, abrir <int>,
## #   abrirá <int>, absolutismo <int>, abusaron <int>, abusó <int>,
## #   acá <int>, acaba <int>, acabar <int>, acabará <int>, acabarán <int>,
## #   acabo <int>, acabó <int>, académica <int>, académicas <int>,
## #   académico <int>, académicos <int>, acala <int>, acatitla <int>,
## #   acayucan <int>, accede <int>, acceso <int>, accidente <int>,
## #   acción <int>, acciones <int>, account <int>, accounting <int>,
## #   acdc <int>, acelera <int>, acento <int>, aceptado <int>,
## #   aceptar <int>, acepto <int>, aceptó <int>, acerca <int>,
## #   acertijo <int>, ácido <int>, aclamada <int>, aclare <int>,
## #   aclaren <int>, acompañada <int>, acompañados <int>, acompañan <int>,
## #   acompañando <int>, acompañantes <int>, acompañar <int>,
## #   acompañarte <int>, acompañé <int>, aconseja <int>, aconsejable <int>,
## #   acoplamiento <int>, acordamos <int>, acordar <int>, acordé <int>,
## #   acoso <int>, acostumbraba <int>, activa <int>, activado <int>,
## #   activas <int>, actividad <int>, actividades <int>, activista <int>,
## #   acto <int>, actor <int>, actos <int>, actriz <int>, actuación <int>,
## #   actual <int>, actualidad <int>, actualizaciones <int>,
## #   actualizadas <int>, actualizar <int>, actualmente <int>,
## #   actuando <int>, actuar <int>, acude <int>, acudes <int>,
## #   acuerdo <int>, acuerdos <int>, acuíferos <int>, acusa <int>,
## #   acusan <int>, acusen <int>, ad <int>, adán <int>, adaptación <int>,
## #   adaptado <int>, ...
bayes_cmll[["sets"]][["test"]]
## # A tibble: 387 x 6,727
##    screen_name  `_mx` `_paolimeow` `_siqure`     a abajo abastecimiento
##    <fct>        <int>        <int>     <int> <int> <int>          <int>
##  1 CMLL_OFICIAL    NA           NA        NA     1    NA             NA
##  2 CMLL_OFICIAL    NA           NA        NA     4    NA             NA
##  3 CMLL_OFICIAL    NA           NA        NA     2    NA             NA
##  4 CMLL_OFICIAL    NA           NA        NA     1    NA             NA
##  5 CMLL_OFICIAL    NA           NA        NA     1    NA             NA
##  6 CMLL_OFICIAL    NA           NA        NA     2    NA             NA
##  7 CMLL_OFICIAL    NA           NA        NA     1    NA             NA
##  8 CMLL_OFICIAL    NA           NA        NA     1    NA             NA
##  9 CMLL_OFICIAL    NA           NA        NA     1    NA             NA
## 10 CMLL_OFICIAL    NA           NA        NA    NA    NA             NA
## # ... with 377 more rows, and 6,720 more variables: abiertas <int>,
## #   abiertos <int>, abogado <int>, abogados <int>, abordar <int>,
## #   abordo <int>, about <int>, abracemos <int>, abrazo <int>, abre <int>,
## #   abren <int>, abreviar <int>, abrieron <int>, abril <int>, abrir <int>,
## #   abrirá <int>, absolutismo <int>, abusaron <int>, abusó <int>,
## #   acá <int>, acaba <int>, acabar <int>, acabará <int>, acabarán <int>,
## #   acabo <int>, acabó <int>, académica <int>, académicas <int>,
## #   académico <int>, académicos <int>, acala <int>, acatitla <int>,
## #   acayucan <int>, accede <int>, acceso <int>, accidente <int>,
## #   acción <int>, acciones <int>, account <int>, accounting <int>,
## #   acdc <int>, acelera <int>, acento <int>, aceptado <int>,
## #   aceptar <int>, acepto <int>, aceptó <int>, acerca <int>,
## #   acertijo <int>, ácido <int>, aclamada <int>, aclare <int>,
## #   aclaren <int>, acompañada <int>, acompañados <int>, acompañan <int>,
## #   acompañando <int>, acompañantes <int>, acompañar <int>,
## #   acompañarte <int>, acompañé <int>, aconseja <int>, aconsejable <int>,
## #   acoplamiento <int>, acordamos <int>, acordar <int>, acordé <int>,
## #   acoso <int>, acostumbraba <int>, activa <int>, activado <int>,
## #   activas <int>, actividad <int>, actividades <int>, activista <int>,
## #   acto <int>, actor <int>, actos <int>, actriz <int>, actuación <int>,
## #   actual <int>, actualidad <int>, actualizaciones <int>,
## #   actualizadas <int>, actualizar <int>, actualmente <int>,
## #   actuando <int>, actuar <int>, acude <int>, acudes <int>,
## #   acuerdo <int>, acuerdos <int>, acuíferos <int>, acusa <int>,
## #   acusan <int>, acusen <int>, ad <int>, adán <int>, adaptación <int>,
## #   adaptado <int>, ...
  • Una lista con los resultados de Naïve Bayes: modelo y predicciones.
bayes_cmll[["resultado"]][["modelo"]]
===================== Naive Bayes ===================== 
Call: 
naive_bayes.formula(formula = bayes_formula, data = lista_sets[["train"]])

A priori probabilities: 

CMLL_OFICIAL         Otro 
   0.1129032    0.8870968 

Tables: 
      
_mx    CMLL_OFICIAL Otro
  mean                 1
  sd                    

          
_paolimeow CMLL_OFICIAL Otro
      mean                 1
      sd                    

       
_siqure CMLL_OFICIAL Otro
   mean                 1
   sd                    

      
a      CMLL_OFICIAL      Otro
  mean    1.3191489 1.3054545
  sd      0.5936762 0.6345923

      
abajo  CMLL_OFICIAL Otro
  mean                 1
  sd                   0

# ... and 6721 more tables
bayes_cmll[["resultado"]][["prediccion"]]
 [1] CMLL_OFICIAL CMLL_OFICIAL CMLL_OFICIAL CMLL_OFICIAL
 [5] CMLL_OFICIAL CMLL_OFICIAL CMLL_OFICIAL Otro        
 [9] CMLL_OFICIAL CMLL_OFICIAL CMLL_OFICIAL Otro        
[13] Otro         CMLL_OFICIAL CMLL_OFICIAL CMLL_OFICIAL
[17] CMLL_OFICIAL CMLL_OFICIAL CMLL_OFICIAL CMLL_OFICIAL
[21] Otro         CMLL_OFICIAL Otro         Otro        
[25] CMLL_OFICIAL
Levels: CMLL_OFICIAL Otro
  • Una matriz de confusión
bayes_cmll[["confusion"]]

 Confusion Matrix and Statistics

               Reference
 Prediction     CMLL_OFICIAL Otro
   CMLL_OFICIAL           38    8
   Otro                   12  329

                Accuracy : 0.9483          
                  95% CI : (0.9213, 0.9682)
     No Information Rate : 0.8708          
     P-Value [Acc > NIR] : 3.295e-07       

                   Kappa : 0.7622          
  Mcnemar's Test P-Value : 0.5023          

             Sensitivity : 0.76000         
             Specificity : 0.97626         
          Pos Pred Value : 0.82609         
          Neg Pred Value : 0.96481         
              Prevalence : 0.12920         
          Detection Rate : 0.09819         
    Detection Prevalence : 0.11886         
       Balanced Accuracy : 0.86813         

        'Positive' Class : CMLL_OFICIAL

Finalmente, si así lo deseamos, podemos simplificar aún más nuestro código con la función map() del paquete purrr, que aprovecha las capacidades de programación funcional de R. De este modo podemos hacer múltiples análisis con una sóla línea de código.

Para ello creamos una lista con todos los nombres de usuario que nos interesan y después le aplicamos nuestra función hacer_bayes() con map.

lista_usuarios <- list(lopezobrador_ = "lopezobrador_",
                       MSFTMexico = "MSFTMexico",
                       UNAM_MX  = "UNAM_MX",
                       CMLL_OFICIAL = "CMLL_OFICIAL")
lista_bayes <- map(lista_usuarios, hacer_bayes, tabla = tuits_df)

Para concluir

En este artículo revisamos cómo implementar Naïve Bayes para clasificar texto usando R. Como pudimos ver, la parte más compleja del proceso es preparar los datos para análisis, una vez hecho esto, ajustar un modelo de Naïve Bayes y evaluar su precisión es relativamente sencillo.

Dimos un vistazo a algunas de las medidas usadas para evaluar la precisión de las predicciónes de un modelo de clasificación. Con ello comprobamos que es posible obtener buenos resultados de Naïve Bayes incluso sin hacer ajustes específicos.

También revisamos como podemos sistematizar este tipo de análisis, de modo tal que nos sea posible realizarlos de manere repetida con un mínimo de esfuerzo.

Ha quedado pendiente discutir de qué manera podemos mejorar nuestro modelo de predicción, por ejemplo, mediante el uso de Laplace Smoothing o de probabilidades a priori, pero con lo aquí presentado debería ser un punto de partida suficiente para que estes en condiciones de realizar clasificaciones de texto usando Naïve Bayes y R.


Dudas, comentarios y correcciones son bienvenidas:

El código y los datos usados en este documento se encuentran en Github:

https://github.com/jboscomendoza/rpubs/tree/master/bayes_twitter

Este artículo se publicó originalmente en abril de 2017 en:

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *