朴素贝叶斯(normal bayes)及其应用(MLIA第四章)

这里写图片描述

1. 概念简介

贝叶斯(约1701-1761) Thomas Bayes,英国数学家。约1701年出生于伦敦,做过神甫。1742年成为英国皇家学会会员。1761年4月7日逝世。贝叶斯在数学方面主要研究概率论。他首先将归纳推理法用于概率论基础理论,并创立了贝叶斯统计理论,对于统计决策函数、统计推断、统计的估算等做出了贡献。

贝叶斯定理也称贝叶斯推理,早在18世纪,英国学者贝叶斯(1702~1763)曾提出计算条件概率的公式用来解决如下一类问题:假设 H[1],H[2],H[n] 互斥且构成一个完全事件,已知它们的概率 P(H[i]),i=1,2,,n ,现观察到某事件A与 H[1],H[2],H[n] 相伴随机出现,且已知条件概率 P(A|H[i]) ,求 P(H[i]|A)

通俗理解:有一系列事件,都可能导致A出现,现在A发生了,想要是哪一个事件导致了这个结果或者某一事件导致这个结果的可能性有多大。

贝叶斯公式(发表于1763年)为:

P(H[i]|A)=P(H[i])P(A|H[i])P(H[1])P(A|H[1])+P(H[2])P(A|H[2])++P(H[n])P(A|H[n])

对其进行重新表示:

P(Ai|B)=P(B|Ai)P(Ai)nk=1P(B|Ak)P(Ak)(1)

注:分母其实就是A的全概率,分子 P(B|Ai)P(Ai) 是全概率求和因子中的一个。

其中 Ai,,An 为完备事件组,即 ni=1Ai=Ω,AiAj=ϕ,P(Ai)>0

推导过程:

对于两个关联事件A和B,同时发生的概率为: P(AB)=P(AB)P(B)=P(BA)P(A) 。因此可以得到:

P(A|B)=P(B|A)P(A)P(B)

其中, P(B)=P(BA)P(A)+P(BA)P(A) 。因此可以得到:

P(A|B)=P(B|A)P(A)P(B|A)P(A)+P(B|A)P(A)(2)

公式1如果A只有两种情况,则为公式2。

2. 使用条件概率来分类

样本S由N个特征(属性)构成,它有一个分类标签,从只有两个属性以及只有两个类别(二分类)的简单情况开始,表示为
S0(x0,y0,C0or1)
S1(x1,y1,C0or1)
……

现在有一个新的样本(x,y),它属于 C0 还是 C1 ,或者问分别的概率是多少?

这就是求 p(ci|x,y) 了,应用贝叶斯公式:

p(ci|x,y)=p(ci)p(x,y|ci)p(x,y)
分母是什么:是x,y的联合概率,朴素贝叶斯假设x,y是独立的,也就是样本的属性之间没有关联, p(x,y)=p(x)p(y) ,这种假设比较理想(朴素),因此成为朴素贝叶斯。

重要:以判定分类为目标时,因为无论i取哪个类别,分母都是一样的,所以我们不一定要计算分母。分母的计算往往是很多小于1的浮点数相乘,就算计算出来也不精确。

又因为假设属性独立, p(x,y|ci)=p(x|ci)p(y|ci) p(ci) 本身很好算,就是该分类在总分类中的占比,而 p(x|ci) 是分类确定后x属性在样本中出现的次数除以 ci 中x,y出现的总次数(即x在ci中的频率)。

上述分析,很容易推广到任意多个属性和任意多个分类的情况。

3.样本训练

如上一节所讲,样本和分类标签已知,那我们要学习什么呢?
其实我们要学习到的是一系列数值或者一系列概率,就是上一节中的分子,它由

P(Ci)=count(Ci)total(C)(1)

P(x|Ci),P(y|Ci),P(z|Ci),...(2)

组成,计算方法上一节已经描述过了。

现在我们可以写一个python函数来实现它,它的输入是①样本矩阵②分类标签列表

