《机器学习实战》-04 朴素贝叶斯

说明:

作业的所有代码都要基于Python3
学习大纲:https://blog.csdn.net/qq_34243930/article/details/84669684
(所有计划均在学习大纲里)

04 朴素贝叶斯(第二周)

4.1 基于贝叶斯决策理论的分类方法
4.2 条件概率
4.3 使用条件概率来分类
4.4 使用朴素贝叶斯进行文档分类
4.5 使用 Python 进行文本分类
4.6 示例:使用朴素贝叶斯过滤垃圾邮件
4.7 示例:使用朴素贝叶斯分类器从个人广告中获取区域倾向

学习内容

学习时间: 12/11-12/14
任务1题目: 书籍阅读
任务详解: 阅读《机器学习实战》书籍第四章
参考文献: 李航《统计学习方法》第4章
参考资料: [通俗易懂!白话朴素贝叶斯]
( https://mp.weixin.qq.com/s/7xRyZJpXmeB77MZNLqVf3w)
作业1: 概括朴素贝叶斯分类算法的原理,为什么称之为“朴素”?
作业2:
试由下表的训练数据学习一个朴素贝叶斯分类器并确定x=(2,S)的类标记y。表中X1和X2为特征。
《机器学习实战》-04 朴素贝叶斯_第1张图片
作业3: 将本章中“使用朴素贝叶斯过滤垃圾邮件”完整代码键入,并添加详细注释。
作业4: 将本章中“使用朴素贝叶斯分类器从个人广告中获取区域倾向”完整代码键入,并添加详细注释。

[通俗易懂!白话朴素贝叶斯]

( https://mp.weixin.qq.com/s/7xRyZJpXmeB77MZNLqVf3w)

用买瓜问题来理解先验概率、后验概率、联合概率、全概率

1、先验概率:
如果我对这个西瓜没有任何了解,包括瓜的颜色、形状、瓜蒂是否脱落。按常理来说,西瓜成熟的概率大概是 60%。那么,这个概率 P(瓜熟) 就被称为先验概率
也就是说,先验概率是根据以往经验和分析得到的概率,先验概率无需样本数据,不受任何条件的影响。 就像只根据常识而不根据西瓜状态来判断西瓜是否成熟,这就是先验概率。
2、后验概率:
以前学到了一个判断西瓜是否成熟的常识,就是看瓜蒂是否脱落。一般来说,瓜蒂脱落的情况下,西瓜成熟的概率大一些,大概是 75%。如果把瓜蒂脱落当作一种结果,然后去推测西瓜成熟的概率,这个概率 P(瓜熟 | 瓜蒂脱落) 就被称为后验概率后验概率类似于条件概率
3、联合概率:
买西瓜的例子中,P(瓜熟,瓜蒂脱落) 称之为联合分布,它表示瓜熟了且瓜蒂脱落的概率。关于联合概率,满足下列乘法等式:
在这里插入图片描述
其中,P(瓜熟 | 瓜蒂脱落) 就是刚刚介绍的后验概率,表示在“瓜蒂脱落”的条件下,“瓜熟”的概率P(瓜蒂脱落 | 瓜熟) 表示在“瓜熟”的情况下,“瓜蒂脱落”的概率
4、全概率公式:
接着想如何计算瓜蒂脱落的概率呢?实际上可以分成两种情况:一种是瓜熟状态下瓜蒂脱落的概率,另一种是瓜生状态下瓜蒂脱落的概率。瓜蒂脱落的概率就是这两种情况之和。因此,我们就推导出了全概率公式
在这里插入图片描述

单个特征判断瓜熟

如果我现在挑到了一个瓜蒂脱落的瓜,则该瓜是好瓜的概率多大?
显然,这是一个计算后验概率的问题,根据我们上面推导的联合概率和全概率公式,可以求出:
在这里插入图片描述
注意,以上这种计算后验概率的公式就是利用贝叶斯定理。不知不觉,可以说你已经掌握了贝叶斯定理的思想了。

多个特征判断瓜熟

在网上搜索了一下。知道判断一个瓜是否熟了,除了要看瓜蒂是否脱落,还要看瓜的形状颜色。形状有圆和尖之分,颜色有深绿、浅绿、青色之分。
现在,特征由原来的 1 个,变成现在的 3 个,我们用 X 表示特征,用 Y 表示瓜的类型(瓜熟还是瓜生)。则根据贝叶斯定理,后验概率 P(Y=ck | X=x) 的表达式为:
在这里插入图片描述
其中,ck 表示类别,k 为类别个数。
本例中,k = 1,2;c1 表示瓜熟,c2 表示瓜生。
有一点需要注意,这里的特征 X 不再是单一的,而是包含了 3 个特征。因此,条件概率 P(X=x | Y=ck) 假设各个条件相互独立,也就是说假设不同特征之间是相互独立的。这样,P(X=x | Y=ck) 就可以写成:
在这里插入图片描述
其中,n 为特征个数,j 表示当前所属特征。针对这个例子,P(X=x | Y=ck) 可以写成:
在这里插入图片描述
这种条件独立性的假设就是朴素贝叶斯法“朴素”二字的由来。这一假设让朴素贝叶斯法变得简单,但是有时候会牺牲一定的分类准确率。

这样,利用朴素贝叶斯思想,我们就可以把后验概率写成:
在这里插入图片描述

现在,拿起一个西瓜,观察了它的瓜蒂、形状、颜色三个特征,就能根据上面的朴素贝叶斯公式,分别计算 c1(瓜熟)和 c2(瓜生)的概率,即 P(Y=c1 | X=x) 和 P(Y=c2 | X=x)。然后再比较 P(Y=c1 | X=x) 和 P(Y=c2 | X=x) 值的大小:
若 P(Y=c1 | X=x) > P(Y=c2 | X=x),则判断瓜熟;
若 P(Y=c1 | X=x) < P(Y=c2 | X=x),则判断瓜生。
值得注意的是上式中的分母部分,对于所有的 ck 来说,都是一样的。因此,分母可以省略,不同的 ck,仅比较 P(Y=ck | X=x) 的分子即可:
在这里插入图片描述

朴素贝叶斯分类

买瓜之前,还有一件事情要做,就是搜集样本数据。
《机器学习实战》-04 朴素贝叶斯_第2张图片
其中,瓜蒂分为脱落和未脱,形状分为圆形和尖形,颜色分为深绿、浅绿、青色。不同特征组合对应着瓜熟或者瓜生。

现在,挑了一个西瓜,它的瓜蒂脱落、形状圆形、颜色青色。这时候就完全可以根据样本数据和朴素贝叶斯法来计算后验概率。
首先,对于瓜熟的情况:
(由样本数据可得),
瓜熟的先验概率: P(瓜熟) = 6 / 10 = 0.6。
条件概率: P(脱落 | 瓜熟) = 4 / 6 = 2 / 3。(瓜熟条件下,脱落的)
条件概率: P(圆形 | 瓜熟) = 4 / 6 = 2 / 3。
条件概率: P(青色 | 瓜熟) = 2 / 6 = 1 / 3。

计算后验概率分子部分
P(瓜熟) × P(脱落 | 瓜熟) × P(圆形 | 瓜熟) × P(青色 | 瓜熟) = 0.6 × (2 / 3) × (2 / 3) × (1 / 3) = 4 / 45。

然后,对于瓜生的情况:
瓜生的先验概率: P(瓜生) = 4 / 10 = 0.4。
条件概率: P(脱落 | 瓜生) = 1 / 4 = 0.25。
条件概率: P(圆形 | 瓜生) = 1 / 4 = 0.25。
条件概率: P(青色 | 瓜生) = 1 / 4 = 0.25。

计算后验概率分子部分:
P(瓜生) × P(脱落 | 瓜生) × P(圆形 | 瓜生) × P(青色 | 瓜生) = 0.4 × 0.25 × 0.25 × 0.25 = 1 / 160。

因为 4 / 45 > 1 / 160,所以预测为瓜熟。终于计算完了,这个西瓜瓜蒂脱落、形状圆形、颜色青色,应该是熟瓜。

作业2

试由下表的训练数据学习一个朴素贝叶斯分类器并确定x=(2,S)的类标记y。表中X1和X2为特征。
《机器学习实战》-04 朴素贝叶斯_第3张图片

解答:
和判断瓜熟or瓜生一样的做法。

首先,对于Y=1的情况:
(由样本数据可得),
Y=1的先验概率: P(Y=1) = 10 / 16 = 5 / 8。
条件概率: P(X1=1 | Y=1) = 2 / 10 = 1 / 5。(Y=1条件下,X1=1的)
条件概率: P(X1=2 | Y=1) = 4 / 10 = 2 / 5
条件概率: P(X1=3 | Y=1) = 4 / 10 = 2 / 5。
条件概率: P(X2=S | Y=1) = 2 / 10 = 1 / 5
条件概率: P(X2=M | Y=1) = 4 / 10 = 2 / 5。
条件概率: P(X2=L | Y=1) = 4 / 10 = 2 / 5。

计算后验概率分子部分
P(Y=1) × P(X1=2 |Y=1) × P(X2=S | Y=1) = (5 / 8) × (2 / 5) × (1 / 5) = 4 / 5。

然后,对于Y=-1的情况:
Y=-1的先验概率: P(Y=-1) = 6 / 16 = 3 / 8。
条件概率: P(X1=1 | Y=-1) = 3 / 10 = 1 / 5。(Y=-1条件下,X1=1的)
条件概率: P(X1=2 | Y=-1) = 2 / 10 = 1 / 5
条件概率: P(X1=3 | Y=1) = 1 / 10 。
条件概率: **P(X2=S | Y=-1) = 3 / 10 **。
条件概率: P(X2=M | Y=-1) = 2 / 10 = 1 / 5。
条件概率: P(X2=L | Y=-1) = 1 / 10 。

计算后验概率分子部分
P(Y=-1) × P(X1=2 |Y=-1) × P(X2=S | Y=-1) = (3 / 8) × (1 / 5) × (3 / 10) = 9 / 400。
因为 4 / 5 > 9 / 400,所以预测类别y=1。

《第4章-基于概率论的分类方法:朴素贝叶斯》

《机器学习实战》-04 朴素贝叶斯_第4张图片
本章会给出一些使用概率论进行分类的方法。首先从一个最简单的概率分类器开始,然后给出一些假设来学习朴素贝叶斯分类器。我们称之为“朴素”,是因为整个形式化过程只做最原始、最简单的假设。不必担心,你会详细了解到这些假设。
我们将充分利用Python的文本处理能力将文档切分成词向量,然后利用词向量对文档进行分类。
我们还将构建另一个分类器,观察其在真实的垃圾邮件数据集中的过滤效果,必要时还会回顾一下条件概率。
最后,我们将介绍如何从个人发布的大量广告中学习分类器,并将学习结果转换成人类可理解的信息。

4.1 基于贝叶斯决策理论的分类方法

《机器学习实战》-04 朴素贝叶斯_第5张图片
朴素贝叶斯是贝叶斯决策理论的一部分,所以讲述朴素贝叶斯之前有必要快速了解一下贝叶斯决策理论
假设现在我们有一个数据集,它由两类数据组成,数据分布如图4-1所示。
《机器学习实战》-04 朴素贝叶斯_第6张图片
假设有位读者找到了描述图中两类数据的统计参数。(暂且不用管如何找到描述这类数据的统计参数,第10章会详细介绍。)我们现在用 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。
也就是说,我们会选择高概率对应的类别。这就是贝叶斯决策理论的核心思想,即选择具有最高概率的决策。

4.2 条件概率

(这里书上讲的不是很好)
《机器学习实战》-04 朴素贝叶斯_第7张图片
要计算 P(gray) 或者 P(black) ,事先得知道石头所在桶的信息会不会改变结果?你有可能已经想到计算从B桶中取到灰色石头的概率的办法,这就是所谓的条件概率(conditionalprobability)。假定计算的是从B桶取到灰色石头的概率,这个概率可以记作 P(gray|bucketB) ,
我们称之为“在已知石头出自B桶的条件下,取出灰色石头的概率”。
不难得到, P(gray|bucketA)值为2/4, P(gray|bucketB) 的值为1/3。
《机器学习实战》-04 朴素贝叶斯_第8张图片
另一种有效计算条件概率的方法称为贝叶斯准则。贝叶斯准则告诉我们如何交换条件概率中的条件与结果,即如果已知 P(x|c) ,要求 P(c|x) ,那么可以使用下面的计算方法:
在这里插入图片描述

4.3 使用条件概率来分类

在这里插入图片描述
但这两个准则并不是贝叶斯决策理论的所有内容。使用 p1( ) 和 p2( ) 只是为了尽可能简化描述,而真正需要计算和比较的是 p(c 1 |x, y) 和 p(c 2 |x, y) 。
这些符号所代表的具体意义是:
给定某个由x、y表示的数据点,那么该数据点来自类别 c 1 的概率是多少?数据点来自类别 c 2 的概率又是多少?
注意这些概率与刚才给出的概率 p(x, y|c 1 ) 并不一样,不过可以使用贝叶斯准则来交换概率中条件与结果。具体地,应用贝叶斯准则得到:
在这里插入图片描述
使用这些定义,可以定义贝叶斯分类准则为:
 如果 P(c1|x, y) > P(c2|x, y) ,那么属于类别 c1 。
 如果 P(c1|x, y) < P(c2|x, y) ,那么属于类别 c2 。
使用贝叶斯准则,可以通过已知的三个概率值来计算未知的概率值。

4.4 使用朴素贝叶斯进行文档分类

机器学习的一个重要应用就是文档的自动分类。
在文档分类中,整个文档(如一封电子邮件)是实例,而电子邮件中的某些元素则构成特征。虽然电子邮件是一种会不断增加的文本,但我们同样也可以对新闻报道、用户留言、政府公文等其他任意类型的文本进行分类。我们可以观察文档中出现的词,并把每个词的出现或者不出现作为一个特征,这样得到的特征数目就会跟词汇表中的词目一样多。
朴素贝叶斯是上节介绍的贝叶斯分类器的一个扩展,是用于文档分类的常用算法。
使用每个词作为特征并观察它们是否出现,这样得到的特征数目会有多少呢?
《机器学习实战》-04 朴素贝叶斯_第9张图片
假设词汇表中有1000个单词。要得到好的概率分布,就需要足够的数据样本,假定样本数为N。
前面讲到的约会网站示例中有1000个实例,手写识别示例中每个数字有200个样本,而决策树示例中有24个样本。其中,24个样本有点少,200个样本好一些,而1000个样本就非常好了。
约会网站例子中有三个特征。由统计学知,如果每个特征需要N个样本,那么对于10个特征将需要N 10 个样本,对于包含1000个特征的词汇表将需要 N 1000 N^{1000} N1000个样本。可以看到,所需要的样本数会随
着特征数目增大而迅速增长。
如果特征之间相互独立,那么样本数就可以从 N 1000 N^{1000} N1000减少到1000×N。
所谓独立(independence) 指的是统计意义上的独立,即一个特征或者单词出现的可能性与它和其他单词相邻没有关系。举
个例子讲,假设单词bacon出现在unhealthy后面与出现在delicious后面的概率相同。(当然,我们知道这种假设并不正确,bacon常常出现在delicious附近,而很少出现在unhealthy附近),这个假设正是朴素贝叶斯分类器中朴素(naive)一词的含义。
朴素贝叶斯分类器中的另一个假设是,每个特征同等重要 。 其实这个假设也有问题。 如果要判断留言板的留言是否得当,那么可能不需要看完所有的1000个单词,而只需要看10~20个特征就足以做出判断了。尽管上述假设存在一些小的瑕疵,但朴素贝叶斯的实际效果却很好。

4.5 使用 Python 进行文本分类

要从文本中获取特征,需要先拆分文本。 具体如何做呢?这里的特征是来自文本的词条(token),一个词条是字符的任意组合。可以把词条想象为单词,也可以使用非单词词条,如URL、IP地址或者任意其他字符串。然后将每一个文本片段表示为一个词条向量,其中值为1表示词条出现在文档中,0表示词条未出现。
以在线社区的留言板为例。为了不影响社区的发展,我们要屏蔽侮辱性的言论,所以要构建一个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么就将该留言标识为内容不当。过滤这类内容是一个很常见的需求。对此问题建立两个类别:侮辱类和非侮辱类,使用1和0分别表示。
接下来首先给出将文本转换为数字向量的过程,然后介绍如何基于这些向量来计算条件概率并在此基础上构建分类器,最后还要介绍一些利用Python实现朴素贝叶斯过程中需要考虑的问题。

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

我们将把文本看成单词向量或者词条向量,也就是说将句子转换为向量。考虑出现在所有文档中的所有单词,再决定将哪些词纳入词汇表或者说所要的词汇集合,然后必须要将每一篇文档转换为词汇表上的向量。
接下来我们正式开始。打开文本编辑器,创建一个叫bayes.py的新文件,然后将下面的程序清单添加到文件中。
《机器学习实战》-04 朴素贝叶斯_第10张图片

"""
Created on Dec 11, 2018
"bayes.py"
@author: xpt
"""
def loadDataSet():
    """
    函数 loadDataSet() 创建了一些实验样本
    :return postingList:进行词条切分后的文档集合
    :return classVec:类别标签的集合,这些文本的类别由人工标注
    """
    # postingList:进行词条切分后的文档集合
    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):
    """
    创建一个包含在所有文档中出现的不重复词的列表
    :param dataSet:dataSet将会是loadDataSet()返回的第一个变量postingList
    :return: list(vocabSet)是一个包含在所有文档中出现的不重复词的列表
    """
    vocabSet = set([])  # 创建一个空集合
    for document in dataSet:  # 将每篇文档返回的新词集合添加到该集合中
        vocabSet = vocabSet | set(document)  # 求两个集合的并集
    return list(vocabSet)


def setOfWords2Vec(vocabList, inputSet):
    """
    该函数的输入参数为词汇表及某个文档,输出的是文档向量
    :param vocabList:词汇表
    :param inputSet:某个文档
    :return:文档向量,向量的每一元素为1或0,分别表示词汇表中的单词在输入文档中是否出现
    """
    returnVec = [0]*len(vocabList)  # 创建一个和词汇表等长的向量,并将其元素都设置为0
    for word in inputSet:  # 遍历文档中的所有单词
        if word in vocabList:  # 如果出现了词汇表中的单词
            returnVec[vocabList.index(word)] = 1  # 则将输出的文档向量中的对应值设为1
        else:
            print("the word: %s is not in my Vocabulary!" % word)
    return returnVec

【1】第一个函数 loadDataSet() 创建了一些实验样本。
该函数返回的第一个变量是进行词条切分后的文档集合,这些文档来自斑点犬爱好者留言板。这些留言文本被切分成一系列的词条集合,标点符号从文本中去掉,后面会探讨文本处理的细节。
loadDataSet( ) 函数返回的第二个变量是一个类别标签的集合。这里有两类,侮辱性和非侮辱性。这些文本的类别由人工标注,这些标注信息用于训练程序以便自动检测侮辱性留言。
【2】下一个函数 createVocabList() 会创建一个包含在所有文档中出现的不重复词的列表,为此使用了Python的 set 数据类型。将词条列表输给 set 构造函数, set 就会返回一个不重复词表。
首先,创建一个空集合 ,然后将每篇文档返回的新词集合添加到该集合中 。操作符 | 用于求两个集合的并集,这也是一个按位或( OR )操作符(参见附录C)。在数学符号表示上,按位或操作与集合求并操作使用相同记号。
单步调试:
《机器学习实战》-04 朴素贝叶斯_第11张图片
《机器学习实战》-04 朴素贝叶斯_第12张图片
《机器学习实战》-04 朴素贝叶斯_第13张图片
《机器学习实战》-04 朴素贝叶斯_第14张图片
【3】获得词汇表后,便可以使用函数 setOfWords2Vec() ,该函数的输入参数为词汇表及某个文档,输出的是文档向量,向量的每一元素为1或0,分别表示词汇表中的单词在输入文档中是否出现。
函数首先创建一个和词汇表等长的向量,并将其元素都设置为0 。
接着,遍历文档中的所有单词,如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1。一切都顺利的话,就不需要检查某个词是否还在 vocabList 中,后边可能会用到这一操作。

现在看一下这些函数的执行效果,保存 bayes.py 文件,然后在Python提示符下输入:
《机器学习实战》-04 朴素贝叶斯_第15张图片

"""
Created on Dec 11, 2018
"bayes_test.py"
@author: xpt
"""
import bayes

listOPosts, listClasses = bayes.loadDataSet()
myVocabList = bayes.createVocabList(listOPosts)
print(myVocabList)

《机器学习实战》-04 朴素贝叶斯_第16张图片
检查上述词表,就会发现这里不会出现重复的单词。目前该词表还没有排序,需要的话,稍后可以对其排序。

下面看一下函数 setOfWords2Vec() 的运行效果:
《机器学习实战》-04 朴素贝叶斯_第17张图片

"""
Created on Dec 11, 2018
"bayes_test.py"
@author: xpt
"""
import bayes

listOPosts, listClasses = bayes.loadDataSet()
myVocabList = bayes.createVocabList(listOPosts)
# print(myVocabList)
print(bayes.setOfWords2Vec(myVocabList, listOPosts[0]))  # 输入文档为listOPosts中第1个文档

print(bayes.setOfWords2Vec(myVocabList, listOPosts[3]))  # 输入文档为listOPosts中第4个文档

《机器学习实战》-04 朴素贝叶斯_第18张图片
该函数使用词汇表或者想要检查的所有单词作为输入,然后为其中每一个单词构建一个特征。一旦给定一篇文档(斑点犬网站上的一条留言),该文档就会被转换为词向量。
接下来检查一下函数的有效性。 myVocabList 中索引为2的元素是什么单词?应该是单词help。该单词在第一篇文档中出现,现在检查一下看看它是否出现在第四篇文档中(没有出现)。
《机器学习实战》-04 朴素贝叶斯_第19张图片
在这里插入图片描述
在这里插入图片描述

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

前面介绍了如何将一组单词转换为一组数字,接下来看看如何使用这些数字计算概率。现在已经知道一个词是否出现在一篇文档中,也知道该文档所属的类别。还记得3.2节提到的贝叶斯准则?我们重写贝叶斯准则,将之前的x、y 替换为w。粗体w表示这是一个向量,即它由多个数值组成。在这个例子中,数值个数与词汇表中的词个数相同。
在这里插入图片描述
我们将使用上述公式,对每个类计算该值,然后比较这两个概率值的大小。如何计算呢?
首先可以通过类别 i (侮辱性留言或非侮辱性留言)中文档数除以总的文档数来计算概率 p ( c i ) p\left ( c_{i} \right ) p(ci)
接下来计算 p ( w → ∣ c i ) p\left ( \overrightarrow{w}|c_{i} \right ) p(w ci) ,这里就要用到朴素贝叶斯假设。
p ( w → ∣ c i ) p\left ( \overrightarrow{w}|c_{i} \right ) p(w ci) 就是
在这里插入图片描述
如果将 w 展开为一个个独立特征,那么就可以将上述概率写作 p ( w 0 , w 1 , w 2 . . . . w N , ∣ c i ) p\left ( w_{0},w_{1},w_{2}^....w_{N},|c_{i} \right ) p(w0,w1,w2....wN,ci)。这里假设所有词都互相独立,该假设也称作条件独立性
假设,它意味着可以使用 p ( w 0 ∣ c i ) p ( w 1 ∣ c i ) p ( w 2 ∣ c i ) . . . p ( w N ∣ c i ) p\left ( w_{0}|c_{i} \right )p\left ( w_{1}|c_{i} \right )p\left ( w_{2}|c_{i} \right )...p\left ( w_{N}|c_{i} \right ) p(w0ci)p(w1ci)p(w2ci)...p(wNci) 来计算上述概率,这就极大地简化了计算的过程。
该函数的伪代码如下:
《机器学习实战》-04 朴素贝叶斯_第20张图片
我们利用下面的代码来实现上述伪码。打开文本编辑器,将这些代码添加到 bayes.py 文件中。该函数使用了NumPy的一些函数,故应确保将 from numpy import * 语句添加到 bayes.py文件的最前面。
《机器学习实战》-04 朴素贝叶斯_第21张图片

import numpy as np

def trainNB0(trainMatrix, trainCategory):
    """

    :param trainMatrix:文档矩阵trainMatrix是函数setOfWords2Vec() 返回的文档向量的总和
    :param trainCategory:由每篇文档类别标签所构成的向量,也就是loadDataSet()中的classVec:类别标签的集合,这些文本的类别由人工标注
    :return p0Vect, p1Vect:两个类别的概率向量
    :return pAbusive:一个概率,就是任意文档属于侮辱性文档的概率,trainCategory(即classVec)中所有1累加占的比例。
    """
    numTrainDocs = len(trainMatrix)
    numWords = len(trainMatrix[0])
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    p0Num = np.zeros(numWords); p1Num = np.zeros(numWords)      
    p0Denom = 0.0; p1Denom = 0.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 = p1Num/p1Denom        
    p0Vect = p0Num/p0Denom       
    return p0Vect, p1Vect, pAbusive

代码函数中的输入参数为文档矩阵 trainMatrix (trainMatrix 是函数setOfWords2Vec() 返回的文档向量的总和,下面将会讲到),以及由每篇文档类别标签所构成的向量trainCategory 。
【1】首先,计算文档属于侮辱性文档( class=1 )的概率,即 P(1) 。因为这是一个二类分类问题,所以可以通过 1-P(1) 得到 P(0) 。对于多于两类的分类问题,则需要对代码稍加修改。
计算 p ( w i ∣ c 1 ) p\left ( w_{i}|c_{1} \right ) p(wic1) p ( w i ∣ c 0 ) p\left ( w_{i}|c_{0} \right ) p(wic0),需要初始化程序中的分子变量和分母变量 。
【2】由于w中元素如此众多,因此可以使用NumPy数组快速计算这些值。上述程序中的分母变量是一个元素个数等于词汇表大小的NumPy数组。在 for 循环中,要遍历训练集 trainMatrix 中的所有文档。一旦某个词语(侮辱性或正常词语)在某一文档中出现,则该词对应的个数( p1Num 或者 p0Num )就加1,而且在所有的文档中,该文档的总词数也相应加1
【3】对于两个类别都要进行同样的计算处理。最后,对每个元素除以该类别中的总词数 。利用NumPy可以很好实现,用一个数组除以浮点数即可,若使用常规的Python列表则难以完成这种任务,读者可以自己尝试一下。最后,函数会返回两个向量和一个概率(PAbusive:侮辱性文档的概率)。

接下来试验一下。将程序清单4-2中的代码添加到bayes.py文件中,在Python提示符下输入:
在这里插入图片描述
该(上面)语句从预先加载值中调入数据。
在这里插入图片描述
至此我们构建了一个包含所有词的列表 myVocabList 。
在这里插入图片描述
该 for 循环使用词向量来填充 trainMat 列表。
下面给出属于侮辱性文档的概率以及两个类别的概率向量。
在这里插入图片描述
接下来看这些变量的内部值:
这就是任意文档属于侮辱性文档的概率:
在这里插入图片描述
《机器学习实战》-04 朴素贝叶斯_第22张图片

"""
Created on Dec 11, 2018
"bayes_test.py"
@author: xpt
"""
import bayes

listOPosts, listClasses = bayes.loadDataSet()
myVocabList = bayes.createVocabList(listOPosts)
# print(myVocabList)
# print(bayes.setOfWords2Vec(myVocabList, listOPosts[0]))  # 输入文档为listOPosts中第1个文档
# print(bayes.setOfWords2Vec(myVocabList, listOPosts[3]))  # 输入文档为listOPosts中第4个文档
trainMat = []
for postinDoc in listOPosts:
    trainMat.append(bayes.setOfWords2Vec(myVocabList, postinDoc))
p0V, p1V, pAb = bayes.trainNB0(trainMat, listClasses)
print(pAb)
print(p0V)
print(p1V)

《机器学习实战》-04 朴素贝叶斯_第23张图片
首先,我们发现文档属于侮辱类的概率 pAb 为0.5,该值是正确的。
接下来,看一看在给定文档类别条件下词汇表中单词的出现概率,看看是否正确。词汇表中的第一个词是cute,其在类别0中出现1次,而在类别1中从未出现。对应的条件概率分别为0.041 666 67与0.0。该计算是正确的。
我们找找所有概率中的最大值,该值出现在 P(1) 数组第26个下标位置,大小为0.157 894 74。在myVocabList 的第26个下标位置上可以查到该单词是stupid。这意味着stupid是最能表征类别1 (侮辱性文档类)的单词。

使用该函数进行分类之前,还需解决函数中的一些缺陷。

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

【1】利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,即计算 p ( w 0 ∣ 1 ) p\left ( w_{0}|1 \right ) p(w01) p ( w 1 ∣ 1 ) p\left ( w_{1}|1 \right ) p(w11) p ( w 2 ∣ 1 ) p\left ( w_{2}|1 \right ) p(w21)。如果其中一个概率值为0,那么最后的乘积也为0。为降低这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。
在文本编辑器中打开bayes.py文件,并将 trainNB0() 的第4行和第5行修改为:
在这里插入图片描述
【2】另一个遇到的问题是下溢出,这是由于太多很小的数相乘造成的。当计算乘积 p ( w 0 ∣ c i ) p ( w 1 ∣ c i ) p ( w 2 ∣ c i ) . . . p ( w N ∣ c i ) p\left ( w_{0}|c_{i} \right )p\left ( w_{1}|c_{i} \right )p\left ( w_{2}|c_{i} \right )...p\left ( w_{N}|c_{i} \right ) p(w0ci)p(w1ci)p(w2ci)...p(wNci)时,由于大部分因子都非常小,所以程序会下溢出或者得到不正确的答案。(读者可以用Python尝试相乘许多很小的数,最后四舍五入后会得到0。)
一种解决办法是对乘积取自然对数。在代数中有 ln(a*b) = ln(a)+ln(b) ,于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。
《机器学习实战》-04 朴素贝叶斯_第24张图片
图4-4给出函数 f(x) 与 ln(f(x)) 的曲线。检查这两条曲线,就会发现它们在相同区域内同时增加或者减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。通过修改 return前的两行代码,将上述做法用到分类器中:
在这里插入图片描述
修改如下:

import numpy as np

def trainNB0(trainMatrix, trainCategory):
    """
    :param trainMatrix:文档矩阵trainMatrix是函数setOfWords2Vec() 返回的文档向量的总和
    :param trainCategory:由每篇文档类别标签所构成的向量,也就是loadDataSet()中的classVec:类别标签的集合,这些文本的类别由人工标注
    :return p0Vect, p1Vect:两个类别的概率向量
    :return pAbusive:一个概率,就是任意文档属于侮辱性文档的概率,trainCategory(即classVec)中所有1累加占的比例。
    """
    numTrainDocs = len(trainMatrix)
    numWords = len(trainMatrix[0])
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    p0Num = np.zeros(numWords); p1Num = np.zeros(numWords)      #change to np.ones()
    p0Denom = 0.0; p1Denom = 0.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 = p1Num/p1Denom         #change to np.log()
    p0Vect = p0Num/p0Denom          #change to np.log()
    return p0Vect, p1Vect, pAbusive

现在已经准备好构建完整的分类器了。当使用NumPy向量处理功能时,这一切变得十分简单。
打开文本编辑器,将下面的代码添加到bayes.py中:
《机器学习实战》-04 朴素贝叶斯_第25张图片

def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    """
    :param vec2Classify: 要分类的向量 vec2Classify
    :param p0Vec,p1Vec,pClass1:使用函数 trainNB0() 计算得到的三个概率
    :return:返回大概率对应的类别标签
    """
    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():
    """
    封装函数,输出预测结果
    :return: 输出预测类别标签
    """
    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']
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
    print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))
    
    testEntry = ['stupid', 'garbage']
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
    print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))

【1】程序清单4-3的代码有4个输入:要分类的向量 vec2Classify 以及使用函数 trainNB0() 计算得到的三个概率。
使用NumPy的数组来计算两个向量相乘的结果 。这里的相乘是指对应元素
相乘,即先将两个向量中的第1个元素相乘,然后将第2个元素相乘,以此类推vec2Classify * p1Vec我认为是将每个词与其对应的概率相关联起来!!)。接下来将词汇表中所有词的对应值相加,然后将该值加到类别的对数概率上。最后,比较类别的概率返回大概率对应的类别标签。
【2】代码的第二个函数是一个便利函数(convenience function),该函数封装所有操作,以节省输入4.3.1节中代码的时间。

下面来看看实际结果。将程序清单4-3中的代码添加之后,在Python提示符下输入:
《机器学习实战》-04 朴素贝叶斯_第26张图片

"""
Created on Dec 11, 2018
"bayes_test.py"
@author: xpt
"""
import bayes

print(bayes.testingNB())

《机器学习实战》-04 朴素贝叶斯_第27张图片
这个例子非常简单,但是它展示了朴素贝叶斯分类器的工作原理。接下来,我们会对代码做些修改,使分类器工作得更好。

4.5.4 准备数据:文档词袋模型

目前为止,我们将每个词的出现与否作为一个特征,这可以被描述为词集模型(set-of-words model)
如果一个词在文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为词袋模型(bag-of-words model)
在词袋中,每个单词可以出现多次,而在词集中,每个词只能出现一次。为适应词袋模型,需要对函数 setOfWords2Vec()
稍加修改,修改后的函数称为 bagOfWords2Vec() 。
下面的程序清单给出了基于词袋模型的朴素贝叶斯代码。它与函数setOfWords2Vec() 几乎完全相同,唯一不同的是每当遇到一个单词时,它会增加词向量中的对应值,而不只是将对应的数值设为1。
《机器学习实战》-04 朴素贝叶斯_第28张图片

def bagOfWords2VecMN(vocabList, inputSet):
    returnVec = [0]*len(vocabList)
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] += 1
    return returnVec

现在分类器已经构建好了,下面我们将利用该分类器来过滤垃圾邮件。

4.5 Python 语法解析

1、Python set() 函数

set() 函数创建一个无序不重复元素集,可进行关系测试,删除重复数据,还可以计算交集、差集、并集等。
set 语法:class set([iterable])
例如:vocabSet = set([]) 创建空列表
set举例见 https://blog.csdn.net/qq_34243930/article/details/83748085#t27
中集合的用法
操作符 | 用于求两个集合的并集

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

在前面那个简单的例子中,我们引入了字符串列表。使用朴素贝叶斯解决一些现实生活中的问题时,需要先从文本内容得到字符串列表,然后生成词向量。
下面这个例子中,我们将了解朴素贝叶斯的一个最著名的应用:电子邮件垃圾过滤。首先看一下如何使用通用框架来解决该问题。
《机器学习实战》-04 朴素贝叶斯_第29张图片
下面首先给出
将文本解析为词条的代码
。然后将该代码和前面的分类代码集成为一个函数,该函数在测试分类器的同时会给出错误率。

4.6.1 准备数据:切分文本

前一节介绍了如何创建词向量,并基于这些词向量进行朴素贝叶斯分类的过程。前一节中的词向量是预先给定的,下面介绍如何从文本文档中构建自己的词列表。
对于一个文本字符串,可以使用Python的 string.split() 方法将其切分。下面看看实际的运行效果。在Python提示符下输入:
在这里插入图片描述
《机器学习实战》-04 朴素贝叶斯_第30张图片
可以看到,切分的结果不错,但是标点符号也被当成了词的一部分。可以使用正则表示式来切分句子,其中分隔符是除单词、数字外的任意字符串。
《机器学习实战》-04 朴素贝叶斯_第31张图片
在这里插入图片描述
现在得到了一系列词组成的词表,但是里面的空字符串需要去掉。可以计算每个字符串的长度,只返回长度大于0的字符串。
在这里插入图片描述
《机器学习实战》-04 朴素贝叶斯_第32张图片
最后,我们发现句子中的第一个单词是大写的。如果目的是句子查找,那么这个特点会很有用。但这里的文本只看成词袋,所以我们希望所有词的形式都是统一的,不论它们出现在句子中间、结尾还是开头。
Python中有一些内嵌的方法,可以将字符串全部转换成小写( .lower() )或者大写( .upper() ),借助这些方法可以达到目的。于是,可以进行如下处理:
在这里插入图片描述
《机器学习实战》-04 朴素贝叶斯_第33张图片
现在来看数据集中一封完整的电子邮件的实际处理结果。该数据集放在email文件夹中,该文件夹又包含两个子文件夹,分别是spam与ham。
《机器学习实战》-04 朴素贝叶斯_第34张图片
《机器学习实战》-04 朴素贝叶斯_第35张图片
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
显示太长,不好截图,复制结果如下:
《机器学习实战》-04 朴素贝叶斯_第36张图片
文件夹ham下的6.txt文件非常长,这是某公司告知我他们不再进行某些支持的一封邮件。需要注意的是,由于是URL:answer.py?hl=en&answer=174623 的一部分,因而会出现en和py这样的单词。当对URL进行切分时,会得到很多的词。我们是想去掉这些单词,因此在实现时会过滤掉长度小于3的字符串。本例使用一个通用的文本解析规则来实现这一点。在实际的解析程序中,要用更高级的过滤器来对诸如HTML和URI的对象进行处理。目前,一个URI最终会解析成词汇表中的单词,比如www.whitehouse.gov会被解析为三个单词。文本解析可能是一个相当复杂的过程。接下来将构建一个极其简单的函数,你可以根据情况自行修改。

