Objetivo

En las siguientes sesiones, queremos ilustrar como utilizar el teorema de Bayes para implementar un detector de correo basura.

\[ P(\text{spam} \mid \text{millonario})=\frac{P(\text{millonario}\mid \text{spam}) P(\text{spam})}{P(\text{millonario})}\]

Ejemplo de juguete

Introducimos la idéa con este ejemplo sencillo.

Ejemplo con datos de SMS basura

El archivo sms.csv contiene un estudio de mensajes SMS clasificados como correo basura (spam) o normal (ham). Podemos guardar el archivo en nuestros ordenadores personales. Luego al abrir R, cambiamos el directorio al directorio donde tenemos guardado el archivo.

rm(list=ls())
setwd("C:/Users/mwiper/Desktop") # Cambia esta comando como es necesario 
sms <- read.csv("sms.csv",sep=",")
names(sms)
## [1] "type" "text"
head(sms)
##   type
## 1  ham
## 2  ham
## 3  ham
## 4 spam
## 5 spam
## 6  ham
##                                                                                                                                                                text
## 1                                                                                                                 Hope you are having a good week. Just checking in
## 2                                                                                                                                           K..give back my thanks.
## 3                                                                                                                       Am also doing in cbe only. But have to pay.
## 4            complimentary 4 STAR Ibiza Holiday or £10,000 cash needs your URGENT collection. 09066364349 NOW from Landline not to lose out! Box434SK38WP150PPM18+
## 5 okmail: Dear Dave this is your final notice to collect your 4* Tenerife Holiday or #5000 CASH award! Call 09061743806 from landline. TCs SAE Box326 CW25WX 150ppm
## 6                                                                                                                Aiya we discuss later lar... Pick u up at 4 is it?

Queremos utilizar el clasificador bayesiano ingenuo para construir un filtro de correo basura.

Preparación del Corpus

Un corpus es una collección de documentos.

library(tm)
## Warning: package 'tm' was built under R version 3.5.2
## Loading required package: NLP
corpus <- Corpus(VectorSource(sms$text))
inspect(corpus[1:3])
## <<SimpleCorpus>>
## Metadata:  corpus specific: 1, document level (indexed): 0
## Content:  documents: 3
## 
## [1] Hope you are having a good week. Just checking in
## [2] K..give back my thanks.                          
## [3] Am also doing in cbe only. But have to pay.

El comando VectorSource dice a la función corpus que cada documento es una entrada en el vector.

Limpieza del Corpus

Distintos textos pueden contener la misma palabra Hello!, Hello, hello y lo normal es considerar todas las versiones como la misma.

clean_corpus <- tm_map(corpus, tolower)
## Warning in tm_map.SimpleCorpus(corpus, tolower): transformation drops
## documents
inspect(clean_corpus[1:3])
## <<SimpleCorpus>>
## Metadata:  corpus specific: 1, document level (indexed): 0
## Content:  documents: 3
## 
## [1] hope you are having a good week. just checking in
## [2] k..give back my thanks.                          
## [3] am also doing in cbe only. but have to pay.

Puede ser útil también quitar los números …

clean_corpus <- tm_map(clean_corpus, removeNumbers)
## Warning in tm_map.SimpleCorpus(clean_corpus, removeNumbers): transformation
## drops documents

… y la puntuación.

clean_corpus <- tm_map(clean_corpus, removePunctuation)
## Warning in tm_map.SimpleCorpus(clean_corpus, removePunctuation):
## transformation drops documents

Hay muchas palabras que no van a ser informativas como el, un, a, y, … llamadas palabras vacias o stop words.

stopwords("en")[1:10]
##  [1] "i"         "me"        "my"        "myself"    "we"       
##  [6] "our"       "ours"      "ourselves" "you"       "your"
clean_corpus <- tm_map(clean_corpus, removeWords,
stopwords("en"))
## Warning in tm_map.SimpleCorpus(clean_corpus, removeWords, stopwords("en")):
## transformation drops documents

Finalmente queremos quitar el exceso de espacios blancos.

Remove the excess white space:

clean_corpus <- tm_map(clean_corpus, stripWhitespace)
## Warning in tm_map.SimpleCorpus(clean_corpus, stripWhitespace):
## transformation drops documents

Visualización de los datos: nubes de palabras

La manera más sencilla de ver la diferencia entre mensajes de correo basura y mensajes normales es hacer nubes de las palabras más frecuentes en los dos tipos de mensaje.

Primero obtenemos los índices de los mensajes basura y los mensajes normales:

spam_indices <- which(sms$type == "spam")
spam_indices[1:3]
## [1] 4 5 9
ham_indices <- which(sms$type == "ham")
ham_indices[1:3]
## [1] 1 2 3

Ahora vemos las palabras más frecuentes (que ocurren por lo menos 40 veces) en las dos clases de mensaje. Primero los mensajes normales …

require(wordcloud)
## Loading required package: wordcloud
## Loading required package: RColorBrewer
library(wordcloud)
wordcloud(clean_corpus[ham_indices], min.freq=40, scale=c(3,.5))

… y segundo los mensajes basura.

wordcloud(clean_corpus[spam_indices], min.freq=40, scale=c(5,.8))

Construyendo un filtro de Spam