from numpy import *
def trainNB0(trainMatrix: array, trainCategory: list):
    '''
    朴素贝叶斯训练
    :param trainMatrix: 训练集,每行是一个样本,每列是一个特征(属性),特征为数字
    :param trainCategory: 类别列表,每个元素是对应的训练集的样本的标签
    :return: {类别->[特征1的条件概率,特征2的条件概率……] }即{类别->概率}  
    '''

    # 每个类别和数量的映射
    cateAndCount = dict((cate, trainCategory.count(cate)) for cate in set(trainCategory))
    # print(cateAndCount)
    # 每个分类中各特征的条件概率,即p(x_i|c_i)
    cateAndFeatureProbList = {}
    # 类别概率即p(c_i)
    cateAndProb = {}
    # 统计每个分类中总特征数
    cateAndFeatureTotalCount = {}
    # 特征个数
    numOfFeatures = len(trainMatrix[0])
    # 初始化每个分类的特征概率分布和特征总数
    for cate in set(trainCategory):
        # 如果其中一个概率值为0,那么最后的乘积也为0。为降低
        # 这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。
        cateAndFeatureProbList[cate] = ones(numOfFeatures)
        cateAndFeatureTotalCount[cate] = 2.0 
        # 这就是P(C_i)
        cateAndProb[cate] = cateAndCount[cate] / len(trainCategory)

    for i in range(len(trainMatrix)):
        # 这个样本所属分类
        cate = trainCategory[i]
        # 矩阵相加
        cateAndFeatureProbList[cate] += trainMatrix[i]
        # 增加所有特征的计数
        cateAndFeatureTotalCount[cate] += sum(trainMatrix[i])

    for cate in set(trainCategory):
        # 单个特征值求和除以全部特征值求和,就是在这个类别中这个特征的条件概率
        cateAndFeatureProbList[cate] = cateAndFeatureProbList[cate] / cateAndFeatureTotalCount[cate]

    return cateAndFeatureProbList, cateAndProb

我们借助了numpy的array上的一些便捷函数,如训练集的行向量按分类相加,自然得到了这个分类下各个特征的值的和,这个结果仍是一个行向量,这个行向量除以全部特征值总和仍是一个向量,这时向量中的每个值都是特征的条件概率。

返回的第二个值是各分类的占比,这是一个字典。

4.需要找个例子来学一学

案例来自《机器学习实战》

构建一个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么就将该留言标识为内容不当。过滤这类内容是一个很常见的需求。对此问题建立两个类别:侮辱类和非侮辱类,使用1和0分别表示

原始样本

