Chapter4 朴素贝叶斯

发布时间 2023-04-22 12:48:52作者: gao79138

朴素贝叶斯


1. 简介

    朴素贝叶斯是一种基于概率论的分类方法。它主要借助条件概率和贝叶斯公式来对样本进行分类。

2. 优缺点

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

3. 条件概率与贝叶斯公式

    在介绍朴素贝叶斯之前,我们需要了解一下条件概率与贝叶斯公式。先看下面的案例:

img

    根据古典概型:
    灰色石头的概率:3/7
    黑色石头的概率:4/7
    再看一组案例:

img

    那么,我们需要计算两个问题:
        1. A桶中,灰球的概率是多少?
        2. B桶中,灰球的概率是多少?
    根据上图,我们不难得到:
        1. 2/4
        2. 1/3
    因此,上述两个问题的概率就是条件概率。用公式表示:
        1. P(gray|bucketA)
        2. P(gray|bucketB)
    接下来,我们给出条件概率的计算公式: 

img

    我们来验证一下上述公式:
        根据上图,我们可以知道P(gray and bucketB)的结果为1/7,P(bucketB)的结果为3/7,因此上述公式的概率为1/3,跟上述结果是相等的。
    接下来,我们计算一下这个概率:
        已知一个球是灰球,求其在B桶的概率,即P(bucketB|gray)。
        上述的这个条件概率就是贝叶斯公式。因此,贝叶斯公式是另外一种计算条件概率的方法。将上面的条件概率进行展开:
        P(bucketB|gray) = P(gray and bucketB) / P(gray)
        最后计算的结果是1/3。
如果已知P(x|c),想求P(c|x),那么可以采用下面的计算方法(贝叶斯公式)

img

4. 使用条件概率与贝叶斯公式来进行分类

    我们先给出需要进行分类的贝叶斯公式:

img

    其中上述公式的i代表类别数
    使用上述两个公式进行分类的准则如下:

    给定两个条件概率:P(c1|x,y)和P(c2|x,y),现在需要判断点(x,y)属于哪个类别。
        If P(c1|x,y) > P(c2|x,y),则(x,y)点属于类别c1
        If P(c1|x,y) < P(c2|x,y),则(x,y)点属于类别c2
    相信你看到这里,肯定是满头雾水。别着急,我们将上述的公式抽象成实际问题。
    假设,我们有一组训练样本(特征和类别已知)和测试样本(特征已知,类别未知)。样本的类别为ci。样本的特征有两种,均为x和y,我们可以抽象成点(x,y)。
    那么,通过训练样本,对于以下三种概率,我们肯定是已知的。
        1. 点(x,y)的概率,即特征取值分别为x和y的概率
        2. ci的概率,即类别ci在训练样本中的概率
        3. 在类别为ci的条件下,特征取值为x和y的概率。即条件概率P(x,y|ci)。
    由于在测试样本中,类别是未知的。我们需要通过某一个点来反推该点属于某一类别的概率。即,特征取值为x和y,该点(x,y)属于某个类别的概率?即P(ci|x,y)。
    因此,这就是为什么要采用贝叶斯公式来进行样本分类。

4. 朴素贝叶斯的一般过程

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

5. 使用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']]
    # 代表每个文档所对应的分类(0代表正常言论,1代表侮辱性言论)
    classVec = [0,1,0,1,0,1]

    return postingList,classVec
# 该函数的作用就是根据输入的训练集(文档集合),生成所有文档中所出现的词条列表
# 之后可根据该词条列表来构建出每一个文档所对应的词条向量,进而利用朴素贝叶斯来计算条件概率
# 该函数位于bayes.py文件中

def createVocabList(dataSet):
    # 创建集合的目的就是所输入的文档集合不可避免的会出现一些重复的词条,因此我们需要进行去重。
    vocabSet = set()                # 创建一个空集合,代表词条列表
    for document in dataSet:
        vocabSet = vocabSet | set(document)         # 将文档中出现的词条去重之后,添加到词条列表中。 | 代表求并集
    return list(vocabSet)           # 返回词条列表,以列表的形式
# 该函数的作用就是根据词条列表来返回每一个文档所对应的词条向量
# 函数的两个参数分别为:词条列表、输入文档(划分完词条后)
# 该函数位于bayes.py文件中

