Pyhthon3《机器学习实战》学习笔记三:朴素贝叶斯

一 前言

      前两章的KNN分类算法和决策树分类算法最终都是预测出实例的确定的分类结果,但是,有时候分类器会产生错误结果;本章要学的朴素贝叶斯分类算法则是给出一个最优的猜测结果,同时给出猜测的概率估计值。

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

贝叶斯决策理论

    假设现在我们有一个数据集,它由两类数据组成,数据分布如下图所示:

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

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

    也就是说,我们会选择高概率对应的类别。这就是贝叶斯决策理论的核心思想,即选择具有最高概率的决策。已经了解了贝叶斯决策理论的核心思想,那么接下来,就是学习如何计算p1和p2概率。

二 条件概率

有一个装了 7 块石头的罐子,其中 3 块是白色的,4 块是黑色的。如果从罐子中随机取出一块石头,那么是白色石头的可能性是多少?由于取石头有 7 种可能,其中 3 种为白色,所以取出白色石头的概率为 3/7 。那么取到黑色石头的概率又是多少呢?很显然,是 4/7 。我们使用 P(white) 来表示取到白色石头的概率,其概率值可以通过白色石头数目除以总的石头数目来得到。

计算 P(white) 或者 P(black) ,如果事先我们知道石头所在桶的信息是会改变结果的。这就是所谓的条件概率(conditional probablity)。假定计算的是从 B 桶取到白色石头的概率,这个概率可以记作 P(white|bucketB) ,我们称之为“在已知石头出自 B 桶的条件下,取出白色石头的概率”。很容易得到,P(white|bucketA) 值为 2/4 ,P(white|bucketB) 的值为 1/3 。

条件概率的计算公式如下:

P(white|bucketB) = P(white and bucketB) / P(bucketB)

首先,我们用 B 桶中白色石头的个数除以两个桶中总的石头数,得到 P(white and bucketB) = 1/7 .其次,由于 B 桶中有 3 块石头,而总石头数为 7 ,于是 P(bucketB) 就等于 3/7 。于是又 P(white|bucketB) = P(white and bucketB) / P(bucketB) = (1/7) / (3/7) = 1/3 。

另外一种有效计算条件概率的方法称为贝叶斯准则。贝叶斯准则告诉我们如何交换条件概率中的条件与结果,即如果已知 P(x|c),要求 P(c|x),那么可以使用下面的计算方法

2.2 使用条件概率来分类

  假设这里要被分类的类别有两类,类c1和类c2,那么我们需要计算概率p(c1|x,y)和p(c2|x,y)的大小并进行比较:

如果:p(c1|x,y)>p(c2|x,y),则(x,y)属于类c1

         p(c1|x,y)<p(c2|x,y),则(x,y)属于类c2

  我们知道p(x,y|c)的条件概率所表示的含义为:已知类别c1条件下,取到点(x,y)的概率;那么p(c1|x,y)所要表达的含义呢?显然,我们同样可以按照条件概率的方法来对概率含义进行描述,即在给定点(x,y)的条件下,求该点属于类c1的概率值。那么这样的概率该如何计算呢?显然,我们可以利用贝叶斯准则来进行变换计算:
  p(ci|x,y)=p(x,y|ci)*p(ci)/p(x,y)

利用上面的公式,我们可以计算出在给定实例点的情况下,分类计算其属于各个类别的概率,然后比较概率值,选择具有最大概率的那么类作为点(x,y)的预测分类结果。

2.3 朴素贝叶斯中朴素含义

   "朴素"含义:本章算法全称叫朴素贝叶斯算法,显然除了贝叶斯准备,朴素一词同样重要。这就是我们要说的条件独立性假设的概念。条件独立性假设是指特征之间的相互独立性假设,所谓独立,是指的是统计意义上的独立,即一个特征或者单词出现的可能性与它和其他单词相邻没有关系。举个例子来说,假设单词bacon出现在unhealthy后面是与delisious后面的概率相同。当然,我们知道其实并不正确,但这正是朴素一词的含义。同时,朴素贝叶斯另外一个含义是,这些特征同等重要。虽然这些假设都有一定的问题,但是朴素贝叶斯的实际效果却很好。

三 实践

3.1 拆分文本,准备数据

  要从文本中获取特征,显然我们需要先拆分文本,这里的文本指的是来自文本的词条,每个词条是字符的任意组合。词条可以为单词,当然也可以是URL,IP地址或者其他任意字符串。将文本按照词条进行拆分,根据词条是否在词汇列表中出现,将文档组成成词条向量,向量的每个值为1或者0,其中1表示出现,0表示未出现。

接下来,以在线社区的留言为例。对于每一条留言进行预测分类,类别两种,侮辱性和非侮辱性,预测完成后,根据预测结果考虑屏蔽侮辱性言论,从而不影响社区发展。

 词表到向量的转换函数

import  numpy as np

"""
实验样本
词表到向量的转换函数
"""
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):
    """

    :param dataSet: 样本数据集
    :return:       返回不重复的词条列表,也就是词汇表
    """
    vocabSet = set([])                                       # 创建一个空的不重复列表
    for document in dataSet:
        vocabSet = vocabSet | set(document)                  # 取并集
    return list(vocabSet)

def setOfWords2Vec(vocabList,inputSet):
    """

    :param vocabList: createVocabList返回的列表
    :param inputSet:  切分的词条列表
    :return:          文档向量,词集模型
    """
    returnVec = [0] * len(vocabList)                        # 创建一个所有元素都为0 的向量
    for word in inputSet:                                   # 遍历每个词条
        if word in vocabList:                               # 如果词条存在于词汇表中
            returnVec[vocabList.index(word)] = 1
        else:
            print("the word: %s is not in my Vocabulary! " % word)
    return returnVec

if __name__ == '__main__':
    postingList,classVec = loadDataSet()
    print("postingList:\n",postingList)
    myVocabList = createVocabList(postingList)
    print("myVocabList:\n",myVocabList)
    trainMat =[]
    for postinDoc in postingList:
        trainMat.append(setOfWords2Vec(myVocabList,postinDoc))
    print("trainMat:\n",trainMat)

 

需要说明的是,上面函数creatVocabList得到的是所有文档中出现的词汇列表,列表中没有重复的单词,每个词是唯一的。

3.2 由词向量计算朴素贝叶斯用到的概率值

  这里,如果我们将之前的点(x,y)换成词条向量w(各维度的值由特征是否出现的0或1组成),在这里词条向量的维度和词汇表长度相同。

  p(ci|w)=p(w|ci)*p(ci)/p(w)

我们将使用该公式计算文档词条向量属于各个类的概率,然后比较概率的大小,从而预测出分类结果。

  具体地,首先,可以通过统计各个类别的文档数目除以总得文档数目,计算出相应的p(ci);然后,基于条件独立性假设,将w展开为一个个的独立特征,那么就可以将上述公式写为p(w|ci)=p(w0|ci)*p(w1|ci)*...p(wN|ci),这样就很容易计算,从而极大地简化了计算过程

朴素贝叶斯 工作原理

提取所有文档中的词条并进行去重
获取文档的所有类别
计算每个类别中的文档数目
对每篇训练文档: 
    对每个类别: 
        如果词条出现在文档中-->增加该词条的计数值(for循环或者矩阵相加)
        增加所有词条的计数值(此类别下词条总数)
对每个类别: 
    对每个词条: 
        将该词条的数目除以总词条数目得到的条件概率(P(词条|类别))
返回该文档属于每个类别的条件概率(P(类别|文档的所有词条))

代码如下:

def trainNB0(trainMatrix,trainCategory):
    """

    :param trainMatrix:   训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵
    :param trainCategory: 训练类别标签向量,即loadDataSet返回的classVec
    :return:
         p0Vect:          侮辱类的条件概率数组
         p1Vect:          非侮辱类的条件概率数组
         pAbusive:        文档属于侮辱类的概率
    """
    numTrainDocs = len(trainMatrix)                                  # 计算训练文档的数目
    numWords = len(trainMatrix[0])                                   # 计算每篇文档的词条数
    pAbusive = sum(trainCategory) / float(numTrainDocs)              # 文档属于侮辱类的概率
    p0Num = np.ones(numWords)                                       # 词条出现数初始化0
    p1Num = np.ones(numWords)
    p0Denom = 2.0                                                    # 分母初始化为0
    p1Denom = 2.0
    for i in range(numTrainDocs):
        if trainCategory[i] == 1:                                    # 统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)·
            p1Num += trainMatrix[i]
            p1Denom += sum(trainMatrix[i])
        else:
            p0Num += trainMatrix[i]                                  # 统计属于非侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)·
            p0Denom += sum(trainMatrix[i])

    p1Vect = np.log(p1Num / p1Denom)                                 # 取对数,防止下溢出
    p0Vect = np.log(p0Num / p1Denom)
    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 = trainNB0(trainMat, classVec)
    print('p0V:\n', p0V)
    print('p1V:\n', p1V)
    print('classVec:\n', classVec)
    print('pAb:\n', pAb)

  

由于概率都很小,那么相乘之后就更小,会造成四舍五入之后为0,解决这个问题的办法是我们对概率取对数。一下输出为负数的结果是取对数后的值。运行结果如下,p0V存放的是每个单词属于类别0,也就是非侮辱类词汇的概率,p1V存放的就是各个单词属于侮辱类的条件概率。pAb就是先验概率。

3.3 测试朴素贝叶斯算法

