機器學習-分類-樸素貝葉斯算法

樸素貝葉斯概述

樸素貝葉斯(Naive Bayes)是一種基於概率統計的分類方法,在文本處理領域有着廣泛的應用

“樸素” — 條件獨立假設,即事件之間沒有關聯關係

何解?比如,投擲一個骰子兩次,第1次和第2次出現的數字是獨立的、不相關的,那麼這兩個事件則是條件獨立

貝葉斯定理:
P(AB)=P(A)P(BA)P(B) P(A|B)=\frac{P(A)P(B|A)}{P(B)}

P(A)AP(A):A事件發生的概率(先驗概率)
P(B)BP(B):B事件發生的概率(邊際似然度)
P(AB)BP(A|B):B事件發生的情況下,AA事件發生的概率(後驗概率)
P(BA)AP(B|A):A事件發生的情況下,BB事件發生的概率(似然度)

來個例子加深理解

使用一個劣質的呼氣檢測儀檢測酒駕,5%的概率會把一個正常人判定爲酒駕,真正酒駕的預測率卻爲100%,根據以往數據統計,大概有0.1%的司機爲醉駕

那麼,一個被呼氣檢測儀判定爲酒駕的,真的是酒駕的概率是多少?

假設有1000人,那麼1個真酒駕,999x5%個誤判爲酒駕,真正酒駕概率爲:
11+999×5%=1.96% \frac{1}{1+999\times 5\%}=1.96\%
下面用樸素貝葉斯定理來求解:
P(A)P(A):真正酒駕
P(B)P(B):顯示酒駕
P(BA)P(B|A):真酒駕顯示爲酒駕
P(AB)P(A|B):顯示酒駕爲真酒駕(待求)

P(A)=0.1%P(A)=0.1\%
P(B)=0.1%×100%+(10.1%)×5%P(B)=0.1\% \times100\% + (1-0.1\%)\times 5\%
P(BA)=100%P(B|A) = 100\%

P(AB)=P(A)P(BA)P(B)=0.1%×100%0.1%×100%+(10.1%)×5%=1.96%P(A|B)=\frac{P(A)P(B|A)}{P(B)}=\frac{0.1\%\times 100\%}{0.1\% \times100\% +(1-0.1\%)\times 5\%}=1.96\%

樸素貝葉斯在機器學習裏面的應用:

假設有一個數據集 [x(i),y(i)][x^{(i)}, y^{(i)}],其中 x(i)=[x1,x2,...,xn]x^{(i)}=[x_1, x_2, ..., x_n]y(i)[C1,C2,...,Cb]y^{(i)}∈[C_1, C_2, ..., C_b],即 x(i)x^{(i)} 是一個向量,存在 nn 個輸入特徵,這個數據集共有 bb 個類別

現在針對一個輸入向量x(i)x^{(i)},預測y(i)y^{(i)}的值,即,預測x(i)x^{(i)}的分類

用統計學語言描述就是:當觀察到輸入樣本爲 xx 時 ,其所屬的類別 y=Cky=C_k 的概率,其中Ck[C1,C2,...,Cb]C_k∈[C_1, C_2, ..., C_b],那麼現在需要求出所有 bb 個類別的概率,其中最大的概率 CkC_k 就是 xx 所屬的類別,使用條件概率公式表示爲:
P(Ckx)P(C_k|x)

用樸素貝葉斯定理進行變換:
P(Ckx)=P(Ck)P(xCk)P(x)P(C_k|x)=\frac{P(C_k)P(x|C_k)}{P(x)}

對於一個固定的數據集,CkP(x)C_k、P(x) 都是固定的值,那麼:
P(Ckx)P(Ck)P(xCk)P(C_k|x)∝P(C_k)P(x|C_k)

根據聯合概率(兩個獨立事件同時發生),P(A,B)=P(AB)P(B)P(A, B)=P(A|B)P(B),可得:
P(Ck)P(xCk)=P(x,Ck)P(C_k)P(x|C_k)=P(x, C_k)

其中 xx 是有一個有 nn 個特徵的向量,那麼:
P(x,Ck)=P(x1,x2,...xn,Ck)P(x, C_k)=P(x_1, x_2,...x_n, C_k)

