library(tidyverse)
library(tm)
library(SnowballC)
library(wordcloud)
library(e1071)
library(caret)

Recogida de datos

Para este análisis usaremos el dataset que se encuentra en este site.

Este conjunto de datos incluye el texto de los mensajes SMS junto con una etiqueta que indica si el mensaje no es deseado. Los mensajes basura se etiquetan como spam, mientras que los mensajes legítimos se etiquetan como ham.

Descargamos el archivo y factorizamos la columna type

# Asignamos a la variable 'temp' el archivo en cuestión
archivo = "https://raw.githubusercontent.com/stedy/Machine-Learning-with-R-datasets/master/sms_spam.csv"
file <- download.file(archivo, destfile = "sms_spam.csv")
# Utilizamos la función 'unz' para extraer el archivo CSV y lo asignamos a la variable 'temp'
data <- read.csv("sms_spam.csv", stringsAsFactors = F)
data$type <- factor(data$type)

Limpieza y normalización de los textos

A continuación utilizaremos las funcionalidades del paquete de text mining tm.

En primer lugar guardamos el conjunto de textos con todas sus palabras en un espacio de memoria temporal de tipo VCorpus

sms_text <- VCorpus(VectorSource(data$text))

A continuación efectuamos las siguientes operaciones en los textos

  1. Transformar en minúsculas
  2. Eliminar todos los números
  3. Eliminar las stop words
  4. Eliminar los signos de puntuación
  5. Stemming
  6. Eliminar espacios sobrantes
sms_text_clean_1 <- tm_map(sms_text, content_transformer(tolower))
sms_text_clean_2 <- tm_map(sms_text_clean_1, removeNumbers)
sms_text_clean_3 <- tm_map(sms_text_clean_2, removeWords, stopwords())

# Para evitar el problema de unir palabras, haremos una función que reemplace los signos de puntuación por espacios, en lugar de eliminarlos
sms_text_clean_4 <- tm_map(sms_text_clean_3, content_transformer(function(x) { gsub("[[:punct:]]+", " ", x) }))

sms_text_clean_5 <- tm_map(sms_text_clean_4, stemDocument)
sms_text_clean_6 <- tm_map(sms_text_clean_5, stripWhitespace)

A continuación, construiremos la matriz de términos del documento. Con ella representaremos la frecuencia con la cual aparecen todas las palabras en todos los mensajes.

sms_dtm <- DocumentTermMatrix(sms_text_clean_6)

Creación de datasets de training y test

Dividiremos los datos en dos partes: 75 por ciento para entrenamiento y 25 por ciento para pruebas. Además, gurardaremos las etiquetas con la clasificación de cada SMS en otros dos vectores.

sms_dtm_train <- sms_dtm[1:4169, ] 
sms_dtm_test <- sms_dtm[4170:5574, ]

sms_train_labels <- data[1:4169, ]$type 
sms_test_labels <- data[4170:5574, ]$type

Tanto los datos de entrenamiento como los datos de prueba contienen alrededor del 13 por ciento de spam. Esto sugiere que los mensajes de spam se han dividido proporcionalmente entre los dos conjuntos de datos.

prop.table(table(sms_train_labels))
## sms_train_labels
##       ham      spam 
## 0.8647158 0.1352842
prop.table(table(sms_test_labels))
## sms_test_labels
##       ham      spam 
## 0.8697509 0.1302491

Visualización de los datos en nubes de palabras

Mostraremos una nube de palabras con los términos más frecuentes de los mensajes. Escogemos como parámetro una frecuencia mínima de 100 apariciones, lo que supone que mostraremos palabras que aparezcan como mínimo en el 2% de los mensajes.

wordcloud(sms_text_clean_6, min.freq = 100, random.order = FALSE)

Necesitamos hacernos una idea de si existe alguna diferencia entre las palabras más utilizadas para Spam y para Ham, para ello crearemos dos subsets y sus nubes de palabras correspondientes.

spam <- filter(data, data$type == "spam")
ham <- filter(data, data$type == "ham")
  1. SPAM
wordcloud(spam$text, min.freq = 50, random.order = FALSE)

  1. HAM
wordcloud(ham$text, min.freq = 100, random.order = FALSE)

Las palabras que más aparecen en los mensajes de SPAM son call, free, txt, mobile, text, claim, prize y urgent.

Reducción de características

Actualmente la matriz de mensajes/palabras tiene aproximadamente 6500 características o palabras, es decir, hay una característica por cada palabra que aparezca al menos un mensaje SMS. Para reducirlas, eliminaremos cualquier palabra que aparezca en menos de 7 mensajes. Esto nos da un total de 900 palabras frecuentes.

