naive bayes识别垃圾短信

作者: leengsmile | 来源:发表于2017-03-26 01:25 被阅读1509次

    本文对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%的准确分类,效果良好。

    相关文章

      网友评论

        本文标题:naive bayes识别垃圾短信

        本文链接:https://www.haomeiwen.com/subject/huuhottx.html