本章的最後一個例子非常有趣。我們前面介紹了樸素貝葉斯的兩個實際應用的例子,第一個例子是過濾網站的惡意留言,第二個是過濾垃圾郵件。分類還有大量的其他應用。我曾經見過有人使用樸素貝葉斯從他喜歡及不喜歡的女性的社交網絡檔案學習相應的分類器,然後利用該分類器測試他是否會喜歡一個陌生女人。分類的可能應用確實有很多,比如有證據表示,人的年齡越大,他所用的詞也越好。那麼,可以基於一個人的用詞來推測他的年齡嗎?除了年齡之外,還能否推測其他方面?廣告商往往想知道關於一個人的一些特定人口統計信息,以便能夠更好地定向推銷廣告。從哪裡可以獲得這些訓練數據呢?事實上,互聯網上擁有大量的訓練數據。幾乎任一個能想到的利基市場1都有專業社區,很多人會認為自己屬於該社區。4.5.1節中的斑點犬愛好者網站就是一個非常好的例子。
1. 利基(niche)是指針對企業的優勢細分出來的市場,這個市場不大,而且沒有得到令人滿意的服務。產品推進這個市場,有盈利的基礎。在這裡特指針對性和專業性都很強的產品。也就是說,利基是細分市場沒有被服務好的群體。——譯者注
在這個最後的例子當中,我們將分別從美國的兩個城市中選取一些人,通過分析這些人發佈的徵婚廣告信息,來比較這兩個城市的人們在廣告用詞上是否不同。如果結論確實是不同,那麼他們各自常用的詞是哪些?從人們的用詞當中,我們能否對不同城市的人所關心的內容有所瞭解?
示例:使用樸素貝葉斯來發現地域相關的用詞
- 收集數據:從RSS源收集內容,這裡需要對RSS源構建一個接口。
- 準備數據:將文本文件解析成詞條向量。
- 分析數據:檢查詞條確保解析的正確性。
- 訓練算法:使用我們之前建立的
trainNB0
函數。- 測試算法:觀察錯誤率,確保分類器可用。可以修改切分程序,以降低錯誤率,提高分類結果。
- 使用算法:構建一個完整的程序,封裝所有內容。給定兩個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提示符下輸入:
>>> import feedparser
>>>ny=feedparser.parse(\'http://newyork.craigslist.org/stp/index.rss\')
我決定使用Craigslist中比較純潔的那部分內容,其他內容稍顯少兒不宜。你可以查閱feedparser.org中出色的說明文檔以及RSS源。要訪問所有條目的列表,輸入:
>>> ny[\'entries\']
>>> len(ny[\'entries\'])
100
可以構建一個類似於spamTest
的函數來對測試過程自動化。打開文本編輯器,輸入下列程序清單中的代碼。
程序清單4-6 RSS源分類器及高頻詞去除函數
#❶(以下四行)計算出現頻率
def calcMostFreq(vocabList,fullText):
import operator
freqDict = {}
for token in vocabList:
freqDict[token]=fullText.count(token)
sortedFreq = sorted(freqDict.iteritems, key=operator.itemgetter(1), reverse=True)
return sortedFreq[:30]
def localWords(feed1,feed0):
import feedparser
docList=; classList = ; fullText =
minLen = min(len(feed1[\'entries\']),len(feed0[\'entries\']))
for i in range(minLen):
#❷ 每次訪問一條RSS源
wordList = textParse(feed1[\'entries\'][i][\'summary\'])
docList.append(wordList)
fullText.extend(wordList)
classList.append(1)
wordList = textParse(feed0[\'entries\'][i][\'summary\'])
docList.append(wordList)
fullText.extend(wordList)
classList.append(0)
#❸(以下四行)去掉出現次數最高的那些詞
vocabList = createVocabList(docList)
top30Words = calcMostFreq(vocabList,fullText)
for pairW in top30Words:
if pairW[0] in vocabList: vocabList.remove(pairW[0])
trainingSet = range(2*minLen); testSet=
for i in range(20):
randIndex = int(random.uniform(0,len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
trainMat=; trainClasses =
for docIndex in trainingSet:
trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
trainClasses.append(classList[docIndex])
p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
errorCount = 0
for docIndex in testSet:
wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
if classifyNB(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源作為參數。RSS源要在函數外導入,這樣做的原因是RSS源會隨時間而改變。如果想通過改變代碼來比較程序執行的差異,就應該使用相同的輸入。重新加載RSS源就會得到新的數據,但很難確定是代碼原因還是輸入原因導致輸出結果的改變。函數localWords
與程序清單4-5中的spamTest
函數幾乎相同,區別在於這裡訪問的是RSS源❷而不是文件。然後調用函數calcMostFreq
來獲得排序最高的30個單詞並隨後將它們移除❸。函數的剩餘部分與spamTest
基本類似,不同的是最後一行要返回下面要用到的值。
你可以註釋掉用於移除高頻詞的三行代碼,然後比較註釋前後的分類性能❸。我自己也嘗試了一下,去掉這幾行代碼之後,我發現錯誤率為54%,而保留這些代碼得到的錯誤率為70%。這裡觀察到的一個有趣現象是,這些留言中出現次數最多的前30個詞涵蓋了所有用詞的30%。我在進行測試的時候,vocabList
的大小約為3000個詞。也就是說,詞彙表中的一小部分單詞卻佔據了所有文本用詞的一大部分。產生這種現象的原因是因為語言中大部分都是冗余和結構輔助性內容。另一個常用的方法是不僅移除高頻詞,同時從某個預定詞表中移除結構上的輔助詞。該詞表稱為停用詞表(stop word list),目前可以找到許多停用詞表(在本書寫作期間,http://www.ranks.nl/resources/stopwords.html 上有一個很好的多語言停用詞列表)。
將程序清單4-6中的代碼加入到bayes.py
文件之後,可以通過輸入如下命令在Python中進行測試:
>>> reload(bayes)
<module \'bayes\' from \'bayes.py\'>
>>>ny=feedparser.parse(\'http://newyork.craigslist.org/stp/index.rss\')
>>>sf=feedparser.parse(\'http://sfbay.craigslist.org/stp/index.rss\')
>>> vocabList,pSF,pNY=bayes.localWords(ny,sf)
the error rate is: 0.1
>>> vocabList,pSF,pNY=bayes.localWords(ny,sf)
the error rate is: 0.35
為了得到錯誤率的精確估計,應該多次進行上述實驗,然後取平均值。這裡的錯誤率要遠高於垃圾郵件中的錯誤率。由於這裡關注的是單詞概率而不是實際分類,因此這個問題倒不嚴重。可以通過函數caclMostFreq
改變要移除的單詞數目,然後觀察錯誤率的變化情況。
4.7.2 分析數據:顯示地域相關的用詞
可以先對向量pSF與pNY進行排序,然後按照順序將詞打印出來。下面的最後一段代碼會完成這部分工作。再次打開bayes.py文件,將下面的代碼添加到文件中。
程序清單4-7 最具表徵性的詞彙顯示函數
def getTopWords(ny,sf):
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**
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 **\"
for item in sortedNY:
print item[0]
程序清單4-7中的函數getTopWords
使用兩個RSS源作為輸入,然後訓練並測試樸素貝葉斯分類器,返回使用的概率值。然後創建兩個列表用於元組的存儲。與之前返回排名最高的X個單詞不同,這裡可以返回大於某個閾值的所有詞。這些元組會按照它們的條件概率進行排序。
下面看一下實際的運行效果,保存bayes.py文件,在Python提示符下輸入:
>>> reload(bayes)
<module \'bayes\' from \'bayes.pyc\'>
>>> bayes.getTopWords(ny,sf)
the error rate is: 0.2
SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**
love
time
will
there
hit
send
francisco
female
NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**
friend
people
will
single
sex
female
night
420
relationship
play
hope
最後輸出的單詞很有意思。值得注意的現象是,程序輸出了大量的停用詞。移除固定的停用詞看看結果會如何變化也十分有趣。依我的經驗來看,這樣做的話,分類錯誤率也會降低。