4.6.2 测试算法:使用朴素贝叶斯进行交叉验证

下面将文本解析器集成到一个完整分类器中。打开文本编辑器,将下面程序清单中的代码添加到bayes.py文件中。
《机器学习实战》-04 朴素贝叶斯_第37张图片

def textParse(bigString):    
    """
    该函数去掉少于两个字符的字符串,并将所有字符串转换为小写
    :param bigString: 接受参数为一个大字符串
    :return: 输出字符串列表
    """
    import re
    listOfTokens = re.split(r'\W+', bigString)
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]


def spamTest():
    """
    对贝叶斯垃圾邮件分类器进行自动化处理
    :return: 
    """
    docList = []; classList = []; fullText = []
    for i in range(1, 26):     # 导入文件夹 spam 与 ham下的文本文件,并将它们解析为词列表
        wordList = textParse(open('email/spam/%d.txt' % i, encoding="ISO-8859-1").read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(1)

        wordList = textParse(open('email/ham/%d.txt' % i, encoding="ISO-8859-1").read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)

    vocabList = createVocabList(docList)  # create vocabulary

    trainingSet = range(50); testSet = []           # 构建一个测试集
    for i in range(10):
        randIndex = int(np.random.uniform(0, len(trainingSet)))  # uniform() 方法将随机生成下一个实数,它在[x,y]范围内
        testSet.append(trainingSet[randIndex])  # 选择出的数字所对应的文档被添加到测试集
        del(list(trainingSet)[randIndex])       # 同时也将其从训练集中剔除

    trainMat = []; trainClasses = []
    #  遍历训练集的所有文档,对每封邮件基于词汇表并使用 setOfWords2Vec()函数来构建词向量
    for docIndex in trainingSet:  # 这些词在 traindNB0() 函数中用于计算分类所需的概率
        trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
        trainClasses.append(classList[docIndex])
    p0V, p1V, pSpam = trainNB0(np.array(trainMat), np.array(trainClasses))

    errorCount = 0
    # 遍历测试集,对其中每封电子邮件进行分类
    for docIndex in testSet:
        wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
        if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
            errorCount += 1    # 如果邮件分类错误,则错误数加1
            print("classification error", docList[docIndex])
    print('the error rate is: ', float(errorCount)/len(testSet))
    # return vocabList, fullText

第一个函数 textParse() 接受一个大字符串并将其解析为字符串列表。该函数去掉少于两个字符的字符串,并将所有字符串转换为小写。
你可以在函数中添加更多的解析操作,但是目前的实现对于我们的应用足够了。
第二个函数 spamTest() 对贝叶斯垃圾邮件分类器进行自动化处理。
【1】导入文件夹 spam 与 ham下的文本文件,并将它们解析为词列表 。
【2】接下来构建一个测试集与一个训练集,两个集合中的邮件都是随机选出的。本例中共有50封电子邮件,并不是很多,其中的10封电子邮件被随机选择为测试集。分类器所需要的概率计算只利用训练集中的文档来完成。Python变量 trainingSet是一个整数列表,其中的值从0到49。接下来,随机选择其中10个文件 。选择出的数字所对应的文档被添加到测试集,同时也将其从训练集中剔除。这种随机选择数据的一部分作为训练集,而剩余部分作为测试集的过程称为留存交叉验证(hold-out cross validation)。假定现在只完成了一次迭代,那么为了更精确地估计分类器的错误率,就应该进行多次迭代后求出平均错误率。
【3】接下来的 for 循环遍历训练集的所有文档,对每封邮件基于词汇表并使用 setOfWords2Vec()函数来构建词向量。这些词在 trainNB0() 函数中用于计算分类所需的概率。
【4】然后遍历测试集,对其中每封电子邮件进行分类 。如果邮件分类错误,则错误数加1,最后给出总的错误百分比。

下面对上述过程进行尝试。输入程序清单4-5的代码之后,在Python提示符下输入:
《机器学习实战》-04 朴素贝叶斯_第38张图片

"""
Created on Dec 11, 2018
"bayes_test.py"
@author: xpt
"""
import bayes

bayes.spamTest()
bayes.spamTest()
bayes.spamTest()
bayes.spamTest()

《机器学习实战》-04 朴素贝叶斯_第39张图片
函数 spamTest() 会输出在10封随机选择的电子邮件上的分类错误率。既然这些电子邮件是随机选择的,所以每次的输出结果可能有些差别。如果发现错误的话,函数会输出错分文档的词表,这样就可以了解到底是哪篇文档发生了错误。如果想要更好地估计错误率,那么就应该将上述过程重复多次,比如说10次,然后求平均值。我这么做了一下,获得的平均错误率为6%。
这里一直出现的错误是将垃圾邮件误判为正常邮件。相比之下,将垃圾邮件误判为正常邮件要比将正常邮件归到垃圾邮件好。为避免错误,有多种方式可以用来修正分类器,这些将在第7章中进行讨论
目前我们已经使用朴素贝叶斯来对文档进行分类,接下来将介绍它的另一个应用。下一个例子还会给出如何解释朴素贝叶斯分类器训练所得到的知识。

4.6 Python 语法解析

1、Python的 string.split() 方法

描述:split()通过指定分隔符对字符串进行切片,如果参数 num 有指定值,则仅分隔 num+1 个子字符串
语法:str.split(str="", num=string.count(str))返回分割后的字符串列表。
参数:
str – 以str为分隔符,默认为所有的空字符,包括空格、换行(\n)、制表符(\t)等。
num – 分割次数。默认为 -1, 即分隔所有。

2、Python的string.lower()/upper()方法

Python中有一些内嵌的方法,可以将字符串全部转换成小写( .lower() )或者大写( .upper() )
语法:
str.lower()返回将字符串中所有大写字符转换为小写后生成的字符串。

str.upper()返回小写字母转为大写字母的字符串。

3、Python 正则表达式 import re

正则表达式是一个特殊的字符序列,它能帮助你方便的检查一个字符串是否与某种模式匹配。
Python 自1.5版本起增加了re 模块,它提供 Perl 风格的正则表达式模式。
re 模块使 Python 语言拥有全部的正则表达式功能。
compile 函数根据一个模式字符串和可选的标志参数生成一个正则表达式对象。该对象拥有一系列方法用于正则表达式匹配和替换。
re 模块也提供了与这些方法功能完全一致的函数,这些函数使用一个模式字符串做为它们的第一个参数。
http://www.runoob.com/python3/python3-reg-expressions.html
关于compile 函数
compile 函数用于编译正则表达式,生成一个正则表达式( Pattern )对象,供 match() 和 search() 这两个函数使用。
语法格式为:re.compile(pattern[, flags])
参数:
pattern : 一个字符串形式的正则表达式
flags 可选,表示匹配模式,比如忽略大小写,多行模式等,具体参数为:

  • re.I 忽略大小写
  • re.L 表示特殊字符集 \w, \W, \b, \B, \s, \S 依赖于当前环境
  • re.M 多行模式
  • re.S 即为’ . ‘并且包括换行符在内的任意字符(’ . '不包括换行符)
  • re.U 表示特殊字符集 \w, \W, \b, \B, \d, \D, \s, \S 依赖于Unicode 字符属性数据库
  • re.X 为了增加可读性,忽略空格和’ # '后面的注释

4、Python uniform() 函数

random.uniform
描述:uniform() 方法将随机生成下一个实数,它在[x,y]范围内。
语法:

import random  # numpy的random模块
random.uniform(x, y)

or

import numpy as np
np.random.uniform(x, y)

注意:uniform()是不能直接访问的,需要导入 random 模块,然后通过 random 静态对象调用该方法。
参数:
x – 随机数的最小值。
y – 随机数的最大值。
返回值:返回一个浮点数。
所以用的时候如果需要转化成int类型:int(np.random.uniform(x, y))

4.7 示例:使用朴素贝叶斯分类器从个人广告中获取区域倾向

本章的最后一个例子非常有趣。我们前面介绍了朴素贝叶斯的两个实际应用的例子,第一个例子是过滤网站的恶意留言,第二个是过滤垃圾邮件。
分类还有大量的其他应用。我曾经见过有人使用朴素贝叶斯从他喜欢及不喜欢的女性的社交网络档案学习相应的分类器,然后利用该分类器测试他是否会喜欢一个陌生女人。
分类的可能应用确实有很多,比如有证据表示,人的年龄越大,他所用的词也越好。那么,可以基于一个人的用词来推测他的年龄吗?除了年龄之外,还能否推测其他方面?广告商往往想知道关于一个人的一些特定人口统计信息,以便能够更好地定向推销广告。从哪里可以获得这些训练数据呢?
事实上,互联网上拥有大量的训练数据。几乎任一个能想到的利基市场都有专业社区,很多人会认为自己属于该社区。4.5.1节中的斑点犬爱好者网站就是一个非常好的例子。
在这个最后的例子当中,我们将分别从美国的两个城市中选取一些人,通过分析这些人发布的征婚广告信息,来比较这两个城市的人们在广告用词上是否不同。如果结论确实是不同,那么他们各自常用的词是哪些?从人们的用词当中,我们能否对不同城市的人所关心的内容有所了解?
《机器学习实战》-04 朴素贝叶斯_第40张图片
(?什么是RSS源
RSS源是一种描述和同步网站内容的格式,是目前使用最广泛的XML应用。RSS应用在国外已经非常普遍,从个人博客(Blog)栏目、企业站点到世界级的门户都提供基于RSS的服务。
RSS 源是一种简便方式,以确保成本保持最新版本与您喜欢的网站,例如博客或联机杂志。如果网站提供 RSS 源,获取帖子上升,然后您可以阅读摘要或整个文章时通知您。)
咳咳,还是不太懂啥是RSS源,似懂非懂,那就先往下看吧。。。

下面将使用来自不同城市的广告训练一个分类器,然后观察分类器的效果。我们的目的并不是使用该分类器进行分类,而是通过观察单词和条件概率值来发现与特定城市相关的内容。

4.7.1 收集数据:导入 RSS 源

接下来要做的第一件事是使用Python下载文本。幸好,利用RSS,这些文本很容易得到。现在所需要的是一个RSS阅读器。Universal Feed Parser是Python中最常用的RSS程序库。
你可以在http://code.google.com/p/feedparser/下浏览相关文档,然后和其他Python包一样来安装feedparse。首先解压下载的包,并将当前目录切换到解压文件所在的文件夹,然后在Python提示符下敲入 >>python setup.py install 。
下面使用Craigslist上的个人广告,当然希望是在服务条款允许的条件下。打开Craigslist上的RSS源,在Python提示符下输入:
在这里插入图片描述
我决定使用Craigslist中比较纯洁的那部分内容,其他内容稍显少儿不宜。你可以查阅feedparser.org中出色的说明文档以及RSS源。要访问所有条目的列表,输入:
在这里插入图片描述
这个RSS源好像访问不了,可以用一下我的博客RSS:https://blog.csdn.net/qq_34243930/rss/list
在这里插入图片描述
可以构建一个类似于 spamTest() 的函数来对测试过程自动化。打开文本编辑器,输入下列程序清单中的代码。
《机器学习实战》-04 朴素贝叶斯_第41张图片

def calcMostFreq(vocabList, fullText):
    """
    该函数遍历词汇表中的每个词并统计它在文本中出现的次数,然后根据出现次数从高到低对词典进行排序,最后返回排序最高的30个单词。
    :param vocabList:词汇表
    :param fullText:文本
    :return:返回排序最高的30个单词
    """
    import operator
    freqDict = {}
    for token in vocabList:
        freqDict[token] = fullText.count(token)
    sortedFreq = sorted(freqDict.items(), key=operator.itemgetter(1), reverse=True)
    return sortedFreq[:30]


def localWords(feed1, feed0):
    """
    :param feed1,feed0:使用两个RSS源作为参数
    :return: vocabList, p0V, p1V
    """
    import feedparser
    docList = []; classList = []; fullText = []
    minLen = min(len(feed1['entries']), len(feed0['entries']))
    for i in range(minLen):
        wordList = textParse(feed1['entries'][i]['summary'])
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(1) #NY is class 1

        wordList = textParse(feed0['entries'][i]['summary'])
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)

    vocabList = createVocabList(docList)#create vocabulary

    # 用于移除高频词
    top30Words = calcMostFreq(vocabList, fullText)   #remove top 30 words
    for pairW in top30Words:
        if pairW[0] in vocabList: vocabList.remove(pairW[0])

    trainingSet = range(2*minLen); testSet = []           #create test set
    for i in range(20):
        randIndex = int(np.random.uniform(0, len(trainingSet)))
        testSet.append(trainingSet[randIndex])
        del(list(trainingSet)[randIndex])

    trainMat = []; trainClasses = []
    for docIndex in trainingSet:#train the classifier (get probs) trainNB0
        trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
        trainClasses.append(classList[docIndex])
    p0V, p1V, pSpam = trainNB0(np.array(trainMat), np.array(trainClasses))

    errorCount = 0
    for docIndex in testSet:        #classify the remaining items
        wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
        if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
            errorCount += 1
    print('the error rate is: ', float(errorCount)/len(testSet))
    return vocabList, p0V, p1V

上述代码类似程序清单4-5中的函数 spamTest() ,不过添加了新的功能。代码中引入了一个辅助函数 calcMostFreq() 。该函数遍历词汇表中的每个词并统计它在文本中出现的次数,然后根据出现次数从高到低对词典进行排序,最后返回排序最高的30个单词。你很快就会明白这个函数的重要性。

下一个函数 localWords() 使用两个RSS源作为参数。
【1】RSS源要在函数外导入,这样做的原因是RSS源会随时间而改变。如果想通过改变代码来比较程序执行的差异,就应该使用相同的输入。重新加载RSS源就会得到新的数据,但很难确定是代码原因还是输入原因导致输出结果的改变。
【2】函数 localWords() 与程序清单4-5中的 spamTest() 函数几乎相同,区别在于这里访问的是RSS源 而不是文件
【3】然后调用函数 calcMostFreq() 来获得排序最高的30个单词并随后将它们移除 。
【4】函数的剩余部分与 spamTest() 基本类似,不同的是最后一行要返回下面要用到的值。
你可以注释掉用于移除高频词的三行代码,然后比较注释前后的分类性能 。我自己也尝试了一下,去掉这几行代码之后,我发现错误率为54%,而保留这些代码得到的错误率为70%。
这里观察到的一个有趣现象是,这些留言中出现次数最多的前30个词涵盖了所有用词的30%。我在进行测试的时候, vocabList 的大小约为3000个词。也就是说,词汇表中的一小部分单词却占据了所有文本用词的一大部分。产生这种现象的原因是因为语言中大部分都是冗余和结构辅助性内容。
另一个常用的方法是不仅移除高频词,同时从某个预定词表中移除结构上的辅助词。该词表称为停用词表(stop word list),目前可以找到许多停用词表(在本书写作期间,http://www.ranks.nl/resources/stopwords.html 上有一个很好的多语言停用词列表)。

将程序清单4-6中的代码加入到 bayes.py 文件之后,可以通过输入如下命令在Python中进行测试:
《机器学习实战》-04 朴素贝叶斯_第42张图片
两个RSS源访问不了。
用纽约的本地新闻:https://newyork.craigslist.org/d/activity-partners/search/act
和洛杉矶的本地新闻:https://losangeles.craigslist.org/d/activity-partners/search/act来替换
但是我还是运行不了,大家自己试试吧。。。

为了得到错误率的精确估计,应该多次进行上述实验,然后取平均值。这里的错误率要远高于垃圾邮件中的错误率。由于这里关注的是单词概率而不是实际分类,因此这个问题倒不严重。可以通过函数 caclMostFreq() 改变要移除的单词数目,然后观察错误率的变化情况。

4.7.2 分析数据:显示地域相关的用词

可以先对向量pSF与pNY进行排序,然后按照顺序将词打印出来。下面的最后一段代码会完成这部分工作。再次打开bayes.py文件,将下面的代码添加到文件中。
《机器学习实战》-04 朴素贝叶斯_第43张图片

def getTopWords(ny, sf):
    """
    :param ny,sf:使用两个RSS源作为输入
    :return:返回大于某个阈值的所有词
    """
    import operator
    vocabList, p0V, p1V = localWords(ny, sf)
    topNY = []; topSF = []
    for i in range(len(p0V)):
        if p0V[i] > -6.0: topSF.append((vocabList[i], p0V[i]))
        if p1V[i] > -6.0: topNY.append((vocabList[i], p1V[i]))
    sortedSF = sorted(topSF, key=lambda pair: pair[1], reverse=True)
    print("SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**")
    for item in sortedSF:
        print(item[0])
    sortedNY = sorted(topNY, key=lambda pair: pair[1], reverse=True)
    print("NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**")
    for item in sortedNY:
        print(item[0])


程序清单4-7中的函数 getTopWords() 使用两个RSS源作为输入,然后训练并测试朴素贝叶斯分类器,返回使用的概率值。然后创建两个列表用于元组的存储。与之前返回排名最高的X个单词不同,这里可以返回大于某个阈值的所有词。这些元组会按照它们的条件概率进行排序。

下面看一下实际的运行效果,保存bayes.py文件,在Python提示符下输入:
《机器学习实战》-04 朴素贝叶斯_第44张图片
最后输出的单词很有意思。值得注意的现象是,程序输出了大量的停用词。移除固定的停用词看看结果会如何变化也十分有趣。依我的经验来看,这样做的话,分类错误率也会降低。

4.7 Python 语法解析

1、python解析RSS(feedparser安装与使用)

https://blog.csdn.net/qq_34243930/article/details/86777720

2、Python3 count()方法

描述:count() 方法用于统计字符串里某个字符出现的次数。可选参数为在字符串搜索的开始与结束位置。
语法:str.count(sub, start= 0,end=len(string))
参数:
sub – 搜索的子字符串
start – 字符串开始搜索的位置。默认为第一个字符,第一个字符索引值为0。
end – 字符串中结束搜索的位置。字符中第一个字符的索引为 0。默认为字符串的最后一个位置。

3、sorted() 函数

sorted() 函数对所有可迭代的对象进行排序操作。
sorted(iterable, key=None, reverse=False)
iterable – 可迭代对象。
key – 主要是用来进行比较的元素,只有一个参数,具体的函数的参数就是取自于可迭代对象中,指定可迭代对象中的一个元素来进行排序。
reverse – 排序规则,reverse = True 降序 , reverse = False 升序(默认)。

4、字典(Dictionary)iteritems()方法

在3.x 里 用 items()替换iteritems()
Python 字典(Dictionary) items() 函数以列表返回可遍历的(键, 值) 元组数组。
dict.items()

5、operator.itemgetter函数

operator模块提供的itemgetter函数用于获取对象的哪些维的数据,参数为一些序号。
《机器学习实战》-04 朴素贝叶斯_第45张图片

6、lamda函数

《机器学习实战》-04 朴素贝叶斯_第46张图片
《机器学习实战》-04 朴素贝叶斯_第47张图片

4.8 本章小结

对于分类而言,使用概率有时要比使用硬规则更为有效。贝叶斯概率及贝叶斯准则提供了一种利用已知值来估计未知概率的有效方法。

可以通过特征之间的条件独立性假设,降低对数据量的需求。独立性假设是指一个词的出现概率并不依赖于文档中的其他词。当然我们也知道这个假设过于简单。这就是之所以称为朴素贝叶斯的原因。尽管条件独立性假设并不正确,但是朴素贝叶斯仍然是一种有效的分类器。

利用现代编程语言来实现朴素贝叶斯时需要考虑很多实际因素。下溢出就是其中一个问题,它可以通过对概率取对数来解决词袋模型在解决文档分类问题上比词集模型有所提高。还有其他一些方面的改进,比如说移除停用词,当然也可以花大量时间对切分器进行优化

本章学习到的概率理论将在后续章节中用到,另外本章也给出了有关贝叶斯概率理论全面具体的介绍。

接下来的一章将暂时不再讨论概率理论这一话题,介绍另一种称作Logistic回归的分类方法及一些优化算法。

你可能感兴趣的:(python,机器学习实战,机器学习实战)