机器学习之朴素贝叶斯详细介绍及实例应用

基于概率论的分类方法:朴素贝叶斯

朴素贝叶斯算法:朴素贝叶斯算法是有监督的学习算法,同样是解决分类的问题,之所以称之为朴素,也就是因为整个其整个形式化过程只做最原始,最简单的假设。

优点:在数据较少的情况下仍然有效,可以处理多类别的问题。
缺点:对于输入数据的准备方式较为敏感。
适用数据类型:标称型数据。

朴素贝叶斯是贝叶斯决策理论的一部分,所以讲朴素贝叶斯之前有必要了解贝叶斯决策理论。

贝叶斯决策理论
我们可以看一个数据集,它由两类数据组成,它们的数据分布图如图:
我们现在用p1(x,y)表示数据点(x,y)属于类别1(即图中用圆点表示的类别)的概率,用p2(x,y)表示数据点(x,y)表示数据点(x,y)属于类别2(即图中用三角形表示的类别)的概率,那么对于一个新的数据点(x,y),可以这样判断:

如果p1(x,y) > p2(x,y),那么类别为1
如果p1(x,y) < p2(x,y),那么类别为2

也就表示我们会选择高概率对应的类别,这就是贝叶斯决策理论的核心思想,即具有最高概率的决策,所以我们该学习的就是计算p1和p2的方法。

条件概率
计算p1和p2,我们先了解条件概率。相信学过概率论的伙伴肯定很熟悉,它就是指事件B发生的情况下,事件A发生的概率,我们用P(A|B)来表示。它的计算公式就是 P ( A ∣ B ) = P ( A ∩ B ) P ( B ) P(A|B)=\frac{P(A\cap B)}{P(B)} P(AB)=P(B)P(AB)
所以 P ( A ∩ B ) = P ( A ∣ B ) ∗ P ( B ) P(A\cap B)=P(A|B)*P(B) P(AB)=P(AB)P(B)
同理我们可以得出 P ( A ∩ B ) = P ( B ∣ A ) ∗ P ( A ) P(A\cap B)=P(B|A)*P(A) P(AB)=P(BA)P(A)
所以 p ( A ∣ B ) = P ( B ∣ A ) P ( A ) P ( B ) p(A|B)=\frac{P(B|A)P(A)}{P(B)} p(AB)=P(B)P(BA)P(A)
这样我们简单了解了条件概率的计算公式,我们可以举个例子来练练手,
在这里插入图片描述
一共有七个球,3个灰色,4个黑色,我们随机抽取一个球,为灰色的概率是3/7,为黑色的概率是4/7,这个简单。我们都知道,如果这些球放入两个桶中,如图在这里插入图片描述
如果我们要计算从B桶中抽到灰色球的概率,这就是属于条件概率了,我们可以记为P(gray|B),字面意思就是已知球出自B桶,取出灰色的球的概率,我们看图可以直接得出结果是1/3,我们用公式来计算,球出自B桶的概率P(B)=3/7,球出自B桶且是灰色的概率P(gray and B) = 1/7,所以相除结果正是为1/3,当然想更深入的学习的话可以自行搜索学习。

使用条件概率分类
我们知道要用p1和p2来进行计算比较,但是使用这两者只是为了尽可能简化描述,我们真正需要计算和比较的是P(c1|x,y)和P(c2|x,y),这些符号的具体意义是给定某个点(x,y),判断该点来自类别ci的概率是多少。所以我们就可以用条件概率来表示 P ( C i ∣ x , y ) = P ( x , y ∣ C i ) P ( C i ) P ( x , y ) P(C_i|x,y)=\frac{P(x,y|C_i)P(C_i)}{P(x,y)} P(Cix,y)=P(x,y)P(x,yCi)P(Ci)
所以我们可以通过其他三个已知的概率值来计算未知的概率值。

使用朴素贝叶斯进行文档分类
机器学习的一个重要的应用就是文档的自动分类。我们可以观察文档中出现的词,并把每个词的出现和不出现作为一个特征,这样得到的特征数目就跟词汇表中的词目一样多。朴素贝叶斯是贝叶斯分类器的一个扩展,是用于文档分类的常用算法。

朴素贝叶斯的一般过程:

1.收集数据:可以使任何方法。文章使用RSS源。(RSS源是一种描述和同步网站内容的格式)
2.准备数据:需要数值型或者布尔型数据 。
3.分析数据:有大量的特征时,绘制特征作用不大,此时用直方图效果更好。
4.训练算法:计算不同的独立特征的条件概率。
5.测试算法:计算错误率。
6.使用算法:一个常见的朴素贝叶斯应用是文档分类,可以在任意的分类场景中使用朴素贝叶斯分类器,不一定非要是文本。

以社区留言为例。为了不影响社区的发展,我们要屏蔽侮辱性的言论,所以要构建一个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么就讲该留言标识为内容不当,过滤这类内容是很常见的需求,对此问题,我们可以建立两个类别:侮辱类和非侮辱类,我们也分别用数字1和0表示。

准备数据:从文本中构建向量:
我们将文本看成单词向量或者是词条向量(一个词条是字符的任意组合,可以把词条想象为单词,也可以使用非单词的词条,如url,ip地址,或者其他任意的字符串),也就是说将句子转为向量。考虑出现在所有文档中的单词,再决定将哪些词纳入词汇表或者说词汇集合,然后将每一篇文档转换为词汇表上的向量。

我们可以编写代码:

from numpy import *
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
if __name__=='__main__':
    listOPosts,listClasses = loadDataSet()
    print(listOPosts)
    print(listClasses)

在这里插入图片描述
我们从结果可以看出listOPosts中存放了词条的列表,listClasses存放了每个词条所属的类别,1表示的是侮辱类,而0则表示非侮辱类。

接着我们需要将创建词汇表,并且将词条转为词条向量(至于词条的切分方法后面会介绍):

from numpy import *
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)
        #取并集(document用set保证词条里单词不重复,取并集保证两者结合也没有重复的单词)
    return list(vocabSet)
#以列表的形式返回词条列表

def setOfWords2Vec(vocabList,inputSet):
    #根据词汇表,对实验样本(inputSet)向量化
    returnVec = [0]*len(vocabList)
    #创建一个与词汇表同等大小的零向量
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1
        #如果单词出现在词汇表中,则在词汇表中搜索其位置
        #同时置为1
        else:
            print("the word :%s is not in mu vocabulary!"%word)
    return returnVec
    #返回文档向量
if __name__=='__main__':
	listOPosts,listClasses = loadDataSet()
    myVocabList = createVocabList(listOPosts)
    print(myVocabList)
    # #print(setOfWords2Vec(myVocabList,listOPosts[0]))
    trainMat = []
    for postinDoc in listOPosts:#遍历每一个词条
        trainMat.append(setOfWords2Vec(myVocabList,postinDoc))
        #将转换的词条向量加入到列表中
    print(trainMat)

在这里插入图片描述
由此我们就得到了不重复的词汇表和词条向量。之后我们就要构造朴素贝叶斯分类器训练函数。

我们将一组单词转为一组数字,我们就利用它们来计算概率,我们知晓一个词是否出现在一篇文档中,也知道该文档所属的类别,我们可以重写公式,我们用W替换x,y,W是一个向量,它由多个值一同组成的,在此例子中,数值的个数与词汇表中的单词个数是相同的。 P ( C i ∣ W ) = P ( W ∣ C i ) P ( C i ) P ( W ) P(C_i|W)=\frac{P(W|C_i)P(C_i)}{P(W)} P(CiW)=P(W)P(WCi)P(Ci)
这个公式我们也很好理解的,P(Ci)就是类别概率,我们只需要用类别的文档数(即属于侮辱性留言的文档数或者非侮辱性留言的文档数)除以总的文档数就可以得到。而计算P(W|Ci),这里要用到朴素贝叶斯假设,如果将W展开为一个个独立特征,那么就可以将上述写成P(W0,W1,W2,…Wn|Ci),这里假设所有的词都互相独立,这种假设也称为条件独立性假设,这种假设虽然有问题,但实际效果却很好,它意味着可以使用P(W0|Ci)P(W1|Ci)…P(Wn|Ci)来计算上述概率,而这也称为朴素贝叶斯推断,后续代码提到我们会继续讲解,所以我们编写代码:

from numpy import *
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)
        #取并集(document用set保证词条里单词不重复,取并集保证两者结合也没有重复的单词)
    return list(vocabSet)
#以列表的形式返回词条列表

def setOfWords2Vec(vocabList,inputSet):
    #根据词汇表,对实验样本(inputSet)向量化
    returnVec = [0]*len(vocabList)
    #创建一个与词汇表同等大小的零向量
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1
        #如果单词出现在词汇表中,则在词汇表中搜索其位置
        #同时置为1
        else:
            print("the word :%s is not in mu vocabulary!"%word)
    return returnVec
    #返回文档向量
def trainNBO(trainMatrix,trainCategory):
    #朴素贝叶斯分类器训练函数
    #参数为文档矩阵(即setOfWords2Vec返回的矩阵向量)和类标签向量
    numTrainDocs = len(trainMatrix)
    #统计训练文档的数目
    numWords = len(trainMatrix[0])
    #统计每个文档的词条数
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    #sum没有参数表示全部相加,实质上就是统计为1的数量(即侮辱性文档的数量)
    #除以文档总数,算出概率(即P(ci))
    p0Num = zeros(numWords)
    # #创建一维零数组,长度与每个文档的词条数相同
    p1Num = zeros(numWords)
    # #创建一维零数组,长度与每个文档的词条数相同
    p0Denom = 0.0
    p1Denom = 0.0
    #分母初始化为0
    for i in range(numTrainDocs):
        #遍历统计数据(即P(W0|C1),P(W1|C1)...)
        if trainCategory[i] == 1:
            #如果类别为侮辱性的
            p1Num += trainMatrix[i]
            #对应的单词的数加一
            p1Denom += sum(trainMatrix[i])
            #文档中出现的单词数全部统计相加
        else:#类别是非侮辱性的,遍历统计数据(即P(W0|C2),P(W1|C2)...)
            p0Num +=trainMatrix[i]
            #对应的单词数加一
            p0Denom += sum(trainMatrix[i])
            # 文档中出现的单词数全部统计相加
    p1Vect = p1Num/p1Denom
    p0Vect = p0Num/p0Denom
    #计算数据,即单词在侮辱性和非侮辱性文档中的出现的概率(p(w|ci))
    return p0Vect,p1Vect,pAbusive
    #返回属于非侮辱性文档的条件概率,属于侮辱性文档的条件概率和文档属于侮辱类的概率

