机器学习3朴素贝叶斯
朴素贝叶斯,基本思想就是,给出一个分类问题,对于待求项,属于哪个分类的概率最大,那这个待求项就属于哪个分类。
给出基本公式
假设要分类物有n个特征,分别为F1、F2、F3、…、Fn,现在有m个类别分别是C1、C2、C3、…、Cm.贝叶斯就是计算出概率最大的那个分类。
具体贝叶斯定理参考http://zh.wikipedia.org/wiki/%E8%B4%9D%E5%8F%B6%E6%96%AF%E5%AE%9A%E7%90%86
对于多个特征,我们进行求解。
P(C|F1F2...Fn)= P(F1F2...Fn|C)P(C) / P(F1F2...Fn)
解释也就是,在都有这些特征的基础上属于类别C的概率等于在已知是C类,含有这些特征的概率乘以该分类是C类的概率,最终除以该分类物都含有这些特征的概率。
好,对于P(F1F2...Fn)对所有的分类物都是一样的,这样问题就可以简化为求
P(F1F2...Fn|C)P(C)的最大值。
而对于朴素贝叶斯来说,它更简化了,它认为分类物所有的特征都是独立的。这样我们就可以进一步简化为:
P(F1F2...Fn|C)P(C)= P(F1|C)P(F2|C) ... P(Fn|C)P(C)
这样我们的计算就变得简单很多了,在给定分类下某个特征发生的概率,这个我们根据样本数据是可以得到的。左边就可以计算出来。
虽然现实中可能这些所有的特征都相互独立,不过通过这样的假设求出的结果还是比较准确的。
这边是假设相互独立,而如果假设一个特征只与前面一个特征或者i个特征有关的话,那这个又转化成马尔科夫的问题。
好,那下面就通过Python代码来进一步的了解这个问题。这是一个社区留言板的例子,主要就是对文本进行分类,对有侮辱性的言论的文本进行屏蔽。
1: #-*- coding: utf-8 -*
2: #导入数据 postingList对应社区的6条留言,[0,1,0,1,0,1]对应于2、4、6句是侮辱性语句,即所属类别
3: def loadDataSet():
4: postingList=[['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
5: ['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
6: ['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
7: ['stop', 'posting', 'stupid', 'worthless', 'garbage'],
8: ['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
9: ['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
10: classVec = [0,1,0,1,0,1]
11: return postingList,classVec
12:
13: #得到词汇表
14: def createVocabList(dataSet):
15: vocabSet = set([]) #set是不重复集
16: for document in dataSet:
17: vocabSet = vocabSet | set(document) #对每个文档求得的结果求并,去重复的
18: return list(vocabSet)#转换成list
19:
20: #根据词汇表构成0向量,对每个文档每个词对应向量中的位置赋值为1
21: def setOfWords2Vec(vocabList, inputSet):
22: returnVec = [0]*len(vocabList)#构建vocabList长度的0向量
23: for word in inputSet:
24: if word in vocabList:
25: returnVec[vocabList.index(word)] = 1
26: else: print "the word: %s is not in my Vocabulary!" % word
27: return returnVec
上述代码所做的事情是找出所有文档中所有不重复的词,构成词汇表,根据词汇表及其长度构建每个文档的词向量(文档中的词对应词汇表中的位置为1,在这边没有考虑这个词在文档中出现了几次,只考虑了这个词在文档中是否出现,没有考虑不同的词的权重不一样,后续讨论)。
得到如下结果,比如第一个文档的词向量
1: >>> import bayes
2: >>> listOPosts,listClasses = bayes.loadDataSet()
3: >>> myVocabList = bayes.createVocabList(listOPosts)
4: >>> bayes.setOfWords2Vec(myVocabList,listOPosts[0])
5: [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1]
下面给出计算每一个词在每个类别下的概率。
1:
2: def trainNB0(trainMatrix,trainCategory):
3: numTrainDocs = len(trainMatrix)# 6
4: numWords = len(trainMatrix[0])#得到词汇表的长度32
5: pAbusive = sum(trainCategory)/float(numTrainDocs)#(0+1+0+1+0+1)/6=0.5
6: p0Num = zeros(numWords)#长度为numWords32全为0的数组
7: p1Num = zeros(numWords)
8: p0Denom = 0.0
9: p1Denom = 0.0
10: for i in range(numTrainDocs):
11: if trainCategory[i] == 1:
12: p1Num += trainMatrix[i]
13: p1Denom += sum(trainMatrix[i])
14: else:
15: p0Num += trainMatrix[i]
16: p0Denom += sum(trainMatrix[i])
17: p1Vect = p1Num/p1Denom
18: p0Vect = p0Num/p0Denom
19: return p0Vect,p1Vect,pAbusive
结果
1: >>> import bayes
2: >>> trainMatrix,trainCategory = bayes.getTrainMatrix()
3: >>> p0V,p1V,pAb = bayes.trainNB0(trainMatrix,trainCategory)
4: >>> p0V
5: array([ 0.04166667, 0.04166667, 0.04166667, 0. , 0. ,
6: 0.04166667, 0.04166667, 0.04166667, 0. , 0.04166667,
7: 0.04166667, 0.04166667, 0.04166667, 0. , 0. ,
8: 0.08333333, 0. , 0. , 0.04166667, 0. ,
9: 0.04166667, 0.04166667, 0. , 0.04166667, 0.04166667,
10: 0.04166667, 0. , 0.04166667, 0. , 0.04166667,
11: 0.04166667, 0.125 ])
12: >>> p1V
13: array([ 0. , 0. , 0. , 0.05263158, 0.05263158,
14: 0. , 0. , 0. , 0.05263158, 0.05263158,
15: 0. , 0. , 0. , 0.05263158, 0.05263158,
16: 0.05263158, 0.05263158, 0.05263158, 0. , 0.10526316,
17: 0. , 0.05263158, 0.05263158, 0. , 0.10526316,
18: 0. , 0.15789474, 0. , 0.05263158, 0. ,
19: 0. , 0. ])
这边我添加了一个函数
1: def getTrainMatrix():
2: listOPosts,listClasses = loadDataSet()
3: myVocabList = createVocabList(listOPosts)
4: trainMat = []
5: for postinDoc in listOPosts:
6: trainMat.append(setOfWords2Vec(myVocabList,postinDoc))
7: return trainMat,listClasses
这边其实是有问题的,之所有这边没有直接套用贝叶斯公式,因为这边计算单个词在某个类别下的概率为0很正常,具体看公式
P(c0|w)=P(w|c0)P(c0)/P(w)
在计算P(w|c0) = p(w0,w1,w2…,wn|c0),这边为朴素贝叶斯,则考虑w相互独立,
则P(w|c0) = P(w0|c0).P(w1|c0)…….P(wn|c0)
而其中任意一个wi在某个类别下不出现很正常,则P(wi|c0)就为0,则P(c0|w)= 0,显然这个不是我们想要的。
为了降低这种影响,我们将所有词出现初始化为1,并将分母初始化为2
修改部分如下:
1: p0Num = ones(numWords)#长度为numWords32全为1的数组
2: p1Num = ones(numWords)
3: p0Denom = 2.0
4: p1Denom = 2.0
1: 0.0000034*0.00000023*0.000000045*0.00000000045*0.00000000000000045*0.0000000000000034*0.0000000000000000000000000000000000000001*0.00000000000000000000000001*0.000000000000000000000001*0.000000000000000000000000000000000000000000000000000000000000000000000000000000001*0.00000000000000000000000000000000000000000001*0.0000000000000000000000000000000000000000000001*0.00000000000000000000000000001
2: 0.0
这个我们一般的处理办法是通过取对数的方式。这样小数就可以转化大值类型的数,避免最后的四舍五入,可以看如下:
1: >>> log(0.00000000000000000000000000000000000000000001)
2: -101.31374409173802
则把源代码修改如下:
1: p1Vect = log(p1Num/p1Denom) #change to log()
2: p0Vect = log(p0Num/p0Denom) #change to log()
另外要说明的是取对数了不影响函数的单调性,形象一点可以看下图:
这几个问题解决后,下面开始写出最后的分类算法,,继续给出点公式,我们知道
P(w|c0)P(c0) = P(w0|c0).P(w1|c0)…….P(wn|c0)P(c0)
这样两边取对数
ln(P(w|c0)P(c0)) = ln((w0|c0))+ln(P(w1|c0))+……+ln(P(wn|c0))+ln(P(c0))
之前说了,对于P(w)大家一样,要求的话通过全概率公式即可以求。我们就不求了。
由于函数加ln不影响函数单调性,这样对于P(c|w)我们只要求ln((w0|c0))+ln(P(w1|c0))+……+ln(P(wn|c0))+ln(P(c0))
而每一个ln(wi|ci)我们已经求出。这样对于一个新的w,我们看w中有哪些wi,这样我们构建一个词汇表大小的向量,对i位赋值为1,其它位赋值为0,我们通过向量相乘,这样就可以求得每一个ln((wi|ci),然后相加,最后再加上ln(ci)就可以求得。
代码如下:
1: def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
2: p1 = sum(vec2Classify * p1Vec) + log(pClass1)
3: p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)
4: if p1 > p0:
5: return 1
6: else:
7: return 0
最后我们给出测试函数:
1: def testingNB():
2: listOPosts,listClasses = loadDataSet()
3: myVocabList = createVocabList(listOPosts)
4: trainMat=[]
5: for postinDoc in listOPosts:
6: trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
7: p0V,p1V,pAb = trainNB0(array(trainMat),array(listClasses))
8: testEntry = ['love', 'my', 'dalmation']
9: thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
10: print testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb)
11: testEntry = ['stupid', 'garbage']
12: thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
13: print testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb)
最终结果如下:
1: >>> import bayes
2: >>> bayes.testingNB()
3: ['love', 'my', 'dalmation'] classified as: 0
4: ['stupid', 'garbage'] classified as: 1
之前我们说过上面只考虑了,这个词是否出现过,没有考虑这个词出现了几次。下面将考虑词出现的次数,代码如下:
1: def bagOfWords2VecMN(vocabList, inputSet):
2: returnVec = [0]*len(vocabList)
3: for word in inputSet:
4: if word in vocabList:
5: returnVec[vocabList.index(word)] += 1 #这边改成了累加
6: return returnVec
示例:使用朴素贝叶斯过滤垃圾邮件
给出过程如下:
切分文本
这边是英文,英文由于单词之间有空格,方便切分,而中文除了句子之间有标点符号外就不好分割了,这样就有了一个新的问题,中文分词问题。这个在以后会介绍,这边根据书先对英文进行分词。
python中有split()方法很容易就切分。
1: >>> myStr = 'This book is the best book on Python.'
2: >>> myStr.split()
3: ['This', 'book', 'is', 'the', 'best', 'book', 'on', 'Python.']
1: >>> import re
2: >>> regEx = re.compile('\\W*')
3: >>> listOfTokens = regEx.split(myStr)
4: >>> listOfTokens
5: ['This', 'book', 'is', 'the', 'best', 'book', 'on', 'Python', '']
之后还有一个空格,我们通过查看字符的长度来解决
1: >>> [tok for tok in listOfTokens if len(tok)>0]
2: ['This', 'book', 'is', 'the', 'best', 'book', 'on', 'Python']
另外我们考虑构建词库,并不用考虑单词的大小写,全部改为小写
1: >>> [tok.lower() for tok in listOfTokens if len(tok)>0]
2: ['this', 'book', 'is', 'the', 'best', 'book', 'on', 'python']
文本解析是很大的一个工程,这个正则表达式成书的就好几本了,值得研究还有很多,这边只作简单的解析,复杂的先不予考虑。
比如对于如下文档解析
1: >>> emailText = 'http://www.google.com/support/sites/bin/answer.py?hl=en&answer=174623'
2: >>> listOfTokens = regEx.split(emailText)
3: >>> listOfTokens
4: ['http', 'www', 'google', 'com', 'support', 'sites', 'bin', 'answer', 'py', 'hl', 'en', 'answer', '174623']
里面会含有py、en这些不是单词,所有在考虑对于这些的时候,我们处理的时候去掉长度小于3的字符串。
下面给出怎么训练及测试代码
1: def textParse(bigString):
2: import re
3: listOfTokens = re.split(r'\W*', bigString)
4: return [tok.lower() for tok in listOfTokens if len(tok) > 2]
5:
6: def spamTest():
7: docList=[]; classList = []; fullText =[]
8: for i in range(1,26):
9: wordList = textParse(open('email/spam/%d.txt' % i).read())
10: docList.append(wordList)
11: fullText.extend(wordList)
12: classList.append(1)
13: wordList = textParse(open('email/ham/%d.txt' % i).read())
14: docList.append(wordList)
15: fullText.extend(wordList)
16: classList.append(0)
17: vocabList = createVocabList(docList)#创建词汇表
18: trainingSet = range(50); testSet=[] #创建从50个样本集随机取10个作为测试集
19: for i in range(10):
20: #random.uniform(a, b),用于生成一个指定范围内的随机符点数,两个参数其中一个是上限,一个是下限。
21: randIndex = int(random.uniform(0,len(trainingSet)))
22: testSet.append(trainingSet[randIndex])
23: del(trainingSet[randIndex])
24: trainMat=[]; trainClasses = []
25: print('len(trainingSet)',len(trainingSet))#这边是40,要说明一下这个方法
26: for docIndex in trainingSet:
27: trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
28: trainClasses.append(classList[docIndex])
29: p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
30: errorCount = 0
31: for docIndex in testSet:
32: wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
33: if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
34: errorCount += 1
35: print "classification error",docList[docIndex]
36: print 'the error rate is: ',float(errorCount)/len(testSet)
随机选取一部分作为训练集,剩下的作为测试集,用80%作为训练集,20%作为测试集,这种方法叫留存交叉验证。现在我们只作出一次迭代,为了更精确的估计分类器的错误率,我们应该多次迭代后求出平均错误率。
最后结果如下:
1: >>> import bayes
2: >>> bayes.spamTest()
3: ('len(trainingSet)', 40)
4: classification error ['oem', 'adobe', 'microsoft', 'softwares', 'fast', 'order', 'and', 'download', 'microsoft', 'office', 'professional', 'plus', '2007', '2010', '129', 'microsoft', 'windows', 'ultimate', '119', 'adobe', 'photoshop', 'cs5', 'extended', 'adobe', 'acrobat', 'pro', 'extended', 'windows', 'professional', 'thousand', 'more', 'titles']
5: the error rate is: 0.1
这边有误判的邮件,如果将垃圾邮件判为正常邮件,这个还好,如果把正常邮件判别为垃圾邮件这个就有问题了,下面我们会给出模型的评估,以及模型的修正,这个再后面会继续介绍。
下面给出另外一个例子。
从个人广告中获取区域倾向
前面介绍了过滤网站恶意留言,第二个过滤垃圾邮件,这个例子来发现地域相关的用词
具体流程
这边给出一个题外话,这边要安装feedparser,安装之前用Setuptools 参考https://pypi.python.org/pypi/setuptools#windows windows安装直接下载脚本ez_setup.py
或者直接下载了安装版http://www.lfd.uci.edu/~gohlke/pythonlibs/#setuptools
不过我用的是2.7的它有个bug,这个要修改一下,不然安装报错,具体参考http://bugs.python.org/review/9291/diff/1663/Lib/mimetypes.py
python安装目录lib下的mimetypes.py要修改,修改地方我贴过来
1: import sys
2: import posixpath
3: import urllib
4: +from itertools import count
5: try:
6: import _winreg
7: except ImportError:
8: @@ -239,19 +240,11 @@
9: return
10:
11: def enum_types(mimedb):
12: - i = 0
13: - while True:
14: + for i in count():
15: try:
16: - ctype = _winreg.EnumKey(mimedb, i)
17: + yield _winreg.EnumKey(mimedb, i)
18: except EnvironmentError:
19: break
20: - try:
21: - ctype = ctype.encode(default_encoding) # omit in 3.x!
22: - except UnicodeEncodeError:
23: - pass
24: - else:
25: - yield ctype
26: - i += 1
下面我们就可以简单的使用这个feedparser,下面查看一下条目的列表数目
1: >>> import feedparser
2: >>> ny = feedparser.parse('http://newyork.craigslist.org/stp/index.rss')
3: >>> len(ny['entries'])
4: 100
下面写下程序RSS源分类器及高频词去除函数。
在对文本进行解析的时候,我们分析每个词出现的次数,但是有些词出现的很多,但是却没有实际的意思,反而影响权重,比如我们中文中的,的、得等词,英文中的一些简单的代词,谓语动词等等,因此处理的时候要去掉这些高频词汇。
下面处理的时候添加去除前30个高频词的函数
1: def calcMostFreq(vocabList,fullText):
2: import operator
3: freqDict = {}
4: for token in vocabList:#遍历词汇表
5: freqDict[token]=fullText.count(token)#统计token出现的次数 构成词典
6: sortedFreq = sorted(freqDict.iteritems(), key=operator.itemgetter(1), reverse=True)
7: return sortedFreq[:30]
8:
9: #这个跟spamTest()基本上一样,不同在于这边访问的是RSS源,最后返回词汇表,以及不同分类每个词出现的概率
10: def localWords(feed1,feed0):#使用两个RSS源作为参数
11: import feedparser
12: docList=[]; classList = []; fullText =[]
13: minLen = min(len(feed1['entries']),len(feed0['entries']))
14: for i in range(minLen):
15: wordList = textParse(feed1['entries'][i]['summary'])
16: docList.append(wordList)
17: fullText.extend(wordList)
18: classList.append(1)
19: wordList = textParse(feed0['entries'][i]['summary'])
20: docList.append(wordList)
21: fullText.extend(wordList)
22: classList.append(0)
23: vocabList = createVocabList(docList)#创建词汇表
24: top30Words = calcMostFreq(vocabList,fullText) #去掉top30的词
25: for pairW in top30Words:
26: if pairW[0] in vocabList: vocabList.remove(pairW[0])
27: trainingSet = range(2*minLen); testSet=[] #创建测试集
28: for i in range(20):
29: randIndex = int(random.uniform(0,len(trainingSet)))
30: testSet.append(trainingSet[randIndex])
31: del(trainingSet[randIndex])
32: trainMat=[]; trainClasses = []
33: for docIndex in trainingSet:
34: trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
35: trainClasses.append(classList[docIndex])
36: p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
37: errorCount = 0
38: for docIndex in testSet:
39: wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
40: if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
41: errorCount += 1
42: print 'the error rate is: ',float(errorCount)/len(testSet)
43: return vocabList,p0V,p1V
运行结果
1: >>> import bayes
2: >>> import feedparser
3: >>> ny = feedparser.parse('http://newyork.craigslist.org/stp/index.rss')
4: >>> sf = feedparser.parse('http://sfbay.craigslist.org/stp/index.rss')
5: >>> vocabList,pSF,pNY = bayes.localWords(ny,sf)
6: the error rate is: 0.45
7: >>> vocabList,pSF,pNY = bayes.localWords(ny,sf)
8: the error rate is: 0.4
9: >>> vocabList,pSF,pNY = bayes.localWords(ny,sf)
10: the error rate is: 0.45
11: >>> vocabList,pSF,pNY = bayes.localWords(ny,sf)
12: the error rate is: 0.25
13: >>> vocabList,pSF,pNY = bayes.localWords(ny,sf)
14: the error rate is: 0.3
15: >>> vocabList,pSF,pNY = bayes.localWords(ny,sf)
16: the error rate is: 0.4
17: >>> vocabList,pSF,pNY = bayes.localWords(ny,sf)
18: the error rate is: 0.35
19: >>> vocabList,pSF,pNY = bayes.localWords(ny,sf)
20: the error rate is: 0.5
21: >>> vocabList,pSF,pNY = bayes.localWords(ny,sf)
22: the error rate is: 0.45
为了得到错误率的精确估计,我们对上述实验进行多次,后取平均值。
另外这个错误率挺高的,不过我们现在关心的是单词的概率,我们可以改变上面去掉30个词,我们可以去掉前100,另外我们可以通过整理的停用词表,就是用于句子结构的辅助词表,这样最后的错误率会有一定的改观。
最后我们分析一下数据,显示地域相关的用词,也就是计算在这个分类下出现概率最大的词。
1: def getTopWords(ny,sf):
2: import operator
3: vocabList,p0V,p1V=localWords(ny,sf)
4: topNY=[]; topSF=[]
5: for i in range(len(p0V)):
6: if p0V[i] > -5.4 : topSF.append((vocabList[i],p0V[i]))
7: if p1V[i] > -5.4 : topNY.append((vocabList[i],p1V[i]))
8: sortedSF = sorted(topSF, key=lambda pair: pair[1], reverse=True)
9: print "SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**"
10: for item in sortedSF:
11: print item[0]
12: sortedNY = sorted(topNY, key=lambda pair: pair[1], reverse=True)
13: print "NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**"
14: for item in sortedNY:
15: print item[0]
结果如下:
1: >>> import bayes
2: >>> import feedparser
3: >>> ny = feedparser.parse('http://newyork.craigslist.org/stp/index.rss')
4: >>> sf = feedparser.parse('http://sfbay.craigslist.org/stp/index.rss')
5: >>> bayes.getTopWords(ny,sf)
6: the error rate is: 0.4
7: SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**
8: watch
9: true
10: let
11: found
12: play
13: chill
14: should
15: live
16: where
17: other
18: NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**
19: minded
20: did
21: women
22: nyc
23: home
24: feel
25: few
26: non
27: platonic
28: average
29: problem
那这样这朴素贝叶斯的使用基本完成,虽然我们假设属性值相互独立不准确,不过有研究表明,在有些领域该算法完全可以跟决策树,神经网络这样算法媲美。
如果独立假设可以满足的话,则该分类算法和其他分类算法相比的话,目前有最高的准确率跟效率。
对于贝叶斯方法,下面可以研究的贝叶斯网络方法,这个在后续工作中会继续学习。