在深入了解朴素贝叶斯理论之前,我们先得明白以下几个点:
回顾之前所学k-邻近算法和决策树,都是基于“对数据实例属于哪一个明确的类”的这样一个问题,作出艰难的回答。缺点就是会产生错误结果。
而这时我们可以要求分类器给出一个最优的类别结果猜测,同时给出这个猜测的概率估计值。
先统计某个特征在数据集中取某个特定值的次数,然后除以数据集的实力总数,得到了特征取该值的概率。
利用python的文本处理能力将文档切分成词向量,然后利用词向量对文档分类。
若只有两个事件A,B,那么,P(A|B) = P(AB)/P(B)。
意思是事件A在另外一个事件B已经发生条件下的发生概率。条件概率表示为P(A|B),读作“在B条件下A的概率”。
分析:一般事件A和事件B一般是有交集的,若没有交集(互斥),则条件概率为0
用图更能容易的说明上述问题,我们进行某一实验,某一实验所有的可能的样本的结合为Ω(也即穷举实验的所有样本),圆圈A代表事件A所能囊括的所有样本,圆圈B代表事件B所能囊括的所有样本。
由图再来理解一下这个问题:“B已经发生的条件下,A发生的概率”,这句话中,“B已经发生”就相当于已经把样本的可选范围限制在了圆圈B中,其实就等价于这句话:“在圆圈B中,A发生的概率”,显然P(A|B)就等于AB交集中样本的数目/B的样本数目。
如果事件组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)
MAP: Maximum A Posterior
x 属于类别 c* 的概率:P(C=c*|X=x)>P(C=c|X=x) , c≠c*, c=c1,c2,…,cL
下面举个例子,已知有一个数据集,它由两类数据组成,如下图:
我们用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算法效率会非常低
决策树在划分数据时会遇到困难,选出最优分类的时候不能很好解决。倘若不止两个轴,那么计算量更是成倍增加。
与全概率公式解决的问题相反,贝叶斯公式是建立在条件概率的基础上寻找事件发生的原因(即大事件A已经发生的条件下,分割中的小事件Bi的概率),设B1,B2,…是样本空间Ω的一个划分,则对任一事件A(P(A)>0),有
上式即为贝叶斯公式(Bayes formula),Bi 常被视为导致试验结果A发生的”原因“,P(Bi)(i=1,2,…)表示各种原因发生的可能性大小,故称先验概率;
P(Bi|A)(i=1,2…)则反映当试验产生了结果A之后,再对各种原因概率的新认识,故称后验概率。
先抛出一个问题:假设要猜测某西瓜的好坏问题,如何判断正反概率?
根据自己购买西瓜的经验,好瓜的概率是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|来 近似。
西瓜好坏的概率和其属性有关; 当观测到西瓜样本(属性取值)x时,它为好瓜概率是多少?
p(y=1|x), p(y=0|x)
后验概率:观测到 x 后对结果 y 的估计;
后验概率给定数据样本x时cj成立的概率P(cj | x )被称为后验概率 (posterior probability),
因为它反映了在看到数据样本 x 后 cj成立的置信度。 大部分机器学习模型尝试得到后验概率。
朴素贝叶斯分类器(Naïve Bayes Classifier)采用了“属性条件独立性 假设”,即每个属性独立地对分类结果发生影响。
其中d为属性数目,Bi为 B 在第i个属性上的取值
对于所有类别来说P(B)都相同,所以只需要计算比较分子即可。
这个是出自于课堂ppt的案例,但还是希望在博客里复现一遍:
例子:用西瓜数据集训练一个朴素贝叶斯分类器,对下面的测试例进行分类,判断是不是好瓜
然后我们根据给定的属性值去训练集中找出来相应的数据,计算:
注意因为密度不是离散,是连续的,区分:
对比:
零概率问题: 在计算事件的概率时,如果某个事件在观察样本库(训练集)中没有出现过,会导致该事件的概率结果是0。这是不合理的,不能因为一个事件没有观察到,就被认为该事件一定不可能发生(即该事件的概率为0)。
拉普拉斯平滑是为了解决零概率的问题。
法国数学家拉普拉斯最早提出用加1的方法,估计没有出现过的现象的概率。拉普拉斯平滑,又叫加一平滑,它对分子划分的计数加1,分母加类别数。
理论假设:假定训练样本很大时,每个分量x的计数加1造成的估计概率变化可以忽略不计,但可以方便有效的避免零概率问题。
整个文档如电子邮件是一个实例,文档中的某些元素构成特征。我们可以统计文档中出现的词,并用它出现或不出现,出现次数作为特征。这样就可以用一个长度统一,内容是特征的向量来描述文档。
朴素贝叶斯中朴素naive的含义是每个特征互相独立,所以根据乘法原理,总概率为各特征相乘。
Python中为了防止下溢出通常计算log(prob),把相乘操作转换为相加。
将文本中的段落句子看成单词向量。从文档中的所有单词帅选出哪些单词纳入词汇表,然后必须要将每一篇文档转换为词汇表上的向量。这里有一组已经切分完成的文本,将单词存放到列表中,并对词汇向量进行分类标注:
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)
测试结果:
从运行结果可以看出,我们已经使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)
输出结果:
从运行结果可以看出,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]))
实验结果:
红框中的词汇表对照inputSet的部分是postingList[0],得出词汇表的单词在输入文档中是否出现。
朴素贝叶斯分类器训练函数
"""
函数说明:朴素贝叶斯分类器训练函数
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)
运行结果:
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就是先验概率。
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()
利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,即计算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))的曲线。
发现它们在相同区域内同时增加或者减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。
修改:
p1Vect = log(p1Num/p1Denom) #取对数,防止下溢出
p0Vect = log(p0Num/p0Denom)
Github上下载:email数据集,其中ham为非垃圾邮件,spam为垃圾邮件。
对于一个文本字符串,我们可以使用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)
运行结束得到一个词汇表:
根据词汇表,我们就可以将每个文本向量化。我们将数据集分为训练集和测试集,使用留存交叉验证的方式测试朴素贝叶斯分类器的准确性。代码如下:
#文件解析及完整的垃圾邮件测试函数
#函数说明:接收一个大字符串并将其解析为字符串列表
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()
函数spamTest()会输出在10封随机选择的电子邮件上的分类错误概率。既然这些电子邮件是随机选择的,所以每次的输出结果可能有些差别。
如果发现错误的话,函数会输出错误的文档的此表,这样就可以了解到底是哪篇文档发生了错误。
如果想要更好地估计错误率,那么就应该将上述过程重复多次,比如说10次,然后求平均值。
相比之下,将垃圾邮件误判为正常邮件要比将正常邮件归为垃圾邮件好。
朴素贝叶斯的优缺点
优点:效率较高;对缺失数据不太敏感;能处理多分类任务。
缺点:要求数据的相关性很低,各个数据之间的依赖性要保持在很低的水平上。