欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Naive Bayes Classifier详解(附带概率论公式推导)

程序员文章站 2022-05-30 19:35:30
...

Naive Bayes Classifier详解

第八次写博客,本人数学基础不是太好,如果有幸能得到读者指正,感激不尽,希望能借此机会向大家学习。这一篇的内容来自于各种书籍和网上资料,以及自己的一些见解。

预备知识:

这一部分主要是谈一谈概率论中的相关内容,以及贝叶斯决策论的介绍。

贝叶斯定理(Bayes’ theorem)

  假设X,YX,Y是一对随机变量,他们的联合概率P(Y=y,X=x)P\left(Y=y,X=x\right)是指XX取值为xxYY取值为yy的概率,条件概率是指一随机变量在另一个随机变量取值已知的情况下取某一特定值的概率。例如,条件概率P(Y=yX=x)P\left(Y=y|X=x\right)是指在变量XX取值为xx的情况下,变量YY取值为yy的概率。XXYY的联合概率和条件概率满足如下关系:

Naive Bayes Classifier详解(附带概率论公式推导)

其中P(XY)P\left(X|Y\right)P(YX)P\left(Y|X\right))为变量XXYY)的后验概率,P(X)P\left(X\right)P(Y)P\left(Y\right))为变量XXYY)的先验概率。上式还可以写成如下形式,称为贝叶斯定理:

Naive Bayes Classifier详解(附带概率论公式推导)

极大似然估计

  假设条件概率P(XY=y)P\left(X|Y=y\right)服从某一确定的概率分布模型,且该模型由参数θ\theta唯一确定。为了确定θ\theta的值,我们假设该参数的预测值为θ^\hat{\theta},则存在条件概率P(Xθ^)P\left(X|\hat{\theta}\right),可以定量的评价预测值与实际值的符合程度。
  实际上概率分布模型的训练过程,就是参数估计过程。对于参数估计,统计学界的两个学派扥别提出了不同的解决方案:频率主义学派认为参数虽然未知,但却是客观存在的“固定值”,因此,可以通过优化似然函数等准则来确定参数值;贝叶斯学派则认为参数是未观察到的“随机变量”,其本身也可以符合某种特殊的分布,因此,可以假设参数服从某个先验分布,然后基于观测到的数据来计算参数的后验分布。下面对源自频率主义学派的极大似然估计(Maximum Likelihood Estimation,简称MLE)进行介绍,这是根据数据采样来估计概率分布参数的经典方法。
  令DyD_y表示数据集DD中,随机变量YY取值为yy的样本组成的集合,假设这些样本是独立同分布的,则参数θ\theta对于数据集DyD_y的似然是

Naive Bayes Classifier详解(附带概率论公式推导)

  对参数θ\theta进行极大似然估计,就是去寻找能最大化似然P(Dyθ)P\left(D_y|\theta\right)的参数值θ^\hat{\theta}。从直观上看,极大似然估计是试图在参数θ\theta的所有可能的取值中,找到一个可以使数据xxxDyx\in{D_y})出现在集合(DyD_y)中的可能性最大的值。
  式(1)中条件概率的联乘操作容易导致下溢,故通常使用对数似然(log-likelihood)

Naive Bayes Classifier详解(附带概率论公式推导)

此时参数θ\theta的极大似然估计为

Naive Bayes Classifier详解(附带概率论公式推导)

MLE估计条件概率密度函数

1.连续属性情况下
  假设,条件概率密度函数P(XY=y)N(μc,σc2)P\left(X|Y=y\right){\thicksim}N\left(\mu_c,\sigma_c^2\right),则参数μc\mu_cσc\sigma_c的极大似然估计【2】为

Naive Bayes Classifier详解(附带概率论公式推导)

  这里假定xx为随机向量,从上式可以看出,通过极大似然法得到的正态分布均值就是样本均值,方差就是(xμc)(xμc)T\left(x-\mu_c\right)\left(x-\mu_c\right)^T的均值。则条件概率密度可以表示为

