貝葉斯分類器
首先要理解貝葉斯決策的理論依據,引用西瓜書上的原話:對于分類任務,在所有相關概率都已知的理想情形下,貝葉斯決策論考慮如何基于這些概率和誤判損失來選擇最優(yōu)的類別標記。
然后引入我們很熟悉的貝葉斯公式:
P
(
c
∣
x
)
=
P
(
c
)
P
(
x
∣
c
)
P
(
x
)
P(c\mid \boldsymbol{x}) = \frac{P(c)P(\boldsymbol{x} \mid c)}{P(\boldsymbol{x})}
P(c∣x)=P(x)P(c)P(x∣c)?
其中
c
c
c 是類別標記,
x
x
x 是樣本點(一個包含n維屬性的向量)。
P
(
c
)
P(c)
P(c)就是所謂的“先驗”概率,這個概率是可以通過數據集統(tǒng)計直接得到的,那么
P
(
c
∣
x
)
P(c \mid \boldsymbol{x})
P(c∣x)就是所謂的“后驗“概率,即我們要在已有數據的信息背景下推斷得到的。
與其它機器學習的算法不同,貝葉斯分類算法似乎看不出一個明顯的待訓練參數,但觀察公式也能明白,我們要求出的 P ( c ∣ x ) P(c \mid \boldsymbol{x}) P(c∣x)是由 P ( c ) 、 P ( x ∣ c ) P(c)、P(\boldsymbol{x} \mid c) P(c)、P(x∣c)以及 P ( x ) P(\boldsymbol{x}) P(x)三者變量所共同決定的,而這三者的現實意義其實就是給定的信息背景(數據集)——多數情況下,我們在不同的信息背景下總能得到不同的 P ( c ∣ x ) 、 P ( c ) 、 P ( x ∣ c ) P(c \mid \boldsymbol{x})、P(c)、P(\boldsymbol{x} \mid c) P(c∣x)、P(c)、P(x∣c),進而推出不同的 P ( c ∣ x ) P(c \mid \boldsymbol{x}) P(c∣x)。
有些信息背景對于作出決策的貢獻是“好的”,這時 P ( c ∣ x ) P(c \mid \boldsymbol{x}) P(c∣x)體現出來的意義能很真實地反映出作出某項決策的正確性,而在有些信息背景(比如樣本過于稀疏)下得出的結果就并不能很好地反映待檢測樣本所屬的真實類別,進而造成誤分類。
于是Bayes分類器的訓練意義在于尋求“好的”數據集,使得后驗概率值能較好地反映出決策的真實性。
何為樸素
從概率學原理來講,類條件概率 P ( x ∣ c ) P(\boldsymbol{x} \mid c) P(x∣c),是所有屬性上的聯(lián)合概率,很難從有限的訓練樣本直接估計而得。那么為避開這個障礙,樸素貝葉斯分類器采用了“屬性條件獨立性假設”:對已知類別假設所有屬性之間相互獨立。
此時類條件概率滿足:
P
(
x
∣
c
)
=
∏
i
=
1
d
P
(
x
i
∣
c
)
P(\boldsymbol{x} \mid c)=\prod_{i=1}^n5n3t3zP(x_i \mid c)
P(x∣c)=i=1∏d?P(xi?∣c)
其中
d
d
d 代表樣本點的屬性個數,
x
i
x_i
xi? 代表
x
\boldsymbol{x}
x的各個屬性。
于是開頭的貝葉斯公式進一步推:
P
(
c
∣
x
)
=
P
(
c
)
P
(
x
∣
c
)
P
(
x
)
=
P
(
c
)
∏
i
=
1
d
P
(
x
i
∣
c
)
P
(
x
)
P(c\mid \boldsymbol{x}) = \frac{P(c)P(\boldsymbol{x} \mid c)}{P(\boldsymbol{x})}=\frac{P(c)\prod_{i=1}^n5n3t3zP(x_i \mid c)}{P(\boldsymbol{x})}
P(c∣x)=P(x)P(c)P(x∣c)?=P(x)P(c)∏i=1d?P(xi?∣c)?
于是在此假設前提下,進而大大地簡化了計算,這也正是“樸素(Naive)”一詞修飾的由來。
而且在一般分類任務下,是不會計算 P ( x ) P(\boldsymbol x) P(x)的,而是只計算分子便進行比較。
案例:基于SMS Spam Collection數據集的廣告郵件分類
SMS數據集
SMS Spam Collection是用于廣告短信識別的經典數據集,完全來自真實短信內容,包括4831條正常短信和747條廣告短信。
其內容如下,每一個line
都表示一段郵件,"ham"和"spam"分別表示郵件的類別“正常郵件”和“廣告郵件”,然后以'\t'
為間隔右邊的長字符串為郵件內容。
詞向量表示
- 每個詞語的出現各看成一個事件,先分別計算單個詞語的事件概率進行訓練,然后將一個完整的郵件看成這些事件的交事件。
給定一系列郵件的文本,將每個郵件的關鍵詞提取出來,我們認為長度>2
的單詞才為關鍵詞,然后將這些關鍵詞轉換為小寫,其組成的列表作為一個數據樣本,所以加載數據集如下
# 創(chuàng)建數據集,加載數據
adClass = 1 # 廣告,垃圾標識
def textParse(bigString):
'''接收一個長字符串解析成字符串列表'''
#將特殊符號作為劃分標志進行字符串切分,即非字母,非數字
listOfTokens = re.split(r'\W+', bigString)
#除單字母 其它單詞全變成小寫
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
def loadDataSet():
'''加載數據集合及其對應的分類'''
classVec = [] #0-1列表 第i個元素標識了wordList第i行類別
wordList = [] #提取出的詞矩陣,每一行是對應一個郵件的單詞列表
smss = open("./SMSSpamCollection.txt", 'r', encoding = 'utf-8')
data = csv.reader(smss, delimiter = '\t')
for line in data: #line:左邊一個"ham" or "spam",右邊一個大字符串
if line[0] == "ham":
classVec.append(0)
else:
classVec.append(1)
wordList.append(textParse(line[1]))
return wordList, classVec
打印數據樣本,可以看到郵件文本以及它的關鍵詞列表:
然后,將這些詞全放在一起,構成一個“語料庫”。
def doc2VecList(docList): #docList是一個二維矩陣,每行表示一個郵件的關鍵詞組成的列表
"""數據進行并集操作,最后返回一個詞不重復的并集"""
a = list(reduce(lambda x, y:set(x) | set(y), docList))
return a
這么做的意義在于我們要改變這個數據樣本的表示方式(否則不利于概率計算),在這里就是用詞向量的表示方法:
對于一個數據樣本,將其視作一個長度為 n = ∣ 語料庫中詞的個數 ∣ n=\left | 語料庫中詞的個數 \right | n=∣語料庫中詞的個數∣ 的01向量,如果樣本某個詞在語料庫中出現了,那就在這個詞的對應位置記1,否則記0.
于是該詞向量就有了n個屬性,每個屬性取值∈{0,1}。
def words2Vec(vecList, inputWords): #vecList:語料庫,inputWords:輸入的詞組
'''把單詞轉化為詞向量'''
dimensions = len(vecList)
resultVec = [0] * dimensions
for i in range(dimensions):
if vecList[i] in inputWords:
resultVec[i] += 1
#轉化為一維數組
return array(resultVec)
Laplacian平滑
接下來就是計算
- P ( c ) P(c) P(c)
- P ( x ∣ c ) = ∏ i = 1 d P ( x i ∣ c ) P(\boldsymbol{x} \mid c)=\prod_{i=1}^n5n3t3zP(x_i \mid c) P(x∣c)=i=1∏d?P(xi?∣c)
但這里尤其需要注意的是待分類樣本詞向量中可能存在“詞沒有記錄在語料庫”中的情況,也即它屬于任何類別的概率值為0,顯然會導致
∏
i
=
1
d
P
(
x
i
∣
c
)
\prod_{i=1}^n5n3t3zP(x_i \mid c)
∏i=1d?P(xi?∣c)直接變成0不合理,因此進行**+1的Laplacian平滑處理**:
P
^
(
c
)
=
∣
D
c
∣
+
1
∣
D
∣
+
N
\hat{P}{(c)} = \frac{|D_c| + 1} {|D|+N}
P^(c)=∣D∣+N∣Dc?∣+1?
P ^ ( x i ∣ c ) = ∣ D c , x i ∣ + 1 ∣ D c ∣ + N i \hat{P}({x_i\mid c}) = \frac{|D_{c,x_i}| + 1} {|D_c|+N_i} P^(xi?∣c)=∣Dc?∣+Ni?∣Dc,xi??∣+1?
- 也就是令沒出現過的詞“所屬類別0和類別1的次數均+1”,“類別0的樣本數和類別1的樣本數均+2(類別數)”。
在代碼中體現為:
'''Laplacian +1平滑'''
# 全部都初始化為1(全1數組), 防止出現概率為0的情況
p0Num = ones(numWords)
p1Num = ones(numWords)
# 相應的單詞初始化為2
p0Words = 2.0
p1Words = 2.0
訓練過程
注:這里的訓練過程跟測試集的內容無關系,而是一種預存儲*。
首先,根據訓練集計算從
i
∈
i\in
i∈[1~
n
]
n]
n] 的所有
P
(
x
i
∣
0
)
P(x_i \mid 0)
P(xi?∣0)和
P
(
x
i
∣
1
)
P(x_i \mid 1)
P(xi?∣1),在這里
x
i
x_i
xi?指代的語料庫中第
i
i
i個詞,然后存到兩個數組里——p0Vec
和p1Vec
,在這里有個小技巧就是在存儲時對每個P值都取了一個log
運算,這樣可以擴大數值域,方便后面的計算。
# 統(tǒng)計每個分類的詞的總數
for i in range(numTrainClass):
if trainClass[i] == 1:
# 數組在對應的位置上相加
p1Num += trainMatrix[i]
p1Words += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i]
p0Words += sum(trainMatrix[i])
# 計算每種類型里面, 每個單詞出現的概率
# 在計算過程中,由于概率的值較小,于是取對數擴大數值域
p0Vec = log(p0Num / p0Words) #P(所有xi|0)
p1Vec = log(p1Num / p1Words) #P(所有xi|1)
# 計算在類別中1出現的概率,0出現的概率可通過1-p得到
pClass1 = sum(trainClass) / float(numTrainClass) #P(c=1)
分類過程
對于測試集的詞向量testVec
存在如下關系:
∏
i
=
1
d
P
(
x
i
∣
1
或
0
)
=
∏
i
∈
{
i
∣
t
e
s
t
V
e
c
[
i
]
=
1
}
P
(
x
i
∣
1
或
0
)
\prod_{i=1}^n5n3t3zP(x_i \mid 1或0)=\prod_{i\in\{i\mid testVec[i] = 1\}}P(x_i \mid 1或0)
i=1∏d?P(xi?∣1或0)=i∈{i∣testVec[i]=1}∏?P(xi?∣1或0)
也就是說,只需要testVec * p0Vec
和testVec * p1Vec
便可分別得到在testVec第i個位置上為1的
P
(
x
i
∣
0
)
P(x_i \mid 0)
P(xi?∣0)和
P
(
x
i
∣
1
)
P(x_i \mid 1)
P(xi?∣1)了!文章來源:http://www.zghlxwxcb.cn/news/detail-465076.html
所以只要計算 P ( c ) ∏ i ∈ { i ∣ t e s t V e c [ i ] = 1 } P ( x i ∣ 1 或 0 ) P(c)\prod_{i\in\{i\mid testVec[i] = 1\}}P(x_i \mid 1或0) P(c)∏i∈{i∣testVec[i]=1}?P(xi?∣1或0)便得到兩個后驗概率,同樣根據對數特性 l n [ P ( c ) × P ( X 1 ∣ c ) × P ( X 2 ∣ c ) × . . . × P ( X n ∣ c ) ] = l n P ( c ) + l n P ( X 1 ∣ c ) + . . . + l n P ( X n ∣ c ) ln[{ P(c)×P(X1|c)×P(X2|c)×...×P(Xn|c)}] = lnP(c) + lnP(X1|c) + ... + lnP(Xn|c) ln[P(c)×P(X1∣c)×P(X2∣c)×...×P(Xn∣c)]=lnP(c)+lnP(X1∣c)+...+lnP(Xn∣c),將乘法改成加法:文章來源地址http://www.zghlxwxcb.cn/news/detail-465076.html
def classifyNB(testVec, p0Vec, p1Vec, pClass1):
"""分類, 返回分類結果 0 or 1"""
# 因為概率的值太小了,所以乘法改加法,可以簡化計算且不失精度
p1 = sum(testVec * p1Vec) + log(pClass1)
p0 = sum(testVec * p0Vec) + log(1 - pClass1)
if p0 > p1:
return 0
return 1
完整代碼
#導入所需庫文件
from numpy import *
from functools import reduce
import re
import csv
def textParse(bigString):
'''接收一個長字符串解析成字符串列表'''
#將特殊符號作為劃分標志進行字符串切分,即非字母,非數字
listOfTokens = re.split(r'\W+', bigString)
#除單字母 其它單詞全變成小寫
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
# 創(chuàng)建數據集,加載數據
adClass = 1 # 廣告,垃圾標識
def loadDataSet():
'''加載數據集合及其對應的分類'''
classVec = [] # 0-1列表 第i個元素標識了wordList第i行類別
wordList = [] # 提取出的詞矩陣,每一行是對應一個郵件的單詞列表
smss = open("./SMSSpamCollection.txt", 'r', encoding='utf-8')
data = csv.reader(smss, delimiter='\t')
for line in data: # line:左邊一個"ham" or "spam",右邊一個大字符串
if line[0] == "ham":
classVec.append(0)
else:
classVec.append(1)
wordList.append(textParse(line[1]))
return wordList, classVec
def doc2VecList(docList):
"""函數說明:數據進行并集操作,最后返回一個詞不重復的并集"""
#reduce(function, iterable[, initializer]): 從左至右積累地應用到 iterable 的條目,以便將該可迭代對象縮減為單一的值
a = list(reduce(lambda x, y:set(x) | set(y), docList))
return a #['','',...,'']
def words2Vec(vecList, inputWords): #所有詞,輸入的詞組
'''把單詞轉化為詞向量'''
dimensions = len(vecList)
resultVec = [0] * dimensions
for i in range(dimensions):
if vecList[i] in inputWords:
resultVec[i] += 1
#轉化為一維數組
return array(resultVec)
def trainNB(trainMatrix, trainClass):
"""函數說明:計算,生成每個詞對于類別上的概率"""
# 類別行數
numTrainClass = len(trainClass)
# 列數
numWords = len(trainMatrix[0])
# 全部都初始化為1(全1數組), 防止出現概率為0的情況出現
p0Num = ones(numWords)
p1Num = ones(numWords)
# 相應的單詞初始化為2
p0Words = 2.0
p1Words = 2.0
# 統(tǒng)計每個分類的詞的總數
for i in range(numTrainClass):
if trainClass[i] == 1:
# 數組在對應的位置上相加
p1Num += trainMatrix[i]
p1Words += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i]
p0Words += sum(trainMatrix[i])
# 計算每種類型里面, 每個單詞出現的概率
# 在計算過程中,由于概率的值較小,所以我們就取對數進行比較,其中l(wèi)n可替換為log的任意對數底
p0Vec = log(p0Num / p0Words)
p1Vec = log(p1Num / p1Words)
# 計算在類別中1出現的概率,0出現的概率可通過1-p得到
pClass1 = sum(trainClass) / float(numTrainClass)
return p0Vec, p1Vec, pClass1
def classifyNB(testVec, p0Vec, p1Vec, pClass1):
"""分類, 返回分類結果 0 or 1"""
# 因為概率的值太小了,所以乘法改加法
# 根據對數特性ln{ P(c)×P(X1|c)×P(X2|c)×...×P(Xn|c) } = lnP(c) + lnP(X1|c) + ... + lnP(Xn|c)
# 可以簡化計算且不失精度
'''test * pVec已經在trainNB中取過對數了直接相加'''
p1 = sum(testVec * p1Vec) + log(pClass1)
p0 = sum(testVec * p0Vec) + log(1 - pClass1)
if p0 > p1:
return 0
return 1
def printClass(words, testClass):
if testClass == adClass:
print(words, '推測為:廣告郵件')
else:
print(words, '推測為:正常郵件')
def tNB():
# 加載訓練數據集
docList, classVec = loadDataSet() # 單詞矩陣、 01向量
# 生成包含所有單詞的list
allWordsVec = doc2VecList(docList)
# 構建詞向量矩陣
'''lambda中的x對應docList的每一行詞組'''
trainMat = list(map(lambda x: words2Vec(allWordsVec, x), docList)) # 和docList對應的行(詞)向量組
# 訓練計算每個詞在分類上的概率
# 其中p0V:每個單詞在“非”分類出現的概率, p1V:每個單詞在“是”分類出現的概率 pClass1:類別中是1的概率
p0V, p1V, pClass1 = trainNB(trainMat, classVec)
# 測試數據集
text1 = "As a valued cutomer, I am pleased to advise you that following recent review of your Mob No"
testwords1 = textParse(text1)
testVec1 = words2Vec(allWordsVec, testwords1)
# 通過將單詞向量testVec代入,根據貝葉斯公式,比較各個類別的后驗概率,判斷當前數據的分類情況
testClass1 = classifyNB(testVec1, p0V, p1V, pClass1)
# 打印出測試結果
printClass(testwords1, testClass1)
text2 = "Please don't text me anymore. I have nothing else to say"
testwords2 = textParse(text2)
testVec2 = words2Vec(allWordsVec, testwords2)
testClass2 = classifyNB(testVec2, p0V, p1V, pClass1)
printClass(testwords2, testClass2)
if __name__ == '__main__':
tNB()
到了這里,關于自實現樸素貝葉斯分類器with案例:基于SMS Spam Collection數據集的廣告郵件分類的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!