library(tidyverse)
library(tm)
library(SnowballC)
library(wordcloud)
library(e1071)
library(caret)
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)
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
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)
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
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")
wordcloud(spam$text, min.freq = 50, random.order = FALSE)
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.
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.
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)
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
##
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.