Naive Bayes Classifier详解(附带概率论公式推导)

2.离散属性情况下
  对于条件概率P(X=xY=y)P\left(X=x|Y=y\right),直接根据集合{(x,y)Y=y,xX}\{\left(x,y\right)|Y=y,\forall{x\in{X}}\}{(x,y)Y=y,X=x}\{\left(x,y\right)|Y=y,X=x\}所占的比例来进行估计。

贝叶斯决策论(Bayesian decision theory)

  贝叶斯决策论是概率框架下实施决策的基本方法。对分类任务来说,在所有相关概率都已知的情况下,贝叶斯决策论考虑如何基于这些概率和误判损失来选择最优的类别标记。
  假设有NN种可能的类别标记,即y={c1,c2,...,cN}y=\{c_1,c_2,...,c_N\}λij\lambda_{ij}是将一个真实标记为cjc_j的样本误分类为cic_i所产生的损失。基于后验概率P(cix)P\left(c_i|x\right)可获得将样本xx分类为cic_i所产生的期望损失(expected lost),即在样本xx上的“条件风险”(conditional risk)

Naive Bayes Classifier详解(附带概率论公式推导)

  我们的任务是寻找一个判定准则h:xyh:x\mapsto{y}来最小化总体风险

Naive Bayes Classifier详解(附带概率论公式推导)

  对于每个样本xx,若hh可以最小化条件风险R(h(x)x)R\left(h\left(x\right)|x\right),则总体风险R(h)R\left(h\right)也将被最小化,这就产生了贝叶斯判定准则(Bayes Decision Rule):为了最小化总体风险,只需在每个样本上选择使得条件风险R(cx)R\left(c|x\right)最小的类别标记,即

Naive Bayes Classifier详解(附带概率论公式推导)

  此时,h(x)h^*\left(x\right)称为贝叶斯最优分类器(Bayes optimal Classfier),与之对应的总体风险R(h)R\left(h^*\right)称为贝叶斯风险(Bayes Risk)。1R(h)1-R\left(h^*\right)反映了分类器所能达到的最好性能,即通过机器学习所能产生的模型精度的理论上限。
当目标是最小化分类错误率,则误判损失λij\lambda_{ij}可以写为

Naive Bayes Classifier详解(附带概率论公式推导)

此时条件风险为

Naive Bayes Classifier详解(附带概率论公式推导)

  因此,根据式(2)可以得到最小化分类错误率的贝叶斯最优分类器为

Naive Bayes Classifier详解(附带概率论公式推导)

即对每个样本xx,选择能够使后验概率P(cx)P\left(c|x\right)最大的类别标记。
  由上式看出,如果要使用贝叶斯判定准则来最小化决策风险,首先要计算后验概率P(cx)P\left(c|x\right),因此,从这个角度来看,机器学习所要实现的是基于有限的训练样本集,尽可能准确的估计出P(cx)P\left(c|x\right)。主要有两种策略:给定xx,可以通过直接对P(cx)P\left(c|x\right)进行建模来预测cc,这样得到的是“判别式模型”(discriminative model);也可以先对联合概率P(x,c)P\left(x,c\right)建模,然后在由此获得P(cx)P\left(c|x\right),这样得到的是“生成式模型”(generative model)。显然,决策树、BP神经网络、支持向量机等,都可以归入判别式模型的范畴。
  对生成式模型来件,必须要考虑的是

Naive Bayes Classifier详解(附带概率论公式推导)

  基于贝叶斯定理【1】,上式可写为

