机器学习—基于概率论的分类方法朴素贝叶斯

文章目录

  • 前言
    • 1、朴素贝叶斯的不同之处
    • 2、基本思想
    • 3、条件概率
    • 4、全概率
  • 基于贝叶斯决策理论的分类方法
    • 1、MAP分类准则
    • 2、贝叶斯公式
    • 3、先验概率
    • 4、后验概率
    • 5、朴素贝叶斯分类器
    • 6、基于朴素贝叶斯分类器的案例
    • 7、拉普拉斯修正
  • 文档分类问题
    • 1、准备数据:从文本中构建词向量
    • 2、训练算法:从词向量计算概率
    • 3、测试算法:根据现实情况修改分类器
  • 过滤垃圾邮件问题
    • 1. 收集数据
    • 2. 准备数据:切分文本
  • 实验总结


前言

在深入了解朴素贝叶斯理论之前,我们先得明白以下几个点:

1、朴素贝叶斯的不同之处

回顾之前所学k-邻近算法和决策树,都是基于“对数据实例属于哪一个明确的类”的这样一个问题,作出艰难的回答。缺点就是会产生错误结果。
而这时我们可以要求分类器给出一个最优的类别结果猜测,同时给出这个猜测的概率估计值。

2、基本思想

先统计某个特征在数据集中取某个特定值的次数,然后除以数据集的实力总数,得到了特征取该值的概率。
利用python的文本处理能力将文档切分成词向量,然后利用词向量对文档分类。

3、条件概率

若只有两个事件A,B,那么,P(A|B) = P(AB)/P(B)。
意思是事件A在另外一个事件B已经发生条件下的发生概率。条件概率表示为P(A|B),读作“在B条件下A的概率”。
机器学习—基于概率论的分类方法朴素贝叶斯_第1张图片
分析:一般事件A和事件B一般是有交集的,若没有交集(互斥),则条件概率为0

用图更能容易的说明上述问题,我们进行某一实验,某一实验所有的可能的样本的结合为Ω(也即穷举实验的所有样本),圆圈A代表事件A所能囊括的所有样本,圆圈B代表事件B所能囊括的所有样本。

由图再来理解一下这个问题:“B已经发生的条件下,A发生的概率”,这句话中,“B已经发生”就相当于已经把样本的可选范围限制在了圆圈B中,其实就等价于这句话:“在圆圈B中,A发生的概率”,显然P(A|B)就等于AB交集中样本的数目/B的样本数目。

4、全概率

如果事件组B1,B2,… 满足
(1)、B1,B2…两两互斥,即 Bi ∩ Bj = ∅ ,i≠j , i,j=1,2,…,且P(Bi)>0,i=1,2,…;
(2)、B1∪B2∪…=Ω ,则称事件组 B1,B2,…是样本空间Ω的一个划分
设 B1,B2,…是样本空间Ω的一个划分,A为任一事件,则:
P(A)=P(A|B1)*P(B1) + P(A|B2)*P(B2) + … + P(A|Bn)*P(Bn).

以下图为例,P(A)= P(A|B1)*P(B1) + P(A|B2)*P(B2) +P(A|B3)*P(B3) + P(A|B4)*P(B4)机器学习—基于概率论的分类方法朴素贝叶斯_第2张图片

基于贝叶斯决策理论的分类方法

1、MAP分类准则

MAP: Maximum A Posterior
x 属于类别 c* 的概率:P(C=c*|X=x)>P(C=c|X=x) , c≠c*, c=c1,c2,…,cL

下面举个例子,已知有一个数据集,它由两类数据组成,如下图:
机器学习—基于概率论的分类方法朴素贝叶斯_第3张图片
我们用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

也就是说,我们会选择高概率对应的类别。这就是贝叶斯决策理论的核心思想,即选择具有最高概率的决策。

如果不用贝叶斯决策理论:
1、使用第1章的 kNN,进行1000次距离计算;
2、使用第2章的决策树,分别沿x轴、y轴划分数据;

kNN算法效率会非常低
决策树在划分数据时会遇到困难,选出最优分类的时候不能很好解决。倘若不止两个轴,那么计算量更是成倍增加。

2、贝叶斯公式

与全概率公式解决的问题相反,贝叶斯公式是建立在条件概率的基础上寻找事件发生的原因(即大事件A已经发生的条件下,分割中的小事件Bi的概率),设B1,B2,…是样本空间Ω的一个划分,则对任一事件A(P(A)>0),有
机器学习—基于概率论的分类方法朴素贝叶斯_第4张图片

上式即为贝叶斯公式(Bayes formula),Bi 常被视为导致试验结果A发生的”原因“,P(Bi)(i=1,2,…)表示各种原因发生的可能性大小,故称先验概率;
P(Bi|A)(i=1,2…)则反映当试验产生了结果A之后,再对各种原因概率的新认识,故称后验概率。

如何更好的理解?看下图(当i=4时,黄色部分):
机器学习—基于概率论的分类方法朴素贝叶斯_第5张图片

3、先验概率

先抛出一个问题:假设要猜测某西瓜的好坏问题,如何判断正反概率?

根据自己购买西瓜的经验,好瓜的概率是p(好瓜)=0.6,坏瓜概率是p(坏瓜)=0.4

则可将p(好瓜)=0.6,p(坏瓜)=0.4作为先验概率,也就是观测新样本前就已知的先验分布p(y)

概念:P(cj)代表还没有训练模型之前,根据历史数据/经验估算cj 拥有的初始概率。P(cj)常被称为cj的先验概率(prior probability) , 它反映了cj的概率分布,该分布独立于样本。 通常可以用样例中属于cj的样例数|cj|比上总样例数|D|来 近似。

4、后验概率

西瓜好坏的概率和其属性有关; 当观测到西瓜样本(属性取值)x时,它为好瓜概率是多少?

p(y=1|x), p(y=0|x)

后验概率:观测到 x 后对结果 y 的估计;

后验概率给定数据样本x时cj成立的概率P(cj | x )被称为后验概率 (posterior probability),
因为它反映了在看到数据样本 x 后 cj成立的置信度。 大部分机器学习模型尝试得到后验概率。

5、朴素贝叶斯分类器

朴素贝叶斯分类器(Naïve Bayes Classifier)采用了“属性条件独立性 假设”,即每个属性独立地对分类结果发生影响。
在这里插入图片描述
其中d为属性数目,Bi为 B 在第i个属性上的取值
对于所有类别来说P(B)都相同,所以只需要计算比较分子即可。
在这里插入图片描述
机器学习—基于概率论的分类方法朴素贝叶斯_第6张图片

6、基于朴素贝叶斯分类器的案例

这个是出自于课堂ppt的案例,但还是希望在博客里复现一遍:

例子:用西瓜数据集训练一个朴素贝叶斯分类器,对下面的测试例进行分类,判断是不是好瓜
在这里插入图片描述
然后我们根据给定的属性值去训练集中找出来相应的数据,计算:
机器学习—基于概率论的分类方法朴素贝叶斯_第7张图片
机器学习—基于概率论的分类方法朴素贝叶斯_第8张图片
注意因为密度不是离散,是连续的,区分:
对比:
机器学习—基于概率论的分类方法朴素贝叶斯_第9张图片

机器学习—基于概率论的分类方法朴素贝叶斯_第10张图片

7、拉普拉斯修正

零概率问题: 在计算事件的概率时,如果某个事件在观察样本库(训练集)中没有出现过,会导致该事件的概率结果是0。这是不合理的,不能因为一个事件没有观察到,就被认为该事件一定不可能发生(即该事件的概率为0)。

拉普拉斯平滑是为了解决零概率的问题。

法国数学家拉普拉斯最早提出用加1的方法,估计没有出现过的现象的概率。拉普拉斯平滑,又叫加一平滑,它对分子划分的计数加1,分母加类别数。
理论假设:假定训练样本很大时,每个分量x的计数加1造成的估计概率变化可以忽略不计,但可以方便有效的避免零概率问题。

文档分类问题

整个文档如电子邮件是一个实例,文档中的某些元素构成特征。我们可以统计文档中出现的词,并用它出现或不出现,出现次数作为特征。这样就可以用一个长度统一,内容是特征的向量来描述文档。

朴素贝叶斯中朴素naive的含义是每个特征互相独立,所以根据乘法原理,总概率为各特征相乘。

Python中为了防止下溢出通常计算log(prob),把相乘操作转换为相加。

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

将文本中的段落句子看成单词向量。从文档中的所有单词帅选出哪些单词纳入词汇表,然后必须要将每一篇文档转换为词汇表上的向量。这里有一组已经切分完成的文本,将单词存放到列表中,并对词汇向量进行分类标注:

1、创建实验样本:

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 is abusive, 0 not
    return postingList,classVec

if __name__ == '__main__':
    #测试loadDataSet()函数
    postingList,classVec = loadDataSet()
    print(postingList)
    print(classVec)

测试结果:
机器学习—基于概率论的分类方法朴素贝叶斯_第11张图片
从运行结果可以看出,我们已经使postingList存放词条列表,classVec存放每个词条的所属类别,1代表侮辱类 ,0代表非侮辱类。

2、创建一个包含在所有文档中出现的不重复词条的列表,也就是词汇表:

def createVocabList(dataSet):
    #创建一个空集
    vocabSet = set([])
    for document in dataSet:
        #两个集合的并集,|用于求两个集合的并集,所以对象得都是集合,故用到set函数
        vocabSet = vocabSet | set(document)
    #set函数得到的是字典,需要返回不重复的词条列表
    return list(vocabSet)

测试函数:

if __name__ == '__main__':
    # 测试loadDataSet()函数
    postingList, classVec = loadDataSet()
    # 测试createVocabList()函数
    myVocabList = createVocabList(postingList)
    print('myVocabList:\n', myVocabList)

输出结果:
机器学习—基于概率论的分类方法朴素贝叶斯_第12张图片
从运行结果可以看出,postingList是原始的词条列表,myVocabList是词汇表。myVocabList是所有单词出现的集合,没有重复的元素。

3、得到文档向量
使用函数setOfWords2Vec(),该函数的输入参数为词汇表及某个文档,输出的是文档向量,根据vocabList词汇表,将inputSet向量化,向量的每个元素为1或0,分别表示词汇表中的单词在输入文档中是否出现。

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

if __name__ == '__main__':
    # 测试loadDataSet()函数
    postingList, classVec = loadDataSet()
    # 测试createVocabList()函数
    myVocabList = createVocabList(postingList)
    print('myVocabList:\n', myVocabList)
    # 测试setOfWords2Vec()函数
    print('testResult:\n', setOfWords2Vec(myVocabList, postingList[0]))

实验结果:
机器学习—基于概率论的分类方法朴素贝叶斯_第13张图片
红框中的词汇表对照inputSet的部分是postingList[0],得出词汇表的单词在输入文档中是否出现。

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

朴素贝叶斯分类器训练函数

"""
函数说明:朴素贝叶斯分类器训练函数

Parameters:
    trainMatrix - 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵
    trainCategory - 训练类别标签向量,即loadDataSet返回的classVec
Returns:
    p0Vect - 非侮辱类的条件概率数组
    p1Vect - 侮辱类的条件概率数组
    pAbusive - 文档属于侮辱类的概率
"""
def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix)                            #计算训练的文档数目
    numWords = len(trainMatrix[0])                            #计算每篇文档的词条数
    pAbusive = sum(trainCategory)/float(numTrainDocs)        #文档属于侮辱类的概率
    p0Num = zeros(numWords); p1Num = zeros(numWords)        #创建numpy.zeros数组,词条出现数初始化为0
    p0Denom = 0.0; p1Denom = 0.0                            #分母初始化为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:                                                #统计属于非侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    p1Vect = p1Num/p1Denom
    p0Vect = 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 = trainNB0(trainMat, classVec)
    print('p0V:\n', p0V)
    print('p1V:\n', p1V)
    print('classVec:\n', classVec)
    print('pAb:\n', pAb)

运行结果:
机器学习—基于概率论的分类方法朴素贝叶斯_第14张图片
p0V存放的是每个单词属于类别0,也就是非侮辱类词汇的概率。比如p0V的stupid这个单词属于非侮辱类的概率为0。
同理,p1V的stupid这个单词属于侮辱类的概率为0.15789474,也就是约等于15.79%的概率。显而易见,这个单词属于侮辱类。

pAb是所有侮辱类的样本占所有样本的概率,从classVec中可以看出,一用有3个侮辱类,3个非侮辱类。所以侮辱类的概率是0.5。
因此p0V存放的就是P(worthless|非侮辱类) = 0.、P(love|非侮辱类) = 0.04166667,.一直到P(not|非侮辱类) = 0,即各个单词属于非侮辱类的条件概率。同理,p1V存放的就是各个单词属于侮辱类的条件概率。pAb就是先验概率。

3、测试算法:根据现实情况修改分类器

1、函数说明:朴素贝叶斯分类器分类函数,训练好分类器,我们可以开始使用分类器来进行分类。

def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
	p1 = reduce(lambda x,y:x*y, vec2Classify * p1Vec) * pClass1    			#对应元素相乘
	p0 = reduce(lambda x,y:x*y, vec2Classify * p0Vec) * (1.0 - pClass1)
	print('p0:',p0)
	print('p1:',p1)
	if p1 > p0:
		return 1
	else: 
		return 0

其中,vec2Classify - 待分类的词条数组,p0Vec - 侮辱类的条件概率数组,p1Vec -非侮辱类的条件概率数组,pClass1 - 文档属于侮辱类的概率。
2、测试朴素贝叶斯分类器

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']									#测试样本1
	thisDoc = array(setOfWords2Vec(myVocabList, testEntry))				#测试样本向量化
	if classifyNB(thisDoc,p0V,p1V,pAb):
		print(testEntry,'属于侮辱类')										#执行分类并打印分类结果
	else:
		print(testEntry,'属于非侮辱类')										#执行分类并打印分类结果
	testEntry = ['stupid', 'garbage']										#测试样本2

	thisDoc = array(setOfWords2Vec(myVocabList, testEntry))				#测试样本向量化
	if classifyNB(thisDoc,p0V,p1V,pAb):
		print(testEntry,'属于侮辱类')										#执行分类并打印分类结果
	else:
		print(testEntry,'属于非侮辱类')										#执行分类并打印分类结果

if __name__ == '__main__':
	testingNB()

结果:
机器学习—基于概率论的分类方法朴素贝叶斯_第15张图片
发现p0和p1都为0,显然该算法无法进行比较。

利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,即计算p(w0|1)p(w1|1)p(w2|1)。如果其中有一个概率值为0,那么最后的成绩也为0。这就是零概率问题,不能因为样本没有,就认为不可能发生。 为了降低这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。
这种做法就叫做拉普拉斯平滑,又被称为加1平滑,是比较常用的平滑方法,它就是为了解决0概率问题。
修改:

p0Num = np.ones(numWords); p1Num = np.ones(numWords)    #创建numpy.ones数组,词条出现数初始化为1,拉普拉斯平滑
p0Denom = 2.0; p1Denom = 2.0                            #分母初始化为2,拉普拉斯平滑

另一个问题就是下溢出,这是由于太多很小的数相乘造成的。在程序中,在相应小数位置进行四舍五入,计算结果可能就变成0了。为了解决这个问题,对乘积结果取自然对数,在代数中有ln(a*b) = ln(a) + ln(b)。通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。下图给出函数f(x)和ln(f(x))的曲线。
机器学习—基于概率论的分类方法朴素贝叶斯_第16张图片
发现它们在相同区域内同时增加或者减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。
修改:

p1Vect = log(p1Num/p1Denom)                            #取对数,防止下溢出         
p0Vect = log(p0Num/p0Denom) 

机器学习—基于概率论的分类方法朴素贝叶斯_第17张图片
机器学习—基于概率论的分类方法朴素贝叶斯_第18张图片
从运行结果可以看出,已经不存在零概率问题。

过滤垃圾邮件问题

1. 收集数据

Github上下载:email数据集,其中ham为非垃圾邮件,spam为垃圾邮件。

2. 准备数据:切分文本

对于一个文本字符串,我们可以使用split函数以非字母、非数字作为符号进行切分。代码如下:

ef textParse(bigString):                                                   #将字符串转换为字符列表
	listOfTokens = re.split(r'[\W*]', bigString)                              #将特殊符号作为切分标志进行字符串切分,即非字母、非数字
	return [tok.lower() for tok in listOfTokens if len(tok) > 2]            #除了单个字母,例如大写的I,其它单词变成小写

