naive bayes識(shí)別垃圾短信

本文對(duì)brett的機(jī)器學(xué)習(xí)與R語(yǔ)言(Machine Learning with R)一書(shū)中的垃圾短信識(shí)別的筆記。在brett的書(shū)中,介紹了如何通過(guò)naive bayes對(duì)短信進(jìn)行訓(xùn)練,并預(yù)測(cè)短信是否為垃圾短信。naive bayes的精度可以達(dá)到98.06%,召回率為87.6%,準(zhǔn)確率為97.6%。

在kaggle上有一個(gè)類(lèi)似的dataset,叫SMS Spam Collection Dataset,kaggle spam sms。Brett書(shū)中的數(shù)據(jù)是對(duì)該數(shù)據(jù)源的修正,內(nèi)容上基本類(lèi)似,這里使用kaggle的數(shù)據(jù)集。

首先加載需要用到的包

require(data.table)     # read the csv file in a faster way
require(magrittr)       # enable pipeline operator
require(tm)             # construct text vector
require(e1071)          # naive bayes 
require(gmodels)        # create contigency table
require(caret)          # split data into training and test sets

首先通過(guò)data.table包來(lái)讀取數(shù)據(jù)

sms_raw <- fread(
    "../data/SMSSpamCollection",
    header = FALSE,
    encoding = "Latin-1",
    sep = "\t"
)

需要注意的是,這里讀取的是kaggle上的數(shù)據(jù)集,同時(shí)也在UCI上存放,uci sms。SMSSpamCollection有兩列數(shù)據(jù),第一列表示短信是否為垃圾短信,spam表示垃圾短信,ham表示正常短信;第二列是具體的短信內(nèi)容。將SMSSpamCollection的內(nèi)容讀入R語(yǔ)言,并保存在sms_raw變量中,通過(guò)setnames修改列名。

setnames(sms_raw, c("type", "text"))
sms_raw[, type:= factor(type)]

sms_raw有5574行文本數(shù)據(jù)。

 sms_raw[, .N]
[1] 5574

可以大致看看sms_raw中垃圾短信與正常短信的條數(shù)

table(sms_raw[, type]) %>% prop.table()

      ham      spam
0.8659849 0.1340151

其中有86.6%左右的短信是正常短信,有13.4%的短信為垃圾短信。下面分別看一下垃圾短信與正常短信的云圖,看看二者在文本內(nèi)容上是否有顯著區(qū)別。

require(dplyr)
require(wordcloud)
require(RColorBrewer)
pal <- brewer.pal(7, "Dark2")
sms_raw[type == "spam", text] %>%
    wordcloud(min.freq = 20,
              random.order = FALSE, colors = pal
    )
sms_raw[type == "ham", text] %>%
    wordcloud(min.freq = 70,
              random.order = FALSE, colors = pal
    )

通過(guò)如上的程序,分別得到spam有ham短信的云圖,可以從云圖上可以得知,垃圾短信中以較大的概率出現(xiàn)free,而正常短信更多的是一些常規(guī)描述的詞匯。根據(jù)出現(xiàn)的單詞,可以大致判斷出短信是否是垃圾短信。

ham_wordcloud.jpg
spam_wordcloud.jpg

本文采用naive bayes的方法識(shí)別垃圾短信,根據(jù)naive bayes的條件獨(dú)立的假設(shè),若確定短信的是垃圾短信與否之后,短信內(nèi)的單詞相互獨(dú)立,相互不影響。雖然這個(gè)假設(shè)與事實(shí)不符,比如若垃圾短信中出現(xiàn)buy,那么出現(xiàn)on sale的概率會(huì)相對(duì)而言更大(相對(duì)于不知道buy這個(gè)單詞,此處是條件獨(dú)立的轉(zhuǎn)述),但是這對(duì)結(jié)果卻沒(méi)多大影響。

為評(píng)估naive bayes的性能,將數(shù)據(jù)集分成訓(xùn)練集和測(cè)試集,75%的數(shù)據(jù)用于訓(xùn)練,25%的數(shù)據(jù)用于評(píng)估算法的性能。

set.seed(1071)
train_index <- createDataPartition(sms_raw$type, p = 0.75, list = FALSE)
sms_raw_train <- sms_raw[train_index, ]
sms_raw_test <- sms_raw[-train_index, ]

其中設(shè)置隨機(jī)數(shù)是為了結(jié)果的可重復(fù)性,1071是我常用的一個(gè)隨機(jī)數(shù),是R語(yǔ)言中著名的機(jī)器學(xué)習(xí)包e1071的數(shù)字部分。createDataPartition則來(lái)自于caret包,該函數(shù)通過(guò)對(duì)數(shù)據(jù)進(jìn)行抽樣,保證訓(xùn)練集與測(cè)試集中,垃圾短信的比例一致,避免訓(xùn)練集中出現(xiàn)大量的正常短信,而幾乎沒(méi)有垃圾短信這樣的情況。createDataPartition 的第一個(gè)參數(shù)是vector,函數(shù)根據(jù)這個(gè)參數(shù)內(nèi)容進(jìn)行抽樣,p=0.75表示75%的數(shù)據(jù)進(jìn)入訓(xùn)練集,則有25%的數(shù)據(jù)進(jìn)入測(cè)試集,list=FASLE表示返回結(jié)果的格式為常規(guī)的數(shù)組,否則將返回一個(gè)列表,更具體的用法可以參考對(duì)應(yīng)的幫助文檔。

下面看看訓(xùn)練集與測(cè)試集中spam/ham郵件的分布情況,通過(guò)上述的抽樣,訓(xùn)練集與測(cè)試集中郵件分布一致。

table(sms_raw_train[, type]) %>% prop.table()

      ham      spam
0.8658537 0.1341463
table(sms_raw_test[, type]) %>% prop.table()

      ham      spam
0.8663793 0.1336207

為簡(jiǎn)化后續(xù)的文本處理,定義兩個(gè)輔助函數(shù),corpus生成語(yǔ)料庫(kù),clean函數(shù)則對(duì)語(yǔ)料庫(kù)進(jìn)行一些清洗,比如刪除數(shù)字,stopwords,標(biāo)點(diǎn)符號(hào),首尾的空白字符等。

corpus <- function(x) VectorSource(x) %>% VCorpus(readerControl = list(reader = readPlain))
clean <- function(x) {
    x %>%
        tm_map(content_transformer(tolower)) %>%
        tm_map(content_transformer(removeNumbers)) %>%
        tm_map(content_transformer(removeWords), stopwords()) %>%
        tm_map(content_transformer(removePunctuation)) %>%
        tm_map(content_transformer(stripWhitespace))
}

這里有一個(gè)坑,如果使用常規(guī)的Corpus函數(shù)代替上述的VCorpus,則有可能導(dǎo)致后續(xù)的預(yù)測(cè)出現(xiàn)反常的現(xiàn)象,比如大部分的spam郵件預(yù)測(cè)為ham。具體原因我沒(méi)有細(xì)究,在調(diào)試跟蹤tmt包多個(gè)函數(shù)后,發(fā)現(xiàn)若使用Corpus函數(shù),在對(duì)訓(xùn)練集的文本構(gòu)建document-term frequency矩陣時(shí),出現(xiàn)錯(cuò)誤的結(jié)果。所以解決方法是使用VCorpus,并指定reader,在后續(xù)的操作中,使用content_transformer包裝所有的處理函數(shù)。

通過(guò)上述的輔助函數(shù),構(gòu)建訓(xùn)練集的預(yù)料數(shù)據(jù)

sms_corpus_train <- corpus(sms_raw_train[, text]) %>% clean

可以通過(guò)inspect函數(shù)查看對(duì)應(yīng)的corpus

sms_raw_train[1, text]
[1] "Ok lar... Joking wif u oni..."

inspect(sms_corpus_train[[1]])
<<PlainTextDocument>>
Metadata:  7
Content:  chars: 23

ok lar joking wif u oni

可知原始的文本,已經(jīng)轉(zhuǎn)換成小寫(xiě)單詞,且刪除了標(biāo)點(diǎn)符號(hào)。
下一步根據(jù)corpus構(gòu)建文檔詞頻矩陣(document-term frequency)

sms_dtm_train_all <- DocumentTermMatrix(sms_corpus_train)

可以刪除出現(xiàn)次數(shù)過(guò)少的單詞,這些單詞出現(xiàn)較少,刪除這些單詞對(duì)預(yù)測(cè)的結(jié)果沒(méi)有(估計(jì))影響。

sms_dict <- findFreqTerms(sms_dtm_train_all, 5)

刪除詞頻少于5的單詞,剩下的單詞做為后續(xù)構(gòu)建dtm的單詞表sms_dict,sms_dict其實(shí)是字符串列表,是那些出現(xiàn)頻次超過(guò)5次的單詞。
根據(jù)前文構(gòu)建的單詞表,重新構(gòu)建訓(xùn)練集和測(cè)試集的dtm矩陣。

sms_dtm_train <- DocumentTermMatrix(
    sms_corpus_train, control = list(dictionary = sms_dict)
)

sms_dtm_test <- DocumentTermMatrix(
    sms_corpus_test, control = list(dictionary = sms_dict)
)

DocumentTermMatrix返回的是一種特殊的矩陣,類(lèi)似于稀疏矩陣,是繼承于slam包的simple_triplet_matrix,不過(guò)不需要深入了解底層的結(jié)構(gòu),把sms_dtm_train當(dāng)成普通的矩陣看待即可。

sms_dtm_train_all %>% class
[1] "DocumentTermMatrix"    "simple_triplet_matrix"

在naive bayes的算法中,計(jì)算的是單詞表中,每個(gè)單詞出現(xiàn)與否的概率,上述產(chǎn)生的dtm矩陣記錄的是每條短信中,每個(gè)單詞的出現(xiàn)次數(shù),因此需要做進(jìn)一步的轉(zhuǎn)換。若出現(xiàn)次數(shù)大于0次,表明該單詞出現(xiàn)在短信中,需要做一定的操作。

convert_counts <- function(x) {

    x <- ifelse(x > 0, "Yes", "No")
}

sms_train <- sms_dtm_train %>% 
    apply(MARGIN = 2, convert_counts)

sms_test <- sms_dtm_test %>% 
    apply(MARGIN = 2, convert_counts)

通過(guò)上述的方法,得到一個(gè)跟dtm矩陣同樣大小的矩陣(因?yàn)槭褂昧薬pply,sms_train是普通的矩陣),且各元素是字符“Yes”或者“No”。

下面調(diào)用e1071中的naive bayes函數(shù),對(duì)訓(xùn)練數(shù)據(jù)進(jìn)行模型訓(xùn)練,得到模型nb_model_0,并在測(cè)試集上使用該模型預(yù)測(cè)。(預(yù)測(cè)可能需要數(shù)秒鐘的時(shí)間,后續(xù)將會(huì)介紹更快的glmnet方法對(duì)垃圾短信的識(shí)別,到時(shí)訓(xùn)練和預(yù)測(cè)都能比較快速的完成)

nb_model_0 <- naiveBayes(sms_train, sms_raw_train$type)
pred_0 <- predict(nb_model_0, sms_test)

為了看預(yù)測(cè)結(jié)果的精度,構(gòu)建confusion matrix

CrossTable(
    sms_raw_test$type, pred_0, 
    prop.t = FALSE, prop.chisq = FALSE,
    dnn = c("actual", "pred")
)

   Cell Contents
|-------------------------|
|                       N |
|           N / Row Total |
|           N / Col Total |
|-------------------------|

Total Observations in Table:  1392 

             | pred 
      actual |       ham |      spam | Row Total | 
-------------|-----------|-----------|-----------|
         ham |      1202 |         4 |      1206 | 
             |     0.997 |     0.003 |     0.866 | 
             |     0.981 |     0.024 |           | 
-------------|-----------|-----------|-----------|
        spam |        23 |       163 |       186 | 
             |     0.124 |     0.876 |     0.134 | 
             |     0.019 |     0.976 |           | 
-------------|-----------|-----------|-----------|
Column Total |      1225 |       167 |      1392 | 
             |     0.880 |     0.120 |           | 
-------------|-----------|-----------|-----------|

使用naive bayes預(yù)測(cè)垃圾短信,有(1202+163)/1392 = 98.06%的短信被正確分類(lèi),spam郵件的召回率為87.6%,準(zhǔn)確率為97.6%,這就是本文最開(kāi)始提到的結(jié)果。沒(méi)有使用特別復(fù)雜的特征提取方法,僅僅通過(guò)naive bayes,就達(dá)到98%的準(zhǔn)確分類(lèi),效果良好。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容