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

在前面那个简单的例子中,我们引入了字符串列表。使用朴素贝叶斯解决一些现实生活中的问题时,需要先从文本内容得到字符串列表,然后生成词向量。下面这个例子中,我们将了解朴素贝叶斯的一个最著名的应用:电子邮件垃圾过滤。首先看一下如何使用通用框架来解决该问题。

示例:使用朴素贝叶斯对电子邮件进行分类

  1. 收集数据:提供文本文件。
  2. 准备数据:将文本文件解析成词条向量。
  3. 分析数据:检查词条确保解析的正确性。
  4. 训练算法:使用我们之前建立的trainNB0函数。
  5. 测试算法:使用classifyNB,并且构建一个新的测试函数来计算文档集的错误率。
  6. 使用算法:构建一个完整的程序对一组文档进行分类,将错分的文档输出到屏幕上。

下面首先给出将文本解析为词条的代码。然后将该代码和前面的分类代码集成为一个函数,该函数在测试分类器的同时会给出错误率。

4.6.1 准备数据:切分文本

前一节介绍了如何创建词向量,并基于这些词向量进行朴素贝叶斯分类的过程。前一节中的词向量是预先给定的,下面介绍如何从文本文档中构建自己的词列表。

对于一个文本字符串,可以使用Python的string.split方法将其切分。下面看看实际的运行效果。在Python提示符下输入:  

