《機器學習實戰》-樸素貝葉斯

基於機率論的分類方法:樸素貝葉斯

本章內容算法

  • 基於貝葉斯決策理論的分類方法
  • 條件機率
  • 使用條件機率來分類
  • 使用樸素貝葉斯進行文檔分類
  • 使用 Python 進行文本分類
  • 示例:使用樸素貝葉斯過濾垃圾郵件
  • 總結

k-近鄰算法和決策樹都要求分類器作出決策,並經過輸入特徵輸出一個最優的類別猜想結果,同時給出這個猜想值的機率估計值。
機率論是許多機器學習學習算法的基礎,例如在計算信息增益的時候咱們講到的每一個特徵對應不一樣值所佔的權重,即取得某個特徵值的機率。
本章則會從一個最簡單的機率分類器開始,而後給出一些假設來學習樸素貝葉斯分類器。app

基於貝葉斯決策理論的分類方法

樸素貝葉斯的優缺點

優勢:在數據較少的狀況下仍然有效,能夠處理多類別問題
缺點:對於輸入數據的準備方式較爲敏感
適用數據類型:標稱型數據

貝葉斯決策理論

如圖4-1所示,假設咱們有一個由兩類數據組成的數據集
​ 圖4-1 兩類數據組成的數據集框架

對於圖4-1,咱們用\(p1(x,y)\)表示數據點\((x,y)\)屬於圓點表示的類別的機率;用\(p2(x,y)\)表示數據點\((x,y)\)屬於三角形表示的類別的機率。
那麼對於一個新數據點\((x,y)\),能夠用下面的規則來判斷它的類別:
​ * 若是\(p1(x,y)>p2(x,y)\),那麼類別爲1
​ * 若是\(p2(x,y)>p1(x,y)\),那麼類別爲2
經過該規則,咱們能夠發現咱們始終會選擇高几率對應的類別。這就是貝葉斯決策理論的核心思想,即選擇最高几率的決策。
現現在,假設該圖中的整個數據使用6個浮點數來表示,而且只用兩行 Python 代碼計算類別。咱們會可能有如下三種方法能夠解決:
​ 1. 使用kNN,進行1000次距離計算
​ 2. 使用決策樹,分別由 x 軸,y 軸劃分數據
​ 3. 計算數據點屬於每一個類別的機率,並進行比較
決策樹不會很是成功;而使用 kNN 計算量較於方法3過於龐大,所以咱們會選擇方法3對數據進行分類。
爲了可以使用方法3,咱們須要經過條件機率這個知識點計算p1和p2的機率大小less

條件機率

假設棋盆裏有7粒棋子,3枚是灰色的,4枚是黑色的。若是從棋盆裏取出一枚棋子,取出白棋子的機率爲3/7,取出黑棋子的機率是4/7。
若是咱們隨機的把這7粒棋子裝入兩個棋盆中,如圖4-2
​ 圖4-2 裝入兩個棋盆的棋子dom

咱們把取到灰棋的機率稱爲 P(g),取到黑棋的機率稱爲 P(b)。如今咱們想要知道從 B 桶中取到灰棋的機率,須要用到的就是條件機率,記做 P(g|b_B)=1/3。
條件機率的計算公式:P(g|b_B) = P(g and b_B)/P(b_B) = (1/7) / (3/7) = 1/3
另外一張有效計算條件機率的方法稱爲貝葉斯準則,若是已知P(x|c),能夠求出P(c|x)的值
\(p(c|x)={\frac{p(x|c)p(c)}{p(x)}}\)機器學習

使用條件機率來分類

以前介紹貝葉斯決策理論須要計算兩個機率p1(x,y)和p2(x,y),可是在本例中真正須要計算的事 p(c~1~|x,y) 和 p(c~2~|x,y)。這些符號表明的意義是:給定某個由 x、y 表示的數據點,那麼這個數據點來自類別 c~1~ 和 c~2~的機率是多少?而這個符號咱們能夠經過貝葉斯準則獲得:
\(p(c_i|x,y)={\frac{p(x,y|c_i)p(c_i)}{p(x,y)}}\)
使用定義後,能夠定義貝葉斯分類準則爲:
​ * 若是 P(c~1~|x,y) > P(c~2~|x,y),那麼屬於類別 c~1~
​ * 若是 P(c~2~|x,y) < P(c~2~|x,y),那麼屬於類別 c~2~函數

