这本书的前两章要求分类器做出艰难决策,给出“该数据实例属于哪一类”这类问题的明确答案。不过,分类器有时会产生错误结果,这时可以要求分类器给出一个最优的类别猜测结果,同时给出这个猜测的概率估计值。
优点:在数据较少的情况下仍然有效,可以处理多类别的问题
缺点:对于输入数据的准备方法较为敏感
使用数据类型:标称型数据
核心思想:选择高概率对应的类别
机器学习的一个重要的应用就是文档的自动分类。
我们可以观察文档中出现的词,并把每个词的出现或不出现作为一个特征,这样得到的特征数目就会跟词汇表中的词数目一样多。朴素贝叶斯是贝叶斯分类器的一个扩展,是用于文档分类的常用算法
朴素:每个条件是相对独立的,互不影响,完全条件无关,在数学上称之为朴素
一般过程: 1.收集数据:可以使用任何方法,本章使用RSS源 2.准备数据:需要数值型或者布尔型数据 3.分析数据:有大量特征时,绘制特征作用不大,此时使用直方图效果更好 4.训练数据:计算不同的独立特征的条件概率 5.测试算法:计算错误率 6.使用算法:一个常见的朴素贝叶斯应用是文档分类。可以在任意的分类场景中使用朴素贝叶斯分类器,不一定是文本
由统计学可知,如果每个特征需要N个样本,那么对于10个特征将需要N的十次方个样本
如果特征之间相互独立,那么样本数就可以从N的1000次方减少到1000*N
朴素的含义:1.各特征相互独立 2.每个特征同等重要
要从文本中获取特征,需要先拆分文本
这里的特征是来自文本的词条,一个词条是字符的任意组合。可以把词条想象成单词,也可以使用非单词词条,如URL、IP地址或者其他任意字符串。然后将每个文本片段表示为一个词条向量,其中值为1表示出现在文档中,值为0表示未出现
把文本看成单词向量或词条向量,也就是说把句子转换为向量。考虑出现在所有文档中的所有单词,再决定将哪些词纳入词汇表或者说所要的词汇集合,然后必须要将每一篇文档转换为词汇上的向量
# 词表到向量的转换函数 # 创建实验样本 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]] # 1代表侮辱性文字,0代表正常言论 # 这些文本的类别由人工标注,这些标注信息用于训练程序以便自动检测侮辱性留言 classVec=[0,1,0,1,0,1] return postingList,classVec # 创建一个包含所有文档中不重复词的列表 def createVocabList(dataSet): # set构造函数,返回一个不重复词表 # 先创建一个空集合 vocabSet=set([]) # 将每篇文档返回的新词集合添加到该集合中 for document in dataSet: # |求两个集合的并集 vocabSet=vocabSet|set(document) # 返回list的不重复词表 return list(vocabSet) # 输出文档向量,向量的每一元素为1或0,分别表示词汇表中的单词在输入文档中是否出现 # 输入 词汇表 文档 def setOfWords2Vec(vocabList,inputSet): # 创建一个所含元素都为0的向量 returnVec=[0]*len(vocabList) # 遍历每个词汇,如果在文档里,向量为1,如果不在,向量为0 # 遍历文档中的每一个单词 for word in inputSet: # 检查其是否在词汇表中 if word in inputSet: # 在的话就对应向量处为1 returnVec[vocabList.index(word)]=1 else: # 不在的话则输出该单词不在字典里 print("这个单词%s不在字典里" %word) # 返回最终得到的向量 return returnVec
调用上面定义的loadDataSet()函数和createVocabList()函数运行可得到结果
listOposts,listClasses=loadDataSet() myVocabList=createVocabList(listOposts) listOposts,listClasses,myVocabList
得到的运行结果为:
然后调用色图OfWordsVec()函数可得到词向量
setOfWords2Vec(myVocabList,listOposts[0])
运行结果为:
利用贝叶斯进行计算
# 朴素贝叶斯分类器训练函数 import numpy as np # 文档向量矩阵 每篇文档类别标签所构成的向量 def trainNB0(trainMatrix,trainCategory): # 得到文档矩阵的数量 numTrainDocs=len(trainMatrix) # 得到第一个文档的长度 numWords=len(trainMatrix[0]) # 得到属于侮辱性文档的概率 # 属于侮辱性文档的数量/文档数量 pAbusive=sum(trainCategory)/float(numTrainDocs) # 生成一个numWords*1的全是0的矩阵 p0Num=np.zeros(numWords) p1Num=np.zeros(numWords) # 初始化概率 p0Denom=0.0 #不属于侮辱性文章的概率 p1Denom=0.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=p1Num/p1Denom p0Vect=p0Num/p0Denom # 返回属于侮辱性文字的概率、不属于侮辱性文字的概率 侮辱性文档的概率 return p0Vect,p1Vect,pAbusive
调用上面定义的函数并运行可得到
myVocabList=createVocabList(listOposts) # 不重复的所有词列表 print(myVocabList) # 创建一个空的列表 trainMat=[] # 得到一个向量列表 for postinDoc in listOposts: # 关于此列表是否在listOposts文档中 trainMat.append(setOfWords2Vec(myVocabList,postinDoc)) print(trainMat) # 所有文档的向量列表 分类的类别 p0V,p1V,pAb=trainNB0(trainMat,listClasses) # 输入侮辱性文档的概率 两个类别的概率向量 pAb,p0V,p1V
运行结果为:
可以看到先是输出了所有的不重复词列表,和所有文档的向量列表
然后下面输出了输入侮辱性文档的概率和两个类别每个词分别的概率矩阵
# 修改trainNB0()函数 # import numpy as np def trainNB0(trainMatrix,trainCategory): numTrainDocs=len(trainMatrix) numWords=len(trainMatrix[0]) # 得到属于侮辱性文档的概率 pAbusive=sum(trainCategory)/float(numTrainDocs) # 为防止概率为0的数据对结果造成影响,将所有词的出现数设置为1,并将分母初始化为2 # 生成一个numWords*1的全是1的矩阵 p0Num=np.ones(numWords) p1Num=np.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=np.log(p1Num/p1Denom) p0Vect=np.log(p0Num/p0Denom) return p0Vect,p1Vect,pAbusive # 朴素贝叶斯分类函数 # 要分类的向量 三个概率 def classifyNB(vec2Classfy,p0Vec,p1Vec,pClass1): p1=sum(vec2Classfy*p1Vec)+np.log(pClass1) p0=sum(vec2Classfy*p0Vec)+np.log(1.0-pClass1) # 如果p1的概率大于p0的概率,则类别为1 if p1>p0: return 1 # 反之则为0 else: return 0
检测分类效果
def testingNB(): # 加载原始数据 list0Posts,listClasses=loadDataSet() # 得到不重复单词列表 myVocabList=createVocabList(list0Posts) # 创建一个空向量 trainMat=[] # 遍历原始文档列表中的每个文档 for postinDoc in list0Posts: # 得到文档向量矩阵 trainMat.append(setOfWords2Vec(myVocabList,postinDoc)) # 得到三个概率 p0V,p1V,pAb=trainNB0(np.array(trainMat),np.array(listClasses)) # 定义测试数据 testEntry=[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,classfied as:,classifyNB(thisDoc,p0V,p1V,pAb))
运行这个函数
testingNB()
得到的运行结果为:
将每个词的出现与否作为一个特征->词集模型 每个词出现的次数作为特征->词袋模型
def bagOfWords2VecMN(vocabList,inputSet): returnVec=[0]*len(vocabList) for word in inputSet: if word in vocabList: returnVec[vocabList.index(word)]+=1 return returnVec
收集数据:提供文本文件 准备数据:将文本文件解析成词条向量 分析数据:检查词条确保解析的完整性 训练算法:使用我们之前建立的trainNB0()函数 测试算法:使用classifyNB(),并且构建一个新的测试函数来计算文档集的错误率 使用算法:构建一个完整的程序对一组文档进行分类,将错分的文档输出到屏幕上
mySent=This book is the best book on python or M.L. I have ever laid eye upon. # split函数默认分隔符为空格 mySent.split()
得到的运行结果为:
可以看到切分的效果还具有标点符号,可以使用正则表达式切分句子,以此来去掉标点符号
import re # \W+除字母数字意外的符号 regEx=re.compile(\W+) listOfTokens=regEx.split(mySent) listOfTokens
得到的结果为:
还有一种正则表达式为分开所有字符:
# 分开所有字符 regEx1=re.compile(\W*) listOfTokens1=regEx1.split(mySent) listOfTokens1
得到的运行结果为:
去掉所有空格
[tok for tok in listOfTokens if len(tok)>0]
得到:
或者直接将所有字母转为小写
# .lower()函数将所有单词转为小写 .upper()函数将所有的那次转为大写 [tok.lower() for tok in listOfTokens if len(tok)>0]
得到结果为
文件解析及完整的垃圾邮件测试函数
# 解析为字符串列表 def textParse(bigString): import re # 进行正则表达式拆分 listOfTokens=re.split(W+,bigString) # 过滤掉字母长度小于3的单词并转为小写 return [tok.lower() for tok in listOfTokens if len(tok)>2] import random import numpy as np # 对贝叶斯邮件分类器做自动化处理 def spamTest(): docList=[] classList=[] fullText=[] # 导入并解析准备好的26个文本文件 for i in range(1,26): worldList = textParse(open(email/spam/%d.txt % i).read()) # worldList=textParse(open(email/spam/%s.txt % str(i) ).read()) docList.append(worldList) fullText.extend(worldList) classList.append(1) worldList=textParse(open(email/ham/%d.txt % i).read()) docList.append(worldList) fullText.extend(worldList) classList.append(0) # 得到不重复词列表 vocabList=createVocabList(docList) trainingSet=list(range(50)) testSet=[] # 随机选择十封邮件构测试集 # 训练集中的数据用来进行概率计算 for i in range(10): # 拿到一个随机数 大于0小于训练集长度的一个随机整数 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(np.array(trainMat),np.array(trainClasses)) errorCount=0 # 对测试级进行分类 for docIndex in testSet: wordVector=setOfWords2Vec(vocabList,docList[docIndex]) # 如果得出的分类结果与本身结果不同,错误数+1 if classifyNB(np.array(wordVector),p0V,p1V,pSpam)!=classList[docIndex]: errorCount+=1 print("the error rate is:",float(errorCount/len(testSet))) spamTest()
得到的运行结果为:
如果结论确实是不同,那么他们各自常用的词是哪些
从人们的用词当中,我们能否对不同城市的人所关心的内容有所了解
使用朴素贝叶斯来发现地域相关的用词
①收集数据:从RSS源收集内容,这里需要对RSS源构建一个接口 ②准备数据:将文本文件解析成词条向量 ③分析数据:检查词条向量确保解析的准确性 ④训练算法:使用我们之前建立的trainNB0()函数 ⑤测试算法:观察错误率,确保分类器可用。可以修改切分程序,以降低错误率,提高分类结果 ⑥使用算法:构建一个完整的程序,封装所有的内容。给定两个RSS源,该程序会显示最常用的公共词
检查选择的rss源中是否有数据
import feedparser # 书中提供的RSS源已经不可访问,此处换了一个RSS源 # 检测解析到的RSS源数据是否有数据 ny=feedparser.parse(https://www.sup.org/rss/?feed=history) ny[entries] # 输出数据条数,如果为0则是没有数据 len(ny[entries])
运行结果:
import numpy as np # RSS源分类器及高频词去除函数 def calcMostFreq(vocabList,fullText): import operator freqDict={} for token in vocabList: # 计算每个单词出现的次数 freqDict[token]=fullText.count(token) # 按照逆序从大到小对freqDict进行排序 # iteritems()是python2中的函数,该函数在python3中为tiems() sortedFreq=sorted(freqDict.items(),key=operator.itemgetter(1),reverse=True) # 返回前30个高频单词 return sortedFreq[:30] # 参数为两个解析过的RSS数据源 def localWords(feed1,feed0): import feedparser docList=[] classList=[] # 分类标签 fullText=[] # 拿到两个数据长度最小的那个长度 minLen=min(len(feed1[entries]),len(feed0[entries])) #50 print(minLen) # 遍历连个源数据的前50条数据。并设置分类向量 for i in range(minLen): # 每次访问一条RSS源 worldList=textParse(feed1[entries][i][summary]) # 得到文档矩阵 docList.append(worldList) # 得到一条包含文档中所有词的列表 fullText.extend(worldList) # 添加分类标签 classList.append(1) worldList=textParse(feed0[entries][i][summary]) docList.append(worldList) fullText.extend(worldList) classList.append(0) # 得到包含所有不重复词的列表 vocabList=createVocabList(docList) # 得到出现频率最高的前三十个词,一般都是the、a、等常用单词,与分类无关 top30Words=calcMostFreq(vocabList,fullText) # 遍历这三十个词 for pairw in top30Words: # 如果这个单词在不重复列表中 # print(0) # print(pairw) #(the, 482) 单词+频数在一起 # print(pairw[0]) if pairw[0] in vocabList: # 从列表中删除这个词 vocabList.remove(pairw[0]) #取pairw[0]就是该单词 # 设置一个训练集 trainingSet=list(range(2*minLen)) # 一个测试集 testSet=[] # 随机选择20条数据放到测试集中 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(np.array(trainMat),np.array(trainClasses)) errorCount=0 for docIndex in testSet: worldVector=bagOfWords2VecMN(vocabList,docList[docIndex]) # if classifyNB(np.array(wordVector),p0V,p1V,pSpam)!=classList[docIndex]: # 计算错误率 if classifyNB(np.array(worldVector),p0V,p1V,pSpam)!=classList[docIndex]: errorCount+=1 print(the error rate is :,float(errorCount)/len(testSet)) return vocabList,p0V,p1V
运行上面函数的结果:
ny=feedparser.parse(https://tomaugspurger.github.io/feeds/all.atom.xml) sf=feedparser.parse(https://www.sup.org/rss/?feed=history) vocabList,pSF,pNY=localWords(ny,sf)
运行:
# 显示最具有表征性的词 def getTopWords(ny,sf): import operator 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********************) for item in sortedSF: print(item[0]) sortedNY=sorted(topNY,key=lambda pair:pair[1],reverse=True) print(NY********************) for item in sortedNY: print(item[0]) getTopWords(ny,sf)
得到的运行结果为:
对于分类而言,使用概率有时要比使用硬规则更有效