if __name__=='__main__':
	listOPosts,listClasses = loadDataSet()
    myVocabList = createVocabList(listOPosts)
    print(myVocabList)
    trainMat = []
    for postinDoc in listOPosts:#遍历每一个词条
        trainMat.append(setOfWords2Vec(myVocabList,postinDoc))
        #将转换的词条向量加入到列表中
    p0V,p1V,pAb = trainNBO(trainMat,listClasses)
    print(pAb)
    print(p0V)
    print(p1V)

在这里插入图片描述
这里我们来分析一下,从训练集的分类标签中我们可以看出侮辱性留言和非侮辱性留言的数量各占一半,所以pAb为0.5是正确的,我们再来分析,我们看出词汇表第六个单词是stupid,这很明显是一个侮辱性词汇,所以它在p0V中的概率为0,在p1V中的概率达到最大,为0.15789474,也意味着这个词是最能代表特征1的单词。

但是上述代码其实不是很完善,我们会发现,利用贝叶斯分类器进行对文档的分类的时候,要计算多个概率相乘的结果,我们之前的朴素贝叶斯推断已经提到了,我们上面的运行结果显示有些概率为0,那么相乘后结果也为0了,这并不是我们所想要的结果,所以为了降低这种影响,可以将所有词的出现初始化为1,并将分母初始化为2,即如下的改动:p0Num = ones(numWords);p1Num = ones(numWords);p0Denom = 2.0;p1Denom = 2.0我搜索了相关资料,这其实就是拉普拉斯平滑的体现,即假定训练样本很大时,每个分量x的计数加一造成的估计概率变化可以忽略不计,但可以方便有效的避免零概率的问题。

假设在文本分类中有3个类,C1,C2,C3,在指定的训练样本中,某个词语的K1,各类中观测计数分别为0,990,10,则K1的概率为0,0.99,0.01,若对这三个量使用拉普拉斯平滑,1/1003=0.001,991/1003=0.998,11/1003=0.011。在实际中经常使用加lambda(1>=lambda>=0)来代替加1,如果对N个计数都加上lambda,则这时分母也要加上N*lambda。而这也是为什么前者初始化为1,后者初始化为2了。当然具体可以看这篇博客博客地址

还有一个问题就是下溢出问题了,这是由于太多很小的数相乘造成的。当计算乘积时,由于大部分因子都非常小,所以程序会下溢出或者得不到正确答案(即指定位置四舍五入出现0)。我们便采用对乘积取自然对数的方法。代数中有 l n ( a ∗ b ) = l n ( a ) + l n ( b ) ln(a*b)=ln(a)+ln(b) ln(ab)=ln(a)+ln(b),于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时采用自然对数进行处理不会有任何损失。我们可以观察它们的曲线,在这里插入图片描述
它们在相同的区域内同时递增或者递减,并且在相同点取到极值。它们取值不同,但不影响最终结果。所以我们可以修改代码: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.0-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 = trainNBO(array(trainMat),array(listClasses))
    #训练朴素贝叶斯分类器
    testEntry = ['love','my','dalmation']
    #测试样本
    thisDoc =  array(setOfWords2Vec(myVocabList,testEntry))
    if classifyNB(thisDoc,p0V,p1V,pAb):
        print(testEntry,'属于侮辱类')
    else:
        print(testEntry,'属于非侮辱类')
    testEntry = ['stupid','garbage']
    thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
    if classifyNB(thisDoc,p0V,p1V,pAb):
        print(testEntry,'属于侮辱类')
    else:
        print(testEntry,'属于非侮辱类')

