朴素贝叶斯
本章会给出一些使用概率论进行分类的方法。首先从一个简单的概率分类器开始,然后给出一些假设来学习朴素贝叶斯分类器。我们称之为“朴素”,是因为整个形式化过程只做原始、 简单的假设。
1. 什么是朴素贝叶斯
1.2 基于贝叶斯决策理论的分类方法
- 优点:在数据较少的情况下仍然有效,可以处理多类别问题。
- 缺点:对于输入数据的准备方式较为敏感。
- 适用数据类型:标称型数据。
朴素贝叶斯是贝叶斯决策理论的一部分,所以讲述朴素贝叶斯之前有必要快速了解一下贝叶斯决策理论。 假设现在我们有一个数据集,它由两类数据组成,数据分布下图所示
我们现在用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。
也就是说,我们会选择高概率对应的类别。这就是贝叶斯决策理论的核心思想,即选择具有高概率的决策。
1.2 条件概率
假设现在有一个装了7块石头的罐子,其中3块是灰色的,4块是黑色的。如 果从罐子中随机取出一块石头,那么是灰色石头的可能性是多少?由于取石头有7种可能,其中3 种为灰色,所以取出灰色石头的概率为3/7。那么取到黑色石头的概率又是多少呢?很显然,是 4/7。我们使用P(gray)来表示取到灰色石头的概率,其概率值可以通过灰色石头数目除以总的 石头数目来得到。
如果这7块石头如图4-3所示放在两个桶中,那么上述概率应该如何计算?
要计算P(gray)或者P(black),事先得知道石头所在桶的信息会不会改变结果?你有可能已经想到计算从B桶中取到灰色石头的概率的办法,这就是所谓的条件概率(conditional probability)。假定计算的是从B桶取到灰色石头的概率,这个概率可以记作P(gray|bucketB),我们称之为“在已知石头出自B桶的条件下,取出灰色石头的概率”。不难得到,P(gray|bucketA) 值为2/4,P(gray|bucketB) 的值为1/3。
另一种有效计算条件概率的方法称为贝叶斯准则。贝叶斯准则告诉我们如何交换条件概率中 的条件与结果,即如果已知P(x|c),要求P(c|x),那么可以使用下面的计算方法:
1.3 使用条件概率来分类
前面提到贝叶斯决策理论要求计算两个概率p1(x, y)和p2(x, y):
但这两个准则并不是贝叶斯决策理论的所有内容。使用p1( )和p2( )只是为了尽可能简化描述,而真正需要计算和比较的是p(c1|x, y)和p(c2|x, y)。这些符号所代表的具体意义是: 给定某个由x、y表示的数据点,那么该数据点来自类别c1的概率是多少?数据点来自类别c2的概率又是多少?注意这些概率与刚才给出的概率p(x, y|c1)并不一样,不过可以使用贝叶斯准则来交换概率中条件与结果。具体地,应用贝叶斯准则得到:
使用这些定义,可以定义贝叶斯分类准则为:
- 如果P(c1|x, y) > P(c2|x, y),那么属于类别c1。
- 如果P(c1|x, y) < P(c2|x, y),那么属于类别c2。
2. 使用朴素贝叶斯进行文档分类
机器学习的一个重要应用就是文档的自动分类。在文档分类中,整个文档(如一封电子邮件)是实例,而电子邮件中的某些元素则构成特征。我们可以观察文档中出现的词,并把每个词的出现或者不出现作为一个特征,这样得到的特征数目就会跟词汇表中的词目一样多。
朴素贝叶斯有两个假设,第一就是果特征之间相互独立,即一个特征或者单词出现的可能性与它和其他单词相邻没有关系,举个例子讲,假设单词bacon出现在unhealthy后面与出现在delicious后面的概率相同。当然,我们知道这种假设并不正确,bacon常常出现在delicious附近,而很少出现在unhealthy附近,这个假设正是朴素贝叶斯分类器中朴素(naive)一词的含义。朴素贝叶斯分类器中的另一个假设是,每个特征同等重要。其实这个假设也有问题。 如果要判断留言板的留言是否得当,那么可能不需要看完所有的1000个单词,而只需要看10~20个特征就足以做出判断了。尽管上述假设存在一些小的瑕疵,但朴素贝叶斯的实际效果却很好。
如何使用Python进行文本分类呢。
一个整体思路就是:先从文本中获取特征,这里的特征是来自文本的词条 。可以把词条想象为单词,也可以使用非单词词条,如URL、IP地址或者任意其他字符串。然后将每一个文本片段表示为一个词条向量,其中值为1表示词条出现在文档中,0表示词条未出现。
接下来首先给出将文本转换为数字向量的过程,然后介绍如何基于这些向量来计算条件概率,并在此基础上构建分类器,后还要介绍一些利用Python实现朴素贝叶斯过程中需要考虑的问题。
2.1 准备数据:从文本中构建词向量
我们将把文本看成单词向量或者词条向量,也就是说将句子转换为向量。考虑出现在所有文档中的所有单词,再决定将哪些词纳入词汇表或者说所要的词汇集合,然后必须要将每一篇文档转换为词汇表上的向量。
两个任务:
- 1、构建词典。步骤:遍历所有文档,找到所有的单词,并去重,合并到一个列表里;
- 2、创建文档向量。步骤:根据构建的词典,创建一个和词典相同大小的空列表,遍历指定文档所有单词,出现该单词在词典出现,该位置就记为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
创建词典:
def createVocabList(dataSet):
'''
给定数据集创建词典
'''
vocabSet = set([]) # 初始化词典为空集合
for document in dataSet: # 遍历数据集
vocabSet = vocabSet | set(document) # 取文档单词集合与前面词典做交集
return list(vocabSet)
创建文档向量:
def setOfWords2Vec(vocabList, inputSet):
'''
词集模型:将文本转为词向量
vocabList:词典
inputSet:输入的文本
'''
returnVec = [0]*len(vocabList) # 初始化与词典等长的列表,值都为0
for i, word in enumerate(inputSet): # 遍历文档的索引及单词
# 如果单词在字典出现,词向量的该索引位置就为1
if word in vocabList:
returnVec[i] = 1
else:
print ("the word: %s is not in my Vocabulary!" % word)
return returnVec
测试下代码:
# test1 创建词典
listOPosts, listClasses = loadDataSet()
myVocabList = createVocabList(listOPosts)
print(myVocabList)
# 词典如下:
['please', 'buying', 'steak', 'problems', 'to', 'stop', 'love', 'ate', 'licks', 'food', 'take', 'posting',
'has', 'park', 'how', 'my', 'I', 'him', 'not', 'help', 'maybe', 'so', 'quit', 'worthless', 'flea', 'stupid',
'is', 'garbage', 'mr', 'cute', 'dog', 'dalmation']
# test2 生成词向量函数
wordVec = setOfWords2Vec(myVocabList, listOPosts[0])
print(wordVec)
# 词向量如下
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
2.2 训练算法:从词向量计算概率
前面介绍了如何将一组单词转换为一组数字,接下来看看如何使用这些数字计算概率。现在已经知道一个词是否出现在一篇文档中,也知道该文档所属的类别。
我们重写贝叶斯准则,将之前的x、y 替换为w。w表示这是一个向量,即它由多个数值组成。在这个例子中,数值个数与词汇表中的词个数相同。
上面公式的意思就是,对于给定一个文档,把他转为词向量w,他属于类别ci的概率是多少。对每个类计算该值,然后比较这两个概率值的大小。
如何计算呢?首先p(ci)代表文档类别ci出现的概率,可以通过类别i(侮辱性留言或非侮辱性留言)中文档数除以总的文档数来计算概率p(ci)。接下来计算p(w|ci),这里就要用到朴素贝叶斯假设。如果将w展开为一个个独立特征,那么就可以将上述概率写作p(w0,w1,w2…wN|ci)。这里假设所有词都互相独立,该假设也称作条件独立性假设,它意味着可以使用p(w0|ci)p(w1|ci)p(w2|ci)…p(wN|ci)来计算上述概率,这就极大地简化了计算的过程。
那如何计算p(w0|ci)呢?假如要计算pp(w0|1),即当文档类型为1时,单词w0出现的概率,我们可以用统计的知识,在1类文档中出现单词w0的次数除以1类文档中单词总数。
注意:
- 计算p(w0|1)p(w1|1)p(w2|1)。如果其中一个概率值为0,那么后的乘积也为0。为降低这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。
- 当计算乘积 p(w0|ci)p(w1|ci)p(w2|ci)…p(wN|ci)时,由于大部分因子都非常小,所以程序会下溢出或者得到不正确的答案。一 种解决办法是对乘积取自然对数。在代数中有ln(a*b) = ln(a)+ln(b),于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,由于自然对数是单调递增,所以不会影响概率大小比较。
下面就来书写我们的函数,我希望得到两个东西:
- 1.计算每个单词出现的条件下,属于类别ci的概率,然后放在一个列表中,列表长度与字典长度相同。假如有两个类别,那么我们需要构建两个条件概率矩阵。
- 2.还需要计算每个类别出现的概率p(ci)。这里我们只有两个类别,所以只要计算一个就可以,另外一个就是1-p(ci)。
首先,需要构建一个所有文本的向量列表,用于计算我们的概率,它是把所有文本集转为词向量,然后放在同一个列表中。
# 构建所有文档的的词向量矩阵
trainMat = []
for doc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList, doc))
print(trainMat)
# 结果如下:
[[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
可以看到它是一个6x32的二维列表。其中6是文本的数量,32是词典的大小。
接下来计算概率:
def trainNB0(trainMatrix,trainCategory):
'''
计算概率
trainMatrix:文档矩阵nxm,n:文档数量,m:字典大小
trainCategory:每篇文档类别标签所构成的向量1xn。这里类别只有0和1。
'''
numTrainDocs = len(trainMatrix) # 文档的数量n
numWords = len(trainMatrix[0]) # 字典单词数量m
# 计算别类为1的概率p(1),则类别为0的概率就是p(0)不需要计算
pAbusive = sum(trainCategory)/float(numTrainDocs)
# 初始化类别为0时单词出现次数的矩阵为1,与词典长度相同
p0Num = np.ones(numWords)
# 初始化类别为1时单词出现次数的矩阵为1,与词典长度相同
p1Num = np.ones(numWords)
# 初始化类别为0和1的单词总数为2
p0Denom = 2.0
p1Denom = 2.0
for i in range(numTrainDocs): # 遍历每个文档
if trainCategory[i] == 1: # 如果文档类别为1
# 把该文档出现的单词数量都加1,这里采用的是直接矩阵相加
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i]) # 更新类别为1的文档单词总数
else: #对于类别0也做和1一样的运算
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
# 分别计算类别为1和0的情况下,每个单词出现的条件概率,这里加上log
p1Vect = np.log(p1Num/p1Denom) #change to log()
p0Vect = np.log(p0Num/p0Denom) #change to log()
return p0Vect,p1Vect,pAbusive
测试代码:
# test3
p0V,p1V,pAb = trainNB0(trainMat, listClasses)
print(p0V)
# 结果如下
[-1.87180218 -1.87180218 -1.87180218 -1.87180218 -1.87180218 -1.87180218
-1.87180218 -2.15948425 -2.56494936 -3.25809654 -3.25809654 -3.25809654
-3.25809654 -3.25809654 -3.25809654 -3.25809654 -3.25809654 -3.25809654
-3.25809654 -3.25809654 -3.25809654 -3.25809654 -3.25809654 -3.25809654
-3.25809654 -3.25809654 -3.25809654 -3.25809654 -3.25809654 -3.25809654
-3.25809654 -3.25809654]
print(p1V)
# 结果如下
[-1.65822808 -1.65822808 -1.65822808 -1.65822808 -1.65822808 -1.94591015
-2.35137526 -2.35137526 -3.04452244 -3.04452244 -3.04452244 -3.04452244
-3.04452244 -3.04452244 -3.04452244 -3.04452244 -3.04452244 -3.04452244
-3.04452244 -3.04452244 -3.04452244 -3.04452244 -3.04452244 -3.04452244
-3.04452244 -3.04452244 -3.04452244 -3.04452244 -3.04452244 -3.04452244
-3.04452244 -3.04452244]
print(pAb)
# 结果为0.5
2.3 测试算法
有了上面的概率矩阵,我们就可以计算我们的p(w|0)和p(w|1),然后比较大小,完成分类。代码如下:
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
'''
分类器
vec2Classify:要分类的词向量
p0Vec:上面已经计算好的类别为0的各个单词的条件概率,即p(wi|c0)
p0Vec:上面已经计算好的类别为1的各个单词的条件概率,即p(wi|c1)
pClass1:1类别的概率p(w1)
'''
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1) #element-wise mult
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
书中把以上所有代码封装到一个函数里了,我们看一下:
def testingNB():
'''
便利函数,封装以上所有操作
'''
listOPosts,listClasses = loadDataSet()
myVocabList = createVocabList(listOPosts)
trainMat=[]
for postinDoc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
p0V,p1V,pAb = trainNB0(np.array(trainMat),np.array(listClasses))
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))
下面测试一下:
#test4
testingNB()
# 结果如下
['love', 'my', 'dalmation'] classified as: 1
['stupid', 'garbage'] classified as: 1
2.4 词袋模型
目前为止,我们将每个词的出现与否作为一个特征,这可以被描述为词集模型(set-of-words model)。如果一个词在文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为词袋模型(bag-of-words model)。在词袋中,每个单词可以出现多次,而在词集中,每个词只能出现一次。为适应词袋模型,需要对函数setOfWords2Vec()稍加修改,修改后的函数称为bagOfWords2Vec()。
def bagOfWords2VecMN(vocabList, inputSet):
'''
词集模型:将文本转为词向量
vocabList:词典
inputSet:输入的文本
'''
returnVec = [0]*len(vocabList) # 初始化与文本等长的列表
for i, word in enumerate(inputSet): # 遍历文档的索引及单词
# 如果单词在字典出现,词向量的该索引位置就为1
if word in vocabList:
returnVec[i] += 1
else:
print ("the word: %s is not in my Vocabulary!" % word)
return returnVec
3. 使用朴素贝叶斯过滤垃圾邮件
使用朴素贝叶斯解决一些现实生活中的问题时,需要先从文本内容得到字符串列表,然后生成词向量。下面这个例子中,我们将了解朴素贝叶斯的一个著名的应用:电子邮件垃圾过滤。首先看一下如何使用通用框架来解决该问题。
- 收集数据:提供文本文件。
- 准备数据:将文本文件解析成词条向量。
- 分析数据:检查词条确保解析的正确性。
- 训练算法:使用我们之前建立的trainNB0()函数。
- 测试算法:使用classifyNB(),并且构建一个新的测试函数来计算文档集的错误率。
- 使用算法:构建一个完整的程序对一组文档进行分类,将错分的文档输出到屏幕上。
3.1 准备数据:切分文本
对于一个文本字符串,可以使用Python的string.split()方法将其切分。Python中有一些内嵌的方法,可以将字符串全部转换成小写(.lower())或者大写(.upper()), 借助这些方法可以转换大小写。
现在来看数据集中一封完整的电子邮件的实际处理结果。该数据集放在email文件夹中,该文件夹又包含两个子文件夹,分别是spam与ham。
接下来将构建一个极其简单的函数,可以根据情况自行修改。
def textParse(bigString):
'''
输入含大写字母的字符串,切割为单词列表,且过滤掉长度小于2的单词
'''
import re
listOfTokens = re.split(r'\W+', bigString) # r'\W+'匹配所有非字母和数字的字符
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
3.2 测试算法:使用朴素贝叶斯进行交叉验证
下面将文本解析器集成到一个完整分类器中。
def spamTest():
'''
对贝叶斯垃圾邮件分类器进行自动化处理
'''
docList=[] # 文档分割后的单词矩阵,用于创建词典
classList = [] # 文档类别列表
fullText =[] # 存储所有单词的列表
# 遍历垃圾邮件文档和正常文档,分别是25个
for i in range(1,26):
# 文档转为单词列表
wordList = textParse(open('email/spam/%d.txt' % i).read())
docList.append(wordList) # 单词列表添加到文档矩阵
fullText.extend(wordList) # 单词列表添加到所有单词的列表
classList.append(1) # 类别列表加个1元素
# 对于正常样本做相同操作
wordList = textParse(open('email/ham/%d.txt' % i).read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(0)
vocabList = createVocabList(docList) # 创建词典
# 初始化训练集和测试集
trainingSet = list(range(50)) # 训练集索引列表
testSet=[] # 测试集索引列表
# 8:2划分训练集和测试集
for i in range(10):
randIndex = int(np.random.uniform(0,len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
trainMat=[] # 训练数据的词向量矩阵
trainClasses = [] # 训练数据的类别列表
for docIndex in trainingSet:
trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
trainClasses.append(classList[docIndex])
# 计算训练样本的概率
p0V,p1V,pSpam = trainNB0(np.array(trainMat),np.array(trainClasses))
# 初始化错误的样本数
errorCount = 0
# 验证测试集的正确性
for docIndex in testSet:
# 把测试样本转为词向量
wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
# 如果模型的分类结果不等于真实结果
if classifyNB(np.array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
errorCount += 1
print ("classification error",docList[docIndex])
print ('the error rate is: ',float(errorCount)/len(testSet))
测试一下函数:
spamTest()
# 结果如下
classification error ['peter', 'with', 'jose', 'out', 'town', 'you', 'want', 'meet', 'once', 'while', 'keep', 'things', 'going', 'and', 'some', 'interesting', 'stuff', 'let', 'know', 'eugene']
classification error ['get', 'off', 'online', 'watchesstore', 'discount', 'watches', 'for', 'all', 'famous', 'brands', 'watches', 'arolexbvlgari', 'dior', 'hermes', 'oris', 'cartier', 'and', 'more', 'brands', 'louis', 'vuitton', 'bags', 'wallets', 'gucci', 'bags', 'tiffany', 'jewerly', 'enjoy', 'full', 'year', 'warranty', 'shipment', 'via', 'reputable', 'courier', 'fedex', 'ups', 'dhl', 'and', 'ems', 'speedpost', 'you', 'will', '100', 'recieve', 'your', 'order', 'save', 'off', 'quality', 'watches']
classification error ['arvind', 'thirumalai', 'commented', 'your', 'status', 'arvind', 'wrote', 'you', 'know', 'reply', 'this', 'email', 'comment', 'this', 'status']
the error rate is: 0.3
函数spamTest()会输出在10封随机选择的电子邮件上的分类错误率。既然这些电子邮件是随机选择的,所以每次的输出结果可能有些差别。如果发现错误的话,函数会输出错分文档的词表,这样就可以了解到底是哪篇文档发生了错误。如果想要更好地估计错误率,那么就应该将上述过程重复多次,比如说10次,然后求平均值。
书中还有其他案例可自行学习。
总结
对于分类而言,使用概率有时要比使用硬规则更为有效。贝叶斯概率及贝叶斯准则提供了一种利用已知值来估计未知概率的有效方法。
可以通过特征之间的条件独立性假设,降低对数据量的需求。独立性假设是指一个词的出现概率并不依赖于文档中的其他词。当然我们也知道这个假设过于简单。这就是之所以称为朴素贝叶斯的原因。尽管条件独立性假设并不正确,但是朴素贝叶斯仍然是一种有效的分类器。
利用现代编程语言来实现朴素贝叶斯时需要考虑很多实际因素。下溢出就是其中一个问题,它可以通过对概率取对数来解决。词袋模型在解决文档分类问题上比词集模型有所提高。还有其他一些方面的改进,比如说移除停用词,当然也可以花大量时间对切分器进行优化。