机器学习之贝叶斯分类(python实现)

朴素贝叶斯(Naive Bayesian)是最为广泛使用的分类方法,它以概率论为基础,是基于贝叶斯定理和特征条件独立假设的分类方法。

原理

朴素贝叶斯(Naive Bayesian)是基于贝叶斯定理和特征条件独立假设原则的分类方法。通过给出的特征计算分类的概率,选取概率大的情况进行分类。也是基于概率论的一种机器学习分类方法。分类目标确定,属于监督学习。

通过概率来衡量事件发生的可能性。概率论和统计学恰好是两个相反的概念,统计学是抽取部分样本进行统计来估算总体的情况,而概率论是通过总体情况来估计单个事件或者部分事情的发生情况。因此,概率论需要已知的数据去预测未知的事件。 
例如,我们看到天气乌云密布,电闪雷鸣并阵阵狂风,在这样的天气特征(F)下,我们推断下雨的概率比不下雨的概率大,也就是p(下雨)>p(不下雨),所以认为待会儿会下雨。这个从经验上看对概率进行判断。 
而气象局通过多年长期积累的数据,经过计算,今天下雨的概率p(下雨)=85%,p(不下雨)=15%,同样的,p(下雨)>p(不下雨),因此今天的天气预报肯定预报下雨。这是通过一定的方法计算概率从而对下雨事件进行判断。

为什么叫朴素贝叶斯:简单,易于操作,基于特征独立性假设,也即各个特征彼此独立,互相不影响发生。

条件概率

某个事件已发生的情况下另外一个事件发生的概率。计算公式如下:P(A|B)=P(A∩B) / P(B)
简单理解:画维恩图,两个圆圈相交的部分就是A发生B也发生了,因为求的是B发生下A发生的概率。B相当于一个新的样本空间。AB/B即可。

概率相乘法则:P(A∩B)=P(A)P(B|A) or P(A∩B)=P(B)P(A|B)
独立事件的概率:P(A∩B)=P(A)P(B)

贝叶斯定理

如果有穷k个互斥事件,B1, B2,,,Bk 并且 P(B1)+P(B2)+⋅⋅⋅+P(Bk)=1和一个可以观测到的事件A,那么有:
image.png

分类原理

基于概率论,二分类问题如下:
如果p1 > p2, 分入类别1; 否则分入类别2。

其次,贝叶斯定理,有
p(ci|x,y) = p(x,y|ci) * p(ci) / p(x,y)
x, y 表示特征变量,如下例子中的单词。Ci表示类别。p(ci | x, y) 即表示在特征x, y出现的情况下,分入类别Ci的概率。结合如上:
p(ci | x, y) > p(cj | x, y), 分入类别i, 否则分入类别j。

贝叶斯定理最大的好处是可以用已知的三个概率去计算未知的概率,而如果仅仅是为了比较p(ci|x,y)和p(cj|x,y)的大小,只需要已知两个概率即可,分母相同,比较p(x,y|ci)p(ci)和p(x,y|cj)p(cj)即可。

特征条件独立性假设原则

朴素贝叶斯常用与对文档分类。根据文档中出现的词汇,判断文章属于什么类别。将词汇出现的特征条件用词向量W表示,由多个值组成,值的个数和训练集中的词汇表个数相同。
上面的贝叶斯公式可以表示为:
p(ci|ω)=p(ω|ci) * p(ci) / p(ω)
各个单词的出现不会相互影响,则p(ω|ci) = p(ω0|ci)*p(ω1|ci)*...* p(ωk|ci)

算法实现

import numpy as np
np.seterr(divide='ignore', invalid='ignore')  #消除向量中除以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表示侮辱性言论,0表示正常
    return postingList, classVec

根据文档词汇构建词向量:

def createVocabList(dataSet):
    vocabSet = set([])
    for document in dataSet:
        vocabSet = vocabSet | set(document)
    return list(vocabSet)

# 对输入的词汇表构建词向量
def setOfWords2Vec(vocabList, inputSet):
    returnVec = np.zeros(len(vocabList)) #生成零向量的array
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1 #有单词,该位置填充1
        else:
            print("the word: %s is not in my Vocabulary" % word)
            # pass
    return returnVec  #返回0,1的向量

