此文旨在把trainNB0這個函式詳細講清楚。
下面所做的工作都是為了求下面這個貝葉斯概率,也叫條件概率:
為了計算方便,書中的操作實際上是把這個式子轉化為了下式:
概率P(ci)就是通過類別i(侮辱性留言或非侮辱性留言)中文件數除以總的文件數來得到的,也就是最後得到的計算結果0.5。
這裡有一個重要的轉化,因為w是一個詞條向量,它可以展開為[w0, w1, w2,.......wn]。因為我們此例用到的是樸素貝葉斯假設,所以所有詞條都互相獨立,
此假設也稱為條件獨立性假設。那麼就意味著我們可以做這樣的變換:
p(w|ci) == p(w0,w1,w2,......w2|ci) == p(w0|ci)p(w1|ci)p(w2|ci).......p(wn|ci)
然後這部分就可以轉化為 p(w0,w1,w2,......w2|ci)p(ci) /p(w),進一步轉化為:p(w0|ci)p(w1|ci)p(w2|ci).......p(wn|ci)/p(w)
這個轉化,是本例能夠成立的一個必要條件。
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
下面把這個函式逐步分解:
1.引數
此函式的引數有兩個,一個是trainMatrix,另一個是trainCategory,這兩個引數是一步一步的資料處理產生的結果,本節的目的是說明這兩個引數值的產生過程。詳細如下:
1.1第一步 建立實驗樣本
可能是為了簡化操作,突出重點,作者在這裡手工建立了資料集,手工設定了類別,在實際的應用場景中,應當是自動判斷自動生成的。
listOPosts,listClasses = bayes.loadDataSet()
這一句產生了listOPosts和listClasses
詳細內容分別是:
listOPosts:
[['my','dog','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']
]
listClasses:
[0,1,0,1,0,1]
其中的listOPosts即list Of Posts,文件列表,就是帖子列表、郵件列表等等。你可以認為列表中的一元素就是一個帖子或者回復,
在此例中一共6個文件、帖子、回覆(以後統稱文件)。
分別是:
['my','dog','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']
可以看到,2、4、6句標紅部分,存在侮辱性詞條,第1、3、5個句子,不存在侮辱性詞條,所以,對應的類別標籤設定為
listClasses = [0,1,0,1,0,1]
1.2第二步 建立包含所有不重複詞條的集合(詞彙表)
這一步是為了產生一個大而全的集合,這個集合包括了所有文件(即第一步產生的6個文件)中的詞條,但每個詞條都不重複。
#建立一個所有文件中的不重複單詞列表 def createVocabList(dataSet): vocabSet = set([]) #建立一個空集 n = 0 for document in dataSet: vocabSet = vocabSet | set(document) #建立兩個集合的並集 n += 1 # print('vocabSet:',n,vocabSet) # print('文件集合的總長度:',len(vocabSet)) a = list(vocabSet) a.sort()
return a
Python中的集合(set)具有消除重複元素的功能。
書中程式碼沒有排序。為了看得更清楚,我加上了排序。
上述程式碼中的
vocabSet = vocabSet | set(document)
並集操作,相當於 += 操作
此函式的引數dataSet,即是上一步產生的listOPosts
呼叫方式:
myVocablList = createVocabList(listOfPosts)
執行結果是:
['I', 'ate', 'buying', 'cute', 'dalmation', 'dog', 'flea', 'food', 'garbage', 'has', 'help', 'him', 'how', 'is', 'licks', 'love',
'maybe', 'mr', 'my', 'not', 'park', 'please', 'posting', 'problems', 'quit', 'so', 'steak', 'stop', 'stupid', 'take', 'to', 'worthless']
1.3第三步 文件向量
獲得詞彙表後,便可以使用函式setOfWords2Vec(),該函式的輸入引數為詞彙表及某個文件,輸出的是文件向量,向量的每一元素為1或0,分別表示詞彙表中的單詞在輸入文件中是否出現。
def setOfWords2Vec(vocabList, inputSet): returnVec = [0] * len(vocabList)
for word in inputSet: if word in vocabList: # print("word:",word) returnVec[vocabList.index(word)] = 1 else: print("the word:%s is not in my Vocabulary!" % word) return returnVec # 返回一個list
vocabList即上一步產生的詞彙表,inputSet可以是任意一篇文件,此處為了簡化操作,在6篇文件中選取。
呼叫方式:
listOfPosts,listClasses = loadDataSet() print(listOfPosts) myVocablList = createVocabList(listOfPosts) print(myVocablList) l = listOfPosts[0] l.append("中華人民共和國") l.append("kk") print("listOfPosts:", listOfPosts[0]) b = setOfWords2Vec(myVocablList, listOfPosts[0]) print(b)
我們的輸入是:
listOfPosts[0],它的值是:
['my', 'dog', 'dog', 'has', 'flea', 'problems', 'help', 'please']
從索引為0的元素開始迴圈,如果這個元素存在於詞彙表中,則把要返回的類別向量returnVec中對應位置的值設為1。
此處第1個值是my,它存在於詞彙表中,位置是18,所以把returnVec中的對應位置的值設定為1
得到:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
I | ate | buying | cute | dalmation | dog | flea | food | garbage | has | help | him | how | is | licks | love | maybe | mr | my | not | park | please | posting | problems | quit | so | steak | stop | stupid | take | to | worthless |
第2個值是dog,它存在於詞彙表中,位置是5,把returnVec中的對應位置的值設定為1
得到:[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
I | ate | buying | cute | dalmation | dog | flea | food | garbage | has | help | him | how | is | licks | love | maybe | mr | my | not | park | please | posting | problems | quit | so | steak | stop | stupid | take | to | worthless |
以此類推,直到最後得到:
[0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
至此,我們得到了一篇文件listOfPosts[0]的詞向量
用同樣的方式,我們還可以得到listOfPosts[1]、listOfPosts[2]、listOfPosts[3]、listOfPosts[4]、listOfPosts[5]文件的詞向量,分別是:
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0]
[1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1]
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0]
[0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1]
1.4第四步
至此,我們可以說明trainNB0(trainMatrix, trainCategory)中的引數是什麼了。
trainMatrix就是由各個文件轉化成的詞向量構成的矩陣,而trainCategory就是這幾個文件的類別,也就是這幾個文件是不是含有侮辱性詞條。
trainMatrix的值為:
[
[0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0],
[1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0],
[0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1]
]
trainCategory的值為:
[0, 1, 0, 1, 0, 1]
2.過程
numTrainDocs = len(trainMatrix)
這句是取得詞向量矩陣的長度,也就是說文件的數量,此例中是6個。
numWords = len(trainMatrix[0])
取得詞向量矩陣中第一條記錄的長度,也就是詞條(即特徵)的數量,此例應當是32個。
pAbusive = sum(trainCategory)/float(numTrainDocs)
p表示概率,abusive的意思是辱罵的、濫用的,pAbusive表示辱罵文件的概率。這個值即是第一節的公式中所需要的P(Ci),是通過類別i(侮辱性留言或非侮辱性留言)中文件數除以總的文件數來計算的。
sum(trainCategory) ==> sum([0, 1, 0, 1, 0, 1]) ==> 3
此處用==>符號表示“推出”、“等於”
numTrainDocs==6
所以
pAbusive = sum(trainCategory)/float(numTrainDocs)==>
pAbusive == 3/6 ==>
pAbusive == 0.5
也就是說,6篇文件,其中有3篇含有侮辱性詞條,概率是0.5,即P(C1)==0.5。
需要求的3個值,已經求出了一個,還需要P(w|Ci)和P(w)兩個值。
p0Num = zeros(numWords)
p1Num = zeros(numWords)
上面這兩句是要初始化一個概率,是什麼概率?
p0Denom = 0.0; p1Denom = 0.0
上式中的Denom是分母的意思,把分母項置為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])
按照文件個數,從0到5迴圈。
如果文件類別是侮辱性(trainCategory[i] == 1),則把侮辱性文件的詞向量相疊加,否則把非侮辱性文件的詞向量相疊加。這樣說有點拗口,看看下面的實際執行過程:
由前面的計算結果可知,trainCategory的值是[0, 1, 0, 1, 0, 1],放在這裡看著方便。
i==0時:
trainCategory[0]是0,
所以p0Num += trainMatrix[0],
而trainMatrix[0]是[0, 0, 0, 0, 0, 1[5], 1[6], 0, 0, 1[9], 1[10], 0, 0, 0, 0, 0, 0, 0, 1[18], 0, 0, 1[21], 0, 1[22], 0, 0, 0, 0, 0, 0, 0, 0]
為了方便比較,我在列表中增加了中括號括起來的索引值
同時,p0Denom += sum(trainMatrix[0]),trainMatrix[0]中有7個1,所以此時p0Denom的值是7
i==1時:
trainCategory[1]是1,
所以p1Num += trainMatrix[1],
而trainMatrix[1]是[0, 0, 0, 0, 0, 1[5], 0, 0, 0, 0, 0, 1[11], 0, 0, 0, 0, 1[16], 0, 0, 1[19], 1[20], 0, 0, 0, 0, 0, 0, 0, 1[28], 1[29], 1[30], 0]
同時,p1Denom += sum(trainMatrix[1]),trainMatrix[1]中有8個1,所以此時p1Denom的值是8
i==2時:
trainCategory[2]是0,所以p0Num += trainMatrix[2]
而trainMatrix[2]是[1[0], 0, 0, 1[3], 1[4], 0, 0, 0, 0, 0, 0, 1[5], 0, 1[7], 0, 1[9], 0, 0, 1[12], 0, 0, 0, 0, 0, 0, 1[19], 0, 0, 0, 0, 0, 0]
疊加之後,p0Num的值為:[ 1. 0. 0. 1. 1. 1. 1. 0. 0. 1. 1. 1. 0. 1. 0. 1. 0. 0.2. 0. 0. 1. 0. 1. 0. 1. 0. 0. 0. 0. 0. 0.]
可以看到,是列表中的每個位置對應的值相加。
同時,p0Denom += sum(trainMatrix[2]),trainMatrix[2]中有8個1,所以此時p0Denom的值是7+8=15
以此類推,最後的結果是:
p0Num == [ 1. 1. 0. 1. 1. 1. 1. 0. 0. 1. 1. 2. 1. 1. 1. 1. 0. 1.3. 0. 0. 1. 0. 1. 0. 1. 1. 1. 0. 0. 1. 0.]
p1Num == [ 0. 0. 1. 0. 0. 2. 0. 1. 1. 0. 0. 1. 0. 0. 0. 0. 1. 0.0. 1. 1. 0. 1. 0. 1. 0. 0. 1. 3. 1. 1. 2.]
p0Denom==24
p1Denom==19
插播一句,發現了一個翻譯錯誤: 英文版第70頁,原文是The numerator is a NumPy array with the same number of elements as you have words in your vocabulary. 中文版第61頁,譯文是“上述程式中的分母變數是一個元素個數等於詞彙表大小的NumPy陣列。”
應改為:“上述程式中的分子變數是一個元素個數等於詞彙表大小的NumPy陣列。”
執行結果如下:
p0V: [ 0.04166667 0.04166667 0. 0.04166667 0.04166667 0.04166667 0.04166667 0. 0. 0.04166667 0.04166667 0.08333333 0.04166667 0.04166667 0.04166667 0.04166667 0. 0.04166667 0.125 0. 0. 0.04166667 0. 0.04166667 0. 0.04166667 0.04166667 0.04166667 0. 0.
|
對於這個結果,我曾經對作者的說明感到困惑不解。下面列出我經過逐步瞭解後的解釋:
首先,我們發現文件屬於侮辱類的概率pAb為0.5,該值是正確的。
接下來,看一看在給定文件類別條件下詞彙表中單詞的出現概率,看看是否正確。
詞彙表中的第一個詞是cute,其在類別0中出現1次,而在類別1中從未出現。對應的條件概率分別為0.041 666 67與0.0。該計算是正確的。
我們找找所有概率中的最大值,該值出現在P(1)陣列第26個下標位置,大小為0.15789474。在myVocabList的第26個下標位置上可以查到該單詞是stupid。
這意味著stupid是最能表徵類別1(侮辱性文件類)的單詞。
第一句說,“我們發現文件屬於侮辱類的概率pAb為0.5,該值是正確的。”,0.5這個數值的來源是清楚的,但此處作者做了一個診斷,說該值是正確的,是什麼意思?一直沒太明白。
可能一:有3個非侮辱,3個侮辱,所以概率是0.5,正確的。
可能二:經過計算,和我們肉眼可見的3/6符合,所以結果是正確的。如果是這樣,那這是一句廢話,本來就是按照這個演算法計算的,何必要強調一下。
還有其它可能嗎?待定,也許將來更加深入以後會知道。
第二句,接下來,看一看在給定文件類別條件下詞彙表中單詞的出現概率,看看是否正確。這裡指的是P(w|ci)
第三句,最大值是0.15789474,對應的詞條是stupid,它在類別為1的類別中出現了3次,所以它是最能表往類別1的詞條。此處存疑。
如果把3個stupid分別改成stupid、fuck、shit,那麼它就會和其它只出現1一次的詞條一樣,值變為0.05263158。
這個時候,誰是更能突出表徵類別1的詞條?
這一節只是把詞條的出現概率計算完畢,沒有完成整個算式。
4.5.3 測試演算法:根據現實情況修改分類器
前面所提到的概率公式轉化結果p(w0|ci)p(w1|ci)p(w2|ci).......p(wn|ci)/p(w)的意義是,列表內部的概率相乘,得到的積除以p(w)。
如果有概率為0,那麼乘積就是0,憑經驗也可以知道這是不合理的,對於這個問題,書中給出了一個方法,將所有詞的出現數初始化為1,並將分母初始化為2。
書中只給出了方法,並沒有解釋為什麼這麼做。後經查詢,這種方法叫拉普拉斯平滑。來源:https://www.cnblogs.com/knownx/p/7860174.html
背景:為什麼要做平滑處理?
零概率問題,就是在計算例項的概率時,如果某個量x,在觀察樣本庫(訓練集)中沒有出現過,會導致整個例項的概率結果是0。在文字分類的問題中,當一個詞語沒有在訓練樣本中出現,該詞語調概率為0,使用連乘計算文字出現概率時也為0。這是不合理的,不能因為一個事件沒有觀察到就武斷的認為該事件的概率是0。
拉普拉斯的理論支撐
為了解決零概率的問題,法國數學家拉普拉斯最早提出用加1的方法估計沒有出現過的現象的概率,所以加法平滑也叫做拉普拉斯平滑。
假定訓練樣本很大時,每個分量x的計數加1造成的估計概率變化可以忽略不計,但可以方便有效的避免零概率問題。
它的背後的原理就是當數量特別龐大時,個體就沒有那麼重要。99%和100%在概率上來講也沒什麼區別。
p(w0|ci)p(w1|ci)p(w2|ci).......p(wn|ci)