簡介
朴素貝葉斯是一種基於概率進行分類的算法,跟之前的邏輯回歸有些相似,兩者都使用了概率和最大似然的思想。但與邏輯回歸不同的是,朴素貝葉斯通過先驗概率和似然概率計算樣本在每個分類下的概率,並將其歸為概率值最大的那個分類。朴素貝葉斯適用於文本分類、垃圾郵件處理等NLP下的多分類問題。
核心思想
每個樣本的分類由組成這個樣本的特征所決定,所以,如果能求出每個特征在每個分類下的概率,就能通過最大似然法求出這個樣本在每個分類下的概率,概率值最大的那個分類即是樣本的分類。用貝葉斯公式表示,即:
\( \begin{equation*} p(y_i|x) = \frac{p(y_i)*p(x|y_i)}{p(x)} \end{equation*} \)
其中:
\(\qquad p(y_i|x)\)表示樣本 \(x\) 是類別 \(y_i\) 的概率;
\(\qquad y_i \mbox{ 表示某一種類別, } y_i \in \{y|y_1, y_2, ..., y_n\}\),所以 \(p(y_i)\) 即先驗概率;
\(\qquad p(x|y_i)\) 是似然概率,需要先統計樣本特征 \(x_i\) 在 \(y_i\) 類別下的分布,再計算得出;
\(\qquad p(x)\) 是樣本 \(x\) 出現的概率,由於當前樣本是已知的,所以可以將 \(p(x)\) 視作常量,在比較概率大小時可忽略。
所以只要求出先驗概率和似然概率,即可預測出樣本 \(x\) 的分類。
一般步驟
1.數據預處理
需要注意的問題有:
(1)如遇中文單詞,需要將其編碼成數字或字符,以方便統計;
(2)將整個數據集分為訓練集和測試集,便於評估模型。通常使用留出法進行分割。
2.數據統計
統計樣本的分類情況以及樣本特征在每個分類下的分布情況。
(1)統計樣本的分類情況:即每個分類在整體中的占比個數;
(2)樣本特征在每個分類下的分布情況:對於不同的數據集,有着不同的定義方式,例如在文本分類中,可以將單詞是否在文章中出現過作為統計標准(不考慮重復出現的情況),也可以將單詞在文章中出現的次數作為統計標准。前者統計的粒度相對粗一些,后者的粒度相對細一些。
3.訓練模型
根據統計數據,計算先驗概率和似然概率,作為訓練的模型。
(1)先驗概率:可通過計算每個分類在整體的占比比例得出;
(2)似然概率:根據特征在每個分類下的分布情況,可計算出特征在每個分類下的概率。仍以文本分類為例,單詞出現過的文章篇數/當前分類下文章總篇數(單詞出現在該分類下的總次數/當前分類下的單詞總數)即每個特征在當前分類下的概率,再使用最大似然法,則可得到每個樣本在當前分類下的概率。
(3)對缺失特征的考慮。訓練集的大小有限,不可能包含所有特征,如果測試集或后來需要預測的數據包含模型中不存在的特征時,需要給這些不存在的特征一個較小的概率值。
4.模型評估
使用測試集預測分類,並與實際分類相比較,得出模型准確率。
在用測試集預測分類的過程中,需要計算似然概率,即所有單詞的概率相乘。但是考慮到一篇文章的單詞量很大,所有概率相乘的話會影響似然概率值的精度,不利於后續的比較,所以統一對似然概率取對數,轉換成概率對數的累加:
\( \begin{equation*} p(x|y_i) = \log{\prod \limits_{i=1}^{m}{p(x_i|y_i)}} = \sum_{i=1}^{m}\log{p(x_i|y_i)} \end{equation*} \)
下面就以一個文章分類的例子對朴素貝葉斯分類進行運用。
文本分類實例
現在有一個包含了上千篇文章的article數據集(提取碼:ivo8),已知文章共分為商業、汽車和體育三大類,試通過朴素貝葉斯建立預測模型。
1.數據預處理
首先,我們先來對數據有個初步的了解。打開文件目錄,發現共有3000多篇文章,文章的分類已經包含在文件名中了:
隨便打開一篇文章,發現內容是中文,意味着需要對其進行編碼,方便后續的統計。不過文章是已經分過詞的:
所以我們在這一階段需要做的工作有:
(1)提取文章對應的標簽;
(2)對中文單詞進行編碼;
(3)把所有文章分成訓練集和測試集兩類,並輸出到相應目錄
class NB:
def __init__(self):
self.article_path = 'articles' # 存放3000+文章的目錄
self.dataset_path = 'dataset' # 生成數據集的目錄
self.train_set = 'train.txt' # 訓練集文件名稱
self.test_set = 'test.txt' # 測試集文件名稱
self.train_threshold = 0.8 # 訓練集划分比例
def prepare_data(self):
if not os.path.exists(self.article_path):
print('文件目錄不存在,請重新指定!')
return
# 對中文單詞重新編碼,然后區分訓練集和測試集,輸出成文件
file_list = os.listdir(self.article_path)
# 如果目錄不存在,直接創建
if not os.path.exists(self.dataset_path):
os.system('mkdir ' + self.dataset_path)
train_f = open(os.path.join(self.dataset_path, self.train_set), 'w')
test_f = open(os.path.join(self.dataset_path, self.test_set), 'w')
# 對中文進行編碼
trans_dict = {}
code = 0
class_id = -1
if file_list:
for file_name in file_list:
# 獲取文章分類
if 'business' in file_name:
class_id = 0
elif 'auto' in file_name:
class_id = 1
elif 'sports' in file_name:
class_id = 2
# 每篇文章占一行,第一位存放文章分類
output_str = str(class_id) + ' '
file = os.path.join(self.article_path, file_name)
with open(file, 'r', encoding='utf-8') as f:
lines = f.readlines()
for line in lines:
for word in line.strip().split():
# 按照單詞出現順序進行編碼
if word not in trans_dict:
trans_dict[word] = code
code += 1
output_str += str(trans_dict[word]) + ' '
# 使用留出法區分訓練集和測試集
num = random.random()
if num < self.train_threshold:
output_f = train_f
else:
output_f = test_f
# 輸出編碼后的文章
output_f.write(output_str + '\n')
train_f.close()
test_f.close()
經過了第一步的處理,我們得到了經過編碼的訓練集和測試集。
2.數據統計
有了訓練集,我們就可以統計每個類別的數量以及單詞在每個分類中出現的次數。這里我們使用比較細的粒度進行統計,因為文章數量比較多,重復的詞肯定有不少,重復出現的次數在一定程度上也影響着樣本所屬的類別。
class NB:
def __init__(self):
... ...
def prepare_data(self):
... ...
def stat_data(self):
# 統計每個分類出現的次數
class_dict = {}
# 統計每個單詞在每個分類下的出現次數
class_word_dict = {}
with open(os.path.join(self.dataset_path, self.train_set), 'r') as f:
lines = f.readlines()
for line in lines:
word_list = line.strip().split()
# 樣本分類
class_id = word_list[0]
# 處理數據結構
if class_id not in class_dict:
class_dict[class_id] = 0
class_word_dict[class_id] = {}
# 統計分類個數
class_dict[class_id] += 1
# 統計每個單詞在每個分類下的出現次數
for word in word_list[1:]:
if word not in class_word_dict[class_id]:
class_word_dict[class_id][word] = 1
else:
class_word_dict[class_id][word] += 1
# 返回統計結果
return class_dict, class_word_dict
3.訓練模型
有了統計結果,我們就可以計算先驗概率以及單詞在每個類別中出現的概率,並輸出模型:
class NB:
def __init__(self):
... ...
self.model_path = 'model' # 生成預測模型文件的存放目錄
self.miss_word_prob = 0.1 # 缺失單詞的默認概率
def prepare_data(self):
... ...
def stat_data(self):
... ...
def train_model(self):
class_dict, class_word_dict = self.stat_data()
# 先驗概率p(y)
class_prob = {}
# 單詞在每個分類中出現的概率
class_word_prob = {}
# 默認概率,用於單詞不在模型中的情況
class_default_prob = {}
for class_id in class_dict:
# 當前分類下所有文章個數
class_sum = sum(class_dict.values())
class_prob[class_id] = class_dict[class_id] / class_sum
for class_id in class_word_dict:
# 當前分類下所有單詞出現次數的總和
class_word_sum = sum(class_word_dict[class_id].values())
if class_id not in class_word_prob:
class_word_prob[class_id] = {}
for word in class_word_dict[class_id]:
# 每個單詞在當前分類下出現的概率
class_word_prob[class_id][word] = class_word_dict[class_id][word] / class_word_sum
# 計算單詞缺失的情況下的概率
class_default_prob[class_id] = self.miss_word_prob / (1.0 + class_word_sum)
# 輸出model文件至目錄
if not os.path.exists(self.model_path):
os.system('mkdir ' + self.model_path)
self.json_file_dump(class_prob, self.model_path, 'class_prob.txt')
self.json_file_dump(class_word_prob, self.model_path, 'class_word_prob.txt')
self.json_file_dump(class_default_prob, self.model_path, 'class_default_prob.txt')
@staticmethod
def json_file_dump(dump_file, dump_file_path, dump_filename):
# 導出模型到目錄
dump_file_f = open(os.path.join(dump_file_path, dump_filename), 'w')
json.dump(dump_file, dump_file_f)
dump_file_f.close()
這里我們補充了兩個初始化參數,self.model_path和self.miss_word_prob,分別用來存放模型文件以及計算缺失單詞的概率,我們用self.miss_word_prob再除以每個分類的單詞總數,當做每個分類下缺失單詞的出現概率。
4.模型評估
最后我們通過測試集,對模型的預測性能進行評估。
class NB:
def __init__(self):
... ...
def prepare_data(self):
... ...
def stat_data(self):
... ...
def train_model(self):
... ...
@staticmethod
def json_file_load(file_path, filename):
# 加載模型文件
file_f = open(os.path.join(file_path, filename), 'r')
file_dict = json.load(file_f)
file_f.close()
return file_dict
def predict(self):
# 存儲真實值和預測值
real_list = []
pre_list = []
# 讀取模型文件
class_prob = self.json_file_load(self.model_path, 'class_prob.txt')
class_word_prob = self.json_file_load(self.model_path, 'class_word_prob.txt')
class_default_prob = self.json_file_load(self.model_path, 'class_default_prob.txt')
# 使用測試集計算p(y|x)預測class_id
# 朴素貝葉斯公式:
# p(y|x) = p(x|y)*p(y) / p(x)
with open(os.path.join(self.dataset_path, self.test_set), 'r') as f:
lines = f.readlines()
for line in lines:
# p(y|x)
article_class_prob = {}
word_list = line.strip().split()
# 真實分類
class_id = word_list[0]
real_list.append(class_id)
# 先驗概率p(y)取對數
for class_id in class_prob:
article_class_prob[class_id] = math.log(class_prob[class_id])
# 似然概率p(x|y)
for word in word_list[1:]:
for class_id in class_word_prob:
if word not in class_word_prob[class_id]:
article_class_prob[class_id] += math.log(class_default_prob[class_id])
else:
article_class_prob[class_id] += math.log(class_word_prob[class_id][word])
# 取概率最大的為預測值
max_prob = max(article_class_prob.values())
for class_id in article_class_prob:
if article_class_prob[class_id] == max_prob:
pre_list.append(class_id)
# 計算准確率
accurate = 0
for i in range(len(real_list)):
if real_list[i] == pre_list[i]:
accurate += 1
accurate_rate = round(accurate / len(real_list) * 100, 2)
return '模型預測准確率為:' + str(accurate_rate) + '%'
最后,跑一次程序,可以得到以下結果:
# 初始化
nb = NB()
# 數據預處理
nb.prepare_data()
# 訓練
nb.train_model()
# 預測
accurate_rate = nb.predict()
print(accurate_rate)
# 輸出結果
模型預測准確率為:97.08%
總結
朴素貝葉斯算法由於簡單、易於理解,經常用於文本分類等場景中。但由於它是基於“樣本特征之間是相互獨立的”這一假設,所以不適合一些基於上下文進行判斷的任務。
該算法不像之前的線性回歸和k-means聚類那樣,可以通過畫圖對數據集和分類結果有比較直觀的認識,但是只要理解了算法中最大似然的思想,以及利用貝葉斯公式計算似然概率的方法,就已經可以說是掌握了。