Naive Bayes Classifier详解(附带概率论公式推导)

  其中P(c)P\left(c\right)是类先验概率,P(xc)P\left(x|c\right)是样本xx相对于类标记cc的后验概率(或称“似然”),P(x)P\left(x\right)是用于归一化的“证据因子”(evidence)。对给定的样本xx,证据因子P(x)P\left(x\right)与类标记无关,因此P(cx)P\left(c|x\right)估计的问题就转化为如何根据训练集来估计类先验概率P(c)P\left(c\right)和后验概率P(xc)P\left(x|c\right)
  其中,类先验概率P(c)P\left(c\right)可以根据跟类样本在DD中出现的频率来进行估计。然而,对于后验概率P(xc)P\left(x|c\right),当xx为一个含有多个属性的属性向量时,就要估计关于xx所有属性的联合概率,如果直接通过每种属性值组合在DD中出现的频率来估计,那么计算量将非常巨大,例如xx中含有dd个二值的离散属性,那么有2d2^d个频率值需要计算。

推导过程

主要分为六部分:类先验概率P(c)P\left(c\right)的估计、后验概率P(xc)P\left(x|c\right)的估计、朴素贝叶斯表达式的推导、用于应对特殊情况的平滑处理、贝叶斯误差率和分类边界以及朴素贝叶斯的应用。

类先验概率P(c)P\left(c\right)的估计

  由贝叶斯决策论【4】可知,当有充足的独立同分布样本时,可以根据跟类样本在DD中出现的频率来容易地估计出类先验概率

Naive Bayes Classifier详解(附带概率论公式推导)

其中,D|D|为集合DD中样本点的个数,Dc|D_c|为集合DcD_c中样本点的个数,DcD_c为集合DD中所有类别标记为cc的样本点组成的集合。

后验概率P(xc)P\left(x|c\right)的估计

  由贝叶斯决策论【4】可知,根据式(4)来估计后验概率P(cx)P\left(c|x\right)的主要困难在于后验概率P(xc)P\left(x|c\right)的估计,由于P(xc)P\left(x|c\right)是所有属性上的联合概率,难以从有限的训练样本中直接估计得出。为了避开这个障碍,朴素贝叶斯分类器(naive Bayes classifier)采用了“属性条件独立性假设”,即对已知类别,假设所有属性相互独立。换言之,假设每个属性独立的对分类结果产生影响。可以得到下式,

Naive Bayes Classifier详解(附带概率论公式推导)

其中,dd为属性个数,xix_i为属性向量xx的第ii个属性值,P(xic)P\left(x_i|c\right)是类型标记为cc时,xix_i出现的概率,可以通过下式计算得到,

Naive Bayes Classifier详解(附带概率论公式推导)

其中,Dc,xiD_{c,x_i}为类别标记为cc的集合中,所有第ii个属性取值为xix_i的样本点构成的集合。
  当属性为连续型,且假定其条件概率密度P(xic)N(μc,i,σc,i2)P\left(x_i|c\right){\thicksim}N\left(\mu_{c,i},\sigma_{c,i}^2\right)时,其中μc,i\mu_{c,i}σc,i\sigma_{c,i}分别为第cc类样本在第ii个属性上的均值和方差,根据极大似然法【3】对其进行估计后,可以得到

Naive Bayes Classifier详解(附带概率论公式推导)

表达式的推导

  根据条件独立性假设,式(4)可以重新写做

Naive Bayes Classifier详解(附带概率论公式推导)

其中,dd为属性个数,xix_i为属性向量xx的第ii个属性值,而条件概率度P(xic)P\left(x_i|c\right)可以通过式(6)或(7)来进行计算。由于对所有类别来说P(x)P\left(x\right)相同,因此基于贝叶斯决策论【4】的贝叶斯判定准则为

Naive Bayes Classifier详解(附带概率论公式推导)

这就是朴素贝叶斯分类器的表达式。

