[白話解析] 深入淺出樸素貝葉斯模型原理及應用

0x00 摘要

樸素貝葉斯模型是機器學習中經常提到的概念。但是相信很多朋友都是知其然而不知其所以然。本文將盡量使用易懂的方式介紹樸素貝葉斯模型原理,並且通過具體應用場景和原始碼來幫助大家深入理解這個概念。

0x01 IT相關概念

1. 分類問題

  • 已知m個樣本 (x1,y1), ...... (xm,ym),x是特徵變數,y是對應的類別。要求得一個模型函式或者對映規則h,對於新的樣本 xt,能夠儘量準確的預測出 yt = h(xt)。

  • 我們也可以從概率的角度來考慮一下上述問題。假設y有m個類別,即 y1,......yn ∈ {C1,......Cm},對於樣本 xt,如果能計算出每個類別的條件概率 P(C1|xt),......P(Cm|xt),那麼可以認為概率最大的那個類別就是 xt 所屬的類別。

h叫做分類器。分類演算法的任務就是構造分類器h

  • 分類器一個直觀理解就是在通過計算出的後驗概率得到每個類別的概率,並輸出最高類別的概率為分類結果。
  • 分類演算法的內容是要求給定特徵,讓我們得出類別,這也是所有分類問題的關鍵。每一個不同的分類演算法,對應著不同的核心思想。

2. 樸素貝葉斯

樸素貝葉斯(Naive Bayes)演算法理論基礎是基於貝葉斯定理和條件獨立性假設的一種分類方法。樸素的意思是假設各個特徵之間相互條件獨立的。

貝葉斯分類器的基本方法:在統計資料的基礎上,依據找到的一些特徵屬性,來計算各個類別的概率,找到概率最大的類,從而實現分類。即貝葉斯分類器通過預測一個物件屬於某個類別的概率,再預測其類別。

  • 找到一個已知分類的待分類項集合,這個集合叫做訓練樣本集。

  • 統計得到在各類別下各個特徵屬性的條件概率估計。
  • 找出最大概率的那個類。

3. 公式解說

3.1 貝葉斯定理

前文呼延灼的方法是:
求解問題(A): 呼延灼想知道自己是否是公明哥哥的心腹,用A來代表"你是大哥的心腹"。
已知結果(B): 大哥對你下拜。記作事件B。
推理結果 P(A|B): 想通過大哥對你下拜這個事件,來判斷大哥視你為心腹的概率。

於是有:

P(A|B) = P(B|A)P(A)/P(B)
P(A|B) 也就是在B事件"大哥下拜"發生之後,對A事件"大哥視你為心腹"概率的重新評估。

其實上述公式也隱含著:通過貝葉斯公式能夠把人分成兩類:大哥的心腹 / 普通下屬

3.2 用類別的思路重新解讀

所以貝葉斯公式可以用類別的思路重新解讀。

我們把 B 理解成“具有某特徵”,把A理解成“類別標籤”。在最簡單的二分類問題(是與否判定)下,我們將A 理解成“屬於某類”的標籤。

P(類別|特徵)=P(特徵|類別)P(類別)/P(特徵)
  • P(A)是先驗概率,表示每種類別分佈的概率;
  • P(B|A)是條件概率,表示在某種類別前提下,某事發生的概率;該條件概率可通過統計而得出,這裡需要引入極大似然估計概念。
  • P(A|B)是後驗概率,表示某事發生了,並且它屬於某一類別的概率,有了這個後驗概率,便可對樣本進行分類。後驗概率越大,說明某事物屬於這個類別的可能性越大,便越有理由把它歸到這個類別下。

3.3 擴充套件到多個條件(特徵)

之前只假設A只有B一個條件, 但在實際應用中,很少有一件事只受一個特徵影響的情況,往往影響一件事的因素有多個。假設,影響 B 的因素有 n 個,分別是 b1,b2,…,bn。

則 P(A|B) 可以寫為:

P(A|b1,b2,...,bn) = P(A) P(b1,b2,...,bn|A) / P(b1,b2,...,bn)

因為假設從 b1 到 bn 這些特徵之間,在概率分佈上是條件獨立的,也就是說每個特徵 bi與其他特徵都不相關。所以可以做如下轉換

P(b1,b2,...,bn|A) = P(b1|A)P(b2|A)...P(bn|A)

這個轉換其實就是 獨立變數的聯合分佈 = 各變數先驗分佈的乘積。只不過這裡是條件概率,但是因為變換前後都有同樣的條件 A,從樣本空間 A 的角度看,其實就是聯合分佈轉換成先驗分佈的乘積。

所以貝葉斯定理可以做如下推導

P(A|b1,b2,...,bn) = P(A) [P(b1|A)P(b2|A)...P(bn|A)] / P(b1,b2,...,bn)

0x02 呼延灼如何應用樸素貝葉斯模型來分類:

話說在前文[白話解析] 深入淺出貝葉斯定理中,呼延灼通過貝葉斯定理,推出了自己不是公明哥哥心腹的結論。雖然有些氣悶,但是也好奇於貝葉斯定理的威力,於是他就決定用樸素貝葉斯模型對馬軍頭領和步軍頭領進行分類。

1. 極簡版樸素貝葉斯分類模型

目前有一個極簡版樸素貝葉斯分類模型,能區分出兩個類(A1, A2),用來分類的特徵也有兩個(B1, B2)。
所以公式為:

P(A|B1,B2) = P(A) [P(B1|A)P(B2|A)] / P(B1,B2)

這個就是分類器:

P(A|B1,B2) = P(A) [P(B1|A)P(B2|A)] / P(B1,B2) = P(A) [P(B1|A)P(B2|A)] / [P(B1) P(B2)]

b1,b2表示特徵變數,Ai表示分類,p(Ai|b1,b2)表示在特徵為b1,b2的情況下分入類別Ai的概率

再重溫下樸素貝葉斯分類器,通過預測一個物件屬於某個類別的概率,再預測其類別。

  • 找到一個已知分類的待分類項集合,這個集合叫做訓練樣本集。

  • 統計得到在各類別下各個特徵屬性的條件概率估計。
  • 找出最大概率的那個類。

2. 已知條件

樣本是10位馬軍頭領, 10位步兵頭領,現在設定如下:

已知有兩個分類:
A1=馬軍頭領
A2=步軍頭領

兩個用來分類的特徵:
F1=紋身
F2=鬧事

特徵可以如下取值:
f11 = 有紋身
f12 = 無紋身
f21 = 愛鬧事
f22 = 不愛鬧事

有了分類器模型和預製條件,下面就看如何推匯出分類器模型引數了。

3. 訓練過程和資料

以下是根據已知資料統計得來。就是由實際數值訓練出來的 分類器引數

假定 馬軍頭領中,2位有紋身,1位愛鬧事,步兵頭領中,7位有紋身,6位愛鬧事。所以得到統計資料如下:

P(有紋身) = P(f11) = (7+2)/20 = 9/20 = 0.45
P(無紋身) = P(f12) = 11/20 = 0.55
P(愛鬧事) = P(f21) = 7/20 = 0.35
P(不愛鬧事) = P(f22) = 13/20 = 0.65

P(F1=f11|A=A1) = P(有紋身|馬軍頭領) = 2/20 = 0.1
P(F1=f12|A=A1) = P(無紋身|馬軍頭領) = 8/20 = 0.4
P(F1=f11|A=A2) = P(有紋身|步兵頭領) = 7/20 = 0.35
P(F1=f12|A=A2) = P(無紋身|步兵頭領) = 3/20 = 0.15
P(F2=f21|A=A1) = P(愛鬧事|馬軍頭領) = 1/20 = 0.05
P(F2=f22|A=A1) = P(不愛鬧事|馬軍頭領) = 9/20 = 0.45
P(F2=f21|A=A2) = P(愛鬧事|步兵頭領) = 6/20 = 0.3
P(F2=f22|A=A2) = P(不愛鬧事|步兵頭領) = 4/20 = 0.2

這樣就訓練(統計)出來了一個分類器模型的引數

可以結合之前的分類器

P(A|F1,F2) = P(A) [P(F1|A)P(F2|A)] / P(F1,F2) = P(A) [P(F1|A)P(F2|A)] / [P(F1) P(F2)]

來對 "待分類資料" 做處理了。

4. 如何分類

如果有某位頭領 x:不紋身,不鬧事。進行鍼對兩個分類(馬軍頭領,步兵頭領)進行兩次運算,得出兩個數值。

(不紋身,不鬧事)是馬軍頭領的可能性

P(馬軍頭領|不紋身,不鬧事) = P(馬軍頭領)  [P(無紋身|馬軍頭領) P(不鬧事|馬軍頭領) ] / [P(無紋身)P(不鬧事)]

P(A=A1|x) = p(A=A1) P(F1=f12|A=A1)p(F2=f22|A=A1) / [P(f12)P(f22)] = 0.5 * 0.4 * 0.45 / [0.55 * 0.65] = 0.18 / [0.55 * 0.65] = 0.25

(不紋身,不鬧事)是步兵頭領的可能性

P(步兵頭領|不紋身,不鬧事) = P(步兵頭領)  [P(無紋身|步兵頭領) P(不鬧事|步兵頭領) ] / [P(無紋身)P(不鬧事)]

P(A=A2|x) = p(A=A2) P(F1=f12|A=A2)p(F2=f22|A=A2) / [P(f12)P(f22)] = 0.5 * 0.15 * 0.2 / [0.55 * 0.65] = 0.03 / [0.55 * 0.65] = 0.04

所以x是馬軍的可能性更大。

貝葉斯定理最大的好處是可以用已知的頻率去計算未知的概率,我們 簡單地將頻率當成了概率

0X03 參考snowNLP的原始碼

我們可以通過snowNLP的原始碼來對樸素貝葉斯模型再進一步理解。

在bayes物件中,有兩個屬性d和total,d是一個數據字典,total儲存所有分類的總詞數,經過train方法訓練資料集後,d中儲存的是每個分類標籤的資料key為分類標籤,value是一個AddOneProb物件。

這裡的程式碼就是簡單地將頻率當成了概率。訓練就是統計各個分類標籤(key)所對應的個數。

1. 原始碼

#訓練資料集
def train(self, data):
    #遍歷資料集,data 中既包含正樣本,也包含負樣本
    for d in data: # data中是list
        # d[0]:分詞的結果,list
        # d[1]:標籤-->分類類別,正/負樣本的標記
        c = d[1]
        #判斷資料字典中是否有當前的標籤
        if c not in self.d:
            #如果沒有該標籤,加入標籤,值是一個AddOneProb物件
            self.d[c] = AddOneProb()  # 類的初始化
        #d[0]是評論的分詞list,遍歷分詞list
        for word in d[0]:
            #呼叫AddOneProb中的add方法,新增單詞
            self.d[c].add(word, 1)
    #計算總詞數,是正類和負類之和
    self.total = sum(map(lambda x: self.d[x].getsum(), self.d.keys())) # # 取得所有的d中的sum之和
                
class AddOneProb(BaseProb):
def __init__(self):
    self.d = {}
    self.total = 0.0
    self.none = 1
 
#新增單詞
def add(self, key, value):
    #更新該類別下的單詞總數
    self.total += value
    #如果單詞未出現過,需新建key
    if not self.exists(key):
        #將單詞加入對應標籤的資料字典中,value設為1
        self.d[key] = 1
        #更新總詞數
        self.total += 1
    #如果單詞出現過,對該單詞的value值加1
    self.d[key] += value

具體分類則是計算各個分類標籤的概率

#貝葉斯分類
def classify(self, x):
    tmp = {}
    #遍歷每個分類標籤
    for k in self.d: # 正類和負類
        #獲取每個分類標籤下的總詞數和所有標籤總詞數,求對數差相當於log(某標籤下的總詞數/所有標籤總詞數)
        tmp[k] = log(self.d[k].getsum()) - log(self.total) # 正類/負類的和的log函式-所有之和的log函式
        for word in x:
            #獲取每個單詞出現的頻率,log[(某標籤下的總詞數/所有標籤總詞數)*單詞出現頻率]
            tmp[k] += log(self.d[k].freq(word))
    #計算概率
    ret, prob = 0, 0
    for k in self.d:
        now = 0
        try:
            for otherk in self.d:
                now += exp(tmp[otherk]-tmp[k])
            now = 1/now
        except OverflowError:
            now = 0
        if now > prob:
            ret, prob = k, now
    return (ret, prob)

2. 原始碼推導公式

對於有兩個類別c1,c1的分類問題來說,其特徵為w1,⋯,wn,特徵之間是相互獨立的,屬於類別c1的貝葉斯模型的基本過程為:

P(c1∣w1,⋯,wn)=P(w1,⋯,wn∣c1)⋅P(c1) / P(w1,⋯,wn)
    
如果做句子分類,可以認為是出現了w1, w2, ..., wn這些詞之後,該句子被歸納到c1類的概率。

其中:

P(w1,⋯,wn)=P(w1,⋯,wn∣c1)⋅P(c1) + P(w1,⋯,wn∣c2)⋅P(c2)

預測的過程使用到了上述的公式,即:
\[ P(c1∣w1,⋯,wn)=\frac{P(w1,⋯,wn∣c1)⋅P(c1)}{P(w1,⋯,wn∣c1)⋅P(c1)+P(w1,⋯,wn∣c2)⋅P(c2)} \]
對上述的公式簡化:
\[ P(c1∣w1,⋯,wn)=\frac{P(w1,⋯,wn∣c1)⋅P(c1)}{P(w1,⋯,wn∣c1)⋅P(c1)+P(w1,⋯,wn∣c2)⋅P(c2)} \]

\[ =\frac{1}{1+\frac{P(w1,⋯,wn∣c2)⋅P(c2)}{P(w1,⋯,wn∣c1)⋅P(c1)}} \]

\[ =\frac{1}{1+exp[log(\frac{P(w1,⋯,wn∣c2)⋅P(c2)}{P(w1,⋯,wn∣c1)⋅P(c1)})]} \]

\[ =\frac{1}{1+exp[log(P(w1,⋯,wn∣c2)⋅P(c2))−log(P(w1,⋯,wn∣c1)⋅P(c1))]} \]

其中,分母中的1可以改寫為:
\[ 1=exp[log(P(w1,⋯,wn∣c1)⋅P(c1))−log(P(w1,⋯,wn∣c1)⋅P(c1))] \]

3. 結合公式再詳解程式碼

根據上面的公式,針對c1, c2,我們需要

a. 先求

\[ P(w1,⋯,wn∣c1)⋅P(c1) \]

b. 再求

\[ P(c1) \]

結合程式碼

p(Ck) = k這類詞出現的概率 = self.d[k].getsum() / self.total
p(w1|Ck) = w1這個詞在Ck類出現的概率 = self.d[k].freq(word)
k = 1,2

c. 再計算

\[ log(P(w1,⋯,wn∣c1)⋅P(c1)) \]

這個公式就是
\[ log(P(w1|c1)...p(wn∣c1)⋅P(c1)) \]
這個公式的結果就是:
\[ log(sum_{p(w1|C1)...p(wn|C1)}) + log(P(c1)) \]
最後展開:
\[ log(sum_{p(w1|C1)...p(wn|C1)}) + log(self.d[1].getsum()) - log(self.total)) \]
這個就是下面的 tmp[k]。其中,第一個for迴圈中的tmp[k]對應了公式中的log(P(ck)),第二個for迴圈中的tmp[k]對應了公式中的log(P(w1,⋯,wn∣ck)⋅P(ck))。兩個for迴圈的結果就是最終的tmp[k]。

def classify(self, x):
    tmp = {}
    for k in self.d: # 正類和負類
        tmp[k] = log(self.d[k].getsum()) - log(self.total) # 正類/負類的和的log函式-所有之和的log函式
        for word in x:
            tmp[k] += log(self.d[k].freq(word)) # 詞頻,不存在就為0
    ret, prob = 0, 0
    for k in self.d:
        now = 0
        try:
            for otherk in self.d:
                now += exp(tmp[otherk]-tmp[k]) # for迴圈中有一個結果是0, exp(0)就是1.就是上面分母中的1
            now = 1/now
        except OverflowError:
            now = 0
        if now > prob:
            ret, prob = k, now
    return (ret, prob)

0x04 參考

樸素貝葉斯分類模型(一)

樸素貝葉斯分類——大道至簡

帶你搞懂樸素貝葉斯分類演算法

snownlp情感分析原始碼解析

樸素貝葉斯詳解及中文輿情分析

[資料探勘]樸素貝葉斯分類

NLP系列(2)_用樸素貝葉斯進行文字分類(上)

情感分析——深入snownlp原理和實