使用樸素貝葉斯進行文檔分類

經過觀察文檔中出現的詞,對於現存的一個詞彙表,把每一個詞的出現或者不出現做爲一個特徵。
樸素貝葉斯的通常過程
​ 1. 收集數據:可使用任何方法。本章使用 RSS 源
​ 2. 準備數據:須要數值型或者布爾型數據
​ 3. 分析數據:有大量特徵時,繪製特徵做用不大,此時使用直方圖效果更好
​ 4. 訓練算法:計算不一樣的獨立特徵的條件機率
​ 5. 測試算法:計算錯誤率
​ 6. 使用算法:一個常見的樸素貝葉斯應用是文檔分類。能夠在任意的分類場景中使用樸素貝葉斯分類器,不必定非要是文本。
假設詞彙表中有1000個單詞。要獲得好的機率分佈,就須要足夠的數據樣本,假設樣本數爲 N。
由統計學可知,若是每一個特徵須要 N 個樣本,那麼對於10個特徵將須要 N^10^個樣本,對於包含1000個特徵的詞彙表將須要 N^1000^個樣本。
若是特徵之間相互獨立,那麼樣本數就能夠從 N^1000^減小到1000*N。獨立指的是統計意義上的獨立,即一個特徵或者單詞出現的可能性與其餘相鄰單詞沒有關係。post

使用 Python 進行文本分類

拆分文本以後對比拆分後的詞條向量和現有的詞彙表,詞條出現即不合法詞條值爲1;詞條未出現即合法詞條值爲0。學習

準備數據:從文本中構建詞向量

爲了可以對比詞條和詞彙表,咱們考慮文檔中的全部單詞並把把文本構形成詞條向量,再決定將哪些詞歸入詞彙表。
所以咱們須要構造一個函數 load_data_set 方法。

# bayes.py

def load_data_set():
    """建立實驗樣本"""
    # 實驗樣本
    posting_list = [['my', 'dog', 'has', 'flea', 'problem', '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']]

    # 樣本標記
    class_vec = [0, 1, 0, 1, 0, 1]  # 1表明侮辱性文字,0表明正常言論

    return posting_list, class_vec


def create_vocab_list(data_set):
    """建立詞彙表"""
    vocab_set = set([])

    # 建立不含重複單詞的詞彙表
    for document in data_set:
        vocab_set = vocab_set | set(document)

    return list(vocab_set)


def set_of_words2vec(vocab_list, input_set):
    """建立文檔向量"""
    # 建立一個和詞彙表等長且元素全爲0的列表
    return_vec = [0] * len(vocab_list)

    # 遍歷文檔
    for word in input_set:
        # 若是單詞出如今詞彙表中,則將0替換成1
        if word in vocab_list:
            return_vec[vocab_list.index(word)] = 1
        else:
            print('{} 不在詞彙表中'.format(word))

    return return_vec


def set_of_words2vec_run():
    list_of_posts, list_classes = load_data_set()
    my_vocab_list = create_vocab_list(list_of_posts)
    print(my_vocab_list)
    # ['park', 'ate', 'licks', 'steak', 'please', 'love', 'is', 'him', 'maybe', 'how', 'dog', 'has', 'food', 
    # 'dalmation', 'I', 'my', 'stop', 'worthless', 'help', 'garbage', 'stupid', 'quit', 'cute', 'mr', 'buying', 
    # 'posting', 'not', 'flea', 'problem', 'so', 'to', 'take']
    return_vec = set_of_words2vec(my_vocab_list, list_of_posts[0])
    print(return_vec)
    # [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1]


if __name__ == '__main__':
    set_of_words2vec_run()

訓練算法:從詞向量計算機率

