要從文本中獲取特徵,需要先拆分文本。具體如何做呢?這裡的特徵是來自文本的詞條(token),一個詞條是字符的任意組合。可以把詞條想像為單詞,也可以使用非單詞詞條,如URL、IP地址或者任意其他字符串。然後將每一個文本片段表示為一個詞條向量,其中值為1表示詞條出現在文檔中,0表示詞條未出現。
以在線社區的留言板為例。為了不影響社區的發展,我們要屏蔽侮辱性的言論,所以要構建一個快速過濾器,如果某條留言使用了負面或者侮辱性的語言,那麼就將該留言標識為內容不當。過濾這類內容是一個很常見的需求。對此問題建立兩個類別:侮辱類和非侮辱類,使用1和0分別表示。
接下來首先給出將文本轉換為數字向量的過程,然後介紹如何基於這些向量來計算條件概率,並在此基礎上構建分類器,最後還要介紹一些利用Python實現樸素貝葉斯過程中需要考慮的問題。
4.5.1 準備數據:從文本中構建詞向量
我們將把文本看成單詞向量或者詞條向量,也就是說將句子轉換為向量。考慮出現在所有文檔中的所有單詞,再決定將哪些詞納入詞彙表或者說所要的詞彙集合,然後必須要將每一篇文檔轉換為詞彙表上的向量。接下來我們正式開始。打開文本編輯器,創建一個叫bayes.py的新文件,然後將下面的程序清單添加到文件中。
程序清單4-1 詞表到向量的轉換函數
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
def createVocabList(dataSet):
#❶ 創建一個空集
vocabSet = set()
for document in dataSet:
#❷ 創建兩個集合的並集
vocabSet = vocabSet | set(document)
return list(vocabSet)
def setOfWords2Vec(vocabList, inputSet):
#❸ 創建一個其中所含元素都為0的向量
returnVec = [0]*len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] = 1
else: print \"the word: %s is not in my Vocabulary!\" % word
return returnVec
第一個函數loadDataSet
創建了一些實驗樣本。該函數返回的第一個變量是進行詞條切分後的文檔集合,這些文檔來自斑點犬愛好者留言板。這些留言文本被切分成一系列的詞條集合,標點符號從文本中去掉,後面會探討文本處理的細節。loadDataSet( )
函數返回的第二個變量是一個類別標籤的集合。這裡有兩類,侮辱性和非侮辱性。這些文本的類別由人工標注,這些標注信息用於訓練程序以便自動檢測侮辱性留言。
下一個函數createVocabList
會創建一個包含在所有文檔中出現的不重複詞的列表,為此使用了Python的set
數據類型。將詞條列表輸給set
構造函數,set
就會返回一個不重複詞表。首先,創建一個空集合❶,然後將每篇文檔返回的新詞集合添加到該集合中❷。操作符|
用於求兩個集合的並集,這也是一個按位或(OR
)操作符(參見附錄C)。在數學符號表示上,按位或操作與集合求並操作使用相同記號。
獲得詞彙表後,便可以使用函數setOfWords2Vec
,該函數的輸入參數為詞彙表及某個文檔,輸出的是文檔向量,向量的每一元素為1或0,分別表示詞彙表中的單詞在輸入文檔中是否出現。函數首先創建一個和詞彙表等長的向量,並將其元素都設置為0❸。接著,遍歷文檔中的所有單詞,如果出現了詞彙表中的單詞,則將輸出的文檔向量中的對應值設為1。一切都順利的話,就不需要檢查某個詞是否還在vocabList
中,後邊可能會用到這一操作。
現在看一下這些函數的執行效果,保存bayes.py
文件,然後在Python提示符下輸入:
>>> import bayes
>>> listOPosts,listClasses = bayes.loadDataSet
>>> myVocabList = bayes.createVocabList(listOPosts)
>>> myVocabList
[\'cute\', \'love\', \'help\', \'garbage\', \'quit\', \'I\', \'problems\', \'is\', \'park\',
\'stop\', \'flea\', \'dalmation\', \'licks\', \'food\', \'not\', \'him\', \'buying\',
\'posting\', \'has\', \'worthless\', \'ate\', \'to\', \'maybe\', \'please\', \'dog\',
\'how\', \'stupid\', \'so\', \'take\', \'mr\', \'steak\', \'my\']
檢查上述詞表,就會發現這裡不會出現重複的單詞。目前該詞表還沒有排序,需要的話,稍後可以對其排序。
下面看一下函數setOfWords2Vec
的運行效果:
>>> bayes.setOfWords2Vec(myVocabList, listOPosts[0])
[0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 1]
>>> bayes.setOfWords2Vec(myVocabList, listOPosts[3])
[0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0]
該函數使用詞彙表或者想要檢查的所有單詞作為輸入,然後為其中每一個單詞構建一個特徵。一旦給定一篇文檔(斑點犬網站上的一條留言),該文檔就會被轉換為詞向量。接下來檢查一下函數的有效性。myVocabList
中索引為2的元素是什麼單詞?應該是單詞help
。該單詞在第一篇文檔中出現,現在檢查一下看看它是否出現在第四篇文檔中。
4.5.2 訓練算法:從詞向量計算概率****
前面介紹了如何將一組單詞轉換為一組數字,接下來看看如何使用這些數字計算概率。現在已經知道一個詞是否出現在一篇文檔中,也知道該文檔所屬的類別。還記得3.2節提到的貝葉斯準則?我們重寫貝葉斯準則,將之前的x、y 替換為w。粗體w表示這是一個向量,即它由多個數值組成。在這個例子中,數值個數與詞彙表中的詞個數相同。
我們將使用上述公式,對每個類計算該值,然後比較這兩個概率值的大小。如何計算呢?首先可以通過類別i
(侮辱性留言或非侮辱性留言)中文檔數除以總的文檔數來計算概率p(ci)
。接下來計算p(w|ci)
,這裡就要用到樸素貝葉斯假設。如果將w展開為一個個獨立特徵,那麼就可以將上述概率寫作p(w0,w1,w2..wN|ci)
。這裡假設所有詞都互相獨立,該假設也稱作條件獨立性假設,它意味著可以使用p(w0|ci)p(w1|ci)p(w2|ci)...p(wN|ci)
來計算上述概率,這就極大地簡化了計算的過程。
該函數的偽代碼如下:
計算每個類別中的文檔數目 對每篇訓練文檔: 對每個類別: 如果詞條出現在文檔中→ 增加該詞條的計數值 增加所有詞條的計數值 對每個類別: 對每個詞條: 將該詞條的數目除以總詞條數目得到條件概率 返回每個類別的條件概率
我們利用下面的代碼來實現上述偽碼。打開文本編輯器,將這些代碼添加到bayes.py
文件中。該函數使用了NumPy的一些函數,故應確保將from numpy import *
語句添加到bayes.py
文件的最前面。
程序清單4-2 樸素貝葉斯分類器訓練函數
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix)
numWords = len(trainMatrix[0])
pAbusive = sum(trainCategory)/float(numTrainDocs)
#❶ (以下兩行)初始化概率
p0Num = zeros(numWords); p1Num = 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 #change to log
#❸ 對每個元素做除法
p0Vect = p0Num/p0Denom #change to log
return p0Vect,p1Vect,pAbusive
代碼函數中的輸入參數為文檔矩陣trainMatrix
,以及由每篇文檔類別標籤所構成的向量trainCategory
。首先,計算文檔屬於侮辱性文檔(class=1
)的概率,即P(1)
。因為這是一個二類分類問題,所以可以通過1-P(1)
得到P(0)
。對於多於兩類的分類問題,則需要對代碼稍加修改。
計算p(wi|c1)
和p(wi|c0)
,需要初始化程序中的分子變量和分母變量❶。由於w中元素如此眾多,因此可以使用NumPy數組快速計算這些值。上述程序中的分母變量是一個元素個數等於詞彙表大小的NumPy數組。在for
循環中,要遍歷訓練集trainMatrix
中的所有文檔。一旦某個詞語(侮辱性或正常詞語)在某一文檔中出現,則該詞對應的個數(p1Num
或者p0Num
)就加1,而且在所有的文檔中,該文檔的總詞數也相應加1❷。對於兩個類別都要進行同樣的計算處理。
最後,對每個元素除以該類別中的總詞數❸。利用NumPy可以很好實現,用一個數組除以浮點數即可,若使用常規的Python列表則難以完成這種任務,讀者可以自己嘗試一下。最後,函數會返回兩個向量和一個概率。
接下來試驗一下。將程序清單4-2中的代碼添加到bayes.py
文件中,在Python提示符下輸入:
>>> from numpy import * >>> reload(bayes) >>> listOPosts,listClasses = bayes.loadDataSet
該語句從預先加載值中調入數據
>>> myVocabList = bayes.createVocabList(listOPosts)
至此我們構建了一個包含所有詞的列表myVocabList
。
>>> trainMat= >>> for postinDoc in listOPosts: ... trainMat.append(bayes.setOfWords2Vec(myVocabList, postinDoc)) ...
該for
循環使用詞向量來填充trainMat
列表。下面給出屬於侮辱性文檔的概率以及兩個類別的概率向量。
>>> p0V,p1V,pAb=bayes.trainNB0(trainMat,listClasses)
接下來看這些變量的內部值:
>>> pAb
0.5
這就是任意文檔屬於侮辱性文檔的概率。
>>> p0V
array([ 0.04166667, 0.04166667, 0.04166667, 0. , 0. ,
.
.
0.04166667, 0. , 0.04166667, 0. , 0.04166667,
0.04166667, 0.125 ])
>>> p1V
array([ 0. , 0. , 0. , 0.05263158, 0.05263158,
.
.
0. , 0.15789474, 0. , 0.05263158, 0. ,
0. , 0. ])
首先,我們發現文檔屬於侮辱類的概率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 測試算法:根據現實情況修改分類器
利用貝葉斯分類器對文檔進行分類時,要計算多個概率的乘積以獲得文檔屬於某個類別的概率,即計算p(w0|1)p(w1|1)p(w2|1)
。如果其中一個概率值為0,那麼最後的乘積也為0。為降低這種影響,可以將所有詞的出現數初始化為1,並將分母初始化為2。
在文本編輯器中打開bayes.py文件,並將trainNB0( )
的第4行和第5行修改為:
p0Num = ones(numWords); p1Num = ones(numWords)
p0Denom = 2.0; p1Denom = 2.0
另一個遇到的問題是下溢出,這是由於太多很小的數相乘造成的。當計算乘積p(w0|ci)p(w1|ci)p(w2|ci)...p(wN|ci)
時,由於大部分因子都非常小,所以程序會下溢出或者得到不正確的答案。(讀者可以用Python嘗試相乘許多很小的數,最後四捨五入後會得到0。)一種解決辦法是對乘積取自然對數。在代數中有ln(a*b) = ln(a)+ln(b)
,於是通過求對數可以避免下溢出或者浮點數捨入導致的錯誤。同時,採用自然對數進行處理不會有任何損失。圖4-4給出函數f(x)
與ln(f(x))
的曲線。檢查這兩條曲線,就會發現它們在相同區域內同時增加或者減少,並且在相同點上取到極值。它們的取值雖然不同,但不影響最終結果。通過修改return
前的兩行代碼,將上述做法用到分類器中:
p1Vect = log(p1Num/p1Denom)
p0Vect = log(p0Num/p0Denom)
圖4-4 函數f(x)
與ln(f(x))
會一塊增大。這表明想求函數的最大值時,可以使用該函數的自然對數來替換原函數進行求解
現在已經準備好構建完整的分類器了。當使用NumPy向量處理功能時,這一切變得十分簡單。打開文本編輯器,將下面的代碼添加到bayes.py中:
程序清單4-3 樸素貝葉斯分類函數
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
#❶ 元素相乘
p1 = sum(vec2Classify * p1Vec) + log(pClass1)
p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
def testingNB:
listOPosts,listClasses = loadDataSet
myVocabList = createVocabList(listOPosts)
trainMat=
for postinDoc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
p0V,p1V,pAb = trainNB0(array(trainMat),array(listClasses))
testEntry = [\'love\', \'my\', \'dalmation\']
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print testEntry,\'classified as: \',classifyNB(thisDoc,p0V,p1V,pAb)
testEntry = [\'stupid\', \'garbage\']
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print testEntry,\'classified as: \',classifyNB(thisDoc,p0V,p1V,pAb)
程序清單4-3的代碼有4個輸入:要分類的向量vec2Classify
以及使用函數trainNB0
計算得到的三個概率。使用NumPy的數組來計算兩個向量相乘的結果❶。這裡的相乘是指對應元素相乘,即先將兩個向量中的第1個元素相乘,然後將第2個元素相乘,以此類推。接下來將詞彙表中所有詞的對應值相加,然後將該值加到類別的對數概率上。最後,比較類別的概率返回大概率對應的類別標籤。這一切不是很難,對吧?
代碼的第二個函數是一個便利函數(convenience function),該函數封裝所有操作,以節省輸入4.3.1節中代碼的時間。
下面來看看實際結果。將程序清單4-3中的代碼添加之後,在Python提示符下輸入:
>>> reload(bayes)
<module \'bayes\' from \'bayes.pyc\'>
>>>bayes.testingNB
[\'love\', \'my\', \'dalmation\'] classified as: 0
[\'stupid\', \'garbage\'] classified as: 1
對文本做一些修改,看看分類器會輸出什麼結果。這個例子非常簡單,但是它展示了樸素貝葉斯分類器的工作原理。接下來,我們會對代碼做些修改,使分類器工作得更好。
4.5.4 準備數據:文檔詞袋模型
目前為止,我們將每個詞的出現與否作為一個特徵,這可以被描述為詞集模型(set-of-words model)。如果一個詞在文檔中出現不止一次,這可能意味著包含該詞是否出現在文檔中所不能表達的某種信息,這種方法被稱為詞袋模型(bag-of-words model)。在詞袋中,每個單詞可以出現多次,而在詞集中,每個詞只能出現一次。為適應詞袋模型,需要對函數setOfWords2Vec
稍加修改,修改後的函數稱為bagOfWords2Vec
。
下面的程序清單給出了基於詞袋模型的樸素貝葉斯代碼。它與函數setOfWords2Vec
幾乎完全相同,唯一不同的是每當遇到一個單詞時,它會增加詞向量中的對應值,而不只是將對應的數值設為1。
程序清單4-4 樸素貝葉斯詞袋模型
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0]*len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1
return returnVec
現在分類器已經構建好了,下面我們將利用該分類器來過濾垃圾郵件。