機器學習:基於概率的樸素貝葉斯分類器詳解--Python實現以及專案實戰

語言: CN / TW / HK

本文已參與「新人創作禮」活動,一起開啟掘金創作之路。

一、準備資料

建立一個bayes.py程式,從文字中構建詞向量,實現詞表向向量轉換函式。

```from numpy import * def loadDataSet(): postingList = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'], # 分詞可用wordcloud ['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 createVocabList(dataSet): vocabSet = set([]) for document in dataSet: vocabSet = vocabSet | set(document) #建立兩個集合的並集 劃掉重複出現的單詞 return list(vocabSet)

處理樣本輸出為向量形式

def setOfWords2Vec(vocaList , inputSet): returnVec = [0]*len(vocaList)#建立一個其中所含元素全為0的向量代替文字 for word in inputSet: if word in vocaList: returnVec[vocaList.index(word)] = 1 else: print("the word:%s is not in my Vocabulary!"" % word") return returnVec ``` 第一個函式建立了一些實驗樣本,第二個函式建立一個包含在所有文件中出現的不重複的列表,第三個函式輸入引數為詞彙表及某個文件,輸出的是文件向量,向量的每一個元素為1或0,分別表示詞彙表中的單詞在輸入文件中是否出現。

可檢驗函式是否正常工作:

listOPosts,listClasses=loadDataSet() myVocabList=createVocabList(listOPosts) print(myVocabList) print(setOfWords2Vec(myVocabList,listOPosts[0])) print(setOfWords2Vec(myVocabList,listOPosts[3]))

image.png

二、訓練演算法:從詞向量計算概率

該函式虛擬碼如下:

image.png 根據前篇基礎理論先求得P(w|ci),再計算P(ci)。

樸素貝葉斯分類器訓練函式:

def trainNB0(trainMatrix,trainCategory):#樸素貝葉斯分類器訓練函式。引數:1:向量化文件2:詞條向量 numTrainDocs = len(trainMatrix)#文字矩陣 numWords = len(trainMatrix[0]) pAbusive = sum(trainCategory)/float(numWords) p0Num = zeros(numWords);p1Num = zeros(numWords)#建立兩個長度為詞條向量等長的列表,平滑處理:初始值設為1 p0Denom = 0.000001;p1Denom = 0.000001 for i in range (numTrainDocs): if trainCategory[i] ==1: p1Num += trainMatrix[i] p1Denom += sum(trainMatrix[i]) else: p0Num += trainMatrix[i] p0Denom += sum(trainMatrix[i]) p1Vect = p1Num/p1Denom # 利用Numpy陣列計算p(wi/c1),即類1條件下各詞條出現的概率 p0Vect = p0Num/p0Denom # 利用Numpy陣列計算p(wi/c0),為避免下溢,後面會改為log() return p0Vect, p1Vect, pAbusive # 返回 由於當p0Num時會報RuntimeWarning: invalid value encountered in true_divide,這是由0/0導致,因此在設定p0Denom時不能設定為0.

首先,計算文件屬於侮辱性文件(class=1)的概率,即P(1)。P(0)可由1-P(1)得到。

檢驗:

image.png 利用貝葉斯分類器對文件進行分類時,要進行多個概率的乘積可獲得文件屬於某個類別額的概率,即計算p(w0|1)p(w1|1)p(w2|1)。其中一個概率值為0,那麼最後的乘積也為0.我們可以將所有出現的詞初始值初始化為1,並將分母初始化為2. 修改:

``` p0Num = ones(numWords); p1Num = ones(numWords)#建立兩個長度為詞條向量等長的列表,平滑處理:初始值設為1 p0Denom = 2.0;p1Denom = 2.0#平滑處理,初始值設為2

``` 另一個問題為下溢位,這是由於太多很小的數相乘造成的。當計算乘積 p(w0|ci) * p(w1|ci) * p(w2|ci)... p(wn|ci) 時,由於大部分因子都非常小,所以程式會下溢位或者得到不正確的答案。(用 Python 嘗試相乘許多很小的數,最後四捨五入後會得到 0)。一種解決辦法是對乘積取自然對數。在代數中有 ln(a * b) = ln(a) + ln(b), 於是通過求對數可以避免下溢位或者浮點數舍入導致的錯誤。同時,採用自然對數進行處理不會有任何損失。

p1Vect = log(p1Num/p1Denom)#利用Numpy陣列計算p(wi/c1),即類1條件下各詞條出現的概率 p0Vect = log(p0Num/p0Denom)#利用Numpy陣列計算p(wi/c0),為避免下溢,後面會改為log()

image.png

三、分類函式

樸素貝葉斯分類函式:

#樸素貝葉斯分類函式 def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):#注意引數2,3均已log化 p1 = sum(vec2Classify * p1Vec) + log(pClass1)# P(w|c1) * P(c1) ,即貝葉斯準則的分子 p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1) # P(w|c0) * P(c0) ,即貝葉斯準則的分子 if p1 > p0: return 1 else: return 0 """ 使用演算法: # 將乘法轉換為加法 乘法:P(C|F1F2...Fn) = P(F1F2...Fn|C)P(C)/P(F1F2...Fn) 加法:P(F1|C)*P(F2|C)....P(Fn|C)P(C) -> log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C)) :param vec2Classify: 待測資料[0,1,1,1,1...],即要分類的向量 :param p0Vec: 類別0,即正常文件的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表 :param p1Vec: 類別1,即侮辱性文件的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表 :param pClass1: 類別1,侮辱性檔案的出現概率 :return: 類別1 or 0 """ # 計算公式 log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C)) 測試:

def testingNB(): """ 測試樸素貝葉斯演算法 """ # 1. 載入資料集 listPosts,listClasses =loadDataSet() #2. 建立單詞集合 myVocabList = createVocabList(listPosts) #3.計算單詞是否出現並建立資料矩陣 trainMat = [] for postinDoc in listPosts: trainMat.append(setOfWords2Vec(myVocabList,postinDoc)) #4.訓練資料 p0V,p1V,pAb = trainNB0(array(trainMat),array(listClasses)) #5.測試資料 testEntry = ['love','my','dalmation'] thisDoc = array(setOfWords2Vec(myVocabList,testEntry)) print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb)) testEntry = ['stupid', 'garbage'] thisDoc = array(setOfWords2Vec(myVocabList, testEntry)) print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))

image.png

四、文件詞袋模型

由於我們將每個詞的出現作為一個特徵,這可以被描述為詞集模型。但單詞往往有多義性,意味著一個單詞在文件出現可能代表有不同的含義。這種方法被稱為詞袋模型。

在詞袋中,每個單詞可以出現多次,而在詞集中,每個詞只能出現一次。為適應詞袋模型,需要對函式setOfWords2Vec稍加修改:

def bagOfWords2VecMN(vocaList , inputSet): returnVec = [0]*len(vocaList)#建立一個其中所含元素全為0的向量代替文字 for word in inputSet: if word in vocaList: returnVec[vocaList.index(word)] += 1 #每遇到一個單詞,相應加一 else: print("the word:%s is not in my Vocabulary!"" % word") return returnVec

五、使用樸素貝葉斯過濾垃圾郵件

image.png

1.收集資料

使用樸素貝葉斯過濾垃圾郵件資料集

資料集說明: 資料集下包含兩個資料夾,其中spam資料夾下為垃圾郵件,ham資料夾下為非垃圾郵件。

資料集格式: txt檔案

2.準備資料(處理資料)

英文由於單詞之間有空格,方便切分。中文有jieba庫,有興趣的可以瞭解一下。

```myStr = 'This book is the best book on Python.' myStr.split()

['This', 'book', 'is', 'the', 'best', 'book', 'on', 'Python.'] ``` 但是最後一個詞有標點符號,這個我們通過正則表示式解決,正則表示式在文字分類中是有很大作用的。

import re regEx = re.compile('\\W*') listOfTokens = regEx.split(myStr) listOfTokens ['This', 'book', 'is', 'the', 'best', 'book', 'on', 'Python', '']

image.png 這裡會有空字串產生。我們可以計算字串的長度,只返回字串長度大於0的字串。

[tok for tok in listOfTokens if len(tok)>0] ['This', 'book', 'is', 'the', 'best', 'book', 'on', 'Python'] 另外我們考慮構建詞庫,並不用考慮單詞的大小寫,全部改為小寫

[tok.lower() for tok in listOfTokens if len(tok)>0] ['this', 'book', 'is', 'the', 'best', 'book', 'on', 'python'] 這麼一來我們就完成了簡單文字的切分。當然一些文字也有非常複雜的處理方法,具體看文字的內容和性質。

3.測試演算法:使用樸素貝葉斯進行交叉驗證

def textParse(bigString): import re listOfTokens = re.split(r'\W*', bigString) return [tok.lower() for tok in listOfTokens if len(tok) > 2] def spamTest(): docList = [] # 文件(郵件)矩陣 classList = [] # 類標籤列表 for i in range(1, 26): wordlist = textParse(open('trashclass/spam/{}.txt'.format(str(i))).read()) docList.append(wordlist) classList.append(1) wordlist = textParse(open('trashclass/ham/{}.txt'.format(str(i))).read()) docList.append(wordlist) classList.append(0) vocabList = bayes.createVocabList(docList) # 所有郵件內容的詞彙表 import pickle file=open('trashclass/vocabList.txt',mode='wb') #儲存詞彙表 二進位制方式寫入 pickle.dump(vocabList,file) file.close() # 對需要測試的郵件,根據其詞表fileWordList構造向量 # 隨機構建40訓練集與10測試集 trainingSet = list(range(50)) testSet = [] for i in range(10): randIndex = int(np.random.uniform(0, len(trainingSet))) testSet.append(trainingSet[randIndex]) del (trainingSet[randIndex]) trainMat = [] # 訓練集 trainClasses = [] # 訓練集中向量的類標籤列表 for docIndex in trainingSet: # 使用詞袋模式構造的向量組成訓練集 trainMat.append(bayes.setOfWords2Vec(vocabList, docList[docIndex])) trainClasses.append(classList[docIndex]) p0v,p1v,pAb=bayes.trainNB0(trainMat,trainClasses) file=open('trashclass/threeRate.txt',mode='wb') #用以儲存分類器的三個概率 二進位制方式寫入 pickle.dump([p0v,p1v,pAb],file) file.close() errorCount=0 for docIndex in testSet: wordVector=bayes.setOfWords2Vec(vocabList,docList[docIndex]) if bayes.classifyNB(wordVector,p0v,p1v,pAb)!=classList[docIndex]: errorCount+=1 return float(errorCount)/len(testSet) 加入序列化永久性儲存物件,儲存物件的位元組序列到本地檔案中。本例中共有50封電子郵件,其中10封電子郵件被隨機選擇為測試集合。選擇出的數字所對應的文件被新增到測試集,同時也將其從訓練集中剔除。這種隨機選擇資料的一部分作為訓練集,而剩餘部分作為測試集的過程稱為留存交叉驗證。現在我們只作出一次迭代,為了更精確的估計分類器的錯誤率,我們應該多次迭代後求出平均錯誤率。

當然你也可以用:

from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0) 方法很多很簡單,這裡不重複敘述。

(對了上面的程式碼setOfWords2Vec其實是bagOfWords2VecMN,我只是沒有換名字而已,內容是bagOfWords2VecMN)

開始構造分類器:

```import bayes import numpy as np import tkinter as tk from tkinter import filedialog def fileClassify(filepath): import pickle fileWordList=textParse(open(filepath,mode='r').read()) file=open('trashclass/vocabList.txt',mode='rb') vocabList=pickle.load(file) vocabList=vocabList fileWordVec=bayes.setOfWords2Vec(vocabList,fileWordList) #被判斷文件的向量 file=open('trashclass/threeRate.txt',mode='rb') rate=pickle.load(file) p0v=rate[0];p1v=rate[1];pAb=rate[2] return bayes.classifyNB(fileWordVec,p0v,p1v,pAb)

if name=='main': print('樸素貝葉斯分類的錯誤率為:{}'.format(spamTest())) #測試演算法的錯誤率 # filepath=input('輸入需判斷的郵件路徑') root = tk.Tk() root.withdraw() Filepath = filedialog.askopenfilename() # 獲得選擇好的檔案 print(Filepath) #判斷某一路徑下的郵件是否為垃圾郵件 if fileClassify(Filepath)==1: print('垃圾郵件') else: print('非垃圾郵件') ``` 這裡我直接用Tk直接選路徑懶得打了QWQ

image.png

image.png

點關注,防走丟,如有紕漏之處,請留言指教,非常感謝

以上就是本期全部內容。我是fanstuck ,有問題大家隨時留言討論 ,我們下期見。