机器学习实战(3)——朴素贝叶斯(上)(附带Python3源码与资源分享)

一、理论基础
1、数学基础
(1)联合概率
表示两个时间同时发生的概率,A与B的联合概率表示为:P(AnB)或者P(A,B)或者P(AB)
(2)条件概率

如上图所示,条件概率(Conditional probability)是指在事件B发生的情况下,A发生的概率,用公式表示为P(A|B)
(3)贝叶斯定理
贝叶斯定理描述了随机事件A与B的条件概率的一则定理,其推导过程为:

最后一个公式就是贝叶斯定理的公式了,说白了就是知道单独A和B的概率,然后又知道反条件概率(条件颠倒)

(3)全概率公式
  如果事件组C1,C2,.... 满足
  • C1,C2....两两互斥,即 Ci ∩ Cj = ∅ ,i≠j , i,j=1,2,....,且P(Ci)>0,i=1,2,....;
  • C1∪C2∪....=Ω ,则称事件组 C1,C2,...是样本空间Ω的一个划分
设 C1,C2,...是样本空间Ω的一个划分,A为任一事件,则:

举个全概率公式计算的例子吧:一共有10张彩票,其中有2张有奖,10人依次购买,问:第一个人鹤第二个人的中奖概率是否相同?
设B = “第一个人中奖”,A = “第二个人中奖”,很显然,根据古典概型我们可以得到:P(B)= 2/10 = 1/5 ,但是P(A),却有很多不同的意见
  • 一部分同学认为P(A)=1/5
  • 另外一部分人认为,P(A)与B是否发生有关系,如果B发生了,则P(A)=1/9,如果B没有发生,则P(A)=2/9
第二种观点听起来很有道理,但是稍加分析,大家就会发现它犯了一个致命的错误,即将“概率”与“条件概率”这两个不同的概念混淆了,事实上,“1/9”和“2/9”不是A发生的概率,而是在B或者非B发生的条件下,A发生的条件概率。
也就是说,第一个观点是正确的,那么P(A)=1/5又是怎么来的呢?
设 C = “第一人是否中奖”,则C有两个互斥选择:C1 = “第一个人中奖”,C2 = “第一个人未中奖”,那么接下来就可以根据全概率公式得到:
P(A) = P(C1)*P(A | C1)+ P(C2)*P(A | C2) = 1/5 * 1/9 + 4/5 * 2/9 = 1/5

全概率公式,说白了就是将条件拆分,拆分成一个个比较好求的小的互斥事件,这样求起来就非常简单了,总体而言就是条件概率的一种变形求法
可以参考:维基百科

2、贝叶斯决策理论
朴素贝叶斯是贝叶斯决策理论的一部分,后者是前者的基础,所以在讲述朴素贝叶斯之前,有必要快速了解一下贝叶斯决策理论。
假设我们现在有一个数据集,它由两类数据(原点和三角)组成,数据分布图如下所示:

因为图形的概率分布参数我们是已知的,所以如果现在有一点A(x,y),我们就可以根据概率分布得到这一点A属于这两个类别的概率是多少,这里我们用P1(x,y)表示数据点A属于类别1(图中圆点部分)的概率,用P2(x,y)表示数据点A属于类别 2(三角区域)的概率,那么对于一个新的数据点(x,y),可以下面规则来判断其类别:
  • 如果P1(x,y)>P2(x,y),那么点A类别为1
  • 如果P1(x,y)<P2(x,y),那么点A类别为0
也就是说,我们会选择概率比较大的决策,这就是贝叶斯决策理论的核心了,当然了,我们在生活中大部分情况也是这么做的,就好像是买彩票一样,我们买的号,大都是认为这个号,中奖的概率比较大,所以才会买的。我们来类别一下之前学到的KNN、决策树与贝叶斯分类器,哪个效果更好:
  • 对于图中数据,给出一个新数据点进行划分,KNN最大的问题就是计算量很大,每次对新的数据点进行划分,都要进行1000次距离计算(假设有1000个数据点)
  • 而如果用决策树的话,面临最大的问题是:如果划分属性,也就是要沿着X轴与Y轴进行划分数据(将连续数据变为离散数据)
  • 如果采用贝叶斯的话,只需要计算该数据点属于每个类别的概率(上图只需要计算2次),然后进行比较即可
注意:当然上面算法比较的目的,不是说明贝叶斯决策分类多么好,而是说我们针对某个问题选择算法的时候,要看看各个算法的应用领域,然后选择最优的算法(当然也是为了引出贝叶斯分类器的优势所在)

2、贝叶斯定理
(1)转化为数学模型
根据之前的数学知识,上面出现的P1(x,y)和P2(x,y)可以统一成为P(C | A ),其中C是数据点的类别向量(c1,c2,,,),而A则是数据点(x,y),具体含义就是给定数据点A,得到其所属类别的概率
所以上面的分类问题,我们可以看成获得P(C | A),然后根据各个类别的概率,找到最大的概率即可,大体来说有两个策略:
  • 给定X,可以通过直接建模P(C| A)来预测C(当然是有数据集样本的),这样得到的是“判别式模型”(discriminative models)
  • 给定X,也可以先对联合概率分布P(A,C)建模,然后再由此获得P(C | A),这样得到的是“生成式模型”(generative models)