if __name__ == '__main__':
    listPosts, listClasses = loadDataSet()
    myVocabList = createVocabList(listPosts)
    print(myVocabList)
 

输出结果如下:
['flea', 'ate', 'how', 'licks', 'quit', 'problems', 'dog', 'I', 'garbage', 'help', 'is', 'cute', 'steak', 'to', 'worthless', 'please', 'has', 'posting', 'buying', 'love', 'food', 'so', 'my', 'take', 'dalmation', 'stop', 'park', 'not', 'stupid', 'him', 'mr', 'maybe'], 表示不同类别言论去重后得到的词向量。
[ 1. 0. 0. 0. 0. 0. 1. 0. 0. 0. 1. 1. 1. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]: 表示词汇集1中的单词是否在词向量中出现。

如上,这个方法只记录了每个词是否出现,并没有记录出现次数,成为词集模型。如果记录词出现的次数,这样的词向量构建方法称为词袋模型,如下。本文只使用词集模型。

# 词袋模型
def bagofWords2VecMN(vocabList, inputSet):
    returnVec = [0] * len(vocabList)
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] += 1
    return vocabList #返回非负整数的词向量

运用词向量计算概率:

def trainNB0(trainMatrix, trainCategory):
    numTrainDocs = len(trainMatrix)  #文档数目
    numWord = len(trainMatrix[0])  #词汇表数目
    print(numTrainDocs, numWord)
    pAbusive = sum(trainCategory) / len(trainCategory) #p1, 出现侮辱性评论的概率 [0, 1, 0, 1, 0, 1]
    p0Num = np.zeros(numWord)
    p1Num = np.zeros(numWord)

    p0Demon = 0
    p1Demon = 0

    for i in range(numTrainDocs):
        if trainCategory[i] == 0:
            p0Num += trainMatrix[i] #向量相加
            p0Demon += sum(trainMatrix[i]) #向量中1累加其和
        else:
            p1Num += trainMatrix[i]
            p1Demon += sum(trainMatrix[i])
    p0Vec = p0Num / p0Demon
    p1Vec = p1Num / p1Demon

    return p0Vec, p1Vec, pAbusive

if __name__ == '__main__':
    listPosts, listClasses = loadDataSet()
    myVocabList = createVocabList(listPosts)
    trainMat = []
    trainMat = []
    for postinDoc in listPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    print(trainMat)
    p0Vec, p1Vec, pAbusive = trainNB0(trainMat, listClasses)
    print(p0Vec, p1Vec, pAbusive)

输出结果稍微有点多,慢慢来看:
trainMat:表示数据中六个给定的特征在词集模型中的出现情况。

array([ 0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  1.,  1.,  0.,
        0.,  0.,  1.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
        0.,  0.,  0.,  0.,  1.,  1.]), array([ 0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,
        0.,  1.,  0.,  1.,  1.,  1.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,
        0.,  0.,  1.,  0.,  0.,  0.]), array([ 1.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,
        1.,  0.,  0.,  1.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,
        1.,  1.,  0.,  0.,  0.,  1.]), array([ 0.,  1.,  1.,  1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
        0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,
        0.,  0.,  0.,  0.,  0.,  0.]), array([ 0.,  1.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  1.,  0.,  0.,  0.,
        0.,  0.,  0.,  1.,  0.,  0.,  1.,  0.,  1.,  0.,  0.,  0.,  0.,
        0.,  0.,  1.,  1.,  0.,  1.]), array([ 0.,  0.,  1.,  0.,  1.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,
        0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,
        0.,  0.,  0.,  0.,  0.,  0.])]

print(numTrainDocs, numWord): 6 32 (6个文档,一共32个词汇)
print(p0Vec, p1Vec, pAbusive):pAbusive是文档中是侮辱性言论的概率,为0.5。
p0Vec表示类别0(非侮辱言论)中的词在词向量中出现的概率:

[ 0.  0.04166667  0.04166667  0.04166667  0.04166667  0.
  0.08333333  0.04166667  0.          0.04166667  0.          0.04166667
  0.          0.04166667  0.          0.          0.04166667  0.04166667
  0.04166667  0.04166667  0.04166667  0.          0.          0.04166667
  0.04166667  0.04166667  0.          0.125       0.          0.04166667
  0.04166667  0.04166667] 

算法的改进:

  1. 部分概率为0,用于上面计算独立特征概率相乘是永远为0.因此,将所有词出现的次数初始化为1,某类词项初始化为2.
  2. 由于计算得到的概率太小,不断的相乘可能会导致结果溢出。因此对其取对数,单调性相同,不会影响最后对结果的比较。函数如下:
def trainNB1(trainMatrix, trainCategory):
    numTrainDocs = len(trainMatrix)  #文档数目
    numWord = len(trainMatrix[0])  #词汇表数目
    pAbusive = sum(trainCategory) / len(trainCategory) #p1, 出现侮辱性评论的概率
    p0Num = np.ones(numWord)  #修改为1
    p1Num = np.ones(numWord)

    p0Demon = 2 #修改为2
    p1Demon = 2

    for i in range(numTrainDocs):
        if trainCategory[i] == 0:
            p0Num += trainMatrix[i] #向量相加
            p0Demon += sum(trainMatrix[i]) #向量中1累加其和
        else:
            p1Num += trainMatrix[i]
            p1Demon += sum(trainMatrix[i])
    p0Vec = np.log(p0Num / p0Demon)  #求对数
    p1Vec = np.log(p1Num / p1Demon)

    return p0Vec, p1Vec, pAbusive

注意:这里得到p0Vec可能是没有规律的,但其对最后的概率比较没有影响。

运用分类器函数进行文档分类

def classifyNB(vec2Classify, p0Vc,  p1Vc, pClass1):
    p1 = sum(vec2Classify * p1Vc) * pClass1
    p0 = sum(vec2Classify * p0Vc) * (1-pClass1)
    # p1 = sum(vec2Classify * p1Vc) + np.log(pClass1)    #取对数,防止结果溢出
    # p0 = sum(vec2Classify * p0Vc) + np.log(1 - pClass1)
    if p1 > p0:
        return 1
    else:
        return 0

解释一下:vec2Classify是所需分类文档的词量。根据公式 p(ci|ω)=p(ω|ci)p(ci) / p(ω), 已知特征向量求分类的概率等于 p(ω|ci)p(ci)。忽略分母:

p(ci)好求,用样本集中,ci的数量/总样本数即可 
p(ω|ci)由于各个条件特征相互独立且地位相同,`p(ω|ci)=p(w0|ci)p(w1|ci)p(w2|ci)......p(wN|ci)`,可以分别求p(w0|ci),p(w1|ci),p(w2|ci),......,p(wN|ci),从而得到p(ω|ci)。  
而求p(ωk|ci)也就变成了求在分类类别为ci的文档词汇表集合中,单个词项ωk出现的概率。

测试分类函数

使用两个不同的样本来测试分类函数:


# 构造样本测试
def testingNB():
    listPosts, listClasses = loadDataSet()
    myVocabList = createVocabList(listPosts)
    trainMat = []
    for postinDoc in listPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    p0v, p1v, pAb = trainNB0(trainMat, listClasses)
    # print(p0v, p1v, pAb)
    testEntry = ['love']
    thisDoc = setOfWords2Vec(myVocabList, testEntry)
    print(testEntry, 'classified as', classifyNB(thisDoc, p0v, p1v, pAb))

    testEntry = ['stupid', 'garbage']
    thisDoc = (setOfWords2Vec(myVocabList, testEntry))
    print(testEntry, 'classified as:', classifyNB(thisDoc, p0v, p1v, pAb))

if __name__ == '__main__':
    testingNB()

观察结果,可以看到将两个文档正确的分类。
完整代码请查看:

github:naive_bayes

总结

  • 朴素贝叶斯分类
  • 条件概率
  • 贝叶斯定理
  • 特征条件独立性假设原则
  • 根据文档构建词向量
  • 词集模型和词袋模型
  • 概率为0,方便计算的改进和防止溢出的取对数改进

参考文章:
机器学习之朴素贝叶斯(NB)分类算法与Python实现

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容