if __name__ == '__main__':
	docList = []; classList = []
	for i in range(1, 26):                                                  #遍历25个txt文件
		wordList = textParse(open('email/spam/%d.txt' % i, 'r').read())     #读取每个垃圾邮件,并字符串转换成字符串列表
		docList.append(wordList)
		classList.append(1)                                                 #标记垃圾邮件,1表示垃圾文件
		wordList = textParse(open('email/ham/%d.txt' % i, 'r').read())      #读取每个非垃圾邮件,并字符串转换成字符串列表
		docList.append(wordList)
		classList.append(0)                                                 #标记非垃圾邮件,1表示垃圾文件
	vocabList = createVocabList(docList)                                    #创建词汇表,不重复
	print(vocabList)

运行结束得到一个词汇表:
机器学习—基于概率论的分类方法朴素贝叶斯_第19张图片
根据词汇表,我们就可以将每个文本向量化。我们将数据集分为训练集和测试集,使用留存交叉验证的方式测试朴素贝叶斯分类器的准确性。代码如下:

#文件解析及完整的垃圾邮件测试函数
#函数说明:接收一个大字符串并将其解析为字符串列表


def textParse(bigString):                                                   #将字符串转换为字符列表
	listOfTokens = re.split(r'[\W*]', bigString)                              #将特殊符号作为切分标志进行字符串切分,即非字母、非数字
	return [tok.lower() for tok in listOfTokens if len(tok) > 2]            #除了单个字母,例如大写的I,其它单词变成小写


#函数说明:测试朴素贝叶斯分类器
def spamTest():
	docList = []; classList = []; fullText = []
	for i in range(1, 26):                                                  #遍历25个txt文件
		wordList = textParse(open('email/spam/%d.txt' % i, 'r').read())     #读取每个垃圾邮件,并字符串转换成字符串列表
		docList.append(wordList)
		fullText.append(wordList)
		classList.append(1)                                                 #标记垃圾邮件,1表示垃圾文件
		wordList = textParse(open('email/ham/%d.txt' % i, 'r').read())      #读取每个非垃圾邮件,并字符串转换成字符串列表
		docList.append(wordList)
		fullText.append(wordList)
		classList.append(0)                                                 #标记非垃圾邮件,1表示垃圾文件
	vocabList = createVocabList(docList)                                    #创建词汇表,不重复
	trainingSet = list(range(50)); testSet = []                             #创建存储训练集的索引值的列表和测试集的索引值的列表
	for i in range(10):                                                     #从50个邮件中,随机挑选出40个作为训练集,10个做测试集
		randIndex = int(random.uniform(0, len(trainingSet)))                #随机选取索索引值
		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                                                          #错误分类计数
	for docIndex in testSet:                                                #遍历测试集
		wordVector = setOfWords2Vec(vocabList, docList[docIndex])           #测试集的词集模型
		if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:    #如果分类错误
			errorCount += 1                                                 #错误计数加1
			print("分类错误的测试集:",docList[docIndex])
	print('错误率:%.2f%%' % (float(errorCount) / len(testSet) * 100))


if __name__ == '__main__':
	spamTest()

运行结果:
机器学习—基于概率论的分类方法朴素贝叶斯_第20张图片
机器学习—基于概率论的分类方法朴素贝叶斯_第21张图片

机器学习—基于概率论的分类方法朴素贝叶斯_第22张图片
机器学习—基于概率论的分类方法朴素贝叶斯_第23张图片
函数spamTest()会输出在10封随机选择的电子邮件上的分类错误概率。既然这些电子邮件是随机选择的,所以每次的输出结果可能有些差别。
如果发现错误的话,函数会输出错误的文档的此表,这样就可以了解到底是哪篇文档发生了错误。
如果想要更好地估计错误率,那么就应该将上述过程重复多次,比如说10次,然后求平均值。
相比之下,将垃圾邮件误判为正常邮件要比将正常邮件归为垃圾邮件好。


实验总结

朴素贝叶斯的优缺点
优点:效率较高;对缺失数据不太敏感;能处理多分类任务。
缺点:要求数据的相关性很低,各个数据之间的依赖性要保持在很低的水平上。

你可能感兴趣的:(c++,r语言,big,data)