《机器学习实战》-朴素贝叶斯分类器

一、朴素贝叶斯分类器原理

  这一部分的内容来自《机器学习-周志华》。
(1)朴素贝叶斯表达式的推导
1.假设有分类器h(x)->c,将样本x归类为类c,那么最优的贝叶斯分类器为
这里写图片描述
2.上式可以转化为
这里写图片描述
这里写图片描述
3.由于P(x)对于所有类都相同,因此求h*(x)=arg maxP(c|x)就是求如下式子
这里写图片描述
(2)求解朴素贝叶斯分类器
这里写图片描述
(3)例子
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
(4)拉普拉斯修正
这里写图片描述
这里写图片描述

二、使用朴素贝叶斯分类器对文本进行分类

  本例子是手工制作六个短文本及其对应标签(标签1-侮辱性,标签0-非侮辱性),然后使用该6条数据训练朴素贝叶斯分类进,然后进行分类测试:测试某条文本信息是否具有侮辱性。
(1)将6个短文本数据转化为词向量

'''
    def loadDataSet():创建6条短文本,并给这些短文本贴标签(侮辱类 或 非侮辱类)
'''
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]    #0代表非侮辱类,1代表侮辱类
    return postingList,classVec

'''
   def createVocabList(dataSet):该函数的作用是创建词汇表vocabSet。
   dataSet的格式是函数loadDataSet()返回的格式,可以将
   dataSet出现的所有单词聚集到一个set中(每种单词只会出现一次)
'''
def createVocabList(dataSet):
    vocabSet = set([])  #create empty set
    for document in dataSet:
        vocabSet = vocabSet | set(document) #union of the two sets
    return list(vocabSet)

'''
    def setOfWords2Vec(vocabList, inputSet):inputSet只是某个短文本,该函数将inputSet按照词汇表vocabList的格式转化为词向量。
    例如vocabList=['cute','love','help'],当短文本出现单词'cute',但没出息单词'love'、'help',则
    该行短文本对应的词向量为[1,0,0]
'''
def setOfWords2Vec(vocabList, inputSet):
    returnVec = [0]*len(vocabList)
    for word in inputSet:
        if word in vocabList: #当该word在词汇表vocabList中,则对应位置标记为1,注意如果当前短文本inputSet某个单词
                              # 出现了多次,那么最后该单词在词向量对应位置还是标记为1
            returnVec[vocabList.index(word)] = 1
        else: print ("the word: %s is not in my Vocabulary!" % word)
    return returnVec

展示结果:
这里写图片描述
这里写图片描述

(2)使用数据集对应的词向量集训练贝叶斯分类器
  训练贝叶斯分类器,实质就是分别统计在标签为1(侮辱性)的短文本中各种单词出现的频率、在标签为0(非侮辱性)的短文本中各种单词出现的概率。
所用函数:

'''
    def trainNB(trainMatrix, trainCategory): 其中trainMatrix是词向量矩阵, trainCategory是每个词向量对应的标签。
    返回p0Vect(向量p0Vect表示在非侮辱性文档中,各种单词出现的频率), 
    p1Vect(向量p1Vect表示在侮辱性文档中,各种单词出现的频率), 
    pAbusive(侮辱性的短文本在总体短文本数量中的比例)
'''
def trainNB(trainMatrix, trainCategory):
    numTrainDocs = len(trainMatrix) #获取词向量矩阵的行数,就是获取短文本的个数(每个短文本对应一个词向量)
    numWords = len(trainMatrix[0]) #词向量的长度就是单词表的长度,这也是获取单词表的单词个数
    pAbusive = sum(trainCategory) / float(numTrainDocs) #sum(trainCategory)用来统计标签中1(表示侮辱性)的个数,这句
                                                        # 代码用来计算侮辱性的短文本在总体短文本数量中的比例
    p0Num = zeros(numWords); #创建长度等于词向量长度的0向量p0Num
    p1Num = zeros(numWords)  #创建长度等于词向量长度的0向量p1Num
    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 #向量p1Vect表示在侮辱性文档中,各种单词出现的频率
    p0Vect = p0Num / p0Denom #向量p0Vect表示在非侮辱性文档中,各种单词出现的频率
    return p0Vect, p1Vect, pAbusive