若是咱們將上述公式中的(x,y)替換成粗體 w,粗體 w 表示一個由多個值組成的向量。即上述的 return_vec。
\(p(c_i|w)={\frac{p(w|c_i)p(c_i)}{p(w)}}\)
對上述公式的解釋:p(c~i~)指的是類別 i 中文檔數除以總的文檔數;p(w|c~i~)須要用到樸素貝葉斯假設,即將 w 展開爲一個獨立特徵,那麼就能夠寫做p(w~0~,w~1~,w~2~...w~N~|c~i~)。假設全部詞都相互獨立,便可以使用p(w~0~|c~i~)p(w~1~|c~i~)p(w~2~|c~i~)...p(w~N~|c~i~)來計算上述機率。
函數的僞代碼以下:

```
計算每一個類別中的文檔數目
對每篇訓練文檔:
    對每一個類別:
        若是詞條出如今文檔中-》增長該詞條的計數值
        增長全部詞條的計數值
    對每一個類別:
        對每一個詞條:
            將該詞條的數目除以總詞條數目獲得條件機率
    返回每一個類別的條件機率
```

咱們構造一個 train_nb_0 方法來實現咱們的僞代碼:

# bayes.py

def train_nb_0(train_matrix, train_category):
    """計算每一個類別中的文檔數目"""
    import numpy

    num_train_docs = len(train_matrix)
    num_words = len(train_matrix[0])

    # 計算屬於侮辱性文檔的機率,由於侮辱爲1,非侮辱爲0,使用 sum 函數能夠計算
    p_abusive = sum(train_category) / float(num_train_docs)

    # 初始化機率
    p0_num = numpy.zeros(num_words)
    p1_num = numpy.zeros(num_words)
    p0_denom = 0
    p1_denom = 0

    # 遍歷文檔矩陣
    for i in range(num_train_docs):
        # 計算文檔中侮辱性的句子總和以及單詞在詞彙表中的計數總和
        if train_category[i] == 1:
            p1_num += train_matrix[i]
            p1_denom += sum(train_matrix[i])
        # 計算文檔中非侮辱性的句子總和以及單詞在詞彙表中的計數總和
        else:
            p0_num += train_matrix[i]
            p0_denom += sum(train_matrix[i])

    # 計算每一個元素除以該類別中的總詞數,即矩陣中的每一個元素除以int(p1_denom)
    p1_vect = p1_num / p1_denom
    p0_vect = p0_num / p0_denom

    return p0_vect, p1_vect, p_abusive


def train_nb_0_run():
    # 建立詞彙表
    list_of_posts, list_classes = load_data_set()
    my_vocab_list = create_vocab_list(list_of_posts)

    # 把數據集中的全部單詞轉化成0,1的值,即不在詞彙表中、在詞彙表中
    train_mat = []
    for post_in_doc in list_of_posts:
        # 比較詞彙表與句子,在詞彙表中則詞彙表中單詞數+1
        train_mat.append(set_of_words2vec(my_vocab_list, post_in_doc))

    # 每一個元素除以該類別總詞數,返回每一個類別的條件機率
    p0_v, p1_v, p_ab = train_nb_0(train_mat, list_classes)
    print(p0_v)
    print(p1_v)
    print(p_ab)
    # 0.5


if __name__ == '__main__':
    # set_of_words2vec_run()
    train_nb_0_run()

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

利用貝葉斯對文檔進行分類時,要計算多個機率的乘積以得到文檔屬於某個類別的機率,即計算 p(w~0~|1)p(w~1~|1)p(w~2~|1)。若是其中一個機率值爲0,那麼最後的乘積也爲0.所以將全部詞的出現次數初始化爲1,並將分母初始化爲2。
所以修改 bayes.py 文件下的 train_nb_0 方法

# bayes.py

def train_nb_0(train_matrix, train_category):
    """計算每一個類別中的文檔數目"""
    import numpy

    num_train_docs = len(train_matrix)
    num_words = len(train_matrix[0])

    p_abusive = sum(train_category) / float(num_train_docs)

    # 用1填充矩陣
    p0_num = numpy.ones(num_words)
    p1_num = numpy.ones(num_words)
    p0_denom = 2
    p1_denom = 2

    for i in range(num_train_docs):
        if train_category[i] == 1:
            p1_num += train_matrix[i]
            p1_denom += sum(train_matrix[i])
        else:
            p0_num += train_matrix[i]
            p0_denom += sum(train_matrix[i])

    p1_vect = p1_num / p1_denom
    p0_vect = p0_num / p0_denom

    return p0_vect, p1_vect, p_abusive

