[白話解析] 深入淺出朴素貝葉斯模型原理及應用
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)
預測的過程使用到了上述的公式,即:
對上述的公式簡化:
其中,分母中的1可以改寫為:
3. 結合公式再詳解代碼
根據上面的公式,針對c1, c2,我們需要
a. 先求
b. 再求
結合代碼
p(Ck) = k這類詞出現的概率 = self.d[k].getsum() / self.total
p(w1|Ck) = w1這個詞在Ck類出現的概率 = self.d[k].freq(word)
k = 1,2
c. 再計算
這個公式就是
這個公式的結果就是:
最后展開:
這個就是下面的 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)