一、 朴素贝叶斯理论
1、什么是朴素贝叶斯
朴素贝叶斯(naive Bayes)是贝叶斯决策理论的一部分,是基于贝叶斯定理与特征条件独立假设的分类方法,它是一种简单但极为强大的预测建模算法。之所以称为朴素贝叶斯,是因为它假设每个输入变量是独立的。这个假设很硬,现实生活中根本不满足,但是这项技术对于绝大部分的复杂问题仍然非常有效。最为广泛的两种分类模型是决策树模型(Decision Tree Model)和朴素贝叶斯模型(Naive Bayesian Model,NBM)。
2、朴素贝叶斯的算法原理
朴素贝叶斯分类(NBC)是以贝叶斯定理为基础并且假设特征条件之间相互独立的方法,先通过已给定的训练集,以特征词之间独立作为前提假设,学习从输入到输出的联合概率分布,再基于学习到的模型,输入X求出使得后验概率最大的输出Y。
计算原理:
我们使用P1(x,y)表示数据点(x,y)属于类别1(如下图橘色的圆点)的概率,使用P2(x,y)表示数据点(x,y)属于类别2(如下图蓝色的叉叉)的概率,那么,对于一个新的数据点(x1,y1),我们可以用以下规则来判断它的类别:
- 如果p1(x1,y1) > p2(x1,y1),那么这个点类别为1;
- 如果p1(x1,y1) < p2(x1,y1),那么这个点类别为2;
- 如果p1(x1,y1) = p2(x1,y1),那么这个点可设为1也可设为2。
3、贝叶斯分类典型应用
- 垃圾邮件过滤
- 文本情感分析:电影评论
- 论坛真实账号监测
- 浏览器输入自动提示
二、几个必知的概率
1、先验概率和后验概率
- 先验概率(Prior probability) :指根据以往经验和分析得到的概率,如全概率公式,它往往作为"由因求果"问题中的"因"出现的概率。
- 后验概率(Posterior probability ):指在得到“结果”的信息后重新修正的概率,是“执果寻因”问题中的"果"。就是发生结果之后,推测原因的概率。
举个例子:
- 如果一个不透明袋子中有十个球,4个黑球、6个白球,你现在把手伸进去随机抓取一个,那么抓到黑球的概率很明显就是4/10,也就是2/5,这就是所谓的先验概率。
- 而后验概率就是你在不知道里面有多少球的情况下,你随机抓取几次,根据你抓取的情况去判断黑球以及白球的比例,最终算出来的每次抓取得到哪种球的概率。
2、联合概率和边缘概率
- 联合概率(joint probability)指的是包含多个条件且所有条件同时成立的概率,记作P(X=a,Y=b)或P(a,b),有的书上也习惯记作P(ab),但是这种记法个人不太习惯,所以下文采用以逗号分隔的记法。
- 边缘概率(marginal probability)是与联合概率对应的,P(X=a)或P(Y=b),这类仅与单个随机变量有关的概率称为边缘概率。
这个很好理解,举一个和之前差不多的例子:
- 如果一个不透明袋子中有十个球,4个黑球、6个白球,你现在把手伸进去随机抓取一个,这时你抽取黑球的概率,这就是边缘概率,也就是只有一个条件下某件事情出现的概率。
- 这时,如果还有一个装了6个球,其中3个黑球、3个白球的袋子,两个袋子不知道哪个是哪个的情况下,请问你在第二个袋子中抽到黑球的概率,这个就是一个求联合概率的问题。
3、条件概率
条件概率是我们朴素贝叶斯算法的核心,是必须要理解的一个概率问题。
条件概率 (conditional probability) 是指在事件 B 发生的情况下,事件 A 发生的概率,通常记为 P(A | B)。推导如下:
P ( A ∣ B ) = P ( A ⋂ B ) P ( B ) P(A|B) = \frac{P(A \bigcap B)}{P(B)} P(A∣B)=P(B)P(A⋂B)因此: P ( A ⋂ B ) = P ( A ∣ B ) P ( B ) P(A \bigcap B)=P(A|B)P(B) P(A⋂B)=P(A∣B)P(B) P ( A ⋂ B ) = P ( B ∣ A ) P ( A ) P(A \bigcap B) = P(B|A)P(A) P(A⋂B)=P(B∣A)P(A)可得: P ( A ∣ B ) P ( B ) = P ( B ∣ A ) P ( A ) P(A|B)P(B)=P(B|A)P(A) P(A∣B)P(B)=P(B∣A)P(A)由此可以推出朴素贝叶斯公式: P ( A ∣ B ) = P ( B ∣ A ) P ( A ) P ( B ) P(A|B)= \frac{P(B|A)P(A)}{P(B)} P(A∣B)=P(B)P(B∣A)P(A)这也就是条件概率的计算公式。
这个公式指的是:
在B事件的条件之下,算A事件的概率,我们可以拆分为:已知的在A事件概率下B事件的概率乘以B事件的概率,再除以A事件的概率。
三、如何去计算
由前面我们知道了一个求条件概率的公式: P ( A ∣ B ) = P ( B ∣ A ) P ( A ) P ( B ) P(A|B)= \frac{P(B|A)P(A)}{P(B)} P(A∣B)=P(B)P(B∣A)P(A)在我们的朴素贝叶斯中,如何去运用这个公式呢?
根据我们的条件概率公式,我们对此进行一些推导,则可转化为: p ( y i ∣ x ) = p ( x ∣ y i ) p ( y i ) p ( x ) p(y_i|x)=\frac{p(x|y_i)p(y_i)}{p(x)} p(yi∣x)=p(x)p(x∣yi)p(yi)由此我们展开可得: p ( y i ∣ x ) = p ( x 1 ∣ y i ) p ( x 2 ∣ y i ) . . . p ( x n ∣ y i ) p ( y i ) p(y_i|x)=p(x_1|y_i)p(x_2|y_i)...p(x_n|y_i)p(y_i) p(yi∣x)=p(x1∣yi)p(x2∣yi)...p(xn∣yi)p(yi)注意:在这个地方,因为我们求的不是概率,而是概率大小,而同条件下,求不同事件的概率,在最后进行除法时,除的是同一个概率,所以仅仅比较大小的情况下不需要再进行一次除法,从而得到上面的公式。
接下来我由一个示例图举一个例子讲述一下怎么计算的:
编号 | X1 | X2 | Y |
---|---|---|---|
1 | 1 | S | -1 |
2 | 1 | M | -1 |
3 | 1 | M | 1 |
4 | 1 | S | 1 |
5 | 1 | S | -1 |
6 | 2 | S | -1 |
7 | 2 | M | -1 |
8 | 2 | M | 1 |
9 | 2 | L | 1 |
10 | 2 | L | 1 |
11 | 3 | L | 1 |
12 | 3 | M | 1 |
13 | 3 | M | 1 |
14 | 3 | L | 1 |
15 | 3 | L | -1 |
如表,可知,总共有15条数据,我们可知几个基础的概率:
P(Y=1) = 9/15, P(Y=-1) = 6/15
由此我们可以求一些相关的条件概率:
Y=1时X1各个值的概率 | Y=1时X2各个值的概率 | Y=-1时X1各个值的概率 | Y=-1时X2各个值的概率 |
---|---|---|---|
P(X1=1|Y=1)=2/9 | P(X2=S|Y=1)=1/9 | P(X1=1|Y=-1)=3/6 | P(X2=S|Y=-1)=3/6 |
P(X1=2|Y=1)=3/9 | P(X2=M|Y=1)=4/9 | P(X1=2|Y=-1)=2/6 | P(X2=M|Y=-1)=2/6 |
P(X1=3|Y=1)=4/9 | P(X2=L|Y=1)=4/9 | P(X1=3|Y=-1)=1/6 | P(X2=L|Y=-1)=1/6 |
所以,我们可以求出X1=2,X2=S时Y=1的概率大还是Y=-1的概率大:
P
(
Y
=
1
∣
X
1
=
2
,
X
2
=
S
)
=
P
(
X
1
=
2
∣
Y
=
1
)
∗
P
(
X
2
=
S
∣
Y
=
1
)
∗
P
(
Y
=
1
)
=
3
9
∗
1
9
∗
9
15
=
1
45
P(Y=1|X_1=2,X_2=S)=P(X_1=2|Y=1)*P(X_2=S|Y=1)*P(Y=1)=\frac{3}{9} * \frac{1}{9}* \frac{9}{15}=\frac{1}{45}
P(Y=1∣X1=2,X2=S)=P(X1=2∣Y=1)∗P(X2=S∣Y=1)∗P(Y=1)=93∗91∗159=451
P
(
Y
=
−
1
∣
X
1
=
2
,
X
2
=
S
)
=
P
(
X
1
=
2
∣
Y
=
−
1
)
∗
P
(
X
2
=
S
∣
Y
=
−
1
)
∗
P
(
Y
=
−
1
)
=
2
6
∗
3
6
∗
6
15
=
1
15
P(Y=-1|X_1=2,X_2=S)=P(X_1=2|Y=-1)*P(X_2=S|Y=-1)*P(Y=-1)=\frac{2}{6} * \frac{3}{6} * \frac{6}{15} =\frac{1}{15}
P(Y=−1∣X1=2,X2=S)=P(X1=2∣Y=−1)∗P(X2=S∣Y=−1)∗P(Y=−1)=62∗63∗156=151
明显可知,该条件下它的标记应该是Y=-1。这就是朴素贝叶斯在计算概率分类时的原理。
四、代码实现
1、数据准备工作
from numpy import *
import re
import feedparser
# 词表到向量的转换函数
def loadDataSet(): # 创建一些实验样本
posttingList = [['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 posttingList, classVec
def createVocabList(dataSet): # 创建一个包含在所有文档中出现的不重复词的列表,为此使用Python的set数据类型
vocabSet = set([])
for document in dataSet:
vocabSet = vocabSet | set(document) # 去重
return list(vocabSet)
def setOfWords2Vec(vocabList, inputSet): # 该函数的输入参数为词汇表及某个文档,输出的是文档向量
returnVec = [0] * len(vocabList) # 创建一个其中所含元素都为0的向量
for word in inputSet: # 遍历每个词条
if word in vocabList: # 如果词条存在于词汇表中,则置1
returnVec[vocabList.index(word)] = 1
else:
print('the word: ' + word + 'is not in my Vocabulary!')
return returnVec
在这里,我们创建了一个6条数据,并对每条数据进行了分类,这个过程得到的数据如下:
listOPosts, listClasses = loadDataSet()
myVovatbList = createVocabList(listOPosts)
myVovatbList
>>> ['food', 'love', 'is', 'quit', 'stop', 'steak', 'posting', 'has', 'how', 'maybe', 'worthless', 'not', 'him', 'licks', 'ate', 'problems', 'please', 'dog', 'stupid', 'mr', 'my', 'park', 'dalmation', 'I', 'help', 'cute', 'flea', 'garbage', 'buying', 'take', 'so', 'to']
setOfWords2Vec(myVovatbList, listOPosts[3])
>>> [0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]
2、朴素贝叶斯分类器训练函数
# 朴素贝叶斯分类器训练函数
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) # 计算训练的文档数目
numWords = len(trainMatrix[0]) # 计算每篇文档的词条数
pAbusive = sum(trainCategory) / float(numTrainDocs) # 文档属于侮辱类的概率
p0Num = ones(numWords)
p1Num = ones(numWords) # 创建NumPy.ones数组,词条出现数初始化为1,拉普拉斯平滑
p0Denom = 2.0
p1Denom = 2.0 # 分母初始化为2,拉普拉斯平滑
for i in range(numTrainDocs):
if trainCategory[i] == 1: # 统计属于侮辱类的条件概率所需的数据
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else: # 统计属于非侮辱类的条件概率所需的数据
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p1Vect = log(p1Num / p1Denom) # 取对数,防止下溢出
p0Vect = log(p0Num / p0Denom)
return p0Vect, p1Vect, pAbusive # 返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率
3、朴素贝叶斯分类函数
# 朴素贝叶斯分类函数
def classifyNb(vec2Classify, p0Vec, p1Vec, pClass1): # 对词进行分类
p1 = sum(vec2Classify * p1Vec) + log(pClass1)
p0 = sum(vec2Classify * p0Vec) + 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: # 6个向量
trainMat.append(setOfWords2Vec(myVocabList, postinDoc)) # 将一个一个样本,利用词汇表转换为词向量
p0V, p1V, pAb = trainNB0(array(trainMat), array(listClasses)) # 训练-计算条件概率 0-非侮辱类条件概率集 1-侮辱类条件概率集
testEntry = ['love', 'my', 'dalmation']
thisDoc = array(setOfWords2Vec(myVocabList, testEntry)) # 测试样本向量化
print(str(testEntry) + ' classified as: ' + str(classifyNb(thisDoc, p0V, p1V, pAb)))
testEntry = ['stupid', 'garbage']
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print(str(testEntry) + ' classified as: ' + str(classifyNb(thisDoc, p0V, p1V, pAb)))
在这里,我们输入了一些词进行测试,运行结果如下:
testingNB()
>>> ['love', 'my', 'dalmation'] classified as: 0
['stupid', 'garbage'] classified as: 1
五、垃圾邮件过滤示例
在这里给大家演示一下书上的例子,垃圾邮件过滤,本示例的数据集来自书中所给,如果有兴趣的同学可以点击此处下载书中相关的数据集以及代码,同时里面也带有《机器学习实战》英文版和中文版PDF电子书(侵权请告知)。下面我演示一下这个示例,以及最后的结果。
# 文件解析及完整的垃圾邮件测试函数
def textParse(bigString): # 接收一个大写字符串并将其解析为字符串列表
regEx = re.compile('\\W*')
listOfTokens = regEx.split(str(bigString))
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
def spamTest(): # 对贝叶斯垃圾邮件分类器进行自动化处理
docList = []
classList = []
fullText = []
for i in range(1, 26): # 访问25个txt文件
wordList = textParse(open('email/spam/'+ str(i) +'.txt', 'rb').read()) # 读取每个垃圾邮件,并将字符串转换成字符串列表
docList.append(wordList)
fullText.extend(wordList)
classList.append(1) # 标记垃圾邮件,1表示垃圾文件
wordList = textParse(open('email/ham/'+ str(i) +'.txt', 'rb').read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(0) # 标记非垃圾邮件,0表示非垃圾邮件
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(array(trainMat), array(trainClasses)) # 训练朴素贝叶斯模型
errorCpunt = 0 # 错误分类计数
for docIndex in testSet: # 遍历测试集
wordVector = setOfWords2Vec(vocabList, docList[docIndex]) # 测试集的词集模型
if classifyNb(array(wordVector), p0V, p1V, pSpam) != classList[docIndex]: # 如果分类错误的情况发生
errorCpunt += 1 # 错误计数加1
print('the error rate is: ' + str(float(errorCpunt)/len(testSet)))
通过运行该模型,最终我们可以得到一个结果,但是,请注意: 因为每次在取数据集的时候是随机的,所有最终的结果会有一定的误差,这里我给出三次运行的结果:
spamTest()
>>> the error rate is: 0.1
spamTest()
>>> the error rate is: 0.2
spamTest()
>>> the error rate is: 0.0
六、总结
优点:
- 朴素贝叶斯模型发源于古典数学理论,有稳定的分类效率。
- 对小规模的数据表现很好,能个处理多分类任务,适合增量式训练,尤其是数据量超出内存时,我们可以一批批的去增量训练。
- 对缺失数据不太敏感,算法也比较简单,常用于文本分类。
缺点:
- 理论上,朴素贝叶斯模型与其他分类方法相比具有最小的误差率。但是实际上并非总是如此,这是因为朴素贝叶斯模型给定输出类别的情况下,假设属性之间相互独立,这个假设在实际应用中往往是不成立的,在属性个数比较多或者属性之间相关性较大时,分类效果不好。而在属性相关性较小时,朴素贝叶斯性能最为良好。对于这一点,有半朴素贝叶斯之类的算法通过考虑部分关联性适度改进。
- 需要知道先验概率,且先验概率很多时候取决于假设,假设的模型可以有很多种,因此在某些时候会由于假设的先验模型的原因导致预测效果不佳。
- 由于我们是通过先验和数据来决定后验的概率从而决定分类,所以分类决策存在一定的错误率。
- 对输入数据的表达形式很敏感。