• 04机器学习实战之朴素贝叶斯


    朴素贝叶斯 概述

    贝叶斯分类是一类分类算法的总称,这类算法均以贝叶斯定理为基础,故统称为贝叶斯分类。本章首先介绍贝叶斯分类算法的基础——贝叶斯定理。最后,我们通过实例来讨论贝叶斯分类的中最简单的一种: 朴素贝叶斯分类。

    贝叶斯理论 & 条件概率

    贝叶斯理论

    我们现在有一个数据集,它由两类数据组成,数据分布如下图所示:

    朴素贝叶斯示例数据分布

    我们现在用 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

    也就是说,我们会选择高概率对应的类别。这就是贝叶斯决策理论的核心思想,即选择具有最高概率的决策。

    使用条件概率来分类

    上面我们提到贝叶斯决策理论要求计算两个概率 p1(x, y) 和 p2(x, y):

    • 如果 p1(x, y) > p2(x, y), 那么属于类别 1;
    • 如果 p2(x, y) > p1(X, y), 那么属于类别 2.

    这并不是贝叶斯决策理论的所有内容。使用 p1() 和 p2() 只是为了尽可能简化描述,而真正需要计算和比较的是 p(c1|x, y) 和 p(c2|x, y) .这些符号所代表的具体意义是: 给定某个由 x、y 表示的数据点,那么该数据点来自类别 c1 的概率是多少?数据点来自类别 c2 的概率又是多少?注意这些概率与概率 p(x, y|c1) 并不一样,不过可以使用贝叶斯准则来交换概率中条件与结果。具体地,应用贝叶斯准则得到:

    应用贝叶斯准则

    使用上面这些定义,可以定义贝叶斯分类准则为:

    • 如果 P(c1|x, y) > P(c2|x, y), 那么属于类别 c1;
    • 如果 P(c2|x, y) > P(c1|x, y), 那么属于类别 c2.

    在文档分类中,整个文档(如一封电子邮件)是实例,而电子邮件中的某些元素则构成特征。我们可以观察文档中出现的词,并把每个词作为一个特征,而每个词的出现或者不出现作为该特征的值,这样得到的特征数目就会跟词汇表中的词的数目一样多。

    我们假设特征之间 相互独立 。所谓 独立(independence) 指的是统计意义上的独立,即一个特征或者单词出现的可能性与它和其他单词相邻没有关系,比如说,“我们”中的“我”和“们”出现的概率与这两个字相邻没有任何关系。这个假设正是朴素贝叶斯分类器中 朴素(naive) 一词的含义。朴素贝叶斯分类器中的另一个假设是,每个特征同等重要。

    Note: 朴素贝叶斯分类器通常有两种实现方式: 一种基于伯努利模型实现,一种基于多项式模型实现。这里采用前一种实现方式。该实现方式中并不考虑词在文档中出现的次数,只考虑出不出现,因此在这个意义上相当于假设词是等权重的。

    朴素贝叶斯 场景

    机器学习的一个重要应用就是文档的自动分类。

    在文档分类中,整个文档(如一封电子邮件)是实例,而电子邮件中的某些元素则构成特征。我们可以观察文档中出现的词,并把每个词作为一个特征,而每个词的出现或者不出现作为该特征的值,这样得到的特征数目就会跟词汇表中的词的数目一样多。

    朴素贝叶斯是上面介绍的贝叶斯分类器的一个扩展,是用于文档分类的常用算法。下面我们会进行一些朴素贝叶斯分类的实践项目。

    朴素贝叶斯 原理

    朴素贝叶斯 工作原理

    提取所有文档中的词条并进行去重
    获取文档的所有类别
    计算每个类别中的文档数目
    对每篇训练文档: 
        对每个类别: 
            如果词条出现在文档中-->增加该词条的计数值(for循环或者矩阵相加)
            增加所有词条的计数值(此类别下词条总数)
    对每个类别: 
        对每个词条: 
            将该词条的数目除以总词条数目得到的条件概率(P(词条|类别))
    返回该文档属于每个类别的条件概率(P(类别|文档的所有词条))
    

    朴素贝叶斯 开发流程

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

    朴素贝叶斯 算法特点

    优点: 在数据较少的情况下仍然有效,可以处理多类别问题。
    缺点: 对于输入数据的准备方式较为敏感。
    适用数据类型: 标称型数据。
    

    相关阅读:https://blog.csdn.net/u012162613/article/details/48323777

    朴素贝叶斯 项目案例

    项目案例1: 屏蔽社区留言板的侮辱性言论

    项目概述

    构建一个快速过滤器来屏蔽在线社区留言板上的侮辱性言论。如果某条留言使用了负面或者侮辱性的语言,那么就将该留言标识为内容不当。对此问题建立两个类别: 侮辱类和非侮辱类,使用 1 和 0 分别表示。

    开发流程

    收集数据: 可以使用任何方法
    准备数据: 从文本中构建词向量
    分析数据: 检查词条确保解析的正确性
    训练算法: 从词向量计算概率
    测试算法: 根据现实情况修改分类器
    使用算法: 对社区留言板言论进行分类
    

    收集数据: 可以使用任何方法

    本例是我们自己构造的词表:

    posting_list = [
        ['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', 'gar e'],
        ['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
        ['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
    class_vec = [0, 1, 0, 1, 0, 1]  # 1 is 侮辱性的文字, 0 is not

    准备数据: 从文本中构建词向量:

    def create_vocab_list(data_set):
        """
        获取所有单词的集合
        :param data_set: 数据集
        :return: 所有单词的集合(即不含重复元素的单词列表)
        """
        vocab_set = set()  # create empty set
        for item in data_set:
            # | 求两个集合的并集,# set()返回一个不重复的单词列表,将该列表加入到
            # vocab集合中
            vocab_set = vocab_set | set(item)  
        return list(vocab_set)
    def set_of_words2vec(vocab_list, input_set):
        """
        遍历查看该单词是否出现,出现该单词则将该单词置1
        :param vocab_list: 所有单词集合列表
        :param input_set: 输入一条数据集,如:posting_list[0]
        :return: 匹配列表[0,1,0,1...],其中 1与0 表示词汇表中的单词是否出现在输入的数据集中
        """
        # 创建一个和词汇表等长的向量,并将其元素都设置为0
        result = [0] * len(vocab_list)
        # 遍历文档中的所有单词,如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1
        for word in input_set:
            if word in vocab_list:
                result[vocab_list.index(word)] = 1
            else:
                # 这个后面应该注释掉,因为对你没什么用,这只是为了辅助调试的
                # print('the word: {} is not in my vocabulary'.format(word))
                pass
        return result

    训练算法: 从词向量计算概率

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

    重写贝叶斯准则

    我们使用上述公式,对每个类计算该值,然后比较这两个概率值的大小。

    问: 上述代码实现中,为什么没有计算P(w)?

    答:根据上述公式可知,我们右边的式子等同于左边的式子,由于对于每个ci,P(w)是固定的。并且我们只需要比较左边式子值的大小来决策分类,那么我们就可以简化为通过比较右边分子值得大小来做决策分类。

    首先可以通过类别 i (侮辱性留言或者非侮辱性留言)中的文档数除以总的文档数来计算概率 p(ci)接下来计算 p(w | ci) ,这里就要用到朴素贝叶斯假设。如果将 w 展开为一个个独立特征,那么就可以将上述概率写作 p(w0, w1, w2...wn | ci) 。这里假设所有词都互相独立,该假设也称作条件独立性假设(例如 A 和 B 两个人抛骰子,概率是互不影响的,也就是相互独立的,A 抛 2点的同时 B 抛 3 点的概率就是 1/6 * 1/6),它意味着可以使用 p(w0 | ci)p(w1 | ci)p(w2 | ci)...p(wn | ci) 来计算上述概率,这样就极大地简化了计算的过程。

    朴素贝叶斯分类器训练函数

    import numpy as np
    
    
    def train_naive_bayes(train_mat, train_category):
        """
        朴素贝叶斯分类修正版, 注意和原来的对比,为什么这么做可以查看书
        :param train_mat:  type is ndarray
                        总的输入文本,大致是 [[0,1,0,1], [], []]
        :param train_category: 文件对应的类别分类, [0, 1, 0],
                                列表的长度应该等于上面那个输入文本的长度
        :return: 两个条件概率向量,一个概率
        """
        train_doc_num = len(train_mat)
        words_num = len(train_mat[0])
        # 因为侮辱性的被标记为了1, 所以只要把他们相加就可以得到侮辱性的有多少
        # 侮辱性文件的出现概率,即train_category中所有的1的个数,
        # 代表的就是多少个侮辱性文件,与文件的总数相除就得到了侮辱性文件的出现概率
        pos_abusive = np.sum(train_category) / train_doc_num
        # 单词出现的次数
        # 原版,变成ones是修改版,这是为了防止数字过小溢出
        # p0num = np.zeros(words_num)
        # p1num = np.zeros(words_num)
        p0num = np.ones(words_num)
        p1num = np.ones(words_num)
        # 整个数据集单词出现的次数(原来是0,后面改成2了)
        p0num_all = 2.0
        p1num_all = 2.0
    
        for i in range(train_doc_num):
            # 遍历所有的文件,如果是侮辱性文件,就计算此侮辱性文件中出现的侮辱性单词的个数
            if train_category[i] == 1:
                p1num += train_mat[i]    # 直接把两个list相加,对应位置元素相加,
                # 最后直接一除就可以得到对应的概率list
                p1num_all += np.sum(train_mat[i])  # 标签为1的总词数
            else:
                p0num += train_mat[i]
                p0num_all += np.sum(train_mat[i])  # 标签为0的总词数
        # 后面改成取 log 函数
        p1vec = np.log(p1num / p1num_all)
        p0vec = np.log(p0num / p0num_all)
        return p0vec, p1vec, pos_abusive

    测试算法: 根据现实情况修改分类器

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

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

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

    函数图像

    使用算法: 对社区留言板言论进行分类

    朴素贝叶斯分类函数

    def classify_naive_bayes(vec2classify, p0vec, p1vec, p_class1):
        """
        使用算法:
            # 将乘法转换为加法
            乘法:P(C|F1F2...Fn) = P(F1F2...Fn|C)P(C)/P(F1F2...Fn)
            加法:P(F1|C)*P(F2|C)....P(Fn|C)P(C) -> log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C))
        :param vec2classify: 待测数据[0,1,1,1,1...],即要分类的向量
        :param p0vec: 类别0,即正常文档的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表
        :param p1vec: 类别1,即侮辱性文档的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表
        :param p_class1: 类别1,侮辱性文件的出现概率
        :return: 类别1 or 0
        """
        # 计算公式  log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C))
        # 使用 NumPy 数组来计算两个向量相乘的结果,这里的相乘是指对应元素相乘,即先将两个向量中的第一个元素相乘,然后将第2个元素相乘,以此类推。
        # 我的理解是:这里的 vec2Classify * p1Vec 的意思就是将每个词与其对应的概率相关联起来
        # 可以理解为 1.单词在词汇表中的条件下,文件是good 类别的概率 也可以理解为 2.在整个空间下,文件既在词汇表中又是good类别的概率
        p1 = np.sum(vec2classify * p1vec) + np.log(p_class1)
        p0 = np.sum(vec2classify * p0vec) + np.log(1 - p_class1)
        if p1 > p0:
            return 1
        else:
            return 0

    测试

    def testing_naive_bayes(list_post, list_classes):
        """
        测试朴素贝叶斯算法
        :return: no return 
        """
        # 1. 创建单词集合
        vocab_list = create_vocab_list(list_post)
    
        # 2. 计算单词是否出现并创建数据矩阵
        train_mat = []
        for post_in in list_post:
            train_mat.append(
                # 返回m*len(vocab_list)的矩阵, 记录的都是0,1信息
                # 其实就是那个东西的句子向量(就是data_set里面每一行,也不算句子吧)
                set_of_words2vec(vocab_list, post_in)
            )
        # 3. 训练数据
        p0v, p1v, p_abusive = train_naive_bayes(np.array(train_mat), np.array(list_classes))
        # 4. 测试数据
        test_one = ['love', 'my', 'dalmation']
        test_one_doc = np.array(set_of_words2vec(vocab_list, test_one))
        print('the result is: {}'.format(classify_naive_bayes(test_one_doc, p0v, p1v, p_abusive)))
        test_two = ['stupid', 'garbage']
        test_two_doc = np.array(set_of_words2vec(vocab_list, test_two))
        print('the result is: {}'.format(classify_naive_bayes(test_two_doc, p0v, p1v, p_abusive)))

    完整代码:

    In [19]:
    posting_list = [
        ['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']]
    class_vec = [0, 1, 0, 1, 0, 1]  # 1 is 侮辱性的文字, 0 is not
    
    In [20]:
    def create_vocab_list(data_set):
        """
        获取所有单词的集合
        :param data_set: 数据集
        :return: 所有单词的集合(即不含重复元素的单词列表)
        """
        vocab_set = set()  # create empty set
        for item in data_set:
            # | 求两个集合的并集,# set()返回一个不重复的单词列表,将该列表加入到
            # vocab集合中
            vocab_set = vocab_set | set(item)  
        return list(vocab_set)
    
    In [21]:
    vocab_list = create_vocab_list(posting_list)
    vocab_list
    
    Out[21]:
    ['him',
     'dog',
     'take',
     'cute',
     'ate',
     'love',
     'quit',
     'not',
     'worthless',
     'so',
     'garbage',
     'flea',
     'stop',
     'maybe',
     'licks',
     'how',
     'food',
     'dalmation',
     'has',
     'I',
     'park',
     'posting',
     'help',
     'please',
     'to',
     'problems',
     'stupid',
     'steak',
     'buying',
     'mr',
     'my',
     'is']
    In [22]:
    def set_of_words2vec(vocab_list, input_set):
        """
        遍历查看该单词是否出现,出现该单词则将该单词置1
        :param vocab_list: 所有单词集合列表
        :param input_set: 输入一条数据集,如:posting_list[0]
        :return: 匹配列表[0,1,0,1...],其中 1与0 表示词汇表中的单词是否出现在输入的数据集中
        """
        # 创建一个和词汇表等长的向量,并将其元素都设置为0
        result = [0] * len(vocab_list)
        # 遍历文档中的所有单词,如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1
        for word in input_set:
            if word in vocab_list:
                result[vocab_list.index(word)] = 1
            else:
                # 这个后面应该注释掉,因为对你没什么用,这只是为了辅助调试的
                # print('the word: {} is not in my vocabulary'.format(word))
                pass
        return result
    
    In [23]:
    set_of_words2vec(vocab_list, posting_list[0])
    
    Out[23]:
    [0,
     1,
     0,
     0,
     0,
     0,
     0,
     0,
     0,
     0,
     0,
     1,
     0,
     0,
     0,
     0,
     0,
     0,
     1,
     0,
     0,
     0,
     1,
     1,
     0,
     1,
     0,
     0,
     0,
     0,
     1,
     0]
    In [24]:
    import numpy as np
    
    
    def train_naive_bayes(train_mat, train_category):
        """
        朴素贝叶斯分类修正版, 注意和原来的对比,为什么这么做可以查看书
        :param train_mat:  type is ndarray
                        总的输入文本,大致是 [[0,1,0,1], [], []]
        :param train_category: 文件对应的类别分类, [0, 1, 0],
                                列表的长度应该等于上面那个输入文本的长度
        :return: 两个条件概率向量,一个概率
        """
        train_doc_num = len(train_mat)
        words_num = len(train_mat[0])
        # 因为侮辱性的被标记为了1, 所以只要把他们相加就可以得到侮辱性的有多少
        # 侮辱性文件的出现概率,即train_category中所有的1的个数,
        # 代表的就是多少个侮辱性文件,与文件的总数相除就得到了侮辱性文件的出现概率
        pos_abusive = np.sum(train_category) / train_doc_num
        # 单词出现的次数
        # 原版,变成ones是修改版,这是为了防止数字过小溢出
        # p0num = np.zeros(words_num)
        # p1num = np.zeros(words_num)
        p0num = np.ones(words_num)
        p1num = np.ones(words_num)
        # 整个数据集单词出现的次数(原来是0,后面改成2了)
        p0num_all = 2.0
        p1num_all = 2.0
    
        for i in range(train_doc_num):
            # 遍历所有的文件,如果是侮辱性文件,就计算此侮辱性文件中出现的侮辱性单词的个数
            if train_category[i] == 1:
                p1num += train_mat[i]    # 直接把两个list相加,对应位置元素相加,
                # 最后直接一除就可以得到对应的概率list
                p1num_all += np.sum(train_mat[i])  # 标签为1的总词数
            else:
                p0num += train_mat[i]
                p0num_all += np.sum(train_mat[i])  # 标签为0的总词数
        # 后面改成取 log 函数
        p1vec = np.log(p1num / p1num_all)
        p0vec = np.log(p0num / p0num_all)
        return p0vec, p1vec, pos_abusive
    
    In [25]:
    def classify_naive_bayes(vec2classify, p0vec, p1vec, p_class1):
        """
        使用算法:
            # 将乘法转换为加法
            乘法:P(C|F1F2...Fn) = P(F1F2...Fn|C)P(C)/P(F1F2...Fn)
            加法:P(F1|C)*P(F2|C)....P(Fn|C)P(C) -> log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C))
        :param vec2classify: 待测数据[0,1,1,1,1...],即要分类的向量
        :param p0vec: 类别0,即正常文档的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表
        :param p1vec: 类别1,即侮辱性文档的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表
        :param p_class1: 类别1,侮辱性文件的出现概率
        :return: 类别1 or 0
        """
        # 计算公式  log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C))
        # 使用 NumPy 数组来计算两个向量相乘的结果,这里的相乘是指对应元素相乘,即先将两个向量中的第一个元素相乘,然后将第2个元素相乘,以此类推。
        # 我的理解是:这里的 vec2Classify * p1Vec 的意思就是将每个词与其对应的概率相关联起来
        # 可以理解为 1.单词在词汇表中的条件下,文件是good 类别的概率 也可以理解为 2.在整个空间下,文件既在词汇表中又是good类别的概率
        p1 = np.sum(vec2classify * p1vec) + np.log(p_class1)
        p0 = np.sum(vec2classify * p0vec) + np.log(1 - p_class1)
        if p1 > p0:
            return 1
        else:
            return 0
    
    In [26]:
    def testing_naive_bayes(list_post, list_classes):
        """
        测试朴素贝叶斯算法
        :return: no return 
        """
        # 1. 创建单词集合
        vocab_list = create_vocab_list(list_post)
    
        # 2. 计算单词是否出现并创建数据矩阵
        train_mat = []
        for post_in in list_post:
            train_mat.append(
                # 返回m*len(vocab_list)的矩阵, 记录的都是0,1信息
                # 其实就是那个东西的句子向量(就是data_set里面每一行,也不算句子吧)
                set_of_words2vec(vocab_list, post_in)
            )
        # 3. 训练数据
        p0v, p1v, p_abusive = train_naive_bayes(np.array(train_mat), np.array(list_classes))
        # 4. 测试数据
        test_one = ['love', 'my', 'dalmation']
        test_one_doc = np.array(set_of_words2vec(vocab_list, test_one))
        print('the result is: {}'.format(classify_naive_bayes(test_one_doc, p0v, p1v, p_abusive)))
        test_two = ['stupid', 'garbage']
        test_two_doc = np.array(set_of_words2vec(vocab_list, test_two))
        print('the result is: {}'.format(classify_naive_bayes(test_two_doc, p0v, p1v, p_abusive)))
    
    In [28]:
    testing_naive_bayes(posting_list, class_vec)
    
     
    the result is: 0
    the result is: 1
    
  • 相关阅读:
    ffmpeg rtmp推流 视频转码
    java日志发展史 log4j slf4j log4j2 jul jcl 日志和各种桥接包的关系
    nginx stream 流转发,可以转发rtmp、mysql访问流,转发rtmp、jdbc请求
    java web http 转https 通过nginx代理访问
    linux 服务器磁盘挂载
    novnc 通过websockify代理 配置多点访问
    linux 文件服务 minio 安装部署配置
    AOP实现原理,手写aop
    java 泛型
    JAVA反射getGenericSuperclass()用法
  • 原文地址:https://www.cnblogs.com/xinmomoyan/p/10494444.html
Copyright © 2020-2023  润新知