平滑处理

  需要注意的是,若某个属性值在训练集中没有与某个类同时出现过,则直接基于式(6)进行概率估计,再根据式(8)进行判别将出现问题。例如,当属性值xkx_k没有与类cc同时出现过,那么无论该样本的其他属性是什么,哪怕在其他属性上明显属于类cc(例如,P(xic)=1,ikP\left(x_i|c\right)=1,i\neq{k}),分类结果都将拒绝划分到cc中,这显然是不合理的。
  因此,为例避免其他属性携带的信息被训练集中从未出现过的属性值“抹去”,在估计概率值时通常要进行“平滑处理”(smoothing),常用“拉普拉斯修正”(Laplacian correction)。具体来说,令NN表示训练集DD中可能的类别数,NiN_i表示第ii个属性可能的取值数,则式(5)和式(6)分别修正为

Naive Bayes Classifier详解(附带概率论公式推导)

  显然,拉普拉斯修正避免了因训练样本不充分而导致概率估值为零的问题,并且在训练集变大时,修正过程所引入的先验的影响也会逐渐变得可忽略,使得估值逐渐趋向于实际概率值。
  平滑处理的另一种方法是使用m估计(m-estimate),即

Naive Bayes Classifier详解(附带概率论公式推导)

其中pp是由用户指定的参数,当Dc=0D_c=0(没有类别标记为cc的训练样本)时,p^(xic)=p\hat{p}\left(x_i|c\right)=p即类别标记为cc的样本中,第ii个属性取值为xix_i的先验概率;mm称为“等价样本大小的参数”,它决定了先验概率pp与观测概率Dc,xiDc\frac{|D_{c,x_i}|}{|D_c|}之间的平衡。当m=Ni,p=1Nim=N_i,p=\frac{1}{N_i}时,mm估计退化为“拉普拉斯修正”中的条件概率估计式。

贝叶斯误差率和分类边界

  假设后验概率P(Xc)P\left(X|c\right)的真实概率分布已知,我们就可以确定分类任务的理想决策边界。例如,考虑任务:根据体长区分美洲鳄和鳄鱼。一条成年鳄鱼的平均体长大约15英尺,而一条成年美洲鳄的体长大约12英尺。假设它们的体长xx服从标准差为2英尺的高斯分布,那么二者的类条件概率表示如下:

Naive Bayes Classifier详解(附带概率论公式推导)

  下图给出了鳄鱼和美洲鳄条件概率的比较(书上的图画的比较好,我这个是简化版,凑活着看吧,大体意思差不多)

Naive Bayes Classifier详解(附带概率论公式推导)

  假设它们的先验概率相同,理想决策边界x^\hat{x}满足:

Naive Bayes Classifier详解(附带概率论公式推导)

根据上面给出的条件概率公式,可以得到

Naive Bayes Classifier详解(附带概率论公式推导)

解得x^=13.5\hat{x}=13.5。该例的决策边界处在两个均值的中点。
  当先验概率不同时,决策边界向先验概率较小的类移动。此外,给定数据上的任何分类器所达到的最小误差都是可计算的。上例中的理想决策边界把体长小于x^\hat{x}的分类为美洲鳄,把体长大于x^\hat{x}的分类为鳄鱼。该分类器的误差率等于鳄鱼的后验概率曲线下面的区域(0到x^\hat{x})加上美洲鳄后验概率曲线下面的区域(x^\hat{x}从到\infty):

Naive Bayes Classifier详解(附带概率论公式推导)

总误差率称为贝叶斯误差率(Bayes error rate)。

朴素贝叶斯的应用

  在现实任务中,朴素贝叶斯分类器有多种使用方式。若任务对测试速度要求怀高,则对给定训练集,可将朴素贝叶斯分类器设计的所有概率估值事先计算好存储起来,这样在进行预测时,只需“查表”即可进行判别;若任务数据更替频繁,则可采用“懒惰学习”方式,先不进行任何训练,待收到预测请求时再根据当前数据集进行概率估值;若数据不断增加,则可在现有估值的基础上,仅对新增样本的属性值所涉及的概率估值进计数修正即可实现“增量学习”。


代码实现及对比

