vlambda博客
学习文章列表

朴素贝叶斯概述与文本分类

        书上对朴素贝叶斯的解释有点绕,我结合自己的理解用案例做一点解释。

        朴素贝叶斯分类器是一类应用贝叶斯定理分类器的总称。朴素贝叶斯分类器以贝叶斯定理为基础, 朴素指的是假设各个特征之间相互独立,互不影响贝叶斯定理基于条件概率,即:在事件B发生的前提下,事件A发生的概率。由此可知,贝叶斯思想是从先验概率计算出后验概率。

        因为各个特征之间相互独立,所以:

                    P(X,Y) = P(X)·P(Y)

        条件概率公式为:

                    P(Y|X) = P(X,Y)/P(X)

                    P(X|Y) = P(X,Y)/P(Y)

        条件公式联立代换得:

                    P(Y|X) = P(X|Y)P(Y) / P(X)

        这就是贝叶斯定理。

        那么用于监督分类时,令X代表一个n维的向量,表示n个特征;C表示标签。分类的目的就是找到X和C之间的对应关系,从而计算出P(C|X),即当预测对象具备X的特征时,分类为C的概率。公式如下:


举个例子:

朴素贝叶斯概述与文本分类

        此处的C就是["男", "女"]标签;X指的就是["驾龄", "平均车速"]。我们现在有一个预测对象"驾龄=2","平均车速=80"。我们想预测一下驾驶员的性别。使用的还是这个公式:

朴素贝叶斯概述与文本分类

朴素贝叶斯概述与文本分类

        所谓由先验概率计算出后验概率,就是指我们先对答案进行假设,最后挑选出概率大的。譬如该案例,先假设性别为男,在计算性别为男的前提下,每一个特征的概率(此处仅有两个特征);再假设性别为女,同样计算。最后比较两种答案的可能性,性别为男大于性别为女,所以分类器输出概率大的。

        我们注意到计算过程中只用到了公式右边的分子,没有计算分母的原因是我们最后是要比较两个概率的大小,而此处的P(X)是不变的,所以计算省略。

        由于条件独立性这个强假设的存在,朴素贝叶斯分类器十分简单。但是,它仍然有非常不错的效果。原因何在呢?人们在使用分类器之前,首先做的第一步(也是最重要的一步)往往是特征选择(feature selection),这个过程的目的就是为了排除特征之间的共线性、选择相对较为独立的特征。其次,当我们假设特征之间相互独立时,这事实上就暗含了正则化的过程;而不考虑变量之间的相关性有效的降低了朴素贝叶斯的分类方差。虽然这有可能提高分类的偏差,但是如果这样的偏差不改变样本的排列顺序,那么它对分类的结果影响不大。



Python进行文本分类

        要从文本中获取特征,需要先拆分文本。这里说的特征是来自于文中的词条,一个词条是字符的任意组合。可以把词条想象为单词,也可以是非单词的URL、IP等)。然后将每一个文本片段表示为一个词条向量,其中值为1表示词条出现在文档中,0表示未出现。

        以留言板为例,我们需要屏蔽侮辱性言论,所以要构建一个快速过滤器,如果某条留言出现了负面或者侮辱性言论,那么将该留言标记为内容不当。对此问题建立两个类别:侮辱类和非侮辱类,使用1和0表示。

        接下来,我们首先将文本转换为数字向量,然后介绍如何基于这些向量来计算条件概率。


准备数据:从文本中构建词向量

         我们要将句子转换为向量,然后考虑出现在所有文档中的所有单词,并决定将哪些单词纳入词汇表(词汇集合),然后必需将每一篇文档转换为词汇表上的向量。

创建bayes.py并添加下面程序:

def loadDataSet(): postingList = [["my", "dog", "has" ,"flea", "problems", "help", "please"], \ ["maybe", "not", "take", "him", "to", "dog", "park", "stupid"], \ ["my", "dalmation", "is", "so", "cute", "I", "love", "him"], \ ["stop", "posting", "stupid", "worthless", "garbage"], \ ["mr", "licks", "ate", "my", "steak", "how", "to", "stop", "him"], \ ["quit", "buying", "worthless", "dog", "food", "stupid"]] classVec = [0, 1, 0, 1, 0, 1] return postingList,classVec

def createVocabList(dataSet): vocabSet = set([]) # 创建一个空集 for document in dataSet: vocabSet = vocabSet | set(document) # 创建两个集合的并集 return list(vocabSet)