def setOfWords2Vec(vocabList,inputSet):
    returnVec = [0]*len(vocabList)          # 生成该文档的词条向量,该词条向量的长度与词条列表相同。并初始化为0
                                            # 其中词条向量中的某个词条为0代表该词条未出现在该词条列表中,如果出现则为1
    for word in inputSet:                   # 遍历每一个词条
        if word in vocabList:               # 如果该词条出现在了词条列表中,
            returnVec[vocabList.index(word)] = 1    # 则词条向量中的对应词条置为1
        else:
            print("the word %s is not in my Vocabulary" % word)     # 输出
    return returnVec                        # 返回该文档所对应的词条向量
# 该函数的作用是用于测试
# 该函数位于personalTest.py文件中

import baye1

listOPosts,listClasses = baye1.loadDataSet()    # 加载训练集
myVocabList = baye1.createVocabList(listOPosts) # 生成词条列表
print(myVocabList)
'''
['stupid', 'buying', 'not', 'help', 'flea', 'please', 'ate', 
'licks', 'so', 'food', 'love', 'to', 'has', 'cute', 'mr',
 'is', 'posting', 'maybe', 'dalmation', 'park', 'how', 
 'problems', 'him', 'garbage', 'worthless', 'stop', 
 'steak', 'I', 'my', 'take', 'quit', 'dog']
'''
# 注意: 词条向量里面的每一个词条都是该文档(样本)的特征
# 只不过这里的特征只用0和1表示(0代表未出现,1代表出现)
print(baye1.setOfWords2Vec(myVocabList,listOPosts[0]))          # 输出第一个文档所对应的词条向量
'''
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0]

'''
print(baye1.setOfWords2Vec(myVocabList,listOPosts[3]))          # 输出第四个文档所对应的词条向量
'''
[0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
'''
    获取到了每个文档的词条向量之后,我们接下来需要利用它来计算条件概率。

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

    由于本案例当中特征数量很多,因此可将上述的贝叶斯公式更改一下,我们将使用更改完之后的贝叶斯公式来进行分类。(其中w代表测试样本的词条向量)

img

# 该函数的作用就是用于计算某个类别的概率以及以该类别为条件下每个特征(词条)的条件概率
# 注意:这里使用词条是否出现来当作特征,一个词条出现与否,与另一个词条出现与否无关,因此特征之间是相互独立的
# 因此P(w0,w1,...,wn|ci) = P(w0|ci)P(w1|ci)...P(wn|ci)
# 所以,该函数依照上述的条件概率公式来进行计算
# 该函数位于bayes.py文件中

# trainMatrix代表训练矩阵(里面存储的是每个文档所对应的词条向量)
# trainCategory存储的是每个文档所对应的类别
def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix)             # 训练文档的总数量
    numWords = len(trainMatrix[0])              # 词条数量(特征数量)(单词数量)
    # 这里需要注意:sum(trainCategory)代表文档属于侮辱性文档的数量
    # 由于本案例属于二类分类问题(侮辱性文档/非侮辱性文档),因此侮辱性文档与非侮辱性文档之间是相互对立的。
    # 因此非侮辱性文档的概率 = 1 - 侮辱性文档的概率
    pAbusive = sum(trainCategory) / float(numTrainDocs)     # 计算文档属于侮辱性文档的概率
    p0Num = zeros(numWords)                     # 代表记录属于类别0的每个单词出现数量的数组(初始化为0)
    p1Num = zeros(numWords)                     # 代表记录属于类别1的每个单词出现数量的数组(初始化为0)
    p0Denom = 0.0                               # 代表属于类别0的单词总数量
    p1Denom = 0.0                               # 代表属于类别1的单词总数量
    # 如何计算在某类别条件下某特征的概率?
    # P(wi|ci) = 某类别条件下该特征的总数量/某类别条件下的单词总数量
    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 = p1Num / p1Denom                    # p1Vect代表在类别1条件下各个特征的条件概率
    p0Vect = p0Num / p0Denom                    # 类别0
    return p0Vect,p1Vect,pAbusive
# 该函数的作用是用于测试
# 该函数位于personalTest.py文件中

import baye1

listOPosts,listClasses = baye1.loadDataSet()
myVocabList = baye1.createVocabList(listOPosts)
print(myVocabList)

trainMat = []

for postinDoc in listOPosts:
    trainMat.append(baye1.setOfWords2Vec(myVocabList,postinDoc))

p0V,p1V,pAb = baye1.trainNB0(trainMat,listClasses)

print(p0V)              # 类别0条件下各个特征的条件概率
print(p1V)              # 类别1
print(pAb)              # 文档为侮辱性文档的概率

