naive bayes识别垃圾短信

本文对brett的机器学习与R语言(Machine Learning with R)一书中的垃圾短信识别的笔记。在brett的书中,介绍了如何通过naive bayes对短信进行训练,并预测短信是否为垃圾短信。naive bayes的精度可以达到98.06%,召回率为87.6%,准确率为97.6%。

在kaggle上有一个类似的dataset,叫SMS Spam Collection Dataset,kaggle spam sms。Brett书中的数据是对该数据源的修正,内容上基本类似,这里使用kaggle的数据集。

首先加载需要用到的包

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

首先通过data.table包来读取数据

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

需要注意的是,这里读取的是kaggle上的数据集,同时也在UCI上存放,uci sms。SMSSpamCollection有两列数据,第一列表示短信是否为垃圾短信,spam表示垃圾短信,ham表示正常短信;第二列是具体的短信内容。将SMSSpamCollection的内容读入R语言,并保存在sms_raw变量中,通过setnames修改列名。

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

sms_raw有5574行文本数据。

 sms_raw[, .N]
[1] 5574

可以大致看看sms_raw中垃圾短信与正常短信的条数

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

      ham      spam
0.8659849 0.1340151

其中有86.6%左右的短信是正常短信,有13.4%的短信为垃圾短信。下面分别看一下垃圾短信与正常短信的云图,看看二者在文本内容上是否有显著区别。

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
    )

通过如上的程序,分别得到spam有ham短信的云图,可以从云图上可以得知,垃圾短信中以较大的概率出现free,而正常短信更多的是一些常规描述的词汇。根据出现的单词,可以大致判断出短信是否是垃圾短信。

ham_wordcloud.jpg
spam_wordcloud.jpg

本文采用naive bayes的方法识别垃圾短信,根据naive bayes的条件独立的假设,若确定短信的是垃圾短信与否之后,短信内的单词相互独立,相互不影响。虽然这个假设与事实不符,比如若垃圾短信中出现buy,那么出现on sale的概率会相对而言更大(相对于不知道buy这个单词,此处是条件独立的转述),但是这对结果却没多大影响。

为评估naive bayes的性能,将数据集分成训练集和测试集,75%的数据用于训练,25%的数据用于评估算法的性能。

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, ]

其中设置随机数是为了结果的可重复性,1071是我常用的一个随机数,是R语言中著名的机器学习包e1071的数字部分。createDataPartition则来自于caret包,该函数通过对数据进行抽样,保证训练集与测试集中,垃圾短信的比例一致,避免训练集中出现大量的正常短信,而几乎没有垃圾短信这样的情况。createDataPartition 的第一个参数是vector,函数根据这个参数内容进行抽样,p=0.75表示75%的数据进入训练集,则有25%的数据进入测试集,list=FASLE表示返回结果的格式为常规的数组,否则将返回一个列表,更具体的用法可以参考对应的帮助文档。

下面看看训练集与测试集中spam/ham邮件的分布情况,通过上述的抽样,训练集与测试集中邮件分布一致。

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

为简化后续的文本处理,定义两个辅助函数,corpus生成语料库,clean函数则对语料库进行一些清洗,比如删除数字,stopwords,标点符号,首尾的空白字符等。

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))
}

这里有一个坑,如果使用常规的Corpus函数代替上述的VCorpus,则有可能导致后续的预测出现反常的现象,比如大部分的spam邮件预测为ham。具体原因我没有细究,在调试跟踪tmt包多个函数后,发现若使用Corpus函数,在对训练集的文本构建document-term frequency矩阵时,出现错误的结果。所以解决方法是使用VCorpus,并指定reader,在后续的操作中,使用content_transformer包装所有的处理函数。

通过上述的辅助函数,构建训练集的预料数据

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

可以通过inspect函数查看对应的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

可知原始的文本,已经转换成小写单词,且删除了标点符号。
下一步根据corpus构建文档词频矩阵(document-term frequency)

sms_dtm_train_all <- DocumentTermMatrix(sms_corpus_train)

可以删除出现次数过少的单词,这些单词出现较少,删除这些单词对预测的结果没有(估计)影响。

sms_dict <- findFreqTerms(sms_dtm_train_all, 5)

删除词频少于5的单词,剩下的单词做为后续构建dtm的单词表sms_dict,sms_dict其实是字符串列表,是那些出现频次超过5次的单词。
根据前文构建的单词表,重新构建训练集和测试集的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返回的是一种特殊的矩阵,类似于稀疏矩阵,是继承于slam包的simple_triplet_matrix,不过不需要深入了解底层的结构,把sms_dtm_train当成普通的矩阵看待即可。

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

在naive bayes的算法中,计算的是单词表中,每个单词出现与否的概率,上述产生的dtm矩阵记录的是每条短信中,每个单词的出现次数,因此需要做进一步的转换。若出现次数大于0次,表明该单词出现在短信中,需要做一定的操作。

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)

通过上述的方法,得到一个跟dtm矩阵同样大小的矩阵(因为使用了apply,sms_train是普通的矩阵),且各元素是字符“Yes”或者“No”。

下面调用e1071中的naive bayes函数,对训练数据进行模型训练,得到模型nb_model_0,并在测试集上使用该模型预测。(预测可能需要数秒钟的时间,后续将会介绍更快的glmnet方法对垃圾短信的识别,到时训练和预测都能比较快速的完成)

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

为了看预测结果的精度,构建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预测垃圾短信,有(1202+163)/1392 = 98.06%的短信被正确分类,spam邮件的召回率为87.6%,准确率为97.6%,这就是本文最开始提到的结果。没有使用特别复杂的特征提取方法,仅仅通过naive bayes,就达到98%的准确分类,效果良好。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容