樸素貝葉斯演算法
一、概述
貝葉斯分類演算法是統計學的一種概率分類方法,樸素貝葉斯分類是貝葉斯分類中最簡單的一種。其分類原理就是利 用貝葉斯公式根據某特徵的先驗概率計算出其後驗概率,然後選擇具有最大後驗概率的類作為該特徵所屬的類。之 所以稱之為”樸素”,是因為貝葉斯分類只做最原始、最簡單的假設:所有的特徵之間是統計獨立的。 假設某樣本X有a1,a2,...an 個屬性,那麼有P(X)=P(a1,a2,a3,...an)=P(a1)*P(a2)*...P(an) 。滿足這 樣的公式就說明特徵統計獨立。
1. 條件概率公式
條件概率(Condittional probability),就是指在事件B發生的情況下,事件A發生的概率,用P(A|B)來表示。
根據文氏圖可知:在事件B發生的情況下,事件A發生的概率就是P(A∩B)除以P(B)。
同理可得:
P(A∩B)=P(B|A)P(A)
所以,
P(A|B)P(B)=P(B|A)P(A)
接著看全概率公式,如果事件A1,A2,A3,...An構成一個完備事件,且都有正概率,那麼對任意一個事件B,則有:
P(B)=P(BA1)+P(BA2)+...+P(BAn)
=P(B|A1)P(A1)+P(B|A2)P(A2)+....+P(B|An)P(An)
2. 貝葉斯推斷
根據條件概率和全概率公式,可以得到貝葉斯公式如下:
P(A)稱為"先驗概率"(Prior probability),即在B事件發生之前,我們對A事件概率的一個判斷。
P(A|B)稱為"後驗概率"(Posterior probability),即在B事件發生之後,我們對A事件概率的重新評估。 P(B|A)/P(B)稱為"可能性函式"(Likely hood),這是一個調整因子,使得預估概率更接近真實概率。
所以條件概率可以理解為:後驗概率 = 先驗概率 * 調整因子
如果"可能性函式">1,意味著"先驗概率"被增強,事件A的發生的可能性變大;
如果"可能性函式"=1,意味著B事件無助於判斷事件A的可能性;
如果"可能性函式"<1,意味著"先驗概率"被削弱,事件A的可能性變小。
二、樸素貝葉斯種類
在scikit-learn中,一共有3個樸素貝葉斯的分類演算法。分別是GaussianNB,MultinomialNB和BernoulliNB
1. GaussianNB
GaussianNB就是先驗為高斯分佈(正態分佈)的樸素貝葉斯,假設每個標籤的資料都服從簡單的正態分佈
其中Ck為Y的第k類類別。和 為需要從訓練集估計的值。
這裡,用scikit-learn簡單實現一下GaussianNB。
#匯入包
import pandas as pd from sklearn.naive_bayes import GaussianNB from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score #匯入資料集 from sklearn import datasets
iris=datasets.load_iris() #切分資料集
Xtrain, Xtest, ytrain, ytest = train_test_split(iris.data,
iris.target, random_state=12) #建模 clf = GaussianNB() clf.fit(Xtrain, ytrain) #在測試集上執行預測,proba匯出的是每個樣本屬於某類的概率 clf.predict(Xtest) clf.predict_proba(Xtest) #測試準確率 accuracy_score(ytest, clf.predict(Xtest))
2. MultinomialNB
MultinomialNB就是先驗為多項式分佈的樸素貝葉斯。它假設特徵是由一個簡單多項式分佈生成的。多項分佈可以 描述各種型別樣本出現次數的概率,因此多項式樸素貝葉斯非常適合用於描述出現次數或者出現次數比例的特徵。 該模型常用於文字分類,特徵表示的是次數,例如某個詞語的出現次數。
多項式分佈公式如下:
其中,是第k個類別的第j維特徵的第l個取值條件概率。 mk是訓練集中輸出為第k類的樣本個 數。λ 為一個大於0的常數,常常取為1,即拉普拉斯平滑。也可以取其他值。
3. BernoulliNB
BernoulliNB就是先驗為伯努利分佈的樸素貝葉斯。假設特徵的先驗概率為二元伯努利分佈,即如下式:
此時l只有兩種取值。 Xjl只能取值0或者1。
在伯努利模型中,每個特徵的取值是布林型的,即true和false,或者1和0。在文字分類中,就是一個特徵有沒有在 一個文件中出現。
總結:
一般來說,如果樣本特徵的分佈大部分是連續值,使用GaussianNB會比較好。 如果如果樣本特徵的分佈大部分是多元離散值,使用MultinomialNB比較合適。 而如果樣本特徵是二元離散值或者很稀疏的多元離散值,應該使用BernoulliNB。
三、樸素貝葉斯之鳶尾花資料實驗
應用GaussianNB對鳶尾花資料集進行分類。
1. 匯入資料
import numpy as np import pandas as pd import random dataSet =pd.read_csv('iris.txt',header = None) dataSet.head()
2. 切分訓練集和測試集
import random """ 函式功能:隨機切分訓練集和測試集 引數說明: dataSet:輸入的資料集 rate:訓練集所佔比例 返回: 切分好的訓練集和測試集 """ def randSplit(dataSet, rate): l = list(dataSet.index) #提取出索引 random.shuffle(l) #隨機打亂索引 dataSet.index = l #將打亂後的索引重新賦值給原資料集 n = dataSet.shape[0] #總行數 m = int(n * rate) #訓練集的數量 train = dataSet.loc[range(m), :] #提取前m個記錄作為訓練集 test = dataSet.loc[range(m, n), :] #剩下的作為測試集 dataSet.index = range(dataSet.shape[0]) #更新原資料集的索引 test.index = range(test.shape[0]) #更新測試集的索引 return train, test
train,test= randSplit(dataSet, 0.8)
3. 構建高斯樸素貝葉斯分類器
def gnb_classify(train, test): labels = train.iloc[:, -1].value_counts().index #提取訓練集的標籤種類 mean = [] #存放每個類別的均值 std = [] #存放每個類別的方差 result = [] #存放測試集的預測結果 for i in labels: item = train.loc[train.iloc[:, -1] == i, :] #分別提取出每一種類別 m = item.iloc[:, :-1].mean() #當前類別的平均值 s = np.sum((item.iloc[:, :-1] - m) ** 2) / (item.shape[0]) #當前類別的方差 mean.append(m) #將當前類別的平均值追加至列表 std.append(s) #將當前類別的方差追加至列表 means = pd.DataFrame(mean, index=labels) #變成DF格式,索引為類標籤 stds = pd.DataFrame(std, index=labels) #變成DF格式,索引為類標籤 for j in range(test.shape[0]): iset = test.iloc[j, :-1].tolist() # 當前測試例項 iprob = np.exp(-1*(iset-means)**2/(stds*2))/(np.sqrt(2*np.pi*stds)) #正態分佈公式 prob = 1 #初始化當前例項總概率 for k in range(test.shape[1]-1): #遍歷每個特徵 prob *= iprob[k] #特徵概率之積即為當前例項概率 cla = prob.index[np.argmax(prob.values)] #返回最大概率的類別 result.append(cla) test['predict']=result acc = (test.iloc[:,-1]==test.iloc[:,-2]).mean() #計算預測準確率 print(f'模型預測準確率為{acc}') return test
4. 測試模型預測效果
將切分好的訓練集和測試集帶入模型,檢視模型預測結果
gnb_classify(train,test)
執行10次,檢視結果
for i in range(20): train,test= randSplit(dataSet, 0.8) gnb_classify(train,test)
四、使用樸素貝葉斯進行文件分類
樸素貝葉斯一個很重要的應用就是文字分類,所以我們以線上社群留言為例。為了不影響社群的發展,我們要遮蔽 侮辱性的言論,所以要構建一個快速過濾器,如果某條留言使用了負面或者侮辱性的語言,那麼就將該留言標誌為 內容不當。過濾這類內容是一個很常見的需求。對此問題建立兩個型別:侮辱類和非侮辱類,使用1和0分別表示。
我們把文字看成單詞向量或者詞條向量,也就是說將句子轉換為向量。考慮出現所有文件中的單詞,再決定將哪些 單詞納入詞彙表或者說所要的詞彙集合,然後必須要將每一篇文件轉換為詞彙表上的向量。簡單起見,我們先假設 已經將本文切分完畢,存放到列表中,並對詞彙向量進行分類標註。
1. 構建詞向量
留言文字已經被切分好,並且人為標註好類別,用於訓練模型。類別有兩類,侮辱性(1)和非侮辱性(0)。
此案例所有的函式:
-
loadDataSet:建立實驗資料集
-
createVocabList:生成詞彙表
-
setOfWords2Vec:生成詞向量
-
get_trainMat:所有詞條向量列表
-
trainNB:樸素貝葉斯分類器訓練函式
-
classifyNB:樸素貝葉斯分類器分類函式
-
testingNB:樸素貝葉斯測試函式
""" 函式功能:建立實驗資料集 引數說明:無引數 返回: postingList:切分好的樣本詞條 classVec:類標籤向量 """ def loadDataSet(): dataSet=[['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 dataSet,classVec
dataSet,classVec=loadDataSet()
生成詞彙表:
""" 函式功能:將切分的樣本詞條整理成詞彙表(不重複) 引數說明: dataSet:切分好的樣本詞條 返回: vocabList:不重複的詞彙表 """ def createVocabList(dataSet): vocabSet = set() #建立一個空的集合 for doc in dataSet: #遍歷dataSet中的每一條言論 vocabSet = vocabSet | set(doc) #取並集 vocabList = list(vocabSet) return vocabList
vocabList = createVocabList(dataSet)
生成詞向量:
函式功能:根據vocabList詞彙表,將inputSet向量化,向量的每個元素為1或0
引數說明: vocabList:詞彙表 inputSet:切分好的詞條列表中的一條 返回: returnVec:文件向量,詞集模型 """ def setOfWords2Vec(vocabList, inputSet): returnVec = [0] * len(vocabList) #建立一個其中所含元素都為0的向量 for word in inputSet: #遍歷每個詞條 if word in vocabList: #如果詞條存在於詞彙表中,則變為1 returnVec[vocabList.index(word)] = 1 else: print(f" {word} is not in my Vocabulary!" ) return returnVec #返回文件向量
所有詞條向量列表:
""" 函式功能:生成訓練集向量列表 引數說明: dataSet:切分好的樣本詞條 返回: trainMat:所有的詞條向量組成的列表 """ def get_trainMat(dataSet): trainMat = [] #初始化向量列表 vocabList = createVocabList(dataSet) #生成詞彙表 for inputSet in dataSet: #遍歷樣本詞條中的每一條樣本 returnVec=setOfWords2Vec(vocabList, inputSet) #將當前詞條向量化 trainMat.append(returnVec) #追加到向量列表中 return trainMat
測試函式執行結果:
trainMat = get_trainMat(dataSet)
2. 樸素貝葉斯分類器訓練函式
詞向量構建好之後,我們就可以來構建樸素貝葉斯分類器的訓練函數了
""" 函式功能:樸素貝葉斯分類器訓練函式 引數說明: trainMat:訓練文件矩陣 classVec:訓練類別標籤向量 返回: p0V:非侮辱類的條件概率陣列 p1V:侮辱類的條件概率陣列 pAb:文件屬於侮辱類的概率 """ def trainNB(trainMat,classVec): n = len(trainMat) #計算訓練的文件數目 m = len(trainMat[0]) #計算每篇文件的詞條數 pAb = sum(classVec)/n #文件屬於侮辱類的概率 p0Num = np.zeros(m) #詞條出現數初始化為0 p1Num = np.zeros(m) #詞條出現數初始化為0 p0Denom = 0 #分母初始化為0 p1Denom = 0 #分母初始化為0 for i in range(n): #遍歷每一個文件 if classVec[i] == 1: #統計屬於侮辱類的條件概率所需的資料 p1Num += trainMat[i] p1Denom += sum(trainMat[i]) else: #統計屬於非侮辱類的條件概率所需的資料 p0Num += trainMat[i] p0Denom += sum(trainMat[i]) p1V = p1Num/p1Denom p0V = p0Num/p0Denom return p0V,p1V,pAb #返回屬於非侮辱類,侮辱類和文件屬於侮辱類的概率
測試函式,檢視結果
p0V,p1V,pAb = trainNB(trainMat, classVec)
3. 測試樸素貝葉斯分類器
from functools import reduce """ 函式功能:樸素貝葉斯分類器分類函式 引數說明: vec2Classify:待分類的詞條陣列 p0V:非侮辱類的條件概率陣列 p1V:侮辱類的條件概率陣列 pAb:文件屬於侮辱類的概率 返回: 0:屬於非侮辱類 1:屬於侮辱類 """ def classifyNB(vec2Classify, p0V, p1V, pAb): p1 = reduce(lambda x,y:x*y, vec2Classify * p1V) * pAb #對應元素相乘 p0 = reduce(lambda x,y:x*y, vec2Classify * p0V) * (1 - pAb) print('p0:',p0) print('p1:',p1) if p1 > p0: return 1 else: return 0
""" 函式功能:樸素貝葉斯測試函式 引數說明: testVec:測試樣本 返回:測試樣本的類別 """ def testingNB(testVec): dataSet,classVec = loadDataSet() #建立實驗樣本 vocabList = createVocabList(dataSet) #建立詞彙表 trainMat= get_trainMat(dataSet) #將實驗樣本向量化 p0V,p1V,pAb = trainNB(trainMat,classVec) #訓練樸素貝葉斯分類器 thisone = setOfWords2Vec(vocabList, testVec) #測試樣本向量化 if classifyNB(thisone,p0V,p1V,pAb): print(testVec,'屬於侮辱類') #執行分類並列印分類結果 else: print(testVec,'屬於非侮辱類') #執行分類並列印分類結果
#測試樣本1 testVec1 = ['love', 'my', 'dalmation'] testingNB(testVec1) #測試樣本2 testVec2 = ['stupid', 'garbage'] testingNB(testVec2)
你會發現,這樣寫的演算法無法進行分類,p0和p1的計算結果都是0,顯然結果錯誤。這是為什麼呢?
4. 樸素貝葉斯改進之拉普拉斯平滑
利用貝葉斯分類器對文件進行分類時,要計算多個概率的乘積以獲得文件屬於某個類別的概率,即計算 p(w0|1)p(w1|1)p(w2|1)。如果其中有一個概率值為0,那麼最後的成績也為0。顯然,這樣是不合理的,為了降低 這種影響,可以將所有詞的出現數初始化為1,並將分母初始化為2。這種做法就叫做拉普拉斯平滑(Laplace Smoothing)又被稱為加1平滑,是比較常用的平滑方法,它就是為了解決0概率問題。
另外一個遇到的問題就是下溢位,這是由於太多很小的數相乘造成的。我們在計算乘積時,由於大部分因子都很 小,所以程式會下溢或者得不到正確答案。為了解決這個問題,對乘積結果取自然對數。通過求對數可以避免下溢 出或者浮點數舍入導致的錯誤。同時,採用自然對數進行處理不會有任何損失。下圖給出函式f(x)和ln(f(x))的曲 線。
檢查這兩條曲線就會發現它們在相同區域內同時增加或者減少,並且在相同點上取到極值。它們的取值雖然不同, 但不影響最終結果。因此可以修改程式碼如下:
def trainNB(trainMat,classVec): n = len(trainMat) #計算訓練的文件數目 m = len(trainMat[0]) #計算每篇文件的詞條數 pAb = sum(classVec)/n #文件屬於侮辱類的概率 p0Num = np.ones(m) #詞條出現數初始化為1 p1Num = np.ones(m) #詞條出現數初始化為1 p0Denom = 2 #分母初始化為2 p1Denom = 2 #分母初始化為2 for i in range(n): #遍歷每一個文件 if classVec[i] == 1: #統計屬於侮辱類的條件概率所需的資料 p1Num += trainMat[i] p1Denom += sum(trainMat[i]) else: #統計屬於非侮辱類的條件概率所需的資料 p0Num += trainMat[i] p0Denom += sum(trainMat[i]) p1V = np.log(p1Num/p1Denom) p0V = np.log(p0Num/p0Denom) return p0V,p1V,pAb #返回屬於非侮辱類,侮辱類和文件屬於侮辱類的概率
檢視程式碼執行結果:
p0V,p1V,pAb = trainNB(trainMat,classVec)
def classifyNB(vec2Classify, p0V, p1V, pAb): p1 = sum(vec2Classify * p1V) + np.log(pAb) #對應元素相乘 p0 = sum(vec2Classify * p0V) + np.log(1- pAb) #對應元素相乘 if p1 > p0: return 1 else: return 0
測試程式碼執行結果:
#測試樣本1 testVec1 = ['love', 'my', 'dalmation'] testingNB(testVec1) #測試樣本2 testVec2 = ['stupid', 'garbage'] testingNB(testVec2)
這樣看,結果就沒什麼問題了。