在这里插入图片描述
由结果看朴素贝叶斯分类器分类正确。
在classifyNB函数中,p1 = sum(vec2Classify * p1Vec) + log(pClass1)代码可能有些难理解,我们提出来解释一下,我们之前的公式是 P ( C i ∣ W ) = P ( W ∣ C i ) P ( C i ) P ( W ) P(C_i|W)=\frac{P(W|C_i)P(C_i)}{P(W)} P(CiW)=P(W)P(WCi)P(Ci),代码中的ver2Classify是测试词条向量,p1Vec是侮辱类的条件概率,两者相乘其实就是贝叶斯推断的扩展。我们前面对条件概率进行了变形,比如 P ( A ∣ B ) = P ( A ) P ( B ∣ A ) P ( B ) P(A|B)=P(A)\frac{P(B|A)}{P(B)} P(AB)=P(A)P(B)P(BA),这里把P(A)称为先验概率,即在事件B发生之前,我们对A事件概率的一个判断;P(A|B)称为后验概率,即在B事件发生之后,我们对A事件概率的重新评估;P(B|A)/P(B)称为可能性函数,这是一个调整因子,使得预估概率更加接近真实概率,所以我们可以写成如下:

后验概率 = 先验概率 * 调整因子

但是我们是比较两者的大小,同时我们也发现两者的分母相同,都是P(W),所以我们只需要比较分子即可,这样可以减少计算量。
讲到这,我们就可以理解vec2Classify * p1Vec的含义,就是在计算后验概率,不过没有除以分母,即计算每个单词出现的概率,而使用sum进行相加,是因为是在对数函数里进行,实际上是相乘,但是根据上面提到的对数代数公式,转为两个对数相加,最后的加上文档类别概率也是一样的道理,也是相乘转为相加计算。

文档词袋模型
目前为止我们将每个词的出现与否作为一个特征,这可以被描述为词集模型,如果一个词在文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为词袋模型。在词袋中,每个单词可以出现多次,而在词集中,每个词只能出现一次,为适应词袋模型,我们修改setOfWords2Vec()为bagOfWords2Vec()

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

即单词没出现一次相应的就加一。

实例应用:使用朴素贝叶斯过滤垃圾文件
前面的简单例子中。我们引入了字符串列表。但是在使用朴素贝叶斯解决一些现实生活的问题时,需要先从文本内容得到字符串列表,然后生成词向量。接下来我们将了解朴素贝叶斯的一个最著名的应用:电子邮件垃圾过滤。先来看使用朴素贝叶斯对电子邮件进行分类的步骤:

1.收集数据:提供文本文件
2.准备数据:将文本文件解析成词条向量
3.分析数据:检查词条确保解析的正确性
4.训练算法:使用我们之前建立的trainNB0()函数
5.测试算法:使用classifyNB(),并且构建一个新的测试函数来计算文档集的错误率
6.使用算法:构建一个完整的程序对一组文档进行分类,将错分的文档输出到屏幕上

收集数据:之前的博客我已经给出了书中数据内容的下载地址,这里我再给出来,数据下载地址,点击source code下载即可。我们下载后发现有两个文件夹ham和spam,每个的里面都有25个txt文件。

准备数据:切分文本
前面的例子都是已经切分好的字符串列表,这里我们将了解如何切分。我们将使用到python的第三方库re。我们来看代码解释:

import re
def textParse(bigString):#接收字符串并转为字符串列表
    listOfTokens = re.split(r'\W+',bigString)
    #以特殊符号作为切分标志,即非字母,非数字
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]
#除了单个字母,其余皆转为小写字母返回列表

re.split()函数的意思就是将一个字符串按照给定的正则表达式的匹配结果进行切分并且返回列表。上述代码中的r’\W+’ 就是正则表达式的字符串或原生字符串的表示,而bigString就是待匹配的字符串,对于正则表达式中的前缀r,它其实可以避免部分疑惑,因为r开头的python字符串是raw字符串(即原生字符串),它里面的所有字符串都不会被转义。比如’\n’我们都知道是换行符,但是r’\n’它就是一个斜杠加一个字符n,没有被转意。对于其中的’\W’和’+’,前一个意思是单词字符,等价于只取除[A-Za-z0-9_]这些之外的字符,而后者是将前一个字符0次或无限次扩展
(书中是*,但是python3的正则则表达处理单词是使用+ )
,所以两者结合就是按除单词,数字和下划线之外的任意字符串作为分隔符。

了解了如何切分字符串,我们可以编写邮件的解析和测试函数:

import re
from numpy import *
def trainNBO(trainMatrix,trainCategory):
    #朴素贝叶斯分类器训练函数
    #参数为文档矩阵(即setOfWords2Vec返回的矩阵向量)和类标签向量
    numTrainDocs = len(trainMatrix)
    #统计训练文档的数目
    numWords = len(trainMatrix[0])
    #统计每个文档的词条数
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    #sum没有参数表示全部相加,实质上就是统计为1的数量(即侮辱性文档的数量)
    #除以文档总数,算出概率(即P(ci))
    p0Num = ones(numWords)
    p1Num = ones(numWords)
    #创建一维的全部为1的数组
    p0Denom = 2.0
    p1Denom = 2.0
    #分母初始化为2
    #p0Num = zeros(numWords)
    # #创建一维零数组,长度与每个文档的词条数相同
    #p1Num = zeros(numWords)
    # #创建一维零数组,长度与每个文档的词条数相同
    #p0Denom = 0.0
    #p1Denom = 0.0
    #分母初始化为0
    for i in range(numTrainDocs):
        #遍历统计数据
        if trainCategory[i] == 1:
            #如果类别为侮辱性的
            p1Num += trainMatrix[i]
            #对应的单词的数加一
            p1Denom += sum(trainMatrix[i])
            #文档中出现的单词数全部统计相加
        else:#类别是非侮辱性的
            p0Num +=trainMatrix[i]
            #对应的单词数加一
            p0Denom += sum(trainMatrix[i])
            # 文档中出现的单词数全部统计相加
    p1Vect = log(p1Num/p1Denom)
    p0Vect = log(p0Num/p0Denom)
    #p1Vect = p1Num/p1Denom
    #p0Vect = p0Num/p0Denom
    #计算数据,即单词在侮辱性和非侮辱性文档中的出现的概率(p(w|ci))
    return p0Vect,p1Vect,pAbusive
    #返回属于非侮辱性文档的条件概率,属于侮辱性文档的条件概率和文档属于侮辱类的概率


def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):
    #朴素贝叶斯分类函数,参数是待分类的词条,非侮辱类的条件概率
    #侮辱类的条件概率,文档属于侮辱类的概率
    p1 = sum(vec2Classify * p1Vec) + log(pClass1)
    #计算属于侮辱类的概率
    p0 = sum(vec2Classify * p0Vec) + log(1.0-pClass1)
    #计算属于非侮辱类的概率
    if p1 > p0:
        return 1
    else:
        return 0

def createVocabList(dataSet):
    #将实验样本词条整理成不重复的词条列表,即词汇表
    vocabSet = set([])
    #创建一个空的不重复的集合
    for document in dataSet:
        vocabSet = vocabSet | set(document)
        #取并集(document用set保证词条里单词不重复,取并集保证两者结合也没有重复的单词)
    return list(vocabSet)
#以列表的形式返回词条列表

def setOfWords2Vec(vocabList,inputSet):
    #根据词汇表,对实验样本(inputSet)向量化
    returnVec = [0]*len(vocabList)
    #创建一个与词汇表同等大小的零向量
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1
        #如果单词出现在词汇表中,则在词汇表中搜索其位置
        #同时置为1
        else:
            print("the word :%s is not in mu vocabulary!"%word)
    return returnVec
    #返回文档向量

def textParse(bigString):#接收字符串并转为字符串列表
    listOfTokens = re.split(r'\W+',bigString)
    #以特殊符号作为切分标志,即非字母,非数字
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]
#除了单个字母,其余皆转为小写字母返回列表

def spamTest():#垃圾邮件测试函数
    docList = []
    classList = []
    fullText = []
    #创建三个空列表
    for i in range(1,26):
        #遍历所有txt文件,spam和ham文件里都只有25个文件
        wordList = textParse(open('spam/%d.txt'%i,encoding='utf-8',errors='ignore').read())
        #读取每个垃圾邮件,并都转为字符串列表
        docList.append(wordList)
        #将每个列表都存储在一个总列表里
        fullText.extend(wordList)
        #将所有列表元素都集合在一个列表里
        classList.append(1)
        #类别标签标记为垃圾邮件为1
        wordList = textParse(open('ham/%d.txt'%i,encoding='utf-8',errors='ignore').read())
        #读取每个非垃圾邮件,并都转为字符串列表
        docList.append(wordList)
        fullText.extend(wordList)
        #同上
        classList.append(0)
        #标签标记为非垃圾邮件为0
    vocabList = createVocabList(docList)
    #创建不重复的词汇表
    trainingSet = list(range(50))
    #创建一个长度为50的列表
    #即存储训练集的索引值的列表,因为共有50个文件
    testSet = []
    #创建测试集的索引列表
    for i in range(10):#随机选取10个作为测试集
        randIndex = int(random.uniform(0,len(trainingSet)))
        #随机生成索引值
        testSet.append(trainingSet[randIndex])
        #将选取的测试用例索引添加到列表中
        del(trainingSet[randIndex])
        #训练集中删除该索引
    trainMat = []
    #用来存储训练词向量集
    trainClasses = []
    #用来存储类别标签向量
    for docIndex in trainingSet:#遍历训练集
        trainMat.append(setOfWords2Vec(vocabList,docList[docIndex]))
        #构建词向量,并添加到训练矩阵中
        trainClasses.append(classList[docIndex])
        #存储每个的类别标签
    p0V,p1V,pSpam = trainNBO(array(trainMat),array(trainClasses))
    #朴素贝叶斯分类器
    errorCount = 0
    #定义错误计数器
    for docIndex in testSet:#遍历测试集
        wordVector = setOfWords2Vec(vocabList,docList[docIndex])
        #构建测试集的词向量
        if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
            errorCount += 1
            #分类错误,计数器加一
    print("the error rate is :",(float(errorCount)/len(testSet)))
    #计算错误率并打印
if __name__='__main__':
	spamTest()

