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})}\]
Introducimos la idéa con este ejemplo sencillo.
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.
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.
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
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))
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]
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,]
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))
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
}
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"
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"
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.
predicciones <- predict(classifier, newdata=sms_dtm_test)
table(predicciones, sms_test$type)
##
## predicciones ham spam
## ham 1202 31
## spam 5 152
Funcionamiento del clasificador:
Clasifica correctamente 1202/1207 = 99% de los correos normales;
Clasifica correctamente 152/183 = 83% de los mensajes basura ;
Funciona bastante bien.
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.