结果:
这里写图片描述
(3)使用贝叶斯分类器进行分类
   这里测试的测试短文本[‘love’, ‘my’, ‘dalmation’]、[‘stupid’, ‘garbage’],要求也贝斯分类器能够将第一个文本分类为非侮辱性(标签为0),将第二个文本分类为侮辱性(标签为1)。
所用函数:

'''
    def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):vec2Classify是待分类短文本对应的词向量。p0Vec表示在非侮辱
    性文档中,各种单词出现的频率。p1Vec表示在侮辱性文档中,各种单词出现的频率。
    该函数可能判断vec2Classify是属于侮辱性文本还是非侮辱性文本
'''
def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):
    p1=prod(vec2Classify*p1Vec)*pClass1 #vec2Classify*p1Vec表示在p1Vec中,将没有在待分类词向量vec2Classify中出现的单
                                        #词的频率置0。prod(vec2Classify*p1Vec)表示将向量(vec2Classify*p1Vec)的各个元素相乘。
                                        #这句代码是求vec2Classify属于侮辱性短文本的概率
    p0=prod(vec2Classify*p0Vec)*(1-pClass1) #这句代码是求vec2Classify属于非侮辱性短文本的概率
    if p1>p0:
        return 1
    else:
        return 0

'''
    testingNB():使用函数loadDataSet()的返回值作为训练集,分别测试短文本['love', 'my', 'dalmation']、['stupid', 'garbage']
    是属于侮辱性文本还是非侮辱性文本
'''
def testingNB():
    listOPosts,listClasses = loadDataSet() #获取短文本训练集
    myVocabList = createVocabList(listOPosts) #获取训练集对应的单词表
    trainMat=[]
    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc)) #将短文本训练集转化对应的词向量集
    p0V,p1V,pAb = trainNB(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))

结果:
   结果不尽人意,贝斯分类器能够将第一、二个文本都分类为非侮辱性(标签为0)【具体原因,参见《周志华-机器学习》中朴素贝叶斯分类器需要拉普拉斯修正的原因】,这就需要作拉普拉斯修正,如何修正如下文。
这里写图片描述
(4)对贝叶斯分类器进行修改,使其更适用于实际情况
1.对函数trainNB()进行拉普拉斯修正(具体原理见“一、朴素贝叶斯分类器原理-(4)拉普拉斯修正”)
 p0Num=ones(numWords);   p1Num=ones(numWords)
 p0Denom=2.0;         p1Denom=2.0
2.将函数trainNB()中的词频率向量 p1Vect、p0Vect中各个元素取对数
    p1Vect = log(p1Num/p1Denom)
    p0Vect = log(p0Num/p0Denom)
  词向量中各元素对应的是每种词出现的频率,在贝叶斯求概率时,需要将这些词频率相乘,但是当这些频率很小时,程序会下溢出(下溢出:指当要表示的数据的绝对值小于计算机所能表示的最小绝对值时,程序会四舍五入得到0)。为了解决下溢出,可以将频率取对数解决。例如两个频率为a,b,则取对数后,乘法需要变成加法:ln(a*b) = ln(a)+ln(b),将函数classifyNB(vec2Classify,p0Vec,p1Vec,pClass1),修改为classifyNB0(vec2Classify, p0Vec, p1Vec, pClass1)。最后分类的准则是比较向量p1Vect与p0Vect的中各元素相乘后的大小关系,取对数后,虽然向量p1Vect与p0Vect的中各元素的值发生了改变,但是不影响最终的判断结果。
  以下是取对数前后的函数图象,它们有相同的单调区间,且在相同的x值上取得极值。
这里写图片描述
3.修改后对应的函数


'''
    def trainNB0(trainMatrix, trainCategory): 其中trainMatrix是词向量矩阵, trainCategory是每个词向量对应的标签。
    返回p0Vect(向量p0Vect表示在非侮辱性文档中,各种单词出现的频率的对数), 
    p1Vect(向量p1Vect表示在侮辱性文档中,各种单词出现的频率的对数), 
    pAbusive(侮辱性的短文本在总体短文本数量中的比例)   
'''
def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix)
    numWords = len(trainMatrix[0])
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    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)          #防止相乘的因子过小,造成相乘结果为0,因此取对数
    p0Vect = log(p0Num/p0Denom)          #防止相乘的因子过小,造成相乘结果为0,因此取对数
    return p0Vect,p1Vect,pAbusive

