机器学习 - 朴素贝叶斯
简介
朴素贝叶斯是一种基于概率进行分类的算法,跟之前的逻辑回归有些相似,两者都使用了概率和最大似然的思想。但与逻辑回归不同的是,朴素贝叶斯通过先验概率和似然概率计算样本在每个分类下的概率,并将其归为概率值最大的那个分类。朴素贝叶斯适用于文本分类、垃圾邮件处理等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聚类那样,可以通过画图对数据集和分类结果有比较直观的认识,但是只要理解了算法中最大似然的思想,以及利用贝叶斯公式计算似然概率的方法,就已经可以说是掌握了。