1 引言

各位朋友大家好,欢迎来到月来客栈,我是掌柜空字符。

前面的文章中掌柜虽然已经详细地介绍了朴素贝叶斯算法的基本思想与原理,但对于具体的实现细节掌柜并没有过多的介绍。在这篇文章中,掌柜将会从零开始一步一步地来介绍如何实现朴素贝叶斯(估计)模型,以便让大家对于朴素贝叶斯算法有着更加清晰的认识。

2 朴素贝叶斯原理

在正式介绍朴素贝叶斯的代码实现之前,掌柜先带着大家简单地来回顾一下算法求解步骤与计算过程;然后再根据计算过程的顺序来一步一步实现整个模型。对于朴素贝叶斯的详细原理可以参加文章朴素贝叶斯算法

2.1 求解步骤

根据前面文章的介绍,可以将朴素贝叶斯分类算法的求解步骤总结如下:

输入:训练数据T={(x1,y1),(x2,y2),...,(xm,ym)},其中xi=(xi(1),xi(2),...,xi(n))Txi(j)是第i个样本的第j维特征,xi(j){aj1,aj2,...,ajSj}ajl是第j维特征可能取得的第l个值。同时,j=1,2,...,nl=1,2,...,Sjyi{c1,c2,...,cK};以及实例x;

输出:实例x的分类[1]。

1) 计算先验概率与条件概率

根据极大似然估计,用给定的数据集来计算各类别的先验概率和条件概率。

P(Y=ck)=i=1mI(yi=ck)m,k=1,2,...,K(1)
P(X(j)=ajl|Y=ck)=i=1mI(xi(j)=ajl,yi=ck)i=1mI(yi=ck)j=1,2,...,n;  l=1,2,...,Sj;  k=1,2,...,K(2)

其中I()指示函数,当yi=ck时输出值为1,反之则为0。

2) 计算各特征取值下的后验概率

P(Y=ck)=j=1nP(X(j)=x(j)|Y=ck),k=1,2,...,K(3)

3) 极大化后验概率确定类别

y=argmaxckP(Y=ck)j=1nP(X(j)=x(j)|Y=ck)(4)

最后,只需要选择对应后验概率值最大的类别作为该样本的预测类别即可。

2.2 计算示例

在简单介绍完朴素贝叶斯的计算过程后,下面再来通过一个实际的计算示例来体会一下朴素贝叶斯分类器的计算流程。

如表1所示为一个信用卡审批数据集,其中X(1)A1={0,1}表示有无工作,X(2)A2={0,1}表示是否有房,X(3)A3={D,S,T}表示学历等级,YC={0,1}表示是否审批通过的类标记。试由表6-1中的训练集学习一个朴素贝叶斯分类器,并确定x=(0,1,D)的类标记Y

表 1. 示例计算数据

根据式(1),由表1易知,各个类别的先验概率为

P(Y=0)=515,  P(Y=1)=1015(5)

各类别下不同特征取值的条件概率为

P(X(1)=0|Y=0)=45,P(X(1)=1|Y=0)=15P(X(2)=0|Y=0)=45,P(X(2)=1|Y=0)=15P(X(3)=D|Y=0)=15,P(X(3)=S|Y=0)=15P(X(3)=T|Y=0)=35,P(X(1)=0|Y=1)=310P(X(1)=1|Y=1)=710,P(X(2)=0|Y=1)=410P(X(2)=1|Y=1)=610,P(X(3)=D|Y=1)=210P(X(3)=S|Y=1)=310,P(X(3)=T|Y=1)=510(6)

以上计算过程便是根据训练集训练朴素贝叶斯分类器模型参数的过程。根据这些参数,便可以对给定的x=(0,1,D)进行预测。

首先根据式(4)分别计算出其属于各个类别的后验概率为

P(Y=0|X=x)=P(Y=0)P(X(1)=0|Y=0)P(X(2)=1|Y=0)P(X(3)=D|Y=0)=515451515=4375(7)
P(Y=1|X=x)=P(Y=1)P(X(1)=0|Y=1)P(X(2)=1|Y=1)P(X(3)=D|Y=1)=1015310610210=3125(8)

于是便可以知道,样本x=(0,1,D)属于y=1的可能性最大。

3 朴素贝叶斯实现

在回顾完朴素贝叶斯的相关基本原理后,接下来掌柜就开始分步对各个部分的实现进行介绍。同时,需要说明的是以下实现代码均参考自sklearn 0.24.0 中的CategoricalNB模块,只是对部分处理逻辑进行了修改与简化。

3.1 特征计数实现

通过第2节的内容可知,不管是计算先验概率还是条件概率,在这之前都需要先统计训练集中各个样本及样本特征取值的分布情况。因此,这里首先需要初始化相关的计数器;然后再对样本和特征样本特征取值的分布情况进行统计。

如下代码所示便是对两个重要计数器初始化工作:

在上述代码中,第3-4行是初始化平滑项系数alpha。第7行class_count_被初始化成了一个形状为[n_classes,]的全零向量,其中n_classes表示分类的类别数量,而每个维度分别表示每个类别的样本数量(例如[2,2,3]表示0,1,2这三个类别的样本数分别是2,2,3),其目的是后续用于计算每个类别的先验概率。第8行category_count_被初始化成了一个包含有n_features_个元素的列表,其中n_features_表示数据集的特征维度数量,同时category_count_中每个元素的形状是[n_classes,0](后续每个元素将会更新为[n_classes,len(X_i)]的形状,len(X_i)表示X_i这个特征的取值情况数量);而category_count_的作用是记录在各个类别下每个特征变量中各种取值情况的数量,例如category_count_[i][j][k]为10表示含义就是特征i在类别j下特征取值为k的样本数量为10个。

在初始化两个计数器之后,进一步便可以实现各个类别及特征分布的统计,代码如下:

在上述代码中,第1行参数Y是原始标签经过one-hot编码后的形式,例如3分类问题中类别1会被编码成[0,1,0]的形式,因此Y的形状为[n,n_classes];第9行代码是计算得到每个类别对应的样本数量;第10行则是统计每个特征维度的取值数量(因为特征取值是从0开始的所以后面加了1),例如[3 3 3 3]表示四个特征维度的取值均有3种情况;第12-13行开始遍历每个特征并取对应的特征列;第14-16行是对category_count_中的每个元素填充self.n_categories_[i]列全0向量,此时category_count_中每个元素将会变成形状为[n_classes,len(X_i)]的全零矩阵;第17行则是根据输入的每一列特征等相关参数来更新category_count_计数器。

第3-5行为遍历每一个样本类别,并取每个类别下对应样本的索引,同时统计当前类别下特征列X_feature中各个取值下的数量。同时,第5行中np.bincount的作用的是统计每个值出现的次数,例如:

第6-7行则是用来更新cat_count中当前输入特征每种取值情况的分布数量,例如cat_count[i,k]表示的是第i个类别下,特征X_feature的第k个取值的数量。

表 1. 示例计算数据

例如对于表1中的数据集来说,其对应的category_count_计数器的结果为:

其中[[4., 1.],[3., 7.]]分别表示的含义就是:对于第1个特征X(1)来说,在y=0这个类别下,X(1)取值为0的情况有4种(表中第4-7号样本),取值为1的情况有1种(第14号样本);在y=1这个类别下,X(1)取值为0的情况有3种种(第1-3号样本),取值为1的情况有7种。

同理,对于第2个特征X(2)来说,在y=0这个类别下,X(2)取值为0的情况有4种,取值为1的情况有1种;在y=1这个类别下,X(2)取值为0的情况有4种,取值为1的情况有6种。

到此,对于数据集中样本及特征分布情况的计数就算是统计完了,接下来开始先验概率和条件概率。

3.2 先验概率实现

有了数据集中各个样本的分布情况后,计算先验概率就变得十分简单了,代码如下:

在上述代码中,第2-3便是用来计算各个类别的先验概率,其中带有alpha的地方为平滑处理项。值得一提的是在sklearn中对于类别的先验概率并没有采取平滑处理(等价于第2-3行中alpha=0时的情况),这是因为数据集中也不可能出现整个类别缺失的情况。这里加上平滑处理只是为了与之前介绍的内容相匹配。

3.3 条件概率实现

进一步,可以根据category_count_中的统计结果来实现各特征取值的条件概率,代码如下:

在上述代码中,第4行是取当前特征在不同类别下各个特征的取值的统计结果,并加上平滑项系数;第5-6行是计算在当前特征下各类别样本的总数,并进行平滑处理;第7行则是计算每个特征取值对应的条件概率。

表 1. 示例计算数据

因此,feature_prob_[i][j][k]表示的就是特征i在类别j下取值为k时的概率。例如对于表1中的训练数据集来说,计算得到feature_prob_的结果如下所示:

上述结果中,第1个元素表示:对于第1个特征X(1)来说,在y=0这个类别下,X(1)=0的概率为0.8X(1)=1的概率为0.2;对于第1个特征X(1)来说,在y=1这个类别下,X(1)=0的概率为0.3X(1)=1的概率为0.7。这一结果也可以同式(6)中的结果进行对比。

3.4 模型拟合实现

上述先验概率和条件概率的计算对应便是整个模型的拟合过程,换句话说对于贝叶斯算法来说,所谓的模型拟合就是计算先验概率和条件概率。在实现这部分代码之后,再通过一个函数将整个过程串起来即可,代码如下:

在上述代码中,第2-4行用来将原始[0,1,2,3...]这样的标签转换为one-hot编码形式的标签值,形状为[n,n_classes];第5行用来记录原始的标签类别,形状为[n_classes,];第6-7行用来处理当数据集为二分类时fit_transform处理后的结果并不是one-hot形式,需要添加一列来转换成one-hot形式;第8行是获取数据集的类别数量;第9-12行则分别是上面3节内容介绍到的初始化计数器、特征取值分布情况统计、计算先验概率和计算条件概率。

3.5 后验概率实现

在完成模型的拟合过程后,对于新输入的样本来说其最终的预测结果则取决于对应的极大后验概率。实现代码如下所示:

在上述代码中,第2-4行用来判断输入样本的特征维度是否等于训练集中样本的特征维度;第5行是初始化每个样本的联合似然估计值;第6-7行是遍历测试数据中对应的每一列特征;第11行是取每个特征取值下对应类别的条件概率,并进行累乘(因为self.feature_prob_[i][:, indices]的形状为[n_classes,n],而jll的初始形状为[n,n_classes]所以前者要进行转置);第12行则是计算每个样本对应的后验概率估计。

同时值得注意的一点是,在文本向量化中对于考虑词频的词袋模型来说,其向量表示并不能直接使用于这里实现的贝叶斯模型。因为此时训练集词表中每个词出现的次数并不是该特征维度对应的取值情况数,而是表示的频次。例如在训练集中“客栈”这个词出现的最大次数为10,那么模型在拟合过程中就会认为“客栈”这个维度的特征取值有10种情况,并以此进行建模;但是当测试集中的某个样本里“客栈”这个词出现的频次为11时,那么模型便会认为该维度多了一种取值情况,进而无法取到对应的条件概率。所以掌柜在8-10行中加了对应的判断条件。

在实现完个样本后验概率的计算结果后,最后一步需要完成的便是极大化操作,即从所有后验概率中选择最大的概率值对应的类别作为该样本的预测类别即可。实现代码如下所示:

在上述代码中,第4行便是极大化后验概率的操作;第5-8样则是根据对应的参数来返回预测后的结果。

3.6 使用示例

下面,掌柜先以表1中的模拟数据来通过上述实现的代码进行示例。

首先需要构建表1中的模拟数据,代码如下:

其中特征X(3)的取值这里使用了0,1,2来进行代替。

接着来实现模型的构建部分,代码如下:

在上述代码中,掌柜分别用了我们自己动手实现的贝叶斯模型和sklearn中的CategoricalNB模块进行对比;第13-17行为相关计算过程的日志打印输出,关于这部分内容可以参见文章训练模型时如何保存训练日志?

最终,上述代码中的运行结果如下所示:

在上述结果中,第17行输出的便是样本[0, 1, 0]的原始预测概率,也就是式(7)(8)中的计算结果;第18行则是最终的预测类别以及经过归一化后的概率输出;第20-22行则是sklearn中CategoricalNB模块的输出结果,之所以第22行中的概率值不一样是因为CategoricalNB在计算条件概率以及先验概率时都先取了log对数,但是这并不影响最终的预测结果。不过从结果来看,取对数后两个类别预测的概率有更大的区分度。

下面,掌柜再通过一个垃圾邮件分类实例来测试上述代码实现。

上述代码便是一个基于朴素贝叶斯模型的垃圾邮件分类示例,这里需要注意的是在对文本进行表示的时候采用的是不考虑词频的词袋模型,具体做法可以参见文章文本特征表示与模型复用第7.1节内容。

上述代码运行结束后便可以看到如下所示的结果:

可以看到,两者在各项评估指标上并没有任何差异。

4 总结

在这篇文章中,掌柜首先简单回顾了朴素贝叶斯算法的基本原理和计算示例;然后分别详细介绍了朴素贝叶斯中样本分布及特征取值的统计、先验概率和条件概率的计算、极大化后验概率的代码实现;最后,分别以模拟数据和真实的文本数据来测试了实现代码的正确性。

本次内容就到此结束,感谢您的阅读!如果你觉得上述内容对你有所帮助,欢迎点赞分享!若有任何疑问与建议,请添加掌柜微信nulls8(备注来源)或加群进行交流。青山不改,绿水长流,我们月来客栈见

引用

[1]李航,统计机器学习,清华大学出版社

[2]Scikit-learn: Machine Learning in Python, Pedregosa et al., JMLR 12, pp. 2825-2830, 2011.

[3]训练模型时如何保存训练日志?

[4]文本特征提取与模型的复用

[5]代码仓库:https://github.com/moon-hotel/MachineLearningWithMe