在 scikit-learn 机器学习框架中,sklearn.naive_bayes.MultinomialNB
是对多项式朴素贝叶斯算法的工程实现。接下来,通过一个例子能够理解:
- 算法的拟合和推理的计算过程;
- 拉普拉斯平滑计算在拟合时的作用;
- 词频是如何影响推理得到的后验概率;
下面是实验数据:
features = ['a1', 'a2', 'a3'] inputs = [[10, 2, 3], [2, 3, 4], [3, 4, 5]] labels = [0, 0, 1]
1. fit 函数
fit 函数在 scikit-learn 中用于拟合训练数据,即:学习训练数据。在多项式朴素贝叶斯算法中,fit 函数究竟从训练数据中学习到了什么?
fit 函数通过对训练集中的多个特征和标签进行对数似然分析,分别得到每个类别的对数似然,以及在不同类别条件下,每个特征的对数似然值,即:我们会从训练数据中学习到两类统计数据:
- class_log_prior_ 每个类别的先验对数概率
- feature_log_prob_ 每个特征的对数概率
为什么不是概率,而是对数概率?多项式朴素贝叶斯算法中,需要求解不同特征的联合概率(特征独立条件下),这就涉及到了多个小于1的概率值的乘积计算。这带来两个缺点:
- 多个小于 1 的概率值乘积可能会带来数值下溢出问题,即:乘积的值越来越小,直至溢出;
- 计算效率也较低。
将连续乘积的计算,转换为对数计算,将乘积运算转换为加法运算,一定程度上加快了推理时的计算速度,以及数值溢出的问题。
下面是通过手动模拟计算的过程,来说明 fit 函数的拟合计算过程:
import numpy as np from sklearn.naive_bayes import MultinomialNB def test(): # 特征名 fnames = ['a1', 'a2', 'a3'] # 输入值 inputs = [[10, 2, 3], [2, 3, 4], [3, 4, 5]] # 标签值 labels = [0, 0, 1] # 初始化多项式朴素贝叶斯 estimator = MultinomialNB() # 你和训练数据 estimator.fit(inputs, labels) # 训练之后: 标签的先验对数似然 print(estimator.class_log_prior_) # 训练之后: 特征的对数似然 print(estimator.feature_log_prob_) print('-' * 20) # 1. 统计不同类别的标签数量,例如: 在训练集中,0标签样本出现2次,1标签样本出现1次. [2, 1] # 2. 统计在不同类别下,各个特征出现的次数. 例如: 在0标签下,三个特征出现的次数为 [12, 5, 7], 在1标签下出现的次数为 [3, 4, 5] # 3. 计算在不同类别下,各个特征的对数似然. 注意, 此时进行了拉普拉斯平滑计算,分子+1,分母+特征数量3. 例如:在0标签下分别计算 [log(12+1 / 12+5+7+3), log(5+1 / 12+5+7+3), log(7+1 / 12+5+7+3)] # 例如: 0类别样本中,各个特征的对数似然 print('%.8f' % np.log( (12+1)/(12+5+7+3) )) print('%.8f' % np.log( (5+1) /(12+5+7+3) )) print('%.8f' % np.log( (7+1) /(12+5+7+3) )) # 4. 计算不同类别的对数似然. 例如:0类别样本出现2次,共3条样本,则该类别的先验对数似然值为 log(2/3) # 例如: 0类别标签的对数似然 print('%.8f' % np.log(2/3)) if __name__ == '__main__': test()
程序输出结果:
# 算法输出的类别先验对数概率 [-0.40546511 -1.09861229] # 算法输出的每个特征在不同类别条件下的对数概率 [ 0类别: [-0.73088751 -1.5040774 -1.21639532] 1类别: [-1.32175584 -1.09861229 -0.91629073] ] -------------------- # 手动计算得出的0类别的每个特征对数概率,我们发现和 sklearn 计算得出的结果一样 -0.73088751 -1.50407740 -1.21639532 # 手动计算得出的0类别的先验对数概率,我们发现和 sklearn 计算得出的结果一样 -0.40546511
从 fit 函数的计算过程可以看到,当某个特征词在训练集中出现为 0 时,可以通过拉普拉斯平滑计算来得到估计的对数概率值 log(1/12+5+7+3)
2. predict_log_proba 函数
我们先使用 scikit-learn 来计算得到对数概率,然后再分步理解具体的计算过程:
import numpy as np from sklearn.naive_bayes import MultinomialNB def test(): # 特征名 fnames = ['a1', 'a2', 'a3'] # 输入值 inputs = [[10, 2, 3], [2, 3, 4], [3, 4, 5]] # 标签值 labels = [0, 0, 1] # 初始化多项式朴素贝叶斯 estimator = MultinomialNB() # 你和训练数据 estimator.fit(inputs, labels) # 计算样本属于不同类别的对数概率 proba = estimator.predict_log_proba([[1, 2, 3]]) print('样本输入0类别对数概率:', proba[0][0]) print('样本输入1类别对数概率:', proba[0][1]) if __name__ == '__main__': test() # 程序输出结果: 样本输入0类别对数概率: -0.9294055098327014 样本输入1类别对数概率: -0.5021770282648301
下面为分步计算过程,计算输入样本属于不同类别的后验对数概率:输入向量
* 0类别的每个特征的对数概率
+ 每个类别的先验对数概率
,计算过程如下:
# 待推理的输入为: [[1, 2, 3]] # estimator.class_log_prior_ 值为: [-0.40546511 -1.09861229] # estimator.feature_log_prob_ 值为: [[-0.73088751 -1.5040774 -1.21639532], [-1.32175584 -1.09861229 -0.91629073]] # 1. 输入样本属于0类别的对数概率 post_0 = np.sum(np.array([1, 2, 3]) * estimator.feature_log_prob_[0]) + estimator.class_log_prior_[0] # 2. 输入样本属于1类别的对数概率 post_1 = np.sum(np.array([1, 2, 3]) * estimator.feature_log_prob_[1]) + estimator.class_log_prior_[1] print('属于0类别的后验概率为: post_0 ', post_0) print('属于1类别的后验概率为: post_1 ', post_1) # 输出结果: 属于0类别的后验概率为: post_0 -7.7936933831769855 属于1类别的后验概率为: post_1 -7.366464901609114 # 上面的结果等价于下面 # print(estimator.predict_joint_log_proba([[1, 2, 3]]))
接下来,对当前样本计算得到的两个后验概率进行标准化,公式如下:
由于我们计算的是对数概率值,所以对标准后得到的概率值再次进行对数计算,公式如下:
上述公式在计算第二部分时,为了避免指数计算时溢出的问题,所以会进行如下方式计算:
最终,对数标准概率的计算公式如下:
# 得到数值中的最大值 a_max = np.max([post_0, post_1]) # 在计算指数之前,先减去最大值,避免数值溢出,最后再加上最大值,以还原到原始数值的尺度 post_norm_0 = post_0 - (np.log(np.sum(np.exp(post_0 - a_max) + np.exp(post_1 - a_max))) + a_max) post_norm_1 = post_1 - (np.log(np.sum(np.exp(post_0 - a_max) + np.exp(post_1 - a_max))) + a_max) print('样本输入0类别对数概率:', post_norm_0) print('样本输入1类别对数概率:', post_norm_1) 输出结果: 样本输入0类别对数概率: -0.9294055098327014 样本输入1类别对数概率: -0.5021770282648301
从这个计算过程可看到,当输入的特征中,某些特征值为 0,多项式朴素贝叶斯的计算也不会受到影响。另外,也可以看到某个特征词出现的次数对后验概率的影响还是有的,例如:a1
特征词在 0
类别的对数概率为 -0.73088751
,当推理数据中,含有 1 特征词时,会计算 1 * -0.73088751
,当含有 2 个该特征词时,会计算 2 * -0.73088751
。从这里,我们可以把训练时得到的每个特征的概率值理解为某个特征词对某个类别预测的重要程度。