手撕朴素贝叶斯分类器源码(Naive Bayesian)
Posted 小学生和机器学习的故事
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手撕朴素贝叶斯分类器源码(Naive Bayesian)相关的知识,希望对你有一定的参考价值。
条件概率
乘法公式
全概率公式
贝叶斯公式
条件概率表示的基本事实是,在给定条件下B下,某件事情A发生的概率。
记作:
如果已知事件B为家中至少有个男孩儿,则再发生A事件的概率则为:
对上面的条件概率分子分母同时除以4, 也就是样本空间的大小,则推导出如下式:
所以可以得到一般性结论,条件概率是两个没有条件的概率商。这里可以通过一张图来理解贝叶斯公式在做什么:
又因为条件B被分割为互补相容的几个部分,所以可以根据:
这里用C和D表示两个新的事件以免混淆, 因为两个事件互不相容,所以同时发生的概率则为0, 又有条件概率公式,则可以得到:
则可以整理该式子变成最简的∑和式。
贝叶斯公式则是在乘法公式和全概率公式上推导出来的著名公式,其实就是对应项的替换而已。假设B1, B1,...Bn是样本空间的一个分割,他们之间互补相容,并集为整个样本空间,则:
则分别将分子分母替换,就可以得到著名的贝叶斯公式。以垃圾邮件分类为例,有垃圾邮件和正常邮件,我们先根据垃圾邮件出现的数目,计算垃圾邮件出现的概率值,假设垃圾邮件出现为事件B1, 则正常邮件出现的为事件B0,事件A为某篇文章出现的条件,我们可以计算出P(B1), P(B0), P(A|B1), P(A|B0), 也就是垃圾邮件的概率,正常邮件的概率, 在垃圾邮件当中,A邮件出现的概率,在正常邮件当中,A邮件出现的概率,以及P(A),A邮件出现的概率。则当我们获得了一封新的邮件A_new, 则可以计算P(B1|A_new)和P(B0|A_new), 也就是新邮件属于垃圾邮件的概率和属于正常邮件的概率,当属于垃圾邮件的概率大于正常邮件的概率,我们则认为邮件是垃圾邮件,反之为正常邮件。这里要说明的一点是,计算P(A|B1)和P(A|B0)时,我们将考虑每个单词出现的概率,而不是整个邮件文档出现的概率,也就是将一封邮件出现的条件概率表示为:
之所以叫做朴素贝叶斯, 就是因为假设不同单词之间是独立的,相互不影响,因为这样公式比较简单,另外一般也能得到一个好的结果。所有最终用于预测新邮件属于垃圾邮件和正常邮件时,我们只需要计算贝叶斯公式的分子即可,我们只需要比较大小,并不关注具体概率值的大小,这里是数学角度的处理方式,当我们撰写贝叶斯公式的代码时,因为矩阵是稀疏矩阵,某个单词的条件概率很可能为0, 最后计算整个邮件出现的概率时,要使用各个单词的条件概率乘积为0, 这时可以强行将每个单词出现的次数出现为0的,计数为1。另外计算机的储存问题,当数字非常小时,会出现下溢出的问题,感兴趣的读者可以自行查阅这个问题,这里我们的处理方式是将小数取ln值,自然对数为底,或者以其他数字为底都可以。因为ln函数是单调的递增函数,这就是贝叶斯分类器在数学计算上和代码撰写上几个非常有用的技巧。下面介绍一下如何将邮件抽象为稀疏矩阵。
以垃圾邮件分类器为例,构建这个算法的步骤大致如下:
将每一封邮件,看做一个特征向量,将每一封邮件当中的出现的词提取出来,如果内容是英文的邮件,可以按照空格来分割文本内容,如果是中文的邮件,则相对复杂一些,需要一些分词的文本库,或者使用一些分词软件,因为中文的习惯就是词与词之间并没有类似英文的空格,这就导致中文的处理略微复杂,另外一个就是类似于语气助词等,并不是关键的词,需要将该类词语去除,一般会提高分类器的预测精度。然后将所有邮件当中出现的词去重,取最大的并集,取并集之后的每个单词则做为最终的特征,或者叫做属性。例如有两封邮件的内容分别是:“my dog has flea probelems help please.”;“maybe not take him to dog park stupid”,则提取两封邮件当中出现的所有单词集合为{my dog has flea probelems help please maybe not take him to park stupid},其中dog这个单词都出现在两封邮件当中了,所以将其去重,则两封邮件则可以按照单词是否出现在单词并集列表中(出现为1,未出现为0),转换为特征向量:
[1,1,1,1,1,1,1,0,0,0,0,0,0,0]
[0,1,0,0,0,0,1,1,1,1,1,1,1,1]
所有邮件当中出现的单词总数为14,所以特征向量长度为14。这就是如何将邮件内容抽象成稀疏矩阵的方法。然后根据邮件的是否为垃圾邮件,将特征向量标记,是垃圾邮件标记为1, 不是则标记为0, 这就是训练集的标签。
根据训练数据和贝叶斯公式,计算条件概率,得到条件概率之后,也就是相当于得到了决策的根据,也可以理解为训练模型。
根据得到的条件概率,比较大小,如果新邮件属于垃圾邮件的概率大于属于正常邮件的概率,那么我们就认为该邮件属于垃圾邮件,反之为正常邮件。
使用测试集验证分类器效果。
######################
# autor : liuxuang
# date : 20200907
# 微信 : LXYweixinID
######################
##设置垃圾邮件和正常邮件的文件存储路径
HamEmail <- "G:/email/ham/"
SpamEmail <- "G:/email/spam/"
##定义处理多个文件,并转换为稀疏矩阵的函数
MutiDocuments2matrix <- function(Email_filePath = "G:/email"){
#加载数据处理的dplyr包和字符串处理的stringr包
#不规则的邮件和常规的数据格式不同,我们使用read_lines函数逐行处理
library(dplyr)
library(stringr)
library(readr)
##设置垃圾邮件和正常邮件的文件路径
HamEmail_path <- sprintf("%s/ham", Email_filePath)
SpamEmail_path <- sprintf("%s/spam", Email_filePath)
#取所有文件的单词并集
VocabListSet <- function(Email_type){
if(Email_type == "ham"){
setwd(HamEmail_path)
}else if(Email_type == "spam"){
setwd(SpamEmail_path)
}
VocabList_set <- NULL
VocabList_List <- list()
Res <- list()
#list.files()函数是获取文件路径下所有文件名称
for(email in list.files()){
temp_data <- read_lines(email, skip_empty_rows = T)
#这里的正则表达式是提取所有有英文字母组成的字符串,也就是英文单词
vocabList <- str_extract_all(temp_data, "[A-Za-z]+", simplify = T)
vocabList <- vocabList[str_length(vocabList) > 2]
VocabList_List[[email]] <- vocabList
VocabList_set <- c(VocabList_set, vocabList)
}
#将单词列表每封邮件分别存储到Res里面的两个部分
#R语言不像python可以返回多个对象,必须将其存入到一个对象中
#这里使用list结构存储单词列表等内容
Res[["VocabList_set"]] <- unique(VocabList_set)
Res[["VocabList_List"]] <- VocabList_List
return(Res)
}
ham_Res <- VocabListSet("ham")
spam_Res <- VocabListSet("spam")
VOCBLIST <- union(ham_Res$VocabList_set, spam_Res$VocabList_set)
##VOCBLIST是所有文件中的单词并集
##编写将文件处理成向量的函数,其中只计数1次,初始化计数为0,
#防止计算概率乘积时概率为0
doc2Vector <- function(filename, VOCBLIST, doc_type){
VoccabVector <- rep(0, length(VOCBLIST))
if(doc_type == "ham"){
for(word in ham_Res$VocabList_List[[filename]]){
if(word %in% ham_Res$VocabList_set){
VoccabVector[which(word == ham_Res$VocabList_set)] = 1
}
}
}else{
for(word in ham_Res$VocabList_List[[filename]]){
if(word %in% spam_Res$VocabList_set){
VoccabVector[which(word == spam_Res$VocabList_set)] = 1
}
}
}
return(VoccabVector)
}
##将正常邮件的稀疏矩阵存储到VocMatrix_ham
VocMatrix_ham <- NULL
for(file in list.files(HamEmail_path)){
VocMatrix_ham <- rbind(VocMatrix_ham, doc2Vector(file, VOCBLIST, "ham"))
}
VocMatrix_ham <- as.data.frame(VocMatrix_ham)
##将正常邮件的稀疏矩阵存储到VocMatrix_spam
VocMatrix_spam <- NULL
for(file in list.files(SpamEmail_path)){
VocMatrix_spam <- rbind(VocMatrix_spam, doc2Vector(file, VOCBLIST, "spam"))
}
VocMatrix_spam <- as.data.frame(VocMatrix_spam)
#将垃圾邮件和正常邮件合并,并添加训练的标签,垃圾邮件为1,正常邮件为0
VocMatrix <- as_tibble(rbind(VocMatrix_spam, VocMatrix_ham))
names(VocMatrix) <- VOCBLIST
VocMatrix <- VocMatrix %>% mutate(Labels = c(rep(1, 25), rep(0, 25)))
return(VocMatrix)
}
#将训练标签labels添加到矩阵当中,返回该稀疏矩阵,用于计算条件概
VocMatrix <- MutiDocuments2matrix()
VocMatrix_add1 <- VocMatrix %>% select(-Labels) + 1
$Labels Labels <- VocMatrix
print(VocMatrix_add1)
##条件概率
##fliter函数用以区分垃圾邮件和正常邮件,也就是控制条件,利用sum计数
##mean函数是一个编程技巧,如果原向量只有0和1,那么求均值就是1的比率
##其中p1和p0表示每个单词在特定条件下的条件概率
##pSam表示垃圾邮件的概率,分母就不用求了,因为分母相同,比分子大小就可以
##这里是将上面的条件概率公式转换成代码语言,需要理解,多看几遍。
p1_sum <- sum(VocMatrix_add1 %>% filter(Labels == 1))
p0_sum <- sum(VocMatrix_add1 %>% filter(Labels == 0))
p1 <- log(apply(VocMatrix_add1 %>%
= 1) %>% =
2, sum) / p1_sum)
p0 <- log(apply(VocMatrix_add1 %>%
= 0) %>% =
2, sum) / p0_sum)
pSpam <- mean(VocMatrix$Labels)
然后编写新邮件预测的函数:
NB_Predict <- function(documnes_path, VOCBLIST, p1_mum, p0_mum, pSpam){
##读取数据,转换为向量
temp_data <- read_lines(documnes_path, skip_empty_rows = T)
vocabList <- str_extract_all(temp_data, "[A-Za-z]+", simplify = T)
vocabList <- vocabList[str_length(vocabList) > 2]
##初始化为0,可以作为条件进行选择,当没出现该词后,数值为0,和p1或者p0相乘后
##起到筛选的作用
VocabVector <- rep(0, length(VOCBLIST))
for(word in vocabList){
if(word %in% VOCBLIST){
VocabVector[which(word == VOCBLIST)] = 1
}
}
##因为之前的函数已经取了log,所以log(AB) = log(A) + log(B)
##输出概率值最大标签就是预测值
p1_predict <- sum(VocabVector * p1_mum) + log(pSpam)
p0_predict <- sum(doc_vector * p0_mum) + log(1 - pSpam)
if(p1_predict > p0_predict){
print("Spam")
}else{
print("ham")
}
}
test1_file <- "G:/email/test1.txt"
tees2_file <- "G:/email/test2.txt"
NB_Predict(test1_file, VOCBLIST, p1, p0, pSpam)
#[1] : "Spam"
NB_Predict(test2_file, VOCBLIST, p1, p0, pSpam)
#[1] : "ham"
以上是关于手撕朴素贝叶斯分类器源码(Naive Bayesian)的主要内容,如果未能解决你的问题,请参考以下文章
R构建朴素贝叶斯分类器(Naive Bayes Classifier)