机器学习系列--Naive Bayes Classification

Posted 哇小明

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了机器学习系列--Naive Bayes Classification相关的知识,希望对你有一定的参考价值。

Native Bayes

贝叶斯决策理论的核心思想:选择最高概率的决策。朴素贝叶斯是贝叶斯决策理论的一部分。
下面不加证明地直接给出贝叶斯定理:
朴素贝叶斯分类的正式定义如下:

因为分母对于所有类别为常数,因为我们只要将分子最大化皆可。又因为各特征属性是条件独立的,所以有:

分类问题

现在实际的来研究一个文本分类的问题,下面是朴素贝叶斯分析问题的一般过程:

1.收集数据:可以使用任何方法。
2.准备数据:需要数值型或者布尔型的数据。
3.分析数据:有大量的特征时,绘制特征作用不大,此时使用直方图效果会更好。
4.训练算法:计算不同的独立特征的条件概率。
5.测试算法:计算错误率。
6.使用算法:一个常见的朴素贝叶斯应用是文档分类,可以在任意的分类场景中使用朴素贝叶斯分类器,不一定非要是文本。

为了简单起见,先研究一个社区的评论条目,构建一个快速过滤器,来分辨每条评论属于侮辱性评论还是非侮辱性评论,分别用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']]
    classVec = [0,1,0,1,0,1]    #1 is abusive, 0 not
    return postingList,classVec

上面这个load数据的函数,功能很简单就是创建两个数组,并返回它们,一个是词条列表即评论,共6个评论,一个是每个词条所对应的类别列表。这里所说的词条也就是一个句子,比如例子中的一个评论,后续还可能是一篇文章,一封邮件等,而词条列表则是由这些词条组成的数组。后面会这么用这个函数:

def createVocabList(dataSet):
    vocabSet = set([])  #create empty set
    for document in dataSet:
        vocabSet = vocabSet | set(document) #union of the two sets
    return list(vocabSet)

这个函数旨在创建包含上面词条列表所有词汇,但不会重复的数组。先创建一个空的set数据类型,然后遍历每个词条,用 ‘|’求并集,返回不重复的单词。看一下函数的效果:

返回的不重复的词汇列表,可能是没按照顺序的,不过这个无关紧要。

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

上面这个函数的作用就是将词汇数组,输出成程序可以更好识别的向量。向量的每一个元素为1或0,表示词汇表中的单词在输入文档(词条)中是否出现。实现原理是遍历文档每个词,再判断每个词是否出现在词汇表中,进而对输出向量进行赋值操作。看一下效果:

从词向量计算概率

将朴素贝叶斯的定理应用到这个社区评论过滤的案例中,前面已经提到用x,y表示的贝叶斯定理,现在用w 代表一个向量,该向量由多个数值组成,其实际意义就是社区每条评论的数值化结果即[0,1,0,0,1,1,1….]这种形式,用字母Ci代表评论的类别,在这个案例中只有两个类别:C1=1,侮辱性评论,C2=非侮辱性评论。那我们这个案例的研究目的就是分辨新输入的评论属于哪一类别,也就是给定评论内容(词条数组向量)的条件下,属于哪种类别的概率,用符号表示就是P(Ci | w)。有贝叶斯定理可以得到:

利用这个公式,每当输入一个文档,我们会计算这个文档属于每一个类别的概率,选择最大概率的那个类别作为评判结果,体现了贝叶斯决策理论的核心思想:选择最高概率的决策。
主要分析一下右边的式子:

分子:P(w | Ci)代表着在已知类别的情况下,对每个单词求其出现的概率,这个就跟前一篇介绍生成学习算法中大象和小狗的例子差不多了,在这里w回事由很多值组成的向量,即可写成P(w0,w1,w2,w3,…,wn|Ci),按照前面朴素贝叶斯的假设:w中每个特征条件独立,所以可以写成:P(w0 | Ci)P(w1 | Ci)P(w2 | Ci)P(w3 | Ci)…..P(wn | Ci) ,按照这样来计算上述概率。P(Ci)为ci类别的个数占总个数的概率,比如这个案例中p(C1)=p(c2)=3/6=0.5。

分母:是表示某个词向量的概率,在实际情况中,词向量是给定的也就是输入数据,所以分母通常为常数,所以可忽略不计,不影响整体概率结果。

下面这个函数是求P(w |ci)的过程:

def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix) #trainMatrix的长度,也就是几篇文档
    numWords = len(trainMatrix[0])  #trainMatrix中每篇文档的长度,即有多少个单词
    pAbusive = sum(trainCategory)/float(numTrainDocs) #trainCategory中类别1的个数除以文档总数,也就是类别1的比例
    p0Num = ones(numWords); p1Num = ones(numWords)   #创建全是0的数组,后面为了防止出现全0导致概率无效,换成了全1数组,change to ones() 
    p0Denom = 2.0; p1Denom = 2.0                        #分母基数初始化为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)     #这里原本是不加log运算,得出的结果就是概率,不过后面会有乘法计算,为了避免最后结果变成0,采用log运算 #change to log()
    p0Vect = log(p0Num/p0Denom)          #change to log()
    return p0Vect,p1Vect,pAbusive   #分别返回类别0中,各个单词的概率;类别1中各个单词的概率;类别为1的概率 

上面这些函数的过程,下面用手写草图详细介绍下其中数据的处理流程,便于理解:

上述函数的运行结果就是得到在两个类别下的每个单词的概率:

下面是加上log函数的结果:

p0v: [-2.56494936 -2.56494936 -2.56494936 -3.25809654 -2.56494936 -2.56494936
 -2.56494936 -3.25809654 -2.56494936 -3.25809654 -3.25809654 -2.56494936
 -2.56494936 -3.25809654 -2.56494936 -2.56494936 -2.56494936 -3.25809654
 -2.15948425 -3.25809654 -3.25809654 -3.25809654 -2.56494936 -1.87180218
 -3.25809654 -3.25809654 -2.56494936 -2.56494936 -2.56494936 -2.56494936
 -2.56494936 -2.56494936]
p1v: [-1.94591015 -3.04452244 -3.04452244 -2.35137526 -3.04452244 -3.04452244
 -3.04452244 -1.94591015 -3.04452244 -2.35137526 -2.35137526 -3.04452244
 -3.04452244 -1.65822808 -3.04452244 -3.04452244 -3.04452244 -2.35137526
 -2.35137526 -2.35137526 -2.35137526 -2.35137526 -3.04452244 -3.04452244
 -2.35137526 -2.35137526 -3.04452244 -2.35137526 -2.35137526 -3.04452244
 -3.04452244 -3.04452244]

经过数据训练之后我们得到两个类别下每个单词会出现的概率,以上数据仅为参考,我发现函数 createVocabList 每次出来的结果都不一样,是随机的,我用python是3.6的,所以这里就不一一对比数据的准确性,我再debug的时候对比是没错的。

关键部分理解完了,下面分析一下如何使用这个训练结果,我贴上全部的代码:

import numpy as np


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) #union of the two sets
    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

def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix)
    numWords = len(trainMatrix[0])
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    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(vec2Classify, p0Vec, p1Vec, pClass1):
    p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)  # element-wise mult
    p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
    if p1 > p0:
        return 1
    else:
        return 0


def bagOfWords2VecMN(vocabList, inputSet):
    returnVec = [0] * len(vocabList)
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] += 1
    return returnVec


if __name__ == '__main__':
    listOPosts, listClasses = loadDataSet()
    myVocabList = createVocabList(listOPosts)
    print(myVocabList)
    trainMat = []
    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    p0V, p1V, pAb = trainNB0(np.array(trainMat), np.array(listClasses))
    # print("p0v:",p0V)
    # print("p1v:",p1V)
    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, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))

主要业务逻辑看main函数,

p0V, p1V, pAb = trainNB0(np.array(trainMat), np.array(listClasses))

这一步过后就得到了每个单词的概率向量了。接下来开始分类,对于新的数据(语句),这边先得对照词汇表转化成向量。

testEntry = ['love', 'my', 'dalmation']
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))

向量化的结果(这个也要看词汇表排序的,仅做参考):

[0 0 1 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

下面进入分类:

def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)  # element-wise mult
    p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
    if p1 > p0:
        return 1
    else:
        return 0

输入的四个参数,第一个是新数据的向量化结果,第二第三分别是各类别下每个单词概率的向量,第四个为其中某一类别的概率。
函数主要实现了贝叶斯公式中 右边分子的部分,也就是 在给定类别下这个词向量的概率和这个类别的概率的相乘。

这里的sum(vec2Classify * p1Vec) 操作,实际上就是:
[0 0 1 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] * [-2.56494936 -2.56494936 -2.56494936 -3.25809654 -2.56494936 -2.56494936
-2.56494936 -3.25809654 -2.56494936 -3.25809654 -3.25809654 -2.56494936
-2.56494936 -3.25809654 -2.56494936 -2.56494936 -2.56494936 -3.25809654
-2.15948425 -3.25809654 -3.25809654 -3.25809654 -2.56494936 -1.87180218
-3.25809654 -3.25809654 -2.56494936 -2.56494936 -2.56494936 -2.56494936
-2.56494936 -2.56494936]
一一对应相乘最后相加,可以看到这个过程,0所对应的项相乘都变成0了,只有在词汇表中出现过的单词为1 余对应概率相乘才会有有效结果,最后相加是求这整个词向量的概率结果,这里想加并没有和前面的“w中每个特征条件独立,所以可以写成:P(w0 | Ci)P(w1 | Ci)P(w2 | Ci)P(w3 | Ci)…..P(wn | Ci) ,按照这样来计算上述概率。”矛盾,这边是log函数相加,log(P(w0 | Ci)P(w1 | Ci)P(w2 | Ci)P(w3 | Ci)…..P(wn | Ci))= log((P(w0 | Ci))+log(P(w1 | Ci))+……+log(P(wn | Ci))。最后加上类别概率的log结果,其实就是公式中的P(w|Ci)*P(Ci)的相乘结果的log,可以拆成两个log相加。

分类函数中,把把每个类别的概率都算出来,然后进行对比,谁比较大就属于那个类别。这就是简单的朴素贝叶斯分类算法,选择概率最大的那个。

以上是关于机器学习系列--Naive Bayes Classification的主要内容,如果未能解决你的问题,请参考以下文章

机器学习实战三(Naive Bayes)

python Naive Bayes模型的示例代码。参考:机器学习在行动第4章。

阿里云机器学习算法:朴素贝叶斯(Naive Bayes)

机器学习分类实例——SVM(修改)/Decision Tree/Naive Bayes

机器学习笔记之朴素贝叶斯(Naive Bayes)原理

机器学习---朴素贝叶斯分类器(Machine Learning Naive Bayes Classifier)