另外一個問題是下溢出,即因爲太多很小的數相乘形成的。當計算p(w~0~|c~i~)p(w~1~|c~i~)p(w~2~|c~i~)...p(w~N~|c~i~)時,因爲大部分因子都很是小,Python 得出的計算結果可能四捨五入以後會獲得0。所以咱們利用代數中的 ln(a*b)=ln(a)+ln(b),即修改 train_nb_0 方法中的返回結果

def train_nb_0(train_matrix, train_category):
    """計算每一個類別中的文檔數目"""
    import numpy

    num_train_docs = len(train_matrix)
    num_words = len(train_matrix[0])

    p_abusive = sum(train_category) / float(num_train_docs)

    # 用1填充矩陣
    p0_num = numpy.ones(num_words)
    p1_num = numpy.ones(num_words)
    p0_denom = 2
    p1_denom = 2

    for i in range(num_train_docs):
        if train_category[i] == 1:
            p1_num += train_matrix[i]
            p1_denom += sum(train_matrix[i])
        else:
            p0_num += train_matrix[i]
            p0_denom += sum(train_matrix[i])
    
    p1_vect = numpy.log(p1_num / p1_denom)
    p0_vect = numpy.log(p0_num / p0_denom)

    return p0_vect, p1_vect, p_abusive

經過 classify_nb 方法構建完整的分類器:

# bayes.py

def classify_nb(vec2_classify, p0_vec, p1_vec, p_class1):
    """構建完整的分類器"""
    import math
    p1 = sum(vec2_classify * p1_vec) + math.log(p_class1)
    p0 = sum(vec2_classify * p0_vec) + math.log(1 - p_class1)

    if p1 > p0:
        return 1
    else:
        return 0


def testing_nb_run():
    import numpy

    list_of_posts, list_classes = load_data_set()
    my_vocab_list = create_vocab_list(list_of_posts)

    train_mat = []
    for post_in_doc in list_of_posts:
        train_mat.append(set_of_words2vec(my_vocab_list, post_in_doc))

    p0_v, p1_v, p_ab = train_nb_0(train_mat, list_classes)
    test_entry = ['love', 'my', 'dalmation']
    this_doc = numpy.array(set_of_words2vec(my_vocab_list, test_entry))
    print('{} 分類結果 {}'.format(test_entry, classify_nb(this_doc, p0_v, p1_v, p_ab)))

    # test_entry = ['stupid', 'garbage']
    # this_doc = numpy.array(set_of_words2vec(my_vocab_list, test_entry))
    # print('{} 分類結果 {}'.format(test_entry, classify_nb(this_doc, p0_v, p1_v, p_ab)))


if __name__ == '__main__':
    # set_of_words2vec_run()
    # train_nb_0_run()
    testing_nb_run()

準備數據:文檔詞袋模型

若是一個詞在文檔中只出現一次做爲特徵,那麼該模型稱做詞集模型;若是一個詞在文檔中出現不止一次,那麼文檔是否出現做爲特徵並不能確切的表達出某種信息,這種模型稱做詞袋模型。
爲了適應詞袋模型,咱們把 set_of_words2vec 方法修改爲 bag_of_words2vec 方法

# bayes.py

def bag_of_words2vec(vocal_list, input_set):
    """詞袋模型"""
    return_vec = [0] * len(vocal_list)

    # 遍歷詞彙表有對應的單詞計數加1
    for word in input_set:
        if word in vocal_list:
            return_vec[vocal_list.index(word)] += 1

    return return_vec

示例:使用樸素貝葉斯過濾垃圾郵件

咱們已經手動構建了一個樸素貝葉斯分類器,咱們能夠經過上述的分類器過濾垃圾郵件。可是下面咱們將介紹如何使用通用框架來解決該問題。
​ 1. 收集數據:提供文本文件
​ 2. 準備數據:將文本解析成詞條向量
​ 3. 分析數據:檢查詞條確保解析的正確性
​ 4. 訓練算法:使用咱們以前創建的 train_nb_0 方法
​ 5. 測試算法:使用 classify_nb 方法,而且構建一個新的測試函數來計算文檔集的錯誤率
​ 6. 使用算法:構建一個完整的程序對一組文檔進行分類,將錯分的文檔輸出到屏幕上

