Naive Bayes Classifier详解(附带概率论公式推导)
Naive Bayes Classifier详解
第八次写博客,本人数学基础不是太好,如果有幸能得到读者指正,感激不尽,希望能借此机会向大家学习。这一篇的内容来自于各种书籍和网上资料,以及自己的一些见解。
预备知识:
这一部分主要是谈一谈概率论中的相关内容,以及贝叶斯决策论的介绍。
贝叶斯定理(Bayes’ theorem)
假设是一对随机变量,他们的联合概率是指取值为且取值为的概率,条件概率是指一随机变量在另一个随机变量取值已知的情况下取某一特定值的概率。例如,条件概率是指在变量取值为的情况下,变量取值为的概率。和的联合概率和条件概率满足如下关系:
其中()为变量()的后验概率,()为变量()的先验概率。上式还可以写成如下形式,称为贝叶斯定理:
极大似然估计
假设条件概率服从某一确定的概率分布模型,且该模型由参数唯一确定。为了确定的值,我们假设该参数的预测值为,则存在条件概率,可以定量的评价预测值与实际值的符合程度。
实际上概率分布模型的训练过程,就是参数估计过程。对于参数估计,统计学界的两个学派扥别提出了不同的解决方案:频率主义学派认为参数虽然未知,但却是客观存在的“固定值”,因此,可以通过优化似然函数等准则来确定参数值;贝叶斯学派则认为参数是未观察到的“随机变量”,其本身也可以符合某种特殊的分布,因此,可以假设参数服从某个先验分布,然后基于观测到的数据来计算参数的后验分布。下面对源自频率主义学派的极大似然估计(Maximum Likelihood Estimation,简称MLE)进行介绍,这是根据数据采样来估计概率分布参数的经典方法。
令表示数据集中,随机变量取值为的样本组成的集合,假设这些样本是独立同分布的,则参数对于数据集的似然是
对参数进行极大似然估计,就是去寻找能最大化似然的参数值。从直观上看,极大似然估计是试图在参数的所有可能的取值中,找到一个可以使数据()出现在集合()中的可能性最大的值。
式(1)中条件概率的联乘操作容易导致下溢,故通常使用对数似然(log-likelihood)
此时参数的极大似然估计为
MLE估计条件概率密度函数
1.连续属性情况下
假设,条件概率密度函数,则参数和的极大似然估计【2】为
这里假定为随机向量,从上式可以看出,通过极大似然法得到的正态分布均值就是样本均值,方差就是的均值。则条件概率密度可以表示为
2.离散属性情况下
对于条件概率,直接根据集合中所占的比例来进行估计。
贝叶斯决策论(Bayesian decision theory)
贝叶斯决策论是概率框架下实施决策的基本方法。对分类任务来说,在所有相关概率都已知的情况下,贝叶斯决策论考虑如何基于这些概率和误判损失来选择最优的类别标记。
假设有种可能的类别标记,即,是将一个真实标记为的样本误分类为所产生的损失。基于后验概率可获得将样本分类为所产生的期望损失(expected lost),即在样本上的“条件风险”(conditional risk)
我们的任务是寻找一个判定准则来最小化总体风险
对于每个样本,若可以最小化条件风险,则总体风险也将被最小化,这就产生了贝叶斯判定准则(Bayes Decision Rule):为了最小化总体风险,只需在每个样本上选择使得条件风险最小的类别标记,即
此时,称为贝叶斯最优分类器(Bayes optimal Classfier),与之对应的总体风险称为贝叶斯风险(Bayes Risk)。反映了分类器所能达到的最好性能,即通过机器学习所能产生的模型精度的理论上限。
当目标是最小化分类错误率,则误判损失可以写为
此时条件风险为
因此,根据式(2)可以得到最小化分类错误率的贝叶斯最优分类器为
即对每个样本,选择能够使后验概率最大的类别标记。
由上式看出,如果要使用贝叶斯判定准则来最小化决策风险,首先要计算后验概率,因此,从这个角度来看,机器学习所要实现的是基于有限的训练样本集,尽可能准确的估计出。主要有两种策略:给定,可以通过直接对进行建模来预测,这样得到的是“判别式模型”(discriminative model);也可以先对联合概率建模,然后在由此获得,这样得到的是“生成式模型”(generative model)。显然,决策树、BP神经网络、支持向量机等,都可以归入判别式模型的范畴。
对生成式模型来件,必须要考虑的是
基于贝叶斯定理【1】,上式可写为
其中是类先验概率,是样本相对于类标记的后验概率(或称“似然”),是用于归一化的“证据因子”(evidence)。对给定的样本,证据因子与类标记无关,因此估计的问题就转化为如何根据训练集来估计类先验概率和后验概率。
其中,类先验概率可以根据跟类样本在中出现的频率来进行估计。然而,对于后验概率,当为一个含有多个属性的属性向量时,就要估计关于所有属性的联合概率,如果直接通过每种属性值组合在中出现的频率来估计,那么计算量将非常巨大,例如中含有个二值的离散属性,那么有个频率值需要计算。
推导过程
主要分为六部分:类先验概率的估计、后验概率的估计、朴素贝叶斯表达式的推导、用于应对特殊情况的平滑处理、贝叶斯误差率和分类边界以及朴素贝叶斯的应用。
类先验概率的估计
由贝叶斯决策论【4】可知,当有充足的独立同分布样本时,可以根据跟类样本在中出现的频率来容易地估计出类先验概率
其中,为集合中样本点的个数,为集合中样本点的个数,为集合中所有类别标记为的样本点组成的集合。
后验概率的估计
由贝叶斯决策论【4】可知,根据式(4)来估计后验概率的主要困难在于后验概率的估计,由于是所有属性上的联合概率,难以从有限的训练样本中直接估计得出。为了避开这个障碍,朴素贝叶斯分类器(naive Bayes classifier)采用了“属性条件独立性假设”,即对已知类别,假设所有属性相互独立。换言之,假设每个属性独立的对分类结果产生影响。可以得到下式,
其中,为属性个数,为属性向量的第个属性值,是类型标记为时,出现的概率,可以通过下式计算得到,
其中,为类别标记为的集合中,所有第个属性取值为的样本点构成的集合。
当属性为连续型,且假定其条件概率密度时,其中和分别为第类样本在第个属性上的均值和方差,根据极大似然法【3】对其进行估计后,可以得到
表达式的推导
根据条件独立性假设,式(4)可以重新写做
其中,为属性个数,为属性向量的第个属性值,而条件概率度可以通过式(6)或(7)来进行计算。由于对所有类别来说相同,因此基于贝叶斯决策论【4】的贝叶斯判定准则为
这就是朴素贝叶斯分类器的表达式。
平滑处理
需要注意的是,若某个属性值在训练集中没有与某个类同时出现过,则直接基于式(6)进行概率估计,再根据式(8)进行判别将出现问题。例如,当属性值没有与类同时出现过,那么无论该样本的其他属性是什么,哪怕在其他属性上明显属于类(例如,),分类结果都将拒绝划分到中,这显然是不合理的。
因此,为例避免其他属性携带的信息被训练集中从未出现过的属性值“抹去”,在估计概率值时通常要进行“平滑处理”(smoothing),常用“拉普拉斯修正”(Laplacian correction)。具体来说,令表示训练集中可能的类别数,表示第个属性可能的取值数,则式(5)和式(6)分别修正为
显然,拉普拉斯修正避免了因训练样本不充分而导致概率估值为零的问题,并且在训练集变大时,修正过程所引入的先验的影响也会逐渐变得可忽略,使得估值逐渐趋向于实际概率值。
平滑处理的另一种方法是使用m估计(m-estimate),即
其中是由用户指定的参数,当(没有类别标记为的训练样本)时,即类别标记为的样本中,第个属性取值为的先验概率;称为“等价样本大小的参数”,它决定了先验概率与观测概率之间的平衡。当时,估计退化为“拉普拉斯修正”中的条件概率估计式。
贝叶斯误差率和分类边界
假设后验概率的真实概率分布已知,我们就可以确定分类任务的理想决策边界。例如,考虑任务:根据体长区分美洲鳄和鳄鱼。一条成年鳄鱼的平均体长大约15英尺,而一条成年美洲鳄的体长大约12英尺。假设它们的体长服从标准差为2英尺的高斯分布,那么二者的类条件概率表示如下:
下图给出了鳄鱼和美洲鳄条件概率的比较(书上的图画的比较好,我这个是简化版,凑活着看吧,大体意思差不多)
假设它们的先验概率相同,理想决策边界满足:
根据上面给出的条件概率公式,可以得到
解得。该例的决策边界处在两个均值的中点。
当先验概率不同时,决策边界向先验概率较小的类移动。此外,给定数据上的任何分类器所达到的最小误差都是可计算的。上例中的理想决策边界把体长小于的分类为美洲鳄,把体长大于的分类为鳄鱼。该分类器的误差率等于鳄鱼的后验概率曲线下面的区域(0到)加上美洲鳄后验概率曲线下面的区域(从到):
总误差率称为贝叶斯误差率(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()
算法效果
上图是通过40个训练样本训练出来的朴素贝叶斯分类器对10个测试样本进行分类的实例,由于词汇列表中每个词对应的属性具有偏斜性质,即这些词同时在一封邮件中出现的概率很低,以词集模型举例,词汇列表中出现的词往往只占列表中的一小部分。因此,该代码并没有计算每一类中每个词没有出现的后验概率,而是仅仅通过他们出现的后验概率来对新邮件进行预测。