【資料科學系統學習】機器學習演算法 # 西瓜書學習記錄 [6] 樸素貝葉斯實踐

xx要努力學程式設計發表於2018-09-11

本篇內容為《機器學習實戰》第 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

不足之處,歡迎指正。

相關文章