全文共3759字,预计阅读时间35分钟。
第十三章 朴素贝叶斯算法
13.1一个简易的垃圾邮件过滤器
13.2 一个更复杂的垃圾邮件过滤器
13.3 算法的实现
13.4 测试模型
13.5 使用我们的模型
13.6 延伸学习
心灵朴素多为巧,头脑朴素却成拙。
——阿纳托尔· 法郎士
如果人们不形成关系网的话,社交网络就不会有太大的用途。因此,DataSciencester 提供了一个非常流行的功能,允许会员之间相互发送邮件。虽然大部分会员都是发邮件嘘寒问暖的良民,但是,总少不了有几个坏家伙,老给其他会员发送垃圾邮件,如致富经、药品广告以及以收费为目的的数据科学家资格认证项目等。因此,用户们开始投诉。不久,邮件服务部的副总就把你叫过去,让你利用数据科学来过滤这些垃圾邮件。
13.1一个简易的垃圾邮件过滤器
想象有一个“全集”,其中存放了从所有可能的邮件中随机选择的邮件。我们令 S 表示事件“这是一封垃圾邮件”并且B是“消息包含bitcoin一词”的事件。贝叶斯定理告诉我们,信息是垃圾邮件,基于包含bitcoin一词的概率是:
分子是消息是垃圾邮件并包bitcoin的概率,而分母只是消息包含bitcoin的概率。因此,你可以认为这个计算只是简单地表示比特币消息是垃圾邮件的比例。
如果我们已经收集了大量垃圾邮件和非垃圾邮件,那么就可以轻松计算 P(B | S) 和 P(B|¬ S)。如果我们进一步假定任何邮件是垃圾邮件或非垃圾邮件的可能性是等同的 (即 P(S)=P(¬ S)=0.5),那么:
举例来说,如果 50% 的垃圾邮件都含有单词 bitcoin,而只有 1% 的非垃圾邮件含有该单词,那么任何一封含有单词 bitcoin的电子邮件为垃圾邮件的概率是:
13.2一个更复杂的垃圾邮件过滤器
假设我们已建立了一个词汇表,其中含有许多单词:w1, …, wn。站在概率论的角度,我们用 Xi 表示事件“一封含有单词 wi 的邮件”。此外,我们还假设(通过一些未指定的过程)已经求出了 P(Xi|S) 和P(X |¬S),前者表示垃圾邮件中出现第 i 个单词的概率,后者表示非垃圾邮件中出现第 i 个 单词的概率。
朴素贝叶斯算法的一个(大的)假设是,给定邮件是或不是垃圾邮件的条件下,其中的每个单词存在与否与其他单词毫不相干。直观地讲,就是知道某封垃圾邮件是否含有单词 bitcoin无法帮助我们判断该垃圾邮件是否含有单词 rolex。如果用数学公式表示的话,就是:
这是一个非常极端的假设(这也部分解释了为何该算法名中含有“朴素”一词)。假设我们的词汇表仅含有单词 bitcoin和 rolex,并且一半的垃圾邮件是推销“廉价伟哥”的,另一半是推销“劳力士正品”的,这样的话,我们可以通过朴素贝叶斯算法计算垃圾邮件中同时出现 bitcoin和 rolex 这两个单词的概率:
之所以得到这样的结果,是因为我们的假设已经把 bitcoin 和 rolex 绝不会同时出现的经验给扔掉了。尽管这个假设与事实并不相符,但是这个模型的表现通常都很好,所以现实中经常用它过滤垃圾邮件。
前面我们曾经利用贝叶斯定理过滤只涉及 “bitcoin”的垃圾邮件,下面我们再次用它来推断一个邮件是垃圾邮件的概率,具体公式如下所示:
朴素贝叶斯假设使我们能够轻松求出公式右边的每个概率:只要将词汇表中各个单词的概率相乘即可。
在实践中,为了避免所谓的下溢(underflflow)问题,你通常希望尽量避免出现大量概率相乘的情况,因为计算机不擅长处理非常接近于零的浮点数。根据代数知识我们知道,log(ab)=loga+logb 且 exp(logx)=x,因此我们一般使用对浮点数更加友好的等效方法来计算p1*…*pn,具体公式如下所示:
现在,唯一的挑战就是估计 P(Xi|S) 和 P(Xi|¬ S) 了,即估计垃圾邮件(或非垃圾邮件)中包含单词 wi 的概率。如果我们掌握了相当数量的“训练”邮件,即标记为垃圾或非垃圾的邮件,那么很明显,这时计算 P(Xi|S) 就简化为求包含单词 wi 的垃圾邮件所占的比例了。
但是,这会引起一个大麻烦。假如词汇表中的单词 data 仅出现在训练集的非垃圾邮件中,那么 P(data|S)=0。也就是说,对于任何含有单词 data 的邮件,我们的朴素贝叶斯分类器总是认为它是垃圾邮件的概率为 0,即使是像含有“data on free bitcoin and authentic rolex watches”(关于廉价伟哥和劳力士正品的数据)这样的邮件也是如此。为了避免这种问题,我们通常要使用某种平滑技术。
准确地说,我们引入一个伪记数(pseudo count),记为 k,并通过下面的公式来计算在一封垃圾邮件中出现第 i 个单词的概率:
P(Xi|¬ S)的计算方法与此类似。亦即,当计算第 i 个单词出现在垃圾邮件中的概率时,我们假定还看到:额外的 k 封非垃圾邮件包含该单词,额外 k 封非垃圾邮件不包含该单词。
例如,如果 data 这个单词在 98 封垃圾邮件中出现了 0 次,并且 k 取值为 1,我们算P(data|S) 为 1/100 = 0.01,这样一来,我们的分类器就能给那些含有单词 data 的邮件为垃圾邮件的概率赋予非 0 值了。
13.3 算法的实现
到目前为止,我们已经学习了构建垃圾邮件分类器所需的各方面的知识。下面,我们首先建立一个简单的函数,来将邮件解析为不同的单词。首先要把各个邮件文本转换为小写形式,然后使re.findall提取由字母、数字和撇号组成的“单词”,最后使用 set函数获得不同的单词:
我们还将为我们的培训数据定义一种类型:
由于我们的分类器需要从训练数据中跟踪单词(tokens)、计数和标签,我们将每个单词看成一个类。按照惯例,我们将非垃圾邮件称为业余(ham)电子邮件。
构造函数将只接受一个参数,即在计算概率时使用的伪计数。它还初始化了一组空的单词(tokens),计数器来跟踪每个tokens在垃圾邮件和ham邮件中被看到的频率,以及它被训练的垃圾邮件和ham邮件的数量:
接下来,我们将给它一种进行一堆信息训练的方法。首先,我们增加spam_messages和ham_messages计数。然后我们为每个消息文本进行分词,对于每个单词,我们根据消息类型递增token_spam_counts或token_ham_counts:
最终,我们将需要预测P(spam |tokens)。正如我们前面看到的,要应用贝叶斯定理,我们需要了解词汇表中的每个token的P(kokens|spam)和(tokens|ham)。因此,我们将创建一个“专用”辅助函数来计算:
最后,我们已经准备好编写预测方法了。如前所述,我们不会将许多小概率相乘,而是处理对数概率:
现在我们有了一个分类器。
13.4 测试模型
让我们通过编写一些单元测试来确保我们的模型正常工作。
首先,让我们检查一下统计数是否正确:
现在让我们做一个预测。我们还将(费力地)手工完成我们的朴素贝叶斯逻辑,并确保我们得到相同的结果:
这个测试通过了,所以似乎我们的模型正在做我们认为正确的事。如果你看看实际的可能性,这两个主要的驱动因素是我们的信息包含垃圾邮件(我们唯一的训练垃圾邮件就包含了),而且它不包含ham(我们的训练ham信息都包含了)。
现在让我们试试一些真实的数据。
13.5 使用我们的模型
一个流行的(如果有点旧的话)数据集是垃圾邮件刺客公共语料库。我们将查看以20021010前缀的文件。
这里有一个脚本,它可以下载并解压缩它们到你选择的目录中(或者你可以手动完成):
文件的位置可能会更改(这发生在本书的第一版和第二版之间),在这种情况下会相应地调整脚本。
下载数据后,你应该有三个文件夹:spam、easy_ham和hard_ham。每个文件夹都包含许多电子邮件,每个电子邮件都包含在一个文件中。为了让问题简单,我们将查看每封邮件的主题行。
我们如何确定主题行?当我们查看这些文件时,它们似乎都以“主题:”开头。所以,我们按照这个规则来处理:
现在我们可以将数据划分为训练数据和测试数据,然后我们准备构建一个分类器:
让我们生成一些预测,检查我们的模型:
这会提供84个真阳性(垃圾邮件归类为“垃圾邮件”)、25个假阳性(ham归类为“垃圾邮件”)、703个真阴性(ham归类为“ham”)和44个假阴性(垃圾邮件归类为“ham”)。这意味着我们的精度为84/(84+25)=77%,我们的召回率为84/(84+44)=65%,这对于如此简单的模型来说是不错的数字。(如果我们多看看主题以外的信息,我们会做得更好。)
我们还可以检查模型的内部信息,查看哪些单词最少代表垃圾邮件:
最发垃圾邮件的词包括销售、抵押贷款、货币和利率,而最糟糕的词包括发垃圾邮件、用户、apt和perl。所以这也给了我们一些直观的信心,即我们的模型基本上是在做正确的事情。
我们怎样才能获得更好的表现呢?一个明显的方法是获得更多的数据来进行训练。还有许多方法可以改进这个模型。这里有一些你可以尝试的可能性:
• 考察邮件内容,而不是仅仅考察邮件主题。你还必须仔细考虑邮件信息headers的处理方式。
• 我们的分类器考虑了训练集中包含的所有单词,即使该单词仅仅出现过一次。修改分类器,让它接受一个可选阈值 min_count,并且如果某个单词在训练集中出现次数少于阈值则不予考虑。
• 分词器不了解相似词(例如 cheap 和 cheapest)。修改分类器,使其接受一个可选的词干分析器(stemmer)函数来找出单词对应的同类词。下面我们以一个非常简单的词干分析器函数为例进行介绍:
自己创建一个非常好用的词干分析器函数非常困难,所以人们通常使用现成的波特词干器(Porter stemmer)()。
• 虽然我们的特征都是采取了“含有单词 wi 的邮件”的形式,但这不代表必须采取这种形式。在我们的代码实现中,也可以添加额外的特征,比如“含有一个数字的邮件”。为此,可以创建类似 contains:number 这样的伪token,然后修改单词分词器(tokenizer),让它在适当的时候生成这些伪token。
13.6 延伸学习
• Paul Graham 的“A Plan for Spam” 和“Better Bayesian Filtering”这两篇文章对如何打造垃圾邮件过滤器提供了更加有趣而深入的介绍。
• scikit-learn 库提供了一个名为 BernoulliNB 的模型,也实现了与本章介绍的相同的朴素贝叶斯算法以及基于该算法的其他变种。