'''
   classifyNB0(vec2Classify,p0Vec,p1Vec,pClass1):vec2Classify是待分类短文本对应的词向量。这里的p0Vec,p1Vec是从函数
    trainNB0(trainMatrix,trainCategory)获得,因此p0Vec表示在非侮辱性文档中,各种单词出现的频率的对数。p1Vec表示在侮辱性文
    档中,各种单词出现的频率的对数。该函数可能判断vec2Classify是属于侮辱性文本还是非侮辱性文本
'''
def classifyNB0(vec2Classify, p0Vec, p1Vec, pClass1):
    p1 = sum(vec2Classify * p1Vec) + log(pClass1)#vec2Classify*p1Vec表示在p1Vec中,将没有在待分类词向量vec2Classify中出现的单
                                                 #词的频率的对数置0。由于p1Vec是单词出现频率的对数,因此sum()将这些对数相加,相
                                                 #当于将频率相乘。该函数原理与函数classifyNB(vec2Classify,p0Vec,p1Vec,pClass1)相同
    p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1) #与上一句代码原理相同,不过这句代码是计算属于非侮辱性短文本的概率
    if p1 > p0:
        return 1
    else: 
        return 0
'''
    testingNB0():使用函数loadDataSet()的返回值作为训练集,分别测试短文本['love', 'my', 'dalmation']、['stupid', 'garbage']
    是属于侮辱性文本还是非侮辱性文本
'''
def testingNB0():
    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: ',classifyNB0(thisDoc,p0V,p1V,pAb))
    testEntry = ['stupid', 'garbage']
    thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
    print (testEntry,'classified as: ',classifyNB0(thisDoc,p0V,p1V,pAb))

4.运行结果
这里写图片描述
(5)将词集模型(set-of-words model)修改为词袋模型(bag-of-words model)
这里写图片描述

'''
    def bagOfWords2VecMN(vocabList, inputSet):该函数是基于词袋模型的,与函数setOfWords2Vec(vocabList, inputSet)唯一
    不同的是,每当遇到一个单词则增加词向量的对应值,而不只是将对应的值置为1
'''
def bagOfWords2VecMN(vocabList, inputSet):
    returnVec = [0]*len(vocabList)
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] += 1
    return returnVec

三、使用朴素贝叶斯过滤垃圾邮件

  本例子的数据集为50封邮件(25封垃圾邮件、25封非垃圾邮件),随机抽取10封作为测试集,剩下的40封作为训练集来训练贝叶斯分类器。
1. 分词函数函数textParse(bigString)


'''
    def textParse(bigString): 输入长字符串bigString,返回一个单词列表(每个单词的长度>2)
'''
def textParse(bigString):
    import re
    listOfTokens = re.split(r'\W*', bigString)
    return [tok.lower() for tok in listOfTokens if len(tok) > 2] 

这里写图片描述
2.对垃圾邮件使用交叉验证法测试分类结果
  交叉验证法:这个函数随机从50封邮件中(25封垃圾邮件、25封非垃圾邮件)中随机抽取10封作为测试集,剩下的40封作为训练集。