这里有个小变动,书中代码是range(50),python3以上的会出现range object doesn’t support item deletion的报错,因为其返回的是range对象,不是数组对象,所以我们要改为list(range(50)).

代码中在50封邮件中随机选取10封作为测试邮件,40封留着作为训练数据集,这种随机选择数据的一部分作为训练集,而剩余部分作为测试集的过程也称为留存交叉验证。我们运行结果在这里插入图片描述
因为是随机选取的测试集,所以每次的结果也都会有差异。如果想要更好的估计错误率,那么就应该将上述过程重复多次,然后求取平均值。

实例应用:使用朴素贝叶斯分类器从个人广告中获取区域倾向
我们前面的两个例子,第一个是过滤网站的恶意留言,第二个是过滤垃圾邮件,当然分类还有许多的其他应用。这最后一个例子,我们将分别从美国的两个城市选取一些人,然后分析这些人发布的征婚广告的信息,来比较这两个城市的人们在广告用词上是否不同。如果结论确实是不同的,那么他们的各自常用的词是哪些。我们先同样来了解它的相关步骤:

1.收集数据:从RSS源收集内容,这里需要对RSS源构建一个接口。
2.准备数据:将文本文件解析成词条向量。
3.分析数据:检查词条确保解析的正确性。
4.训练算法:使用我们之前建立的trainNB0()函数。
5.测试算法:观察错误率,确保分类器可用,可以修改切分程序,以降低错误率,提高分类结果。
6.使用算法:构建一个完整的程序,封装所有的内容。给定两个RSS源,该程序会显示最常用的公共词。

我们使用来自不同城市的广告训练一个分类器,然后观察分类器的效果。当然我们的目的并不是使用该分类器进行分类,而是通过观察单词和条件概率值来发现与特定城市相关的内容。

收集数据:导入RSS源
我们使用python下载文本,可以使用RSS源,当然在这之前我们先要下载安装feedparser,这里我给出feedparser的下载地址,feedparser官方地址,之后步骤自行百度搜素,这里不再细说。书中给出如下代码:

import feedparser
ny = feedparser.parse('http://newyork.craigslist.org/stp/index.rss')
print(ny['entries'])
print(len(ny['entries']))

书中给出的结果是100,我们自行运行后结果为空,大小也为0,要么就是这个RSS源被国内访问屏蔽,要么就是时间过长已经失效了。所以待会我们可以用另外的RSS源来代替。

接下来我们可以编写RSS源的分类器和高频词去除函数:

def calcMostFreq(vocabList,fullText):#统计查找出现次数最多的单词
    freqDict = {}
    #创建字典存储
    for token in vocabList:
        freqDict[token] = fullText.count(token)
        #遍历词汇表,统计其在文本中出现的次数
    sortedFreq = sorted(freqDict.items(),key=operator.itemgetter(1),\
                        reverse=True)
    #根据每个词出现的次数进行从大到小的排序
    return sortedFreq[:30]
    #返回前30个出现次数最多的单词

def localWords(feed1,feed0):#参数为两个RSS源
    docList = []
    classList = []
    fullText = []
    minLen = min(len(feed1['entries']),len(feed0['entries']))
    #选取两者较短的长度
    for i in range(minLen):
        wordList = textParse(feed1['entries'][i]['summary'])
        #提取RSS源中的字符串文本并切分为字符串列表
        #这里可以自己查看它的结构就可以理解
        docList.append(wordList)
        #将每个字符串列表放入一个总的列表里
        fullText.extend(wordList)
        #将所有字符都存储在一个列表里
        classList.append(1)
        #标记为垃圾广告,标签为1
        wordList = textParse(feed0['entries'][i]['summary'])
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)
        #同上,这里标记为非垃圾广告
    vocabList = createVocabList(docList)
    #创建不重复的词汇表
    top30Words = calcMostFreq(vocabList,fullText)
    #存储出现次数最多的前30个单词
    for pairW in top30Words:
        if pairW[0] in vocabList:
            vocabList.remove(pairW[0])
    #遍历单词,在词汇表中去掉出现次数最高的那些单词
    trainingSet = list(range(2*minLen))
    #创建训练集的索引列表
    testSet = []
    #创建测试集的索引列表
    for i in range(20):#随机选取20个作为测试用例
        randIndex = int(random.uniform(0,len(trainingSet)))
        #随机生成索引值
        testSet.append(trainingSet[randIndex])
        #添加训练集的索引值
        del(trainingSet[randIndex])
        #删除训练集中的索引值
    trainMat = []
    trainClasses = []
    for docIndex in trainingSet:
        trainMat.append(bagOfWords2VecMN(vocabList,docList[docIndex]))
        #添加训练词集
        trainClasses.append(classList[docIndex])
        #添加训练类别用例
    p0V,p1V,pSpam = trainNBO(array(trainMat),array(trainClasses))
    #朴素贝叶斯分类器
    errorCount = 0
    #定义错误计数器
    for docIndex in testSet:
        wordVector = bagOfWords2VecMN(vocabList,docList[docIndex])
        #测试集的词集
        if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
            errorCount += 1
            #如果分类错误,计数器加一
    print("the error rate is : ",float(errorCount)/len(testSet))
    return vocabList,p0V,p1V
    #返回值

使用RSS源作为参数导入,这是因为RSS源会随时间而改变,如果想通过改变代码来比较程序的执行差异,就要使用相同的输入。重新加载RSS源会得到新的数据,但很难确定是代码原因还是输入原因导致输出结果的改变。

因为书上给出的两个RSS源都失效了,所以这里我们给出两个替代的RSS源,http://www.nasa.gov/rss/dyn/image_of_the_day.rss和http://sports.yahoo.com/nba/teams/hou/rss.xml,当然书中的RSS源有100个大小,可是这两个都差不多只有10-20个左右,所以书中代码我们需要改变,否则会报错。

移除词频高的单词,它们是词汇表中的一小部分单词,却占据了所有文本用次的一大部分,这是因为语言中大部分都是冗余和结构辅助性内容,会有一定的误差。除此之外还有一个常用的方法不仅可以移除高频词,同时可以从某个预定词表中移除结构上的辅助词,它们也称为停用词,由于我们给出的RSS源的文章数比较小,移除30个词会报错,我们便采用这种方法。

在这篇博客中给出了停词表和其方法,博客地址,将停词表放入到txt文件中,并存放于同目录下,编写函数:

def stopWord():
    wordList = open('stopword.txt').read()
    listOfTokens = re.split(r'\W+',wordList)
    return [tok.lower() for tok in listOfTokens]

同时,localWords函数里面的删除前30个高频词汇的代码也要更改,stopWordList = stopWord(); for stopWord in stopWordList: if stopWord in vocabList: vocabList.remove(stopWord)

再者,原函数中取20个作为测试集,我们也更改为5个,我们给出完整代码运行:

from numpy import  *
import re
import feedparser
import operator

def createVocabList(dataSet):
    #将实验样本词条整理成不重复的词条列表,即词汇表
    vocabSet = set([])
    #创建一个空的不重复的集合
    for document in dataSet:
        vocabSet = vocabSet | set(document)
        #取并集(document用set保证词条里单词不重复,取并集保证两者结合也没有重复的单词)
    return list(vocabSet)
#以列表的形式返回词条列表

def bagOfWords2VecMN(vocabList,inputSet):
    returnVec = [0]*len(vocabList)
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] += 1
    return returnVec
    
def trainNBO(trainMatrix,trainCategory):
    #朴素贝叶斯分类器训练函数
    #参数为文档矩阵(即setOfWords2Vec返回的矩阵向量)和类标签向量
    numTrainDocs = len(trainMatrix)
    #统计训练文档的数目
    numWords = len(trainMatrix[0])
    #统计每个文档的词条数
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    #sum没有参数表示全部相加,实质上就是统计为1的数量(即侮辱性文档的数量)
    #除以文档总数,算出概率(即P(ci))
    p0Num = ones(numWords)
    p1Num = ones(numWords)
    #创建一维的全部为1的数组
    p0Denom = 2.0
    p1Denom = 2.0
    #分母初始化为2
    #p0Num = zeros(numWords)
    # #创建一维零数组,长度与每个文档的词条数相同
    #p1Num = zeros(numWords)
    # #创建一维零数组,长度与每个文档的词条数相同
    #p0Denom = 0.0
    #p1Denom = 0.0
    #分母初始化为0
    for i in range(numTrainDocs):
        #遍历统计数据
        if trainCategory[i] == 1:
            #如果类别为侮辱性的
            p1Num += trainMatrix[i]
            #对应的单词的数加一
            p1Denom += sum(trainMatrix[i])
            #文档中出现的单词数全部统计相加
        else:#类别是非侮辱性的
            p0Num +=trainMatrix[i]
            #对应的单词数加一
            p0Denom += sum(trainMatrix[i])
            # 文档中出现的单词数全部统计相加
    p1Vect = log(p1Num/p1Denom)
    p0Vect = log(p0Num/p0Denom)
    #p1Vect = p1Num/p1Denom
    #p0Vect = p0Num/p0Denom
    #计算数据,即单词在侮辱性和非侮辱性文档中的出现的概率(p(w|ci))
    return p0Vect,p1Vect,pAbusive
    #返回属于非侮辱性文档的条件概率,属于侮辱性文档的条件概率和文档属于侮辱类的概率
def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):
    #朴素贝叶斯分类函数,参数是待分类的词条,非侮辱类的条件概率
    #侮辱类的条件概率,文档属于侮辱类的概率
    p1 = sum(vec2Classify * p1Vec) + log(pClass1)
    #计算属于侮辱类的概率
    p0 = sum(vec2Classify * p0Vec) + log(1.0-pClass1)
    #计算属于非侮辱类的概率
    if p1 > p0:
        return 1
    else:
        return 0

def textParse(bigString):#接收字符串并转为字符串列表
    listOfTokens = re.split(r'\W+',bigString)
    #以特殊符号作为切分标志,即非字母,非数字
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]
#除了单个字母,其余皆转为小写字母返回列表

def stopWord():
    wordList = open('stopword.txt').read()
    listOfTokens = re.split(r'\W+',wordList)
    return [tok.lower() for tok in listOfTokens]


def localWords(feed1,feed0):#参数为两个RSS源
    docList = []
    classList = []
    fullText = []
    minLen = min(len(feed1['entries']),len(feed0['entries']))
    #选取两者较短的长度
    for i in range(minLen):
        wordList = textParse(feed1['entries'][i]['summary'])
        #提取RSS源中的字符串文本并切分为字符串列表
        docList.append(wordList)
        #将每个字符串列表放入一个总的列表里
        fullText.extend(wordList)
        #将所有字符都存储在一个列表里
        classList.append(1)
        #标记为垃圾广告,标签为1
        wordList = textParse(feed0['entries'][i]['summary'])
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)
        #同上,这里标记为非垃圾广告
    vocabList = createVocabList(docList)
    stopWordList = stopWord()
    for stopWord in stopWordList:
        if stopWord in vocabList:
            vocabList.remove(stopWord)
    #创建不重复的词汇表
    # top30Words = calcMostFreq(vocabList,fullText)
    # #存储出现次数最多的前30个单词
    # for pairW in top30Words:
    #     if pairW[0] in vocabList:
    #         vocabList.remove(pairW[0])
    #遍历单词,在词汇表中去掉出现次数最高的那些单词
    trainingSet = list(range(2*minLen))
    #创建训练集的索引列表
    testSet = []
    #创建测试集的索引列表
    for i in range(5):#随机选取20个作为测试用例
        randIndex = int(random.uniform(0,len(trainingSet)))
        #随机生成索引值
        testSet.append(trainingSet[randIndex])
        #添加训练集的索引值
        del(trainingSet[randIndex])
        #删除训练集中的索引值
    trainMat = []
    trainClasses = []
    for docIndex in trainingSet:
        trainMat.append(bagOfWords2VecMN(vocabList,docList[docIndex]))
        #添加训练词集
        trainClasses.append(classList[docIndex])
        #添加训练类别用例
    p0V,p1V,pSpam = trainNBO(array(trainMat),array(trainClasses))
    #朴素贝叶斯分类器
    errorCount = 0
    #定义错误计数器
    for docIndex in testSet:
        wordVector = bagOfWords2VecMN(vocabList,docList[docIndex])
        #测试集的词集
        if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
            errorCount += 1
            #如果分类错误,计数器加一
    print("the error rate is : ",float(errorCount)/len(testSet))
    return vocabList,p0V,p1V
    #返回值
if __name__='__main__':
	ny = feedparser.parse('http://www.nasa.gov/rss/dyn/image_of_the_day.rss')
    sf = feedparser.parse('http://sports.yahoo.com/nba/teams/hou/rss.xml')
    print(len(ny))
    print(len(sf))
    vocabList,pSF,pNY = localWords(ny,sf)

运行结果:在这里插入图片描述
虽然这里的错误率比之前的例子要高一些,但是这里我们关注的是单词的概率而并不是错误率,所以不是很严重。

分析数据:显示地域相关的用词
我们上个函数已经得到了词汇表,和p0V和p1V这两个条件概率,所以我们可以编写函数:

def getTopWords(ny,sf):#最具有表征性的词汇显示函数
    vocabList,p0V,p1V = localWords(ny,sf)
    #获得概率值
    topNY = []
    topSF = []
    for i in range(len(p0V)):
    #遍历概率值
        if p0V[i] > -6.0:
            topSF.append((vocabList[i],p0V[i]))
        if p1V[i] > -6.0:
            topNY.append((vocabList[i],p1V[i]))
            #若概率大于阈值,则往列表中加入相应的单词和其概率组成的
            #二元列表
    sortedSF = sorted(topSF,key=lambda pair:pair[1],reverse=True)
    #对每个二元列表的概率值进行从大到小排序
    print("SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**")
    for item in sortedSF:
        print(item[0])
        #遍历每个列表,先输出概率值大的单词
    sortedNY = sorted(topNY,key=lambda pair:pair[1],reverse=True)
    print("NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**")
    for item in sortedNY:
        print(item[0])

同样,与上面的代码整理后运行getTopWords(ny,sf),(阈值为负数是因为取的对数)由于数量过多,我们修改阈值,将-6改为-4,我们可以得到结果:
在这里插入图片描述
有些人按书上的代码编写可能得到空,这是因为切分的正则处理python3已经变为’\W+’,改完后就可以了,当然大家可以自行修改阈值去观察变化。

PS:至此机器学习实战的朴素贝叶斯的分类算法就讲解结束了,但是它也可以用Sklearn库去实现,之后也会对其进行介绍,也希望这篇博客能给大家带来帮助,如果有用,也希望大家多多支持!

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值