机器学习实战笔记:朴素贝叶斯分类

 前言

在之前我们讲述knn以及决策树时。曾要求分类器做出决策给出“该实例属于哪一类”这类问题的明确答案。不过分类器有时候会产生错误的结果,这个时候要求分类器给出一个最优的类别猜测结果,同时给出这个猜测的概率估计值。之前决策树章节里信息熵的计算就涉及到了一些概率知识,接下来我们将在这个基础上深入讨论。

本章会给出一些使用概率论进行分类的方法。首先从一个最简单的概率分类器开始,然后给出一些假设来学习朴素贝叶斯分类器。我 们 称 之 为“朴素” ,是因为整个形式化过程只做最始、最简单的假设。不必担心,你会详细了解到这些假设。我们将充分利用python的文本处理能力将文档切分成词向量,然后利用词向量对文档进行分类。我们还将构建另一个分类器,观察其在真实的垃圾邮件数据集中的过滤效果,必要时还会回顾一下条件概率。最 后 ,我们将介绍如何从个人发布的大量广告中学习分类器,并将学习结果转换成人类可理解的信息。

朴素贝叶斯理论

贝特斯决策理论

朴素贝叶斯是贝叶斯决策理论的一部分,所以讲述朴素负叶斯之前有必要快速了解一下贝叶
斯决策理论。 假设现在我们有一个数据集,它由两类数据组成,数据分布如下图所示。

我们现在用p1(x,y)表示数据点(x,y)属于类别1(图中用圆点表示的类别)的概率,用p2(x,y)表示数据点(x,y)属于类别2(图中用三角形表示的类别)的概率,那么对于一个新的数据点(x,y),可以用下面的规则来判断他的类别:

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

也就是说我们会选择高概率对应的类别,这就是贝叶斯决策理论的核心思想—选择具有最高概率的决策。那么接下来我们就要计算p1和p2,在计算这两个概率的同时,我们需要了解一下条件概率以及全概率公式。

 条件概率

如图所示,假设A,B是两个事件,并且p(B)>0,那么在事件B已经发生的条件下,事件A发生地概率就可以称之为条件概率,其公式为:

                                                                                    p(A|B)=\frac{p(A\bigcap B)}{p(B)}

可以看到,如果p(A\bigcap B)等于0.即条件概率也为0,所以我们一般说到条件概率这一个概念的时候,最好事件A和事件B都是同一个实验下不同结果的集合。

同理可得:

                                                                                  p(A\bigcap B)=p(A|B)p(B)

                                                                                  p(B\bigcap A)=p(B|A)p(A)

                                                                                  p(A|B)p(B)=p(B|A)p(A)

                                                                                  p(A|B)=\frac{p(B|A)p(A)}{p(B)}

这就是条件概率的计算公式。

 全概率公式

假如事件B1,B2,....,构成了一个完备事件组并且都有正概率,则对于任何一个事件B,都有如下公式成立:

                                                                              p(B)=\sum_{i=1}^{n}p(B_{i})p(A|B_{i})

其中A为任意一个事件(比如上图中的红色区域),将这个公式带入上面的条件概率公式,,此公式便是全概率公式:

                                                                     p(A|B)=\frac{p(B|A)p(A)}{p(B|A)p(A)+p(B|{A}')p({A}')}

贝叶斯推断

对上面的条件概率公式进行变形,我们可以得到如下形式:

                                                                            p(A|B)=p(A)\frac{p(B|A)}{p(B)}

其中P(A)称之为”先验概率“:即在事件B发生之前,我们通过自己的经验判断对A事件概率的一个估计,是主观概率。

P(A|B)称之为”后验概率“:就是在事件B发生之后,我们对A事件概率进行重新评估的概率,是客观概率

P(B|A)/P(B)可以把它当做调整因子,使得预估概率更接近于真实概率。

所以,条件概率可以理解成:后验概率 = 先验概率  x  调整因子

这就是贝叶斯推断的含义。我们先预估一个"先验概率",然后加入实验结果,看这个实验是增强还是削弱了"先验概率",由此得到更接近事实的"后验概率"。如果调整因子P(B|A)/P(B)>1,意味着"先验概率"被增强,事件A的发生的可能性变大;如果"可能性函数"=1,意味着B事件无助于判断事件A的可能性;如果"可能性函数"<1,意味着"先验概率"被削弱,事件A的可能性变小。

举个例子:已知某种疾病的发病率是0.001,即1000人中会有1个人得病。现有一种试剂可检验患者是否得病,它的准确率是0.99,即在患者确实得病的情况下,它有99%的可能呈现阳性。它的误报率是5%,即在患者没有得病的情况下,它有5%的可能呈现阳性。现有一个病人的检验结果为阳性,请问他确实得病的可能性有多大?

p(A)=0.001:疾病的发病率;p(B):检验出阳性的概率;p(D):患病;p(D)=p(A)

                                                                  p(D|B)=p(D)\frac{p(B|D)}{p(B)}

                                                                  p(B|D)=0.99

                                               p(B)=p(B|D)p(D)+p(B|{D}')p({D}')=0.001*0.99+0.999*0.05

最后我们得到了一个惊人的结果,得病的可能性约等于0.019。也就是说,即使检验呈现阳性,病人得病的概率,也只是从0.1%增加到了2%左右。这就是所谓的"假阳性",即阳性结果完全不足以说明病人得病。尽管这种检验的准确率高达0.99,但是可信率却不到百分之2,就是因为它的误报率太高了。

朴素贝叶斯推断

贝叶斯和朴素贝叶斯的概念是不同的,区别就在于朴素这两个字,朴素贝叶斯对每个条件做了一个大胆的假设,即每个条件是相互独立的。比如下面的公式,假设X有n个维度,这样就可以得出:

                                          P(X_1=x_1,X_2=x_2,...X_n=x_n|Y=C_k)=P(X_1=x_1|Y=C_k)P(X_2=x_2|Y=C_k)...P(X_n=x_n|Y=C_k)

从上式可以看出,大大的简化了条件分布,但是这也可能带来预测的不准确性。你会说如果我的特征之间非常不独立怎么办?如果真是非常不独立的话,那就尽量不要使用朴素贝叶斯模型了,考虑使用其他的分类方法比较好。但是一般情况下,样本的特征之间独立这个条件的确是弱成立的,尤其是数据量非常大的时候。虽然我们牺牲了准确性,但是得到的好处是模型的条件分布的计算大大简化了,这就是贝叶斯模型的选择。

我们可以看一个例子:

这个时候,来了第七个患者,一位打喷嚏的工人,请推断他得了什么病。这就是一个分类题。现状把所有患者分成了三类“感冒”“过敏”“脑震荡”,我们的目的是把“打喷嚏的工人”分到这三类中的一类中。具体做法为:根据贝叶斯定理,计算这个“打喷嚏的工人”患三种疾病的概率。

P(感冒|打喷嚏的工人)=P(打喷嚏的工人|感冒) x P(感冒) / p(打喷嚏的工人)

=P(打喷嚏|感冒) x p(工人|感冒) x P(感冒) / p(打喷嚏) x P(工人)

=(2/3) x (1/3) x (1/2) / (1/2 x 1/3)

=66.7%

按照这个方法,计算打喷嚏的工人得另外两种病的概率都为0%。可见,打喷嚏的工人患感冒的概率更高,达到了66.7%。但是一般的分类器都要根据具体业务设置阈值,对于人命关天的事,最好严格一些,比如95%以上才做出判断,那么这里最好的答案应该是“机器无法判断,建议去让医生看看”。

下面我们将利用朴素贝叶斯进行文档分类。

使用贝叶斯分类器进行文本分类

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

接下来首先给出将文本转换为数字向量的过程,然后介绍如何基于这些向量来计算条件概率,并在此基础上构建分类器’最后还要介绍一些实现朴素贝叶斯过程中需要考虑的问题。

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

我们将把文本看成单词向量或者词条向量,也就是说将句子转换为向量。考虑出现在所有文档中的所有单词,再决定将哪些词纳人词汇表或者说所要的词汇集合,然后必须要将每一篇文档转换为词汇表上的向量。接下来我们正式开始。打开文本编辑器,创建一个叫bayes.py的新文件,然后将下面的程序添加到文件中。

import numpy as np
"""
函数说明:创建实验样本
Parameter:
    无
Return:
    postingList - 实验样本切分的词条
    classVec - 类别标签向量
Modify:
    2018/9/11
"""
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]                                                                  #类别标签向量,1代表侮辱性词汇,0代表不是
    return postingList,classVec
"""
函数说明:将切分的实验样本词条整理成不重复的词条列表,也就是词汇表
Parameters:
    dataSet - 整理的样本数据集
Returns:
    vocabSet - 返回不重复的词条列表,也就是词汇表
Modify:
    2018/9/11
"""
def createVocabList(dataSet):
    vocabSet = set([])
    for data in dataSet:
        #创建两个集合的并集
        vocabSet = vocabSet | set(data)
    return list(vocabSet)

"""
函数说明:根据vocabList词汇表,将inputSet向量化,响亮的每个元素为1或者0
Parameter:
    vocabList - createVocabList返回的列表
    inputSet - 切分的词条列表
Returns:
    returnVec - 文档向量,词集模型
Modify:
    2018/9/11
"""
def setofWords2Vec(vocabList,inputSet):
    #创建一个其中所含元素都为0的向量
    returnVec = [0] * len(vocabList)
    for each in inputSet:
        if each in vocabList:
            returnVec[vocabList.index(each)]=1
        else:
            print("the word : %s is not in my Vocabulary"% each)
    return returnVec


if __name__ == '__main__':
    postingList,classVec = loadDataSet()
    print('postingList:\n',postingList)
    myVocabList = createVocabList(postingList)
    print('myVocabList:\n',myVocabList)

执行上出程序后可以看到myVocabList中不会出现重复的单词。但目前该词汇表还没有排序,需要的话,稍后可以对其排序。

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

前面介绍了如何将一组单词转换为一组数字,接下来看看如何使用这些数字计算概率。现在已经知道一个词是否出现在一篇文档中,也知道该文档所属的类别。我们重写贝叶斯准则,将之前的x,y替换为w。w表示这是一个向量,即它由多个数值组成。在这个例子中,数值个数与词汇表中的词个数相同。

                                                                             p(c_{i}|w)=\frac{p(w|c_{i})p(c_{i})}{p(w)}

我们将使用上述公式,对每个类计算该值,然后比较这两个概率值的大小。打开bayes.py文件,添加下述代码:

"""
函数说明:朴素贝叶斯分类器训练函数
Parameter:
    trainMat — 训练文档矩阵,即setofWord2Vec 返回的returnVec构成的矩阵
    trainCategory - 训练类别标签向量,即loadDataSet 返回的classVec
Returns:
    p0Vect - 非侮辱类的条件概率数组
    p1Vect - 侮辱类的条件概率数组
    pAbusive - 文档属于侮辱类的概率
Modify:
2018/9/11
"""

def trainNBO(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix)     #文档数目
    numWords = len(trainMatrix[0])      #词条数
    pAbusive = sum(trainCategory) / float(numTrainDocs)     #文档属于侮辱类的概率
    p0Num = np.ones(numWords)
    p1Num = np.ones(numWords)
    p0Denom = 0.0
    p1Denom = 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])
    #print(p1Denom)
    #print(p1Num)
    p1Vect = np.log(p1Num/p1Denom)      #对每个元素做除法,避免程序输出下溢,采用自然对数处理
    p0Vect = np.log(p0Num/p0Denom)
    return p0Vect,p1Vect,pAbusive

if __name__ == '__main__':
    postingList, classVec = loadDataSet()
    myVocabList = createVocabList(postingList)
    print('myVocabList:\n', myVocabList)
    trainMat = []
    for postinDoc in postingList:
        trainMat.append(setofWords2Vec(myVocabList, postinDoc))
    p0V, p1V, pAb = trainNBO(trainMat, classVec)
    print('p0V:\n', p0V)
    print('p1V:\n', p1V)
    print('classVec:\n', classVec)
    print('pAb:\n', pAb)

运行程序后可以看到pAb=0.5,pAb就是先验概率。现在已经准备好构建完整的分类器了,接下来,使用分类器进行分类,添加以下代码:

"""
函数说明:朴素贝叶斯分类器分类函数
Parameter:
    vec2Classify - 待分类的词条数组
    p0Vec - 侮辱类的条件概率数组
    p1Vec - 非侮辱类的条件概率数组
    pClass1 - 文档属于侮辱类的概率
Returns:
    1 - 属于侮辱类
    0 - 属于非侮辱类
Modify:
    2018/9/14
"""
def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):
    p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
    p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
    if p1 > p0:
        return 1
    else:
        return 0

 
if __name__ == '__main__':
    postingList,classVec = loadDataSet() 
    myVocabList = createVocabList(postingList)
    
    trainMat=[]
    for postinDoc in postingList:
        trainMat.append(setofWords2Vec(myVocabList,postinDoc))
    p0V,p1V,pAb = trainNBO(np.array(trainMat),np.array(classVec))
    testEntry = ['love', 'my', 'dalmation']									
    thisDoc = np.array(setofWords2Vec(myVocabList, testEntry))

    if classifyNB(thisDoc,p0V,p1V,pAb):
         print(testEntry,'属于侮辱类')
    else:
         print(testEntry,'属于非侮辱类')  										
    testEntry = ['stupid', 'garbage']									
    thisDoc = np.array(setofWords2Vec(myVocabList, testEntry))				
    if classifyNB(thisDoc,p0V,p1V,pAb):
        print(testEntry,'属于侮辱类')
    else:
        print(testEntry,'属于非侮辱类')   

输出结果如下:

对文本做一些修改,看看分类器会输出什么结果。这个例子非常简单,但是它展示了朴素贝叶斯分类器的工作原理。接下来,我们会对代码做些修改,使分类器工作得更好。

词袋模型:

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

"""
函数说明:根据vocabList词汇表,构建词袋模型
Parameter:
    vocabList - creatVocabList
    inputSet - 切分的词条列表
Returns:
    returnVec - 文档向量
Modify:
    2018/9/11
"""
def bagOfWords2Vec(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. 使用算法:构建一个完整程序对一组文档分类,将错分的文档输出到屏幕上。

下面首先给出将文本解析为词条的代码。然后将该代码和前面的分类代码集成为一个函数,
该函数在测试分类器的同时会给出错误率。

准备数据:切分文本

打开bayes.py文件,添加下述代码:

import re
import random
import numpy as np

"""
函数说明:接受一个大字符串并将其解析为字符串列表
Parameter:
    无
Returns:
    无
Modify:
    2018/9/11
"""

def textParse(bigString):
    listOfTokens = re.split(r'\W*',bigString)       #将非数字、字母、下划线的字符作为切分标志
    #除了单个字母,例如大写的I,其余的单词变成小写
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]

测试算法:交叉验证

随机选择50个文件中的任意十个文件做测试集,同时将这些测试集从训练集合剔除,进行交叉验证。具体代码如下所示:

"""
函数说明: 测试朴素贝叶斯分类器
Parameters:
    无
Returns:
    无
Modify:
    2018/9/11
"""
def spamTest():
    docList = [];classList = [];fullTest = []
    #导入并解析50个文件,并记录对应的标签值,垃圾邮件标记为1
    for i in range(1,26):
        wordList = textParse(open('email/spam/%d.txt'% i,'r').read())
        docList.append(wordList)
        fullTest.append(wordList)
        classList.append(1)
        wordList = textParse(open('email/ham/%d.txt'% i,'r').read())
        docList.append(wordList)
        fullTest.append(wordList)
        classList.append(0)
    #获得50个文件构成的词列表
    vocabList = createVocablist(docList)
    trainingSet = list(range(50));
    testSet = []
    #随机选取测试集合,并从训练集中去除,留存交叉验证
    for i in range(10):
        randIndex = int(random.uniform(0,len(trainingSet)))
        testSet.append(trainingSet[randIndex])
        del(trainingSet[randIndex])
    trainMat = [];trainClass = []
    #构建训练集词向量列表,训练集标签
    for docIndex in trainingSet:
        trainMat.append(setofWords2Vec(vocabList,docList[docIndex]))
        trainClass.append(classList[docIndex])
    #训练算法,计算分类所需的概率
    p0V,p1V,pSpam = trainNBO(np.array(trainMat),np.array(trainClass))
    #分类错误个数初始化为0
    errorCount = 0
    #,遍历测试集,验证算法错误率
    for docIndex in testSet:
        wordVector = setofWords2Vec(vocabList,docList[docIndex]) 
        if classifyNB(np.array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
            errorCount += 1
            print("分类错误的测试集:",docList[docIndex])
    print('错误率:%.2f%%'%(float(errorCount)/len(testSet)*100))
    

if __name__ == '__main__':
    spamTest()

函数spamTest()会输出在10封随机选择的电子邮件上的分类错误率。既然这些电子邮件是随机选择的,所以每次的输出结果可能有些差别。如果发现错误的话,函数会输出错分文档的词表 ,这样就可以了解到底是哪篇文档发生了错误。如果想要更好地估计错误率,那么就应该将上述过程重复多次,比如说10次 ,然后求平均值。我这么做了一下,获得的平均错误率为6% 。

小结:

朴素贝叶斯算法的主要原理基本已经做了总结,这里对朴素贝叶斯的优缺点做一个总结。

朴素贝叶斯的主要优点有:

1)朴素贝叶斯模型发源于古典数学理论,有稳定的分类效率。

2)对小规模的数据表现很好,能个处理多分类任务,适合增量式训练,尤其是数据量超出内存时,我们可以一批批的去增量训练。

3)对缺失数据不太敏感,算法也比较简单,常用于文本分类。

朴素贝叶斯的主要缺点有:

1) 理论上,朴素贝叶斯模型与其他分类方法相比具有最小的误差率。但是实际上并非总是如此,这是因为朴素贝叶斯模型给定输出类别的情况下,假设属性之间相互独立,这个假设在实际应用中往往是不成立的,在属性个数比较多或者属性之间相关性较大时,分类效果不好。而在属性相关性较小时,朴素贝叶斯性能最为良好。对于这一点,有半朴素贝叶斯之类的算法通过考虑部分关联性适度改进。

2)需要知道先验概率,且先验概率很多时候取决于假设,假设的模型可以有很多种,因此在某些时候会由于假设的先验模型的原因导致预测效果不佳。

3)由于我们是通过先验和数据来决定后验的概率从而决定分类,所以分类决策存在一定的错误率。

4)对输入数据的表达形式很敏感。

PS:

本篇文章所涉及到的数据集以及完整代码都存放在github中

欢迎各位大佬fock or star

https://github.com/AdventureSJ/ML-Notes

猜你喜欢

转载自blog.csdn.net/cv_pyer/article/details/89177301