机器学习机器学习贝叶斯

机器学习实战-03-朴素贝叶斯

2018-08-24  本文已影响30人  nobodyyang

一、朴素贝叶斯介绍

  前两章我们要求分类器做出艰难决策,给出“该数据实例属于哪一类”这类问题的明确答案。不过,分类器有时会产生错误结果,这时可以要求分类器给出一个最优的类别猜测结果,同时给出这个猜测的概率估计值。
  朴素贝叶斯(Naive Bayesian)算法能够根据数据加先验概率来估计后验概率,在垃圾邮件分类、文本分类、信用等级评定等多分类问题中得到广泛应用。朴素贝叶斯算法以自变量之间的独立性和连续变量的正态性假设为前提,会导致算法精度在一定程度上受到影响。


二、朴素贝叶斯理论

1、贝叶斯决策理论
假设现在我们有一个数据集,它由两类数据组成,数据分布如下图所示:

  我们现在用 p1(x,y) 表示数据点(x,y)属于类别1(图中用圆点表示的类别)的概率,用 p2(x,y) 表示数据点(x,y)属于类别2(图中用三角形表示的类别)的概率,那么对于一个新数据点(x,y),可以用下面的规则来判断它的类别:
  如果 p1(x,y) > p2(x,y) ,那么类别为1。
  如果 p2(x,y) > p1(x,y) ,那么类别为2。
  也就是说,我们会选择高概率对应的类别。这就是贝叶斯决策理论的核心思想,即选择具有最高概率的决策。

2、条件概率
  条件概率(Condittional probability)是指在事件B发生的情况下,事件A发生的概率,用P(A|B)来表示。

条件概率计算公式

3、全概率公式
  假定样本空间S,是两个事件A与A'的和。红色部分是事件A,绿色部分是事件A',它们共同构成了样本空间S。

  在这种情况下,事件B可以划分成两个部分。

  得到了条件概率的另一种写法:

4、贝叶斯推断
  对条件概率公式进行变形,可以得到如下形式:

  我们把P(A)称为"先验概率"(Prior probability),即在B事件发生之前,我们对A事件概率的一个判断。
  P(A|B)称为"后验概率"(Posterior probability),即在B事件发生之后,我们对A事件概率的重新评估。
  P(B|A)/P(B)称为"可能性函数"(Likelyhood),这是一个调整因子,使得预估概率更接近真实概率。
  所以,条件概率可以理解成: 后验概率=先验概率x调整因子,这就是贝叶斯推断的含义。
  我们先预估一个"先验概率",然后加入实验结果,看这个实验到底是增强还是削弱了"先验概率",由此得到更接近事实的"后验概率"。如果"可能性函数"P(B|A)/P(B)>1,意味着"先验概率"被增强,事件A的发生的可能性变大;如果"可能性函数"=1,意味着B事件无助于判断事件A的可能性;如果"可能性函数"<1,意味着"先验概率"被削弱,事件A的可能性变小。
  举个例子,从下图中计算从A桶中取到黑色石头的概率。

  H1表示A桶,H2表示B桶。由于这两个桶是一样的,被选中的概率都一样,所以P(H1)=P(H2)=0.5,我们把这个概率就叫做"先验概率",即没有做实验之前,来自A桶的概率是0.5。假定,E表示黑色石头,所以问题变成了在已知E的情况下,来自A桶的概率有多大,即求P(H1|E)。我们把这个概率叫做"后验概率",即在E事件发生之后,对P(H1)的修正。

  这表明,来自A的概率是3/5。也就是说,取出黑色石头之后,H1事件的可能性得到了增强。

5、朴素贝叶斯推断
  贝叶斯和朴素贝叶斯的概念是不同的,区别就在于“朴素”二字,朴素贝叶斯对条件个概率分布做了条件独立性的假设。 比如下面的公式,假设有n个特征:

  由于每个特征都是独立的,我们可以进一步拆分公式 :

三、朴素贝叶斯完成文档分类

贴上第三、第四节的全部代码:
# -*- coding: UTF-8 -*-
from numpy import *
import re
import feedparser


def loadDataSet():
    """
        Function:
            创建实验样本
        Parameters:
            无
        Returns:
            postingList - 实验样本切分的原始词条列表,列表每一行代表一个文档
            classVec - 类别标签向量
        Modify:
            2018-08-11
        """
    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'],
                   ['my', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
                   ['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
    classVec = [0, 1, 0, 1, 0, 1]
    return postingList, classVec


# 统计所有文档中出现的词条列表,即没有重复的单词
def createVocabList(dataSet):
    """
        Function:
            创建一个包含在所有文档中出现的不重复词的列表
        Parameters:
            dataSet - 样本切分词条数据集
        Returns:
            vocabSet - 返回不重复的词条列表,也就是词汇表
        Modify:
            2018-08-11
        """
    vocabSet = set([])
    for document in dataSet:
        # 将文档列表转为集合的形式,保证每个词条的唯一性
        # 然后与vocabSet取并集,向vocabSet中添加没有出现新的词条
        vocabSet = vocabSet | set(document)
    return list(vocabSet)


def setOfWords2Vec(vocabList, inputSet):
    """
        Function:
            根据词条列表中的词条是否在文档中出现(出现1,未出现0),将文档转化为词条向量
        Parameters:
            vocabList - createVocabList返回的列表
            inputSet - 切分的词条列表
        Returns:
            returnVec - 文档向量,词集模型
        Modify:
            2018-08-11
        """
    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):
    """
        Function:
            朴素贝叶斯分类器的训练函数
        Parameters:
            trainMatrix - 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵
            trainCategory - 训练类别标签向量,即loadDataSet返回的classVec
        Returns:
            p0Vect - 非侮辱类的条件概率数组
            p1Vect - 侮辱类的条件概率数组
            pAbusive - 文档属于侮辱类的概率
        Modify:
            2018-08-11
        """
    numTrainDocs = len(trainMatrix)
    numWords = len(trainMatrix[0])
    pAbusive = sum(trainCategory) / float(numTrainDocs)
    p0Num = ones(numWords)
    p1Num = ones(numWords)
    # 初始化所有词出现数为1,并将分母初始化为2,避免某一个概率值为0
    p0Denom = 2.0
    p1Denom = 2.0
    for i in range(numTrainDocs):
        if trainCategory[i] == 1:
            # 统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)...
            p1Num += trainMatrix[i]
            p1Num += trainMatrix[i]
            p1Denom += sum(trainMatrix[i])
        else:
            # 统计属于非侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    # 将结果取自然对数,避免下溢出,即太多很小的数相乘造成的影响
    p1Vect = log(p1Num / p1Denom)
    p0Vect = log(p0Num / p0Denom)
    return p0Vect, p1Vect, pAbusive


# 朴素贝叶斯分类函数
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    """
        Function:
            朴素贝叶斯分类函数
        Parameters:
            vec2Classify - 待分类的词条数组
            p0Vec - 侮辱类的条件概率数组
            p1Vec -非侮辱类的条件概率数组
            pClass1 - 文档属于侮辱类的概率
        Returns:
            0 - 属于非侮辱类
            1 - 属于侮辱类
        Modify:
            2018-08-15
        """
    p1 = sum(vec2Classify * p1Vec) + log(pClass1)
    p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)
    if p1 > p0:
        return 1
    else:
        return 0


def testingNB():
    """
        Function:
            朴素贝叶斯分类测试函数
        Parameters:
            无
        Returns:
            无
        Modify:
            2018-08-15
        """
    listOPosts, listClasses = loadDataSet()
    myVocabList = createVocabList(listOPosts)
    trainMat = []
    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    p0V, p1V, pAb = trainNB0(trainMat, array(listClasses))
    # 测试文档
    testEntry = ['love', 'my', 'dalmation']
    # 将测试文档转为词条向量,并转为NumPy数组的形式
    thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
    # 利用贝叶斯分类函数对测试文档进行分类并打印
    print(testEntry, 'classified as:', classifyNB(thisDoc, p0V, p1V, pAb))
    # 第二个测试文档
    testEntry1 = ['stupid', 'garbage']
    # 同样转为词条向量,并转为NumPy数组的形式
    thisDoc1 = array(setOfWords2Vec(myVocabList, testEntry1))
    print(testEntry1, 'classified as:', classifyNB(thisDoc1, p0V, p1V, pAb))