def loadDataSet() -> (array, list):
    '''
    生成用于测试的数据集和分类标签
    :return:
    '''
    postingList = array([['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: array) -> list:
    '''
    生成数据集中的词汇表
    :param dataSet:
    :return:
    '''
    vocabSet = set()
    for doc in dataSet:
        vocabSet = vocabSet | set(doc)
    return list(vocabSet)


def words2Vec(vocabList: list, wordsList: list) -> array:
    '''
    生成词汇向量表,词集模型
    :param vocabList: 全量词汇表,所有文档中的单词去重后的列表
    :param wordsList:文档,词汇集
    :return: 结果是一个向量,每个元素是1或0,意思是对应于词汇表该位置的单词是否出现在wordsSet中
    '''
    # 长度和词汇表一致
    returnVec = zeros(len(vocabList))
    for word in set(wordsList):
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1

    return returnVec

原始数据集->样本特征矩阵

if __name__ == '__main__':
    postingList, classVec = loadDataSet()
    # 词汇表
    vocabList = createVocabList(postingList)
    # printIndexAndItem("vocabulary", vocabList)
    # 样本集,每行是一个样本也就是一个词汇向量,每列是一个特征,本例中是一个单词
    trainMatrix = zeros((len(postingList), len(vocabList)))
    i = 0
    for doc in postingList:
        wordsVec = words2Vec(vocabList, doc)
        trainMatrix[i, :] = wordsVec[:]
        i += 1

    cateAndWordsProbList, cateAndProb = trainNB0(trainMatrix, classVec)

如果对中间值感兴趣,我们可以打断点或者打印值来观察

这里我们最后得到的两个值
1、是一个映射,key:分类,值是一个列表,列表中是所有单词在这个分类下的条件概率
2、是一个映射:key:分类,值:该分类的概率,因为这个例子中两个分类各占一半,所以肯定是5-5开了

5.分类器

学到这些东西之后,我们就是对新数据进行分类了,输入一个词汇向量,我们输出一个类别

def classifyNB(vect2classify: array, cateAndWordsProbList: dict, cateAndProb: dict):
    '''
    vect2classify属于哪个类别?
    :param vect2classify: 要进行分类的向量
    :param cateAndWordsProbList:  学习到的特征条件概率
    :param cateAndProb: 学习到的分类概率
    :return: 分类
    '''
    # 分类 -> 概率的对数
    log_probs = {}
    for cate, prob in cateAndProb.items():
        # log(连乘p(w_i|c))=Σln(p(w_i|c))
        # 词汇向量表*条件概率=待测doc的条件概率,本来应该连乘,但可能溢出,所以先求log再求和
        log_list = array(list(map(lambda x: log(x), cateAndWordsProbList[cate])))
        sumLogPwiOfC = sum(vect2classify * log_list)
        logProbOfC = log(prob)
        log_probs[cate] = sumLogPwiOfC + logProbOfC  # 这就是对数条件概率的分子了
    #     由于分母是固定的,我们只需要概率最大的那个,而不是真正要算概率

    sortedProbs = sorted(log_probs.items(), key=operator.itemgetter(1), reverse=True)
    return sortedProbs[0][0]

这里要十分注意,对每个类别我们有各特征的条件概率,各概率*待分类向量得到待分类的新数据在此类在各个特征上的条件概率,原则上应该把所有这些概率连乘起来再乘以 P(Ci) ,根据贝叶斯和独立性假设,分子就应该这么算,但是由于大部分因子 P(featurei|Ci) 都非常小,所以程序会下溢出或者得到不正确的答案。(读者可以用Python尝试相乘许多很小的数,最后四舍五入后会得到0。)

解决办法是各因子求自然对数再相加,其实等于连乘后求自然对数。 ln(ab)=ln(a)+ln(b) ,所以在代码中你看到的是“新待分类向量”*“条件概率的自然对数”这个行向量的各分量求和再加 ln(P(Ci))

整个结果是公式中分子求对数,也称对数概率。

因为有多个分类,我们要把各P(分类|新向量)的分子都算出来,再比较个大小,最大的那个就是我们预测的分类。

小测分类器

我们在main入口中继续写下如下代码来看看我们随便写的一些话被判定为具有侮辱性还是不具有

    testEntry = ['dog', 'my']
    # 一定要转成词汇向量
    thisDoc = words2Vec(vocabList, testEntry)
    print("result", classifyNB(thisDoc, cateAndWordsProbList, cateAndProb))

结果是0,容易看出我们啥也没说嘛。
接入我们把testEntry改为['you', 'are','quit','stupid']结果就是1,很显然,因为在所有分类1中几乎都出现了stupid这个词,我们仿写当然要用这个词了。

6.应用于垃圾邮件分类

识别的过程

1.把每个邮件当做一个doc,对英文邮件用非字母和数字的字符(\W)分开为单词称为单词列表

def text2wordList(bigString) -> list:
    listOfTokens = re.compile(r'\W*').split(bigString)
    return [word.lower() for word in listOfTokens if len(word) > 2]

2.用N个doc生成一个全局词汇表

3.随机选择一部分作为训练集,剩余部分作为测试集,用训练集得到词汇表在某分类上的条件概率;用分类器对测试集中的doc进行测试并计算错误率

4.重复步骤3十次统计平均错误率

bayes.py完整代码:

from numpy import *
import operator


def loadDataSet() -> (array, list):
    '''
    生成用于测试的数据集和分类标签
    :return:
    '''
    postingList = array([['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: array) -> list:
    '''
    生成数据集中的词汇表
    :param dataSet:
    :return:
    '''
    vocabSet = set()
    for doc in dataSet:
        vocabSet = vocabSet | set(doc)
    return list(vocabSet)


def words2Vec(vocabList: list, wordsList: list) -> array:
    '''
    生成词汇向量表,词集模型
    :param vocabList: 全量词汇表,所有文档中的单词去重后的列表
    :param wordsList:文档,词汇集
    :return: 结果是一个向量,每个元素是1或0,意思是对应于词汇表该位置的单词是否出现在wordsSet中
    '''
    # 长度和词汇表一致
    returnVec = zeros(len(vocabList))
    for word in set(wordsList):
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1

    return returnVec


def bagOfWords2Vec(vocabList: list, wordsList: list) -> array:
    '''
    生成词汇向量表,词集模型
    :param vocabList: 全量词汇表,所有文档中的单词去重后的列表
    :param wordsList:文档,词汇集
    :return: 结果是一个向量,每个元素是1或0,意思是对应于词汇表该位置的单词是否出现在wordsSet中
    '''
    # 长度和词汇表一致
    returnVec = zeros(len(vocabList))
    for word in wordsList:
        if word in vocabList:
            returnVec[vocabList.index(word)] += 1

    return returnVec


def trainNB0(trainMatrix: array, trainCategory: list):
    '''
    朴素贝叶斯训练
    :param trainMatrix: 向量表,每行是一个样本的向量,每列是一个特征,特征为数字
    :param trainCategory: 类别列表
    :return: {类别->[特征1的条件概率,特征2的条件概率……] } ,{类别->概率}
    '''

    # 每个类别和数量的映射
    cateAndCount = dict((cate, trainCategory.count(cate)) for cate in set(trainCategory))
    # print(cateAndCount)
    # 每个分类中各特征的条件概率
    cateAndFeatureProbList = {}
    # 类别概率
    cateAndProb = {}
    # 统计每个分类中总特征数
    cateAndFeatureTotalCount = {}
    # 特征个数
    numOfFeatures = len(trainMatrix[0])
    # 初始化每个分类的特征概率分布和特征总数
    for cate in set(trainCategory):
        cateAndFeatureProbList[cate] = ones(numOfFeatures)
        cateAndFeatureTotalCount[cate] = 2.0
        # 这就是P(C_i)
        cateAndProb[cate] = cateAndCount[cate] / len(trainCategory)

    for i in range(len(trainMatrix)):
        # 这个文档所属分类
        cate = trainCategory[i]
        # 矩阵相加
        cateAndFeatureProbList[cate] += trainMatrix[i]
        # 增加所有特征的计数
        cateAndFeatureTotalCount[cate] += sum(trainMatrix[i])

    for cate in set(trainCategory):
        # 单个特征值求和除以全部特征值求和,就是在这个类别中这个特征的条件概率
        cateAndFeatureProbList[cate] = cateAndFeatureProbList[cate] / cateAndFeatureTotalCount[cate]

    return cateAndFeatureProbList, cateAndProb


def trainNB1(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)  # change to log()
    p0Vect = log(p0Num / p0Denom)  # change to log()
    printIndexAndItem("p0Vect", p0Vect)
    return p0Vect, p1Vect, pAbusive


def printIndexAndItem(msg, lst: list):
    list_ = [(w, lst[w]) for w in range(len(lst))]
    print(msg, list_)


def classifyNB(vect2classify: array, cateAndWordsProbList: dict, cateAndProb: dict):
    '''
    vect2classify属于哪个类别?
    :param vect2classify: 要进行分类的向量
    :param cateAndWordsProbList:  学习到的特征条件概率
    :param cateAndProb: 学习到的分类概率
    :return: 分类
    '''
    # 分类 -> 概率的对数
    log_probs = {}
    for cate, prob in cateAndProb.items():
        # log(连乘p(w_i|c))=Σln(p(w_i|c))
        # 词汇向量表*条件概率=待测doc的条件概率,本来应该连乘,但可能溢出,所以先求log再求和
        log_list = array(list(map(lambda x: log(x), cateAndWordsProbList[cate])))
        sumLogPwiOfC = sum(vect2classify * log_list)
        logProbOfC = log(prob)
        log_probs[cate] = sumLogPwiOfC + logProbOfC  # 这就是对数条件概率的分子了
    #     由于分母是固定的,我们只需要概率最大的那个,而不是真正要算概率

    sortedProbs = sorted(log_probs.items(), key=operator.itemgetter(1), reverse=True)
    return sortedProbs[0][0]


if __name__ == '__main__':
    postingList, classVec = loadDataSet()
    # 词汇表
    vocabList = createVocabList(postingList)
    # printIndexAndItem("vocabulary", vocabList)
    # 样本集,每行是一个样本也就是一个词汇向量,每列是一个特征,本例中是一个单词
    trainMatrix = zeros((len(postingList), len(vocabList)))
    i = 0
    for doc in postingList:
        wordsVec = words2Vec(vocabList, doc)
        trainMatrix[i, :] = wordsVec[:]
        i += 1

    cateAndWordsProbList, cateAndProb = trainNB0(trainMatrix, classVec)
    # trainNB1(trainMatrix, classVec)
    testEntry = ['dog', 'my']
    testEntry = ['you', 'are','quit','stupid']
    # 一定要转成词汇向量
    thisDoc = words2Vec(vocabList, testEntry)
    print("result", classifyNB(thisDoc, cateAndWordsProbList, cateAndProb))

bayes_email.py完整代码:

# coding=utf-8
from numpy import *
import operator
import re
from os import listdir
import random
from mlInAction import bayes


def text2wordList(bigString) -> list:
    listOfTokens = re.compile(r'\W*').split(bigString)
    return [word.lower() for word in listOfTokens if len(word) > 2]


def spamTest(dir1: str, dir2: str, time: int):
    '''
    测试分类器效果
    :param dir1: 正常邮件所在目录
    :param dir2: 垃圾邮件所在目录
    :return:
    '''
    docList = []
    classList = []
    fileList = listdir(dir1)
    # ------构造doclist begin--------
    # 对于正常邮件
    for fileName in fileList:
        bigStr = open(dir1 + "/" + fileName).read()
        # print(fileName)
        wordList = text2wordList(bigStr)
        docList.append(wordList)
        classList.append(0)
    # 对于垃圾邮件
    fileList = listdir(dir2)
    for fileName in fileList:
        bigStr = open(dir2 + "/" + fileName).read()
        # print(fileName)
        wordList = text2wordList(bigStr)
        docList.append(wordList)
        classList.append(1)
    # ------构造doclist end--------

    vocabList = bayes.createVocabList(docList)

    errorRateSum = 0.0
    for count in range(time):
        _docList = []
        _classList = []
        _docList[:] = docList[:]
        _classList[:] = classList[:]
        # 选择一部分出来作为训练集,10个样本作为测试,其余作为训练集
        trainSet = []
        testSet = []
        testClassList = []
        for i in range(10):
            # 随机整数 0-全集size
            randIndex = int(random.uniform(0, len(_docList)))
            testSet.append(_docList[randIndex])
            testClassList.append(_classList[randIndex])
            del _docList[randIndex]
            del _classList[randIndex]

        # 用训练集构建向量矩阵
        trainMat = []
        for doc in _docList:
            trainMat.append(bayes.bagOfWords2Vec(vocabList, doc))

        # 训练出单词的条件概率和类别的概率分布
        cateAndWordsProbList, cateAndProb = bayes.trainNB0(trainMat, _classList)

        index = 0
        rightCount = 0
        errorCount = 0
        testTotalNum = len(testSet)
        for doc in testSet:
            wordVector = bayes.bagOfWords2Vec(vocabList, doc)
            claz = bayes.classifyNB(wordVector, cateAndWordsProbList, cateAndProb)
            # 预测正确
            if claz == testClassList[index]:
                rightCount += 1
            else:
                errorCount += 1
            index += 1

        rate = errorCount / testTotalNum
        print("交叉验证第{}次,错误率{}".format(count, rate))
        errorRateSum += rate
    return errorRateSum / time


if __name__ == '__main__':
    errorRate = spamTest("email/ham", "email/spam", 10)
    print('total error rate is {}'.format(errorRate))

email的数据:

github

猜你喜欢

转载自blog.csdn.net/zhengwei223/article/details/79101184