【机器学习】朴素贝叶斯分类器
源代码文件请点击此处!
条件概率的定义和公式
- 条件概率:事件 \(B\) 已发生条件下事件 \(A\) 发生的概率,记为 \(P(A|B)\),即
\[P(A|B) = \frac{P(AB)}{P(B)}
\]
- 乘法定理:
\[P(AB) = P(A) P(B|A)
\]
- 全概率公式:
\[\begin{aligned}
P(B) &= P(A_1)P(B|A_1) + P(A_2)P(B|A_2) + ... + P(A_n)P(B|A_n) \\
&= \sum_{i=1}^{n} P(A_i)P(B|A_i)
\end{aligned}
\]
- 贝叶斯(Bayes)公式(逆概率公式):
\[\begin{aligned}
P(A_i|B) &= \frac{P(A_iB)}{P(B)} \\
&= \frac{P(A_i)P(B|A_i)}{P(A_1)P(B|A_1) + P(A_2)P(B|A_2) + ... + P(A_n)P(B|A_n)} \\
&= \frac{P(A_i)P(B|A_i)}{\sum_{i=1}^{n} P(A_i)P(B|A_i)}
\end{aligned}
\]
- 独立概率的乘法规则:事件 \(A\) 和事件 \(B\) 必须相互独立,即一个事件的发生不会影响另一个事件的发生,此时有
\[P(AB) = P(A) P(B) \\
P(A|B) = P(A|\overline{B}) = P(A) \\
P(B|A) = P(B|\overline{A}) = P(B)
\]
- 一些小技巧:
- \(P(A) = P(A|B) + P(A|\overline{B})\)
- \(P(AB|C) = P(A|C) P(B|C)\)
先验概率和后验概率
定义:
- 先验概率:开始时,我们手中没有任何信息,只能计算一个初始概率。
- 事件:新的事件发生,有了新的信息。
- 后验概率:根据这些新信息和先验概率可以得到更好的概率估计。
例如,我们想得知今天下雨的概率:
- 先验概率:开始时,我们手中没有任何信息,只能粗略估计今天下雨的概率为 20%。
- 事件:我们在亚马逊雨林中。
- 后验概率:因此,今天下雨的概率被修正为 70%。
让我们再看以下例子。
【例】某种诊断癌症的试验具有如下效果:被诊断者有癌症,试验反应为阳性的概率为0.95;被诊断者没有癌症,试验反应为阴性的概率为 0.95。现对自然人群进行普查,设被试验的人群中患有癌症的概率为0.005,求:已知试验反应为阳性,该被诊断者确有癌症的概率。
【解】概率树如下:
graph LR
A[所有患者] --0.005--> B[患有癌症]
A[所有患者] --0.995--> C[没有癌症]
B[患有癌症] --0.95--> D[试验反应为阳性]
B[患有癌症] --0.05--> E[试验反应为阴性]
C[没有癌症] --0.05--> F[试验反应为阳性]
C[没有癌症] --0.95--> G[试验反应为阴性]
设 \(A\) 表示“患有癌症”,\(\overline{A}\) 表示“患有癌症”;\(B\) 表示“试验反应为阳性”,\(\overline{B}\) 表示“试验反应为阴性”。由题意得
\[P(A) =0.005, P(\overline{A}) = 0.995 \\
P(B|A) = 0.95, P(\overline{B}|\overline{A}) = 0.05 \\
P(B|\overline{A}) = 1 - P(\overline{B}|\overline{A}) = 0.95
\]
由贝叶斯公式得:
\[\begin{aligned}
P(A|B) &= \frac{P(AB)}{P(B)} \\
&= \frac{P(A)P(B|A)}{P(A)P(B|A) + P(\overline{A})P(B|\overline{A})} \\
&= 0.087
\end{aligned}
\]
通过以上例子可知:
- 先验概率:根据对自然人群的普查,被试验的人群中患有癌症的概率为0.005(同样,没有癌症的概率为0.995)。
- 事件:患者接受了试验,且结果为阳性。
- 后验概率:在得到试验反应为阳性后,该被诊断者确有癌症的概率被修正为 0.087。
使用朴素贝叶斯(Naive Bayes)算法检测垃圾邮件
设电子邮件有 \(n\) 个单词 \(x_1, x_2, ..., x_n\),则邮件是垃圾邮件的概率为:
\[P(垃圾邮件 | x_1, x_2, ..., x_n) = \frac{P(x_1, x_2, ..., x_n | 垃圾邮件) P(垃圾邮件)}{P(x_1, x_2, ..., x_n | 垃圾邮件) P(垃圾邮件) + P(x_1, x_2, ..., x_n | 非垃圾邮件) P(非垃圾邮件)}
\]
假设所有单词的出现都是独立的,则有:
\[P(x_1, x_2, ..., x_n | 垃圾邮件) = P(x_1 | 垃圾邮件) P(x_2 | 垃圾邮件) ... P(x_n | 垃圾邮件) = \prod_i^n P(x_i | 垃圾邮件) \\
P(x_1, x_2, ..., x_n | 非垃圾邮件) = P(x_1 | 非垃圾邮件) P(x_2 | 非垃圾邮件) ... P(x_n | 非垃圾邮件) = \prod_i^n P(x_i | 非垃圾邮件)
\]
根据条件概率公式有:
\[P(x_i | 垃圾邮件) = \frac{P(出现 x_i 的垃圾邮件)}{P(垃圾邮件)} = \frac{出现 x_i 的垃圾邮件数}{电子邮件总数} \cdot \frac{电子邮件总数}{垃圾邮件总数} = \frac{出现 x_i 的垃圾邮件数}{垃圾邮件总数} \\
同理,P(x_i | 非垃圾邮件) = \frac{出现 x_i 的非垃圾邮件数}{非垃圾邮件总数} \\
\]
所以:
\[P(垃圾邮件 | x_1, x_2, ..., x_n) = \frac{\frac{垃圾邮件总数}{电子邮件总数} \cdot \prod_i^n \frac{出现 x_i 的垃圾邮件数}{垃圾邮件总数} }{\frac{垃圾邮件总数}{电子邮件总数} \cdot \prod_i^n \frac{出现 x_i 的垃圾邮件数}{垃圾邮件总数} + \frac{垃圾邮件总数}{电子邮件总数} \cdot \prod_i^n \frac{出现 x_i 的非垃圾邮件数}{非垃圾邮件总数}}
\]
化简得:
\[P(垃圾邮件 | x_1, x_2, ..., x_n) = \frac{垃圾邮件总数 \cdot \prod_i^n \frac{出现 x_i 的垃圾邮件数}{垃圾邮件总数} }{垃圾邮件总数 \cdot \prod_i^n \frac{出现 x_i 的垃圾邮件数}{垃圾邮件总数} + 非垃圾邮件总数 \cdot \prod_i^n \frac{出现 x_i 的非垃圾邮件数}{非垃圾邮件总数}}
\]
代码如下:
import pandas as pd
import numpy as np
import pickle
import os
pkl_dir = os.path.dirname(os.path.abspath(__file__))
pkl_file = 'emails_words_count.pkl'
'''
该函数用于将邮件中所有单词变为小写,并将出现过的单词转换为列表
输出如下所示:左边为邮件文本,右边为出现过的单词的列表
text ... words
0 Subject: naturally irresistible your corporate... ... [love, are, 100, even, isguite, %, to, and, yo...
1 Subject: the stock trading gunslinger fanny i... ... [group, penultimate, tanzania, bedtime, edt, s...
2 Subject: unbelievable new homes made easy im ... ... [complete, loan, rate, this, subject:, 3, adva...
'''
def process_email(text):
text = text.lower()
return list(set(text.split()))
'''
【建立模型】记录每个单词分别在垃圾邮件和非垃圾邮件中的出现次数
输入:
- emails:电子邮件数据集
输出:
- model:二维数组,大小为 (每个邮件的不重复单词个数, 2)
- [word]['spam']:记录该单词出现在垃圾邮件中的个数
- [word]['ham']:记录该单词出现在非垃圾邮件中的个数
'''
def words_count(emails):
pkl_path = pkl_dir + '/' + pkl_file
if os.path.exists(pkl_path):
with open(pkl_path, 'rb') as f:
model = pickle.load(f)
return model
model = {} # 该字典记录了每个单词分别在垃圾邮件和非垃圾邮件中的出现次数
for index, email in emails.iterrows(): # 遍历所有的邮件
for word in email['words']: # 检测该邮件中的每一个单词
if word not in model: # 如果字典中没有这个单词
model[word] = {'spam': 1, 'ham': 1} # 初始化为 1,防止后面计算概率时除数为零
else: # 如果字典中已有这个单词
if email['spam'] == 1: # 如果该邮件是垃圾邮件
model[word]['spam'] += 1
else: # 如果该邮件是非垃圾邮件
model[word]['ham'] += 1
with open(pkl_path, 'wb') as f:
pickle.dump(model, f)
return model
'''
【推理预测】预测指定邮件是否为垃圾邮件
输入:
- email:待预测的电子邮件文本
- model:
- num_spam:数据集中垃圾邮件的总数
- num_ham:数据集中非垃圾邮件的总数
'''
def predict_naive_bayes(email, model, num_spam, num_ham):
email = email.lower() # 待预测邮件文本全部小写化
words = set(email.split()) # 生成单词列表
# 计算邮件的总数
# total = num_ham + num_spam
# 计算概率
spams, hams = [1.0], [1.0]
for word in words:
if word in model:
spams.append(model[word]['spam'] / num_spam)
hams.append(model[word]['ham'] / num_ham)
# np.prod():计算数组中所有元素的乘积
prod_spams = np.prod(spams) * num_spam
prod_hams = np.prod(hams) * num_ham
return prod_spams / (prod_hams + prod_spams)
# ============================= 主程序 main ===================================
if __name__ == '__main__':
emails = pd.read_csv('emails.csv')
emails['words'] = emails['text'].apply(process_email)
num_emails = len(emails)
num_spam = sum(emails['spam'])
num_ham = num_emails - num_spam
print("邮件总数:", num_emails)
print("垃圾邮件数:", num_spam)
print("邮件是垃圾邮件的先验概率:", num_spam / num_emails) # 计算先验概率
print()
# 记录每个单词分别在垃圾邮件和非垃圾邮件中的出现次数
model = words_count(emails)
# 出现单词 lottery 的邮件是垃圾邮件的概率?
sum = model['lottery']['spam'] + model['lottery']['ham']
print("单词 lottery 出现在邮件的次数:", model['lottery'])
print("出现单词 lottery 的邮件是垃圾邮件的概率:", model['lottery']['spam'] / sum)
# 出现单词 sale 的邮件是垃圾邮件的概率?
sum = model['sale']['spam'] + model['sale']['ham']
print("单词 sale 出现在邮件的次数:", model['sale'])
print("出现单词 sale 的邮件是垃圾邮件的概率:", model['sale']['spam'] / sum)
email1 = "Hi mom how are you"
email2 = "buy cheap lottery easy money now"
email3 = "meet me at the lobby of the hotel at nine am"
email4 = "asdfgh" # 不包含字典中任何一个单词,其为垃圾邮件的概率等于先验概率
email5 = "As the sun rose, I packed my bags with essentials like a camera, a water bottle, and a few snacks. Excited and ready for the adventure, I hopped into my car and set the GPS to Laojun Mountain. The drive was a pleasant one, with lush green fields and rolling hills passing by the window."
result = predict_naive_bayes(email5, model, num_spam, num_ham)
print(result)