def classifyNB(vec2classify,p0Vec,p1Vec,pClass1):
    """
    使用算法:
        # 将乘法转换为加法
        乘法:P(C|F1F2...Fn) = P(F1F2...Fn|C)P(C)/P(F1F2...Fn)
        加法:P(F1|C)*P(F2|C)....P(Fn|C)P(C) -> log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C))
    :param vec2Classify: 待测数据[0,1,1,1,1...],即要分类的向量
    :param p0Vec: 类别0,即正常文档的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表
    :param p1Vec: 类别1,即侮辱性文档的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表
    :param pClass1: 类别1,侮辱性文件的出现概率
    :return: 类别1 or 0
    """
    # 计算公式  log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C))
    # 大家可能会发现,上面的计算公式,没有除以贝叶斯准则的公式的分母,也就是 P(w) (P(w) 指的是此文档在所有的文档中出现的概率)就进行概率大小的比较了,
    # 因为 P(w) 针对的是包含侮辱和非侮辱的全部文档,所以 P(w) 是相同的。
    # 使用 NumPy 数组来计算两个向量相乘的结果,这里的相乘是指对应元素相乘,即先将两个向量中的第一个元素相乘,然后将第2个元素相乘,以此类推。
    # 我的理解是:这里的 vec2Classify * p1Vec 的意思就是将每个词与其对应的概率相关联起来
    p1 = np.sum(vec2classify * p1Vec) + np.log(pClass1)               # P(w|c1) * P(c1) ,即贝叶斯准则的分子
    p0 = np.sum(vec2classify * p0Vec) + np.log(1.0 - pClass1)         # P(w|c0) * P(c0) ,即贝叶斯准则的分子·
    if p1 > p0:
        return 1
    else:
        return 0


def testingNB():
    listOPosts,listClasses = loadDataSet()                                     # 1. 加载数据集
    myVocabList = createVocabList(listOPosts)                                  # 2. 创建单词集合
    trainMat = []                                                              # 3. 计算单词是否出现并创建数据矩阵
    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList,postinDoc))                 # 返回m*len(myVocabList)的矩阵, 记录的都是0,1信息
    p0V,p1V,pAb = trainNb0(np.array(trainMat),np.array(listClasses))           # 4. 训练数据

    # 5. 测试数据
    testEntry = ['love','my','dalmation']
    thisDoc = np.array(setOfWords2Vec(myVocabList,testEntry))
    print(testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb))

    testEntry = ['stupid','garbage']
    thisDoc = np.array(setOfWords2Vec(myVocabList,testEntry))
    print(testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb))

这里需要补充一点,上面也提到了关于如何选取文档特征的方法,上面用到的是词集模型,即对于一篇文档,将文档中是否出现某一词条作为特征,即特征只能为0不出现或者1出现;然后,一篇文档中词条的出现次数也可能具有重要的信息,于是我们可以采用词袋模型,在词袋向量中每个词可以出现多次,这样,在将文档转为向量时,每当遇到一个单词时,它会增加词向量中的对应值

具体将文档转为词袋向量的代码为:

def bagOfWords2VecMN(vocabList,inputSet):
    #词袋向量
    returnVec=[0]*len(vocabList)
    for word in inputSet:
        if word in vocabList:
            #某词每出现一次,次数加1
            returnVec[vocabList.index(word)]+=1
    return returnVec

 四 使用朴素贝叶斯过滤垃圾邮件

"""
使用朴素贝叶斯过滤垃圾邮件
"""

def textParse(bigString):
    """
    Desc:
        接收一个大字符串并将其解析为字符串列表
    Args:
        bigString -- 大字符串
    Returns:
        去掉少于 2 个字符的字符串,并将所有字符串转换为小写,返回字符串列表
    """
    import re
    # 使用正则表达式来切分句子,其中分隔符是除单词、数字外的任意字符串
    listOfTokens = re.split(r'\W*', bigString)
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]

def spamTest():
    """
    Desc:
        对贝叶斯垃圾邮件分类器进行自动化处理。
    Args:
        none
    Returns:
        对测试集中的每封邮件进行分类,若邮件分类错误,则错误数加 1,最后返回总的错误百分比。
    """
    docList = []
    classList = []
    fullText = []
    for i in range(1,26):                                               #遍历25个txt文件
        # 切分,解析数据,并归类为 1 类别
        wordList = textParse(open('email/spam/%d.txt' % i,'r' ).read())     #读取每个垃圾邮件,并字符串转换成字符串列表
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(1)

        # 切分,解析数据,并归类为 0 类别
        wordList = textParse(open('email/ham/%d.txt' % i,'r' ).read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)
    # 创建词汇表
    vocabList = createVocabList(docList)
    trainingSet = list(range(50)); testSet = []                              #创建存储训练集的索引值的列表和测试集的索引值的列表
    # 随机取 10 个邮件用来测试
    for i in range(10):
        randIndex = int(np.random.uniform(0,len(trainingSet)))         # random.uniform(x, y) 随机生成一个范围为 x ~ y 的实数
        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 = trainNB0(np.array(trainMat),np.array(trainClasses))
    errorCount = 0.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("the error rate is: " , float(errorCount) / len(testSet))

五 总结

朴素贝叶斯推断的一些优点:

  • 生成式模型,通过计算概率来进行分类,可以用来处理多分类问题。
  • 对小规模的数据表现很好,适合多分类任务,适合增量式训练,算法也比较简单。

朴素贝叶斯推断的一些缺点:

  • 对输入数据的表达形式很敏感。
  • 由于朴素贝叶斯的“朴素”特点,所以会带来一些准确率上的损失。
  • 需要计算先验概率,分类决策存在错误率。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值