下面是我参考《机器学习实战》自己实现的Naive Bayes代码,主要是对垃圾邮件检测中测试集和训练集的划分进行了修改,原代码中数据集虽然进行了划分,但是词汇列表是根据整个数据集生成的,这样虽然不会对预测结果有任何影响,但是随着词汇数增加,会导致计算效率的降低,因此在获取词汇列表之前对数据集进行了划分。

代码细节

"""

@author: Ἥλιος
@CSDN:https://blog.csdn.net/qq_40793975/article/details/81297755

"""
print(__doc__)

import numpy as np
import re
import random


# 加载用于训练模型的数据(邮件)集(标记1是垃圾邮件)
def load_trainSet():
    posting_list = [['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']]
    class_vec = [0, 1, 0, 1, 0, 1]
    return posting_list, class_vec


# 产生词汇列表
def gen_vocabList(posting_list):
    vocabList = set([])
    for posting in posting_list:
        vocabList = vocabList | set(posting)
    vocabList = list(vocabList)
    vocabList.sort()
    return vocabList


# 根据词汇列表产生词集模型
def set_word2vec(wordList, vocabList):
    wordSet = [0] * len(vocabList)
    for word in wordList:
        if word in vocabList:
            wordSet[vocabList.index(word)] = 1
    return wordSet


# 根据词汇列表产生词袋模型
def bag_word2vec(wordList, vocabList):
    wordBag = [0] * len(vocabList)
    for word in wordList:
        if word in vocabList:
            wordBag[vocabList.index(word)] += 1
    return wordBag


# 根据词袋模型训练NB
def trainNB_bag(trainSet, labels):
    vocabList = gen_vocabList(trainSet)
    # print(vocabList)
    m = len(trainSet)
    n = len(vocabList)
    wordSet_matrix = np.mat(np.zeros((m, n)), dtype=np.int32)
    for i in range(m):  # 将训练集转化为词集模型
        wordSet_matrix[i, :] = bag_word2vec(trainSet[i], vocabList)
    label0_index = (1 - np.array(labels)).nonzero()[0].tolist()
    label1_index = np.array(labels).nonzero()[0].tolist()
    num_label0vec = np.sum(wordSet_matrix[label0_index], axis=0)  # 样本标记为0的样本集在词汇列表上对应词的词频数
    num_label1vec = np.sum(wordSet_matrix[label1_index], axis=0)  # 样本标记为1的样本集在词汇列表上对应词的词频数
    num_label0 = np.sum(wordSet_matrix[label0_index])  # 样本标记为0的样本集的所有词频数
    num_label1 = np.sum(wordSet_matrix[label1_index])  # 样本标记为1的样本集的所有词频数
    prob0 = np.log(1 - sum(labels) / m)  # 样本标记为0的先验概率的对数
    prob1 = np.log(sum(labels) / m)  # 样本标记为1的先验概率的对数
    prob0Vec = np.log((num_label0vec + 1) / (num_label0 + 2))  # 引入拉普拉斯修正后的类别0后验概率的对数
    prob1Vec = np.log((num_label1vec + 1) / (num_label1 + 2))  # 引入拉普拉斯修正后的类别1后验概率的对数
    return vocabList, prob0, prob1, prob0Vec, prob1Vec, vocabList


# 根据词集模型训练NB
def trainNB_set(trainSet, labels):
    vocabList = gen_vocabList(trainSet)
    # print(vocabList)
    m = len(trainSet)
    n = len(vocabList)
    wordSet_matrix = np.mat(np.zeros((m, n)), dtype=np.int32)
    for i in range(m):  # 将训练集转化为词集模型
        wordSet_matrix[i, :] = bag_word2vec(trainSet[i], vocabList)
    label0_index = (1 - np.array(labels)).nonzero()[0].tolist()
    label1_index = np.array(labels).nonzero()[0].tolist()
    num_label0vec = np.sum(wordSet_matrix[label0_index], axis=0)  # 样本标记为0的样本集在词汇列表上对应词的词频数
    num_label1vec = np.sum(wordSet_matrix[label1_index], axis=0)  # 样本标记为1的样本集在词汇列表上对应词的词频数
    prob0 = np.log(1 - sum(labels) / m)  # 样本标记为0的先验概率的对数
    prob1 = np.log(sum(labels) / m)  # 样本标记为1的先验概率的对数
    prob0Vec = np.log((num_label0vec + 1) / (m - sum(labels) + 2))  # 引入拉普拉斯修正后的类别0后验概率的对数
    prob1Vec = np.log((num_label1vec + 1) / (sum(labels) + 2))  # 引入拉普拉斯修正后的类别1后验概率的对数
    return vocabList, prob0, prob1, prob0Vec, prob1Vec


# 根据词集模型分类email
def classify_set(email, vocabList, prob0, prob1, prob0Vec, prob1Vec):
    email_word2vec = [0] * len(vocabList)
    for word in email:
        if word in vocabList:
            email_word2vec[vocabList.index(word)] = 1
    prob0_email = prob0 + np.sum(np.mat(email_word2vec) * prob0Vec.T)
    prob1_email = prob1 + np.sum(np.mat(email_word2vec) * prob1Vec.T)
    if prob0_email > prob1_email:
        return 0
    else:
        return 1


# 将文本转化为词列表
def text_parse(big_str):
    list_of_tokens = re.split(r'\W*', big_str)
    return [tok.lower() for tok in list_of_tokens if len(tok) > 2]


# 垃圾邮件分类
def detect_spamEmail():
    trainSet_index = [i+1 for i in range(50)]
    testSet_index = []
    for i in range(10):
        index = int(random.uniform(0, len(trainSet_index)))
        testSet_index.append(trainSet_index[index])
        del(trainSet_index[index])
    trainSet = []
    trainSet_labels = []
    for index in trainSet_index:    # 创建训练集
        if index <= 25:
            trainSet.append(text_parse(open("C:\\Users\\Administrator\\Desktop\\email\\ham\\%d.txt" % index).read()))
            trainSet_labels.append(1)
        else:
            trainSet.append(text_parse(open("C:\\Users\\Administrator\\Desktop\\email\\spam\\%d.txt" % (index-25)).read()))
            trainSet_labels.append(0)
    testSet = []
    testSet_labels = []
    for index in testSet_index:     # 创建测试集
        if index <= 25:
            testSet.append(text_parse(open("C:\\Users\\Administrator\\Desktop\\email\\ham\\%d.txt" % index).read()))
            testSet_labels.append(1)
        else:
            testSet.append(text_parse(open("C:\\Users\\Administrator\\Desktop\\email\\spam\\%d.txt" % (index-25)).read()))
            testSet_labels.append(0)
    vocabList, prob0, prob1, prob0Vec, prob1Vec = trainNB_set(trainSet, trainSet_labels)
    predictLabel = []
    for email in testSet:
        predictLabel.append(classify_set(email, vocabList, prob0, prob1, prob0Vec, prob1Vec))
    print("Pridiction: ", predictLabel)
    print("Real:       ", testSet_labels)
    print("Error rate: ", np.sum(np.array(predictLabel) != np.array(testSet_labels))/len(predictLabel))


detect_spamEmail()

算法效果

Naive Bayes Classifier详解(附带概率论公式推导)

  上图是通过40个训练样本训练出来的朴素贝叶斯分类器对10个测试样本进行分类的实例,由于词汇列表中每个词对应的属性具有偏斜性质,即这些词同时在一封邮件中出现的概率很低,以词集模型举例,词汇列表中出现的词往往只占列表中的一小部分。因此,该代码并没有计算每一类中每个词没有出现的后验概率,而是仅仅通过他们出现的后验概率来对新邮件进行预测。

数据集

email