机器学习算法系列(7)朴素贝叶斯法

朴素贝叶斯法,即naive Bayesian method,之所以“naive”,大概是因为它只用到了浅显的概率论知识,(注意!本文没有任何的数学公式推导!)理解起来也相当容易。但不要看它naive就小瞧了它,朴素贝叶斯法基于贝叶斯定理和特征条件独立,是一种分类方法,它在分类任务中应用相当广泛。在这篇文章中,我会详细介绍贝叶斯法中的三个重要概念,以及用一个小故事和一个Python实现简单文本分类的例子帮助理解。


假设你的朋友近来发现笔记本玩游戏越来越卡,关键时候团战竟然掉帧卡顿,到手的五杀生生没了。为了更好地守护高地,他决定组装一台台式机,在网上苦苦搜寻,找来了一份主要部件的配置单,并征求你的建议,能不能胜任大型游戏

内存CPU主板硬盘
金士顿 骇客神条 8GIntel i5-6600华硕 M5A78L-M西数蓝盘 1TB

虽然你对计算机硬件不太懂,但是你会机器学习算法啊!这时,你手头正好有某神秘组织给你的计算机性能评测数据,那么如何利用好这些数据为你的朋友做出正确的选择呢?假设你有这方面的困惑,来,下面将向你展示如何用经典的贝叶斯方法解决这个问题。


贝叶斯其人

英国统计学家托马斯·贝叶斯(1701?-1761),后来成为了英国皇家学会成员,同时他还是一名哲学家。贝叶斯以前做过神父,为了证明上帝的存在,他建立了概率统计学原理,遗憾的是,这个愿望直到他死都没有实现。贝叶斯本人和他的研究工作在他当时并没有受到多少人的关注,他是如何入选英国皇家学会会员也鲜有记载,直到后来数学家拉普拉斯使他的工作重新为世人所肯定,贝叶斯方法才逐渐为世人瞩目。

托马斯·贝叶斯

托马斯·贝叶斯


三个简单的概念

朴素贝叶斯法的核心是贝叶斯定理,实际上是一个很简单的公式

\[\begin{equation}\begin{split} P(Y|X)=\frac{P(Y)P(X|Y)}{P(X)}\\ \label{bayesrule} \end{split}\end{equation}\]

假设我们有数据集\(Y=(y_1,y_2,...,y_m)\)\(X=(x_1,x_2,...,x_n)\)是数据集的分类标记,我们考虑几个基本的概念

先验概率

先验概率分布是基于已有的数据集得出的,比如要判断一台计算机的性能(\(Y=(strong,weak)\))如何,那么我们可以根据已有的评测数据来进行判断。由已有的评测数据,可以轻松得到计算机性能强弱的概率,这也就是所谓的先验概率。先验概率很容易计算

\[\begin{equation}\begin{split} P(Y=y_i),k=1,2...,m\\ \label{prior} \end{split}\end{equation}\]

条件概率

一般,要判断计算机性能强弱,我们会考虑一些什么特征\(X\)呢?例如,处理器核心数多少、硬盘类型、显卡性能高低、内存大小等等。条件概率就是假设事件发生时,其他现象发生的概率,如考虑计算机性能很强情形下,处理器、硬盘、显卡指标分别所占的概率。

\[\begin{equation}\begin{split} P(X=x_j|Y=y_i),k=1,2,...,m\\ \label{conditional} \end{split}\end{equation}\]

条件独立性假设

条件独立性是贝叶斯法中最重要的一个假设,也就是在分类\(y\)在确定的情况下,各个条件之间是相互独立的,不然在计算\(\eqref{conditional}\)就会有指数级的灾难。既然我们假定各个特征之间相互是不影响的,这样一来,计算就简化了许多。

\[\begin{equation}\begin{split} P(X=x_j|Y=y_i)=\prod_{j=1}^{n}P(x_j|Y=y_i)\\ \label{indepentassumption} \end{split}\end{equation}\]

了解了三个重要概念之后,贝叶斯方法的学习远远还没有结束,仅仅知道基本的概念不足以完成分类的任务。类似于logistic regression,贝叶斯方法实际上也是一种基于概率的学习方法,它也是通过比较概率的相对大小来决定分类的结果。


后验概率最大化

在贝叶斯统计中,一个随机事件或者一个不确定事件的后验概率是在考虑和给出相关证据或数据后所得到的条件概率。

计算\(P(Y=y_i|X=x_j)\)是最终目标,即后验概率。最大化后验概率化也是贝叶斯方法的目标,也就是我们要让式\(\eqref{bayesrule}\)\(P(X|Y)\)最大

\[\begin{equation}\begin{split} arg \quad max\quad \prod_{j=1}^{n}P(x_j|Y=y_i)\\ \label{posteriorprobability} \end{split}\end{equation}\]

贝叶斯估计

注意,朴素贝叶斯法和贝叶斯估计是不同的概念。朴素贝叶斯法中,用极大似然估计\(\eqref{posteriorprobability}\)可能会出现所要估计的概率值为0的情形,这种情形出现一般有两种原因:一是条件概率都很小,连乘会导致后验概率变得很小,一般会通过取对数的方式来避免;二是,当某个特征在训练集中没有与某个类同时出现过,那么直接计算的话,后验概率那就只能为零了,为了规避这种情况,通常要进行“平滑”(smoothing)处理,即拉普拉斯平滑(Laplace smoothing),我们也称这一方法为贝叶斯估计。如下式

\[\begin{equation}\begin{split} P(X=x_j|Y=y_i)=\frac{|I_{x_j}|+\lambda}{|I_{y_i=x_j}|+N_i\lambda}\\ \label{laplacesmoothing} \end{split}\end{equation}\]

\(\eqref{laplacesmoothing}\)\(\lambda\)取0是就是极大似然估计。当\(\lambda\)取1是,就是拉普拉斯平滑,它能够帮助避免训练样本集不充分而导致的概率估值为零的问题。


到底配置单是坑还是…?

为了解决你朋友的问题,你根据神秘组织给你的数据,这组数据根据内存容量、CPU、显卡等指标评估计算机性能。

内存容量CPU核心数显卡硬盘性能强弱
8G单核心集成机械
8G单核心集成固态
4G单核心集成机械
2G双核心集成机械
2G四核心独立机械
2G四核心独立固态
4G四核心独立固态
8G双核心集成机械
8G四核心独立机械
2G双核心独立机械
8G双核心独立固态
4G双核心集成固态
4G单核心独立机械
2G双核心集成固态

现在,贝叶斯就开始派上用场了!

首先,根据\(\eqref{prior}\)先验概率,性能强弱指标下只有强弱之分,令\(y_1\)表示计算机性能“强”,\(y_2\)表示计算机性能“弱”。显然, \[P(Y=y_1)=(9+1)/(14+2)=5/8\\ P(Y=y_0)=(5+1)/(14+2)=3/8\]

由文章开头给出的配置单,对应的特征\(X=(8G,4cores,integrated,hdd)\)。然后,计算条件概率

\[P(“8G”|Y=y_1)=(2+1)/(9+3)=3/12\\ P(“8G”|Y=y_0)=(3+1)/(5+3)=4/8\\ P(“4cores”|Y=y_1)=(3+1)/(9+3)=4/12\\ P(“4cores”|Y=y_0)=(1+1)/(5+3)=2/8\\ P(“integrated”|Y=y_1)=(3+1)/(9+2)=4/11\\ P(“integrated”|Y=y_0)=(4+1)/(5+2)=5/7\\ P(“hdd”|Y=y_1)=(3+1)/(9+2)=4/11\\ P(“hdd”|Y=y_0)=(3+1)/(5+2)=4/7\\ \]

对于给定的\(X=(“8G”,“4cores”,“integrated”,“hdd”)\)计算

\[P(Y=y_1)P(“8G”|Y=y_1)P(“4cores”|Y=y_1)P(“integrated”|Y=y_1)P(“hdd”|Y=y_1)=0.0069\] \[P(Y=y_0)P(“8G”|Y=y_0)P(“4cores”|Y=y_0)P(“integrated”|Y=y_0)P(“hdd”|Y=y_0)=0.0191\]

因为\(P(Y=y_0)P(“8G”|Y=y_0)P(“4cores”|Y=y_0)P(“integrated”|Y=y_0)P(“hdd”|Y=y_0)\)较大,所以\(Y=y_0\),从而可以初步判断按照此配置单组装的电脑性能为弱,绝对又是一个坑!(团战掉线,就跟那些无止尽刷野的坑货一样,都应该封号!)

说点题外话,上面例子中提到的配置单问题应该出现在了显卡上,集成显卡的属性使得整个性能大打折扣,所以关键的地方还是要抛弃原有的主板,考虑买一块独立显卡。


例子:利用贝叶斯进行简单的文本分类

本例所用到的代码来自《机器学习实战》,非常值得推荐。代码虽然不多,但是给我最大的体会就是,懂算法的原理和实实在在在工程中应用实现是完全不同的两回事。正如本博客翻译的Numpy入门教程之前,我一直不明白在图像上怎么进行机器学习的工程应用,后来我翻译完教材之后才恍然大悟,原来图片的每一个像素点可以视为一个数字,所有的像素点不就组成了一个数组吗?开始看贝叶斯的时候,我也一直纳闷,这个贝叶斯方法怎么就可以用于文本分类呢?看不懂的时候,我就联想本文讲的这个组装电脑的故事,一点一点理解,明白了我得先有一个数据集,然后,在现有数据集的基础上计算先验概率,计算先验概率的时候还得注意数值下溢的问题,不然无法理解代码中的log(1.0 - pClass1)log(pClass1)。光看算法,光看书没用啊!拿个例子来做一做,理解会更加深刻!

处理过程

  1. 由样本数据集创建词汇表
  2. 根据输入的待划分数据创建词向量
  3. 计算先验概率,然后比较后验概率
  4. 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from numpy import *
#导入样本数据集
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) #union of the two sets
return list(vocabSet)
#由输入词汇创建词向量
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0]*len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] = 1 #如果word在词汇表中,则向量相应位置为1
else: print "the word: %s is not in my Vocabulary!" % word
return returnVec
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix)
numWords = len(trainMatrix[0])
pAbusive = sum(trainCategory)/float(numTrainDocs) #实验样本中正例的个数,根据第一个函数,pAbusive=0.5
p0Num = ones(numWords); p1Num = ones(numWords) #change to ones()
p0Denom = 2.0; p1Denom = 2.0 #所有词的个数出现次数初始化为1,为了避免概率出现0的情形
for i in range(numTrainDocs):
if trainCategory[i] == 1: #一旦某个词在文档中出现,则该词对应的个数就要加1
p1Num += trainMatrix[i] #trainMatrix是一个array,故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
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + log(pClass1) #element-wise mult
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:
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
p0V,p1V,pAb = trainNB0(array(trainMat),array(listClasses))
testEntry = ['love', 'my', 'dalmation']
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb)
testEntry = ['stupid', 'garbage']
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb)

个人在理解trainNB0(trainMatrix,trainCategory)这个函数的时候卡住了一下,因为我事先没注意到trainMatrix是一个array,因为它的数据类型是列表,所以我不明白为什么p1NumtrainMatrix[i]可以直接叠加(一个数组跟一个数字相加干嘛呢?)。另外,trainNB0(trainMatrix,trainCategory)还用到了拉普拉斯平滑的手段来防止概率为0的情形。这个例子还是存在不足之处的,比如说,数据集太小导致泛化能力不佳,如果我用一个在样本数据集中从未出现过的词来做测试,这个时候测试函数就无法正确检测了。

参考

  1. https://zh.wikipedia.org/wiki/后验概率
  2. 统计学习方法.李航
  3. 机器学习.周志华
  4. 机器学习实战
  5. 一些关于贝叶斯的文章
    1. 学界 | 清华大学计算机系朱军教授:机器学习里的贝叶斯基本理论、模型和算法
    2. 贝叶斯推断及其互联网应用(一):定理简介
    3. 数学之美番外篇:平凡而又神奇的贝叶斯方法
觉得还不错?帮我赞助点域名费吧:)