讀古今文學網 > 機器學習實戰 > 4.6 示例:使用樸素貝葉斯過濾垃圾郵件 >

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章中進行討論。

目前我們已經使用樸素貝葉斯來對文檔進行分類,接下來將介紹它的另一個應用。下一個例子還會給出如何解釋樸素貝葉斯分類器訓練所得到的知識。