樸素貝葉斯概述
樸素貝葉斯(Naive Bayes)是一種基於概率統計的分類方法,在文本處理領域有着廣泛的應用
“樸素” — 條件獨立假設,即事件之間沒有關聯關係
何解?比如,投擲一個骰子兩次,第1次和第2次出現的數字是獨立的、不相關的,那麼這兩個事件則是條件獨立
貝葉斯定理:
事件發生的概率(先驗概率)
事件發生的概率(邊際似然度)
事件發生的情況下,事件發生的概率(後驗概率)
事件發生的情況下,事件發生的概率(似然度)
來個例子加深理解
使用一個劣質的呼氣檢測儀檢測酒駕,5%的概率會把一個正常人判定爲酒駕,真正酒駕的預測率卻爲100%,根據以往數據統計,大概有0.1%的司機爲醉駕
那麼,一個被呼氣檢測儀判定爲酒駕的,真的是酒駕的概率是多少?
假設有1000人,那麼1個真酒駕,999x5%個誤判爲酒駕,真正酒駕概率爲:
下面用樸素貝葉斯定理來求解:
真正酒駕
顯示酒駕
真酒駕顯示爲酒駕
顯示酒駕爲真酒駕(待求)
樸素貝葉斯在機器學習裏面的應用:
假設有一個數據集 ,其中 ,,即 是一個向量,存在 個輸入特徵,這個數據集共有 個類別
現在針對一個輸入向量,預測的值,即,預測的分類
用統計學語言描述就是:當觀察到輸入樣本爲 時 ,其所屬的類別 的概率,其中,那麼現在需要求出所有 個類別的概率,其中最大的概率 就是 所屬的類別,使用條件概率公式表示爲:
用樸素貝葉斯定理進行變換:
對於一個固定的數據集, 都是固定的值,那麼:
根據聯合概率(兩個獨立事件同時發生),,可得:
其中 是有一個有 個特徵的向量,那麼:
根據概率鏈式法則,可得:
現在回過頭來思考 樸素 二字,條件獨立假設的兩個事件之間,是不相關的,換句話說,上述公式的 之間互不相關,如下所示:
那麼上面展開的公式可以改寫爲:
用連乘符號表示上面公式,那麼,最終結果是:
其中, 表示每種類別出現的概率, 表示爲當類別爲 時,特徵 出現的概率,這兩個值均可從數據集中統計得出
下面用另一種方法來理解:
因爲 之間互不相關,那麼, 可以直接寫成 ,因此:
樸素貝葉斯實戰:文檔分類
下面來用Python來實現一個樸素貝葉斯算法,來對20個不同主題的新聞組數據集進行分類,數據集下載地址爲:20 Newsgroups
那麼對於一個文檔,如何着手第一步呢?先看公式:
用語言描述就是,當輸入爲 時,它爲 的概率,就是我們要求的,假如現在有兩類,求出來有 和 ,如果 ,那麼這個 屬於 類,反之,則屬於 類
爲每種類在全部文檔中的概率,假如現在有10篇文檔, 類有4篇, 類有6篇,那麼
表示當類別爲 時, 出現的概率,換句話說,就是 即某個詞在 類所有文檔的詞總和中出現的概率,當然這裏有個連乘符號 ,意思就是,我們需要將某個文檔中所有的詞,在 類所有文檔的詞總和中出現的概率進行連乘,比如, 類所有文檔共100個詞, 這個詞在 類所有文檔中共出現了20次,處於詞向量的第1個位置,那麼 , 這個詞在 類所有文檔中共出現了40次,處於詞向量的第2個位置,那麼 ,假設這篇文檔只有這兩個詞,那麼 :
最後來講 ,它的含義是所有文檔中的詞,在所有文檔的詞總和中出現的概率,對於一個給定的數據集,這個值是固定不變的,那麼,其實在計算時,就不需要計算這個了
總的來說,我們只需要計算 就夠了
下面用代碼實現這個算法,第一步,讀取文件,將讀取出來的每個文本內容切割成單個詞組成的列表,然後將文檔詞列表和所屬標籤分別追加進空列表
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
文本解析切割代碼如下,一般文本里會有一些沒有太大意義的詞,比如定冠詞 ,介詞 之類的,這裏做了三個處理: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, 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_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
有了 和 ,我們就能計算 了,從中找出概率最大的值,其相對應的標籤,就是預測文檔的類別了,下面的 input_vec 和 p_vec 相乘,意思是將輸入詞向量所代表的詞提取出來,input_vec 是一個由 0 和 1 構成的向量,0代表不含詞彙,1代表含有詞彙,相乘的結果就是提取出了當前詞向量所有單個詞的概率向量了,不過由於大量很小的數字連乘,程序會下溢出或者得到不正確的答案,對於這種下溢出的零概率事件,我們對其做一次 運算就可以了,我們把這種做法叫做拉普拉斯平滑:
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()