机器学习实战笔记:朴素贝叶斯分类
前言
在之前我们讲述knn以及决策树时。曾要求分类器做出决策给出“该实例属于哪一类”这类问题的明确答案。不过分类器有时候会产生错误的结果,这个时候要求分类器给出一个最优的类别猜测结果,同时给出这个猜测的概率估计值。之前决策树章节里信息熵的计算就涉及到了一些概率知识,接下来我们将在这个基础上深入讨论。
本章会给出一些使用概率论进行分类的方法。首先从一个最简单的概率分类器开始,然后给出一些假设来学习朴素贝叶斯分类器。我 们 称 之 为“朴素” ,是因为整个形式化过程只做最始、最简单的假设。不必担心,你会详细了解到这些假设。我们将充分利用python的文本处理能力将文档切分成词向量,然后利用词向量对文档进行分类。我们还将构建另一个分类器,观察其在真实的垃圾邮件数据集中的过滤效果,必要时还会回顾一下条件概率。最 后 ,我们将介绍如何从个人发布的大量广告中学习分类器,并将学习结果转换成人类可理解的信息。
朴素贝叶斯理论
贝特斯决策理论
朴素贝叶斯是贝叶斯决策理论的一部分,所以讲述朴素负叶斯之前有必要快速了解一下贝叶
斯决策理论。 假设现在我们有一个数据集,它由两类数据组成,数据分布如下图所示。
我们现在用p1(x,y)表示数据点(x,y)属于类别1(图中用圆点表示的类别)的概率,用p2(x,y)表示数据点(x,y)属于类别2(图中用三角形表示的类别)的概率,那么对于一个新的数据点(x,y),可以用下面的规则来判断他的类别:
- 如果p1(x,y) > p2(x,y),那么类别为1.
- 如果p2(x,y) > p1(x,y),那么类别为2.
也就是说我们会选择高概率对应的类别,这就是贝叶斯决策理论的核心思想—选择具有最高概率的决策。那么接下来我们就要计算p1和p2,在计算这两个概率的同时,我们需要了解一下条件概率以及全概率公式。
条件概率
如图所示,假设A,B是两个事件,并且p(B)>0,那么在事件B已经发生的条件下,事件A发生地概率就可以称之为条件概率,其公式为:
可以看到,如果等于0.即条件概率也为0,所以我们一般说到条件概率这一个概念的时候,最好事件A和事件B都是同一个实验下不同结果的集合。
同理可得:
这就是条件概率的计算公式。
全概率公式
假如事件B1,B2,....,构成了一个完备事件组并且都有正概率,则对于任何一个事件B,都有如下公式成立:
其中A为任意一个事件(比如上图中的红色区域),将这个公式带入上面的条件概率公式,,此公式便是全概率公式:
贝叶斯推断
对上面的条件概率公式进行变形,我们可以得到如下形式:
其中P(A)称之为”先验概率“:即在事件B发生之前,我们通过自己的经验判断对A事件概率的一个估计,是主观概率。
P(A|B)称之为”后验概率“:就是在事件B发生之后,我们对A事件概率进行重新评估的概率,是客观概率
P(B|A)/P(B)可以把它当做调整因子,使得预估概率更接近于真实概率。
所以,条件概率可以理解成:后验概率 = 先验概率 x 调整因子
这就是贝叶斯推断的含义。我们先预估一个"先验概率",然后加入实验结果,看这个实验是增强还是削弱了"先验概率",由此得到更接近事实的"后验概率"。如果调整因子P(B|A)/P(B)>1,意味着"先验概率"被增强,事件A的发生的可能性变大;如果"可能性函数"=1,意味着B事件无助于判断事件A的可能性;如果"可能性函数"<1,意味着"先验概率"被削弱,事件A的可能性变小。
举个例子:已知某种疾病的发病率是0.001,即1000人中会有1个人得病。现有一种试剂可检验患者是否得病,它的准确率是0.99,即在患者确实得病的情况下,它有99%的可能呈现阳性。它的误报率是5%,即在患者没有得病的情况下,它有5%的可能呈现阳性。现有一个病人的检验结果为阳性,请问他确实得病的可能性有多大?
:疾病的发病率;:检验出阳性的概率;:患病;
最后我们得到了一个惊人的结果,得病的可能性约等于0.019。也就是说,即使检验呈现阳性,病人得病的概率,也只是从0.1%增加到了2%左右。这就是所谓的"假阳性",即阳性结果完全不足以说明病人得病。尽管这种检验的准确率高达0.99,但是可信率却不到百分之2,就是因为它的误报率太高了。
朴素贝叶斯推断
贝叶斯和朴素贝叶斯的概念是不同的,区别就在于朴素这两个字,朴素贝叶斯对每个条件做了一个大胆的假设,即每个条件是相互独立的。比如下面的公式,假设X有n个维度,这样就可以得出:
从上式可以看出,大大的简化了条件分布,但是这也可能带来预测的不准确性。你会说如果我的特征之间非常不独立怎么办?如果真是非常不独立的话,那就尽量不要使用朴素贝叶斯模型了,考虑使用其他的分类方法比较好。但是一般情况下,样本的特征之间独立这个条件的确是弱成立的,尤其是数据量非常大的时候。虽然我们牺牲了准确性,但是得到的好处是模型的条件分布的计算大大简化了,这就是贝叶斯模型的选择。
我们可以看一个例子:
这个时候,来了第七个患者,一位打喷嚏的工人,请推断他得了什么病。这就是一个分类题。现状把所有患者分成了三类“感冒”“过敏”“脑震荡”,我们的目的是把“打喷嚏的工人”分到这三类中的一类中。具体做法为:根据贝叶斯定理,计算这个“打喷嚏的工人”患三种疾病的概率。
P(感冒|打喷嚏的工人)=P(打喷嚏的工人|感冒) x P(感冒) / p(打喷嚏的工人)
=P(打喷嚏|感冒) x p(工人|感冒) x P(感冒) / p(打喷嚏) x P(工人)
=(2/3) x (1/3) x (1/2) / (1/2 x 1/3)
=66.7%
按照这个方法,计算打喷嚏的工人得另外两种病的概率都为0%。可见,打喷嚏的工人患感冒的概率更高,达到了66.7%。但是一般的分类器都要根据具体业务设置阈值,对于人命关天的事,最好严格一些,比如95%以上才做出判断,那么这里最好的答案应该是“机器无法判断,建议去让医生看看”。
下面我们将利用朴素贝叶斯进行文档分类。
使用贝叶斯分类器进行文本分类
以在线社区的留言板为例。为了不影响社区的发展,我们要屏蔽侮辱性的言论,所以要构建一个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么就将该留言标识为内容不当。过滤这类内容是一个很常见的需求。对此问题建立两个类别:侮辱类和非侮辱类’使用1和0分别表示。
接下来首先给出将文本转换为数字向量的过程,然后介绍如何基于这些向量来计算条件概率,并在此基础上构建分类器’最后还要介绍一些实现朴素贝叶斯过程中需要考虑的问题。
准备数据:从文本中构建词向量
我们将把文本看成单词向量或者词条向量,也就是说将句子转换为向量。考虑出现在所有文档中的所有单词,再决定将哪些词纳人词汇表或者说所要的词汇集合,然后必须要将每一篇文档转换为词汇表上的向量。接下来我们正式开始。打开文本编辑器,创建一个叫bayes.py的新文件,然后将下面的程序添加到文件中。
import numpy as np
"""
函数说明:创建实验样本
Parameter:
无
Return:
postingList - 实验样本切分的词条
classVec - 类别标签向量
Modify:
2018/9/11
"""
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代表侮辱性词汇,0代表不是
return postingList,classVec
"""
函数说明:将切分的实验样本词条整理成不重复的词条列表,也就是词汇表
Parameters:
dataSet - 整理的样本数据集
Returns:
vocabSet - 返回不重复的词条列表,也就是词汇表
Modify:
2018/9/11
"""
def createVocabList(dataSet):
vocabSet = set([])
for data in dataSet:
#创建两个集合的并集
vocabSet = vocabSet | set(data)
return list(vocabSet)
"""
函数说明:根据vocabList词汇表,将inputSet向量化,响亮的每个元素为1或者0
Parameter:
vocabList - createVocabList返回的列表
inputSet - 切分的词条列表
Returns:
returnVec - 文档向量,词集模型
Modify:
2018/9/11
"""
def setofWords2Vec(vocabList,inputSet):
#创建一个其中所含元素都为0的向量
returnVec = [0] * len(vocabList)
for each in inputSet:
if each in vocabList:
returnVec[vocabList.index(each)]=1
else:
print("the word : %s is not in my Vocabulary"% each)
return returnVec
if __name__ == '__main__':
postingList,classVec = loadDataSet()
print('postingList:\n',postingList)
myVocabList = createVocabList(postingList)
print('myVocabList:\n',myVocabList)
执行上出程序后可以看到myVocabList中不会出现重复的单词。但目前该词汇表还没有排序,需要的话,稍后可以对其排序。
训练算法:从词向量计算概率
前面介绍了如何将一组单词转换为一组数字,接下来看看如何使用这些数字计算概率。现在已经知道一个词是否出现在一篇文档中,也知道该文档所属的类别。我们重写贝叶斯准则,将之前的x,y替换为w。w表示这是一个向量,即它由多个数值组成。在这个例子中,数值个数与词汇表中的词个数相同。
我们将使用上述公式,对每个类计算该值,然后比较这两个概率值的大小。打开bayes.py文件,添加下述代码:
"""
函数说明:朴素贝叶斯分类器训练函数
Parameter:
trainMat — 训练文档矩阵,即setofWord2Vec 返回的returnVec构成的矩阵
trainCategory - 训练类别标签向量,即loadDataSet 返回的classVec
Returns:
p0Vect - 非侮辱类的条件概率数组
p1Vect - 侮辱类的条件概率数组
pAbusive - 文档属于侮辱类的概率
Modify:
2018/9/11
"""
def trainNBO(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix) #文档数目
numWords = len(trainMatrix[0]) #词条数
pAbusive = sum(trainCategory) / float(numTrainDocs) #文档属于侮辱类的概率
p0Num = np.ones(numWords)
p1Num = np.ones(numWords)
p0Denom = 0.0
p1Denom = 0.0 #初始化概率
for i in range(numTrainDocs):
if trainCategory[i] == 1:
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i]) #向量相加
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
#print(p1Denom)
#print(p1Num)
p1Vect = np.log(p1Num/p1Denom) #对每个元素做除法,避免程序输出下溢,采用自然对数处理
p0Vect = np.log(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 = trainNBO(trainMat, classVec)
print('p0V:\n', p0V)
print('p1V:\n', p1V)
print('classVec:\n', classVec)
print('pAb:\n', pAb)
运行程序后可以看到pAb=0.5,pAb就是先验概率。现在已经准备好构建完整的分类器了,接下来,使用分类器进行分类,添加以下代码:
"""
函数说明:朴素贝叶斯分类器分类函数
Parameter:
vec2Classify - 待分类的词条数组
p0Vec - 侮辱类的条件概率数组
p1Vec - 非侮辱类的条件概率数组
pClass1 - 文档属于侮辱类的概率
Returns:
1 - 属于侮辱类
0 - 属于非侮辱类
Modify:
2018/9/14
"""
def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
if __name__ == '__main__':
postingList,classVec = loadDataSet()
myVocabList = createVocabList(postingList)
trainMat=[]
for postinDoc in postingList:
trainMat.append(setofWords2Vec(myVocabList,postinDoc))
p0V,p1V,pAb = trainNBO(np.array(trainMat),np.array(classVec))
testEntry = ['love', 'my', 'dalmation']
thisDoc = np.array(setofWords2Vec(myVocabList, testEntry))
if classifyNB(thisDoc,p0V,p1V,pAb):
print(testEntry,'属于侮辱类')
else:
print(testEntry,'属于非侮辱类')
testEntry = ['stupid', 'garbage']
thisDoc = np.array(setofWords2Vec(myVocabList, testEntry))
if classifyNB(thisDoc,p0V,p1V,pAb):
print(testEntry,'属于侮辱类')
else:
print(testEntry,'属于非侮辱类')
输出结果如下:
对文本做一些修改,看看分类器会输出什么结果。这个例子非常简单,但是它展示了朴素贝叶斯分类器的工作原理。接下来,我们会对代码做些修改,使分类器工作得更好。
词袋模型:
我们将每个词的出现与否作为一个特征,这可以被描述为词集模型。如果一个词在文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为词袋模型化。在词袋中,每个单词可以出现多次 ,而在词集中,每个词只能出现一次。为适应词袋模型,需要对函数setofWords2Vec()稍加修改,修改后的函数称之为bagOfWords2Vec()。
"""
函数说明:根据vocabList词汇表,构建词袋模型
Parameter:
vocabList - creatVocabList
inputSet - 切分的词条列表
Returns:
returnVec - 文档向量
Modify:
2018/9/11
"""
def bagOfWords2Vec(vocabList,inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1
return returnVec
现在分类器巳经构建好了,下面我们将利用该分类器来过滤垃圾邮件。
示例:垃圾邮件分类处理
首先看一下如何使用通用框架来解决该问题:
- 收集数据:提供文本文件
- 准备数据:将文本文件解析成词条向量
- 分析数据:检查词条确保解析的正确性。
- 训练算法:使用我们之前建立的trainNB0函数。
- 测试算法:使用classifyNB(),并且构建一个新的测试函数来计算文档集的错误率
- 使用算法:构建一个完整程序对一组文档分类,将错分的文档输出到屏幕上。
下面首先给出将文本解析为词条的代码。然后将该代码和前面的分类代码集成为一个函数,
该函数在测试分类器的同时会给出错误率。
准备数据:切分文本
打开bayes.py文件,添加下述代码:
import re
import random
import numpy as np
"""
函数说明:接受一个大字符串并将其解析为字符串列表
Parameter:
无
Returns:
无
Modify:
2018/9/11
"""
def textParse(bigString):
listOfTokens = re.split(r'\W*',bigString) #将非数字、字母、下划线的字符作为切分标志
#除了单个字母,例如大写的I,其余的单词变成小写
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
测试算法:交叉验证
随机选择50个文件中的任意十个文件做测试集,同时将这些测试集从训练集合剔除,进行交叉验证。具体代码如下所示:
"""
函数说明: 测试朴素贝叶斯分类器
Parameters:
无
Returns:
无
Modify:
2018/9/11
"""
def spamTest():
docList = [];classList = [];fullTest = []
#导入并解析50个文件,并记录对应的标签值,垃圾邮件标记为1
for i in range(1,26):
wordList = textParse(open('email/spam/%d.txt'% i,'r').read())
docList.append(wordList)
fullTest.append(wordList)
classList.append(1)
wordList = textParse(open('email/ham/%d.txt'% i,'r').read())
docList.append(wordList)
fullTest.append(wordList)
classList.append(0)
#获得50个文件构成的词列表
vocabList = createVocablist(docList)
trainingSet = list(range(50));
testSet = []
#随机选取测试集合,并从训练集中去除,留存交叉验证
for i in range(10):
randIndex = int(random.uniform(0,len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
trainMat = [];trainClass = []
#构建训练集词向量列表,训练集标签
for docIndex in trainingSet:
trainMat.append(setofWords2Vec(vocabList,docList[docIndex]))
trainClass.append(classList[docIndex])
#训练算法,计算分类所需的概率
p0V,p1V,pSpam = trainNBO(np.array(trainMat),np.array(trainClass))
#分类错误个数初始化为0
errorCount = 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('错误率:%.2f%%'%(float(errorCount)/len(testSet)*100))
if __name__ == '__main__':
spamTest()
函数spamTest()会输出在10封随机选择的电子邮件上的分类错误率。既然这些电子邮件是随机选择的,所以每次的输出结果可能有些差别。如果发现错误的话,函数会输出错分文档的词表 ,这样就可以了解到底是哪篇文档发生了错误。如果想要更好地估计错误率,那么就应该将上述过程重复多次,比如说10次 ,然后求平均值。我这么做了一下,获得的平均错误率为6% 。
小结:
朴素贝叶斯算法的主要原理基本已经做了总结,这里对朴素贝叶斯的优缺点做一个总结。
朴素贝叶斯的主要优点有:
1)朴素贝叶斯模型发源于古典数学理论,有稳定的分类效率。
2)对小规模的数据表现很好,能个处理多分类任务,适合增量式训练,尤其是数据量超出内存时,我们可以一批批的去增量训练。
3)对缺失数据不太敏感,算法也比较简单,常用于文本分类。
朴素贝叶斯的主要缺点有:
1) 理论上,朴素贝叶斯模型与其他分类方法相比具有最小的误差率。但是实际上并非总是如此,这是因为朴素贝叶斯模型给定输出类别的情况下,假设属性之间相互独立,这个假设在实际应用中往往是不成立的,在属性个数比较多或者属性之间相关性较大时,分类效果不好。而在属性相关性较小时,朴素贝叶斯性能最为良好。对于这一点,有半朴素贝叶斯之类的算法通过考虑部分关联性适度改进。
2)需要知道先验概率,且先验概率很多时候取决于假设,假设的模型可以有很多种,因此在某些时候会由于假设的先验模型的原因导致预测效果不佳。
3)由于我们是通过先验和数据来决定后验的概率从而决定分类,所以分类决策存在一定的错误率。
4)对输入数据的表达形式很敏感。
PS:
本篇文章所涉及到的数据集以及完整代码都存放在github中
欢迎各位大佬fock or star