基於概率論的樸素貝葉斯法的分類方法及其應用(一)

1 基於貝葉斯的決策理論的分類方法

優點:在數據較少的情況下仍然有效,可以處理多類別問題。
缺點:對於輸入數據的準備方式比較敏感。
使用數據類型:標稱型數據。

貝葉斯決策理論

在這裏插入圖片描述
我們現在用p1(x,y)表示數據點(x,y)屬於類別1(圖中用圓點表示的類別)的概率,用p2(x,y)表示數據點(x,y)屬於類別2(圖中用三角形表示的類別)的概率。對於一個新數據點,可以用下面的規則判定它的類別:

  • 如果p1(x,y)>p2(x,y),那麼爲類別1;
  • 如果p1(x,y)<p2(x,y),那麼爲類別2;

也就是說,我們會選擇高概率的類別。這就是貝葉斯決策的核心思想,即選擇具有最高概率的決策。回到圖4.1,如果圖中的整個數據使用6個浮點數來表示,並且計算類別概率的Python代碼只有兩行,那麼你會更傾向於使用哪種方法來對該數據點進行分類呢?

  • 使用kNN法,進行1000次距離計算;
  • 使用決策樹,分別沿X軸、Y軸劃分數據;
  • 計算數據點屬於每個類別的概率,並進行比較。

使用決策時不會非常成功;而和簡單的概率計算相比,kNN計算量太大,因此,對於上述問題的最佳選擇是使用概率比較的方法。

2 條件概率

假設x,a是兩個事件,已知p(x|a),要求p(a|x),那麼可以使用下面的計算方法:
p(ax)=p(xa)p(a)p(x) p(a|x)=\frac{p(x|a)p(a)}{p(x)}

p(a|x):x發生情況下,a發生的概率。
p(x|a):a發生情況下,x發生的概率。
p(x):x發生的概率。
p(a):a發生的概率。

3 使用條件概率來分類

上面提到過按照貝葉斯決策理論計算要求計算兩個概率p1(x,y)和p2(x,y):

  • 如果p1(x,y)>p2(x,y),那麼爲類別1;
  • 如果p1(x,y)<p2(x,y),那麼爲類別2;

實際上,這兩個準則並不是貝葉斯決策理論的全部內容,真正需要計算和比較的是p(c1x,y)p(c_1|x,y)p(c2x,y)p(c_2|x,y),這些符號所代表的意義是:給定某個x,y表示的數據點,那麼該數據點來自類別c1c_1c2c_2的概率是多少呢? 具體的,可以應用貝葉斯準則得到:
p(cix,y)=p(x,yci)p(ci)p(x,y) p(c_i|x,y)=\frac{p(x,y|c_i)p(c_i)}{p(x,y)}

使用這些定義,可以定義貝葉斯分類準則爲:

  • 如果p(c1x,y)&gt;p(c2x,y)p(c_1|x,y)&gt;p(c_2|x,y),那麼屬於類別爲c1c_1
  • 如果p(c1x,y)&lt;p(c2x,y)p(c_1|x,y)&lt;p(c_2|x,y),那麼屬於類別爲c2c_2

使用貝葉斯準則,可以通過已知的三個概率值來計算未知的概率值。

4 使用貝葉斯進行文檔歸類

機器學習的一個重要應用就是文檔的自動分類,在文檔分類中,整個文檔(如一封電子郵件)是實例,而電子郵件中的某些元素則構成特徵。雖然電子郵件是一種會不斷增加的文本,但我們同樣可以對新聞報道、用戶留言、政府公文等其他任意類型的文本進行分類。我們可以觀察文檔中出現的詞,並把每個詞的出現或者不出現作爲一個特徵。這樣得到的特徵樹木就會跟詞彙表中的詞目一樣多。樸素貝葉斯是上節介紹的貝葉斯分類器的一個擴展,是用於文檔分類的常用算法。
使用每個詞作爲特徵並觀察它們是否出現,這樣得到的特徵數目會有多少呢?據估計,僅在英語中,單詞的總數就有500000之多,爲了能進行英語閱讀,估計需要掌握數千單詞。

樸素貝葉斯的一般過程

  1. 收集數據:可以使用任何方法。這裏使用RSS源。
  2. 準備數據:需要數值型或者布爾型數據。
  3. 分析數據:有大量特徵時,繪製特徵作用不大,此時使用直方圖比較好。
  4. 訓練算法:計算不同的獨立特徵的條件概率 。
  5. 測試算法:計算錯誤率。
  6. 使用算法:一個常見的樸素貝葉斯應用是文檔分類。可以在任意的分類場景中使用樸素貝葉斯分類器,不一定是文本。

如果特徵之間相互獨立,那麼樣本數量就可以從N1000N^{1000}減少到1000×N{1000} \times N,所謂獨立,指的是統計意義上的獨立,即一個特徵或者單詞出現的可能性與它和其他單詞相鄰沒有關係。樸素貝葉斯分類器的另一個假設是每個特徵同等重要。其實這個假設也有問題,如果要判斷留言板內容是否得當,不需要去看完1000個單詞,而只需要10-20個特徵就足以做出判斷了。儘管有小瑕疵,但樸素貝葉斯的實際效果很好。

5 使用Python進行文本分類

要從文本中獲取特徵,必須對文本進行拆分。具體如何做呢?這裏的特徵是來自文本的詞條,一個詞條是字符的任意組合。可以吧詞條想像成單詞,也可以使用非單詞詞條,如URL,IP地址或者任意其他字符串。然後將每一個文本片段表示成一個詞條向量,其中值爲1表示詞條出現在文檔中,值爲0表示詞條未出現。
對於在線留言板,建立兩個類別:侮辱類和非侮辱類。使用1和0來表示。

5.1 準備數據:從文本中構建詞向量

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'],
                   ['quit','buying','worthless','dog','food','stupid']
                   ]
    classVec = [0,1,0,1,0,1]  # 1代表侮辱性文字,0代表正常言論
    return postingList, classVec

def createVocalSet(dataSet):
    vocalSet = set([])         #創建一個空集
    for document in dataSet:
        vocalSet = vocalSet | set(document)  # 創建兩個集合的並集
    return list(vocalSet)
    
#詞集模型,標記出現的詞爲1
def setOfWords2Vec(vocalList,inputSet):       #創建一個其中所含元素都爲0的向量
    returnVec = [0] * len(vocalList)
    for word in inputSet:
        if word in vocalList:
            returnVec[vocalList.index(word)] = 1
        else:
            print("the word: %s is not in my vocabulary!" % word)
    return returnVec

第一個函數loadDataSet()創建了一些實驗樣本。該函數返回的第一個變量是進行詞條切分後的文檔集合。第二個變量是一個類別標籤的集合。這裏有兩類,分別爲侮辱性和非侮辱性。這些文本的標註由人工標註,這些標註信息用於訓練程序以便自動檢測侮辱性留言。
第二個函數createVocalSet()會創建一個包含在所有文檔中出現的不重複詞的列表。此處使用了Python的set數據類型。將詞條列表輸給set構造函數,set就會返回一個不重複詞表。首先,創建一個空集合 , 然後將每篇文檔返回的新詞集合添加到該集合中。操作符丨用於求兩個集合的並集,這也是一個按位或(OR ) 操作符。在數學符號表示上,按位或操作與集合求並操作使用相同記號。
第三個函數setOfWords2Vec(),該參數的輸入參數爲詞彙表和某個文檔,輸出的是文檔向量,向量的每一元素爲1或0,分別表示詞彙表中的單詞在輸入文檔中是否出現。函數首先創建一個和詞彙表等長的向量,並將其他元素設置爲0。接着遍歷文檔中的所有單詞,如果出現了詞彙表中的單詞,則將輸出的文檔向量中的對應值設爲1。現在看一下這些函數的執行效果:

if __name__=="__main__":
#     '''
#     steps:
#     1st : create a original data, like txt ,email address,symbols etc. marking the bad message as zero and good message as one
#     2nd :delete the same element in original data and form a new list as vocabulary list.
#     3nd : compare the vocabulary list and original data, marking one once it appeared in vocabulary and marking zero once it didn't appeared
#     '''
	listOPosts,listClasses = loadDataSet()
       myVocabList = createVocalSet(listOPosts)
       print(myVocabList)
       label0 = setOfWords2Vec(myVocabList, listOPosts[0])
       print(label0)
       label1 = setOfWords2Vec(myVocabList, listOPosts[1])
       print(label1)
       result
['stupid', 'buying', 'love', 'licks', 'ate', 'park', 'stop', 'I', 'please', 'quit', 'maybe', 'worthless', 'dog', 'how', 'flea', 'has', 'not', 'mr', 'posting', 'problems', 'steak', 'dalmation', 'help', 'so', 'my', 'garbage', 'food', 'take', 'is', 'to', 'cute', 'him']
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0]
[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]

5.2 訓練算法:從詞向量計算概率

