第7.5节 高斯朴素贝叶斯原理与实现
各位朋友大家好,欢迎来到月来客栈,我是掌柜空字符。
本期推送内容目录如下,如果本期内容对你有所帮助,欢迎点赞、转发支持!
7.5 高斯朴素贝叶斯原理与实现 7.5.1 算法思想 7.5.2 算法原理 7.5.3 计算示例 7.5.4 高斯贝叶斯实现 7.5.5 模型对比 7.5.6 小结 引用
7.5 高斯朴素贝叶斯原理与实现
在前面两节内容中,笔者分别介绍了基于类别特征的Categorical朴素贝叶斯算法和基于特征权重的Multinomial朴素贝叶斯算法,而两者之间的唯一区别就体现在对条件概率的处理上。在接下来的这节内容中,笔者将会介绍第3种基于朴素贝叶斯思想的极大化后验概率模型——高斯朴素贝叶斯(Gaussian Naive Bayes, GNB)。
7.5.1 算法思想
根据Categorical贝叶斯和Multinomial贝叶斯算法的原理可知,前者只能用于处理类别型取值的特征变量,而后者的初衷也是为了处理包含词频的文本向量表示(尽管从结果上看也适用于类似TFIDF这样的连续型特征)。所谓高斯贝叶斯是指假定样本每个特征维度的条件概率均服从高斯分布,进而再根据贝叶斯公式来计算得到新样本在某个特征分布下其属于各个类别的后验概率,最后通过极大化后验概率来确定样本的所属类别。
7.5.2 算法原理
高斯贝叶斯算法假定数据样本在各个类别下,每个特征变量的条件概率均服从高斯分布,即
其中表示第个特征维度,和分布表示在类别下特征对应的标准差和期望。
在计算得到每个特征维度的条件概率后,再进行极大化后验概率计算
这里需要注意的是,同上一节介绍的多项式朴素贝叶斯一样,在后验概率计算过程中同样进行取对数操作。
7.5.3 计算示例
假设现在有一个基于TFIDF方法表示文本数据,其一共包含有 这3个特征维度,每个维度表示词表中相应词的TFIDF权重,表示样本对应的所属类别,如表7-2所示。现需要预测这个样本的所属类别。
由表7-2易知,各个类别的先验概率为
根据表7-2可知,当时特征对应的参数期望和方差为
同理可得
其中表示第个类别的第个特征对应的期望,表示第个类别的第个特征对应的方差。
进一步,根据式(7.23)可知,当,时对应的条件概率为
同理可得
进一步,各后验概率为
根据以上的计算结果可知,样本属于这个类别的“概率”最大。同时,通常情况下在输出概率时会对上面最后的结果进行softmax操作,最终样本属于3个类别的概率值分别为。
7.5.4 高斯贝叶斯实现
在有了前面Categorical和Multinomial贝叶斯算法的实现经验后,高斯贝叶斯的实现过程就非常容易理解了。下面,笔者依旧分步进行讲解实现。需要说明的是以下实现代码均参考自sklearn 0.24.0 中的GaussianNB
模块,只是对部分处理逻辑进行了修改与简化,完整代码参见Book/Chapter07/C03_naive_bayes_gaussian.py
文件。
1. 参数初始化实现
根据第7.5.2节内容可知,不管是计算先验概率还是条件概率都需要根据训练集计算得到相关参数。因此,这里需要先对各个参数进行初始化,实现代码如下所示:
1 class 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行则是初始化计算条件概率中的期望和方差,其中mu_[i][j]
表示第i
个类别的第j
个特征对应的期望,sigma_[i][j]
表示第i
个类别的第j
个特征对应的方差;第11~12行用来统计每个类别下的样本数以及初始化先验概率。
2. 模型拟合实现
由于参数计算过程较为简单,所以这里并没有将这部分代码单独写为一个方法。整个模型拟合(参数计算)过程实现代码如下:
1 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行用来初始化得到相关参数,也就是上面第1步节介绍的内容;第5行开始逐一遍历每个类别下的样本;第6~7行是计算当前类别中所有样本每个维度所对应的期望和方差;第8行是统计得到当前类别对应的样本数量;第9行则是对计算后的方差进行平滑处理,同时第3行是为了避免加入的平滑项系数太大从而对结果产生严重影响,因此选择了以最大方差最为基础;第10行是计算每个类别对应的先验概率。
3. 后验概率实现
在完成模型的拟合过程后,对于新输入的样本来说其最终的预测结果则取决于对应的极大后验概率。根据式(7.23)可知,后验概率实现代码如下所示:
1 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行是实现式(7.23)中的条件概率计算过程;第8行是计算当前类别下对应的后验概率;第10行则是返回所有样本计算得到后验概率。
在实现每个样本后验概率的计算结果后,最后一步需要完成的便是极大化操作,即从所有后验概率中选择最大的概率值对应的类别作为该样本的预测类别即可,实现代码如下所示: