机器学习4—朴素贝叶斯学习笔记 从香农熵到手推KL散度:一文带你纵览机器学习中的信息论: 深入浅出最大似然估计: 抽球 例子2:正态分布 朴素贝叶斯算法原理小结 《机器学习实战》笔记之四——基于概率论的分类方法:朴素贝叶斯 机器学习实战之朴素贝叶斯 1. 朴素贝叶斯相关的统计学知识 2. 朴素贝叶斯的模型 3. 朴素贝叶斯的推断过程 4. 朴素贝叶斯的参数估计 5. 朴素贝叶斯算法过程 6. 朴素贝叶斯算法小结
信息论与信息熵是 AI 或机器学习中非常重要的概念,我们经常需要使用它的关键思想来描述概率分布或者量化概率分布之间的相似性。在本文中,我们从最基本的自信息和信息熵到交叉熵讨论了信息论的基础,再由最大似然估计推导出 KL 散度而加强我们对量化分布间相似性的理解。最后我们简要讨论了信息熵在机器学习中的应用,包括通过互信息选择决策树的特征、通过交叉熵衡量分类问题的损失和贝叶斯学习等。
>>>>
信息论是应用数学的一个分支,主要研究的是对一个信号包含信息的多少进行量化。它最初被发明是用来研究在一个含有噪声的信道上用离散的字母表来发送消息,例如通过无线电传输来通信。而本文主要探讨信息熵在 AI 或机器学习中的应用,一般在机器学习中,我们可以将信息论应用在连续型变量上,并使用信息论的一些关键思想来描述概率分布或者量化概率分布之间的相似性。
因此在机器学习中,通常要把与随机事件相关信息的期望值进行量化,此外还要量化不同概率分布之间的相似性。在这两种情况下,香农熵都被用来衡量概率分布中的信息内容。香农熵是以信息论之父 Claude Shannon 的名字命名的,也称为信息熵或微分熵(连续)。
自信息
香农熵的基本概念就是所谓的一个事件背后的自信息(self-information),有时候也叫做不确定性。自信息的直觉解释如下,当某个事件(随机变量)的一个不可能的结果出现时,我们就认为它提供了大量的信息。相反地,当观察到一个经常出现的结果时,我们就认为它具有或提供少量的信息。将自信息与一个事件的意外性联系起来是很有帮助的。例如,一个极其偏畸的硬币,每一次抛掷总是正面朝上。任何一次硬币抛掷的结果都是可以完全预测的,这样的话我们就永远不会对某次结果感到惊奇,也就意味着我们从这个实验中得到的信息是 0。换言之,它的自信息是 0。如果硬币的偏畸程度稍微小一些,这样的话,尽管看到正面朝上的概率超过了 50%,每次抛掷还会有一些信息。因此,它的自信息大于 0。如果硬币的偏畸程度是导致反面朝上的结果,我们得到的自信息还是 0。在使用一个没有偏畸的硬币做实验时,每次抛掷得到正面朝上和反面朝上的概率都是 50%,我们会得到最大的意外性,因为在这种情况下硬币抛掷的结果的可预测性是最小的。我们也可以说,均匀分布的熵最大,确定事件的熵最小。
基于以上的非正式需求,我们可以找到一个合适的函数来描述自信息。对于一个可能取值为 x_1,x_2,...,x_n 的离散随机变量 X,它的概率质量函数 P(X),以及任何正的取值在 0 到 1 之间的单调递减函数 I(p_i) 都可以作为信息的度量。此外,还有另外一个关键的属性就是独立事件的可加性;两次连续硬币抛掷的信息应该是一次单独抛掷的 2 倍。这对独立变量而言是有意义的,因为在这种情况下意外性或者不可预测性会增大为之前的两倍。形式上,对于独立事件 x_i 和 x_j 而言,我们需要 I(p_i * p_j) = I(p_i) + I(p_j)。满足所有这些要求的函数就是负对数,因此我们可以使用负对数表示自信息:
图 1 所示是自信息 I(p)。
图 1:函数 I(p) 的自信息。小概率对应着较高的自信息,反之亦然。
我们继续回到简单的硬币抛掷实验中。在信息论中,1bit(也叫做 Shannon)信息代表一次单独硬币抛掷的两种可能结果。相似地,对于两次连续抛掷而言,就需要 4 bit 来描述 4 中可能的结果。通常,用 log_2(n)(2 的对数)bit 来描述 n 个连续的独立随机事件的结果,或者是自信息。下面我们来验证一下一次连续三次的实验中自信息的计算:总共有 2^3=8 种可能的结果,每种结果的概率都是 0.5^3=0.125。所以,这次实验的自信息就是 I(0.125)= -log_2(0.125) = 3。我们需要 3bit 来描述这些所有可能的结果,那么,任何一次连续三次的硬币抛掷的自信息等于 3.0。
我们也可以计算连续随机变量的自信息。图 2 展示了三种不同的概率密度函数及其对应的信息函数。图 2(A)所示的 Dirac delta 对应着很强的偏差,总是同一面朝上的偏畸硬币对应着零熵。所有 p(x)= 0 的地方都对应着无限高的信息量。然而,由于这些零概率的事件永远不会发生,所以这只是一个假设。图 2(B)中的高斯概率密度函数就是对那种经常同一面朝上,但不总是同一面朝上的情况的模拟。最后,图 2(C)描述的是一个均匀分布概率密度函数,它对应着均匀的信息量,和我们没有偏畸的硬币是类似的。
图 2. [-3,3] 上的三种不同的概率密度函数及其自信息 I(p)。(A)Dirac δ函数(完全确定);(B)μ = 0,σ = 0.5 的高斯分布;(C)均匀分布
熵
到目前为止我们只讨论了自信息。在正常的硬币实验中,自信息实际上都等于香农熵,因为所有的结果都是等概率出现的。通常,香农熵是 X 的所有可能结果的自信息期望值:
其中 b 是对数的底数。上面我们使用的是 b=2,其他常见的选择还有 b=10,以及 e。其实这个影响不大,因为不同底数的对数之间存在一个常数的关系。我们这里仍然假设底数为 2,所以我们将省略下面公式中的 b。
如果仔细注意的话,你可能会疑惑,当 p(x_i) = 0 的时候会发生什么,因为这种情况下我们必须计算 0 · log(0)。事实上,我们需要计算的是一个极限:lim_(p→0) p*log(p(x_i))=0。使用洛必达法则或泰勒展开式求解的过程读者可以查阅书籍自行完成。
当香农熵泛化到连续域的时候,通常它指的是一种微分熵,对于连续的随机变量 x 及其概率密度函数 p(x),它的香农熵定义如下:
我们上述三个分布的熵分别是 0(狄拉克δ分布),174(高斯分布)以及 431(均匀分布)。在我们的实验中出现的模式是:越宽广的分布对应着越高的信息熵。仔细观察图 2(B)和图 2(C)有助于你的理解。尽管高斯分布中 I(p)曲线下面的面积要远大于均匀分布,然而它的信息熵要远小于均匀分布,因为信息熵 I(P)是按照概率密度 p 加权的,在高斯分布的两侧,p 接近于 0。更广的概率密度对应着更大的信息熵,有一个很好的比喻帮助记住这个:想象某种气体充满了一个储罐。从物理学中我们可以知道,一个封闭系统中的熵会随着时间增加,而且从来不会减少。在我们从储罐的另一侧注入气体之后,气体粒子的分布会收敛于一个均匀值。低熵意味着高密度的气体粒子聚集在某个特定的区域,而这是永远不会自发发生的。很多气体粒子聚集在某个小面积区域对应的还早呢故事我们的高斯概率密度函数,在狄拉克δ分布中是一个极端粒例子,所有的气体都被压缩在一个无限小的区域。
交叉熵
交叉熵是一个用来比较两个概率分布 p 和 q 的数学工具。它和熵是类似的,我们计算 log(q) 在概率 p 下的期望,而不是反过来:
在信息论中,这个量指的是:如果用「错误」的编码方式 q(而不是 p)去编码服从 q 分布的事件,我们所需要的 bit 数。在机器学习中,这是一个衡量概率分布相似性的有用工具,而且经常作为一个损失函数。因为交叉熵等于 KL 散度加上一项信息熵,即 D_KL(p||q) = H(p, q) - H(p)。而当我们针对 Q 最小化交叉熵时,H(p) 为常量,因此它能够被省略。交叉熵在这种情况下也就等价于 KL 散度,因为 KL 散度可以简单地从最大似然估计推导出来,因此下文详细地以 GAN 为例利用 MLE 推导 KL 散度的表达式。
KL 散度
与交叉熵紧密相关,KL 散度是另一个在机器学习中用来衡量相似度的量:从 q 到 p 的 KL 散度如下:D_KL(p||q)。在贝叶斯推理中,DKL(p||q) 衡量当你修改了从先验分布 q 到后验分布 p 的信念之后带来的信息增益,或者换句话说,就是用后验分布 q 来近似先验分布 p 的时候造成的信息损失。例如,在训练一个变分自编码器的隐藏空间表征时就使用了 KL 散度。KL 散度可以用熵和交叉熵表示:
交叉熵衡量的是用编码方案 q 对服从 p 的事件进行编码时所需 bit 数的平均值,而 KL 散度给出的是使用编码方案 q 而不是最优编码方案 p 时带来的额外 bit 数。从这里我们可以看到,在机器学习中,p 是固定的,交叉熵和 KL 散度之间只相差一个常数可加项,所以从优化的目标来考虑,二者是等价的。而从理论角度而言,考虑 KL 散度仍然是有意义的,KL 散度的一个属性就是,当 p 和 q 相等的时候,它的值为 0。
KL 散度有很多有用的性质,最重要的是它是非负的。KL 散度为 0 当且仅当 P 和 Q 在离散型变量的情况下是相同的分布,或者在连续型变量的情况下是 『几乎 处处』 相同的。因为 KL 散度是非负的并且衡量的是两个分布之间的差异,它经常 被用作分布之间的某种距离。然而,它并不是真的距离因为它不是对称的:对于某 些 P 和 Q,D_KL(P||Q) 不等于 D_KL(Q||P)。这种非对称性意味着选择 D_KL(P||Q) 还是 D_KL(Q||P) 影响很大。
在李弘毅的讲解中,KL 散度可以从极大似然估计中推导而出。若给定一个样本数据的分布 P_data(x) 和生成的数据分布 P_G(x;θ),那么 GAN 希望能找到一组参数θ使分布 P_g(x;θ) 和 P_data(x) 之间的距离最短,也就是找到一组生成器参数而使得生成器能生成十分逼真的图片。
现在我们可以从训练集抽取一组真实图片来训练 P_G(x;θ) 分布中的参数θ使其能逼近于真实分布。因此,现在从 P_data(x) 中抽取 m 个真实样本 {?^1,?^2,…,?^?},其中符号「^」代表上标,即 x 中的第 i 个样本。对于每一个真实样本,我们可以计算 P_G(x^i;θ),即在由θ确定的生成分布中,x^i 样本所出现的概率。因此,我们就可以构建似然函数:
其中「∏」代表累乘、P_G(x^i;θ) 代表第 i 个样本在生成分布出现的概率。从该似然函数可知,我们抽取的 m 个真实样本在 P_G(x;θ) 分布中全部出现的概率值可以表达为 L。又因为若 P_G(x;θ) 分布和 P_data(x) 分布相似,那么真实数据很可能就会出现在 P_G(x;θ) 分布中,因此 m 个样本都出现在 P_G(x;θ) 分布中的概率就会十分大。
下面我们就可以最大化似然函数 L 而求得离真实分布最近的生成分布(即最优的参数θ):
在上面的推导中,我们希望最大化似然函数 L。若对似然函数取对数,那么累乘 ∏ 就能转化为累加 ∑,并且这一过程并不会改变最优化的结果。因此我们可以将极大似然估计化为求令 log[P_G(x;θ)] 期望最大化的θ,而期望 E[logP_G(x;θ)] 可以展开为在 x 上的积分形式:∫P_data(x)logP_G(x;θ)dx。又因为该最优化过程是针对θ的,所以我们添加一项不含θ的积分并不影响最优化效果,即可添加 -∫P_data(x)logP_data(x)dx。添加该积分后,我们可以合并这两个积分并构建类似 KL 散度的形式。该过程如下:
这一个积分就是 KL 散度的积分形式,因此,如果我们需要求令生成分布 P_G(x;θ) 尽可能靠近真实分布 P_data(x) 的参数θ,那么我们只需要求令 KL 散度最小的参数θ。此外,我们可以将 KL 散度的积分形式转换为我们熟悉的 KL 散度表达式:
在离散型变量的情况下,KL 散度衡量的是,当我们使用一种被设计成能够使得概率分布 Q 产生的消息的长度最小的编码,发送包含由概率分布 P 产生的符号消息时,所需要的额外信息量。
深入浅出最大似然估计:
最大似然估计是利用已知的样本的结果,在使用某个模型的基础上,反推最有可能导致这样结果的模型参数值。
抽球
举个通俗的例子:假设一个袋子装有白球与红球,比例未知,现在抽取10次(每次抽完都放回,保证事件独立性),假设抽到了7次白球和3次红球,在此数据样本条件下,可以采用最大似然估计法求解袋子中白球的比例(最大似然估计是一种“模型已定,参数未知”的方法)。当然,这种数据情况下很明显,白球的比例是70%,但如何通过理论的方法得到这个答案呢?一些复杂的条件下,是很难通过直观的方式获得答案的,这时候理论分析就尤为重要了,这也是学者们为何要提出最大似然估计的原因。我们可以定义从袋子中抽取白球和红球的概率如下:
其中theta是未知的,因此,我们定义似然L为:
两边取ln,取ln是为了将右边的乘号变为加号,方便求导。
最大似然估计的过程,就是找一个合适的theta,使得平均对数似然的值为最大。因此,可以得到以下公式:
这里讨论的是2次采样的情况,当然也可以拓展到多次采样的情况:
我们定义M为模型(也就是之前公式中的f),表示抽到白球的概率为theta,而抽到红球的概率为(1-theta),因此10次抽取抽到白球7次的概率可以表示为:
将其描述为平均似然可得:
那么最大似然就是找到一个合适的theta,获得最大的平均似然。因此我们可以对平均似然的公式对theta求导,并另导数为0。
由此可得,当抽取白球的概率为0.7时,最可能产生10次抽取抽到白球7次的事件。
例子2:正态分布
假如有一组采样值(x1,...,xn),我们知道其服从正态分布,且标准差已知。当这个正态分布的期望为多少时,产生这个采样数据的概率为最大?
这个例子中正态分布就是模型M,而期望就是前文提到的theta。
综上所述,可得求解最大似然估计的一般过程为:
1. 写出似然函数;
2. 如果无法直接求导的话,对似然函数取对数;
3. 求导数 ;
4. 求解模型中参数的最优值。
朴素贝叶斯算法原理小结
《机器学习实战》笔记之四——基于概率论的分类方法:朴素贝叶斯
python3.6环境下代码
出现的编码错误问题是ham文件夹下23.txt中的问号解析异常,去掉即可正确运行
test4.py
#-*- coding:utf-8 import sys sys.path.append("bayes.py") import bayes import re import feedparser listOPosts, listClasses = bayes.loadDataSet() myVocabList = bayes.createVocabList(listOPosts) # bayes.setOfWords2Vec(myVocabList, listOPosts[0]) # bayes.setOfWords2Vec(myVocabList, listOPosts[3]) trainMat = [] for postinDoc in listOPosts: trainMat.append(bayes.setOfWords2Vec(myVocabList, postinDoc)) p0V, p1V, pAb = bayes.trainNB0(trainMat, listClasses) bayes.testingNB() regEx = re.compile("\W*") mySent = "This book is the best book on Python or M.L. I have ever laid eyes upon" listOfTokens = regEx.split(mySent) listOfTokens = [tok.lower() for tok in listOfTokens if len(tok) > 0] print(listOfTokens) emailText = open("email/ham/6.txt").read() # emailText = open("email/ham/6.txt", "r", encoding = "utf-8").read() listOfTokens = regEx.split(emailText) print(listOfTokens) bayes.spamTest() print(listOfTokens) ny = feedparser.parse("http://newyork.craigslist.org/stp/index.rss") sf = feedparser.parse("http://sfbay.craigslist.org/stp/index.rss") vocabList, pSF, pNY = bayes.localWords(ny, sf) # vocabList, pSF, pNY = bayes.localWords(ny, sf)
bayes.getTopWords(ny, sf)
# ny['entries'] # numtext = len(ny["entries"]) print("over")
代码注解:
机器学习实战之朴素贝叶斯
''' Created on Oct 19, 2010 @author: Peter ''' 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([]) #create empty 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 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) p0Num = ones(numWords); p1Num = ones(numWords) #change to ones() p0Denom = 2.0; p1Denom = 2.0 #change to 2.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]) p1Vect = log(p1Num/p1Denom) #change to log() p0Vect = log(p0Num/p0Denom) #change to log() return p0Vect,p1Vect,pAbusive def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1): # 待分类向量vec2classify,矩阵向量对应相乘piVec,与其相似的那个值必定大 #log(p(w)) = log(p(w|ci)*p(ci)) = log(p(w0|ci)*p(w1|ci)*……p(wn|ci)*p(ci)) = sum(log(p(wi|ci))) + log(p(ci)) 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 bagOfWords2VecMN(vocabList, inputSet): returnVec = [0]*len(vocabList) for word in inputSet: if word in vocabList: returnVec[vocabList.index(word)] += 1 return returnVec 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)) def textParse(bigString): #input is big string, #output is word list import re listOfTokens = re.split(r'W*', bigString) return [tok.lower() for tok in listOfTokens if len(tok) > 2] def spamTest(): docList=[]; classList = []; fullText =[] for i in range(1,26): wordList = textParse(open('email/spam/%d.txt' % i).read()) docList.append(wordList) fullText.extend(wordList) classList.append(1) wordList = textParse(open('email/ham/%d.txt' % i).read()) docList.append(wordList) fullText.extend(wordList) classList.append(0) vocabList = createVocabList(docList)#create vocabulary trainingSet = list(range(50)); testSet=[] #create test set for i in list(range(10)): randIndex = int(random.uniform(0,len(trainingSet))) testSet.append(trainingSet[randIndex]) del(trainingSet[randIndex]) trainMat=[]; trainClasses = [] for docIndex in trainingSet:#train the classifier (get probs) trainNB0 trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex])) trainClasses.append(classList[docIndex]) p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses)) errorCount = 0 for docIndex in testSet: #classify the remaining items wordVector = bagOfWords2VecMN(vocabList, docList[docIndex]) if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]: errorCount += 1 print("classification error",docList[docIndex]) print('the error rate is: ',float(errorCount)/len(testSet)) #return vocabList,fullText def calcMostFreq(vocabList,fullText): import operator freqDict = {} for token in vocabList: freqDict[token]=fullText.count(token) sortedFreq = sorted(freqDict.items(), key=operator.itemgetter(1), reverse=True) return sortedFreq[:30] def localWords(feed1,feed0): import feedparser docList=[]; classList = []; fullText =[] minLen = min(len(feed1['entries']),len(feed0['entries'])) for i in range(minLen): wordList = textParse(feed1['entries'][i]['summary']) docList.append(wordList) fullText.extend(wordList) classList.append(1) #NY is class 1 wordList = textParse(feed0['entries'][i]['summary']) docList.append(wordList) fullText.extend(wordList) classList.append(0) vocabList = createVocabList(docList)#create vocabulary top30Words = calcMostFreq(vocabList,fullText) #remove top 30 words for pairW in top30Words: if pairW[0] in vocabList: vocabList.remove(pairW[0]) trainingSet = list(range(2*minLen)); testSet=[] #create test set for i in list(range(20)): randIndex = int(random.uniform(0,len(trainingSet))) testSet.append(trainingSet[randIndex]) del(trainingSet[randIndex]) trainMat=[]; trainClasses = [] for docIndex in trainingSet:#train the classifier (get probs) trainNB0 trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex])) trainClasses.append(classList[docIndex]) p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses)) errorCount = 0 for docIndex in testSet: #classify the remaining items wordVector = bagOfWords2VecMN(vocabList, docList[docIndex]) if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]: errorCount += 1 print('the error rate is: ',float(errorCount)/len(testSet)) return vocabList,p0V,p1V def getTopWords(ny,sf): import operator vocabList,p0V,p1V=localWords(ny,sf) topNY=[]; topSF=[] for i in range(len(p0V)): if p0V[i] > -6.0 : topSF.append((vocabList[i],p0V[i])) if p1V[i] > -6.0 : topNY.append((vocabList[i],p1V[i])) sortedSF = sorted(topSF, key=lambda pair: pair[1], reverse=True) print("SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**") for item in sortedSF: print(item[0]) sortedNY = sorted(topNY, key=lambda pair: pair[1], reverse=True) print("NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**") for item in sortedNY: print(item[0])
在所有的机器学习分类算法中,朴素贝叶斯和其他绝大多数的分类算法都不同。对于大多数的分类算法,比如决策树,KNN,逻辑回归,支持向量机等,他们都是判别方法,也就是直接学习出特征输出Y和特征X之间的关系,要么是决策函数P(Y|X)=P(X,Y)/P(X)得出。
朴素贝叶斯很直观,计算量也不大,在很多领域有广泛的应用,这里我们就对朴素贝叶斯算法原理做一个小结。
1. 朴素贝叶斯相关的统计学知识
在了解朴素贝叶斯的算法之前,我们需要对相关必须的统计学知识做一个回顾。
贝叶斯学派很古老,但是从诞生到一百年前一直不是主流。主流是频率学派。频率学派的权威皮尔逊和费歇尔都对贝叶斯学派不屑一顾,但是贝叶斯学派硬是凭借在现代特定领域的出色应用表现为自己赢得了半壁江山。
贝叶斯学派的思想可以概括为先验概率+数据=后验概率。也就是说我们在实际问题中需要得到的后验概率,可以通过先验概率和数据一起综合得到。数据大家好理解,被频率学派攻击的是先验概率,一般来说先验概率就是我们对于数据所在领域的历史经验,但是这个经验常常难以量化或者模型化,于是贝叶斯学派大胆的假设先验分布的模型,比如正态分布,beta分布等。这个假设一般没有特定的依据,因此一直被频率学派认为很荒谬。虽然难以从严密的数学逻辑里推出贝叶斯学派的逻辑,但是在很多实际应用中,贝叶斯理论很好用,比如垃圾邮件分类,文本分类。
我们先看看条件独立公式,如果X和Y相互独立,则有:
我们接着看看条件概率公式:
或者说:
接着看看全概率公式
从上面的公式很容易得出贝叶斯公式:
2. 朴素贝叶斯的模型
从统计学知识回到我们的数据分析。假如我们的分类模型样本是:
即我们有m个样本,每个样本有n个特征,特征输出有K个类别,定义为C1,C2,...,CK。
从样本我们可以学习得到朴素贝叶斯的先验分布P(X=x|Y=Ck)=P(X1=x1,X2=x2,...Xn=xn|Y=Ck),然后我们就可以用贝叶斯公式得到X和Y的联合分布P(X,Y)了。联合分布P(X,Y)定义为:
从上面的式子可以看出P(X1=x1,X2=x2,...Xn=xn|Y=Ck)很难求出,这是一个超级复杂的有n个维度的条件分布。朴素贝叶斯模型在这里做了一个大胆的假设,即X的n个维度之间相互独立,这样就可以得出:
从上式可以看出,这个很难的条件分布大大的简化了,但是这也可能带来预测的不准确性。你会说如果我的特征之间非常不独立怎么办?如果真是非常不独立的话,那就尽量不要使用朴素贝叶斯模型了,考虑使用其他的分类方法比较好。但是一般情况下,样本的特征之间独立这个条件的确是弱成立的,尤其是数据量非常大的时候。虽然我们牺牲了准确性,但是得到的好处是模型的条件分布的计算大大简化了,这就是贝叶斯模型的选择。
最后回到我们要解决的问题,我们的问题是给定测试集的一个新样本特征(x1(test),x2(test),...xn(test),我们如何判断它属于哪个类型?
既然是贝叶斯模型,当然是后验概率最大化来判断分类了。我们只要计算出所有的K个条件概率P(Y=Ck|X=X(test)),然后找出最大的条件概率对应的类别,这就是朴素贝叶斯的预测了。
3. 朴素贝叶斯的推断过程
上节我们已经对朴素贝叶斯的模型也预测方法做了一个大概的解释,这里我们对朴素贝叶斯的推断过程做一个完整的诠释过程。
我们预测的类别P(Y=Ck|X=X(test))最大化的类别,数学表达式为:
由于对于所有的类别计算P(X=X(test),因此,我们的预测公式可以简化为:
接着我们利用朴素贝叶斯的独立性假设,就可以得到通常意义上的朴素贝叶斯推断公式:
4. 朴素贝叶斯的参数估计
在上一节中,我们知道只要求出P(Y=Ck)和P(Xj=Xj(test)|Y=Ck)(j=1,2,...n),我们通过比较就可以得到朴素贝叶斯的推断结果。这一节我们就讨论怎么通过训练集计算这两个概率。
对于mk除以样本总数m。
对于P(Xj=Xj(test)|Y=Ck)(j=1,2,...n),这个取决于我们的先验条件:
a) 如果我们的Xj(test)出现的频率。即:
其中Xj(test)出现的次数。
某些时候,可能某些类别在样本中没有出现,这样可能导致P(Xj=Xj(test)|Y=Ck)为0,这样会影响后验的估计,为了解决这种情况,我们引入了拉普拉斯平滑,即此时有:
其中Oj为第j个特征的取值个数。
b)如果我们我们的Xj(test)出现的频率。此时有:
其中,Xj(test)取值为0和1。
c)如果我们我们的P(Xj=Xj(test)|Y=Ck)的概率分布是:
其中Xj的方差。对于一个连续的样本值,带入正态分布的公式,就可以求出概率分布了。
5. 朴素贝叶斯算法过程
我们假设训练集为m个样本n个维度,如下:
共有K个特征输出类别,分别为Sj为特征j不同的取值数。
输出为实例X(test)的分类。
算法流程如下:
1) 如果没有Y的先验概率,则计算Y的K个先验概率:P(Y=Ck)为输入的先验概率。
2) 分别计算第k个类别的第j维特征的第l个个取值条件概率:P(Xj=xjl|Y=Ck)
a)如果是离散值:
λ可以取值为1,或者其他大于0的数字。
b)如果是稀疏二项离散值:
此时l只有两种取值。
c)如果是连续值不需要计算各个l的取值概率,直接求正态分布的参数:
需要求出Xj的方差。
3)对于实例X(test),分别计算:
4)确定实例Cresult
从上面的计算可以看出,没有复杂的求导和矩阵运算,因此效率很高。
6. 朴素贝叶斯算法小结
朴素贝叶斯算法的主要原理基本已经做了总结,这里对朴素贝叶斯的优缺点做一个总结。
朴素贝叶斯的主要优点有:
1)朴素贝叶斯模型发源于古典数学理论,有稳定的分类效率。
2)对小规模的数据表现很好,能个处理多分类任务,适合增量式训练,尤其是数据量超出内存时,我们可以一批批的去增量训练。
3)对缺失数据不太敏感,算法也比较简单,常用于文本分类。
朴素贝叶斯的主要缺点有:
1) 理论上,朴素贝叶斯模型与其他分类方法相比具有最小的误差率。但是实际上并非总是如此,这是因为朴素贝叶斯模型给定输出类别的情况下,假设属性之间相互独立,这个假设在实际应用中往往是不成立的,在属性个数比较多或者属性之间相关性较大时,分类效果不好。而在属性相关性较小时,朴素贝叶斯性能最为良好。对于这一点,有半朴素贝叶斯之类的算法通过考虑部分关联性适度改进。
2)需要知道先验概率,且先验概率很多时候取决于假设,假设的模型可以有很多种,因此在某些时候会由于假设的先验模型的原因导致预测效果不佳。
3)由于我们是通过先验和数据来决定后验的概率从而决定分类,所以分类决策存在一定的错误率。