机器学习笔记(10)---朴素贝叶斯

基于概率论的分类方法:朴素贝叶斯

前两章KNN和决策树相对来说是很简单的,本章开始学习朴素贝叶斯,使用基于概率论的分类方法。这部分的内容要用一定的统计学和概率论与数理统计的基础。

可参考中国大学MOOC课程概率论与数理统计 由哈工大数学系出品,另外还可参考浙江大学和同济大学的概率论与数理统计课程,相关课程请自行搜索。

朴素贝叶斯总结

贝叶斯相关理论一开始并没有理解,看了好多遍视频,再结合书,来来回回倒腾了好多次,最后再细细体会才算基本上弄明白了。想弄明白贝叶斯相关理论基础需要把前言部分推荐的书《概率论与数理统计》第一章和第二章中条件概率、乘法定理以及全概率弄明白,建议最好跟着老师的视频看。

贝叶斯其实最主要的就是MLiA P60页的这个公式:

p(ci|w)=p(w|ci)p(ci)p(w) p ( c i | w ) = p ( w | c i ) p ( c i ) p ( w )

其中的 w w 是一个句子的词向量(直接理解为句子组成方式即可,如何转换成向量可参考书上),ci" role="presentation">ci 为类别(侮辱性或非侮辱性), p(ci|w) p ( c i | w ) 则表示在给定一个句子组成方式的情况下属于类别 ci c i 的概率是多少,对于多个类别,那么算出来后取概率值最大的类别就是此条句子的类别了。这里还有一个关键是如何把句子转换成向量 w w ,书上都有方法,看下相关源代码即可。。

所以,知道了以上内容后,只要计算等号右边的值,再代入就可计算出相关概率p" role="presentation">p了,需要计算的值有: p(w|ci) p ( w | c i ) p(ci) p ( c i ) p(w) p ( w )

p(ci) p ( c i ) 很容易计算的,就是所有样本中各个类别占的比例,比如本次书中的例子侮辱性语句和非侮辱性语句各3条,所以各占50%。

p(w|ci) p ( w | c i ) 就是通过已经带类别标签的样本,计算组成句子的单词情况。

其实该算法本身是很简单的(不过刚开始学的时候感觉好难),但在实际的运行过程中会有些问题,比如 P62 P 62 4.5.3一节提到的两个问题及处理方法:

  • 多个乘积相乘,如果其中之一为0,那么结果为0的问题。解决方法是在初始化时将所有词的出现数初始化为1,将分母初始化为2.
  • 太多很少的数相乘导致下溢问题。通过对乘积取自然对数解决。

MLiA中的决策树代码

最终效果

最终效果运行如下,输入一语句,给出分类结果,其中0表示非侮辱性语句,1表示侮辱性语句。

['love', 'my', 'dalmation'] classified as:  0
['stupid', 'garbage'] classified as:  1

此书除了给出达到分类效果的目的外,还介绍了直接通过RSS源在线获取方式,获取美国纽约和美国旧金山湾区的言论差异,以及两个区域中使用最频繁的单词。后续的代码会一一贴上,当然是有完整注释的代码。

完整代码

以下给出本人学习测试中使用的带完整注释的代码,只要跳到main函数处,一个一个放开函数进行学习即可。

#coding:utf-8#python3.6
#博客链接:http://blog.csdn.net/rosetta
#date:2018.3.18import numpy as np
import math#和原书代码不一样的地方
import redef 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]    #0表示正常言论,1表示侮辱性言论。6个分类正好对应上述6条语句。return postingList,classVec#createVocabList返回的是所有不重复的单词。
def createVocabList(dataSet):vocabSet = set([])  #create empty setfor document in dataSet:#因为dataSet相当于是二维list(list里面又是一个list),这里就是每次遍历取里面的一个list,# 再拿后面的list和之前的进行或操作,而每个小list本身进行了set()去重,所以本函数最终处理完后返回的数据是去重的。vocabSet = vocabSet | set(document) #union of the two setsreturn list(vocabSet)#输入参数vocabList就是之前去重的单词;inputSet就是原始数据list中的内部List。
#首先有一个所有言论使用过的不重复的单词集合(vocabList),并初始化一个一样大小的向量(returnVec)为0
#然后再取一条言论,遍历这条言论中的单词,取vocabList该单词出现的下标,并至returnVec对应下标位置为1.
#这样返回的returnVec向量就可标识当前语句出现了哪些单词。
def setOfWords2Vec(vocabList, inputSet):returnVec = [0]*len(vocabList)#初始化returnVec为0,长度为vacabList的长度。for word in inputSet:if word in vocabList:returnVec[vocabList.index(word)] = 1else: print("the word: %s is not in my Vocabulary!" % word)return returnVec#这个函数及其内部函数不需要相关数学知识即可看明白。
def loadDataSet_test():listOPosts, listClasses = loadDataSet()myVocabList = createVocabList(listOPosts)#myVocabList是所有不重复的单词,即这里的特征。print("myVocabList:", myVocabList)result1= setOfWords2Vec(myVocabList, listOPosts[0])#表示第1条语句(list中下标为0的语句)中的每个单词出现在特征中的位置。出现置1,不出现为0.print(result1)result2= setOfWords2Vec(myVocabList, listOPosts[3])#表示第4条语句(list中下标为3的语句)。print(result2)#trainNB0()用来计算某个类别中出现的各单词,占该类别所有单词的概率。
#trainMatrix,训练用的6条语句的单词对应的向量;trainCategory这6条语句分别对应的类别,0为非侮辱性,1为侮辱性。
def trainNB0(trainMatrix,trainCategory):numTrainDocs = len(trainMatrix)#一共6条语句的单词向量。#numTrainDocs=6numWords = len(trainMatrix[0])#此次样本一共有不重复的单词32个,包含侮辱性的和非侮辱性的。pAbusive = sum(trainCategory)/float(numTrainDocs)#trainCategory=[0, 1, 0, 1, 0, 1]相加为3,numTrainDocs为6,所以pAbusive=0.5# p0Num = np.ones(numWords); p1Num = np.ones(numWords)     #这里需要加上numpy前缀。# p0Denom = 2.0; p1Denom = 2.0                        #change to 2.0p0Num = np.zeros(numWords); p1Num = np.zeros(numWords)     #这里需要加上numpy前缀。#p0Num={ndarray}[ 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.  0.  0.  0.  0.  0.]#p1Num={ndarray}[ 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.  0.  0.  0.  0.  0.]p0Denom = 0.0; p1Denom = 0.0for i in range(numTrainDocs):if trainCategory[i] == 1:p1Num += trainMatrix[i]p1Denom += sum(trainMatrix[i])else:p0Num += trainMatrix[i]#p0Num为1*32大小的ndarray,某条语句的向量单词每出现一次,就在这个位置加1,用来统计单词出现的总次数。p0Denom += sum(trainMatrix[i])#用来统计该类别语句一共有多少个单词。p1Vect = (p1Num/p1Denom)#该类别语句中出现的各单词总数,分别除以该类别的总数,即该单词在该类别中出现的概率。p1Vect为长度为32的向量,其每个分量为p(wx|ci)#[ 0.          0.05263158  0.05263158  0.15789474  0.05263158  0.10526316# 0.05263158  0.05263158  0.          0.10526316  0.          0.# 0.05263158  0.          0.05263158  0.          0.          0.05263158# 0.05263158  0.          0.          0.          0.05263158  0.          0.# 0.          0.          0.          0.          0.          0.05263158# 0.05263158]p0Vect = (p0Num/p0Denom)return p0Vect,p1Vect,pAbusive
#p0Vect:32个大小的ndarray(因为所有非重复单词一共有32个),非侮辱性语句中的各单词暂该类别所有单词的比例。
#p1Vect:同样是32个大小的ndarray,侮辱性语句中的各单词暂该类别所有单词的比例。
#pAbusive:表示侮辱性语句条数暂总语句条件的比例,本次实验一共6条语句,侮辱性3条,所以pAbusive为3/6=0.5def trainNB1(trainMatrix,trainCategory):numTrainDocs = len(trainMatrix)numWords = len(trainMatrix[0])pAbusive = sum(trainCategory)/float(numTrainDocs)p0Num = np.ones(numWords); p1Num = np.ones(numWords)      #change to ones()p0Denom = 2.0; p1Denom = 2.0                        #change to 2.0for i in range(numTrainDocs):if trainCategory[i] == 1:p1Num += trainMatrix[i]p1Denom += sum(trainMatrix[i])else:p0Num += trainMatrix[i]p0Denom += sum(trainMatrix[i])p1Vect = np.log(p1Num/p1Denom)          #change to log()p0Vect = np.log(p0Num/p0Denom)          #change to log()return p0Vect,p1Vect,pAbusivedef trainNB0_test():listOPosts, listClasses = loadDataSet()myVocabList = createVocabList(listOPosts)print("myVocabList:",myVocabList)trainMat = []for postinDoc in listOPosts:trainMat.append(setOfWords2Vec(myVocabList, postinDoc))print("trainMat:",trainMat)p0V, p1V, pAb=trainNB0(trainMat, listClasses)print(pAb)print(p0V)print(p1V)#考虑到在计算p(w0|1)p(w1|1)p(w2|1),如果当其中一个为0,最后的结果为0,可将所有词的出现初始化为1,分母初始化为2
#另外一个问题是分母
def trainNB1_test():listOPosts, listClasses = loadDataSet()myVocabList = createVocabList(listOPosts)print("myVocabList:",myVocabList)trainMat = []for postinDoc in listOPosts:trainMat.append(setOfWords2Vec(myVocabList, postinDoc))p0V, p1V, pAb=trainNB1(trainMat, listClasses)print(pAb)print(p0V)print(p1V)#vec2Classify是由setOfWords2Vec返回的当前待预测的语句出现的单词的向量
#p0Vec 即p(w0|c0),p(w1|c0),...,p(wn|c0),组成的向量,
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)#p1:当前待预测的语句是侮辱性语句的概率#sum(vec2Classify * p1Vec)就是32维向量中的每个成员都乘以p1Vec中对应位置的值,然后相加。#但这里没弄明白,贝叶斯公式是p(ci|w)=p(w|ci)p(ci)/p(w),这里怎么是p(w|ci)*p1Vec,p1Vec又不是p(ci)??反正先记下来吧,后续再看。#对于这里的解释,此篇文章总结的不错:http://blog.csdn.net/qingyuanluofeng/article/details/64549460#需要比较 p(C1|W) 和p(C0|W)大小,即比较的是在某条语句W出现的情况下,它属于类别1和类别0的概率# p(C1|W)=p(W|C1) * p(C1) / p(W)# p(C0|W)=p(W|C0) * p(C0) / p(W)#因为对于同一条语句计算两个类别的概率时p(W)是一样的。# 所以只需要比较p(W|C1) * p(C1) 与 p(W|C0) * p(C0)的概率,# 取对数后,只需要比较p(W|C1) + p(C1) 与 p(W|C0) + p(C0) 的概率# p(W|C1)=sum(W * p1Vec),即文档W乘以类别1的单词概率向量,然后累加即为在类别C1下文档W的概率#注:这里大致意思已经明白,但取对数的一些细节还不是特别清楚。但已经不影响理解了。p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)##p0:1减去pClass1的概率就是当前类型的概率,即非侮辱性语句的概率if p1 > p0:#对比p1和p0哪个概率大,如果p1大表示此时是侮辱性语句概率大;如果是p0大表示是非侮辱性语句的概率大。return 1else:return 0def bagOfWords2VecMN(vocabList, inputSet):returnVec = [0]*len(vocabList)for word in inputSet:if word in vocabList:returnVec[vocabList.index(word)] += 1return returnVecdef testingNB():listOPosts,listClasses = loadDataSet()myVocabList = createVocabList(listOPosts)trainMat=[]for postinDoc in listOPosts:trainMat.append(setOfWords2Vec(myVocabList, postinDoc))p0V,p1V,pAb = trainNB1(np.array(trainMat),np.array(listClasses))#注意这个地方我使用的是trainNB1,而不是trainNB0testEntry = ['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))#这个函数用来替代setOfWords2Vec()
#该测试函数仅仅是把testingNB()中的setOfWords2Vec()换成了bagOfWords2VecMN()
def bagOfWords2VecMN_test():listOPosts,listClasses = loadDataSet()myVocabList = createVocabList(listOPosts)trainMat=[]for postinDoc in listOPosts:trainMat.append(bagOfWords2VecMN(myVocabList, postinDoc))p0V,p1V,pAb = trainNB1(np.array(trainMat),np.array(listClasses))#注意这个地方我使用的是trainNB1,而不是trainNB0testEntry = ['love', 'my', 'dalmation']thisDoc = np.array(bagOfWords2VecMN(myVocabList, testEntry))print(testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb))testEntry = ['stupid', 'garbage']thisDoc = np.array(bagOfWords2VecMN(myVocabList, testEntry))print(testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb))def textParse(bigString):    #input is big string, #output is word listimport relistOfTokens = 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):#因为spam(垃圾邮件样本)和ham(正常邮件样本)各是25封,所以这里是遍历这些样本。wordList = textParse(open('email/spam/%d.txt' % i).read())docList.append(wordList)#append是把wordList作为一个整体加入到目标变量docList中的。fullText.extend(wordList)#extend是把wordList的各个单词独立出来放到目标变量fullText中的。classList.append(1)#垃圾邮件样本数量加1,所以在classList中追加一个1.wordList = textParse(open('email/ham/%d.txt' % i).read())docList.append(wordList)fullText.extend(wordList)classList.append(0)vocabList = createVocabList(docList)#create vocabularytrainingSet = list(range(50)); testSet=[]           #create test setfor i in range(10):#取了10个,即1/5做为测试数据,而且是随机取的。randIndex = int(np.random.uniform(0,len(trainingSet)))#len(trainingSet)=50# numpy.random.uniform(low,high,size):从一个均匀分布[low,high)中随机采样,注意定义域是左闭右开,即包含low,不包含high.#输出样本数目,为int或元组(tuple)类型,例如,size=(m,n,k), 则输出m*n*k个样本,缺省时输出1个值。#这里因为没有指定size值,所以返回一个数,但这个数是个float类型的,所以在外面又加了个int()强制类型转换testSet.append(trainingSet[randIndex])del(trainingSet[randIndex])#把测试数据剔除trainMat=[]; trainClasses = []for docIndex in trainingSet:#train the classifier (get probs) trainNB0trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))trainClasses.append(classList[docIndex])p0V,p1V,pSpam = trainNB0(np.array(trainMat),np.array(trainClasses))#训练数据的结果#开始测试分类器的正确性。errorCount = 0for docIndex in testSet:        #classify the remaining itemswordVector = bagOfWords2VecMN(vocabList, docList[docIndex])if classifyNB(np.array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:errorCount += 1print("classification error",docList[docIndex])print('the error rate is: ',float(errorCount)/len(testSet))#return vocabList,fullTextdef pre_data():mySent = 'This book is the best book on Python or M.L. I have ever laid eyes upon.'print("split:", mySent.split())#此时会把标点也算到分隔后的单词里。regEx = re.compile('\\W*')#其中的\\W即\\w取反,\\w表示[a-zA-Z0-9_],即去掉大小字母、数字和下划线,*表示0到多次。listOfTokens = regEx.split(mySent)#此时还有空格也算是分隔后的单词,所以需要把空格也去掉。print("listOfTokens:", listOfTokens)toks_noblank  = [tok for tok in listOfTokens if len(tok) >0] #去掉空格。print("toks", toks_noblank)toks_to_lower = [tok.lower() for tok in toks_noblank]#全部转换为小写。print("toks_to_lower", toks_to_lower)#所以上述两步操作合到,并且过滤掉小于3个字符的单词:toks_ok = [tok.lower() for tok in listOfTokens if len(tok)>2]print("toks_ok:", toks_ok)emailText = open('email/ham/6.txt').read()listOfTokens = regEx.split(emailText)print("2,listOfTokens:",listOfTokens)emailText_ok = [text.lower() for text in listOfTokens if len(text)>2]print("emailText_ok", emailText_ok)def spanmTest_test():spamTest()def calcMostFreq(vocabList,fullText):import operatorfreqDict = {}for token in vocabList:freqDict[token]=fullText.count(token)sortedFreq = sorted(iter(freqDict.items()), key=operator.itemgetter(1), reverse=True)return sortedFreq[:30]#本函数和之前的spamTest()函数类似,先计算出不同区域的类别占用比率(类别概率)和该类别中单词的频率。
#再预留出一部分评论,用于测试这些评论是哪个区的人发表的。
def localWords(feed1,feed0):import feedparserdocList=[]; classList = []; fullText =[]minLen = min(len(feed1['entries']),len(feed0['entries']))#分别计算从两个RSS取来的数据的条数,取最小的。本次RSS各是25条数据。for i in range(minLen):wordList = textParse(feed1['entries'][i]['summary'])docList.append(wordList)fullText.extend(wordList)classList.append(1) #NY is class 1wordList = textParse(feed0['entries'][i]['summary'])docList.append(wordList)fullText.extend(wordList)classList.append(0)vocabList = createVocabList(docList)#create vocabularytop30Words = calcMostFreq(vocabList,fullText)   #remove top 30 wordsfor pairW in top30Words:if pairW[0] in vocabList: vocabList.remove(pairW[0])trainingSet = list(range(2*minLen)); testSet=[]           #create test setfor i in range(20):#取出其中的20个为测试集randIndex = int(np.random.uniform(0,len(trainingSet)))testSet.append(trainingSet[randIndex])del(trainingSet[randIndex])trainMat=[]; trainClasses = []for docIndex in trainingSet:#train the classifier (get probs) trainNB0trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))trainClasses.append(classList[docIndex])p0V,p1V,pSpam = trainNB0(np.array(trainMat),np.array(trainClasses))errorCount = 0for docIndex in testSet:        #classify the remaining itemswordVector = bagOfWords2VecMN(vocabList, docList[docIndex])if classifyNB(np.array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:errorCount += 1print('the error rate is: ',float(errorCount)/len(testSet))return vocabList,p0V,p1V#feedparser第三方库简单测试
import feedparser
def rss_test():ny = feedparser.parse('https://newyork.craigslist.org/search/stp?format=rss')#注意,由于是外国网站,所以有点慢。print(ny['entries'])print(len(ny['entries']))def localwords_test():ny = feedparser.parse('https://newyork.craigslist.org/search/stp?format=rss')#newyork(美国纽约)地区的言论sf = feedparser.parse('https://sfbay.craigslist.org/search/stp?format=rss')#SF bay area(美国旧金山湾区)的言论vocabList, pSF, pNY = localWords(ny,sf)print("vocabList", vocabList)# print("pSF:", pSF)# print("pNY:", pNY)def getTopWords(ny,sf):import operatorvocabList,p0V,p1V=localWords(ny,sf)topNY=[]; topSF=[]for i in range(len(p0V)):    #取出各类中统率大于-6.0的单词。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])def getTopWords_test():ny = feedparser.parse('https://newyork.craigslist.org/search/stp?format=rss')#newyork(美国纽约)地区的言论sf = feedparser.parse('https://sfbay.craigslist.org/search/stp?format=rss')#SF bay area(美国旧金山湾区)的言论getTopWords(ny,sf)if __name__ == '__main__':#4.5使用Python进行文本分类#4.5.1从文本中构建词向量# loadDataSet_test()#4.5.2训练算法:从词向量计算概率# trainNB0_test()#4.5.3#解决0问题,我这里把trainNB0改成了trainNB1,后续就用NB1函数。# trainNB1_test()#这个函数已经可以输入句子,然后输出其属性哪一类了。#至此已经可以达到目的了,但其中的公式和贝叶斯公式貌似对不上,后续再看。接下来就是完善代码#testingNB()#4.5.4 对特征向量进行了优化,和词出现的次数也有关系。# bagOfWords2VecMN_test()#4.6使用朴素贝叶斯过滤垃圾邮件#4.6.1准备数据:切分文本# pre_data()#4.6.2测试算法:使用朴素贝叶斯进行交叉验证#实际上把之前的关键函数弄明白,这个函数就很容易理解了。#本函数无法就是从文件中取的数据,然后预留了10个用作测试,其核心处理函数和之前都是一样的。# spanmTest_test()#4.7#需要先安装feedparse第三方库,直接执行:pip install feedparser#或者离线下载地址:https://pypi.python.org/pypi/feedparser/5.2.1#downloads,下载完解压后在cmd下进入对应目录,然后执行python setup.py install#4.7.1 收集数据:导入RSS源# rss_test()#这个函数的功能和4.6.2中的spamTest()类似,区别在于本次获取的数据源是从rss上获取的,spamTest是从文本中获取的。#另外的区别是:本次函数去除了高频词,比如and, is等,使用20条样本用作测试使用。# localwords_test()#4.7.2 分析数据:显示地域相关的用词#该函数就是计算出每类别中各单词的概率,取出概率大于-6.0的单词并打印。# getTopWords_test()


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部