现在谈到朴素贝叶斯算法,总是有种很熟悉的感觉,不是因为我是个算法大佬啊,而是因为刚来导师实验室这边学习的时候,最早接触的算法就是贝叶斯算法了。我当时还转载了网上一位博主写的关于贝叶斯算法的文章,最近在整理前段时间所学的一些知识,所以决定结合自己的感受好好写写,不想敷衍大家,也希望大家能有点感触
在网上其实找了很多关于这方面的资料,但是很悲哀的发现大家写的都大同小异,由于前段时间刚好看了点关于自然语言处理方面的东西,所以想结合NLP关于文本向量的处理方式等方面从另一个角度试试来谈谈自己对于朴素贝叶斯算法的新理解。讲的不好的地方也希望大家多多包涵
由于讲的是部分见解,所以这次我就不大段大段的介绍关于朴素贝叶斯算法的相关知识了,直接就以《机器学习实战》这本书上的文档分类这一实例来讲吧
文档分类应用
在文档分类中,整个文档(比如一封电子邮件)是实例,那么邮件中的单词就可以定义为特征。说到这里,我们有两种定义文档特征的方法。一种是词集模型,另外一种是词袋模型。顾名思义,词集模型就是对于一篇文档中出现的每个词,我们不考虑其出现的次数,而只考虑其在文档中是否出现,并将此作为特征;假设我们已经得到了所有文档中出现的词汇列表,那么根据每个词是否出现,就可以将文档转为一个与词汇列表等长的向量。而词袋模型,就是在词集模型的基础上,还要考虑单词在文档中出现的次数,从而考虑文档中某些单词出现多次所包含的信息。
好了,讲了关于文档分类的特征描述之后,我们就可以开始编代码,实现具体的文本分类了
1 拆分文本,准备数据
要从文本中获取特征,显然我们需要先拆分文本,这里的文本指的是来自文本的词条,每个词条是字符的任意组合。词条可以为单词,当然也可以是URL,IP地址或者其他任意字符串。将文本按照词条进行拆分,根据词条是否在词汇列表中出现,将文档组成成词条向量,向量的每个值为1或者0,其中1表示出现,0表示未出现。
接下来,以在线社区的留言为例。对于每一条留言进行预测分类,类别两种,侮辱性和非侮辱性,预测完成后,根据预测结果考虑屏蔽侮辱性言论,从而不影响社区发展。
词表到向量的转换函数
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'],
['my','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):
#新建一个存放词条的集合
vocabSet=set([])
#遍历文档集合中的每一篇文档
for document in dataSet:
#将文档列表转为集合的形式,保证每个词条的唯一性
#然后与vocabSet取并集,向vocabSet中添加没有出现
#的新的词条
vocabSet=vocabSet|set(document)
#再将集合转化为列表,便于接下来的处理
return list(vocabSet)
#根据词条列表中的词条是否在文档中出现(出现1,未出现0),将文档转化为词条向量
def setOfWords2Vec(vocabSet,inputSet):
#新建一个长度为vocabSet的列表,并且各维度元素初始化为0
returnVec=[0]*len(vocabSet)
#遍历文档中的每一个词条
for word in inputSet:
#如果词条在词条列表中出现
if word in vocabSet:
#通过列表获取当前word的索引(下标)
#将词条向量中的对应下标的项由0改为1
returnVec[vocabSet.index(word)]=1
else: print('the word: %s is not in my vocabulary! '%'word')
#返回inputet转化后的词条向量
return returnVec
上面是用setOfWords2Vec(vocabSet,inputSet)函数即词集模型将文档转化为词条向量,还有一种方法就是使用
bagOfWords2VecMN(vocabList, inputSet)函数即词袋模型将文档转化为词条向量。这两种方法区别很小
词集模型,即对于一篇文档,将文档中是否出现某一词条作为特征,即特征只能为0不出现或者1出现;然后,一篇文档中词条的出现次数也可能具有重要的信息,于是我们可以采用词袋模型,在词袋向量中每个词可以出现多次,这样,在将文档转为向量时,每当遇到一个单词时,它会增加词向量中的对应值
我先将bagOfWords2VecMN(vocabList, inputSet)函数的代码贴出来大家看看,等会儿再一起讲讲它们的优缺点
# 输出文档向量,词袋模型
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1
return returnVec
1)经过自己的一番比较,我发现使用词袋模型转换的文本向量是要比词集模型转换的文本向量取得的效果要好些的,因为正如我上文所提到的:词集模型只是将文档中是否出现某一词条作为特征,即特征只能为0不出现或者1出现。这样会缺失很多关键信息,而词袋模型统计每个词出现的次数的话,所携带的信息自然也多些
2)请记住朴素贝叶斯算法有两个重要假设:
1. 文本中每个词(即特征)之间相互独立,即一个特征或者单词出现的可能性与它和其他单词相邻没有关系
2. 文本中每个特征同等重要,通俗地讲就是每个特征或者单词出现的概率是相等的
正是由于有这两个重要假设,所以我们才能安然理得的使用贝叶斯算法。因为说实话如何合理的将文本转换为文本向量是要考虑很多信息的,当你接触到RNN(循环神经网络)或者是NLP(自然语言处理)这方面的知识时,你会发现一段文本所能携带的信息有太多了。就比如说“明天是星期一,我要去学校”这句话,如果是按照朴素贝叶斯算法的考量,则认为“我”,“星期一”,“学校”这三个词语之间是没有联系即相互独立的,但是在现实世界中很显然这三个单词之间是有很大联系的。这些上下午之间的联系是可以通过一个数据较大的语料库训练出来的(即概率),最后是要保存到文本向量中,再往下讲就是比较深层次的知识了,大家要是感兴趣可以去看看相关专业书籍以了解更多的东西(好吧!坦白说是我肚子里没货啦,哈哈哈哈哈) 说了这么多就是希望大家能好好理解朴素贝叶斯算法的一些内核东西(虽然算法很好理解,但是我们可不能一带而过呦)
3)word2vec(即上文中所提到的词集模型和词袋模型的总称)向量化算法的缺点
这方法真的是太easy了,虽然好用但肯定是存在很多问题的,我大概总结了一下有如下三个问题(其实是别人总结我照搬过来的啦,厚颜无耻的我一点都不脸红,嘿嘿)
1. 维度灾难。很显然,如果上述例子词典中包含10000个单词,那么每个文本需要用10000维的向量表示,也就是说文本中出现的词语位置不为0,其余9000多的位置均为0,如此高维度的向量会严重影响计算速度
2. 无法保留次序信息
3. 存在语义鸿沟的问题
在这儿再多嘴一句,对于word2vec向量化算法改进的方案就是要解决维度灾难这个问题,说到这儿,大家可能立马就想到了降维这个方法,那就恭喜你了,说明你是有点底子的(哈哈哈哈,大佬般的语气) 可以参考神经网络语言模型方法,这里面会告诉你答案的
2. 再给出一个书上的实例:朴素贝叶斯的另一个应用--过滤垃圾邮件的源码,让大家对算法有个比较形象的认识吧(其实是为了凑字数啦,因为源码很简单,大家一看就懂,不懂也要装懂啊!)
#处理数据长字符串
#1 对长字符串进行分割,分隔符为除单词和数字之外的任意符号串
#2 将分割后的字符串中所有的大些字母变成小写lower(),并且只
#保留单词长度大于3的单词
def testParse(bigString):
import re
listOfTokens=re.split(r'\W*',bigString)
return [tok.lower() for tok in listOPosts if len(tok)>2]
def spamTest():
#新建三个列表
docList=[];classList=[];fullTest=[]
#i 由1到26
for i in range(1,26):
#打开并读取指定目录下的本文中的长字符串,并进行处理返回
wordList=testParse(open('email/spam/%d.txt' %i).read())
#将得到的字符串列表添加到docList
docList.append(wordList)
#将字符串列表中的元素添加到fullTest
fullTest.extend(wordList)
#类列表添加标签1
classList.append(1)
#打开并取得另外一个类别为0的文件,然后进行处理
wordList=testParse(open('email/ham/&d.txt' %i).read())
docList.append(wordList)
fullTest.extend(wordList)
classList.append(0)
#将所有邮件中出现的字符串构建成字符串列表
vocabList=createVocabList(docList)
#构建一个大小为50的整数列表和一个空列表
trainingSet=range(50);testSet=[]
#随机选取1~50中的10个数,作为索引,构建测试集
for i in range(10):
#随机选取1~50中的一个整型数
randIndex=int(random.uniform(0,len(trainingSet)))
#将选出的数的列表索引值添加到testSet列表中
testSet.append(trainingSet[randIndex])
#从整数列表中删除选出的数,防止下次再次选出
#同时将剩下的作为训练集
del(trainingSet[randIndex])
#新建两个列表
trainMat=[];trainClasses=[]
#遍历训练集中的吗每个字符串列表
for docIndex in trainingSet:
#将字符串列表转为词条向量,然后添加到训练矩阵中
trainMat.append(setOfWords2Vec(vocabList,fullTest[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 error rate is:',float(errorCount)/len(testSet))
if __name__ == '__main__':
errorRate = 0.0
i = 0
while i < 100:
errorRate += spamTest()/100
i += 1
print("The 10 average errorRate is:", errorRate)
在代码中,我最终是使用交叉验证测试了100次,最终取平均值,这样得到的结果会相对准确些
好了,终于讲完了。如果后面深入了解了自然语言处理这一块,我会再回来补充这一板块的,敬请期待啦(其实我心底里也没底)