'''
    spamTest():从文件夹email下获取25个垃圾邮件和25个非垃圾邮件的txt文件,随机地从这50个邮件中抽取10个作为测试集,剩下的40个
    作为训练集,然后使用基于词袋模型的词向量进行贝叶斯分类。
'''
def spamTest():
    docList=[]; classList = []; fullText =[]
    for i in range(1,26):
        wordList = textParse(open('email/spam/%d.txt' % i).read())
        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)#create vocabulary
    trainingSet = list(range(50)); #trainingSet表示docList中参与训练的短文本词向量的下标
    testSet=[]
    for i in range(10): #从训练集trainingSet中随机抽取10个下标添加到测试集,并将这10个下标从训练集中删除
        randIndex = int(random.uniform(0,len(trainingSet)))
        testSet.append(trainingSet[randIndex])
        del(trainingSet[randIndex])  
    trainMat=[]; trainClasses = []
    for docIndex in trainingSet:#将docList[]中参与训练的短文本转化为基于词袋模型的词向量
        trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
        trainClasses.append(classList[docIndex]) #将参与训练的短文本对应的标签添加到数组trainClasses中
    p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
    errorCount = 0
    for docIndex in testSet:        #对10个用于测试短文本进行贝叶斯分类
        wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
        if classifyNB0(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
            errorCount += 1
            print ("classification error",docList[docIndex])
    print ('the error rate is: ',float(errorCount)/len(testSet))
    #return vocabList,fullText

这里写图片描述

四、使用朴素贝叶斯分类器分析征婚广告信息

  征婚广告信息分别从来与两个城市(newyork、sfbay)的RSS源获得,本例子共有两个任务:使用贝叶斯分类器测试某条数据是来自newyork还是sfbay的、分别找出newyork和sfbay最具有代表性的频繁用词。
(1)获取RSS源数据 —- feedparser包的parse函数示例
  从浏览器中输入”https://newyork.craigslist.org/stp/index.rss“,页面如下面白色图片。黑色图片中的ny[‘entries’][0][‘summary’]表示获取0号item的description(下图红框部分)
这里写图片描述
这里写图片描述
(2)测试某条数据是来自newyork还是sfbay的
  首先使用函数calcMostFreq(vocabList,fullText)去除高频词,因为高频词是两个不同地区的共同点,难以作为某条信息来源的区分依据。然后使用函数localWords(feed1,feed0)进行区分。

'''
    calcMostFreq(vocabList,fullText):vocabList是词汇表,fullText是所有的文本。该函数返回出现频率最高的前30个单词。
'''
def calcMostFreq(vocabList,fullText):
    import operator
    freqDict = {}
    for token in vocabList:
        freqDict[token]=fullText.count(token)
    sortedFreq = sorted(freqDict.items(), key=operator.itemgetter(1), reverse=True)
    return sortedFreq[:30]

'''
    def localWords(feed1,feed0):原理和函数spamTest()一样,不同的是从两个不同的RSS源feed1,feed0获取文本信息。
    该函数将总体数据集文本fullText中,统计出前30个出现频率最高的单词,从词汇表vocabList中删除,之后的贝叶斯分类中这30个单词
    不给予考虑(因为,出现频率高,说明该单词的出现与否,不太能表明该文本是来自feed1还是feed0)。
    该函数从数据集当中随机抽取20个作为测试集,剩下的数据作为训练集。
'''
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) #NY is class 1
        wordList = textParse(feed0['entries'][i]['summary'])
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)
    vocabList = createVocabList(docList)#create vocabulary
    top30Words = calcMostFreq(vocabList,fullText)   #remove top 30 words
    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(20):
        randIndex = int(random.uniform(0,len(trainingSet)))
        testSet.append(trainingSet[randIndex])
        del(trainingSet[randIndex])  
    trainMat=[]; trainClasses = []
    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 classifyNB0(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
            errorCount += 1
    print ('the error rate is: ',float(errorCount)/len(testSet))
    return vocabList,p0V,p1V

这里写图片描述
(3)找出newyork、sfbay两个城市最具有表征性的词语
  经过测试,p0V向量代表的城市使用阈值-4.7(由于之前将词频率取对数了,因此词频率的对数为负数)最能代表sfbay,p1V向量代表的城市使用阈值-4.6最能代表newyork。
1.所用函数

'''
    def getTopWords(ny,sf):通过函数localWords(feed1,feed0)获取去除出现频率最高的前30个单词所对应的两个
    词频率向量p0V(对应的词来源于sf),p1V(对应的词来源于ny)。该函数将向量p0V,p1V中出现的对数频率>-6.0的单词输出,这
    些单词分别最能代表城市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] > -4.7 : topSF.append((vocabList[i],p0V[i]))
        if p1V[i] > -4.6 : topNY.append((vocabList[i],p1V[i]))
    sortedSF = sorted(topSF, key=lambda pair: pair[1], reverse=True)
    print ("最能代表sfbay的单词为:")
    listSF=[]
    for item in sortedSF:
        listSF.append(item[0])
    print (listSF)
    sortedNY = sorted(topNY, key=lambda pair: pair[1], reverse=True)
    print ("最能代表newyork的单词为:")
    listNY=[]
    for item in sortedNY:
        listNY.append(item[0])
    print(listNY)

2.结果
这里写图片描述
这里写图片描述

猜你喜欢

转载自blog.csdn.net/made_in_china_too/article/details/78939374