《機器學習實戰》之四——樸素貝葉斯
這裡寫自定義目錄標題
《機器學習實戰》之四——樸素貝葉斯
knn和決策樹要求分類器對輸入給出分類的結果。而『樸素貝葉斯』是給出一個最優的類別猜測結果,同時給出這個猜測的概率估計值,選擇具有高概率的決策值。
一. 數學部分
樸素貝葉斯的核心還是貝葉斯公式,只不過加上了一些假設條件:
以二分類問題為背景:
要判斷物體屬於哪一類,我們可以通過比較p(c1|x)與p(c2|x)的大小,哪個大,我們就認為屬於哪一類!
1. 根據貝葉斯公式
問題轉換為比較:
對於p(c1), p(c2),利用訓練樣本,我們可以通過已知分類,或者手動分類來獲得一個大的樣本中,有多少輸入C1,多少輸入C2,從而計算出p(c1), p(c2),那麼,現在的問題就是計算p(x|c1), p(x|c2)
注意:對於二分類問題來說p(c2)=1-p(c1)
2. 樸素貝葉斯
對於一個物品x,我們可以用x1,x2,…,xn特徵來代表它。即
樸素貝葉斯的假設:假設每個特徵都是相互獨立的。於是有
那這樣問題就簡單多了,利用訓練樣本,我們只需要計算每個類別下每個特徵的條件概率就可以了。
注意:樸素貝葉斯分類器有兩種實現方法:一種是基於貝努力模型實現(該方法任務特徵是同等重要的,所以在實現過程中並不考慮詞在文件中出現的次數);另一種是基於多項式模型實現的(它考慮詞在文件中的出現次數)
二. 準備資料階段
依照機器學習的步驟,首先是準備輸入資料。我們同樣生成一個簡單的訓練資料。新建檔案bayes.py,編輯程式碼如下:
- 轉換為可接受的資料格式
##詞表到向量的轉換函式
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'],
['qiut','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)
- 有了詞彙表,我們就可以通過詞彙表對輸入的資料進行分析了。我們構建一個函式分析輸入資料。該函式的輸入引數為:詞彙表及資料詞條。輸出為何詞彙條同樣大小的向量,其中向量中的值非0即1,表示詞條中是否出現該單詞。
處理過程:建立和等長的向量,遍歷文件中的所有單詞,如果文件在那個出現了詞彙表中的單詞,則將輸出的文件向量中的對應值設為1,具體程式碼如下:
#將輸入的一個文件生成詞向量,當所輸入的文章中的詞在vocablist中出現時,
#就將其對應的向量設定為1,否則維持原來的不變
def setOfWords2Vec(vocabList, inputSet):
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
三. NB訓練函式
##樸素貝葉斯分類器的訓練函式
##輸入引數:trainMatrix表示輸入的文件矩陣,trainCategory表示每篇文件類別標籤 所構成的向量
def trainNB(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) #計算樣本集的樣本數
numWords = len(trainMatrix[0]) #計算出詞彙表中的總詞數
#計算p(c)的概率,這裡指的是分類為1的概率,因為求和之後為0的不考慮
pAbusive = sum(trainCategory)/float(numTrainDocs)
p0Num = np.ones(numWords) #建立包含numWords個0的向量
p1Num = np.ones(numWords)
p0Denom = 2.0
p1Denom = 2.0
for i in range(numTrainDocs):
#統計每類單詞的數目,注意我們這裡討論的是一個二分問題
#所以可以直接用一個if...else...即可,如果分類較多,則需要更改程式碼
if trainCategory[i] == 1:
#如果說歸為1類,那麼把這個訓練樣本的向量和原先已有的向量求和,
#保證了同一位置(表示同一單詞)的數量的疊加,起到計數的作用
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p1Vect = np.log(p1Num/p1Denom) #p1Num是一個向量,而怕p1Denom是一個數,除了之後計算得到的是每一個單詞出現在不同類別的概率
p0Vect = np.log(p0Num/p0Denom)
return p0Vect, p1Vect, pAbusive
針對演算法部分的改進
- 計算概率時,需要計算多個概率的乘積以獲得文件屬於某個類別的概率,即計算p(w0|ci)p(w1|ci)…p(wN|ci),然後當其中任意一項的值為0,那麼最後的乘積也為0.為降低這種影響,
採用拉普拉斯平滑,在分子上新增a(一般為1),分母上新增ka(k表示類別總數),即在這裡將所有詞的出現數初始化為1,並將分母初始化為2*1=2
p0Num=ones(numWords);p1Num=ones(numWords)
p0Denom=2.0;p1Denom=2.0
- 解決下溢位問題
正如上面所述,由於有太多很小的數相乘。計算概率時,由於大部分因子都非常小,最後相乘的結果四捨五入為0,造成下溢位或者得不到準確的結果,所以,我們可以對成績取自然對數,
即求解對數似然概率。這樣,可以避免下溢位或者浮點數舍入導致的錯誤。同時採用自然對數處理不會有任何損失。
p0Vect=log(p0Num/p0Denom);p1Vect=log(p1Num/p1Denom)
四. NB分類函式
#樸素貝葉斯分類函式,對輸入的待分類的文件詞向量vec2Classify,給出分類結果
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
p0 = sum(vec2Classify * p0Vec) + np.log(1.0-pClass1)
if p1>p0:
return 1
else:
return 0
五. 測試NB分類函式
#測試樸素貝葉斯分類函式
def testingNB():
listOPosts, listClasses = loadDataSet()
myVocabList = createVocabList(listOPosts) #生成詞彙表
trainMat = []
for postingDoc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList, postingDoc))
p0V, p1V, pAb = trainNB(np.array(trainMat),np.array(listClasses)) #訓練NB模型,並且得到p0V,p1V,pAb
testEntry = ['love','my','dalmation']
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, "classified as:", classifyNB(thisDoc,p0V,p1V,pAb))
testEntry = ['stupid','garbage']
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, "classified as:", classifyNB(thisDoc,p0V,p1V,pAb))
之前的演算法我們只考慮了單詞出現與否,使用的是一種詞集模型。貝葉斯有兩種實現方式,另一種多項式模型,需要考慮每個單詞出現的次數,就是所謂的詞袋模型。為了適應這種詞袋模型,我們需要對函式setOfWords2Vec作一下修改:
#構造詞袋模型,每當遇到一個詞,會增加詞向量中的對應值,而不只是將對應的數值設為1
def bagOfWords2Vec(vocabList,inputSet):
returnVec = [0]*len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1
return returnVec
好了,至此我們完成了整個分類器的實現.接下來我們同樣適用一個例項,來測試我們的分類器。
六. 使用樸素貝葉斯進行垃圾郵件過濾
- 準備輸入資料:我們讀取兩個郵件資料夾中的內容,並 把其中的每行都切分成可處理的單詞。在檔案中繼續編輯如下程式碼,實現切分文字。
#檔案解析
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): #每個資料夾下都是有25個文字文件,迴圈讀取25次
wordList = textParse(open('email/spam/%d.txt' % i,encoding="ISO-8859-1").read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(1)
wordList = textParse(open('email/ham/%d.txt' %i,encoding="ISO-8859-1").read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(0)
vocabList = createVocabList(docList)
#range(50)表示從0到50,不包括50
trainingSet = range(50)
testSet=[]
#隨機選取10個作為測試集,剩下的作為訓練集
for i in range(10):
randIndex = int(np.random.uniform(0, len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(list(trainingSet)[randIndex])
#訓練NB模型
trainMat=[]
trainClasses=[]
for docIndex in trainingSet:
trainMat.append(setOfWords2Vec(vocabList, docList[docIndex]))
trainClasses.append(classList[docIndex])
p0V,p1V,pSpam=trainNB(np.array(trainMat),np.array(trainClasses))
#測試
errorCount = 0
for docIndex in testSet:
wordVector = setOfWords2Vec(vocabList, docList[docIndex])
if classifyNB(np.array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
errorCount += 1
# print("classification error:",docList[docIndex])
print(errorCount)
print("the error rate is:",float(errorCount)/len(testSet))
# return vocabList, fullText
後記: