1. 程式人生 > >第4章 基於概率論的分類方法:樸素貝葉斯

第4章 基於概率論的分類方法:樸素貝葉斯

樸素貝葉斯 概述

貝葉斯分類是一類分類演算法的總稱,這類演算法均以貝葉斯定理為基礎,故統稱為貝葉斯分類。本章首先介紹貝葉斯分類演算法的基礎——貝葉斯定理。最後,我們通過例項來討論貝葉斯分類的中最簡單的一種: 樸素貝葉斯分類。

貝葉斯理論 & 條件概率

貝葉斯理論

我們現在有一個數據集,它由兩類資料組成,資料分佈如下圖所示:

樸素貝葉斯示例資料分佈

我們現在用 p1(x,y) 表示資料點 (x,y) 屬於類別 1(圖中用圓點表示的類別)的概率,用 p2(x,y) 表示資料點 (x,y) 屬於類別 2(圖中三角形表示的類別)的概率,那麼對於一個新資料點 (x,y),可以用下面的規則來判斷它的類別:

  • 如果 p1(x,y) > p2(x,y) ,那麼類別為1
  • 如果 p2(x,y) > p1(x,y) ,那麼類別為2

也就是說,我們會選擇高概率對應的類別。這就是貝葉斯決策理論的核心思想,即選擇具有最高概率的決策。

條件概率

如果你對 p(x,y|c1) 符號很熟悉,那麼可以跳過本小節。

有一個裝了 7 塊石頭的罐子,其中 3 塊是白色的,4 塊是黑色的。如果從罐子中隨機取出一塊石頭,那麼是白色石頭的可能性是多少?由於取石頭有 7 種可能,其中 3 種為白色,所以取出白色石頭的概率為 3/7 。那麼取到黑色石頭的概率又是多少呢?很顯然,是 4/7 。我們使用 P(white) 來表示取到白色石頭的概率,其概率值可以通過白色石頭數目除以總的石頭數目來得到。

包含 7 塊石頭的集合

如果這 7 塊石頭如下圖所示,放在兩個桶中,那麼上述概率應該如何計算?

7塊石頭放入兩個桶中

計算 P(white) 或者 P(black) ,如果事先我們知道石頭所在桶的資訊是會改變結果的。這就是所謂的條件概率(conditional probablity)。假定計算的是從 B 桶取到白色石頭的概率,這個概率可以記作 P(white|bucketB) ,我們稱之為“在已知石頭出自 B 桶的條件下,取出白色石頭的概率”。很容易得到,P(white|bucketA) 值為 2/4 ,P(white|bucketB) 的值為 1/3 。

條件概率的計算公式如下:

P(white|bucketB) = P(white and bucketB) / P(bucketB)

首先,我們用 B 桶中白色石頭的個數除以兩個桶中總的石頭數,得到 P(white and bucketB) = 1/7 .其次,由於 B 桶中有 3 塊石頭,而總石頭數為 7 ,於是 P(bucketB) 就等於 3/7 。於是又 P(white|bucketB) = P(white and bucketB) / P(bucketB) = (1/7) / (3/7) = 1/3 。

另外一種有效計算條件概率的方法稱為貝葉斯準則。貝葉斯準則告訴我們如何交換條件概率中的條件與結果,即如果已知 P(x|c),要求 P(c|x),那麼可以使用下面的計算方法:

計算p(c|x)的方法

使用條件概率來分類

上面我們提到貝葉斯決策理論要求計算兩個概率 p1(x, y) 和 p2(x, y):

  • 如果 p1(x, y) > p2(x, y), 那麼屬於類別 1;
  • 如果 p2(x, y) > p1(X, y), 那麼屬於類別 2.

這並不是貝葉斯決策理論的所有內容。使用 p1() 和 p2() 只是為了儘可能簡化描述,而真正需要計算和比較的是 p(c1|x, y) 和 p(c2|x, y) .這些符號所代表的具體意義是: 給定某個由 x、y 表示的資料點,那麼該資料點來自類別 c1 的概率是多少?資料點來自類別 c2 的概率又是多少?注意這些概率與概率 p(x, y|c1) 並不一樣,不過可以使用貝葉斯準則來交換概率中條件與結果。具體地,應用貝葉斯準則得到:

應用貝葉斯準則

使用上面這些定義,可以定義貝葉斯分類準則為:

  • 如果 P(c1|x, y) > P(c2|x, y), 那麼屬於類別 c1;
  • 如果 P(c2|x, y) > P(c1|x, y), 那麼屬於類別 c2.

在文件分類中,整個文件(如一封電子郵件)是例項,而電子郵件中的某些元素則構成特徵。我們可以觀察文件中出現的詞,並把每個詞作為一個特徵,而每個詞的出現或者不出現作為該特徵的值,這樣得到的特徵數目就會跟詞彙表中的詞的數目一樣多。

我們假設特徵之間 相互獨立 。所謂 獨立(independence) 指的是統計意義上的獨立,即一個特徵或者單詞出現的可能性與它和其他單詞相鄰沒有關係,比如說,“我們”中的“我”和“們”出現的概率與這兩個字相鄰沒有任何關係。這個假設正是樸素貝葉斯分類器中 樸素(naive) 一詞的含義。樸素貝葉斯分類器中的另一個假設是,每個特徵同等重要

Note: 樸素貝葉斯分類器通常有兩種實現方式: 一種基於伯努利模型實現,一種基於多項式模型實現。這裡採用前一種實現方式。該實現方式中並不考慮詞在文件中出現的次數,只考慮出不出現,因此在這個意義上相當於假設詞是等權重的。

樸素貝葉斯 場景

機器學習的一個重要應用就是文件的自動分類。

在文件分類中,整個文件(如一封電子郵件)是例項,而電子郵件中的某些元素則構成特徵。我們可以觀察文件中出現的詞,並把每個詞作為一個特徵,而每個詞的出現或者不出現作為該特徵的值,這樣得到的特徵數目就會跟詞彙表中的詞的數目一樣多。

樸素貝葉斯是上面介紹的貝葉斯分類器的一個擴充套件,是用於文件分類的常用演算法。下面我們會進行一些樸素貝葉斯分類的實踐專案。

樸素貝葉斯 原理

樸素貝葉斯 工作原理

提取所有文件中的詞條並進行去重
獲取文件的所有類別
計算每個類別中的文件數目
對每篇訓練文件: 
    對每個類別: 
        如果詞條出現在文件中-->增加該詞條的計數值(for迴圈或者矩陣相加)
        增加所有詞條的計數值(此類別下詞條總數)
對每個類別: 
    對每個詞條: 
        將該詞條的數目除以總詞條數目得到的條件概率(P(詞條|類別))
返回該文件屬於每個類別的條件概率(P(類別|文件的所有詞條))

樸素貝葉斯 開發流程

收集資料: 可以使用任何方法。
準備資料: 需要數值型或者布林型資料。
分析資料: 有大量特徵時,繪製特徵作用不大,此時使用直方圖效果更好。
訓練演算法: 計算不同的獨立特徵的條件概率。
測試演算法: 計算錯誤率。
使用演算法: 一個常見的樸素貝葉斯應用是文件分類。可以在任意的分類場景中使用樸素貝葉斯分類器,不一定非要是文字。

樸素貝葉斯 演算法特點

優點: 在資料較少的情況下仍然有效,可以處理多類別問題。
缺點: 對於輸入資料的準備方式較為敏感。
適用資料型別: 標稱型資料。

樸素貝葉斯 專案案例

專案案例1: 遮蔽社群留言板的侮辱性言論

專案概述

構建一個快速過濾器來遮蔽線上社群留言板上的侮辱性言論。如果某條留言使用了負面或者侮辱性的語言,那麼就將該留言標識為內容不當。對此問題建立兩個類別: 侮辱類和非侮辱類,使用 1 和 0 分別表示。

開發流程

收集資料: 可以使用任何方法
準備資料: 從文字中構建詞向量
分析資料: 檢查詞條確保解析的正確性
訓練演算法: 從詞向量計算概率
測試演算法: 根據現實情況修改分類器
使用演算法: 對社群留言板言論進行分類

收集資料: 可以使用任何方法

本例是我們自己構造的詞表:

def loadDataSet():
    """
    建立資料集
    :return: 單詞列表postingList, 所屬類別classVec
    """
    postingList = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'], #[0,0,1,1,1......]
                   ['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 is abusive, 0 not
    return postingList, classVec

準備資料: 從文字中構建詞向量

def createVocabList(dataSet):
    """
    獲取所有單詞的集合
    :param dataSet: 資料集
    :return: 所有單詞的集合(即不含重複元素的單詞列表)
    """
    vocabSet = set([])  # create empty set
    for document in dataSet:
        # 操作符 | 用於求兩個集合的並集
        vocabSet = vocabSet | set(document)  # union of the two sets
    return list(vocabSet)

def setOfWords2Vec(vocabList, inputSet):
    """
    遍歷檢視該單詞是否出現,出現該單詞則將該單詞置1
    :param vocabList: 所有單詞集合列表
    :param inputSet: 輸入資料集
    :return: 匹配列表[0,1,0,1...],其中 1與0 表示詞彙表中的單詞是否出現在輸入的資料集中
    """
    # 建立一個和詞彙表等長的向量,並將其元素都設定為0
    returnVec = [0] * len(vocabList)# [0,0......]
    # 遍歷文件中的所有單詞,如果出現了詞彙表中的單詞,則將輸出的文件向量中的對應值設為1
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1
        else:
            print "the word: %s is not in my Vocabulary!" % word
    return returnVec

分析資料: 檢查詞條確保解析的正確性

檢查函式執行情況,檢查詞表,不出現重複單詞,需要的話,可以對其進行排序。

>>> listOPosts, listClasses = bayes.loadDataSet()
>>> myVocabList = bayes.createVocabList(listOPosts)
>>> myVocabList
['cute', 'love', 'help', 'garbage', 'quit', 'I', 'problems', 'is', 'park', 
'stop', 'flea', 'dalmation', 'licks', 'food', 'not', 'him', 'buying', 'posting', 'has', 'worthless', 'ate', 'to', 'maybe', 'please', 'dog', 'how', 
'stupid', 'so', 'take', 'mr', 'steak', 'my']

檢查函式有效性。例如:myVocabList 中索引為 2 的元素是什麼單詞?應該是是 help 。該單詞在第一篇文件中出現了,現在檢查一下看看它是否出現在第四篇文件中。

>>> bayes.setOfWords2Vec(myVocabList, listOPosts[0])
[0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1]

>>> bayes.setOfWords2Vec(myVocabList, listOPosts[3])
[0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]

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

現在已經知道了一個詞是否出現在一篇文件中,也知道該文件所屬的類別。接下來我們重寫貝葉斯準則,將之前的 x, y 替換為 w. 粗體的 w 表示這是一個向量,即它由多個值組成。在這個例子中,數值個數與詞彙表中的詞個數相同。

重寫貝葉斯準則

我們使用上述公式,對每個類計算該值,然後比較這兩個概率值的大小。

問: 上述程式碼實現中,為什麼沒有計算P(w)?

答:根據上述公式可知,我們右邊的式子等同於左邊的式子,由於對於每個ci,P(w)是固定的。並且我們只需要比較左邊式子值的大小來決策分類,那麼我們就可以簡化為通過比較右邊分子值得大小來做決策分類。

首先可以通過類別 i (侮辱性留言或者非侮辱性留言)中的文件數除以總的文件數來計算概率 p(ci) 。接下來計算 p(w | ci) ,這裡就要用到樸素貝葉斯假設。如果將 w 展開為一個個獨立特徵,那麼就可以將上述概率寫作 p(w0, w1, w2...wn | ci) 。這裡假設所有詞都互相獨立,該假設也稱作條件獨立性假設(例如 A 和 B 兩個人拋骰子,概率是互不影響的,也就是相互獨立的,A 拋 2點的同時 B 拋 3 點的概率就是 1/6 * 1/6),它意味著可以使用 p(w0 | ci)p(w1 | ci)p(w2 | ci)...p(wn | ci) 來計算上述概率,這樣就極大地簡化了計算的過程。

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

def _trainNB0(trainMatrix, trainCategory):
    """
    訓練資料原版
    :param trainMatrix: 檔案單詞矩陣 [[1,0,1,1,1....],[],[]...]
    :param trainCategory: 檔案對應的類別[0,1,1,0....],列表長度等於單詞矩陣數,其中的1代表對應的檔案是侮辱性檔案,0代表不是侮辱性矩陣
    :return:
    """
    # 檔案數
    numTrainDocs = len(trainMatrix)
    # 單詞數
    numWords = len(trainMatrix[0])
    # 侮辱性檔案的出現概率,即trainCategory中所有的1的個數,
    # 代表的就是多少個侮辱性檔案,與檔案的總數相除就得到了侮辱性檔案的出現概率
    pAbusive = sum(trainCategory) / float(numTrainDocs)
    # 構造單詞出現次數列表
    p0Num = zeros(numWords) # [0,0,0,.....]
    p1Num = zeros(numWords) # [0,0,0,.....]

    # 整個資料集單詞出現總數
    p0Denom = 0.0
    p1Denom = 0.0
    for i in range(numTrainDocs):
        # 是否是侮辱性檔案
        if trainCategory[i] == 1:
            # 如果是侮辱性檔案,對侮辱性檔案的向量進行加和
            p1Num += trainMatrix[i] #[0,1,1,....] + [0,1,1,....]->[0,2,2,...]
            # 對向量中的所有元素進行求和,也就是計算所有侮辱性檔案中出現的單詞總數
            p1Denom += sum(trainMatrix[i])
        else:
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    # 類別1,即侮辱性文件的[P(F1|C1),P(F2|C1),P(F3|C1),P(F4|C1),P(F5|C1)....]列表
    # 即 在1類別下,每個單詞出現的概率
    p1Vect = p1Num / p1Denom# [1,2,3,5]/90->[1/90,...]
    # 類別0,即正常文件的[P(F1|C0),P(F2|C0),P(F3|C0),P(F4|C0),P(F5|C0)....]列表
    # 即 在0類別下,每個單詞出現的概率
    p0Vect = p0Num / p0Denom
    return p0Vect, p1Vect, pAbusive

測試演算法: 根據現實情況修改分類器

在利用貝葉斯分類器對文件進行分類時,要計算多個概率的乘積以獲得文件屬於某個類別的概率,即計算 p(w0|1) * p(w1|1) * p(w2|1)。如果其中一個概率值為 0,那麼最後的乘積也為 0。為降低這種影響,可以將所有詞的出現數初始化為 1,並將分母初始化為 2 (取1 或 2 的目的主要是為了保證分子和分母不為0,大家可以根據業務需求進行更改)。

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

下圖給出了函式 f(x) 與 ln(f(x)) 的曲線。可以看出,它們在相同區域內同時增加或者減少,並且在相同點上取到極值。它們的取值雖然不同,但不影響最終結果。

函式影象

def trainNB0(trainMatrix, trainCategory):
    """
    訓練資料優化版本
    :param trainMatrix: 檔案單詞矩陣
    :param trainCategory: 檔案對應的類別
    :return:
    """
    # 總檔案數
    numTrainDocs = len(trainMatrix)
    # 總單詞數
    numWords = len(trainMatrix[0])
    # 侮辱性檔案的出現概率
    pAbusive = sum(trainCategory) / float(numTrainDocs)
    # 構造單詞出現次數列表
    # p0Num 正常的統計
    # p1Num 侮辱的統計
    p0Num = ones(numWords)#[0,0......]->[1,1,1,1,1.....]
    p1Num = ones(numWords)

    # 整個資料集單詞出現總數,2.0根據樣本/實際調查結果調整分母的值(2主要是避免分母為0,當然值可以調整)
    # p0Denom 正常的統計
    # p1Denom 侮辱的統計
    p0Denom = 2.0
    p1Denom = 2.0
    for i in range(numTrainDocs):
        if trainCategory[i] == 1:
            # 累加辱罵詞的頻次
            p1Num += trainMatrix[i]
            # 對每篇文章的辱罵的頻次 進行統計彙總
            p1Denom += sum(trainMatrix[i])
        else:
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    # 類別1,即侮辱性文件的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表
    p1Vect = log(p1Num / p1Denom)
    # 類別0,即正常文件的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表
    p0Vect = log(p0Num / p0Denom)
    return p0Vect, p1Vect, pAbusive

使用演算法: 對社群留言板言論進行分類

樸素貝葉斯分類函式

def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    """
    使用演算法:
        # 將乘法轉換為加法
        乘法: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))
    # 大家可能會發現,上面的計算公式,沒有除以貝葉斯準則的公式的分母,也就是 P(w) (P(w) 指的是此文件在所有的文件中出現的概率)就進行概率大小的比較了,
    # 因為 P(w) 針對的是包含侮辱和非侮辱的全部文件,所以 P(w) 是相同的。
    # 使用 NumPy 陣列來計算兩個向量相乘的結果,這裡的相乘是指對應元素相乘,即先將兩個向量中的第一個元素相乘,然後將第2個元素相乘,以此類推。
    # 我的理解是:這裡的 vec2Classify * p1Vec 的意思就是將每個詞與其對應的概率相關聯起來
    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


def testingNB():
    """
    測試樸素貝葉斯演算法
    """
    # 1. 載入資料集
    listOPosts, listClasses = loadDataSet()
    # 2. 建立單詞集合
    myVocabList = createVocabList(listOPosts)
    # 3. 計算單詞是否出現並建立資料矩陣
    trainMat = []
    for postinDoc in listOPosts:
        # 返回m*len(myVocabList)的矩陣, 記錄的都是0,1資訊
        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)