5.1節介紹瞭如何將一組單詞轉化爲一組數字,接下來看看如何使用這些數字計算概率。現在已經知道了一個詞是否出現在一篇文檔中,也知道了該文檔所屬的類別。接下來要計算,給定一篇文檔,判斷它的類別。
我們重寫貝葉斯準則,將之前的x,y轉換爲w\vec{w}w\vec{w}表示一個向量,它由多個數值表示。在這個例子中,數值個數與詞彙表中的詞個數相同。
p(ciw)=p(wci)p(ci)p(w)p(c_i|\vec{w})=\frac{p(\vec{w}|c_i)p(c_i)}{p(\vec{w})}
我們將使用上面的公式,對每個類別進行計算(侮辱性和非侮辱性),然後比較這兩個類別的概率值大小。如何計算呢?

  1. 首先可以通過類別i(侮辱性和非侮辱性留言)中文檔書除以總的文檔數來計算概率p(ci)p(c_i)
  2. 接下來計算p(wci)p(\vec{w}|c_i),這裏要用到樸素貝葉斯假設。如果將w\vec{w}展開爲一個個獨立特徵,那麼就可以將上述概率寫作p(w0,w1,w2,,wNci)p(w_0,w_1,w_2,\dots,w_N|c_i),這裏假設所有詞都相互獨立,該假設也被稱爲條件獨立性假設,它意味着可以使用p(w0ci)p(w1ci)p(w2ci)p(wNci)p(w_0|c_i)p(w_1|c_i)p(w_2|c_i)\dots p(w_N|c_i)來計算上述概率。極大簡化了計算過程。

編碼實現:

def trainNB0(trainMatrix, trainCategory):
    numTrainDocs = len(trainMatrix)
    print("numTrainDocs is",numTrainDocs)
    numWords = len(trainMatrix[0])
    print("numWords is", numWords)
    PAbusive = sum(trainCategory)/float(numTrainDocs)
    print("PAbusive is", PAbusive)
    #做一些修改,避免出現在計算條件概率的時候,因爲其中一個值爲0,導致所有的值爲0
    p0Num = zeros(numWords)                     #1 初始化概率
    p1Num = zeros(numWords)
    p0Denom = 0.0
    p1Denom = 0.0
    for i in range(numTrainDocs):
        if trainCategory[i] == 1:
            p1Num += trainMatrix[i]             #2 向量相加
            p1Denom += sum(trainMatrix[i])
        else:
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    #解決溢出問題,取對數運算
    p1Vect = p1Num / p1Denom                    #3 對每個元素做除法
    p0Vect = p0Num / p0Denom
    return p0Vect,p1Vect,PAbusive

代碼函數的輸入參數爲文檔矩陣trainMatrix,以及由每篇文檔類別標籤構成的向量trainCategory。首先計算文檔屬於侮辱性文檔(class=1)的概率,即p(1)。因爲這是一個二分類問題,故p(0)=1p(1)p(0)=1-p(1)。對於多於兩類的分類問題,則需要對代碼進行修改。
計算p(wic1)p(\vec{w_i}|c_1)p(wic0)p(\vec{w_i}|c_0),需要初始化程序中的分母和分子變量。由於w\vec{w}中元素衆多,因此可以使用Numpy數組來快速計算這些值。上述程序中的分母變量是一個元素個數等於詞彙表大小的Numpy數組,在for循環中,要遍歷訓練集trainMatrix中的所有文檔。一旦某個詞語(侮辱性或正常詞語)在某一文檔中出現,則該詞對應的個數(p1Num或p0Num)就加1,而且在所有的文檔中,該文檔的總詞數也相應加1,對於兩個類別都要進行同樣的計算處理。
最後,對每個元素除以該類別中的總詞數。利用NumPy可以很好的實現,用一個數組除以浮點數即可,接下來是代碼的試驗:

if __name__=="__main__":
trainMat = []
    for postinDoc in listOPosts:
        returnVec = setOfWords2Vec(myVocabList,postinDoc)
        print("Every returnVec is",returnVec)
        trainMat.append(setOfWords2Vec(myVocabList,postinDoc))
    print("trainMat is",trainMat)

    p0V,p1V,pAb = trainNB0(trainMat,listClasses)
    print("p0v is ",p0V)
    print("p1V is ",p1V)
    print("pAb is ",pAb)

Result is as follows:
trainMat is [[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1], [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1], [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1], [1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]]
numTrainDocs is 6
numWords is 32
PAbusive is 0.5
p0v is  [0.         0.04166667 0.04166667 0.04166667 0.04166667 0.04166667
 0.         0.         0.04166667 0.         0.04166667 0.04166667
 0.04166667 0.04166667 0.         0.04166667 0.04166667 0.04166667
 0.         0.         0.         0.04166667 0.04166667 0.04166667
 0.         0.08333333 0.04166667 0.04166667 0.         0.04166667
 0.125      0.        ]
p1V is  [0.05263158 0.         0.         0.         0.10526316 0.
 0.05263158 0.05263158 0.         0.05263158 0.05263158 0.
 0.         0.         0.15789474 0.         0.05263158 0.
 0.10526316 0.05263158 0.05263158 0.         0.         0.
 0.05263158 0.05263158 0.         0.         0.05263158 0.
 0.         0.05263158]
pAb is  0.5

首先,我們發現文檔屬於侮辱類的概率爲0.5是正確的,其他概率也基本是正確的。

5.3 測試算法:根據現實情況修改分類器

利用貝葉斯分類器對文檔進行分類時,要計算多個概率的乘積以獲得文檔屬於某個類別的概率。即計算p(w0ci)p(w1ci)p(w2ci)p(w_0|c_i)p(w_1|c_i)p(w_2|c_i),如果其中一個值爲0,那麼最後乘積也爲0。爲降低這種影響,可以將所以詞的出現數初始化爲1,並將分母初始化爲2,修改trainNB0()中第四行和第五行,

    p0Num = ones(numWords)                    
    p1Num = ones(numWords)
    p0Denom = 2.0
    p1Denom = 2.0

另一個遇到的問題是下溢,這是由於太多很小的數相乘造成的。一種解決辦法是對乘積取自然對數。在代數中有ln(xy)=ln(x)+ln(y)ln(x\ast y)=ln(x)+ln(y),於是通過求對數可以避免下溢出或者浮點數舍入導致的錯誤。同時,採用自然對數進行處理不會有任何損失。給出函數f(x)f(x)ln(f(x))ln(f(x))的曲線。檢查這兩條曲線,就會發現它們在相同區域內同時增加或者減少,並且在相同點上取到極值。它們的取值雖然不同,但不影響最終結果。通過修改代碼,可以實現這個過程:

    p1Vect = log(p1Num / p1Denom)                   
    p0Vect = log(p0Num / p0Denom)

現在已經準備好了構建完整的分類器了。

def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):
    p1 = sum(vec2Classify * p1Vec) + log(pClass1)
    p0 = sum(vec2Classify * p0Vec) + log(1-pClass1)
    if p1 > p0:
        return 1
    else:
        return 0

def testingNB():
    listOPosts,listClasses = loadDataSet()
    myVocabList = createVocalSet(listOPosts)
    trainMat = []
    for postinDoc in listOPosts:
        returnVec = setOfWords2Vec(myVocabList, postinDoc)
        print("Every returnVec is", returnVec)
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    print("trainMat is", trainMat)
    p0V, p1V, pAb = trainNB0(array(trainMat), array(listClasses))
    testEntry = ['love','my','dalmation']
    thisDoc = array(setOfWords2Vec(myVocabList,testEntry))
    print(testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb))
    
Result as follows:
    ['love', 'my', 'dalmation'] classified as:  0

classifyNB有4個輸入:要分類的向量vec2Classify,以及使用函數trainNB0()計算出來的p0V, p1V, pAb的概率。使用NumPy數組來計算兩個向量相乘的結果。這裏的相乘指的是向量中對應的元素相乘,即先將兩個向量中的第一個元素相乘,然後在將第二個元素相乘,以此類推。接下來將詞彙表中所有詞的對應值相加,然後將該值加到類比的對數概率上。最後比較類別的概率返回大概率對應的類別標籤。

5.4 準備數據:文檔詞袋模型

目前爲止,我們將每個詞的出現與否作爲一個特徵,這可以被描述爲詞集模型。如果一個詞在文檔中出現不止一次,這可能意味着包含該詞是否出現在文檔中所不能表達的某種信息,這種方法被稱爲詞袋模型化。在詞袋中,每個單詞可以出現多次 ,而在詞集中,每個詞只能出現一次。爲適應詞袋模型,需要對函數稍加修改,修改後的函數稱爲bagOfWords2Vec()。代碼如下:

def bagOfWords2VecMN(vocalList,inputSet):
    returnVec = [0] * len(vocalList)
    for word in inputSet:
        if word in vocalList:
            returnVec[vocalList.index(word)] += 1
    return returnVec

參考資料

[1] Peter Harrington. 機器學習實戰[M]. 北京:人民郵電出版社,2013:53-65.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章