上一篇日志中,我们主要介绍了贝叶斯算法,并提供了 python 实践:
但运行上一篇日志中的示例,我们发现出现了下面的结果:
['love', 'my', 'dalmation'] 属于非侮辱类
['stupid', 'garbage'] 属于非侮辱类
这显然是不正确的,本文,我们就来解决这个问题,同时对算法进行优化并使用 sklearn 来实现算法的实践。
上一篇文章中,我们利用贝叶斯分类器对文档进行分类时,需要算多个概率的乘积以获得文档属于某个类别的概率,即计算 p(w0|1) p(w1|1) p(w2|1),只要有一个概率值为0,那么最终的结果就会随之变成 0,这就是上一篇文章中,算法运行结果两个测试用例都是非侮辱类的原因。 要降低这种影响,可以讲所有词的出现数初始化为 1,并将分母初始化为 2,这个做法就是拉普拉斯平滑。 我们将上一篇日志中代码的 trainNB0 方法中的 p0Num、p1Num、p0Denom、p1Denom 赋值语句改为:
p0Num = np.ones(vocabularysNum)
p1Num = np.ones(vocabularysNum)
p0Denom = 2.0
p1Denom = 2.0
进行拉普拉斯平滑运算后,我们运行程序,仍然得出了两个测试样本均属于非侮辱类的结果,这是为什么呢? 我们查看最终计算出的 p0 和 p1 会发现,他们的结果都是 0,这又是为什么呢? 这是因为出现了另一个问题 — 下溢出。 我们的概率运算中,所有参与运算的概率都太小了,小数相乘会使运算的积进一步减小,最终结果向下溢出超出了计算机浮点数的精度,就都会变成 0。 解决办法很自然的可以想到 — 将乘法运算转换为加法运算,但如何在保证算法正确性的前提下进行转换呢? 在代数中,ln(a * b) = ln(a) + ln(b),同时,自然对数可以保证运算趋势的正确性:
因此我们通过对数运算优化训练函数 trainNB0 与测试函数 classifyNB:
def trainNB0(trainMap, results):
"""
朴素贝叶斯分类器训练函数
:param trainMap: 训练文档矩阵
:param results: 训练类别标签向量
:return:
p0Vect - 侮辱类的条件概率数组
p1Vect - 非侮辱类的条件概率数组
pAbusive - 文档属于侮辱类的概率
"""
dataListNum = len(trainMap)
vocabularysNum = len(trainMap[0])
""" 计算文档属于侮辱词概率 """
pAbusive = sum(results) / float(dataListNum)
p0Num = np.ones(vocabularysNum)
p1Num = np.ones(vocabularysNum)
p0Denom = 2.0
p1Denom = 2.0
""" 将所有行按是否是侮辱类分别叠加,统计各个词出现的次数 """
for i in range(dataListNum):
if results[i] == 1:
p1Num += trainMap[i]
p1Denom += sum(trainMap[i])
else:
p0Num += trainMap[i]
p0Denom += sum(trainMap[i])
""" 计算概率 """
p1Vect = np.log(p1Num / p1Denom)
p0Vect = np.log(p0Num / p0Denom)
return p0Vect, p1Vect, pAbusive
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
"""
朴素贝叶斯分类器分类函数
:param vec2Classify: 待分类的词条数组
:param p0Vec: 侮辱类的条件概率数组
:param p1Vec: 非侮辱类的条件概率数组
:param pClass1: 文档属于侮辱类的概率
:return: 是否属于侮辱类,0. 不属于,1. 属于
"""
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
print("p0: ", p0)
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
print("p1: ", p1)
if p1 > p0:
return 1
else:
return 0
最终我们得到了正确的结果:
p0: -7.694848072384611
p1: -9.826714493730215
['love', 'my', 'dalmation'] 属于非侮辱类
p0: -7.20934025660291
p1: -4.702750514326955
['stupid', 'garbage'] 属于侮辱类
通过上一篇日志的介绍和本文的优化,我们了解了朴素贝叶斯算法的原理和应用,他是一种基于概率的分类器算法,可以用来处理不相干因子的多分类问题,例如根据词频进行文本分类等问题。 那么他又具有哪些优缺点呢?
1. 算法原理和实现简单,通过概率分类 2. 对小规模数据表现很好,适合多分类增量式训练任务
1. 对输入数据的表达形式很敏感 2. 需要计算先验概率,分类决策存在错误率 3. 要求样本之间相互独立,这就是“朴素”的意思,这个限制有时很难做到,或使用者误以为符合而造成错误的结果
sklearn 提供了朴素贝叶斯算法的实现类 — sklearn.naive_bayes.MultinomialNB。 下面的列表中,我们将分类数称为 nc,将特征数称为 nf。
参数名 | 类型 | 可选参数 | 默认值 | 说明 |
---|---|---|---|---|
alpha | float | 非负浮点数 | 1 | 拉普拉斯平滑系数 |
fit_prior | boolean | True/False | True | 是否使用先验分类概率 |
class_prior | array | None 或array(nc*1) | None | 如果指定 fit_prior 为 True,该参数用来提供先验概率 |
属性名 | 类型 | 说明 |
---|---|---|
classlog_prior | array(nc*1) | 每个分类的平滑对数先验概率 |
intercept_ | array(nc*1) | 将多项式朴素贝叶斯理解为线性模型时,与 classlog_prior 相同 |
featurelog_prob | array(nc*nf) | 每个分类的每个特征的对数先验概率(P(x_i|y)) |
coef_ | array(nc*nf) | 将多项式朴素贝叶斯理解为线性模型时,与 featurelog_prob 相同 |
classcount | array(nc*1) | 在拟合过程中每个分类的样本数 |
featurecount | array(nc*nf) | 在拟合过程中每个分类的每个特征的样本数 |
import numpy as np
from sklearn.naive_bayes import MultinomialNB
if __name__ == '__main__':
X = np.random.randint(5, size=(6, 100))
y = np.array([1, 2, 3, 4, 5, 6])
clf = MultinomialNB()
clf.fit(X, y)
print(clf.predict(X[2:3]))
上面的示例,我们通过随机数创建了一个 6*100 的矩阵,其中每个元素都是0到5的随机数,我们用这个矩阵的每一行分别对应 1、2、3、4、5、6,最终,我们用第三行来测试这个模型,果然得到了预期的数字:3。
对于相互独立的样本来说,朴素贝叶斯是一个非常不错的分类器,在自然语言处理和文本特征分析、过滤等领域有着广泛的应用。 事实上,朴素贝叶斯共有三种模型,他们的区别在于计算条件概率的公式不同: 1. 高斯朴素贝叶斯 — 用于符合高斯分布(正态分布)的连续样本数据的分类 2. 多项式朴素贝叶斯 — 我们已经介绍的内容就是多项式朴素贝叶斯模型 3. 伯努利朴素贝叶斯 — 每个特征的取值为0或1,即计算特征是否存在的概率,他是唯一将样本中不存在的特征也引入计算概率的朴素贝叶斯模型
Peter Harrington 《机器学习实战》。 李航 《统计学习方法》。 https://zh.wikipedia.org/wiki/%E6%9C%B4%E7%B4%A0%E8%B4%9D%E5%8F%B6%E6%96%AF%E5%88%86%E7%B1%BB%E5%99%A8。 https://scikit-learn.org/dev/modules/generated/sklearn.naive_bayes.MultinomialNB.html。