準備數據:切分文本

前面建立分類器的詞列表是直接給出的。
做爲程序猿的咱們應該本身切分文本獲得詞向量,所以咱們構造一個 text_parse 方法構造詞列表:

# bayes.py

def text_parse(my_text):
    import re

    # 使用正則切分句子,分隔符是除單詞、數字外的任意字符串
    # 加上[]是避免報警告:不能用零長度字符串切分
    reg_ex = re.compile('[\W*]')
    list_of_tokens = reg_ex.split(my_text)

    # 只返回長度大於0的單詞並把切分的單詞全小寫
    tok_list = [tok.lower() for tok in list_of_tokens if len(tok) > 0]

    return tok_list


def text_parse_run():
    import os

    email_filename = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data-set/email/ham/6.txt')
    with open(email_filename, 'r') as fr:
        email_text = fr.read()
        tok_list = text_parse(email_text)

        print(tok_list)


if __name__ == '__main__':
    # set_of_words2vec_run()
    # train_nb_0_run()
    # testing_nb_run()
    text_parse_run()

這只是一個很簡單的分詞方法,顯示業務必定不是這樣的。千萬別套用。

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

下面咱們把文本解析器集成到一個完整分類器中。
所以咱們構造 spam_test 方法

# bayes.py

def span_test():
    import os
    import numpy

    # 導入並解析文件
    doc_list = []
    class_list = []
    full_text = []
    email_filename = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data-set/email/{}/{}.txt')
    for i in range(1, 26):
        # 切割垃圾郵件文本並添加標記
        word_list = text_parse(open(email_filename.format('spam', i),errors='ignore').read())
        doc_list.append(word_list)
        full_text.extend(word_list)
        class_list.append(1)

        # 切割非垃圾郵件文本並添加標記
        word_list = text_parse(open(email_filename.format('ham', i),errors='ignore').read())
        doc_list.append(word_list)
        full_text.extend(word_list)
        class_list.append(0)

    # 構造詞彙表
    vocab_list = create_vocab_list(doc_list)

    # 從50封郵件中隨機取10封郵件當作測試集
    training_set = list(range(50))
    test_set = []
    for i in range(10):
        rand_index = int(numpy.random.uniform(0, len(training_set)))
        print(rand_index)
        test_set.append(training_set[rand_index])
        del (training_set[rand_index])

    # 建立詞彙向量
    train_mat = []
    train_classes = []
    for doc_index in training_set:
        train_mat.append(set_of_words2vec(vocab_list, doc_list[doc_index]))
        train_classes.append(class_list[doc_index])

    # 經過詞彙向量和向量標記獲得垃圾郵件和非垃圾郵件機率
    p0_v, p1_v, p_spam = train_nb_0(numpy.array(train_mat), numpy.array(train_classes))

    # 測試集測試分類器
    error_count = 0
    for doc_index in test_set:
        word_vector = set_of_words2vec(vocab_list, doc_list[doc_index])
        if classify_nb(numpy.array(word_vector), p0_v, p1_v, p_spam) != class_list[doc_index]:
            error_count += 1
    print('錯誤率:{}'.format(float(error_count) / len(test_set)))


if __name__ == '__main__':
    # set_of_words2vec_run()
    # train_nb_0_run()
    # testing_nb_run()
    # text_parse_run()
    span_test()

總結

對於分類而言,使用機率有時要比使用應規則更爲有效。貝葉斯機率及貝葉斯準則提供了一種利用已知值來估計未知機率的有效方法,也所以該分類決策存在必定的錯誤率。 因爲基礎高等數學《機率論》學得不紮實。剛開始看樸素貝葉斯也有點懵逼,因此代碼註釋較少,本篇博客寫的可能不是很好,可是今天抽空看了幾篇相關博客,下次有空會單獨寫一篇有關樸素貝葉斯的博客,有不懂的能夠私聊,這至關於學習筆記,不是最終稿,更不是我的心得改就懶得改了。

相關文章
相關標籤/搜索