根據概率鏈式法則,可得:
P(x1,x2,...xn,Ck)=P(x1x2,...xn,Ck)P(x2,...xn,Ck)=P(x1x2,...xn,Ck)P(x2x3,...xn,Ck)P(x3,...,xn,Ck)=P(x1x2,...xn,Ck)P(x2x3,...xn,Ck)P(x3x4,...,xn,Ck)...P(xnCk)P(Ck)\begin{aligned}P(x_1, x_2,...x_n, C_k) &=P(x_1|x_2, ... x_n, C_k)P(x_2, ... x_n, C_k) \\ &=P(x_1|x_2, ... x_n, C_k)P(x_2|x_3, ... x_n, C_k)P(x_3, ..., x_n, C_k) \\ &=P(x_1|x_2, ... x_n, C_k)P(x_2|x_3, ... x_n, C_k)P(x_3|x_4, ..., x_n, C_k) ... P(x_n|C_k)P(C_k) \end{aligned}

現在回過頭來思考 樸素 二字,條件獨立假設的兩個事件之間,是不相關的,換句話說,上述公式的 x1x2x3...xnx_1、x_2、x_3 ... x_n 之間互不相關,如下所示:
P(x1x2,...xn,Ck)=P(x1Ck) P(x_1|x_2, ... x_n, C_k)=P(x_1|C_k)

那麼上面展開的公式可以改寫爲:

P(x1,x2,...xn,Ck)=P(x1Ck)P(x2Ck)P(x3Ck),...,P(xnCk)P(Ck) P(x_1, x_2,...x_n, C_k)=P(x_1|C_k)P(x_2|C_k)P(x_3|C_k), ..., P(x_n|C_k)P(C_k)

用連乘符號\prod表示上面公式,那麼,最終結果是:

P(Ckx)=P(Ck)ni=1P(xiCk)P(x) P(C_k|x)=\frac{P(C_k)\prod_{n}^{i=1}P(x_i|C_k) }{P(x)}

其中,P(Ck)P(C_k) 表示每種類別出現的概率,P(xiCk)P(x_i|C_k) 表示爲當類別爲 CkC_k時,特徵 xix_i 出現的概率,這兩個值均可從數據集中統計得出

下面用另一種方法來理解:

P(Ckx)=P(Ck)P(xCk)P(x)=P(Ck)P(x1,x2,x3,...,xnCk)P(x)\begin{aligned} P(C_k|x) &=\frac{P(C_k)P(x|C_k)}{P(x)} \\ &=\frac{P(C_k)P(x_1, x_2, x_3, ..., x_n|C_k)}{P(x)} \end{aligned}

因爲 x1x2x3...,xnx_1、x_2、x_3 ..., x_n 之間互不相關,那麼,P(x1,x2,x3,...,xnCk)P(x_1, x_2, x_3, ..., x_n|C_k) 可以直接寫成 P(x1Ck)P(x2Ck)P(x3Ck),...,P(xnCk)P(x_1|C_k)P(x_2|C_k)P(x_3|C_k), ..., P(x_n|C_k),因此:

P(Ckx)=P(Ck)P(xCk)P(x)=P(Ck)P(x1,x2,x3,...,xnCk)P(x)=P(Ck)P(x1Ck)P(x2Ck)P(x3Ck),...,P(xnCk)P(x)=P(Ckx)=P(Ck)ni=1P(xiCk)P(x)\begin{aligned} P(C_k|x) &=\frac{P(C_k)P(x|C_k)}{P(x)} \\ &=\frac{P(C_k)P(x_1, x_2, x_3, ..., x_n|C_k)}{P(x)} \\ &=\frac{P(C_k)P(x_1|C_k)P(x_2|C_k)P(x_3|C_k), ..., P(x_n|C_k)}{P(x)} \\ &=P(C_k|x)=\frac{P(C_k)\prod_{n}^{i=1}P(x_i|C_k) }{P(x)} \end{aligned}

樸素貝葉斯實戰:文檔分類

下面來用Python來實現一個樸素貝葉斯算法,來對20個不同主題的新聞組數據集進行分類,數據集下載地址爲:20 Newsgroups

那麼對於一個文檔,如何着手第一步呢?先看公式:
P(Ckx)=P(Ck)ni=1P(xiCk)P(x) P(C_k|x)=\frac{P(C_k)\prod_{n}^{i=1}P(x_i|C_k) }{P(x)}
P(Ckx)P(C_k|x) 用語言描述就是,當輸入爲 xx 時,它爲 CkC_k 的概率,就是我們要求的,假如現在有兩類,求出來有 C1C_1C2C_2,如果 C1>C2C_1 > C_2,那麼這個 xx 屬於 C1C_1 類,反之,則屬於 C2C_2

P(Ck)P(C_k) 爲每種類在全部文檔中的概率,假如現在有10篇文檔,C1C_1 類有4篇,C2C_2 類有6篇,那麼 P(C1)=4/10=0.4P(C2)=6/10=0.6P(C_1)=4/10=0.4,P(C_2)=6/10=0.6

P(xiCk)P(x_i|C_k) 表示當類別爲 CkC_k 時,xix_i 出現的概率,換句話說,就是 xix_i 即某個詞在 CkC_k 類所有文檔的詞總和中出現的概率,當然這裏有個連乘符號 \prod,意思就是,我們需要將某個文檔中所有的詞,在 CkC_k 類所有文檔的詞總和中出現的概率進行連乘,比如,C1C_1 類所有文檔共100個詞,carcar 這個詞在 C1C_1 類所有文檔中共出現了20次,處於詞向量的第1個位置,那麼 P(x1)=20/100=0.2P(x_1)=20/100=0.2oiloil 這個詞在 C1C_1 類所有文檔中共出現了40次,處於詞向量的第2個位置,那麼 P(x2)=40/100=0.4P(x_2)=40/100=0.4,假設這篇文檔只有這兩個詞,那麼 :ni=1P(xiCk)=P(x1C1)×P(x2C1)=0.2×0.4=0.08\prod_{n}^{i=1}P(x_i|C_k)=P(x_1|C_1) \times P(x_2|C_1)=0.2\times 0.4=0.08

最後來講 P(x)P(x),它的含義是所有文檔中的詞,在所有文檔的詞總和中出現的概率,對於一個給定的數據集,這個值是固定不變的,那麼,其實在計算時,就不需要計算這個P(x)P(x)

總的來說,我們只需要計算 P(Ck)ni=1P(xiCk)P(C_k)\prod_{n}^{i=1}P(x_i|C_k) 就夠了

下面用代碼實現這個算法,第一步,讀取文件,將讀取出來的每個文本內容切割成單個詞組成的列表,然後將文檔詞列表和所屬標籤分別追加進空列表

def load_files(file_path, file_num):
    label_list = os.listdir(file_path)
    words_list, class_list = [], []
    for label in label_list:
        file_list = os.listdir(file_path + label)
        for file_name in file_list[:file_num]:
            words = text_parse(open(file_path + label + '/' + file_name, 'rb').read())
            words_list.append(words)
            class_list.append(label)
    return words_list, class_list

文本解析切割代碼如下,一般文本里會有一些沒有太大意義的詞,比如定冠詞 thethe,介詞 inonatin、on、at 之類的,這裏做了三個處理:1.將不是字母數字的符號替換成空格,2.過濾無意義的停止詞,3.將字母全部統一成小寫

def text_parse(big_string):
    big_string = big_string.decode('latin-1')  # 可以解碼任意文件
    sub_string = re.sub('[^a-zA-Z0-9]', ' ', big_string)
    list_of_tokens = sub_string.split()
    stop_words = stopwords.words('english')
    return [tok.lower() for tok in list_of_tokens if tok.lower() not in stop_words]

下面的代碼是整合所有訓練文本中的詞,返回一個不重複的詞彙列表

def create_vocab_list(words_list):
    vocab_set = set()
    for doc in words_list:
        vocab_set = vocab_set | set(doc)  # 獲取兩個集合的並集
    return list(vocab_set)  # 詞彙集合

有了詞彙列表後,我們就能根據這個列表將一個文檔詞列表轉化爲詞向量了,舉個例子,比如詞彙列表爲 [a,b,c,d,e][a, b, c, d, e],某個文檔詞列表爲 [a, c, e],那麼我們就用 [1, 0, 1, 0, 1] 來表示這個文檔

def create_dataset(vocab_list, words_list):
    vocab_index = {vocab: vocab_list.index(vocab) for vocab in vocab_list}
    matrix = []
    num, total = 1, len(words_list)
    for words in words_list:
        matrix.append(word_bag_2_vec(vocab_list, words, vocab_index))  # 對每封郵件基於詞彙表,構建詞向量,追加進列表
        print('%s/%s' % (num, total))
        num += 1
    return matrix

下面的代碼是對一個文檔詞列表進行了詞向量化

def word_bag_2_vec(vocab_list, words, vocab_index):  # 詞袋模型
    return_vec = [0] * len(vocab_list)
    words_dict = Counter(words)
    for word, count in words_dict.items():
        if word in vocab_list:
            return_vec[vocab_index[word]] = count
    return return_vec

接下來,就可以計算各個類別中,各個詞佔當前分類詞總和的概率了,這裏將計算結果存儲在了一個字典 p_d 中,該字典的 key 是各類文檔分類名 label,各個 label 鍵對應的值也是字典,用於存儲分類信息,分類信息字典中 p 鍵的值爲當前類別文檔數在全部訓練文檔數中出現的概率,即 P(Ck)P(C_k),p_vec 鍵儲存的值爲上面計算公式中的 P(xCk)P(x|C_k),爲一個矩陣,代碼如下:

def train_nb(train_matrix, train_labels):
    num_train_docs = len(train_matrix)  # 文檔數量
    num_words = len(train_matrix[0])  # 全部文檔的字數
    target_dict = Counter(train_labels)
    p_d = {}
    for label, count in target_dict.items():
        p_d[label] = {'p': count / float(num_train_docs)}  # 當前類別佔全部文檔的比例
    for i in range(num_train_docs):
        p_d[train_labels[i]]['p_count'] = p_d[train_labels[i]].get('p_count', np.ones(num_words)) + train_matrix[i]
        p_d[train_labels[i]]['p_denom'] = p_d[train_labels[i]].get('p_denom', 2) + sum(train_matrix[i])
    for label, d in p_d.items():
        p_d[label]['p_vec'] = d['p_count'] / d['p_denom']
    return p_d

有了 P(Ck)P(C_k)P(xCk)P(x|C_k),我們就能計算 P(Ckx)P(C_k|x) 了,從中找出概率最大的值,其相對應的標籤,就是預測文檔的類別了,下面的 input_vec 和 p_vec 相乘,意思是將輸入詞向量所代表的詞提取出來,input_vec 是一個由 0 和 1 構成的向量,0代表不含詞彙,1代表含有詞彙,相乘的結果就是提取出了當前詞向量所有單個詞的概率向量了,不過由於大量很小的數字連乘,程序會下溢出或者得到不正確的答案,對於這種下溢出的零概率事件,我們對其做一次 loglog 運算就可以了,我們把這種做法叫做拉普拉斯平滑:
P(Ck)P(xCk)=P(Ck)P(x1Ck)P(x2C2)...P(xnCk) P(C_k)P(x|C_k)=P(C_k)P(x_1|C_k)P(x_2|C_2)...P(x_n|C_k)

log(P(Ck)P(xCk))=log(P(Ck)P(x1Ck)P(x2Ck)...P(xnCk))=log(P(Ck))+log(P(x1Cl))+log(P(x2Ck))+...+log(P(xnCk))=log(P(Ck))+sum(log(P(xCk)))\begin{aligned} log(P(C_k)P(x|C_k)) &= log(P(C_k)P(x_1|C_k)P(x_2|C_k)...P(x_n|C_k)) \\ &=log(P(C_k))+log(P(x_1|C_l))+log(P(x_2|C_k))+...+ log(P(x_n|C_k)) \\ &=log(P(C_k))+sum(log(P(x|C_k))) \end{aligned}

def classify_nb(input_vec, p_d):
    p_l = []
    for label, d in p_d.items():
        # pi = sum(input_vec * d['p_vec']) + d['p']
        pi = sum(input_vec * np.log(d['p_vec'])) + np.log(d['p'])
        p_l.append((label, pi))
    sorted_label_p = sorted(p_l, key=operator.itemgetter(1), reverse=True)
    return sorted_label_p[0][0]

