机器学习实践:垃圾邮件分类
这个案例是通过训练一个朴素贝叶斯分类器,对垃圾邮件进行判别,使用R语言进行操作。朴素贝叶斯算法是一种简单高效的分类算法,利用贝叶斯定理,通过简单的概率计算对样本进行分类。算法的基本假设是各个事件之间相互独立,假如A与B相互独立,则有:P(AB) = P(A)*P(B)。当有多个事件时,这些事件同时发生的概率就等于这些事件独立发生的概率的连乘积。具体到这个案例,假如一封邮件中出现 one 这个单词的概率为0.0001,出现 two 的概率为0.002,则one 与 two 同时出现的概率为0.0001*0.002
导入包
主要使用文本挖掘的tm 包
library(tm)
对文件路径进行处理
spam.path = "E:/R/ML/data/classify/spam/" spam_2.path = "E:/R/ML/data/classify/spam_2/" easy_ham.path = "E:/R/ML/data/classify/easy_ham/" easy_ham_2.path = "E:/R/ML/data/classify/easy_ham_2/" hard_ham.path = "E:/R/ML/data/classify/hard_ham/" hard_ham_2.path = "E:/R/ML/data/classify/hard_ham_2/" 这里有3类文件,spam是垃圾邮件,easy_ham是易识别的普通邮件,hard_ham是不易识别的正常邮件,本例就是通过训练spam和easy_ham,来识别hardham
先对垃圾邮件进行处理,
获取每个目录下的文档列表,注意事项:”cmds”文件不是邮件
spam.docs = dir(spam.path) spam.docs = spam.docs[which(spam.docs!="cmds")]
然后从每个文档里面取出正文内容
一般来说,正文内容位于邮件的第一个空白行后面
定义一个获取文档内容的函数
get.msg = function(path){ #先打开文件 cons = file(path,open = "rt",encoding = "latin1") text = readLines(cons) #选取正文:从空白行后的第一行到最后一行 if(is.na(which(text == "")[1])){ msg = "" } else{ msg = text[seq((which(text == "")[1]+1),length(text))] } close(cons) #返回一个字符串向量,以换行符号进行拼接 return(paste(msg,collapse = "\n")) }
通过get.msg函数可以从一个邮件里获取正文
将这个函数应用于spam.docs向量中的每一个文档,
就能获得所有的垃圾邮件的正文
all.spam = sapply(spam.docs,function(p) {get.msg(paste(spam.path,p,sep = ""))})
上面这段代码的意思是:将spam.docs中的每个元素p作为参数
传入函数,这个函数的操作是,拼接完整文档的路径,然后获取
正文,最后返回一个向量
完成词项、文档矩阵
get.tdm = function(doc.vec){ #建立一个语料库 corpus 对象 doc.corpus = Corpus(VectorSource(doc.vec)) #建立词项文档矩阵 doc.tdm = TermDocumentMatrix(doc.corpus, control = list(stopwords=T,removePunctuation=T,removeNumbers=T,minDocFreq = 2)) return(doc.tdm) }
建立垃圾邮件的词项文档矩阵
spam.tdm = get.tdm(all.spam) spam.matrix = as.matrix(spam.tdm)
在这个矩阵里,列表示文档,行表示词
按行求和,求出每一个词在所有文档中出现的总次数
spam.counts = rowSums(spam.matrix)
现在我们已经知道了在所有的文档中,每个词出现的文档的数目,数目越多说明这个词被使用得越频繁
构建一个词频表,列出每个词的对应频次
spam.df = data.frame(cbind(names(spam.counts),as.numeric(spam.counts)),stringsAsFactors = F) names(spam.df) = c("term","frequency") spam.df$frequency = as.numeric(spam.df$frequency)
计算某个词出现的文档在所有文档中的比例
spam.occurrence = sapply(1:nrow(spam.matrix), function(p){length(which(spam.matrix[p,]>0))/ncol(spam.matrix)}) ncol求的是矩阵的列数,也就是文档数,which函数计算出现频数大于0的列,表示出现过 spam.density = spam.df$frequency/sum(spam.df$frequency) spam.df = transform(spam.df,density = spam.density,occurrence = spam.occurrence) head(spam.df[with(spam.df,order(-occurrence)),])
接下来用同样的方法处理容易区分的正常邮件(easy_ham)
easy_ham.docs = dir(easy_ham.path) easy_ham.docs = easy_ham.docs[which(easy_ham.docs!="cmds")]
获取正文
all.easy_ham = sapply(easy_ham.docs,function(p){get.msg(paste(easy_ham.path,p,sep = ""))})
建立词项文档矩阵
easy_ham.tdm = get.tdm(all.easy_ham) easy_ham.matrix = as.matrix(easy_ham.tdm) >建立词项词频表 >先计算每个词出现的次数easy_ham.conuts = rowSums(easy_ham.matrix) easy_ham.df = data.frame(cbind(names(easy_ham.conuts),as.numeric(easy_ham.conuts)),stringsAsFactors = F) names(easy_ham.df) = c("term","frequence") easy_ham.df$frequence = as.numeric(easy_ham.df$frequence)
计算出现某个词的文档数占比
easy_ham.occurrence = sapply(1:nrow(easy_ham.matrix), function(i){length(which(easy_ham.matrix[i,]>0))/ncol(easy_ham.matrix)})
计算词频密度
easy_ham.density = easy_ham.df$frequence/sum(easy_ham.df$frequence)
整合数据
easy_ham.df = transform(easy_ham.df,density = easy_ham.density,occurrence = easy_ham.occurrence)
head(easy_ham.df[with(easy_ham.df,order(-occurrence)),])
定义分类器
classify.email = function(path,train.df,prior = 0.5,c = 1e-6){ msg = get.msg(path) msg.tdm = get.tdm(msg) msg.freq = rowSums(as.matrix(msg.tdm)) #prior表示经验概率,或者说是一个权重,用来表示在随机猜测的情况下猜出是垃圾邮件的概率 # c是一个调整值,用来表示不存在的词的概率,这个值可以根据拉普拉斯法进行调整 #找出测试集与训练集相同的词 msg.match = intersect(names(msg.freq),train.df$term) #假如没有交集,则使用很小的概率 if(length(msg.match)
使用不易识别的邮件进行预测
hard_ham.docs = dir(hard_ham.path) hard_ham.docs = hard_ham.docs[which(hard_ham.docs!="cmds")]
使用垃圾邮件作为训练集预测
hard_ham_spamtest = sapply(hard_ham.docs, function(p){ classify.email(path = file.path(hard_ham.path,p),train.df = spam.df) })
使用正常邮件作为训练集
hard_ham_hamtest = sapply(hard_ham.docs, function(p){ classify.email(path = file.path(hard_ham.path,p),train.df = easy_ham.df) })
作出判断
hardham.res = ifelse(hard_ham_spamtest>hard_ham_hamtest,T,F) summary(hardham.res) FALSE TRUE NA's 245 4 0 在所有的不易分辨的正常邮件中,被分类为垃圾邮件的有4个
将上面的操作封装成一个函数,以便一次执行
spam.classifier = function(path){ pr.spam = classify.email(path,spam.df) pr.ham = classify.email(path,easy_ham.df) return(c(pr.spam,pr.ham,ifelse(pr.spam>pr.ham,1,0))) }
使用第二组不易区分的邮件进行预测
hard_ham2_docs = dir(hard_ham_2.path) hard_ham2_docs = hard_ham2_docs[which(hard_ham2_docs!="cmds")] class.hardham2 = lapply(hard_ham2_docs, function(p){ spam.classifier(path = file.path(hard_ham_2.path,p)) }) class.df = as.data.frame(do.call(rbind,class.hardham2)) names(class.df) = c("pr.spam","pr.ham","spam") table(class.df$spam) 0 1 243 5 将5封难以识别的正常邮件误判为垃圾邮件 准确率有点高,可能原因是训练样本中正常邮件的比例比垃圾邮件大,这方面需要进行调整
这个例子简单地构建一个朴素贝叶斯分类器,并没有进行优化。不过从整个过程看,这个案例的难点主要在于数据的整理:按照目录读取文本、提取正文、分词、建立词项文档矩阵等等,可见在真实项目中,数据处理的过程极重要。