手撕朴素贝叶斯分类器源码(Naive Bayesian)

Posted 小学生和机器学习的故事

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手撕朴素贝叶斯分类器源码(Naive Bayesian)相关的知识,希望对你有一定的参考价值。

鋌~ (最近压力略大,好久没有更新,xixixi) ,今天的主题是 朴素贝叶斯分类器,NB这个缩写真是绝了,确实值得这个缩写,哈哈哈。今天跟大家聊一聊朴素贝叶斯分类器的基本原理和代码编写。贝叶斯分类器的基本原理是使用贝叶斯公式计算各种概率值,然后根据新样本的属性,计算属于某一类的概率,可能略抽象,等下实例讲解,具体是如何操作的。
首先给出一些贝叶斯理论的基本原理介绍。
  • 条件概率

  • 乘法公式

  • 全概率公式

  • 贝叶斯公式

条件概率表示的基本事实是,在给定条件下B下,某件事情A发生的概率。

记作:


如果已知事件B为家中至少有个男孩儿,则再发生A事件的概率则为:


对上面的条件概率分子分母同时除以4, 也就是样本空间的大小,则推导出如下式:

所以可以得到一般性结论,条件概率是两个没有条件的概率商。这里可以通过一张图来理解贝叶斯公式在做什么:

乘法公式简单理解就是将条件概率移项:

全概率公式是计算复杂概率的一个很重要的公式,假设B1, B2,B3...Bn是样本空间的一个划分,他们互不相容,且并集为整个的样本空间,则对于任意的一个事件A来说,则有如下公式:

这里稍加解释,全概率公式是怎么来的,其实可以通过下式推导出来:

又因为条件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函数是单调的递增函数,这就是贝叶斯分类器在数学计算上和代码撰写上几个非常有用的技巧。下面介绍一下如何将邮件抽象为稀疏矩阵。

以垃圾邮件分类器为例,构建这个算法的步骤大致如下:

  1. 将每一封邮件,看做一个特征向量,将每一封邮件当中的出现的词提取出来,如果内容是英文的邮件,可以按照空格来分割文本内容,如果是中文的邮件,则相对复杂一些,需要一些分词的文本库,或者使用一些分词软件,因为中文的习惯就是词与词之间并没有类似英文的空格,这就导致中文的处理略微复杂,另外一个就是类似于语气助词等,并不是关键的词,需要将该类词语去除,一般会提高分类器的预测精度。然后将所有邮件当中出现的词去重,取最大的并集,取并集之后的每个单词则做为最终的特征,或者叫做属性。例如有两封邮件的内容分别是:“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, 这就是训练集的标签。

  2. 根据训练数据和贝叶斯公式,计算条件概率,得到条件概率之后,也就是相当于得到了决策的根据,也可以理解为训练模型。

  3. 根据得到的条件概率,比较大小,如果新邮件属于垃圾邮件的概率大于属于正常邮件的概率,那么我们就认为该邮件属于垃圾邮件,反之为正常邮件。

  4. 使用测试集验证分类器效果。

下面是编写朴素贝叶斯分类器的源代码,首先说明一下数据的形式。数据共50封邮件,来自于机器学习实战当中配套的数据,其中25封为垃圾邮件,标记为spam, 另外25封为正常邮件,标记为ham,  垃圾邮件和正常邮件分别存放在两个不同的文件夹中的不同的文本文件当中。所有这里需要使用一些批量读取文件和正则表达式处理邮件的一些代码技巧。那么我们开始吧!
先预览一封垃圾邮件:

手撕朴素贝叶斯分类器源码(Naive Bayesian)

######################## 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) + 1VocMatrix_add1$Labels <- VocMatrix$Labelsprint(VocMatrix_add1)

手撕朴素贝叶斯分类器源码(Naive Bayesian)

可以得到一个50乘671的稀疏矩阵,每一行表示一封邮件,671列表示每个单词和邮件的标签Labels。然后根据该稀疏矩阵计算我们想要的概率,编写预测函数:
##条件概率##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 %>% filter(Labels == 1) %>% select(-Labels), 2, sum) / p1_sum)p0 <- log(apply(VocMatrix_add1 %>%  filter(Labels == 0) %>% select(-Labels), 2, sum) / p0_sum)pSpam <- mean(VocMatrix$Labels)
这里可以展示一下做过处理之后的垃圾邮件每个单词的条件概率(部分结果):

手撕朴素贝叶斯分类器源码(Naive Bayesian)

然后编写新邮件预测的函数:

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)

朴素贝叶斯分类器Naive Bayes

干货|非常通俗的朴素贝叶斯算法(Naive Bayes)

Naive Bayesian文本分类器

详解线性分类-朴素贝叶斯分类器(Naive Bayes Classifer)白板推导系列笔记

基于Naive Bayes算法的文本分类