def setOfWords2Vec(vocabList, inputSet): returnVec = [0]*len(vocabList) # 创建一个长度为len(vocabList)并且元素均为0的列表作为向量 for word in inputSet: # 检索输入文本的每一个单词 if word in vocabList: # 如果单词出现在词汇表中 returnVec[vocabList.index(word)] = 1 # 将向量对应位置的元素改为1 else:print("the word: %d is not in my Vocabulary!" % word) return returnVec

        注意,贝叶斯分类器通常有两种实现方式:一个是伯努利模型,一个是多项式模型。区别就在于一个是考虑是否出现,一个是考虑出现的次数。这里是基于伯努利模型。

        上面我们通过函数loadDataSet()创建了数据集,该数据集来自于斑点狗爱好者。函数返回两个变量,一个是数据,一个是标签。标签是人工标注的,由0和1组成,表示非侮辱性和侮辱性。createVocbList(dataSet)函数首先创建了一个空集,接着遍历输入数据的每一个元素,将该元素与集合做交集,所以最终的列表是没有重复的。setOfWords2Vec(vocabList, inputSet)函数则用于判断输入是否含有侮辱性词汇。首先创建一个与输入等长的向量,向量均由0组成。接着判断输入中的每一个单词,如果该单词在词汇表中出现,就修改该词汇对应向量位置的元素。


测试一下:

import bayeslistOPosts, listClasses = bayes.loadDataSet()myVocabList = bayes.createVocabList(listOPosts)print(myVocabList)>>>['help', 'dalmation', 'take', 'maybe', 'I', 'problems', 'posting', 'steak', 'flea', 'quit', 'has', 'dog', 'him', 'mr', 'worthless', 'garbage', 'please', 'park', 'my', 'is', 'love', 'cute', 'not', 'buying', 'ate', 'stop', 'stupid', 'licks', 'how', 'so', 'to', 'food']
Process finished with exit code 0

可以看到结果中没有重复元素。


接着测试setOfWords2Vec()的运行效果,我们使用前面的结果作为词汇表,查询输入数据是否包含关键词

import bayeslistOPosts, listClasses = bayes.loadDataSet()myVocabList = bayes.createVocabList(listOPosts)print(bayes.setOfWords2Vec(myVocabList, listOPosts[0]))>>>[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0]
Process finished with exit code 0


训练算法,从词向量计算概率

        前面讲了如何将一组单词转换成一组数字,那么如何使用这些数字计算概率?现在我们知道一个词是否出现在一篇文档中,也知道该文档所属的类别,接着通过贝叶斯定理计算概率。

        将贝叶斯定理的X替换为w,表示一个向量。

        首先用类别i的个数除以文档总数计算p(Ci)。之后计算p(w|Ci),这里就要用到朴素贝叶斯假设,如果将w展开为数个独立特征,则p(w|Ci)=p(w0|Ci)p(w1|Ci)p(w2|Ci)...p(wn|Ci),极大地简化了计算过程。


核心代码:

def trainNB0(trainMatrix, trainCategory): numTrainDocs = len(trainMatrix) # 读取训练集 numWords = len(trainMatrix[0]) # 数据集实例的长度 pAbusive = sum(trainCategory)/float(numTrainDocs) # 此处计算的是p(i=1),先sum(trainCategory)计算了类别为”1“的个数,再除以数据集的大小 p0Num = zeros(numWords); p1Num =zeros(numWords) p0Denom = 0.0; p1Denom = 0.0 for i in range(numTrainDocs): # 对于数据集中的每一个实例 if trainCategory[i] == 1: # 如果标签是”1“ p1Num += trainMatrix[i] # 修改向量为”1“ p1Denom += sum(trainMatrix[i]) # 向量相加 else: p0Num += trainMatrix[i] p0Denom += sum(trainMatrix[i]) # 循环结束后,p1Num是一个存储了”1“出现次数的向量,向量每个值都是次数 # p1Denom将每一个标签为”1“的向量相加 p1Vect = p1Num / p1Denom # 对每个元素做除法 p0Vect = p0Num / p0Denom
return p0Vect, p1Vect, pAbusive


测试一下:

import bayeslistOPosts, listClasses = bayes.loadDataSet() # 加载测试数据集和标签列表myVocabList = bayes.createVocabList(listOPosts) # 整理词汇表print(myVocabList)trainMat = []for i in listOPosts:    trainMat.append(bayes.setOfWords2Vec(myVocabList, i))   # 将每个实例数据转化成向量p0v, p1v, pAb = bayes.trainNB0(trainMat, listClasses)print(p0v)print(p1v)print(pAb)
>>>['my', 'please', 'ate', 'help', 'park', 'cute', 'quit', 'food', 'posting', 'problems', 'I', 'worthless', 'has', 'garbage', 'buying', 'so', 'stop', 'stupid', 'him', 'steak', 'is', 'mr', 'how', 'take', 'maybe', 'dog', 'love', 'flea', 'not', 'licks', 'dalmation', 'to']32[0.125 0.04166667 0.04166667 0.04166667 0. 0.041666670. 0. 0. 0.04166667 0.04166667 0.0.04166667 0. 0. 0.04166667 0.04166667 0.0.08333333 0.04166667 0.04166667 0.04166667 0.04166667 0.0. 0.04166667 0.04166667 0.04166667 0. 0.041666670.04166667 0.04166667][0. 0. 0. 0. 0.05263158 0.0.05263158 0.05263158 0.05263158 0. 0. 0.105263160. 0.05263158 0.05263158 0. 0.05263158 0.157894740.05263158 0. 0. 0. 0. 0.052631580.05263158 0.10526316 0. 0. 0.05263158 0.0. 0.05263158]ratio 1:0.5
Process finished with exit code 0

       首先标签为侮辱的概率为0.5,正确。词汇表第二个单词是"please",在侮辱类未出现,概率是0;在非侮辱类出现1次,概率是0.04166667。下面我们找出概率最大的是0.15789474,下标为18,对应”stupid“。


测试算法:根据现实情况修改分类器

        贝叶斯对文档进行分类时,要计算多个概率的乘积,如果有一个概率值为0,那最后的乘积也是0。为了降低这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。

        修改bayes.py文件trainNB0()的第4行,第5行。

p0Num = ones(numWords); p1Num =ones(numWords)p0Denom = 2.0; p1Denom = 2.0

        另一个问题是下溢出,这是由于太多很小的数相乘造成的。当相乘的一系列因子都很小时,程序得不到正确答案(都会得到0)。一种解决办法是对乘积取自然对数,修改return前的代码:

p1Vect = log(p1Num/p1Denom) p0Vect = log(p0Num/p0Denom)


至此,我们已经准备好构建完整的分类器了。

def classifyNB(vec2Classify, p0vec, p1Vec, pClass1): p1 = sum(vec2Classify * p1Vec) + log(pClass1) # 元素相乘 p0 = sum(vec2Classify * p0vec) + log(1 - pClass1) if p1 > p0: return 1 else: return 0

def testingNB(): listOPosts, listClasses = loadDataSet() myVocabList = createVocabList(listOPosts) trainMat = [] for postinDoc in listOPosts: trainMat.append(setOfWords2Vec(myVocabList, postinDoc)) p0V, p1V, pAb = trainNB0(array(trainMat), array(listClasses)) testEntry = ["love", "my", "dalmation"] thisDoc = array(setOfWords2Vec(myVocabList, testEntry)) print(testEntry, "classified as: ", classifyNB(thisDoc, p0V, p1V, pAb)) testEntry = ["stupid", "garbage"] thisDoc = array(setOfWords2Vec(myVocabList, testEntry)) print(testEntry, "classified as: ", classifyNB(thisDoc, p0V, p1V, pAb))


测试代码:

import bayesbayes.testingNB()>>>['love', 'my', 'dalmation'] classified as: 0['stupid', 'garbage'] classified as: 1
Process finished with exit code 0

        对testingNB()这个测试器内的文本进行修改,会得到不同文本的判定结果,这个例子比较简单,但是展示了朴素贝叶斯分类器的工作原理。


词袋模型

        上面我们建立词汇表时,采用的是去冗余集合,那么”一个词在文中是否出现“与“一个词在文中出现多次”代表的含义是不同的。这也是伯努利模型和多项式模型的区别。如果一个单词在文中出现多次,那么可能传达了某种信息,而这种信息是"这个词在文中是否出现"所无法传达的,这种方法是词袋模型。我们将上面的词集模型稍加修改:

def bagOfWordsVecMN(vocabList, inputSet): returnVec = [0] * len(vocabList) for word in inputSet: if word in vocabList: returnVec[vocabList.index(word)] += 1 return returnVec

至此,完整的朴素贝叶斯分类就构建完成,第一次码这么多字。下面我们可以用这个分类器过滤垃圾邮件。