《機器學習實戰》程式清單4-2 樸素貝葉斯分類器訓練函式

王明輝發表於2018-02-25

 此文旨在把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.
    0.04166667 0. ]


p1V: [ 0. 0. 0.05263158 0. 0. 0.10526316 0. 0.05263158 0.05263158 0. 0. 0.05263158 0. 0. 0. 0. 0.05263158 0. 0. 0.05263158 0.05263158 0. 0.05263158 0. 0.05263158 0. 0. 0.05263158 0.15789474 0.05263158 0.05263158 0.10526316]


pAb: 0.5

 

對於這個結果,我曾經對作者的說明感到困惑不解。下面列出我經過逐步瞭解後的解釋:

首先,我們發現文件屬於侮辱類的概率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)

相關文章