1 引言
各位朋友大家好,欢迎来到月来客栈,我是掌柜空字符。
在前面两篇文章中,掌柜分别介绍了基于类别特征的Categorical朴素贝叶斯算法和基于特征权重的Multinomial朴素贝叶斯算法,而两者的主要却别就在于对条件概率的处理上。在接下来的这篇文章中,掌柜将会介绍第三种基于朴素贝叶斯思想的极大化后验概率模型——高斯朴素贝叶斯(Gaussian Naive Bayes, GNB)。
2 高斯朴素贝叶斯
根据Categorical贝叶斯和Multinomial贝叶斯算法的原理可知,前者只能用于处理类别型取值的特征变量,而后者的初衷也是为了处理包含词频的文本向量表示(尽管从结果上看也适用于类似tf-idf这样的连续型特征)。所谓高斯贝叶斯指的便是假定样本每个特征维度的条件概率均服从高斯分布,进而再根据贝叶斯公式来计算得到新样本在某个特征分布下其属于各个类别的后验概率,最后通过极大化后验概率来确定样本的所属类别。
2.1 算法原理
高斯贝叶斯算法假定数据样本在各个类别下,每个特征变量的条件概率均服从高斯分布,即
其中
在计算得到每个特征维度的条件概率后,再进行极大化后验概率计算
这里需要注意的是在后验概率计算过程中同样进行取对数操作,原因可参见文章多项式朴素贝叶斯原理与实现。
2.2 计算示例
假设现在有一个基于TF-IDF方法表示文本数据,其一共包含有
由表1易知,各个类别的先验概率为
根据表1课时,当
同理可得
其中
进一步,根据式
同理可得
进一步,各后验概率为
根据式
3 高斯叶斯实现
在有了前面Categorical和Multinomial贝叶斯算法的实现经验后,高斯贝叶斯的实现过程就非常容易理解了。下面,掌柜依旧分步进行讲解实现。需要说明的是以下实现代码均参考自sklearn 0.24.0 中的GaussianNB
模块,只是对部分处理逻辑进行了修改与简化。
3.1 参数初始化实现
根据2.1节中的内容可知,不管是计算先验概率还是条件概率都需要根据训练集计算得到相关参数。因此,这里需要先对各个参数进行初始化,代码如下:
xxxxxxxxxx
121class MyGaussianNB(object):
2 def __init__(self, var_smoothing=1e-9):
3 self.var_smoothing = var_smoothing
4
5 def _init_counters(self, X, y):
6 self.classes_ = np.sort(np.unique(y))
7 n_features = X.shape[1]
8 n_classes = len(self.classes_)
9 self.mu_ = np.zeros((n_classes, n_features))
10 self.sigma2_ = np.zeros((n_classes, n_features))
11 self.class_count_ = np.zeros(n_classes, dtype=np.float64)
12 self.class_prior_ = np.zeros(len(self.classes_), dtype=np.float64)
在上述代码中,第3行为方差平滑项,主要是为了避免在计算条件概率是方差(分母)为0的情况,尤其是在高维特征中这种现象很容易出现;第6行用来得到训练集中的分类情况,而排序是为了后面依次遍历每个类别;第7-8行分别用来得到特征维度和分类类别总数;第9-10行则是初始化计算条件概率中的期望和方差(第2节中的式mu_[i][j]
表示第i
个类别的第j
个特征对应的期望,sigma_[i][j]
表示第i
个类别的第j
个特征对应的方差;第11-12行用来统计每个类别下的样本数以及初始化先验概率。
3.2 模型拟合实现
由于参数计算过程较为简单,所以这里并没有将这部分代码单独写为一个方法。整个模型拟合(参数计算)过程实现代码如下:
xxxxxxxxxx
111 def fit(self, X, y):
2 self._init_counters(X, y)
3 self.epsilon_ = self.var_smoothing * np.var(X, axis=0).max()
4 for i, y_i in enumerate(self.classes_): # 遍历每一个类别
5 X_i = X[y == y_i, :] # 取类别y_i对应的所有样本
6 self.mu_[i, :] = np.mean(X_i, axis=0) # 计算期望
7 self.sigma2_[i, :] = np.var(X_i, axis=0) # 计算方差
8 self.class_count_[i] += X_i.shape[0] # 类别y_i对应的样本数量
9 self.sigma2_ += self.epsilon_
10 self.class_prior_ = self.class_count_ / self.class_count_.sum()
11 return self
在上述代码中,第2行用来初始化得到相关参数,也就是3.1节介绍的内容;第5行开始逐一遍历每个类别下的样本;第6-7行是计算当前类别中所有样本每个维度所对应的期望和方差;第8行是统计得到当前类别对应的样本数量;第9行则是对计算后的方差进行平滑处理,同时第3行是为了避免加入的平滑项系数太大从而对结果产生严重影响,因此选择了以最大方差最为基础;第10行是计算每个类别对应的先验概率。
3.3 后验概率实现
在完成模型的拟合过程后,对于新输入的样本来说其最终的预测结果则取决于对应的极大后验概率。根据式
xxxxxxxxxx
101 def _joint_likelihood(self, X):
2 joint_likelihood = []
3 for i in range(np.size(self.classes_)):
4 jointi = np.log(self.class_prior_[i]) # shape: [1,]
5 n_ij = - 0.5 * np.sum(np.log(2. * np.pi * self.sigma2_[i, :]))
6 n_ij -= 0.5 * np.sum(((X - self.mu_[i, :]) ** 2) /
7 (self.sigma2_[i, :]), 1) # shape: [n_samples,]
8 joint_likelihood.append(jointi + n_ij) # [[n_samples,1],..[n_samples,1]]
9 joint_likelihood = np.array(joint_likelihood).T # [n_samples,n_classes]
10 return joint_likelihood
在上述代码中,第4行用来对先验概率取对数操作;第5-7行是实现式
在实现每个样本后验概率的计算结果后,最后一步需要完成的便是极大化操作,即从所有后验概率中选择最大的概率值对应的类别作为该样本的预测类别即可。实现代码如下所示:
xxxxxxxxxx
81 def predict(self, X, with_prob=False):
2 from scipy.special import softmax
3 jll = self._joint_likelihood(X)
4 y_pred = self.classes_[np.argmax(jll, axis=1)]
5 if with_prob:
6 prob = softmax(jll)
7 return y_pred, prob
8 return y_pred
在上述代码中,第4行便是极大化后验概率的操作;第5-8样则是根据对应的参数来返回预测后的结果。
3.4 使用示例
下面,掌柜先以表1中的模拟数据来通过上述实现的代码进行示例。
xxxxxxxxxx
181def load_simple_data():
2 import numpy as np
3 x = np.array([[0.8, 0.2, 0.3, 0.5, 0.1, 0.7, 0.1, 0.3, 0.6, 0.6],
4 [0.3, 0.8, 0.4, 0.5, 0.6, 0.5, 0.6, 0.7, 0.1, 0.6],
5 [0.3, 0.3, 0.6, 0.7, 0.8, 0.8, 0.8, 0.3, 0.3, 0.6]]).transpose()
6 y = np.array([1, 1, 2, 2, 2, 1, 1, 0, 1, 0])
7 return x, y
8
9def test_naive_bayes():
10 x, y = load_simple_data()
11 logging.info(f"========== MyGaussianNB 运行结果 ==========")
12 model = MyGaussianNB()
13 model.fit(x, y)
14 logging.info(f"预测结果: {model.predict(np.array([[0.5, 0.12, 0.218]]), with_prob=True)}")
15 logging.info(f"========== GaussianNB 运行结果 ==========")
16 model = GaussianNB()
17 model.fit(x, y)
18 logging.info(f"预测结果: {model.predict(np.array([[0.5, 0.12, 0.218]]))}")
在上述代码运行结束后可以看到类似如下的运行结果:
xxxxxxxxxx
131- INFO: ========== MyGaussianNB 运行结果 ==========
2- DEBUG: 期望mu = [[0.45 0.65 0.45] [0.48 0.46 0.5 ] [0.3 0.5 0.7 ]]
3- DEBUG: 方差sigma = [[0.0225 0.0025 0.0225] [0.0776 0.0584 0.06] [0.02667 0.00667 0.00667]]
4- DEBUG: 先验概率 = [0.2 0.5 0.3]
5- DEBUG: log先验概率 = [-1.60943791 -0.69314718 -1.2039728 ]
6- DEBUG: 样本预测原始概率为:[[-55.00792571 -0.99994715 -26.14228264]]
7- INFO: 预测结果: (array([1]), array([[3.50455541e-24, 1.00000000e+00, 1.20454334e-11]]))
8- INFO: ========== GaussianNB 运行结果 ==========
9- DEBUG: 期望mu = [[0.45 0.65 0.45] [0.48 0.46 0.5 ] [0.3 0.5 0.7 ]]
10- DEBUG: 方差sigma = [[0.0225 0.0025 0.0225] [0.0776 0.0584 0.06] [0.02667 0.00667 0.00667]]
11- DEBUG: 先验概率 = [0.2 0.5 0.3]
12- DEBUG: log先验概率 = [-1.60943791 -0.69314718 -1.2039728 ]
13- INFO: 预测结果: [1]
在上述结果中,上面部分为本文所实现代码的输出结果,下面部分则是sklearn中GaussianNB
模块的输出结果。
3.5 模型对比
通过连续这几篇文章,掌柜已经详细介绍完了最原始基于类别型变量的Categorical朴素贝叶斯模型、基于词频占比的Multinomial朴素贝叶斯模型以及基于高斯分布的Gaussian朴素贝叶斯模型。对于Categorical NB来说,其只能处理类别型变量作为特征的数据集,例如不考虑词频的词袋模型;对于Multinomial NB来说,其初始动机是为了考虑词袋模型中词频占比对模型的结果的影响,但从结果来看对于类似tfidf的特征表示模型依旧能够有着很好的效果,这是因为Multinomial NB还可以从线性模型的角度来进行解释;对于Gaussian NB来说,它假设数据集中每个特征维度的条件概率均符合高斯分布,因此理论上来讲它更适合处理连续型的特征变量,但从结果来看其对于离散型的特征表示也有着不错的效果。
如表2所示便是前面使用到的垃圾邮件分类数据集在不同方式的特征表示下,3种模型在测试集上的表现结果(示例代码见[5])。
从表2可以看出,对于Categorical NB模型来说只能处理类别型的特征变量,而对于其它两个模型来说3中特征表示均可。同时,考虑词频的Multinomial NB模型在效果上要略好于不考虑词频的Categorical NB模型。从模型效果来看,Gaussian NB模型无论是采用哪种特征表示方式,相较于其它两种模型其结果都是最好的,并且让掌柜意外的是不考虑词频的表示方法在Gaussian NB模型中的结果居然是所有结果里最好的。因此,在实际情况中可以优先考虑使用Gaussian NB模型来进行建模。
4 总结
在这篇文章中,掌柜首先介绍了高斯朴素贝叶斯算法的基本原理;然后通过一个实际的示例来进一步的介绍了高斯贝叶斯算法的原理与计算过程;接着一步一步详细地介绍了高斯贝叶斯的实现方法;最后,掌柜还以真实的垃圾邮件分类数据集为例对比了3种不同模型在3中不同特征表示下的分类效果。
本次内容就到此结束,感谢您的阅读!如果你觉得上述内容对你有所帮助,欢迎点赞分享!若有任何疑问与建议,请添加掌柜微信nulls8(备注来源)或文末留言进行交流。青山不改,绿水长流,我们月来客栈见!
引用
[1] https://scikit-learn.org/stable/modules/naive_bayes.html#naive-bayes
[5] 代码仓库:https://github.com/moon-hotel/MachineLearningWithMe