Dividimos los datos en una parte de entrenimiento y otra parte para comprobar la eficacia del filtro ajustado. En este caso, usamos 75% de los datos para entrenimiento y los restantes para la prueba.

Con los datos originales …

nobs=dim(sms)[1]
train = 1:round(nobs*0.75)
test=(round(nobs*0.75)+1):nobs
sms_train <- sms[train,]
sms_test <- sms[test,]

y el corpus limpio:

corpus_train <- clean_corpus[train]
corpus_test <- clean_corpus[test]

Computar la frecuencia de términos

Usando DocumentTermMatrix, hacemos una estructura de matriz esparsa donde las filas de la matriz refieren al documento y las columnas refieren a las palabras.

sms_dtm <- DocumentTermMatrix(clean_corpus)
inspect(sms_dtm[1:4, 3:10])
## <<DocumentTermMatrix (documents: 4, terms: 8)>>
## Non-/sparse entries: 8/24
## Sparsity           : 75%
## Maximal term length: 6
## Weighting          : term frequency (tf)
## Sample             :
##     Terms
## Docs also back cbe hope just kgive thanks week
##    1    0    0   0    1    1     0      0    1
##    2    0    1   0    0    0     1      1    0
##    3    1    0   1    0    0     0      0    0
##    4    0    0   0    0    0     0      0    0

Dividimos la matriz en la parte de entrenimiento y la parte de prueba.

sms_dtm_train <- sms_dtm[train,]
sms_dtm_test <- sms_dtm[test,]

Identificar palabras frecuentes

No confundimos el clasificador con palabras que ocurren muy pocas (menos de 5) veces.

five_times_words <- findFreqTerms(sms_dtm_train, 5)
length(five_times_words)
## [1] 1228
five_times_words[1:5]
## [1] "checking" "good"     "hope"     "just"     "week"

Reconstruimos las matrices con las palabras frecuentes:

sms_dtm_train <- DocumentTermMatrix(corpus_train, control=list(dictionary = five_times_words))
sms_dtm_test <- DocumentTermMatrix(corpus_test, control=list(dictionary = five_times_words))

Convertir la información de los conteos a Si o No

El clasificador precisa la información de presencia o ausencia de palabras y no de conteos.

convert_count <- function(x){
y <- ifelse(x > 0, 1,0)
y <- factor(y, levels=c(0,1), labels=c("No", "Si"))
y
}

Convertir las matrices

sms_dtm_train <- apply(sms_dtm_train, 2, convert_count)
sms_dtm_train[1:4, 30:35]
##     Terms
## Docs lar  later pick much ask  father
##    1 "No" "No"  "No" "No" "No" "No"  
##    2 "No" "No"  "No" "No" "No" "No"  
##    3 "No" "No"  "No" "No" "No" "No"  
##    4 "No" "No"  "No" "No" "No" "No"
sms_dtm_test <- apply(sms_dtm_test, 2, convert_count)
sms_dtm_test[1:4, 3:10]
##     Terms
## Docs home can  come plan point room weekend cool
##    1 "Si" "No" "No" "No" "No"  "No" "No"    "No"
##    2 "No" "Si" "Si" "Si" "Si"  "Si" "Si"    "No"
##    3 "No" "No" "No" "No" "No"  "No" "No"    "Si"
##    4 "No" "No" "No" "No" "No"  "No" "No"    "No"

Construir el clasificador bayesiano ingenuo

Utilizamos el clasificador del paquete e1071. Utilizamos los datos de entrenimiento para estimar las probabilidades.

library(e1071)
## Warning: package 'e1071' was built under R version 3.5.2
classifier <- naiveBayes(sms_dtm_train, sms_train$type)
class(classifier)
## [1] "naiveBayes"

Mirar las predicciones en la muestra de entrenimiento

Calculamos las predicciones

predicciones <- predict(classifier, newdata=sms_dtm_train)

y comparar las predcciones con los datos reales.

table(predicciones, sms_train$type)
##             
## predicciones  ham spam
##         ham  3593   52
##         spam   12  512

Dentro de muestra, clasifica todos los mensajes normales salvo 12 correctamente y clasifica mal sólo 52 de 564 mensajes basura.

Mirar como van las predicciones en la muestra de prueba

predicciones <- predict(classifier, newdata=sms_dtm_test)
table(predicciones, sms_test$type)
##             
## predicciones  ham spam
##         ham  1202   31
##         spam    5  152

Funcionamiento del clasificador:

Funciona bastante bien.

El clasificador bayesiano

Puede que existan problemas con la estimación clásica cuando no aparecen ciertas palabras en los mensajes de una clase.

Ilustramos el problema con el ejemplo .

El clasificador con distribuciones a priori uniformes equivale a la llamada ‘’suavización de Laplace’’. En el ejemplo de los sms, se puede incluirlo con:

B.clas <- naiveBayes(sms_dtm_train, sms_train$type,laplace = 1)
class(B.clas)
## [1] "naiveBayes"
B.preds <- predict(B.clas, newdata=sms_dtm_test)
table(B.preds, sms_test$type)
##        
## B.preds  ham spam
##    ham  1204   31
##    spam    3  152

El funcionamiento del clasificador bayesiano mejora un poco con respeto al clasificador frecuentista.