>>> mySent=\'This book is the best book on Python or M.L. I have ever laid eyes upon.\'
>>> mySent.split
[\'This\', \'book\', \'is\', \'the\', \'best\', \'book\', \'on\', \'Python\', \'or\', \'M.L.\',\'I\', \'have\', \'ever\', \'laid\', \'eyes\', \'upon.\']     
  

可以看到,切分的结果不错,但是标点符号也被当成了词的一部分。可以使用正则表示式来切分句子,其中分隔符是除单词、数字外的任意字符串。  

>>> import re
>>> regEx = re.compile(\'\\W*\')
>>> listOfTokens = regEx.split(mySent)
>>> listOfTokens
[\'This\', \'book\', \'is\', \'the\', \'best\', \'book\', \'on\', \'Python\', \'or\', \'M\', \'L\', \'\', \'I\', \'have\', \'ever\', \'laid\', \'eyes\', \'upon\', \'\']
  

现在得到了一系列词组成的词表,但是里面的空字符串需要去掉。可以计算每个字符串的长度,只返回长度大于0的字符串。

>>> [tok for tok in listOfTokens if len(tok) > 0] 
  

最后,我们发现句子中的第一个单词是大写的。如果目的是句子查找,那么这个特点会很有用。但这里的文本只看成词袋,所以我们希望所有词的形式都是统一的,不论它们出现在句子中间、结尾还是开头。

Python中有一些内嵌的方法可以将字符串全部转换成小写(.lower)或者大写.upper),借助这些方法可以达到目的。于是,可以进行如下处理:

>>> [tok.lower for tok in listOfTokens if len(tok) > 0]
[\'this\', \'book\', \'is\', \'the\', \'best\', \'book\', \'on\', \'python\', \'or\', \'m\', \'l\', \'i\', \'have\', \'ever\', \'laid\', \'eyes\', \'upon\']
  

现在来看数据集中一封完整的电子邮件的实际处理结果。该数据集放在email文件夹中,该文件夹又包含两个子文件夹,分别是spam与ham。

>>> emailText = open(\'email/ham/6.txt\').read
>>> listOfTokens=regEx.split(emailText)  
  

文件夹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文件中。

程序清单4-5 文件解析及完整的垃圾邮件测试函数

def textParse(bigString):
    import re
    listOfTokens = re.split(r\'W*\', bigString)
    return [tok.lower for tok in listOfTokens if len(tok) > 2]

def spamTest:
    docList=; classList = ; fullText =
    for i in range(1,26):
        #❶ (以下七行)导入并解析文本文件
        wordList = textParse(open(\'email/spam/%d.txt\' % i).read)
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(1)
        wordList = textParse(open(\'email/ham/%d.txt\' % i).read)
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)
    vocabList = createVocabList(docList)
    trainingSet = range(50); testSet=
    #❷(以下四行)随机构建训练集
    for i in range(10):
        randIndex = int(random.uniform(0,len(trainingSet)))
        testSet.append(trainingSet[randIndex])
        del(trainingSet[randIndex])
    trainMat=; trainClasses = 
    for docIndex in trainingSet:
        trainMat.append(setOfWords2Vec(vocabList, docList[docIndex]))
        trainClasses.append(classList[docIndex])
    p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
    errorCount = 0
    #❸(以下四行)对测试集分类
    for docIndex in testSet:
        wordVector = setOfWords2Vec(vocabList, docList[docIndex])
        if classifyNB(array(wordVector),p0V,p1V,pSpam) !=
    classList[docIndex]:
           errorCount += 1
    print \'the error rate is: \',float(errorCount)/len(testSet)   
 

第一个函数textParse接受一个大字符串并将其解析为字符串列表。该函数去掉少于两个字符的字符串,并将所有字符串转换为小写。你可以在函数中添加更多的解析操作,但是目前的实现对于我们的应用足够了。

第二个函数spamTest对贝叶斯垃圾邮件分类器进行自动化处理。导入文件夹spamham下的文本文件,并将它们解析为词列表❶。接下来构建一个测试集与一个训练集,两个集合中的邮件都是随机选出的。本例中共有50封电子邮件,并不是很多,其中的10封电子邮件被随机选择为测试集。分类器所需要的概率计算只利用训练集中的文档来完成。Python变量trainingSet是一个整数列表,其中的值从0到49。接下来,随机选择其中10个文件❷。选择出的数字所对应的文档被添加到测试集,同时也将其从训练集中剔除。这种随机选择数据的一部分作为训练集,而剩余部分作为测试集的过程称为留存交叉验证(hold-out cross validation)。假定现在只完成了一次迭代,那么为了更精确地估计分类器的错误率,就应该进行多次迭代后求出平均错误率。

接下来的for循环遍历训练集的所有文档,对每封邮件基于词汇表并使用setOfWords2Vec函数来构建词向量。这些词在traindNB0函数中用于计算分类所需的概率。然后遍历测试集,对其中每封电子邮件进行分类❸。如果邮件分类错误,则错误数加1,最后给出总的错误百分比。

下面对上述过程进行尝试。输入程序清单4-5的代码之后,在Python提示符下输入:

>>> bayes.spamTest
the error rate is: 0.0
>>> bayes.spamTest
classification error [\'home\', \'based\', \'business\', \'opportunity\', \'knocking\', \'your\', \'door\', \'don\', \'rude\', \'and\', \'let\', \'this\', \'chance\', \'you\', \'can\', \'earn\', \'great\', \'income\', \'and\', \'find\', \'your\', \'financial\', \'life\', \'transformed\', \'learn\', \'more\', \'here\', \'your\', \'success\', \'work\', \'from\', \'home\', \'finder\', \'experts\']
the error rate is: 0.1 
  

函数spamTest会输出在10封随机选择的电子邮件上的分类错误率。既然这些电子邮件是随机选择的,所以每次的输出结果可能有些差别。如果发现错误的话,函数会输出错分文档的词表,这样就可以了解到底是哪篇文档发生了错误。如果想要更好地估计错误率,那么就应该将上述过程重复多次,比如说10次,然后求平均值。我这么做了一下,获得的平均错误率为6%。

这里一直出现的错误是将垃圾邮件误判为正常邮件。相比之下,将垃圾邮件误判为正常邮件要比将正常邮件归到垃圾邮件好。为避免错误,有多种方式可以用来修正分类器,这些将在第7章中进行讨论。

目前我们已经使用朴素贝叶斯来对文档进行分类,接下来将介绍它的另一个应用。下一个例子还会给出如何解释朴素贝叶斯分类器训练所得到的知识。

《机器学习实战》