def bagOfWords2VecMN(vocabList, inputSet):
    """
        Function:
            朴素贝叶斯词袋模型,根据词条列表中的词条是否在文档中出现(出现1,未出现0),将文档转化为词条向量
        Parameters:
            vocabList - createVocabList返回的列表
            inputSet - 切分的词条列表
        Returns:
            returnVec - 文档向量,词集模型
        Modify:
            2018-08-15
        """
    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 textParse(bigString):
    """
        Function:
            将大字符串其解析为字符串列表
        Parameters:
            bigString - 邮件字符串
        Returns:
            tok - 字符串列表
        Modify:
            2018-08-15
        """
    # 对长字符串进行分割,分隔符为除单词和数字之外的任意符号串
    listOfTokens = re.split(r'\W*', bigString)
    # 将分割后的字符串中所有的大些字母变成小写lower(), 并且只保留单词长度大于3的单词
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]


def spamTest():
    """
        Function:
            使用朴素贝叶斯过滤垃圾邮件测试函数
        Parameters:
            无
        Returns:
            无
        Modify:
            2018-08-15
        """
    docList = []
    classList = []
    fullText = []
    for i in range(1, 26):
        wordList = textParse(
            open('D:/PycharmProjects/Machine/machinelearninginaction/Ch04/email/spam/%d.txt' % i).read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(1)
        wordList = textParse(
            open('D:/PycharmProjects/Machine/machinelearninginaction/Ch04/email/ham/%d.txt' % i).read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)
    vocabList = createVocabList(docList)
    trainingSet = list(range(50))
    testSet = []
    # 选10组做测试集,根据随机产生索引值获取
    for i in range(10):
        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(array(trainMat), array(trainClasses))
    errorCount = 0
    # 测试并计算错误率
    for docIndex in testSet:
        wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
        if classifyNB(array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
            errorCount += 1
            print("分类错误的测试集:", docList[docIndex])
    print('the error rate is: ', float(errorCount) / len(testSet))


if __name__ == '__main__':
    # postingList, classVec = loadDataSet()
    # for each in postingList:
    #     print(each)
    # print(classVec)

    # myVocabList = createVocabList(postingList)
    # print('词汇表:', myVocabList)
    # trainMat = []
    # for postinDoc in postingList:
    #     trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    # print('词条向量:', trainMat)
    #
    # p0Vect, p1Vect, pAbusive = trainNB0(trainMat, classVec)
    # print('p0V:\n', p0Vect)
    # print('p1V:\n', p1Vect)
    # print('classVec:\n', classVec)
    # print('pAb:\n', pAbusive)

    # testingNB()

    spamTest()
    spamTest()

  以在线社区的留言板为例。为了不影响社区的发展,我们要屏蔽侮辱性的言论,所以要构建一个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么就将该留言标识为内容不当。过滤这类内容是一个很常见的需求。对此问题建立两个类别:侮辱类和非侮辱类,使用1和0分别表示。

(1)创建实验样本

创建实验样本

(2)构建词条向量

构建词条向量

(3)分类器训练

分类器训练结果

  利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概
率,即计算 p(w 0 |1)p(w 1 |1)p(w 2 |1) 。如果其中一个概率值为0,那么最后的乘积也为0。为降低这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。
  另一个遇到的问题是下溢出,这是由于太多很小的数相乘造成的。当计算乘积
p(w 0 |c i )p(w 1 |c i )p(w 2 |c i )...p(w N |c i ) 时,由于大部分因子都非常小,所以程序会下溢出或者得到不正确的答案。一种解决办法是对乘积取自然对数。在代数中有 ln(a*b) = ln(a)+ln(b) ,于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。

(4)朴素贝叶斯分类函数测试

朴素贝叶斯分类测试函数测试结果

(5)使用文档词袋模型完善朴素贝叶斯分类器

  前面我们将每个词的出现与否作为一个特征,这可以被描述为词集模型(set-of-words model)。如果一个词在文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为词袋模型(bag-of-words model)。在词袋中,每个单词可以出现多次,而在词集中,每个词只能出现一次。为适应词袋模型,需要对函数 setOfWords2Vec()稍加修改,修改后的函数称为bagOfWords2VecMN() 。

四、示例:使用朴素贝叶斯过滤垃圾邮件

使用朴素贝叶斯过滤垃圾邮件测试结果

  函数 spamTest() 会输出在10封随机选择的电子邮件上的分类错误率。既然这些电子邮件是随机选择的,所以每次的输出结果可能有些差别。如果发现错误的话,函数会输出错分文档的词表,这样就可以了解到底是哪篇文档发生了错误。如果想要更好地估计错误率,那么就应该将上述过程重复多次,比如说10次,然后求平均值。

五、应用scikit-learn实现新浪新闻分类

  1、中文语句切分
  英文的语句可以通过非字母和非数字进行切分,同样地,汉语句子也可以直接使用第三方分词组件jieba进行切分。可以在cmd命令行下用pip install jieba命令进行安装即可。新浪新闻数据集已经做好分类,分文件夹保存,分类结果如下:

数据集目录结构 新浪新闻数据集分类 切分结果

  红色框上边还有很多没显示出来。

  2、文本特征选择
  将所有文本分成训练集和测试集,并对训练集中的所有单词进行词频统计,并按降序排序。也就是将出现次数多的词语在前,出现次数少的词语在后进行排序。

  观察一下打印结果,不难发现,里包含了很多标点符号,很显然这些标点符号是不能作为新闻分类的特征的。为了降低这些高频的符号对分类结果的影响,应该要删除他们。
  除了这些,还有"在","了"等这样无关痛痒的词,还有一些数字,数字显然也不能作为分类新闻的特征。所以要消除它们对分类结果的影响,我们可以定制一个规则:首先去掉高频词,至于去掉多少个高频词,我们可以通过观察去掉高频词个数和最终检测准确率的关系来确定。除此之外,去除数字,不把数字作为分类特征。同时,去除一些特定的词语,比如:"的","一","在","不","当然","怎么"这类的对新闻分类无影响的介词、代词、连词。

  现在看到已经滤除了那些没有用的词组了。这个featureWords就是我们最终选出的用于新闻分类的特征。

  3、使用Sklearn构建朴素贝叶斯分类器
  相对于决策树,KNN之类的算法,朴素贝叶斯需要关注的参数是比较少的,是一类比较简单的算法。在scikit-learn中,一共有3个朴素贝叶斯的分类算法类,分别是GaussianNB,MultinomialNB和BernoulliNB。其中GaussianNB是先验为高斯分布的朴素贝叶斯,MultinomialNB是先验为多项式分布的朴素贝叶斯,BernoulliNB是先验为伯努利分布的朴素贝叶斯。
  对于新闻分类,属于多分类问题。可以使用MultinamialNB()完成我们的新闻分类问题。
  贴上代码:

import os
import jieba
import random
from sklearn.naive_bayes import MultinomialNB
import matplotlib.pyplot as plt


def textProcessing(folderPath, testSize=0.2):
    """
        Function:
            中文文本处理
        Parameters:
            folderPath - 文本存放的路径
            testSize - 测试集占比,默认占所有数据集的百分之20
        Returns:
            allWordsList - 按词频降序排序的训练集列表
            trainDataList - 训练集列表
            testDataList - 测试集列表
            trainClassList - 训练集标签列表
            testClassList - 测试集标签列表
        Modify:
            2018-08-22
        """
    folderList = os.listdir(folderPath)
    dataList = []
    classList = []
    # for folder in folderList[0:1]:
    for folder in folderList:
        newFolderList = os.path.join(folderPath, folder)
        files = os.listdir(newFolderList)
        # print(files)
        j = 1
        for file in files:
            if j > 100:
                break
            with open(os.path.join(newFolderList, file), 'r', encoding='utf-8') as f:
                raw = f.read()
            # 精简模式,返回一个可迭代的generator
            wordCut = jieba.cut(raw, cut_all=False)
            # generator转换为list
            wordList = list(wordCut)
            # 添加数据集数据
            dataList.append(wordList)
            # 添加数据集类别
            classList.append(folder)
            j += 1
    # zip()将对象中对应的元素打包成一个个元组
    dataClassList = list(zip(dataList, classList))
    # 将data_class_list乱序
    random.shuffle(dataClassList)
    # 训练集和测试集切分的索引值
    index = int(len(dataClassList) * testSize) + 1
    trainList = dataClassList[index:]
    testList = dataClassList[:index]
    # 与 zip 相反,*zipped 可理解为解压,返回二维矩阵式
    trainDataList, trainClassList = zip(*trainList)
    testDataList, testClassList = zip(*testList)

    allWordsDict = {}
    for wordList in trainDataList:
        for word in wordList:
            if word in allWordsDict.keys():
                allWordsDict[word] += 1
            else:
                allWordsDict[word] = 1

    # dict.items()函数以列表返回可遍历的(键, 值)元组数组
    # 根据键的值倒序排序
    allWordsTupleList = sorted(allWordsDict.items(), key=lambda f: f[1], reverse=True)
    allWordsList, allWordsNums = zip(*allWordsTupleList)
    allWordsList = list(allWordsList)
    return allWordsList, trainDataList, testDataList, trainClassList, testClassList


def makeWordsSet(wordsPath):
    """
        Function:
            读取文件里的内容,并去重
        Parameters:
            folderPath - 文件路径
        Returns:
            wordsSet - 读取的内容的set集合
        Modify:
            2018-08-22
        """
    wordsSet = set()
    with open(wordsPath, 'r', encoding='utf-8') as f:
        for line in f.readlines():
            word = line.strip()
            if len(word) > 0:
                wordsSet.add(word)
    return wordsSet


def wordsDict(allWordsList, deleteN, stopWordsSet=set()):
    """
        Function:
            文本特征选取
        Parameters:
            allWordsList - 按词频降序排序的训练集所有文本列表
            deleteN - 删除词频最高的deleteN个词
        Returns:
            featureWords - 特征集
        Modify:
            2018-08-22
        """
    featureWords = []
    n = 1
    for t in range(deleteN, len(allWordsList), 1):
        if n > 1000:
            break
        if not allWordsList[t].isdigit() and allWordsList[t] not in stopWordsSet and 1 < len(allWordsList[t]) < 5:
            featureWords.append(allWordsList[t])
        n += 1
    return featureWords


def matrixFeatures(trainDataList, testDataList, featureWords):
    """
        Function:
            根据feature_words将文本向量化
        Parameters:
            trainDataList - 训练集
            testDataList - 测试集
            featureWords - 特征集
        Returns:
            trainFeatureList - 训练集向量化列表
            testFeatureList - 测试集向量化列表
        Modify:
            2018-08-22
        """

    def matrixFeature(text, featureWords):
        textWords = set(text)
        # 出现在特征集中,则置1
        features = [1 if word in textWords else 0 for word in featureWords]
        return features

    trainFeatureList = [matrixFeature(text, featureWords) for text in trainDataList]
    testFeatureList = [matrixFeature(text, featureWords) for text in testDataList]
    return trainFeatureList, testFeatureList


def sinaNewsClassifier(trainFeatureList, testFeatureList, trainClassList, testClassList):
    classifier = MultinomialNB().fit(trainFeatureList, trainClassList)
    testAccuracy = classifier.score(testFeatureList, testClassList)
    return testAccuracy


if __name__ == '__main__':
    folderPath = './machinelearninginaction/Ch04/SogouC/Sample'
    # textProcessing(folderPath)

    allWordsList, trainDataList, testDataList, trainClassList, testClassList = textProcessing(folderPath)
    stopWordsFile = './machinelearninginaction/Ch04/SogouC/stopwords_cn.txt'
    stopWordsSet = makeWordsSet(stopWordsFile)
    # featureWords = wordsDict(allWordsList, 100, stopWordsSet)
    # print(featureWords)

    testAccuracyList = []
    deleteNs = range(0, 1000, 20)
    for deleteN in deleteNs:
        featureWords = wordsDict(allWordsList, deleteN, stopWordsSet)
        trainFeatureList, testFeatureList = matrixFeatures(trainDataList, testDataList, featureWords)
        testAccuracy = sinaNewsClassifier(trainFeatureList, testFeatureList, trainClassList, testClassList)
        testAccuracyList.append(testAccuracy)

    plt.figure()
    plt.plot(deleteNs, testAccuracyList)
    plt.title('Relationship of deleteNs and test_accuracy')
    plt.xlabel('deleteNs')
    plt.ylabel('test_accuracy')
    plt.show()
运行结果

  上面绘制出了deleteNs和testAccuracy的关系,这样可以大致确定去掉前多少的高频词汇了。每次运行程序,绘制的图形可能不一定相同。

六、小结

  朴素贝叶斯算法是建立在每一个特征值之间时独立的基础上的监督学习分类算法,而这也是称他为 “朴素”贝叶斯的缘由,在现实环境中,很难达到两个特征值之间绝对的相互独立。

上一篇 下一篇

猜你喜欢

热点阅读