朴素贝叶斯分类涉及的相关概念
贝叶斯分类:
贝叶斯分类是一类分类算法的总称,这类算法均以贝叶斯定理为基础,故统称为贝叶斯分类。
贝叶斯定理:
贝叶斯定理是概率论中的一个定理,它跟随机变量的条件概率以及边缘概率分布有关。在有些关于概率的解释中,贝叶斯定理能够告知我们如何利用新证据修改已有的看法。通常,事件A在事件B(发生)的条件下的概率,与事件B在事件A(发生)的条件下的概率是不一样的。然而,这两者是有确定的关系的,贝叶斯定理就是这种关系的陈述。贝叶斯公式的一个用途在于通过已知的三个概率函数推出第四个。
贝叶斯理论*:
简单概括为,定义p1(x,y)为类型一,p2(x,y)为类型二,相当于将要分类的类别通过概率进行区分,之后对于要判断类别的数据进行相同概率运算。即:若给出数据为(x,y),计算出其对应的P(c1|x,y)和P(c2|x,y)。
若P(c1|x, y) > P(c2|x, y), 那么属于类别 c1
若P(c2|x, y) > P(c1|x, y), 那么属于类别 c2
以此达到分类的作用。
朴素贝叶斯:
基于贝叶斯理论的前提下,附加条件为朴素,即每个特征都是相互独立,都是相同重要的,没有特殊化待遇,都是朴素的特征。
条件概率:
与高中数学所学的差不多,在此不多提。强调一个。P(A|B)是指在事件B发生的情况下事件A发生的概率。其中涉及的后验概率和先验概率也不多提,详情可以见百度贝叶斯定理。
以上为朴素贝叶斯的一些基本理论,下面即为算法的一些应用实践。
应用实例一、判断是否为侮辱性言语
第一个步骤: 创建收集数据
def loadDataSet():
"""
创建数据集
:return: 将一句言论进行断句后变为一个个单词列表postingList,
每句话的所属类别classVec。其中(0为非侮辱性,1为侮辱性)
"""
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] #
return postingList, classVec
第二个步骤: 对已经收集的数据进行处理
def createVocabList(dataSet):
"""
获取所有单词的集合
:param dataSet: 数据集
:return: 所有单词的集合(即不含重复元素的单词列表)
"""
vocabSet = set([]) # create empty set
for document in dataSet:
# 操作符 | 用于求两个集合的并集
vocabSet = vocabSet | set(document) # union of the two sets
return list(vocabSet)
此步骤就是将原先数据集的词的数量进行压缩,将第一歩收集的多个词语,去重后留下不重复的并成一个列表。也可称为一个词汇表。相当于一个小字典。
如下:
[‘cute’, ‘love’, ‘help’, ‘garbage’, ‘quit’, ‘I’, ‘problems’, ‘is’, ‘park’, ‘stop’, ‘flea’, ‘dalmation’, ‘licks’, ‘food’,
‘not’, ‘him’, ‘buying’, ‘posting’, ‘has’, ‘worthless’, ‘ate’, ‘to’, ‘maybe’, ‘please’, ‘dog’, ‘how’, ‘stupid’, ‘so’,
‘take’, ‘mr’, ‘steak’, ‘my’]
def setOfWords2Vec(vocabList, inputSet):
"""
遍历查看该单词是否出现,出现该单词则将该单词置1
:param vocabList: 所有单词集合列表
:param inputSet: 输入数据集
:return: 匹配列表[0,1,0,1...],其中 1与0 表示词汇表中的单词是否出现在输入的数据集中
"""
# 创建一个和词汇表等长的向量,并将其元素都设置为0
returnVec = [0] * len(vocabList)# [0,0......]
# 遍历文档中的所有单词,如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1
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
即对数据集中的每个单词在上一步形成的词汇表中的位置一一对应。即查找了字典里每个词的位置。此时,通过这一步得到的结果returnVec我们可以知道每句话中的单词出现信息都呈现在单词表中。这时,数据处理的任务可以算是完成了,然后就可以进行下一步的操作。
第三个步骤: 具体算法计算及实现
绕了那么大的圈,回到了问题最开始的地方,前面讲了两个步骤都是打杂的工作,就像做一道菜,菜谱给你了,前面的步骤把该准备的原料,要使用的辅料都给你放好讲清楚了,现在是大展身手的时候了。很显然,朴素贝叶斯在这个问题上的具体运用还是需要思考理解分析一下的。
上面介绍的比较简单,只留下了两个公式。
这里还是需要仔细理解一下,说到底就是比较P(c1|x, y) 和 P(c2|x, y)的大小。这代表的是数据点(x,y)在来自类别c1和来自类别c2的概率。这里要应用一种有效计算条件概率的被称为贝叶斯准则的东西。即:
为了得到需要的p(c1|x,y),可以推导出下列公式:
而在处理这个具体问题的时候,显然还要对上式进行进一步处理,我们要把此处的x,y替换成w(一个向量)。
这样就得到了计算公式。
对每个量分析:
p(ci)=某类别(侮辱或不侮辱)的总数\总的文档数 即:第一个任务中得到的classDev中的0或1的数目除以classDev列表的长度。
p(w)在此处可不去计算,因为在比较p(c1|w)和p(c2|w)时,两者的分母相同,则可不考虑,只比较两者分子的大小即可。
p(w|ci)此值即将一个类型的文件的概率总和。具体见训练函数。
训练函数:
def _trainNB0(trainMatrix, trainCategory):
"""
训练数据原版
:param trainMatrix: 文件单词矩阵 [[1,0,1,1,1....],[],[]...]
:param trainCategory: 文件对应的类别[0,1,1,0....],列表长度等于单词矩阵数,其中的1代表对应的文件是侮辱性文件,0代表不是侮辱性矩阵
:return:
"""
numTrainDocs = len(trainMatrix)
numWords = len(trainMatrix[0]
pAbusive = sum(trainCategory) / float(numTrainDocs) #即p(c1) 侮辱性文件的概率
p0Num = zeros(numWords) # [0,0,0,.....]
p1Num = zeros(numWords) # [0,0,0,.....]
p0Denom = 0.0
p1Denom = 0.0
for i in range(numTrainDocs):
if trainCategory[i] == 1:
p1Num += trainMatrix[i] #[0,1,1,....] + [0,1,1,....]->[0,2,2,...
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p1Vect = p1Num / p1Denom# [1,2,3,5]/90->[1/90,...]
p0Vect = p0Num / p0Denom
p1Num和p0Num分别是两种不同类型,单词表中每个单词出现的次数的矩阵,而p1Denom和p0Denom分别为两个类型的单词总数。
由此得到两个矩阵P(F1|C1),P(F2|C1),P(F3|C1),P(F4|C1),P(F5|C1) 和P(F1|C0),P(F2|C0),P(F3|C0),P(F4|C0),P(F5|C0)...。
return p0Vect, p1Vect, pAbusive
此时,可以得到公式右边所需要的各个量,但这个方法有一定的问题。
问题一:要计算多个概率的乘积以获得文档属于某个类别的概率,即计算 p(w0|1) * p(w1|1) * p(w2|1)。如果其中一个概率值为 0,那么最后的乘积也为 0。
解决方法:为降低这种影响,可以将所有词的出现数初始化为 1,并将分母初始化为 2
问题二:当计算乘积 p(w0|ci) * p(w1|ci) * p(w2|ci)… p(wn|ci) 时,由于大部分因子都非常小,所以程序会下溢出或者得到不正确的答案。
解决方法:化乘为加,采用自然对数。对乘积取自然对数。在代数中有 ln(a * b) = ln(a) + ln(b), 于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。
则有了改良后的实现。
def trainNB0(trainMatrix, trainCategory):
"""
训练数据优化版本
:param trainMatrix: 文件单词矩阵
:param trainCategory: 文件对应的类别
:return:
"""
numTrainDocs = len(trainMatrix)
numWords = len(trainMatrix[0])
pAbusive = sum(trainCategory) / float(numTrainDocs)
p0Num = ones(numWords)#[0,0......]->[1,1,1,1,1.....]
p1Num = ones(numWords)
p0Denom = 2.0
p1Denom = 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)
p0Vect = log(p0Num / p0Denom)
return p0Vect, p1Vect, pAbusive
第四个步骤: 将测试数据进行处理
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + log(pClass1) # P(w|c1) * P(c1) ,即贝叶斯准则的分子
#此处的vec2Classify为测试数据在字典表中是否存在返回一个列表,如下:
[0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0]
#此处用加法连接而不是公式中的乘法是因为在上一步中对最后的值求了自然对数,则此处也就变乘为加了。
p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1) # P(w|c0) * P(c0) ,即贝叶斯准则的分子·
if p1 > p0:
return 1
else:
return 0
vec2Classify*p1Vec是两个对应的量相乘,则只有在字典表中出现的词语会与对应在c1类型中该词出现的概率相乘,从而得到其该类型的可能性。(由此点特征也表明,该方法需要保证样本数据比较大,即字典表的信息较为丰富,不然难以进行计算)
第五个步骤:运行 (此步骤…emmm…就是把前面的都串起来,没什么强调的)
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)
应用实例二:过滤垃圾邮件
进行步骤前的了解和准备:
准备工作:提供的数据集为50封邮件(英文),分为垃圾邮件和非垃圾邮件两类。
了解方面:区别是否为垃圾邮件,原理与实例一相似,故此实例中沿用实例一中改进后的算法函数,即上面的trainNB0()函数。
第一个步骤:数据初步处理
因为这个实例是对邮件进行分析处理,首先对邮件内容进行处理,和例子一一样的思路,先将邮件的长句转化为一个一个的单词单词形成列表。此处采用正则表达式的方法处理。下面为实现代码:
def textParse(bigString):
import re
listOfTokens = re.split(r'\W*', bigString)
return [tok.lower() for tok in listOfTokens if len(tok) > 2]#(单词长度小于2的单词大多无特别意思,就不考虑,减小字典的大小)
第二个步骤:数据的分析及判断
def spamTest():
docList = []
classList = []
fullText = []
for i in range(1, 26):
wordList = textParse(open('input/4.NaiveBayes/email/spam/%d.txt' % i).read())
'''读出每封邮件内容,将每个长度超过2的单词记录到一列表中,返回出来。例子如下:
['peter', 'with', 'jose', 'out', 'town', 'you', 'want', 'meet', 'once', 'while', 'keep', 'things', 'going', 'and', 'some', 'interesting', 'stuff', 'let', 'know', 'eugene']
'''
docList.append(wordList) #doclist为所有信的内容的列表即:[[第,一,封],[第,二,封],[,,],[,,],,,]
classList.append(1)
docList.append(wordList)
wordList = textParse(open('input/4.NaiveBayes/email/ham/%d.txt' % i).read())
fullText.extend(wordList)
classList.append(0)
vocabList = createVocabList(docList) #求并集,得到单词表
trainingSet = range(50)
testSet = []
for i in range(10): #随机从50封邮件中任取10封为测试数据,剩余的40封作为数据集
randIndex = int(random.uniform(0, len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
trainMat = []
trainClasses = []
for docIndex in trainingSet:
trainMat.append(setOfWords2Vec(vocabList, docList[docIndex]))#每个单词在单词表中的某个位置是否出现
trainClasses.append(classList[docIndex]) #邮件类型
p0V, p1V, pSpam = trainNB0(array(trainMat), array(trainClasses))
errorCount = 0
#下面为检查错误率
for docIndex in testSet:
wordVector = setOfWords2Vec(vocabList, docList[docIndex])
if classifyNB(array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
errorCount += 1
print 'the errorCount is: ', errorCount
print 'the testSet length is :', len(testSet)
print 'the error rate is :', float(errorCount)/len(testSet)
#多次运行测试,错误率在0.1以内(此时为任意取40封邮件做数据集,10封做测试集)
最后:看看数据集的大小对于错误率的影响有多大。(表格内数据为跑5次结果取平均值)
数据集大小 | 测试集大小 | 错误率 |
---|---|---|
40 | 10 | 0.08 |
45 | 5 | 0 |
10 | 40 | 0.325 |
25 | 25 | 0.16 |
表格显示的不够明显。用matplotlib画图,对其错误率跑10次取平均值,其结果绘图为:
由此可以看出,当总数据集50封邮件一定时,用以测试的数据越小,准确度越高。
应用实例三:从个人广告中获取区域倾向
进行步骤前的了解和准备:
工作:基本与前两个实例操作相同。
了解:该实例是通过常用词判断用户区域。
额外需要了解:
python相关的库有:feedparser和operator。
feedparser库:使用它我们可轻松地实现从任何 RSS 或 Atom 订阅源得到标题、链接和文章的条目了。
RSS是站点用来和其他站点之间共享内容的简易方式( 也叫聚合内容)。RSS使用XML作为彼此共享内容的标准方式。它代表了Really Simple Syndication(或RDF Site Summary,RDF站点摘要)。 它能让别人很容易的发现你已经更新了你的站点, 并让人们很容易的追踪他们阅读的 所有weblogs。
operator库:本模块主要包括一些python内部操作符对应的函数。这些函数主要分为几类:对象比较、逻辑比较、算术运算和序列操作。
##### 第一个步骤:数据初步处理
此步骤与前两个实例的处理不同,同样是构建词向量,但前两个值考虑是否出现,这里要考虑出现的次数,故不是设为1,而是出现一次就加一。前两个实例,只考虑单词出现与否构成的是字典形式,这个实例考虑单词出现的次数,这种方法被称为“词袋模型”。
def bagOfWords2VecMN(vocaList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocaList:
returnVec[vocabList.index(word)] += 1
return returnVec
第二个步骤:数据分析及处理
其算法,因为还是与句子中的关键词有关且涉及分类问题,故仍采用前面已经构件好的算法函数trainNB0()。 不过数据分析上有部分变动,
“`
def localWords(feed1,feed0):
feed1和feed2的格式为:feed1 = feedparser.parse(‘要获取的URL地址’)即:两个RSS源。
docList=[] #和前面的实例一样
classList=[]
fullText=[]
minLen=min(len(feed1['entries']),len(feed0['entries'])) #取数目的最小值,这也是总的数据量的值
for i in range(minLen):
wordList=textParse(feed1['entries'][i]['summary'])#此处“feed1['entries'][i]['summary']”在feedparser库中的一个操作,表示一篇文章的摘要。
docList.append(wordList)#与前一个例子相同
fullText.extend(wordList)
classList.append(1)
wordList=textParse(feed0['entries'][i]['summary'])
docList.append(wordList)
fullText.extend(wordList)
classList.append(0)
vocabList=createVocabList(docList)
top30Words=calcMostFreq(vocabList,fullText) #该函数后面介绍,是将提及的词语次数进行统计分析,返回排名前30的,在后面将这30个词去除,这样的词往往无法体现地域性。
for pairW in top30Words:
if pairW[0] in vocabList:
vocabList.remove(pairW[0])
trainingSet=range(2*minLen)
testSet=[]
for i in range(20):#和之前相同
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
“`
而后,引出两个对于单词数量的统计处理的函数:
def calcMostFreq(vocabList,fullText):
freqDict={}
for token in vocabList: #遍历词汇表中的每个词
freqDict[token]=fullText.count(token) #统计每个词在文本中出现的次数
sortedFreq=sorted(freqDict.iteritems(),key=operator.itemgetter(1),reverse=True) #根据每个词出现的次数从高到底对字典进行排序
return sortedFreq[:30] #返回出现次数最高的30个单词
def getTopWords(ny,sf):#显示出最具地域特征性的词汇
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**"
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**"
for item in sortedNY:
print item[0]
相关代码: