前两章的KNN分类算法和决策树分类算法最终都是预测出实例的确定的分类结果,但是,有时候分类器会产生错误结果;本章要学的朴素贝叶斯分类算法则是给出一个最优的猜测结果,同时给出猜测的概率估计值。
朴素贝叶斯是贝叶斯决策理论的一部分,所以在讲述朴素贝叶斯之前有必要快速了解一下贝叶斯决策理论。
假设现在我们有一个数据集,它由两类数据组成,数据分布如下图所示:
我们现在用 p1(x,y) 表示数据点 (x,y) 属于类别 1(图中用圆点表示的类别)的概率,用 p2(x,y) 表示数据点 (x,y) 属于类别 2(图中三角形表示的类别)的概率,那么对于一个新数据点 (x,y),可以用下面的规则来判断它的类别:
也就是说,我们会选择高概率对应的类别。这就是贝叶斯决策理论的核心思想,即选择具有最高概率的决策。已经了解了贝叶斯决策理论的核心思想,那么接下来,就是学习如何计算p1和p2概率。
有一个装了 7 块石头的罐子,其中 3 块是白色的,4 块是黑色的。如果从罐子中随机取出一块石头,那么是白色石头的可能性是多少?由于取石头有 7 种可能,其中 3 种为白色,所以取出白色石头的概率为 3/7 。那么取到黑色石头的概率又是多少呢?很显然,是 4/7 。我们使用 P(white) 来表示取到白色石头的概率,其概率值可以通过白色石头数目除以总的石头数目来得到。
计算 P(white) 或者 P(black) ,如果事先我们知道石头所在桶的信息是会改变结果的。这就是所谓的条件概率(conditional probablity)。假定计算的是从 B 桶取到白色石头的概率,这个概率可以记作 P(white|bucketB) ,我们称之为“在已知石头出自 B 桶的条件下,取出白色石头的概率”。很容易得到,P(white|bucketA) 值为 2/4 ,P(white|bucketB) 的值为 1/3 。
条件概率的计算公式如下:
P(white|bucketB) = P(white and bucketB) / P(bucketB)
首先,我们用 B 桶中白色石头的个数除以两个桶中总的石头数,得到 P(white and bucketB) = 1/7 .其次,由于 B 桶中有 3 块石头,而总石头数为 7 ,于是 P(bucketB) 就等于 3/7 。于是又 P(white|bucketB) = P(white and bucketB) / P(bucketB) = (1/7) / (3/7) = 1/3 。
另外一种有效计算条件概率的方法称为贝叶斯准则。贝叶斯准则告诉我们如何交换条件概率中的条件与结果,即如果已知 P(x|c),要求 P(c|x),那么可以使用下面的计算方法
假设这里要被分类的类别有两类,类c1和类c2,那么我们需要计算概率p(c1|x,y)和p(c2|x,y)的大小并进行比较:
如果:p(c1|x,y)>p(c2|x,y),则(x,y)属于类c1
p(c1|x,y)
我们知道p(x,y|c)的条件概率所表示的含义为:已知类别c1条件下,取到点(x,y)的概率;那么p(c1|x,y)所要表达的含义呢?显然,我们同样可以按照条件概率的方法来对概率含义进行描述,即在给定点(x,y)的条件下,求该点属于类c1的概率值。那么这样的概率该如何计算呢?显然,我们可以利用贝叶斯准则来进行变换计算:
p(ci|x,y)=p(x,y|ci)*p(ci)/p(x,y)
利用上面的公式,我们可以计算出在给定实例点的情况下,分类计算其属于各个类别的概率,然后比较概率值,选择具有最大概率的那么类作为点(x,y)的预测分类结果。
"朴素"含义:本章算法全称叫朴素贝叶斯算法,显然除了贝叶斯准备,朴素一词同样重要。这就是我们要说的条件独立性假设的概念。条件独立性假设是指特征之间的相互独立性假设,所谓独立,是指的是统计意义上的独立,即一个特征或者单词出现的可能性与它和其他单词相邻没有关系。举个例子来说,假设单词bacon出现在unhealthy后面是与delisious后面的概率相同。当然,我们知道其实并不正确,但这正是朴素一词的含义。同时,朴素贝叶斯另外一个含义是,这些特征同等重要。虽然这些假设都有一定的问题,但是朴素贝叶斯的实际效果却很好。
要从文本中获取特征,显然我们需要先拆分文本,这里的文本指的是来自文本的词条,每个词条是字符的任意组合。词条可以为单词,当然也可以是URL,IP地址或者其他任意字符串。将文本按照词条进行拆分,根据词条是否在词汇列表中出现,将文档组成成词条向量,向量的每个值为1或者0,其中1表示出现,0表示未出现。
接下来,以在线社区的留言为例。对于每一条留言进行预测分类,类别两种,侮辱性和非侮辱性,预测完成后,根据预测结果考虑屏蔽侮辱性言论,从而不影响社区发展。
词表到向量的转换函数
import numpy as np
"""
实验样本
词表到向量的转换函数
"""
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]
return postingList,classVec
def createVocabList(dataSet):
"""
:param dataSet: 样本数据集
:return: 返回不重复的词条列表,也就是词汇表
"""
vocabSet = set([]) # 创建一个空的不重复列表
for document in dataSet:
vocabSet = vocabSet | set(document) # 取并集
return list(vocabSet)
def setOfWords2Vec(vocabList,inputSet):
"""
:param vocabList: createVocabList返回的列表
:param inputSet: 切分的词条列表
:return: 文档向量,词集模型
"""
returnVec = [0] * len(vocabList) # 创建一个所有元素都为0 的向量
for word in inputSet: # 遍历每个词条
if word in vocabList: # 如果词条存在于词汇表中
returnVec[vocabList.index(word)] = 1
else:
print("the word: %s is not in my Vocabulary! " % word)
return returnVec
if __name__ == '__main__':
postingList,classVec = loadDataSet()
print("postingList:\n",postingList)
myVocabList = createVocabList(postingList)
print("myVocabList:\n",myVocabList)
trainMat =[]
for postinDoc in postingList:
trainMat.append(setOfWords2Vec(myVocabList,postinDoc))
print("trainMat:\n",trainMat)
需要说明的是,上面函数creatVocabList得到的是所有文档中出现的词汇列表,列表中没有重复的单词,每个词是唯一的。
这里,如果我们将之前的点(x,y)换成词条向量w(各维度的值由特征是否出现的0或1组成),在这里词条向量的维度和词汇表长度相同。
p(ci|w)=p(w|ci)*p(ci)/p(w)
我们将使用该公式计算文档词条向量属于各个类的概率,然后比较概率的大小,从而预测出分类结果。
具体地,首先,可以通过统计各个类别的文档数目除以总得文档数目,计算出相应的p(ci);然后,基于条件独立性假设,将w展开为一个个的独立特征,那么就可以将上述公式写为p(w|ci)=p(w0|ci)*p(w1|ci)*...p(wN|ci),这样就很容易计算,从而极大地简化了计算过程
提取所有文档中的词条并进行去重
获取文档的所有类别
计算每个类别中的文档数目
对每篇训练文档:
对每个类别:
如果词条出现在文档中-->增加该词条的计数值(for循环或者矩阵相加)
增加所有词条的计数值(此类别下词条总数)
对每个类别:
对每个词条:
将该词条的数目除以总词条数目得到的条件概率(P(词条|类别))
返回该文档属于每个类别的条件概率(P(类别|文档的所有词条))
代码如下:
def trainNB0(trainMatrix,trainCategory):
"""
:param trainMatrix: 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵
:param trainCategory: 训练类别标签向量,即loadDataSet返回的classVec
:return:
p0Vect: 侮辱类的条件概率数组
p1Vect: 非侮辱类的条件概率数组
pAbusive: 文档属于侮辱类的概率
"""
numTrainDocs = len(trainMatrix) # 计算训练文档的数目
numWords = len(trainMatrix[0]) # 计算每篇文档的词条数
pAbusive = sum(trainCategory) / float(numTrainDocs) # 文档属于侮辱类的概率
p0Num = np.ones(numWords) # 词条出现数初始化0
p1Num = np.ones(numWords)
p0Denom = 2.0 # 分母初始化为0
p1Denom = 2.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:
p0Num += trainMatrix[i] # 统计属于非侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)·
p0Denom += sum(trainMatrix[i])
p1Vect = np.log(p1Num / p1Denom) # 取对数,防止下溢出
p0Vect = np.log(p0Num / p1Denom)
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)
由于概率都很小,那么相乘之后就更小,会造成四舍五入之后为0,解决这个问题的办法是我们对概率取对数。一下输出为负数的结果是取对数后的值。运行结果如下,p0V存放的是每个单词属于类别0,也就是非侮辱类词汇的概率,p1V存放的就是各个单词属于侮辱类的条件概率。pAb就是先验概率。
def classifyNB(vec2classify,p0Vec,p1Vec,pClass1):
"""
使用算法:
# 将乘法转换为加法
乘法:P(C|F1F2...Fn) = P(F1F2...Fn|C)P(C)/P(F1F2...Fn)
加法:P(F1|C)*P(F2|C)....P(Fn|C)P(C) -> log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C))
:param vec2Classify: 待测数据[0,1,1,1,1...],即要分类的向量
:param p0Vec: 类别0,即正常文档的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表
:param p1Vec: 类别1,即侮辱性文档的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表
:param pClass1: 类别1,侮辱性文件的出现概率
:return: 类别1 or 0
"""
# 计算公式 log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C))
# 大家可能会发现,上面的计算公式,没有除以贝叶斯准则的公式的分母,也就是 P(w) (P(w) 指的是此文档在所有的文档中出现的概率)就进行概率大小的比较了,
# 因为 P(w) 针对的是包含侮辱和非侮辱的全部文档,所以 P(w) 是相同的。
# 使用 NumPy 数组来计算两个向量相乘的结果,这里的相乘是指对应元素相乘,即先将两个向量中的第一个元素相乘,然后将第2个元素相乘,以此类推。
# 我的理解是:这里的 vec2Classify * p1Vec 的意思就是将每个词与其对应的概率相关联起来
p1 = np.sum(vec2classify * p1Vec) + np.log(pClass1) # P(w|c1) * P(c1) ,即贝叶斯准则的分子
p0 = np.sum(vec2classify * p0Vec) + np.log(1.0 - pClass1) # P(w|c0) * P(c0) ,即贝叶斯准则的分子·
if p1 > p0:
return 1
else:
return 0
def testingNB():
listOPosts,listClasses = loadDataSet() # 1. 加载数据集
myVocabList = createVocabList(listOPosts) # 2. 创建单词集合
trainMat = [] # 3. 计算单词是否出现并创建数据矩阵
for postinDoc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList,postinDoc)) # 返回m*len(myVocabList)的矩阵, 记录的都是0,1信息
p0V,p1V,pAb = trainNb0(np.array(trainMat),np.array(listClasses)) # 4. 训练数据
# 5. 测试数据
testEntry = ['love','my','dalmation']
thisDoc = np.array(setOfWords2Vec(myVocabList,testEntry))
print(testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb))
testEntry = ['stupid','garbage']
thisDoc = np.array(setOfWords2Vec(myVocabList,testEntry))
print(testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb))
这里需要补充一点,上面也提到了关于如何选取文档特征的方法,上面用到的是词集模型,即对于一篇文档,将文档中是否出现某一词条作为特征,即特征只能为0不出现或者1出现;然后,一篇文档中词条的出现次数也可能具有重要的信息,于是我们可以采用词袋模型,在词袋向量中每个词可以出现多次,这样,在将文档转为向量时,每当遇到一个单词时,它会增加词向量中的对应值
具体将文档转为词袋向量的代码为:
def bagOfWords2VecMN(vocabList,inputSet):
#词袋向量
returnVec=[0]*len(vocabList)
for word in inputSet:
if word in vocabList:
#某词每出现一次,次数加1
returnVec[vocabList.index(word)]+=1
return returnVec
"""
使用朴素贝叶斯过滤垃圾邮件
"""
def textParse(bigString):
"""
Desc:
接收一个大字符串并将其解析为字符串列表
Args:
bigString -- 大字符串
Returns:
去掉少于 2 个字符的字符串,并将所有字符串转换为小写,返回字符串列表
"""
import re
# 使用正则表达式来切分句子,其中分隔符是除单词、数字外的任意字符串
listOfTokens = re.split(r'\W*', bigString)
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
def spamTest():
"""
Desc:
对贝叶斯垃圾邮件分类器进行自动化处理。
Args:
none
Returns:
对测试集中的每封邮件进行分类,若邮件分类错误,则错误数加 1,最后返回总的错误百分比。
"""
docList = []
classList = []
fullText = []
for i in range(1,26): #遍历25个txt文件
# 切分,解析数据,并归类为 1 类别
wordList = textParse(open('email/spam/%d.txt' % i,'r' ).read()) #读取每个垃圾邮件,并字符串转换成字符串列表
docList.append(wordList)
fullText.extend(wordList)
classList.append(1)
# 切分,解析数据,并归类为 0 类别
wordList = textParse(open('email/ham/%d.txt' % i,'r' ).read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(0)
# 创建词汇表
vocabList = createVocabList(docList)
trainingSet = list(range(50)); testSet = [] #创建存储训练集的索引值的列表和测试集的索引值的列表
# 随机取 10 个邮件用来测试
for i in range(10):
randIndex = int(np.random.uniform(0,len(trainingSet))) # random.uniform(x, y) 随机生成一个范围为 x ~ y 的实数
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.0
for docIndex in testSet:
wordVector = setOfWords2Vec(vocabList,docList[docIndex])
if classifyNB(np.array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
errorCount += 1
print("分类错误的测试集:", docList[docIndex])
print("the error rate is: " , float(errorCount) / len(testSet))
朴素贝叶斯推断的一些优点:
朴素贝叶斯推断的一些缺点: