目录
引入
生成式模型和判别式模型
朴素贝叶斯实例(了解三大知识点)
朴素贝叶斯相关代码理解
使用Python进行文本分类
从文本中构建词向量
训练算法:从词向量计算概率
朴素贝叶斯分类器
使用朴素贝叶斯过滤垃圾邮件
切分文本
使用朴素贝叶斯进行交叉验证
使用朴素贝叶斯分类器通过博主个人博客内容辨别博主知识侧重点
导入并处理RSS源数据
构造RSS源分类器
分析数据:显示两个博主的博客的表征词
总结:
前面学习的内容都是要求分类器直接给出分类结果,今天学习的朴素贝叶斯可以给出一个最优的类别猜测结果,同时给出这个猜测的概率估计值。
以垃圾邮件分类为例,一封邮件通过朴素贝叶斯得到的分类结果可能会是如下结果:
没有一个准确猜测,但是会给每个猜测计算概率,最后会选取概率最大的猜测作为结果。
先验概率:在训练模型之前,通过历史数据得到的初始概率;该概率与待测样本无关,独立于样本。
后验概率:反映了在看到数据样本x后cj成立的置信度,即通过样本获得新的信息(既有先验概率资料,也有补充资料),利用贝叶斯公式对先验概率进行修正,而后得到的概率。
后验概率实为一个条件概率,即为已知历史数据和现有数据互不重叠的前提下,在历史数据的条件下,现有数据发生的概率
区别:先验概率只需要简单地计算,没有用到贝叶斯公式;而后验概率的计算,要用到贝叶斯公式,而且在利用样本资料计算逻辑概率时,还要使用理论概率分布,需要更多的数理统计知识。
注意:大部分机器学习模型尝试得到后验概率
机器学习可以分为两大视角,分别是生成式模型和判别式模型,本次的朴素贝叶斯是生成式模型的典型代表。
生成式模型是对联合概率P(Di,X)建立函数映射关系。
判别式模型是对条件概率P(Di|X)建立函数映射关系,该模型在学习过程中会生成一个显式决策边界,当待测样本的数据(耦合地包含了决策条件)落入某个边界,则意味着该数据属于这个类别。
举个我能理解的栗子:如何确定一只羊是山羊还是绵羊。
生成式模型:该模型会从训练数据中分别学习出一个山羊模型和一个绵羊模型,然后从待测的样本里提取特征,将特征分别扔进两个模型中并计算条件概率和,哪个概率和大,就属于哪个模型。
判别式模型:该模型会从训练数据中分别提取并学习绵羊和山羊的特征并生成参数,这些参数耦合地包含了区别两种羊的决策条件,然后从待测的样本里提取特征,把特征投入判别式模型直接判别结果。
可以看出,在分类的过程中,生成式模型会产生多个猜测结果模型,生成式模型将从待测样本中提取到的特征分别扔入每个猜测结果模型,遍历完一遍后取概率最大的猜测模型为最终结果,而判别式模型是特征通过唯一一个模型提取得到最终结果。
直接从公式、定理中理解朴素贝叶斯对我来说没有记忆点,所以我从一个实例问题入手。
利用朴素贝叶斯算法对文本分类:
第一步:根据问题写出两个条件概率的表达式,分别代表c是China类和c非China类:
第二步:根据条件概率定理把上式展开:
(知识点一)朴素贝叶斯之所以朴素,是因为假设了特征与特征之间是相互独立的。在这里就可以很好地应用它朴素的特性,特征之间相互独立,求各个特征的联合概率时就是将各个概率直接相乘。
因此,以下两式相等:
这样我们就可以更简单地求解第一步得到的概率表达式,由题易得:P(C)=3/4,P(Chinese|C)=5/8.但是这时可以发现P(Tokyo|C)为零,这个0直接导致了整个概率变成了0,这里就可以引入
(知识点二)——拉普拉斯修正。
别看这个公式长得难看,其实是很好用的,将这个为0的概率,分子加1,分母加上Tokyo这个数据类型的可能取值的数量,即文档中出现的各不相同的词的数量(Chinese、Beijing、Shanghai、Macao、Tokyo、Japan)共6个,因此:
P(Tokyo|C)=0/8 ==>P(Tokyo|C)=(0+1)/(8+6) =1/14
同样,原本为0的P(Japan|C)=1/14,而不为零的P(Chinese|C)也需要进行拉普拉斯修正:P(Chinese|C)=3/7
第三步:还有下列这个概率表达式(c非China类)
同样根据条件概率定理把上式展开:
可以发现(知识点三)这个概率式的分母和第一步的概率式展开后的式子分母相同,因此概率表达式的分母对决策结果没有影响,在计算比较两个概率的时候,只需要计算表达式的分子,再次简化了计算过程。
最后的答案已经不重要了,重要的是我从这个实例中较好地理解了三个知识~
这是一个常见的朴素贝叶斯的应用——文档分类。
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) #首先是set去重 然后的并集就是把去重后的document加到vocabSet里
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
loadDataSet()是创建一些实验样本,主要是模拟文本被处理后的结果——根据文本切割完成的单词,第一个返回值是多个单词集合,第二个返回值是类别标签的集合;createVocabList()是利用set数据类型内的元素不重复的特点构造一个单词不重复的词汇表;setOfWords2Vec()是根据词汇表把输入文档转为文档向量,判断词汇表中的单词是否出现在输入文档中,文档向量中的1代表文档里出现了词汇表这个位置的词,0则没有。
测试一下效果:
myVocabList
['posting','not','stupid','ate','flea','him','please','dalmation',
'quit','licks','stop','worthless','maybe','park','take','is','my',
'problems','food','cute','mr','dog','help','how','I','love',
'garbage','steak','buying','has','so','to']
bayes.setOfWords2Vec(myVocabList,listOPosts[0])
[0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,0,0,1,0,0]
朴素贝叶斯最主要的就是根据以下公式计算每个类的概率,再比较各个类的概率,返回概率最大的类作为最后的结果:
计算概率的过程:首先通过类别i中文档数除以总的文档数来计算p(ci);然后是计算p(w|ci),这里就要用到朴素贝叶斯假设,将w展开为一个个独立特征,假设所有单词都互相独立,该假设也称作条件独立性假设,它意味着可以使用p(w1|ci)p(w2|ci).。。。。p(wn|ci)来计算上述概率,这就极大地简化了计算的过程:
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix)
numWords = len(trainMatrix[0])
pAbusive = sum(trainCategory)/float(numTrainDocs)#算出p(c1)
p0Num = zeros(numWords); p1Num = zeros(numWords) #概率初始化
p0Denom = 0.0; p1Denom = 0.0 #概率初始化
for i in range(numTrainDocs):
if trainCategory[i] == 1:
p1Num += trainMatrix[i] #计算p(w/c1)的分子
p1Denom += sum(trainMatrix[i]) #计算p(w/c1)的分母
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p1Vect = p1Num/p1Denom #计算p(w/c1)
p0Vect = p0Num/p0Denom #计算p(w/c0)
return p0Vect,p1Vect,pAbusive
以上的计算过程主要是利用Numpy数组来处理。返回值为两个向量p1V,p0V和一个概率pAb,p1V为词汇表中各个单词属于类1的概率集合,p0V为词汇表中各个单词属于类0的概率集合,pAb代表全部单词属于类1的概率。
输出结果:
myVocabList
['to', 'garbage', 'worthless', 'is', 'posting', 'has', 'not', 'buying',
'cute', 'him', 'problems', 'steak', 'park', 'dog', 'licks' 'I', 'so', 'ate',
'quit', 'please', 'help', 'stupid', 'stop', 'dalmation', 'maybe', 'love',
'mr', 'food', 'how', 'flea', 'my', 'take']
p0V
array([0.04166667, 0. , 0. , 0.04166667, 0. ,
0.04166667, 0. , 0. , 0.04166667, 0.08333333,
0.04166667, 0.04166667, 0. , 0.04166667, 0.04166667,
0.04166667, 0.04166667, 0.04166667, 0. , 0.04166667,
0.04166667, 0. , 0.04166667, 0.04166667, 0. ,
0.04166667, 0.04166667, 0. , 0.04166667, 0.04166667,
0.125 , 0. ])
p1V
array([0.05263158, 0.05263158, 0.10526316, 0. , 0.05263158,
0. , 0.05263158, 0.05263158, 0. , 0.05263158,
0. , 0. , 0.05263158, 0.10526316, 0. ,
0. , 0. , 0. , 0.05263158, 0. ,
0. , 0.15789474, 0.05263158, 0. , 0.05263158,
0. , 0. , 0.05263158, 0. , 0. ,
0. , 0.05263158])
pAb
0.5
我们可以看到p0V里概率最大的是0.125,这是词汇表里"my"的位置,这说明my是最能表征类0的单词;p1V里概率最大的是0.15789474,这是词汇表里"stupid"的位置,这说明stupid是最能表征类1的单词。
上面的函数已经有了一个不错的计算效果,但是我们还得考虑实际计算中会遇到的两个情况:
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix)
numWords = len(trainMatrix[0])
pAbusive = sum(trainCategory)/float(numTrainDocs)#算出p(c1)
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):
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
classifyNB()函数有四个输入:需要分类的文档的文档向量vec2Classify,以及trainNB0训练出的三个返回值,其中的计算过程就反映了朴素贝叶斯模型的原理:返回概率最大的类别为最终结果,其中就有把乘积取自然对数,换乘法为加法
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', 'garbage']
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print (testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb))
testEntry = ['stupid','my', 'love']
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print (testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb))
这是一个封装了文档输入以及前期计算操作的函数。
输出结果:
['love', 'my', 'garbage'] classified as: 0
['stupid', 'my', 'love'] classified as: 0
['stupid', 'garbage'] classified as: 1
输出了一个不错的分类结果,但是还可以改进。
我们前面得到的文档向量只能反映一个单词是否出现,但实际一个文档中单词出现的频率也能代表某种意义,因此我们需要用到一种叫词袋模型的方法——每遇到一个单词,它会增加词向量中的对应值,而不是单单将数值设为1:
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0]*len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1
return returnVec
前面我们的实验都是用已经切割完成的单词,但实际应用是要直接对原始文本进行分类,因此我们需要一步数据准备:
def textParse(bigString):
import re
listOfTokens = re.split(r'\W+',bigString)
return [tok.lower() for tok in listOfTokens if len(tok) >2]
re.split(r'\W*', bigString)是以除单词、数字外的任意字符串为分隔符切分文本;return [tok.lower() for tok in listOfTokens if len(tok) > 2]是返回小写且长度大于2的字符串,因为当对URL进行切分时,会出现一些长度小于3但我们又不需要的字符串,我们就可以通过以上方法去除这些字符串。以上的文本解析是经过简化的,实际上的过程相当复杂。
下面我们进行的邮件分类实验,共有50封电子邮件,其中10封电子邮件被随机选择为测试集:
def spamTest():
docList=[]; classList = []; fullText =[]
#准备数据 处理数据
for i in range(1,26):
wordList = textParse(open('email/spam/%d.txt' %i).read())
# print(wordList)
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)#创建词汇表
# print(vocabList)
#垃圾25条,非垃圾25条,一共50条
trainingSet = list(range(50)); testSet=[] #create test set
#准备交叉验证,50条里随机10条来做测试集,剩余的做训练集
for i in range(10):
randIndex = int(random.uniform(0,len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
trainMat=[]; trainClasses = []
# 训练出p0V,p1V,pSpam
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 float(errorCount)/len(testSet)
以上的过程是留存交叉验证,在所有数据中随机选择一部分作为测试集,剩余的作为训练集;主要是要训练出p0V、p1V、pAb这三个,其实就是按需调用前面的函数:首先是读出数据,处理数据;然后划分训练集和测试集;接着就是把训练集的文档转为文档向量:那就是需要词汇表和输入文档;接着训练出p0V、p1V、pAb;最后就是根据p0V、p1V、pAb对测试集进行测试。
输出结果:
classification error ['benoit', 'mandelbrot', '1924', '2010', 'benoit', 'mandelbrot', '1924', '2010', 'wilmott', 'team', 'benoit', 'mandelbrot', 'the', 'mathematician', 'the', 'father', 'fractal', 'mathematics', 'and', 'advocate', 'more', 'sophisticated', 'modelling', 'quantitative', 'finance', 'died', '14th', 'october', '2010', 'aged', 'wilmott', 'magazine', 'has', 'often', 'featured', 'mandelbrot', 'his', 'ideas', 'and', 'the', 'work', 'others', 'inspired', 'his', 'fundamental', 'insights', 'you', 'must', 'logged', 'view', 'these', 'articles', 'from', 'past', 'issues', 'wilmott', 'magazine']
the error rate is: 0.1
效果还不错,那我们还可以多次迭代平均,更加精确地估计分类器的错误率:
>>>avererro = 0.0
for i in range(10):
thisError = bayes.spamTest()
avererro += thisError
avererro = avererro/10
输出:
avererro
0.06999999999999999
这就是朴素贝叶斯的其中一个经典应用。
前面的例子都是朴素贝叶斯的经典应用,朴素贝叶斯的应用还有很多,下面我们来看一个例子——通过博主的CSDN博客内容来判断博主知识侧重点,本来是是要跟着书上做通过个人征婚广告信息获取区域倾向的,但是书上提到的SSR源好像有问题,所以我做这个例子。参考了下面这个博客:基于概率论的分类方法:朴素贝叶斯及CSDN_RSS源分析_OraYang的博客-CSDN博客https://blog.csdn.net/u010665216/article/details/78231982首先是收集数据,我们这次不从文本中读取数据,而是利用RSS,从RSS源收集内容,我们可以测试一下效果:
>>>ny = feedparser.parse('http://blog.csdn.net/u010665216/rss/list')
>>>len(ny['entries'])
20
成功读取。
因为博客内容都是与编程相关的,所以两个博主的内容中会有很多重复的高频词,这些重复的高频词对判别结果具有迷惑性,所以我们需要去除一些高频词:
def calcMostFreq(vocabList,fullText):
import operator
freqDict = {} #记录单词频数的集合
for token in vocabList:
freqDict[token]=fullText.count(token) #会以map的形式储存单词频数
#根据单词的出现频次排序
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) #OraYang的博客是类1
wordList = textParse(feed0['entries'][i]['summary'])
docList.append(wordList)
fullText.extend(wordList)
classList.append(0) #J_shine的博客是类0
vocabList = createVocabList(docList)#创建去重词汇表
top30Words = calcMostFreq(vocabList,fullText) #前三十个高频词
# print(top30Words)
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 range(8):
randIndex = int(random.uniform(0,len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
trainMat=[]; trainClasses = []
for docIndex in trainingSet:#把输入转为向量
trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
trainClasses.append(classList[docIndex])
#训练出各个词句的概率
p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
errorCount = 0
#测试
for docIndex in testSet:
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
这个过程和前面的spamTest()相似,无非就是留存交叉验证,在所有数据中随机选择一部分作为测试集,剩余的作为训练集;主要是要训练出p0V、p1V、pSam这三个,其实就是按需调用前面的函数:首先是读出数据,处理数据;然后划分训练集和测试集;接着就是把训练集的文档转为文档向量:那就是需要词汇表和输入文档;接着训练出p0V、p1V、pSam;最后就是根据p0V、p1V、pSam对测试集进行测试,另外还多了一个去除高频词的过程,原因上面也解释过了。
看一下效果:
avererro = 0.0
for i in range(10):
thisError = bayes.localWords(oraYang,j_shine)
avererro += thisError
print ('the average error rate is: ',avererro/10)
the error rate is: 0.5
the error rate is: 0.0
the error rate is: 0.25
the error rate is: 0.375
the error rate is: 0.25
the error rate is: 0.125
the error rate is: 0.25
the error rate is: 0.125
the error rate is: 0.375
the error rate is: 0.375
the average error rate is: 0.2625
平均错误率为0.2625,是个不错的结果,但是还想看一下哪些单词是能够表征博主的文章:
def getTopWords(ny,sf):
import operator
vocabList,p0V,p1V=localWords(ny,sf)
topNY=[]; topSF=[]
#设置阈值并按序来展示两个博主的博客的特征词汇
for i in range(len(p0V)):
if p0V[i] > -4.6 : topSF.append((vocabList[i],p0V[i]))
if p1V[i] > -4.5 : topNY.append((vocabList[i],p1V[i]))
sortedNY = sorted(topNY, key=lambda pair: pair[1], reverse=True)
print ("OraYang*OraYang*OraYang*OraYang*OraYang*OraYang*OraYang*OraYang*OraYang*OraYang*")
for item in sortedNY:
print (item[0])
sortedSF = sorted(topSF, key=lambda pair: pair[1], reverse=True)
print ("J_shine*J_shine*J_shine*J_shine*J_shine*J_shine*J_shine*J_shine*J_shine*J_shine*")
for item in sortedSF:
print (item[0])
是通过设置阈值并根据频次排序来展示两个博主的博客的特征词汇
输出结果:
the error rate is: 0.125
OraYang*OraYang*OraYang*OraYang*OraYang*OraYang*OraYang*OraYang*OraYang*OraYang*
integers
linked
matrix
represented
listnode
80324425
nbsp
using
80556000
list
return
intervals
J_shine*J_shine*J_shine*J_shine*J_shine*J_shine*J_shine*J_shine*J_shine*J_shine*
str
80564649
null
oldtab
mvc
可以从结果看出OraYang博主偏向于python、基础算法,J_shine博主偏向于java后台相关的内容。
对于分类而言,使用概率有时要比使用硬规则更为有效,贝叶斯概率及贝叶斯准则提供了一种利用已知值来估计未知概率的有效方法。使用朴素贝叶斯可以通过假设特征之间的条件独立,降低对数据量的需求以及计算的复杂度。独立性假设是指一个词的出现概率并不依赖于文档中的其他词。当然我们也知道这个假设过于简单。这就是之所以称为朴素贝叶斯的原因。尽管条件独立性假设并不正确,但是朴素贝叶斯仍然是一种有效的分类器。利用现代编程语言来实现朴素贝叶斯时需要考虑很多实际因素。下溢出就是其中一个问题,它可以通过对概率取对数来解决。词袋模型在解决文档分类问题上比词集模型有所提高。