'''
['my', 'park', 'take', 'mr', 'stupid', 'to', 'not', 'steak', 'is', 'how', 'I', 'ate', 'help', 'dog', 'worthless', 'stop', 'has', 'maybe', 'garbage', 'dalmation', 'food', 'posting', 'love', 'licks', 'quit', 'flea', 'cute', 'him', 'please', 'so', 'problems', 'buying']
[0.125      0.         0.         0.04166667 0.         0.04166667
 0.         0.04166667 0.04166667 0.04166667 0.04166667 0.04166667
 0.04166667 0.04166667 0.         0.04166667 0.04166667 0.
 0.         0.04166667 0.         0.         0.04166667 0.04166667
 0.         0.04166667 0.04166667 0.08333333 0.04166667 0.04166667
 0.04166667 0.        ]
[0.         0.05263158 0.05263158 0.         0.15789474 0.05263158
 0.05263158 0.         0.         0.         0.         0.
 0.         0.10526316 0.10526316 0.05263158 0.         0.05263158
 0.05263158 0.         0.05263158 0.05263158 0.         0.
 0.05263158 0.         0.         0.05263158 0.         0.
 0.         0.05263158]
0.5

'''
'''
    通过观察发现,类别1当中的第5个单词的条件概率最大,经查看单词列表发现这个单词为stupid,因此该单词是侮辱性文档中最具有代表性的单词。
'''

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

    首先,上述算法会出现两个问题:
        1. 在条件概率相乘的过程,如果某个条件概率为0,那么总体概率就为0。这显然是不合理的。
        2. 每个条件概率都是不大于1的小数,当条件概率相乘时可能会出现下溢出,进而导致计算的结果不正确。
    因此,所提出的两个解决办法就是:
        1. 将某个类别条件下每个词的出现次数初始化为1,将某个类别条件下所有词的出现总数初始化为2。
        2. 为了避免下溢出问题,我们可以将计算的概率取自然对数。这里需要引进一个公式:ln(ab) = lna + lnb
    所以,我们需要将trainNB0这个函数进行一下修改。
p0Num = ones(numWords)                     # 代表记录属于类别0的每个单词出现数量的数组(初始化为0)
p1Num = ones(numWords)                     # 代表记录属于类别1的每个单词出现数量的数组(初始化为0)
p0Denom = 2.0                               # 代表属于类别0的单词总数量
p1Denom = 2.0                               # 代表属于类别1的单词总数量


p1Vect = log(p1Num / p1Denom)                    # p1Vect代表在类别1条件下各个特征的条件概率
p0Vect = log(p0Num / p0Denom)                    # 类别0
    解决了以上问题之后,我们就可以编写朴素贝叶斯的分类函数了。
# 该函数的作用是利用贝叶斯公式来进行分类
# 该函数有四个参数,第一个代表测试样本的词条向量,我们可以根据该向量
# 以及之后的p0Vec和p1Vec来获得在某些类别下该测试样本中每个特征的条件概率
# p0Vec代表在类别为0条件下,每个特征的条件概率
# p1Vec代表在类别为1条件下,每个特征的条件概率
# pClass1代表类别为1的概率
# 该函数使用了上述的贝叶斯公式来进行分类
# 该函数位于bayes.py文件中
def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):
    # 计算在类别为1的条件下,通过贝叶斯公式计算的条件概率
    # 这里需要注意的是,p1Vec中的条件概率已经取了自然对数,每个独立特征的条件概率相乘取自然对数本质上就是自然对数相加的过程
    # ln(ab) = lna + lnb
    # 根据上述的贝叶斯公式,这里为什么没有除以测试样本特征的概率即p(w)?
    # 因为该测试样本只有一个,因此该测试样本特征的概率为1。
    p1 = sum(vec2Classify*p1Vec) + log(pClass1)
    # 计算在类别为0的条件下,通过贝叶斯公式计算的条件概率
    p0 = sum(vec2Classify*p0Vec) + log(1-pClass1)
    if(p1 > p0):                # 如果该测试样本属于类别1的概率大于类别0
        return 1                # 将该样本预测为类别1
    else:
        return 0                # 类别0
# 该函数的作用是用于测试朴素贝叶斯算法
# 该函数位于bayes.py文件中

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))
# 该函数的作用是用于测试
# 该函数位于personalTest.py文件中

import baye1

baye1.testingNB()

'''
['love', 'my', 'dalmation'] classified as: 0
['stupid', 'garbage'] classified as: 1
'''
    最后需要补充的一点就是:上述案例将每个词的出现与否作为特征,这可以描述为词集模型。如果将每个词的出现次数作为特征,这就称为是词袋模型。