本篇內容為《機器學習實戰》第 4 章 基於概率論的分類方法:樸素貝葉斯程式清單。所用程式碼為 python3。
樸素貝葉斯
優點:在資料較少的情況下仍然有效,可以處理多類別問題。
缺點:對於輸入資料的準備方式較為敏感。
適用資料型別:標稱型資料。
使用 Python 進行文字分類
簡單描述這個過程為:從文字中獲取特徵,構建分類器,進行分類輸出結果。這裡的特徵是來自文字的詞條 (token),需要將每一個文字片段表示為一個詞條向量,其中值為 1 表示詞條出現在文件中,0 表示詞條未出現。
接下來給出將文字轉換為數字向量的過程,然後基於這些向量來計算條件概率,並在此基礎上構建分類器。
下面我們以線上社群的留言板為例,給出一個用來過濾的例子。
為了不影響社群的發展,我們需要遮蔽侮辱性的言論,所以要構建一個快速過濾器,如果某條留言使用來負面或者侮辱性的語言,就將該留言標識為內容不當。對此問題建立兩個類別:侮辱類和非侮辱類,分別使用 1 和 0 來表示。
準備資料:從文字中構建詞向量
程式清單 4-1 詞表到向量的轉換函式
```
Created on Sep 10, 2018
@author: yufei
```
# coding=utf-8
from numpy import *
# 建立一些例項樣本
def loadDataSet():
postingList = [[`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 代表正常言論
"""
變數 postingList 返回的是進行詞條切分後的文件集合。
留言文字被切分成一些列詞條集合,標點符號從文字中去掉
變數 classVec 返回一個類別標籤的集合。
這些文字的類別由人工標註,標註資訊用於訓練程式以便自動檢測侮辱性留言。
"""
return postingList, classVec
"""
建立一個包含在所有文件中出現的不重複詞的列表
是用python的 Set 資料型別
將詞條列表輸給 Set 建構函式,set 就會返回一個不重複詞表
"""
def createVocabList(dataSet):
# 建立一個空集合
vocabSet = set([])
# 將每篇文件返回的新詞集合新增進去,即建立兩個集合的並集
for document in dataSet:
vocabSet = vocabSet | set(document)
# 獲得詞彙表
return list(vocabSet)
# 引數:詞彙表,某個文件
def setOfWords2Vec(vocabList, inputSet):
# 建立一個和詞彙表等長的向量,將其元素都設定為 0
returnVec = [0] * len(vocabList)
# 遍歷文件中所有單詞
for word in inputSet:
# 如果出現詞彙表中的單詞,將輸出的文件向量中的對應值設為 1
if word in vocabList:
returnVec[vocabList.index(word)] = 1
else:
print(`the word: %s is not in my Vocabulary!` % word)
# 輸出文件向量,向量元素為 1 或 0
return returnVec
在 python 提示符下,執行程式碼並得到結果:
>>> import bayes
>>> list0Posts, listClasses = bayes.loadDataSet()
>>> myVocabList = bayes.createVocabList(list0Posts)
>>> myVocabList
[`problems`, `mr`, `ate`, `buying`, `not`, `garbage`, `how`, `maybe`, `stupid`, `cute`, `stop`, `help`, `dalmation`, `take`, `is`, `worthless`, `him`, `flea`, `park`, `my`, `I`, `to`, `licks`, `steak`, `dog`, `love`, `quit`, `so`, `please`, `posting`, `has`, `food`]
即可得到的一個不會出現重複單詞的詞表myVocabList
,目前該詞表還沒有排序。
繼續執行程式碼:
>>> bayes.setOfWords2Vec(myVocabList, list0Posts[3])
[0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
>>> bayes.setOfWords2Vec(myVocabList, list0Posts[0])
[0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0]
函式setOfWords2Vec
使用詞彙表或者說想要檢查的所有單詞作為輸入,然後為其中每一個單詞構建一個特徵。一旦給定一篇文章(本例中指一條留言),該文件就會被轉換為詞向量。
訓練演算法:從詞向量計算概率
函式虛擬碼如下:
··· 計算每個類別中的文件數目
··· 對每篇訓練文件:
······ 對每個類別:
········· 如果詞條出現在文件中—>增加該詞條的計數值
········· 增加所有詞條的計數值
······ 對每個類別:
········· 對每個詞條:
············ 將該詞條對數目除以總詞條數目得到條件概率
······ 返回每個類別對條件概率
程式清單 4-2 樸素貝葉斯分類器訓練函式
```
Created on Sep 11, 2018
@author: yufei
```
# 引數:文件矩陣 trainMatrix,每篇文件的類別標籤所構成的向量 trainCategory
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) #文件的個數
numWords = len(trainMatrix[0]) #獲取第一篇文件的單詞長度
"""
計算文件屬於侮辱性文件的概率
用類別為1的個數除以總篇數
sum([0,1,0,1,0,1])=3,也即是 trainCategory 裡面 1 的個數
"""
pAbusive = sum(trainCategory) / float(numTrainDocs)
"""
初始化概率
當利用貝葉斯分類器對文件分類時,計算多個概率的乘積以獲得屬於某個類別的概率
把所有詞出現次數初始化為1,分母初始化為2,用log避免數太小被約掉
"""
p0Num = ones(numWords)
p1Num = ones(numWords)
p0Denom = 2.0
p1Denom = 2.0
# 遍歷訓練集 trainMatrix 中的所有文件
for i in range(numTrainDocs):
# 侮辱性詞語在某個文件中出現
if trainCategory[i] == 1:
# 該詞對應個數加一,即分子把所有的文件向量按位置累加
# trainMatrix[2] = [1,0,1,1,0,0,0];trainMatrix[3] = [1,1,0,0,0,1,1]
p1Num += trainMatrix[i]
# 文件總詞數加一,即對於分母
# 把trainMatrix[2]中的值先加起來為3,再把所有這個類別的向量都這樣累加起來,這個是計算單詞總數目
p1Denom += sum(trainMatrix[i])
# 正常詞語在某個文件中出現,同上
else:
p0Num += trainMatrix[i]
p0Denom +=sum(trainMatrix[i])
"""
對每個元素除以該類別的總詞數,得條件概率
防止太多的很小的數相乘造成下溢。對乘積取對數
# p1Vect = log(p1Num / p1Denom)
# p0Vect = log(p0Num / p0Denom)
"""
p1Vect = p1Num / p1Denom
p0Vect = p0Num / p0Denom
"""
函式返回兩個向量和一個概率
返回每個類別的條件概率,是一個向量
在向量裡面和詞彙表向量長度相同
每個位置代表這個單詞在這個類別中的概率
"""
return p0Vect, p1Vect, pAbusive
在 python 提示符下,執行程式碼並得到結果:
>>> from numpy import *
>>> importlib.reload(bayes)
<module `bayes` from `/Users/Desktop/Coding/bayes.py`>
>>> list0Posts, listClasses = bayes.loadDataSet()
>>> myVocabList = bayes.createVocabList(list0Posts)
以上,調入資料後構建了一個包含所有詞的列表myVocabList
>>> trainMat = []
>>> for postinDoc in list0Posts:
... trainMat.append(bayes.setOfWords2Vec(myVocabList, postinDoc))
這個for
迴圈使用詞向量來填充trainMat
列表。
繼續給出屬於侮辱性文件的概率以及兩個類別的概率向量。
>>> p0V, p1V, pAb = bayes.trainNB0(trainMat, listClasses)
檢視變數的內部值
>>> pAb
0.5
>>> p0V
array([0.03846154, 0.07692308, 0.03846154, 0.07692308, 0.07692308,
0.07692308, 0.07692308, 0.03846154, 0.03846154, 0.03846154,
0.07692308, 0.07692308, 0.15384615, 0.07692308, 0.07692308,
0.07692308, 0.03846154, 0.07692308, 0.07692308, 0.07692308,
0.07692308, 0.07692308, 0.03846154, 0.07692308, 0.11538462,
0.07692308, 0.07692308, 0.03846154, 0.03846154, 0.03846154,
0.07692308, 0.03846154])
>>> p1V
array([0.0952381 , 0.04761905, 0.0952381 , 0.0952381 , 0.14285714,
0.04761905, 0.04761905, 0.0952381 , 0.0952381 , 0.14285714,
0.04761905, 0.04761905, 0.04761905, 0.04761905, 0.04761905,
0.04761905, 0.0952381 , 0.04761905, 0.04761905, 0.04761905,
0.0952381 , 0.04761905, 0.0952381 , 0.04761905, 0.0952381 ,
0.04761905, 0.04761905, 0.19047619, 0.0952381 , 0.0952381 ,
0.04761905, 0.0952381 ])
我們發現文件屬於侮辱類的概率pAb
為 0.5,檢視pV1
的最大值 0.19047619,它出現在第 27 個下標位置,檢視myVocabList
的第 27 個下標位置該詞為 stupid,說明這是最能表徵類別 1 的單詞。
測試演算法:根據現實情況修改分類器
程式清單 4-3 樸素貝葉斯分類函式
```
Created on Sep 11, 2018
@author: yufei
```
# vec2Classify: 要分類的向量
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + log(pClass1)
p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
def testingNB():
list0Posts, listClasses = loadDataSet()
myVocabList = createVocabList(list0Posts)
trainMat = []
for posinDoc in list0Posts:
trainMat.append(setOfWords2Vec(myVocabList, posinDoc))
p0V, p1V, pAb = trainNB0(array(trainMat), array(listClasses))
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))
在 python 提示符下,執行程式碼並得到結果:
>>> importlib.reload(bayes)
<module `bayes` from `/Users/Desktop/Coding/bayes.py`>
>>> bayes.testingNB()
[`love`, `my`, `dalmation`] classified as: 0
[`stupid`, `garbage`] classified as: 1
分類器輸出結果,分類正確。
準備資料:文件詞袋模型
詞集模型:將每個詞的出現與否作為一個特徵。即我們上面所用到的。
詞袋模型:將每個詞出現次數作為一個特徵。每遇到一個單詞,其詞向量對應值 +1,而不是全設定為 1。
對函式setOfWords2Vec()
進行修改,修改後的函式為bagOfWords2VecMN
。
程式清單 4-4 樸素貝葉斯詞袋模型
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in inputSet:
returnVec[vocabList.index(word)] += 1
return returnVec
修改的地方為:每當遇到一個單詞時,它會增加詞向量中的對應值,而不只是將對應的數值設為 1。
下面我們將利用該分類器來過濾垃圾郵件。
示例:使用樸素貝葉斯過濾垃圾郵件
測試演算法:使用樸素貝葉斯進行交叉驗證
程式清單 4-5 檔案解析及完整的垃圾郵件測試函式
```
Created on Sep 11, 2018
@author: yufei
```
"""
接受一個大字串並將其解析為字串列表
"""
def textParse(bigString): #input is big string, #output is word list
import re
listOfTokens = re.split(r`W*`, bigString)
# 去掉小於兩個字元的字串,並將所有字串轉換為小寫
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
"""
對貝葉斯垃圾郵件分類器進行自動化處理
"""
def spamTest():
docList=[]; classList = []; fullText =[]
#匯入並解析文字檔案為詞列表
for i in range(1,26):
wordList = textParse(open(`email/spam/%d.txt` % i, encoding=`ISO-8859-1`).read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(1)
wordList = textParse(open(`email/ham/%d.txt` % i, encoding=`ISO-8859-1`).read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(0)
vocabList = createVocabList(docList)#create vocabulary
trainingSet = list(range(50)); testSet=[] #create test set
for i in range(10):
randIndex = int(random.uniform(0,len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
trainMat=[]; trainClasses = []
# 遍歷訓練集的所有文件,對每封郵件基於詞彙表並使用 bagOfWords2VecMN 來構建詞向量
for docIndex in trainingSet:#train the classifier (get probs) trainNB0
trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
trainClasses.append(classList[docIndex])
# 用上面得到的詞在 trainNB0 函式中計算分類所需的概率
p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
errorCount = 0
# 對測試集分類
for docIndex in testSet: #classify the remaining items
wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
# 如果郵件分類錯誤,錯誤數加 1
if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
errorCount += 1
print ("classification error",docList[docIndex])
# 給出總的錯誤百分比
print (`the error rate is: `,float(errorCount)/len(testSet))
#return vocabList,fullText
在 python 提示符下,執行程式碼並得到結果:
>>> importlib.reload(bayes)
<module `bayes` from `/Users/Desktop/Coding/bayes.py`>
>>> bayes.spamTest()
classification error [`home`, `based`, `business`, `opportunity`, `knocking`, `your`, `door`, `don`, `rude`, `and`, `let`, `this`, `chance`, `you`, `can`, `earn`, `great`, `income`, `and`, `find`, `your`, `financial`, `life`, `transformed`, `learn`, `more`, `here`, `your`, `success`, `work`, `from`, `home`, `finder`, `experts`]
the error rate is: 0.1
函式spamTest()
會輸出在 10 封隨機選擇的電子郵件上的分類錯誤率。由於是隨機選擇的,所以每次的輸出結果可能有些差別。如果想要更好地估計錯誤率,那麼就應該將上述過程重複多次求平均值。
這裡的程式碼需要注意的兩個地方是:
1、直接使用語句
wordList = textParse(open(`email/spam/%d.txt` % i).read())
報錯UnicodeDecodeError: `utf-8` codec can`t decode byte 0x92 in position 884: invalid start byte
。這是因為在檔案裡可能存在不是以 utf-8 格式儲存的字元,需改為wordList = textParse(open(`email/spam/%d.txt` % i, encoding=`ISO-8859-1`).read())
。2、將隨機選出的文件新增到測試集後,要同時將其從訓練集中刪除,使用語句
del(trainingSet[randIndex])
,此時會報錯TypeError: `range` object doesn`t support item deletion
,這是由於 python2 和 python3 的不同而導致的。在 python2 中可以直接執行,而在 python3 中需將trainingSet
設為trainingSet = list(range(50))
,而不是trainingSet = range(50)
,即必須讓它是一個list
再進行刪除操作。
以上,我們就用樸素貝葉斯對文件進行了分類。
參考連結:
《機器學習實戰》筆記之四——基於概率論的分類方法:樸素貝葉斯
UnicodeDecodeError: `utf-8` codec can`t decode byte 0x92 in position 884: invalid start byte
不足之處,歡迎指正。