python機器學習實戰(三)
版權聲明:本文為博主原創文章,轉載請指明轉載地址
www.cnblogs.com/fydeblog/p/7277205.html
前言
這篇博客是關於機器學習中基於概率論的分類方法--朴素貝葉斯,內容包括朴素貝葉斯分類器,垃圾郵件的分類,解析RSS源數據以及用朴素貝葉斯來分析不同地區的態度.
操作系統:ubuntu14.04 運行環境:anaconda-python2.7-jupyter notebook 參考書籍:機器學習實戰和源碼,機器學習(周志華) notebook writer ----方陽
注意事項:在這里說一句,默認環境python2.7的notebook,用python3.6的會出問題,還有我的目錄可能跟你們的不一樣,你們自己跑的時候記得改目錄,我會把notebook和代碼以及數據集放到結尾的百度雲盤,方便你們下載!
1. 基於貝葉斯決策理論的分類方法
朴素貝葉斯的特點
優 點: 在數據較少的情況下仍然有效,可以處理多類別問題。
缺 點: 對於輸入數據的准備方式較為敏感。
適用數據類型:標稱型數據。
貝葉斯決策理論的核心思想:選擇具有最高概率的決策。(最小化每個樣本的條件風險,則總體風險也就最小,就是選擇最高概率,減小風險)
2. 條件概率
2.1 簡單回顧
條件概率在朴素貝葉斯里面是必不可少的一環,下面來簡單介紹介紹
假設現在有一個裝了7塊石頭的罐子,其中3塊是灰色的, 4塊是黑色的 。如果從罐子中隨機取出一塊石頭,那么是灰色石頭的可能性是多少? 由於取石頭有 7 種可能 ,其中 3種為灰色 ,所以取出灰色石頭的概率為 3/7 。那么取到黑色石頭的概率又是多少呢?很顯然 ,是4/7 。
如果這7塊石頭放在兩個桶中,那么上述概率應該如何計算? (設兩個桶分為A,B,A桶裝了2個灰色和2個黑色的石頭,B桶裝了1個灰色和2個黑色的石頭)
要計算P(gray)或者P(black) ,事先得知道石頭所在桶的信息會不會改變結果?你有可能巳經想到計算從B桶中取到灰色石頭的概率的辦法,這就是所謂的條件概率.
來計算P(gray|bucketB),這個是條件概率,在已知是從B桶拿出石頭的條件下,拿到灰色石頭的概率。
計算公式:P(gray|bucketB) = P(gray and bucketB) / P(bucketB) (將兩者同時發生的概率除以前提條件發生的概率)
我們知道P(bucketB)就是3/7,B桶的石頭數/總石頭數, P(gray and bucketB) 是1/7,B桶中的灰色石頭數/總石頭數,所以P(gray|bucketB) = 1/3
這里說一下P(gray and bucketB) ,它等於P(bucketB|gray)乘以P(gray)的,先發生gray,然后在gray的基礎上發生bucketB,就是gray and bucketB
所以這里的公式還可以變一下,P(gray|bucketB) = P(gray and bucketB) / P(bucketB) =P(bucketB|gray)* P(gray) / P(bucketB)
一般情況下,寫成 p(c|x) = p(x|c)* p(c) / p(x) 這就是貝葉斯准則
2.2 使用條件概率進行分類
貝葉斯決策論中真正比較的是條件概率p(c1|x,y)和p(c2|x,y),這些符號所代表的具體意義是,給定某個由x,y表示的數據點,想知道該數據點來自類別c1的概率是多少?數據點來自類別c2的概率又是多少?
如果 p(c1|x,y) > p(c2|x,y) ,屬於類別c1 如果 p(c2|x,y) > p(c1|x,y) ,屬於類別c2
這些概率可以有2.1的貝葉斯准則計算
3. 使用朴素貝葉斯進行留言分類
朴素貝葉斯的一般過程
(1) 收集數據:可以使用任何方法。本章使用RSS源。
(2) 准備數據:需要數值型或者布爾型數據。
(3) 分析數據:有大量特征時,繪制特征作用不大,此時使用直方圖效果更好。
(4) 訓練算法:計算不同的獨立特征的條件概率。
(5) 測試算法:計算錯誤率。
(6) 使用算法:一個常見的朴素貝葉斯應用是文檔分類。可以在任意的分類場景中使用朴素貝葉斯命類器,不一定非要是文本
朴素貝葉斯的兩個假設 (1) 特征之間是統計獨立的,即一個特征或者單詞出現的可能性與它和其他單詞相鄰沒有關系。 (2) 每個特征同等重要。
以上兩個假設是有問題的,不夠嚴謹,但處理方便,實際效果卻很好。
3.1 准備數據:從文本中構建詞向量
詞表到向量的轉換函數如下:
1 def loadDataSet():
2 postingList=[['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
3 ['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
4 ['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
5 ['stop', 'posting', 'stupid', 'worthless', 'garbage'],
6 ['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
7 ['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
8 classVec = [0,1,0,1,0,1] #1 is abusive, 0 not
9 return postingList,classVec
10
11 def createVocabList(dataSet):
12 vocabSet = set([]) #create empty set
13 for document in dataSet:
14 vocabSet = vocabSet | set(document) #union of the two sets
15 return list(vocabSet)
16
17 def setOfWords2Vec(vocabList, inputSet):
18 returnVec = [0]*len(vocabList)
19 for word in inputSet:
20 if word in vocabList:
21 returnVec[vocabList.index(word)] = 1
22 else: print "the word: %s is not in my Vocabulary!" % word
23 return returnVec
第一個loadDataSet函數是返回詞條切分后的文檔集合postlist(選自斑點犬愛好者留言板)和類別標簽集合classvec(1代表侮辱,0則是正常言論)
第二個createVocabList函數會返回輸入數據集所有不重復詞匯的列表
第三個setOfWords2Vec函數的功能是遍歷輸入vocablist的所有單詞,如果當初出現了InputSet中的單詞,returnVec對應位數的值返回1,無則返回0
簡單來講,第一個函數的作用是界定訓練類別,看之后的文檔是否含有類別中的詞匯,第二個函數的作用是將一篇文檔做成列表,方便后面進行標記。第三個函數則是將第二個函數生成的列表根據第一個類別詞匯進行標記,將單詞轉化成數字,方便后面計算條件概率。
測試一下吧(所有函數都放在bayes中)
cd 桌面/machinelearninginaction/Ch04
/home/fangyang/桌面/machinelearninginaction/Ch04
listOPosts,listClasses = bayes.loadDataSet()
myVocabList = bayes.createVocabList(listOPosts)

bayes.setOfWords2Vec(myVocabList,listOPosts[0])

bayes.setOfWords2Vec(myVocabList,listOPosts[3])

3.2 訓練算法 :從詞向量計算概率
根據上面介紹的三個函數,我們知道如何將一組單詞轉換為一組數字,也知道一個詞是否出現在一篇文檔中。現在已知文檔的類別,讓我們使用轉換得到的數字來計算條件概率吧
還是根據上面的貝葉斯准則來計算條件概率,不過公式會有一點不一樣
p(ci|w) = p(w|ci)* p(ci) / p(w) (這里的ci表示所屬類別,這里有兩種可能性1和0,w為向量,由多個數值組成)
我們根據上面的公式對每個類進行計算,然后比較這兩個概率值的大小。計算過程如下
首先可以通過類別 i ( 侮辱性留言或非侮辱性留言)中文檔數除以總的文檔數來計算概率p(ci),接下來計算p(w|ci),由於p(w|ci) = p(w0,w1,w2..wn|ci),又因為所有詞都相互獨立,所以p(w|ci) = p(w0|ci)p(w1|ci)p(w2|ci)...p(wn|ci)
於是函數的偽代碼相應如下
計算每個類別中的文檔數目
對每篇訓練文檔:
對每個類別:
如果詞條出現文檔中―增加該詞條的計數值
增加所有詞條的計數值
對每個類別:
對每個詞條:
將該詞條的數目除以總詞條數目得到條件概率
返回每個類別的條件概率
參考代碼如下
1 def trainNB0(trainMatrix,trainCategory):
2 numTrainDocs = len(trainMatrix)
3 numWords = len(trainMatrix[0])
4 pAbusive = sum(trainCategory)/float(numTrainDocs)
5 p0Num = zeros(numWords); p1Num = zeros(numWords)
6 p0Denom = 0.0; p1Denom = 0.0
7 for i in range(numTrainDocs):
8 if trainCategory[i] == 1:
9 p1Num += trainMatrix[i]
10 p1Denom += sum(trainMatrix[i])
11 else:
12 p0Num += trainMatrix[i]
13 p0Denom += sum(trainMatrix[i])
14 p1Vect = p1Num/p1Denom
15 p0Vect = p0Num/p0Denom
16 return p0Vect,p1Vect,pAbusive
輸入的trainMatrix是文檔經過setOfWords2Vec函數轉換后的列表,trainCategory是每篇文檔構成類別標簽向量。輸出是返回每個類別的概率,pAbusive等於類別和除以訓練的樣本數,這個就是說明一下文檔類別的概率分布,沒有什么其他意思
由於要算每一個詞語的概率,這里用到里numpy的array數組,可以很方便的計算每個詞語的概率,即是用p0Num和p1Num來統計不同類別樣本的詞語所出現的次數,最后對每個元素除以該類別中的總詞數
來測試一下吧
<module 'bayes' from 'bayes.py'>
listOPosts,listClasses = bayes.loadDataSet()
myVocabList = bayes.createVocabList(listOPosts)
for postinDoc in listOPosts:
trainMat.append(bayes.setOfWords2Vec(myVocabList,postinDoc))
p0V , p1V, pAb = bayes.trainNB0(trainMat,listClasses)


看一看在給定文檔類別條件下詞匯表中單詞的出現概率, 看看是否正確.
詞匯表中的第一個詞是cute , 其在類別 0中出現1次 ,而在類別1中從未出現。對應的條件概率分別為 0.04166667 與 0.0,該計算是正確的
我們找找所有概率中的最大值,該值出現在p(1)數組第21個下標位置,大小為 0.15789474.可以查到該單詞是stupid,這意味着它最能表征類別1的單詞。
3.3 測試算法:根據現實情況修改分類器
利用貝葉斯分類器進行文檔文類時,要計算每個元素的條件概率並相乘,若其中有一個概率值等於0,那么最后的乘積也為0,為降低這種影響,可以將所有詞的出現數初始化為1 ,並將分母初始化為2 。
相應的trainNB0()的第4行和第5行修改為:
p0Num = ones(numWords); p1Num = ones(numWords) #change to ones()
p0Denom = 2.0; p1Denom = 2.0 #change to 2.0
另一個問題是向下溢出,乘積p(w0|ci)p(w1|ci)p(w2|ci)...p(wn|ci)太小的緣故 解決的辦法是對乘積取對數
相應的trainNB0()的第13行和第14行修改為:
p1Vect = log(p1Num/p1Denom) #change to log()
p0Vect = log(p0Num/p0Denom) #change to log()
將更改好的函數命名為trainNB0_change
現在已經准備好構建完整的分類器了。當使用numpy向量處理功能時 , 這一切變得十分簡單.
參考代碼如下:
1 def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
2 p1 = sum(vec2Classify * p1Vec) + log(pClass1) #element-wise mult
3 p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)
4 if p1 > p0:
5 return 1
6 else:
7 return 0
8 def testingNB():
9 listOPosts,listClasses = loadDataSet()
10 myVocabList = createVocabList(listOPosts)
11 trainMat=[]
12 for postinDoc in listOPosts:
13 trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
14 p0V,p1V,pAb = trainNB0_change(array(trainMat),array(listClasses))
15 testEntry = ['love', 'my', 'dalmation']
16 thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
17 print testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb)
18 testEntry = ['stupid', 'garbage']
19 thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
20 print testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb)
第一個函數就是兩個類別的條件概率進行比較,輸出最終的類別信息。 第二個函數就是一個測試函數,函數前面部分跟上面一樣,后面引入兩個測試樣本,進行分類。
<module 'bayes' from 'bayes.pyc'>
['love', 'my', 'dalmation'] classified as: 0
['stupid', 'garbage'] classified as: 1
3.4 文檔詞袋模型
我們將每個詞的出現與否作為一個特征,這可以被描述為詞集模型,上面就是詞集模型。
如果一個詞在文檔中出現不止一次,這可能意味着包含該詞是否出現在文檔中所不能表達的某種信息,這種方法被稱為詞袋模型。
詞集和詞袋的區別:在詞袋中,每個單詞可以出現多次 ,而在詞集中,每個詞只能出現一次。
為適應詞袋模型 ,需要對函數setOfWords2Vec稍加修改,修改后的函數為bagOfWords2Vec,代碼如下:
1 def bagOfWords2VecMN(vocabList, inputSet):
2 returnVec = [0]*len(vocabList)
3 for word in inputSet:
4 if word in vocabList:
5 returnVec[vocabList.index(word)] += 1
6 return returnVec
這個返回的列表表現的是單詞出現的次數,還不再是是否出現
4. 使用朴素貝葉斯過濾垃圾郵件
4.1 准備數據:切分文本
前面介紹的詞向量是直接給定的,下面來介紹如何從文本中構建自己的詞列表
先從一個文本字符串介紹
mySent = ' This book is the best book on python or M.L. I have ever laid eyes upon.'

可以看到, 切分的結果不錯, 但是標點符號也被當成了詞的一部分.
解決方法:可以使用正則表示式來切分句子 ,其中分隔符是除單詞、數字外的任意字符串
regEx = re.compile('\\W*')
listOfTokens = regEx.split(mySent)

可以看到里面的標點沒有了,但剩下一些空字符,還要進行一步,去掉這些空字符。
[tok for tok in listOfTokens if len(tok) >0]

空字符消掉了,我們可以看到,有的詞首字母是大寫的,這對句子查找很有用,但我們是構建詞袋模型,所以還是希望格式統一,還要處理一下
[tok.lower() for tok in listOfTokens if len(tok) >0]

可以看到大寫全部變成了小寫,如果是想從小寫變成大寫,只需將tok.lower()改成top.upper()即可
我們構建一個testParse函數,來切分文本,代碼如下
1 def textParse(bigString): #input is big string, #output is word list
2 import re
3 listOfTokens = re.split(r'\W*', bigString)
4 return [tok.lower() for tok in listOfTokens if len(tok) > 2]
4.2 測試算法:使用朴素貝葉斯進行交叉驗證
參考代碼如下:
1 def spamTest():
2 docList=[]; classList = []; fullText =[]
3 for i in range(1,26):
4 wordList = textParse(open('email/spam/%d.txt' % i).read())
5 docList.append(wordList)
6 fullText.extend(wordList)
7 classList.append(1)
8 wordList = textParse(open('email/ham/%d.txt' % i).read())
9 docList.append(wordList)
10 fullText.extend(wordList)
11 classList.append(0)
12 vocabList = createVocabList(docList)#create vocabulary
13 trainingSet = range(50); testSet=[] #create test set
14 for i in range(10):
15 randIndex = int(random.uniform(0,len(trainingSet)))
16 testSet.append(trainingSet[randIndex])
17 del(trainingSet[randIndex])
18 trainMat=[]; trainClasses = []
19 for docIndex in trainingSet:#train the classifier (get probs) trainNB0
20 trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
21 trainClasses.append(classList[docIndex])
22 p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
23 errorCount = 0
24 for docIndex in testSet: #classify the remaining items
25 wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
26 if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
27 errorCount += 1
28 print "classification error",docList[docIndex]
29 print 'the error rate is: ',float(errorCount)/len(testSet)
30 #return vocabList,fullText
第一個循環是對垃圾郵件和非垃圾郵件進行切分,然后生成詞列表和類標簽
第二個循環是0到50個數中隨機生成10個序號
第三個循環是將第二個循環得到的序號映射到詞列表,得到訓練集和相應的類別,然后進行訓練算法
第四個循環是進行錯誤率計算,分類出的類別與實際類別相比較,累計錯誤的樣本數,最后除以總數,得到錯誤率
the error rate is: 0.0

每次運行得出的結果可能不太一樣,因為是隨機選的序號
5. 使用朴素貝葉斯分類器從個人廣告中獲取區域傾向
在這個最后的例子當中,我們將分別從美國的兩個城市中選取一些人,通過分析這些人發布的征婚廣告信息,來比較這兩個城市的人們在廣告用詞上是否不同。如果結論確實是不同,那么他們各自常用的詞是哪些?從人們的用詞當中,我們能否對不同城市的人所關心的內容有所了解?
下面將使用來自不同城市的廣告訓練一個分類器,然后觀察分類器的效果。我們的目的並不是使用該分類器進行分類,而是通過觀察單詞和條件概率值來發現與特定城市相關的內容。
5.1 收集數據:導入RSS源
接下來要做的第一件事是使用python下載文本,而利用RSS,這很容易得到,而Universal Feed Parser 是python最常用的RSS程序庫。
由於python默認不會安裝feedparser,所以需要自己手動安裝,這里附上ubuntu下的安裝方法
第一步:wget http://pypi.python.org/packages/source/f/feedparser/feedparser-5.1.3.tar.gz#md5=f2253de78085a1d5738f626fcc1d8f71
第二步:tar zxf feedparser-5.1.3.tar.gz
第三步:cd feedparser-5.1.3
第四步:python setup.py install
具體可以看到這個鏈接:blog.csdn.net/tinkle181129/article/details/45343267
相關文檔:http://code.google.com/p/feedparser/
ny = feedparser.parse('http://newyork.craigslist.org/stp/index.rss')
上面是打開了Craigslist上的RSS源,要訪問所有條目的列表,輸入以下代碼

Out:25
可以構建一個類似spamTest的函數來對測試過程自動化
1 def calcMostFreq(vocabList,fullText):
2 import operator
3 freqDict = {}
4 for token in vocabList:
5 freqDict[token]=fullText.count(token)
6 sortedFreq = sorted(freqDict.iteritems(), key=operator.itemgetter(1), reverse=True)
7 return sortedFreq[:30]
8
9 def localWords(feed1,feed0):
10 import feedparser
11 docList=[]; classList = []; fullText =[]
12 minLen = min(len(feed1['entries']),len(feed0['entries']))
13 for i in range(minLen):
14 wordList = textParse(feed1['entries'][i]['summary'])
15 docList.append(wordList)
16 fullText.extend(wordList)
17 classList.append(1) #NY is class 1
18 wordList = textParse(feed0['entries'][i]['summary'])
19 docList.append(wordList)
20 fullText.extend(wordList)
21 classList.append(0)
22 vocabList = createVocabList(docList)#create vocabulary
23 top30Words = calcMostFreq(vocabList,fullText) #remove top 30 words
24 for pairW in top30Words:
25 if pairW[0] in vocabList: vocabList.remove(pairW[0])
26 trainingSet = range(2*minLen); testSet=[] #create test set
27 for i in range(20):
28 randIndex = int(random.uniform(0,len(trainingSet)))
29 testSet.append(trainingSet[randIndex])
30 del(trainingSet[randIndex])
31 trainMat=[]; trainClasses = []
32 for docIndex in trainingSet:#train the classifier (get probs) trainNB0
33 trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
34 trainClasses.append(classList[docIndex])
35 p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
36 errorCount = 0
37 for docIndex in testSet: #classify the remaining items
38 wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
39 if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
40 errorCount += 1
41 print 'the error rate is: ',float(errorCount)/len(testSet)
42 return vocabList,p0V,p1V
localWords函數與之前介紹的spamTest函數類似,不同的是它是使用兩個RSS作為參數。
上面還新增了一個輔助函數calcMostFreq,該函數遍歷詞匯表中的每個詞並統計它在文本中出現的次數,然后根據出現次數從高到低對詞典進行排序 , 最后返回排序最高的30個單詞
下面來測試一下
cd 桌面/machinelearninginaction/Ch04
/home/fangyang/桌面/machinelearninginaction/Ch04
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)
vocabList,pSF,pNY = bayes.localWords(ny,sf)
我們會發現這里的錯誤率要遠高於垃圾郵件中的錯誤率,這是因為這里關注的是單詞概率而不是實際分類,可以通過calcMostFreq函數改變移除單詞數,降低錯誤率,因為次數最多的前30個單詞涵蓋了所有用詞的30%,產生這種現象的原因是語言中大部分都是冗余和結構輔助性內容。
5.2 分析數據:顯示地域相關的用詞
將pSF和pNY進行排序,然后按照順序將詞打印出來,這里用getTopWords函數表示這個功能
1 def getTopWords(ny,sf):
2 import operator
3 vocabList,p0V,p1V=localWords(ny,sf)
4 topNY=[]; topSF=[]
5 for i in range(len(p0V)):
6 if p0V[i] > -6.0 : topSF.append((vocabList[i],p0V[i]))
7 if p1V[i] > -6.0 : topNY.append((vocabList[i],p1V[i]))
8 sortedSF = sorted(topSF, key=lambda pair: pair[1], reverse=True)
9 print "SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**"
10 for item in sortedSF:
11 print item[0]
12 sortedNY = sorted(topNY, key=lambda pair: pair[1], reverse=True)
13 print "NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**"
14 for item in sortedNY:
15 print item[0]
輸入是兩個RSS源,然后訓練並測試朴素貝葉斯分類器,返回使用的概率值然后創建兩個列表用於元組的存儲。與之前返回排名最高的x個單詞不同,這里可以返回大於某個閾值的所有詞。這些元組會按照它們的條件概率進行排序。

值得注意的現象是,程序輸出了大量的停用詞。移除固定的停用詞(比如 there等等)看看結果會如何變化,依本書作者的經驗來看,這樣會使分類錯誤率降低。
小結
(1)對於分類而言,使用概率有時要比使用硬規則更為有效
(2)貝葉斯概率及貝葉斯准則提供了一種利用已知值來估計未知概率的有效方法
(3)獨立性假設是指一個詞的出現概率並不依賴於文檔中的其他詞,這個假設過於簡單。這就是之所以稱為朴素貝葉斯的原因。
(4)下溢出就是其中一個問題,它可以通過對概率取對數來解決
(5)詞袋模型在解決文檔分類問題上比詞集模型有所提高
(6)移除停用詞,可降低錯誤率
(7)花大量時間對切分器進行優化
百度雲鏈接:https://pan.baidu.com/s/1LgKUL7f4ja7mz0js-y62qg
import feedparser