sms_freq_words <- findFreqTerms(sms_dtm_train, 7)

Reducimos el número de columnas relativas a las palabras utilizadas a las de las palabras más frecuentes.

sms_dtm_freq_test <- sms_dtm_test[, sms_freq_words]
sms_dtm_freq_train <- sms_dtm_train[, sms_freq_words]

El clasificador Naive Bayes es normalmente entrenado con características categóricas, y lo que tenemos en realidad es un número que indica el número de veces que aparece la palabra en el mensaje. Necesitamos cambiarlo a una variable categórica que indique simplemente si la palabra aparece o no.

convert_counts <- function(x){
  x <- ifelse(x > 0, "yes", "no")
}

sms_dtm_freq_test <- apply(sms_dtm_freq_test, MARGIN = 2, convert_counts)
sms_dtm_freq_train <- apply(sms_dtm_freq_train, MARGIN = 2, convert_counts)

Ya tenemos lista la matriz para hacer el análisis Naive Bayes: cada fila indica cada uno de los mensajes de texto, y cada columna indica si esa palabra de uso frecuente se utiliza en cada uno de los mensajes.

Entrenamiento del modelo

Vamos a crear un objeto que contenga un clasificador NaiveBayes que pueda ser utilizado para hacer predicciones.

Este algoritmo construye tablas de probabilidades que se utilizan para estimar la probabilidad de que los nuevos ejemplos pertenezcan a varias clases. Las probabilidades se calculan utilizando el teorema de Bayes, que especifica cómo se relacionan los eventos dependientes. Aunque el teorema de Bayes puede ser costoso desde el punto de vista computacional, una versión simplificada que hace supuestos supuestos “ingenuos” sobre la independencia de las características es capaz de manejar conjuntos de datos extremadamente grandes.

sms_classifier <- naiveBayes(sms_dtm_freq_train, sms_train_labels)

Evaluación del modelo

Utilizaremos el clasificador para predecir la naturaleza de los mensajes en el dataset de test, y posteriormente comparar las etiquetas generadas con las etiquetas reales.

prediccion_test <- predict(sms_classifier, sms_dtm_freq_test)
confusionMatrix(data = prediccion_test, reference = sms_test_labels)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  ham spam
##       ham  1216   15
##       spam    6  168
##                                           
##                Accuracy : 0.9851          
##                  95% CI : (0.9772, 0.9907)
##     No Information Rate : 0.8698          
##     P-Value [Acc > NIR] : < 2e-16         
##                                           
##                   Kappa : 0.9326          
##                                           
##  Mcnemar's Test P-Value : 0.08086         
##                                           
##             Sensitivity : 0.9951          
##             Specificity : 0.9180          
##          Pos Pred Value : 0.9878          
##          Neg Pred Value : 0.9655          
##              Prevalence : 0.8698          
##          Detection Rate : 0.8655          
##    Detection Prevalence : 0.8762          
##       Balanced Accuracy : 0.9566          
##                                           
##        'Positive' Class : ham             
## 

Mejora del modelo

A la hora de generar el modelo Naive Bayes, hemos dejado como valor por defecto del parámetro de Laplace = 0. Esto hace que las palabras que tengan una frecuencia de cero, ponderen excepcionalmente en el proceso de clasificación. Es decir, el hecho de que una determinada palabra solamente haya aparecido en un tipo de mensaje, no quiere decir que cualquier mensaje donde aparezca esa palabra deba ser clasificado como tal. Por ello, configuraremos el parámetro laplace = 1.

sms_classifier2 <- naiveBayes(sms_dtm_freq_train, sms_train_labels, laplace = 1)
prediccion_test2 <- predict(sms_classifier2, sms_dtm_freq_test)
confusionMatrix(data = prediccion_test2, reference = sms_test_labels)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  ham spam
##       ham  1216   19
##       spam    6  164
##                                           
##                Accuracy : 0.9822          
##                  95% CI : (0.9738, 0.9885)
##     No Information Rate : 0.8698          
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.919           
##                                           
##  Mcnemar's Test P-Value : 0.0164          
##                                           
##             Sensitivity : 0.9951          
##             Specificity : 0.8962          
##          Pos Pred Value : 0.9846          
##          Neg Pred Value : 0.9647          
##              Prevalence : 0.8698          
##          Detection Rate : 0.8655          
##    Detection Prevalence : 0.8790          
##       Balanced Accuracy : 0.9456          
##                                           
##        'Positive' Class : ham             
## 

Como resultado obtenemos que no ha variado el número de falsos negativos (6), que era el número que nos interesaba reducir: evitar que un mensaje sea clasificado como spam cuando en realidad es ham. Sin embargo, sí ha aumentado el número de falsos positivos.