zoukankan      html  css  js  c++  java
  • 机器学习(一):朴素贝叶斯

    参考:http://www.52nlp.cn/%e7%90%86%e8%ae%ba-%e6%9c%b4%e7%b4%a0%e8%b4%9d%e5%8f%b6%e6%96%af%e6%a8%a1%e5%9e%8b%e7%ae%97%e6%b3%95%e7%a0%94%e7%a9%b6%e4%b8%8e%e5%ae%9e%e4%be%8b%e5%88%86%e6%9e%90#more-10451

    一、理论

    朴素贝叶斯法是基于贝叶斯定理和特征条件独立假设的分类方法,‘朴素’之名来源于特征条件独立的假设,这是一个很强,很简单的假设,因为它意味着不同特征之间不会相互影响,这大大简化了计算。

    首先,从给定的数据集出发,(这些数据集包括用多个特征描述的输入x,以及x对应的类别标记y,X是定义在输入空间上的随机变量,而Y是定义在输出空间上的随机变量,P(X,Y)是X,Y的联合概率分布)求出P(X,Y);然后根据贝叶斯定理,对给定的输入x,求出后验概率最大的输出y。

    如上式,左边是我们要求的,即当给一个要预测的输入x=(x1,x2...,xn)时,要求x的类别y的概率,我们希望有这样一个y值,使得左边的概率最大;右边是可以根据数据集估计出来的概率,且分母是与类别无关的常数,不管y是多少,分母都不变,它只与初始数据集有关,所以当我们只想判断不同y取值下的大小时,可以省去。

    现在将问题简单了一些,只求argmax[y],右边可以根据数据集统计结果得出,即我们选择不同类别的输入x,计算它们的类别概率给定类别下的特征组合的条件概率的乘积,然后比较大小即可,最大的就是要预测的类别值。

    那么朴素贝叶斯是在学什么呢,可以知道,我们不需要去精确的计算P(X,Y)的参数,(朴素贝叶斯法是一种生成方法,同时考虑X和Y的随机性,就算想计算也不能直接计算P(X,Y),必须要通过下面两种方法估计后,再相乘得到联合概率分布)朴素贝叶斯的学习只要通过足够多的数据集估计出即可。

    极大似然估计方法如下,就是比较简单的计数法:

    还有一种是贝叶斯估计,考虑到有时候可能因为数据集很少,某个特征出现分子为0的情况,然后导致后验概率也为0,造成全盘皆输的局面,简单来说就是为了防止出现0的情况使用了平滑:

    二、项目:识别留言板侮辱性评论

    参考:http://www.52nlp.cn/%e5%ae%9e%e7%8e%b0-%e6%9c%b4%e7%b4%a0%e8%b4%9d%e5%8f%b6%e6%96%af%e6%a8%a1%e5%9e%8b%e7%ae%97%e6%b3%95%e7%a0%94%e7%a9%b6%e4%b8%8e%e5%ae%9e%e4%be%8b%e5%88%86%e6%9e%90

    1. 收集数据: 可以是文本数据、数据库数据、网络爬取的数据、自定义数据等等
    2. 数据预处理: 对采集数据进行格式化处理,文本数据的格式一致化,网络数据的分析抽取等,包括中文分词、停用词处理、词袋模型、构建词向量等。
    3. 分析数据: 检查词条确保解析的正确性,根据特征进行模型选择、特征抽取等。
    4. 训练算法: 从词向量计算概率
    5. 测试算法: 根据现实情况修改分类器
    6. 使用算法: 对社区留言板言论进行分类
     1 #加载数据集,这里的postingList刚好模拟了评论加载进来然后分词后的结果,其中每一个子列表都代表着一条评论信息,这里一共有6条评论,作为我们的训练集;然后返回文档列表和文档类别列表
     2 def loadDataSet():
     3     postingList = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
     4                    ['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
     5                    ['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
     6                    ['stop', 'posting', 'stupid', 'worthless', 'garbage'],
     7                    ['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
     8                    ['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
     9     classVec = [0, 1, 0, 1, 0, 1]  # 1代表侮辱性文字,0代表非侮辱文字
    10     return postingList, classVec
    11 
    12 #提取出所有单词(不重复)
    13 def createVocabList(dataSet):
    14     vocabSet = set([])
    15     for document in dataSet:
    16         vocabSet = vocabSet | set(document)  # 操作符 | 用于求两个集合的并集
    17     return list(vocabSet)
    18 
    19 #查找输入数据,即我们要判断类别的句子的单词是否在词汇表中(上一个函数提取出来的是词汇表)
    20 def setOfWords2Vec(vocabList, inputSet):
    21     # 创建一个和词汇表等长的向量,并将其元素都设置为0
    22     returnVec = [0] * len(vocabList)
    23     # 遍历文档中的所有单词,如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1
    24     for word in inputSet:
    25         if word in vocabList:
    26             returnVec[vocabList.index(word)] = 1
    27         else:
    28             print("单词: %s 不在词汇表之中!" % word)
    29     print(returnVec)
    30     return returnVec

    上述代码是默认已经抓取好评论(我们的训练集),并且做好了分词。然后做一个词汇表,和一个全为0的且长度与词汇表相同的列表,对于新的句子,进行分词后看一下这些词在不在词汇表里,在的话,就把0列表中对应单词位置的0改为1,这样的缺点就是如果词汇表很大,那新句子得到的列表表示,和词汇表一样大。

    运行一下看看:

    1 postingList,classVec=loadDataSet()
    2 voc=createVocabList(postingList)
    3 print(postingList[0])
    4 print(voc)
    5 setOfWords2Vec(voc, postingList[0])

    第一个列表是我们将要进行转化的列表(将单词转化成0 1表示),这里直接取的是原来做词汇表其中的一个句子。第二个列表就是词汇表。第三个列表是转化得到的词向量,它和词袋模型不太一样的地方是其中的1表示的是“出现过”,而不是“出现的次数”。

    现在已经知道了一个词是否出现在一篇文档中,也知道该文档所属的类别。接下来我们重写贝叶斯准则,将之前的 x, y 替换为 w. 粗体的 w 表示这是一个向量,即它由多个值组成。在这个例子中,数值个数与词汇表中的词个数相同。

    其中,C表示类别,W表示特征,在这个例子里就是【某词是否出现过】,体现在词向量上面就是【该单词的位置是否为1】。

    首先可以通过类别 i (侮辱性留言或者非侮辱性留言)中的文档数除以总的文档数来计算概率 ,也就是【数据集中类别的概率】。接下来计算 ,这里就要用到朴素贝叶斯假设。如果将 w 展开为一个个独立特征,那么就可以将上述概率写作。这里假设所有词都互相独立,该假设也称作条件独立性假设,它意味着可以使用来计算上述概率,那么具体的要怎么计算条件特征概率呢?我们先看句子(词向量)的类别,再把这类句子的词向量进行位置的加和,再除以这个类别的下的句子的所有单词数。就可以得到在每个类别下,每个单词出现的概率是多少。具体代码实现如下:

    def _trainNB0(trainMatrix, trainCategory):
        numTrainDocs = len(trainMatrix) # 文件数
        numWords = len(trainMatrix[0]) # 第一句话词向量的长度,也就是词汇表的长度
        # 侮辱性文件的出现概率,即trainCategory中所有的1的个数,
        # 代表的就是多少个侮辱性文件,与文件的总数相除就得到了侮辱性文件的出现概率
        pAbusive = sum(trainCategory) / float(numTrainDocs)
    
        # 构造单词出现次数列表
        p0Num = np.zeros(numWords) # [0,0,0,.....]
        p1Num = np.zeros(numWords) # [0,0,0,.....]
        p0Denom = 0.0;p1Denom = 0.0 # 整个数据集单词出现总数
        for i in range(numTrainDocs):
            # 遍历所有的文件,如果是侮辱性文件,就计算此侮辱性文件中出现的侮辱性单词的个数
            if trainCategory[i] == 1:
                p1Num += trainMatrix[i] #[0,1,1,....]->[0,1,1,...]
                p1Denom += sum(trainMatrix[i])
            else:
                # 如果不是侮辱性文件,则计算非侮辱性文件中出现的侮辱性单词的个数
                p0Num += trainMatrix[i]
                p0Denom += sum(trainMatrix[i])
        # 类别1,即侮辱性文档的[P(F1|C1),P(F2|C1),P(F3|C1),P(F4|C1),P(F5|C1)....]列表
        # 即 在1类别下,每个单词出现次数的占比
        p1Vect = p1Num / p1Denom# [1,2,3,5]/90->[1/90,...]
        # 类别0,即正常文档的[P(F1|C0),P(F2|C0),P(F3|C0),P(F4|C0),P(F5|C0)....]列表
        # 即 在0类别下,每个单词出现次数的占比
        p0Vect = p0Num / p0Denom
        return p0Vect, p1Vect, pAbusive
    postingList,classVec=loadDataSet()
    voc=createVocabList(postingList)
    print(voc)
    pos=[]
    for i in range(0,6):
        pos.append(setOfWords2Vec(voc, postingList[i]))
    print(pos)
    p0V,p1V,pAb=_trainNB0(pos,classVec)
    print('类别1的每个单词出现次数占比:',p0V)
    print('类别0的每个单词出现次数占比:',p1V)
    print('侮辱评论出现次数占比:',pAb)

    运行结果为:

    voc是词汇表;pos是将词汇表转化为词向量,它是包含五个子列表的列表,其中每一个子列表都是词向量表示的句子的分词结果;可以看到,词汇表的长度有32,即这五个句子的构成使用到了32个句子,词汇表不会有重复单词。侮辱评论次数占比是一个具体的数值,因为我们只要数一下句子中有几个侮辱性评论就可以了,因为我们的句子类别是[0, 1, 0, 1, 0, 1],显然为0.5。类别0/1的每个单词数显次数占比可以看出也是一个32维的向量,其中有一些位置的元素为0,表示在这个类别中,没有出现过这个单词。

    在利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,即计算 。如果其中一个概率值为 0,那么最后的乘积也为 0。为降低这种影响,可以将所有词的出现数初始化为 1,并将分母初始化为 2 (取1 或 2 的目的主要是为了保证分子和分母不为0,大家可以根据业务需求进行更改)。

    另一个遇到的问题是下溢出,这是由于太多很小的数相乘造成的。当计算乘积  时,由于大部分因子都非常小,所以程序会下溢出或者得到不正确的答案。(用 Python 尝试相乘许多很小的数,最后四舍五入后会得到 0)。一种解决办法是对乘积取自然对数。在代数中有 ln(a * b) = ln(a) + ln(b), 于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。

    函数 f(x) 与 ln(f(x)) 的曲线。可以看出,它们在相同区域内同时增加或者减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。

    根据朴素贝叶斯公式,我们观察分子进行条件概率连乘时候,由于有条件概率极小或者为0,最后导致结果为0 ,显然不符合我们预期结果,因此对训练模型进行优化,其优化代码如下:

    def trainNB0(trainMatrix, trainCategory):
        numTrainDocs = len(trainMatrix) # 总文件数
        numWords = len(trainMatrix[0]) # 总单词数
        pAbusive = sum(trainCategory) / float(numTrainDocs) # 侮辱性文件的出现概率
        # 构造单词出现次数列表,p0Num 正常的统计,p1Num 侮辱的统计
        # 避免单词列表中的任何一个单词为0,而导致最后的乘积为0,所以将每个单词的出现次数初始化为 1
        p0Num = np.ones(numWords)#[0,0......]->[1,1,1,1,1.....],ones初始化1的矩阵
        p1Num = np.ones(numWords)
    
        # 整个数据集单词出现总数,2.0根据样本实际调查结果调整分母的值(2主要是避免分母为0,当然值可以调整)
        # p0Denom 正常的统计
        # p1Denom 侮辱的统计
        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])
        # 类别1,即侮辱性文档的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表,取对数避免下溢出或浮点舍入出错
        p1Vect = np.log(p1Num / p1Denom)
        # 类别0,即正常文档的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表
        p0Vect = np.log(p0Num / p0Denom)
        return p0Vect, p1Vect, pAbusive
    postingList,classVec=loadDataSet()
    voc=createVocabList(postingList)
    print(voc)
    pos=[]
    for i in range(0,6):
        pos.append(setOfWords2Vec(voc, postingList[i]))
    print(pos)
    p0V,p1V,pAb=trainNB0(pos,classVec)
    print('类别1的每个单词出现次数占比:',p0V)
    print('类别0的每个单词出现次数占比:',p1V)
    print('侮辱评论出现次数占比:',pAb)

    为什么会是负数,主要是因为log(小于1的概率)的结果都是负,概率越小就代表这个负数越小而已。

    概率都算出来了之后,那么就可以开始相乘,比较了:

    def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
        # 计算公式  log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C))
        # 使用 NumPy 数组来计算两个向量相乘的结果,这里的相乘是指对应元素相乘,即先将两个向量中的第一个元素相乘,然后将第2个元素相乘,以此类推。这里的 vec2Classify * p1Vec 的意思就是将每个词与其对应的概率相关联起来
        p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
        p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
        if p1 > p0:
            return 1
        else:
            return 0

    全代码如下:

    import numpy as np
    #加载数据集,这里的postingList刚好模拟了评论加载进来然后分词后的结果,其中每一个子列表都代表着一条评论信息,这里一共有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]  # 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):
        # 创建一个和词汇表等长的向量,并将其元素都设置为0
        returnVec = [0] * len(vocabList)
        # 遍历文档中的所有单词,如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1
        for word in inputSet:
            if word in vocabList:
                returnVec[vocabList.index(word)] = 1
            else:
                print("单词: %s 不在词汇表之中!" % word)
        return returnVec
    
    
    
    def trainNB0(trainMatrix, trainCategory):
        numTrainDocs = len(trainMatrix) # 总文件数
        numWords = len(trainMatrix[0]) # 总单词数
        pAbusive = sum(trainCategory) / float(numTrainDocs) # 侮辱性文件的出现概率
        # 构造单词出现次数列表,p0Num 正常的统计,p1Num 侮辱的统计
        # 避免单词列表中的任何一个单词为0,而导致最后的乘积为0,所以将每个单词的出现次数初始化为 1
        p0Num = np.ones(numWords)#[0,0......]->[1,1,1,1,1.....],ones初始化1的矩阵
        p1Num = np.ones(numWords)
    
        # 整个数据集单词出现总数,2.0根据样本实际调查结果调整分母的值(2主要是避免分母为0,当然值可以调整)
        # p0Denom 正常的统计
        # p1Denom 侮辱的统计
        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])
        # 类别1,即侮辱性文档的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表,取对数避免下溢出或浮点舍入出错
        p1Vect = np.log(p1Num / p1Denom)
        # 类别0,即正常文档的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表
        p0Vect = np.log(p0Num / p0Denom)
        return p0Vect, p1Vect, pAbusive
    
    
    
    def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
        # 计算公式  log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C))
        # 使用 NumPy 数组来计算两个向量相乘的结果,这里的相乘是指对应元素相乘,即先将两个向量中的第一个元素相乘,然后将第2个元素相乘,以此类推。这里的 vec2Classify * p1Vec 的意思就是将每个词与其对应的概率相关联起来
        p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
        p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
        if p1 > p0:
            return 1
        else:
            return 0
        
    def testingNB():
        # 1. 加载数据集
        dataSet, Classlabels = loadDataSet()
        # 2. 创建单词集合
        myVocabList = createVocabList(dataSet)
        # 3. 计算单词是否出现并创建数据矩阵
        trainMat = []
        for postinDoc in dataSet:
            # 返回m*len(myVocabList)的矩阵, 记录的都是0,1信息
            trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
        # print('test',len(array(trainMat)[0]))
        # 4. 训练数据
        p0V, p1V, pAb = trainNB0(np.array(trainMat), np.array(Classlabels))
        # 5. 测试数据
        testEntry = ['love', 'my', 'dalmation']
        thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
        print(testEntry, '分类结果是: ', classifyNB(thisDoc, p0V, p1V, pAb))
        testEntry = ['stupid', 'garbage']
        thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
        print(testEntry, '分类结果是: ', classifyNB(thisDoc, p0V, p1V, pAb))
        
    testingNB()

    运行结果为:

    其余代码实现:http://www.52nlp.cn/%e5%ae%9e%e7%8e%b0-%e6%9c%b4%e7%b4%a0%e8%b4%9d%e5%8f%b6%e6%96%af%e6%a8%a1%e5%9e%8b%e7%ae%97%e6%b3%95%e7%a0%94%e7%a9%b6%e4%b8%8e%e5%ae%9e%e4%be%8b%e5%88%86%e6%9e%90

  • 相关阅读:
    JSON的一个例子(代码来源于网上)
    浅谈Event Flow、Process、Method及其Applications
    JS中object与constructor的分析
    浅谈for...in与for....of
    浅谈语言的过去与未来
    正则表达式的四个小应用
    初步了解DOM与BOM
    String Method的字符串变换的一个例子
    update 批量更新
    一个 Tomcat下两个项目。
  • 原文地址:https://www.cnblogs.com/liuxiangyan/p/12483615.html
Copyright © 2011-2022 走看看