在机器学习算法中,决策树、BP神经网络、支持向量机等都是判别式模型,通过数据集得到一个固定的模式,然后通过这个模式,你输入数据点,就可以得到输出的具体的判别类别了,而对于生成式模型,就要考虑:

根据上面的贝叶斯定理,又可以得到:


解释一下公式含义
  • 画红线部分P(C)被称为先验概率(Prior probability)或者似然,也就是在事件A发生之前,我们就对C事件概率的一个判断
  • 画黑线部分P(C|A)被称为后验概率(Posterior probability),也就是在事件A发生后,我们队C事件概率的重新评估
  • 画蓝线部分被称为可能性函数(Likelyhood),这是一个调整因子,使得预估概率更接近真实概率
举个例子吧:
我们在牛家村看到一个男人,然后你认为这个男人有六成可能是牛家村人,然后你又知道了这个男人姓牛,那么你就会认为这个男人有九成的概率是牛家村人!
我们来分析一下,P(男人是牛家村人) = 6/10,这个就是我们的先验概率了,我们在知道他姓牛之前,做出来判断,然后我们知道他姓牛,那么P(男人是牛家村人|姓牛) = 9/10,这个就是后验概率了,我们知道了他姓牛,然后我们对事件概率进行了重新评估

继续咱们上面的话题,所以估计P(C | A)的问题就转换成了如何基于训练数据集D来估计先验概率P(C)和似然P(A | C),类先验概率P(C)表达了样本空间中各类样本所占的比例,根据大数定理,当训练集中包含充足的独立同分布样本是,P(C)可以通过各类样本出现的频率来进行估计

举个例子吧:
一共有10个球,三类红白黄,其中红球有3个,白球有4个,黄球有3个,则对应的P(C)可以求得,P(红)=3/10,P(白)=4/10,P(黄)=3/10

对于类条件概率P(A |C)来说有些复杂,因为它涉及关于A的所有属性的联合概率,直接根据样本出现的频率来估计将会遇到严重的困难,比如假设样本的d个属性都是二值的,则样本空间将有2^d种可能的取值,在现实应用中,这个值往往远大于训练数据样本数m,也就是说很多样本取值在训练集中根本就没有出现,直接使用频率来估计P(A |C)显然是不可行的,因为“未被观测到”与“出现概率为0"通常是不同过的。

针对上面的问题,一种常用的策略就是不采用频率来计算了,而是先假定其具有某种确定的概率分布形式(机器学习常用的手段),再基于训练样本对概率分布的参数进行估计,这个就是贝叶斯决策的大致过程了

(2)基本流程
在上面的论题中,我们要得到数据点属于每个类别的概率,那是要有一个前提的:已经知道了每个类别的概率分布(也就说知道了概率参数和分布类型),但是现实生活中,我们往往是不知道的,往往知道的只是一堆具体的样本数据
因此,基于估计的贝叶斯分类器的基本步骤是:
  • 第一步:通过样本数据来估计类别的概率分布(也就是得到各个参数,概率分布本来就是一个数学公式,其中你知道概率分布的类别,再知道其中的关键参数,就知道了整个分布公式)
  • 第二步:既然知道了概率分布,那么就算出要分类的数据点,属于每个类别的概率即可
  • 第三步:对每个类别概率进行比较,选择概率最大的,然后将类别分给该数据点即可
(3)关键点
上面三步中,最关键的就是第一步,可以说第一步的工作量占据整个算法的九成,根据样本数据,我们如何来估计每个类别的概率分布呢?
这就要用到数学知识了,事实上,概率模型的训练过程,就是参数估计(parameter  estimation)过程,对于参数估计,统计学界的两个学派分布提供了不同的解决方案:
  • 频率主义学派(Frequentisit,也就是古典概率学派):认为参数虽然未知,但是却是客观存在的固定值,因此,可通过优化似然函数等准则来确定参数值
  • 贝叶斯学派(Bayesian,后来发展出来的学派):认为参数是未观察到的随机变量,其本身也也可以有分布,因此,可假定参数服从一个先验分布,然后基于观测到的数据来计算参数的后验分布。
上面的语句可能听着有点迷糊(来自于西瓜书,感觉很好,有兴趣的可以去了解一下)

这里说一下哈,其中频率主义学派最常用的估计方法就是极大似然估计了(当然了,这里就不介绍了,如果想要了解的话,可以去看西瓜书或者看李航的统计学习方法)

3、朴素贝叶斯(Naive Bayes)
回归正题哈,我们这里要说的是贝叶斯分类器的一种——朴素贝叶斯,在上一节中,我们可以知道贝叶斯分类器的原理,就是对类条件进行估计和堆类先验概率进行计算(大数定理),然后再通过贝叶斯定理公式求出各个类别的概率大小,然后选择概率最大的进行分类

但是上面的估计贝叶斯分类中一个最大的难点就是:对于类条件概率P(A|C)的估计,我们很难从有限的数据集直接对其进行准确估计,为了避开这个估计问题,衍生出了朴素贝叶斯分类方法。

注意:这里说衍生出朴素贝叶斯,并不是说朴素贝叶斯解决了贝叶斯的问题,甚至比贝叶斯分类方法要好,这是两种不同的方法,针对不同的问题,表现出不同的效果,没有说一个算法就肯定比另外一个算法要好,注意这里的区别!
贝叶斯分类时一类分类算法的统称,这类算法均以贝叶斯定理为基础,故通称为贝叶斯分类,而朴素贝叶斯分类则是贝叶斯分类中最简单,也是最常见的一种分类方法


朴素贝叶斯巧妙的避开了参数估计过程,它采用“属性条件独立性假设”:对已知类别假设所有属性相互独立,换而言之,假设每个属性独立地对分类结果发生影响

(1)核心算法
朴素贝叶斯分类,说白了就是一种分类方法,其核心算法就是:

也就是,我们上面说的贝叶斯定理,如果说分类,那么也许换一个形式,大家对于贝叶斯定理的理解更加深入一些:

根据上面的公式,我们最终求得就是P(类别|特征),也就是给你一个特征(可以看做是数据点),然后你能够得到其类别概率即可
(2)例题分析
理解一个理论,最有效率的方法就是给出一个具体事例,用问题来分析,给定如下数据集:
颜值
技术
信用
是否录用
大牛
肯定录用
菜鸡
考虑考虑
大牛
肯定录用
中庸
考虑考虑
大牛
考虑考虑
中庸
肯定不录用
菜鸡
考虑考虑
这是一个公司针对招聘人员的特征进行是否录用的数据集,现在让你利用这些数据集,采用朴素贝叶斯分类方法,判断(高,中庸,好)的人是否被录用情况,这是一个典型的分类问题了,首先我们将其转换为具体贝叶斯分类数学问题:
  • P(肯定录用 | 高,中庸,好),P(考虑考虑 | 高,中庸,好)和P(肯定不录用 | 高,中庸,好),三个概率哪个大,大的那个就是分类结果了
让我们直接求其中任一一个公式,我们都是不能直接求的,但是我们有贝叶斯定理啊,可以联系起来(毕竟你朴素贝叶斯的核心算法就是贝叶斯定理嘛):

另外两个同样,这里就不写了,其中P(肯定录用 | 高,中庸,好)是我们不知道的,而且也直接求不出来,但是公式右边三个量,我们却可以直接求出来,首先第一个公式
  • P(高,中庸,好 | 肯定录用) = P(高 | 肯定录用)* P(中庸 | 肯定录用) * P(好 |肯定录用)

注意:上面这个公式是怎么来的呢,这是因为朴素贝叶斯分类自带的前提条件——假设所有属性都是独立的,根据相互独立定理,就可以得到上面的公式了,什么,你都不知道相互独立定理,那你自行去百度吧。

只要我们分别求公式后面三个值,就可以得到P(高,中庸,好 | 肯定录用)的值了,我们的任务也就完成了三分之一!

同样由于所有属性相互独立,所以第二个量可以表示为:
  • P(高,中庸,好)= P(高)* P(中庸)* P(好)
所以整个公式,就可以写成如下形式:

好了,接下来就是小学数学计算的阶段了:
  • P(高 | 肯定录用) = 1/2 (肯定录用样本一共有2个,其中颜值为高的,只有1个)
  • P(中庸| 肯定录用) = 0 /2 = 0 (肯定录用样本中,没有技术中庸的,所以为0)
  • P(好 | 肯定录用) = 2/2 =1
  • P (肯定录用)= 2/7 (一共有7条数据,其中肯定录用的有两条)
  • P(高)= 4/7
  • P (中庸)= 2/7
  • P(好) = 4/7
算完以后,直接将上面的数据代入公式即可,最后求得P(肯定录用 | 高,中庸,好)的概率为 0 ,没错就是0,不用惊讶,只要分子上出现一个0,就会出现概率为0的情况

然后我们再算一下P(考虑考虑 | 高,中庸,好),这里就不领着大家一起算了,大家可以自己算一下,然后再对比一下,看看是不是正确,也考验一下自己是否真正理解了这个过程
  • P(肯定录用 | 高,中庸,好) = 0
  • P(考虑考虑 | 高,中庸,好) = 147/256
  • P(肯定不录用 |高,中庸,好)= 0
综上比较三个概率值,最后得到结果是:考虑考虑

注意:如果用上面的估计贝叶斯方法,那就是先通过上面的数据集,估计出P(是否录用 | 颜值,技术,信用)的概率分布,然后在将(高,中庸,好)带进去求出一个具体的概率,然后进行比较

(3)优缺点
首先,说一下这个朴素贝叶斯其实就是根据数据集得出一个概率,这个概率是基于频数的,既然是基于频数的,那么肯定要用到大数定理,也就说它是很依赖数据集的,如果数据集不够大,如果数据集类别不够全,它很可能出现大的问题(下面会涉及到这个问题),
优点:
  • 算法逻辑简单,就是依赖一个贝叶斯定理公式,剩下的就是小学水平的四则运算了
  • 分类过程开销很小,因为假设所有属性都相互独立,所以将所有属性都单独求概率,不会有联合概率
缺点:
  • 首先,属性间相互独立这个前提在现实生活中,是很少见的,往往不成立,所以在属性个数比较多或者属性之间关系比较大的时候,朴素贝叶斯分类效果不是很好

这里要提几点值得注意和思考的问题:
  • 朴素贝叶斯中朴素二字的含义
朴素,其实就是简单的意思,这里的朴素贝叶斯相比其他贝叶斯分类算法,是最简单,也是最原始的一种方法,其本质就是:贝叶斯定理+假设所有属性相互独立,除此之外,再也没有其他思想了,非常简答粗暴,很容易理解,这就是为什么起名为朴素贝叶斯的缘故。
  • 为什么要假设所有属性相互独立呢?
这个也很好理解,因为它是朴素贝叶斯,追求的就说简答粗暴,怎么简单怎么来,假设所有属性相互独立以后,你就会发现所有疑难都没有,如果你好好的跟着做过上面的例题,你就会发现原理这个朴素贝叶斯过程很简单啊,列好公式,然后就是小学数学计算了,为什么这么简单呢?原因就在这个假设上,如果没有这个假设,你就没有办法将分子中的P(高,中庸,好 | 肯定录用)分解,直接求这个公式是不合理的,也是很困难的,因为你要求所有属性的联合概率,比如上面那个例题一共有三个属性特征:颜值,技术和信用,三个特征总和为:2*3*2=12,(咳,我这个是相当少的,现实生活中往往有很多特征,而每个特征的取值也是非常多的,这样加和起来,最后需要的数据集将是一个庞大的无法想象的数目,如果有一个属性样本没有出现,那么极有可能会导致分子为0,一旦为0,那准确率就会大大降低),总体来说就是两个原因:一个是如果没有这个假设,计算会很复杂,另外一个原因就是因为数据的稀疏性,导致概率为0的情况,导致准确率下降
  • 实际编程中
因为贝叶斯算法实际上比较的是概率的大小,而不是让你真正算出其确切的概率,所以在实际编码的时候,几个类别概率的分母都是一样,这就可以省略了,直接比较分子即可


二、实战之实现言论过滤(简单算法)

1、问题描述(在线社区留言板)

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

主要思路:给你大量的样本语句(同时每个语句附带标签,比如这句话是侮辱还是非侮辱)

  • 那么我们首先要对每条语句进行词条分割,也就是将一个语句分成一个个的词(比如“我是男人”,转换成“我”“是”“男人”)
  • 然后我们要对生成的词条数据集进行去重(也就将重复的词条去掉),变成一个词汇表
  • 制成词汇表以后,我们要根据语句的标签,对每个单词进行概率的确定(比如单词“男人”,它属于侮辱词的概率为多少,非侮辱词汇的概率为多少,注意这两个数值和为1)
  • 好了,上面三步是训练步骤,也就是我们可以根据数据集训练得到词汇表中每个词属性的先验概率
  • 根据先验概率,再给我们一条新的语句,我们就可以判断这条语句属于侮辱语句的概率是多少了(如果属于侮辱语句的概率大于非侮辱语句,则判断该语句为侮辱语句)

2、直接上代码

# -*- coding: UTF-8 -*-
import numpy as np
from functools import reduce

"""
函数1:创建实验样本
功能说明:首先要将文本切分成词条,这个函数就是干这个用的不过,现在已经切好了
返回值说明:postingList就是词条,classVec则是词条对应的分类标签
"""
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                                                         #返回实验样本切分的词条和类别标签向量


"""
函数2: 制作词汇表
函数说明:将切分的实验样本词条整理成不重复的词条列表,也就是词汇表
参数说明:dataSet就是上面的postingList,也就是重复的词条样本集,而vocabSet则是无重复的词汇表
"""
def createVocabList(dataSet):
	vocabSet = set([])  					#创建一个空的不重复列表
	for document in dataSet:
		vocabSet = vocabSet | set(document) #取并集
	return list(vocabSet)

"""
函数3:词汇向量化
函数说明: 根据vocabList词汇表(也就是上面函数制作的词汇表),将inputSet(你输入的词汇)向量化,
          向量的每个元素为1或0,如果词汇表中有这个单词,就置1;没有,就置0
参数说明:最后返回的是文档向量(不是0就是1)
"""
def setOfWords2Vec(vocabList,inputSet):
    returnVec = [0] * len(vocabList)                    # 创建一个其中所含元素都为0的向量
    for word in inputSet:                               #遍历每个词条
        if word in inputSet:                            #如果词条存在于词汇表中,则置1
            returnVec[vocabList.index(word)] = 1
        else:
            print("词汇:%s 并没有在词汇表中" % word)   # 词汇表中没有这个单词,表示出现了问
    return returnVec                                    #返回文档向量


"""
函数4:朴素贝叶斯分类器训练函数
函数说明: 利用朴树贝叶斯求出分类概率,也可以说是求出先验概率
参数说明:
输入参数trainMatrix:是所有样本数据矩阵,每行是一个样本,一列代表一个词条
输入参数trainCategory:是所有样本对应的分类标签,是一个向量,维数等于矩阵的行数
输出参数p0Vect:是一个向量,维数与上面相同,每个元素表示对应样本属于侮辱类的概率
输出参数p1Vect:是一个向量,和上面那个向量互补(因为是二分类问题),每个元素对应样本属于非侮辱类的概率
输出参数pAbusive:是一个概率值,表示这篇文档(所有样本的综合)属于侮辱类的概率
"""
def trainNB0(trainMatrix,trainCategory):
	numTrainDocs = len(trainMatrix)					#训练集中样本数量
	numWords = len(trainMatrix[0])					#每条样本中的词条数量
	pAbusive = sum(trainCategory)/float(numTrainDocs)		#文档属于侮辱类的概率
	p0Num = np.zeros(numWords); p1Num = np.zeros(numWords)	        #创建numpy.zeros数组(维度和numWords一样,但元素全是0)
	p0Denom = 0.0; p1Denom = 0.0                        	        #分母初始化为0.0
	for i in range(numTrainDocs):
		if trainCategory[i] == 1:				#统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)···
			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 = p1Num/p1Denom						#相除
	p0Vect = p0Num/p0Denom
	return p0Vect,p1Vect,pAbusive					#返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率

"""
函数5:朴素贝叶斯分类器分类函数
函数说明: 利用几个函数得到的结果,直接对vec2Classify进行分类,说白了就是利用贝叶斯定理来求了,注意这里没有用到分母,直接求的分子
参数说明:
输入参数vec2Classify——要分类的向量
输入参数后面三个——就是函数3得到的三个输出向量
输入参数:就是分类结果了(因为分类标签就只有2个,如果更改数据的话,这里要改一下)
"""
def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):
    p1 = reduce(lambda x,y:x*y, vec2Classify * p1Vec)*pClass1     #对应元素相乘,相同为1,不同为0
    p0 =reduce(lambda x,y:x*y, vec2Classify*p0Vec)*(1.0-pClass1)
    print('p0:',p0)
    print('p1:',p1)
    if p1>p0:
        return 1
    else:
        return 0


"""
函数6:测试朴素贝叶斯分类器
函数说明: 这个就是一个测试函数了
"""
def testingNB():
    listOPosts,listClasses = loadDataSet()                                  #创建实验样本
    myVocabList = createVocabList(listOPosts)                               #创建词汇表
    trainMat=[]
    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))             #将实验样本向量化
    p0V,p1V,pAb = trainNB0(np.array(trainMat),np.array(listClasses))        #训练朴素贝叶斯分类器
    testEntry = ['love', 'my', 'dalmation']                                 #测试样本1
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))              #测试样本向量化
    if classifyNB(thisDoc,p0V,p1V,pAb):
        print(testEntry,'属于侮辱类')                                        #执行分类并打印分类结果
    else:
        print(testEntry,'属于非侮辱类')                                       #执行分类并打印分类结果
    testEntry = ['stupid', 'garbage']                                       #测试样本2

    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))              #测试样本向量化
    if classifyNB(thisDoc,p0V,p1V,pAb):
        print(testEntry,'属于侮辱类')                                        #执行分类并打印分类结果
    else:
        print(testEntry,'属于非侮辱类')                                       #执行分类并打印分类结果

if __name__ == '__main__':
	testingNB()

3、上述算法函数分析

(1)函数1:创建实验样本

这个就相当简单了,将训练样本语句分割成一个个词条(注意这是重复的),返回词条矩阵和标签向量

(2)函数2:制作词汇表

最后生成的词汇表不再是矩阵,而是一个单纯的列表,依次列举了所有的单词,如下图所示:


(3)函数3:词汇向量化

这个函数有两个参数vocabList和inputSet,第一个参数vocabList就是词汇表,而inputSet则是一句输入单词向量(),举个例子,我输入的vacabList(就是上面的词汇表)为:


输入的inputSet为:


最后输出结果(即returnVec)是:


上面标1的表示,在词汇表的这些位置的单词,你的inputSet语句中都曾经有,而标有0的位置表示没有出现过

注意:这里的1表示的是——出现过,而不是出现次数,这里你无论出现多少次,只要出现就是1,没有出现就是0,不会出现大于1的情况

(4)函数4:朴素贝叶斯分类器训练函数

这个函数说白了就是将你所有样本语句中的词汇都训练一下,看看这些词汇哪些属于侮辱类的词汇(当然得到的是概率值),里面涉及到几个参数,这里讲解一下

输入参数:

  • trainMatrix:这是一个矩阵,表示的是所有样本转换为向量后的结果,如下图所示:


  • trainCategory:这是标签,也就是上面每个向量对应的标签(其中1表示侮辱,0表示非侮辱),如下图:


函数内部的变量参数:

  • p1Num:这是一个向量,记录了词汇表中标签为侮辱类语句中词汇,在词汇表中出现的次数,显示结果如下:


上述列出的数据集中,一共有六个数据样本,但是标有侮辱标签也就是1的,只有三个,找出这三个语句,然后将其词汇按照词汇表一一对照,有一个词,就在那个词上数量,加1,最后就得到上面的p1Num向量

  • p1Denom:这就是一个数字,也就是上面p1Num中所有数字的和,当前值为19,
  • p1Vect:这是上面两个数相除的结果,最后是一个向量,大部分都是0
  • p0Num、p0Denom和p0Vect原理同上,只不过是换了个标签而已

输出参数:

  • p0Vect和p1Vect上面已经说了,这里就不再说了
  • pAbusive是指样本集标签(0,1,0,1,0,1)中,侮辱类的词条占所有词条的比例(也可以说是概率),也就是标1的标签占所有标签的概率(3/6=0.5)

(5)函数5:朴素贝叶斯分类器分类函数

这个函数是上面4个函数的综合运用,主要功能就是你给我一条语句,然后我将这条语句划分成一个个词条,然后根据我拥有的带有标签的样本数据集得到的模型参数(也就是各种先验概率),计算这条语句属于侮辱类语句的概率和非侮辱类的概率,然后取两者大的概率为其分类标签

输入参数:

  • vec2Classify:这是一个向量,是你给我的语句(也就是要分类的语句),然后我经过函数3转换得到的词汇向量(有几个1,就表示这句话有几个单词),我打印了一下:


  • p0Vec、p1Vec和pClass1就是函数4得到的三个返回结果(也就是我们训练数据集得到的有用的模型参数)

函数内部参数:

  • p1表示这条语句(你给我的,要分类的语句)属于侮辱类语句的概率
  • p0表示这条语句(你给我的,要分类的语句)属于非侮辱类语句的概率

困难函数:

  • reduce()函数:就是表示累积或者累加,要看第一个参数赋予的运算方式了,详情可以参考——Python中reduce详解

(6)函数6:测试朴素贝叶斯分类器

这个函数其实属于函数5的升级版,虽然函数5也可以对一条语句进行分类,但是运行函数5要进行语句的预处理,因为函数5的输出参数并不是我们直接就有的,而是要通过运行上面前4个函数才可以得到,所以所以说函数6是总领的作用,将前5个函数都综合起来

当然,这是一个简单的测试函数,测试语句都是自己写在代码中的,如果要运用到现实生活中,肯定是不能这么写的,因为测试数据集往往是比较大的,要程序自己会读取文本才可以,下一个实例,我们会再讲如何进行,这个实例只是简单的将朴素贝叶斯的算法框架得了出来

(7)显示结果:



结果分析:
(1)分子乘积,出现0问题
上面已经说过了,如果朴素贝叶斯很依赖数据集,如果有些数据样本缺少就会出现某条概率为0的情况,我们直接根据公式来解释吧

上面这个公式中,直接看最后一项,分子中一共有好几项相乘,根据乘法定理,只要有一些为0,那么最后结果就会为0,显然我们的数据集过于稀少了,有些样本中没有,比如肯定录用中没有颜值高的,或则肯定录用中没有技术中庸的,或者肯定录用中没有信用良好的,只要有一个为0,就会导致最后结果为0,所以我们在选择算法的时候,一定要看好,数据集是否稀疏,如果数据集过于稀疏,就不要用朴素贝叶斯了,因为效果会很不好。
  • 解决方法:将所有词出现次数初始化为1,并将分母初始化为2,这样就避免了0概率的出现,这种做法叫做拉普拉斯平滑(Laplace Smoothing),又被称为加1平滑
(2)向下溢出问题
由于计算公式中有很多是分式形式,这就导致出现很多小数,而小数相乘,会越乘越小,这样就造成了向下溢出,在程序中,在相应小数位置进行四舍五入,计算结果就可能变成0,为了解决这个问题,可以对乘积结果去自然对数,通过求对数可以避免下溢出或者浮点数四舍五入导致的错误,同时,采用自然对数进行处理不会有任何损失,下图给出函数f(x)和ln(f(x))的曲线:

首先,这是一个比较问题,而不是求值问题,所以即便上面两个函数得到的结果不同,但是不会影响最后的结果!

4、接下来就是如何改进算法了

(1)首先,要改进函数4,也就是trainNB0(trainMatrix,trainCategory),因为这个函数中有分子和分母初始化的步骤,具体改动如下:

"""
函数4:朴素贝叶斯分类器训练函数
函数说明: 利用朴树贝叶斯求出分类概率,也可以说是求出先验概率
参数说明:
输入参数trainMatrix:是所有样本数据矩阵,每行是一个样本,一列代表一个词条
输入参数trainCategory:是所有样本对应的分类标签,是一个向量,维数等于矩阵的行数
输出参数p0Vect:是一个向量,维数与上面相同,每个元素表示对应样本属于侮辱类的概率
输出参数p1Vect:是一个向量,和上面那个向量互补(因为是二分类问题),每个元素对应样本属于非侮辱类的概率
输出参数pAbusive:是一个概率值,表示这篇文档(所有样本的综合)属于侮辱类的概率
"""
def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix)                         
    numWords = len(trainMatrix[0])                          
    pAbusive = sum(trainCategory)/float(numTrainDocs)       
    p0Num = np.ones(numWords);p1Num = np.ones(numWords)    #词条初始化次数为1,避免出现0的情况,拉普拉斯平滑第一步
    p0Denom = 2.0; p1Denom = 2.0                            #分母初始化为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 = np.log(p1Num/p1Denom)                           #相除,然后取对数,防止下溢出
    p0Vect = np.log(p0Num/p0Denom)
    return p0Vect,p1Vect,pAbusive                           

上图中,我只对有更改的地方写了注释,其他地方,你们可以看上面的,避免看的干扰

(2)这一步更改函数5,因为取对数后,我们最后的乘积方式也要改变一下

"""
函数5:朴素贝叶斯分类器分类函数
函数说明: 利用几个函数得到的结果,直接对vec2Classify进行分类,说白了就是利用贝叶斯定理直接求了
但是这里并没有求分母,因为要比较的概率中分母都一样,可以不用求分母
参数说明:
输入参数vec2Classify——要分类的向量
输入参数后面三个——就是函数3得到的三个输出向量
输入参数:就是分类结果了(因为分类标签就只有2个,如果更改数据的话,这里要改一下)
"""
def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):
    p1 = sum(vec2Classify*p1Vec) + np.log(pClass1)     #对应元素相乘,log(A*B)=logA + logB
    p0 =sum(vec2Classify*p0Vec) + np.log(1.0-pClass1)
    print('p0:',p0)
    print('p1:',p1)
    if p1>p0:
        return 1
    else:
        return 0

(3)好了,这里再贴一下最终改版:

# -*- coding: UTF-8 -*-
import numpy as np
from functools import reduce

"""
函数1:创建实验样本
功能说明:首先要将文本切分成词条,这个函数就是干这个用的不过,现在已经切好了
返回值说明:postingList就是词条,classVec则是词条对应的分类标签
"""
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                                                         #返回实验样本切分的词条和类别标签向量


"""
函数2: 制作词汇表
函数说明:将切分的实验样本词条整理成不重复的词条列表,也就是词汇表
参数说明:dataSet就是上面的postingList,也就是重复的词条样本集,而vocabSet则是无重复的词汇表
"""
def createVocabList(dataSet):
	vocabSet = set([])  					#创建一个空的不重复列表
	for document in dataSet:
		vocabSet = vocabSet | set(document) #取并集
	return list(vocabSet)

"""
函数3:词汇向量化
函数说明: 根据vocabList词汇表(也就是上面函数制作的词汇表),将inputSet(你输入的词汇)向量化,
          向量的每个元素为1或0,如果词汇表中有这个单词,就置1;没有,就置0
参数说明:最后返回的是文档向量(不是0就是1)
"""
def setOfWords2Vec(vocabList,inputSet):
    returnVec = [0] * len(vocabList)                    # 创建一个其中所含元素都为0的向量
    for word in inputSet:                               #遍历每个词条
        if word in inputSet:                            #如果词条存在于词汇表中,则置1
            returnVec[vocabList.index(word)] = 1
        else:
            print("词汇:%s 并没有在词汇表中" % word)   # 词汇表中没有这个单词,表示出现了问
    return returnVec                                    #返回文档向量


"""
函数4:朴素贝叶斯分类器训练函数
函数说明: 利用朴树贝叶斯求出分类概率,也可以说是求出先验概率
参数说明:
输入参数trainMatrix:是所有样本数据矩阵,每行是一个样本,一列代表一个词条
输入参数trainCategory:是所有样本对应的分类标签,是一个向量,维数等于矩阵的行数
输出参数p0Vect:是一个向量,维数与上面相同,每个元素表示对应样本属于侮辱类的概率
输出参数p1Vect:是一个向量,和上面那个向量互补(因为是二分类问题),每个元素对应样本属于非侮辱类的概率
输出参数pAbusive:是一个概率值,表示这篇文档(所有样本的综合)属于侮辱类的概率
"""
def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix)                         #训练集中样本数量
    numWords = len(trainMatrix[0])                          #每条样本中的词条数量
    pAbusive = sum(trainCategory)/float(numTrainDocs)       #文档属于侮辱类的概率
    p0Num = np.ones(numWords);p1Num = np.ones(numWords)    #词条初始化次数为1,避免出现0的情况,拉普拉斯平滑第一步
    p0Denom = 2.0; p1Denom = 2.0                            #分母初始化为2.0,拉普拉斯平滑第二步
    for i in range(numTrainDocs):                           #对每个标签进行判断
        if trainCategory[i] == 1:                           #统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)···
            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 = np.log(p1Num/p1Denom)                           #相除,然后取对数,防止下溢出
    p0Vect = np.log(p0Num/p0Denom)
    return p0Vect,p1Vect,pAbusive                           #返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率


"""
函数5:朴素贝叶斯分类器分类函数
函数说明: 利用几个函数得到的结果,直接对vec2Classify进行分类,说白了就是利用贝叶斯定理直接求了
但是这里并没有求分母,因为要比较的概率中分母都一样,可以不用求分母
参数说明:
输入参数vec2Classify——要分类的向量
输入参数后面三个——就是函数3得到的三个输出向量
输入参数:就是分类结果了(因为分类标签就只有2个,如果更改数据的话,这里要改一下)
"""
def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):
    p1 = sum(vec2Classify*p1Vec) + np.log(pClass1)     #对应元素相乘,log(A*B)=logA + logB
    p0 =sum(vec2Classify*p0Vec) + np.log(1.0-pClass1)
    print('p0:',p0)
    print('p1:',p1)
    if p1>p0:
        return 1
    else:
        return 0

"""
函数6:测试朴素贝叶斯分类器
函数说明: 这个就是一个测试函数了
"""
def testingNB():
    listOPosts,listClasses = loadDataSet()                                  #创建实验样本
    myVocabList = createVocabList(listOPosts)                               #创建词汇表
    trainMat=[]
    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))             #将实验样本向量化

    p0V,p1V,pAb = trainNB0(np.array(trainMat),np.array(listClasses))        #训练朴素贝叶斯分类器
    testEntry = ['love', 'my', 'dalmation']                                 #测试样本1
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))              #测试样本向量化
    if classifyNB(thisDoc,p0V,p1V,pAb):
        print(testEntry,'属于侮辱类')                                        #执行分类并打印分类结果
    else:
        print(testEntry,'属于非侮辱类')                                       #执行分类并打印分类结果
    testEntry = ['stupid', 'garbage']                                       #测试样本2

    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))              #测试样本向量化
    if classifyNB(thisDoc,p0V,p1V,pAb):
        print(testEntry,'属于侮辱类')                                        #执行分类并打印分类结果
    else:
        print(testEntry,'属于非侮辱类')                                       #执行分类并打印分类结果

if __name__ == '__main__':
	testingNB()

然后输出结果:


这里输出为什么是负数呢?因为我们最后都采用了对数,那几个相乘的元素都是小于1的,自然是负数,负数相加最后自然也是负数了!这个问题可以参考上面的ln函数图!


几个注意问题:

  • 上面那个拉普拉斯平滑时,分母初始化为2,并不是所有的问题都是初始化为2,这个要根据你的数据集的类别数量来进行修改,这个只有两个类别:侮辱和非侮辱,所以分母是2,如果类别是3,则就要改为3了,这个问题可以参考西瓜书,上面有详细的公式解释
  • 这个算法虽然已经算是完善了,但是还有一步没有搞定,那就数据集的读取问题,我们上面最终版依然还是自己在代码内部写的数据集,这样还是不行的,下一个博客,我们再进行修改完善,同时也再实现几个例子

三、遇到的问题

1、IndentationError: unindent does not match any outer indentation level

截图如下:


说白了,就是粘贴复制代码的时候,有些tab键和空格搞混了,缩进的问题,具体解决方案:解决问题

当然,我用的是Pycharm,所以上面那个解决方面没有什么用,我直接将那个出问题的代码重新自己写了一遍,这才搞定了,以后如果遇到解决方案,会在这里写出来的。

四、资源

五、资源参考

1、西瓜书:链接:https://pan.baidu.com/s/1ozCZb-912fB2auyAGRcNmw 密码:3wg2

2、首先自然是《机器学习实战》这本电子书了,链接为:链接:https://pan.baidu.com/s/1nfJuwI2JQ6OAjM5Jbi7MOg 密码:l5xv(高清彩色版本)

3、这本书附带的源代码与数据集(这里的源代码是这本书附带的,不是我上面写的,有一部分是Python2格式):链接:https://pan.baidu.com/s/1mDqTlRVPAZBkok4E7ToVHQ  密码:162r

4、一些参考书:

用Python做科学计算:链接:https://pan.baidu.com/s/1hEwKT4k3jAqEDslla2L1eQ 密码:0k5l

笨方法学Python:链接:https://pan.baidu.com/s/1MKCKoRZjPV0Q4rQoa2LEpg 密码:enfk

流畅的Python:链接:https://pan.baidu.com/s/1Ln28HA3ITarp4sCPtT85VA 密码:elpc


下一篇博客,再继续完善朴素贝叶斯算法,主要是考虑一篇博客太长的话,很容易看一半就看不下去了,所以这一章就分成两部分来写!


猜你喜欢

转载自blog.csdn.net/yuangan1529/article/details/81006219