完整代碼如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from collections import Counter
from nltk.corpus import stopwords
import numpy as np
import operator
import numba
import re
import os


def create_vocab_list(words_list):
    vocab_set = set()
    for doc in words_list:
        vocab_set = vocab_set | set(doc)  # 獲取兩個集合的並集
    return list(vocab_set)  # 詞彙集合


@numba.jit
def word_bag_2_vec(vocab_list, words, vocab_index):  # 詞袋模型
    return_vec = [0] * len(vocab_list)
    words_dict = Counter(words)
    for word, count in words_dict.items():
        if word in vocab_list:
            return_vec[vocab_index[word]] = count
    return return_vec


def train_nb(train_matrix, train_labels):
    num_train_docs = len(train_matrix)  # 文檔數量
    num_words = len(train_matrix[0])  # 全部文檔的字數
    target_dict = Counter(train_labels)
    p_d = {}
    for label, count in target_dict.items():
        p_d[label] = {'p': count / float(num_train_docs)}  # 當前類別佔全部文檔的比例
    for i in range(num_train_docs):
        p_d[train_labels[i]]['p_count'] = p_d[train_labels[i]].get('p_count', np.ones(num_words)) + train_matrix[i]
        p_d[train_labels[i]]['p_denom'] = p_d[train_labels[i]].get('p_denom', 2) + sum(train_matrix[i])
    for label, d in p_d.items():
        p_d[label]['p_vec'] = d['p_count'] / d['p_denom']
    return p_d


def classify_nb(input_vec, p_d):
    p_l = []
    for label, d in p_d.items():
        # pi = sum(input_vec * d['p_vec']) + d['p']  # 大量很小的數字連乘,程序會下溢出或者得到不正確的答案
        pi = sum(input_vec * np.log(d['p_vec'])) + np.log(d['p'])  # 對每個概率值做自然對數log處理
        p_l.append((label, pi))
    sorted_label_p = sorted(p_l, key=operator.itemgetter(1), reverse=True)
    return sorted_label_p[0][0]


def text_parse(big_string):
    big_string = big_string.decode('latin-1')  # 可以解碼任意文件
    sub_string = re.sub('[^a-zA-Z0-9]', ' ', big_string)
    list_of_tokens = sub_string.split()
    stop_words = stopwords.words('english')
    return [tok.lower() for tok in list_of_tokens if tok.lower() not in stop_words]


def load_files(file_path):
    label_list = os.listdir(file_path)
    words_list, class_list = [], []
    for label in label_list:
        file_list = os.listdir(file_path + label)
        for file_name in file_list:
            words = text_parse(open(file_path + label + '/' + file_name, 'rb').read())
            words_list.append(words)
            class_list.append(label)
    return words_list, class_list


def create_dataset(vocab_list, words_list):
    vocab_index = {vocab: vocab_list.index(vocab) for vocab in vocab_list}
    matrix = []
    num, total = 1, len(words_list)
    for words in words_list:
        matrix.append(word_bag_2_vec(vocab_list, words, vocab_index))  # 對每封郵件基於詞彙表,構建詞向量,追加進列表
        print('%s/%s' % (num, total))
        num += 1
    return matrix


def doc_classify():
    train_words_list, train_labels = load_files('20news/train/')
    test_words_list, test_labels = load_files('20news/test/')
    vocab_list = create_vocab_list(train_words_list)
    train_matrix = create_dataset(vocab_list, train_words_list)
    test_matrix = create_dataset(vocab_list, test_words_list)
    p_d = train_nb(train_matrix, train_labels)
    error_count = 0
    for test_vec, test_label in zip(test_matrix, test_labels):
        pred_label = classify_nb(test_vec, p_d)
        print("%s ==> %s" % (pred_label, test_label))
        if pred_label != test_label:  # 比較分類結果和真實分類
            error_count += 1
            print("===================prediction error====================")
    print('the error rate is:', float(error_count) / len(test_labels))  # 0.13757082152974504


if __name__ == '__main__':
    doc_classify()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章