- 本文收录于Github仓库,欢迎前来 star 呀~ https://github.com/Veal98/CS-Wiki
- 在线阅读地址/更好的阅读体验请移步:https://veal98.gitee.io/cs-wiki/
朴素贝叶斯法是基于贝叶斯定理与特征条件独立性假设的分类方法。对于给定的训练集,首先基于特征条件独立假设学习输入输出的联合概率分布(朴素贝叶斯法这种通过学习得到模型的机制,显然属于生成模型);然后基于此模型,对给定的输入 x,利用贝叶斯定理求出后验概率最大的输出 y。
学习朴素贝叶斯算法之前,我们先搞定下面这些基本概念和数学公式
联合概率表示为包含多个条件并且所有的条件都同时成立的概率,记作 P ( X = a , Y = b ) P(X=a,Y=b) P(X=a,Y=b) 或 P ( a , b ) P(a,b) P(a,b) 或 P ( a b ) P(ab) P(ab)
联合概率分布就是联合概率在样本空间中的分布情况
有一个装了 7 块石头的罐子,其中 3 块是白色的,4 块是黑色的。如果从罐子中随机取出一块石头,那么是白色石头的可能性是多少?
显然,取出白色石头的概率为 3/7 ,取到黑色石头的概率是 4/7 。我们使用 P(white)
来表示取到白色石头的概率,其概率值可以通过白色石头数目除以总的石头数目来得到。
❓ 如果这 7 块石头如下图所示,放在两个桶中,那么上述概率应该如何计算?
要计算 P(white)
或者 P(black)
,显然,石头所在桶的信息是会改变结果的,这就是条件概率 conditional probability。假定计算的是从 B 桶取到白色石头的概率,这个概率可以记作 P(white|bucketB)
,我们称之为 “在已知石头出自 B 桶的条件下,取出白色石头的概率”。
很容易得到,P(white|bucketA)
值为 2/4 ,P(white|bucketB)
的值为 1/3 。
条件概率的计算公式如下:
放到我们这个例子中来: P(white|bucketB) = P(white and bucketB) / P(bucketB)
公式解读:
P(white|bucketB)
:在已知石头出自 B 桶的条件下,取出白色石头的概率P(white and bucketB)
:取出 B 桶中 白色石头的概率 = 1 / 7P(bucketB)
:取出 B 桶中石头的概率 3 / 7另外一种有效计算条件概率的方法称为贝叶斯定理。贝叶斯定理告诉我们如何交换条件概率中的条件与结果,即如果已知 P(X|Y)
,要求 P(Y|X)
:
⭐⭐⭐ P ( Y ∣ X ) = P ( X ∣ Y ) P ( Y ) P ( X ) P(Y|X) = \frac{P(X|Y) P(Y) } {P(X)} P(Y∣X)=P(X)P(X∣Y)P(Y)
这里的每个概率都有其特定的名称:
P ( Y ) P(Y) P(Y):先验概率。先验概率(prior probability)是指事情还没有发生,求这件事情发生的可能性的大小,是先验概率。它往往作为"由因求果"问题中的"因"出现。
P ( Y ∣ X ) P(Y|X) P(Y∣X):后验概率。后验概率是指事情已经发生,求这件事情发生的原因是由某个因素引起的可能性的大小。后验概率的计算要以先验概率为基础
P ( X ∣ Y ) P(X|Y) P(X∣Y) :条件概率,又叫似然概率,一般是通过历史数据统计得到。一般不把它叫做先验概率,但从定义上也符合先验定义。
朴素贝叶斯法通过训练数据集学习联合概率分布 P ( X , Y ) P(X,Y) P(X,Y),其实就是学习先验概率分布和条件概率分布:
于是由条件概率公式 P ( X ∣ Y ) = P ( X , Y ) P ( Y ) P(X|Y) = \frac{P(X,Y)}{P(Y)} P(X∣Y)=P(Y)P(X,Y) 可以求出联合概率分布 P ( X , Y ) P(X,Y) P(X,Y)
条件独立性假设就是各个特征之间互不影响,每个特征都是条件独立的。这一假设使得朴素贝叶斯法变得简单,但是有时候会牺牲一定的分类准确率。
朴素贝叶斯法分类时,对给定的输入 x,通过上述学习到的模型计算后验概率分布 P ( Y = c k ∣ X = x ) P(Y = c_k|X = x) P(Y=ck∣X=x),将后验概率最大的类作为 x 的类的输出。后验概率根据贝叶斯定理进行计算:
对分母上的 P ( X = x ) P(X = x) P(X=x) 应用了全概率公式:
将条件独立性假设 4.3 带入上式:
这就是朴素贝叶斯分类的基本公式
朴素贝叶斯分类器就是取后验概率最大时的分类:
显然,上式中的分母对于所有类别来说都是一样的,对计算结果不会产生影响,所以,朴素贝叶斯分类器可以简化为:⭐⭐⭐
其实朴素贝叶斯分类器的后验概率最大化等价于0-1损失函数时的期望风险最小化。
朴素贝叶斯算法基于不同的概率估计方法具有不同的形式。概率估计方法有以下两种:
可以用极大似然估计法去估计相应的概率。
先验概率 P ( Y = c k ) P(Y = c_k) P(Y=ck) 的极大似然估计是:
设第 j 个特征 x ( j ) x^{(j)} x(j) 可能取值的集合为 a j 1 , a j 2 . . . . . . a j S j {a_{j1},a_{j2}......a_{jS_j}} aj1,aj2......ajSj( S j S_j Sj 表示第 j 个特征可能取值有 S j S_j Sj 个。比如特征 1 可能取值有 2 个,则 S 1 = 2 S_1 = 2 S1=2),条件概率 P ( X ( j ) = a j l ∣ Y = c k ) P(X^{(j)} = a_{jl} | Y = c_k) P(X(j)=ajl∣Y=ck) 的极大似然估计是:
其中, x i ( j ) x_i^{(j)} xi(j) 表示第 i 个样本的第 j 个特征; a j l a_{jl} ajl 表示第 j 个特征可能取的第 l l l 个值(比如特征 1 可能取值有 2 个,则 l l l 可以为 1 或 2); I I I 为指示函数
基于极大似然估计法,⭐ 朴素贝叶斯算法如下:
举个例子:
用极大似然估计可能会出现所要估计的概率为 0 的情况,这会影响到后验概率的计算结果,使分类产生偏差。解决这一问题的方法是采用贝叶斯估计。
条件概率的贝叶斯估计:
其中, λ ≥ 0 λ ≥ 0 λ≥0。当 λ = 0 λ = 0 λ=0 时就是极大似然估计。常取 λ = 1 λ = 1 λ=1,这时称为拉普拉斯平滑 Laplacian smoothing
先验概率的贝叶斯估计:
K K K 的含义和 S j S_j Sj 相同,即表示分类的个数。
举个例子:数据同上例 4.1,按照拉普拉斯平滑估计概率:
该实例来源于《机器学习实战》这本书,撸一遍《统计学习方法》后再看这本书上的算法,真的是豁然开朗
项目概述:使用朴素贝叶斯构建一个快速过滤器来屏蔽侮辱性文档。如果某篇文档使用了负面或者侮辱性的语言,那么就将该文档标识为侮辱性文档。对此问题建立两个类别: 侮辱类和非侮辱类,使用 1 和 0 分别表示。
目的:我们需要把文档中的每个单词利用 0 和 1 来表示,这样方便我们进行处理。
首先手动输入文档数据集的,一个列表代表一篇文档:
import numpy as np
# 先验概率
def trainNB0(trainMatrix, trainCategory):
"""
Desc:
返回每个类别对应的先验概率和条件概率
Params:
trainMatrix: 训练数据集,即各个文档对应的 0-1 序列
trainCategory: 训练数据集的类别,即各个文档对应的分类
Return:
p0Vect: 去重词汇表中每个单词在侮辱性文档中出现的概率
p1Vect:去重词汇表中每个单词在非侮辱性文档中出现的概率
"""
numTrainDocs = len(trainMatrix) # 文件数
numWords = len(trainMatrix[0]) # 单词数
# 先验概率
pAbusive = sum(trainCategory) / float(numTrainDocs) # 侮辱性文件的出现概率,即 trainCategory 中所有 1 的个数(0 1 相加即得 1 的个数)
# 条件概率
# (非)侮辱性单词在每个文件中出现的次数列表
# 比如说 p0Num = [1,3,12,....] 表示第 2 个文档中出现了 3 次非侮辱词汇
p0Num = np.zeros(numWords) # [0,0,0,.....] 非侮辱性单词在每个文件中出现的次数列表
p1Num = np.zeros(numWords) # [0,0,0,.....] 侮辱性单词出在每个文件中出现的次数列表
# (非)侮辱性单词在(非)侮辱性文档出现的总数
p0Denom = 0.0 # 0 非侮辱性词汇在所有非侮辱的文档的出现总数
p1Denom = 0.0 # 1 侮辱性词汇在所有侮辱性的文档的出现总数
for i in range(numTrainDocs):
# 是否是侮辱性文件
if trainCategory[i] == 1:
# 如果是侮辱性文件,对侮辱性文件的向量进行相加,表示在所有侮辱性文件中,去重词汇表中各个词汇出现的次数
p1Num += trainMatrix[i]
# 对向量中的所有元素进行求和,表示在所有侮辱性文件中,去重词汇表中所有词汇出现的次数之和
p1Denom += sum(trainMatrix[i])
else:
# 如果是非侮辱性文件,对非侮辱性文件的向量进行相加,表示在所有非侮辱性文件中,去重词汇表中各个词汇出现的次数
p0Num += trainMatrix[i]
# 对向量中的所有元素进行求和,表示在所有非侮辱性文件中去重词汇表中所有词汇出现的次数之和
p0Denom += sum(trainMatrix[i])
# 在类别 1 即侮辱性文档的条件下,去重词汇表中每个单词出现的概率
p1Vect = p1Num / p1Denom
# 在类别 0 即非侮辱性文档的条件下,去重词汇表中每个单词出现的概率
p0Vect = p0Num / p0Denom
return pAbusive, p0Vect, p1Vect
测试代码:
pAbusive, p0Vect, p1Vect = trainNB0(trainMat, classVec)
可以看到,基于极大似然估计的朴素贝叶斯算法的结果差强人意。
这是因为在利用基于极大似然估计的朴素贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,比如计算 P ( X 1 ( 1 ) ∣ Y = 1 ) ∗ P ( X 1 ( 2 ) ∣ Y = 1 ) ∗ P ( X 1 ( 3 ) ∣ Y = 1 ) ∗ . . . . . . P(X^{(1)}_1|Y=1) * P(X^{(2)}_1|Y=1) * P(X^{(3)}_1|Y=1) *...... P(X1(1)∣Y=1)∗P(X1(2)∣Y=1)∗P(X1(3)∣Y=1)∗......。如果其中一个概率值为 0,那么最后的乘积也为 0。为降低这种影响,接下来我们采用贝叶斯估计对上述算法做一下微小的修改
基于拉普拉斯平滑(λ = 1),在条件概率的计算公式的分子分母上分别加上 λ 和 S j λ S_jλ Sjλ, S j S_j Sj 表示分类的个数,此处只有两个分类,所以 S j = 2 S_j = 2 Sj=2。
即将条件概率和先验概率的分子初始化为 1,将分母初始化为 2 :
# (非)侮辱性单词在每个文件中出现的次数列表
# 比如说 p0Num = [1,3,12,....] 表示第 2 个文档中出现了 3 次非侮辱词汇
p0Num = np.ones(numWords) # [1,1,1,.....] 非侮辱性单词在每个文件中出现的次数列表
p1Num = np.ones(numWords) # [1,1,1,.....] 侮辱性单词出在每个文件中出现的次数列表
# (非)侮辱性单词在(非)侮辱性文档出现的总数
p0Denom = 2.0 # 0 非侮辱性词汇在所有非侮辱的文档的出现总数
p1Denom = 2.0 # 1 侮辱性词汇在所有侮辱性的文档的出现总数
OK,修改好之后我们再来运行一下:
p1Vect
中 第 11 个数值最大,即表示去重词汇表中第 11 个位置的单词即 stupid 是在侮辱性文档中出现概率最大的词:
然而,至此,我们的代码仍然不是完美的,还需要解决一个问题:下溢出问题
下溢出问题:这是由于太多很小的数相乘造成的。比如计算乘积 P ( X 1 ( 1 ) ∣ Y = 1 ) ∗ P ( X 1 ( 2 ) ∣ Y = 1 ) ∗ P ( X 1 ( 3 ) ∣ Y = 1 ) ∗ . . . . . . P(X^{(1)}_1|Y=1) * P(X^{(2)}_1|Y=1) * P(X^{(3)}_1|Y=1) *...... P(X1(1)∣Y=1)∗P(X1(2)∣Y=1)∗P(X1(3)∣Y=1)∗...... 时,由于大部分因子都非常小,所以程序会下溢出或者得到不正确的答案。( 用 Python 尝试相乘许多很小的数,最后四舍五入后会得到 0)。
一种解决办法是对乘积取自然对数。在代数中有 ln(A * B) = ln(A) + ln(B)
, 于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。
下图给出了函数 F(x)
与 ln(F(x))
的曲线。可以看出,它们在相同区域内同时增加或者减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。
✍ 继续修改代码如下:
p1Vect = np.log(p1Num / p1Denom)
p0Vect = np.log(p0Num / p0Denom)
当然,这样获取到的条件概率就是对数形式,所以在接下来的朴素贝叶斯分类器中我们也要为其做相应的修改。
测试修改后的代码:
OK,问题解决完毕,接下来就可以来构建完整的朴素贝叶斯分类器了
回顾一下朴素贝叶斯分类器:
我们将其改写为对数形式:
✍ 朴素贝叶斯分类器的代码如下:
# 朴素贝叶斯分类器
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
"""
Desc:
输入某篇文档的0-1序列 vec2Classify,输出该篇所属的类别(侮辱性文档 1,非侮辱性文档 0)
Args:
vec2Classify: 某篇文档的 0-1 序列
p0Vec:在类别 0 即非侮辱性文档的条件下,去重词汇表中每个单词出现的概率(对数形式)
p1Vec:在类别 1 即侮辱性文档的条件下,去重词汇表中每个单词出现的概率(对数形式)
pClass1: 该篇文档是侮辱性文件的概率(先验概率),注意转换成对数形式
Return:
类别 1/0
"""
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
测试分类器:
# 测试朴素贝叶斯分类器
def test(testDoc):
"""
Args:
testDoc: 测试文档
"""
# 1. 加载数据集
postingList, classVec = loadDataSet()
# 2. 创建去重词汇表
vocabList = createVocabList(postingList) # 去重后的词汇表
# 3. 创建每篇文档对应的 0-1 序列
trainMat = []
for doc in postingList:
trainMat.append(setOfWords2Vec(vocabList, doc))
# 4. 计算条件概率和先验概率
pAbusive, p0Vect, p1Vect = trainNB0(trainMat, classVec)
# 5. 朴素贝叶斯分类器
thisDoc = setOfWords2Vec(vocabList, testDoc) # 将测试文档转换成 0-1 序列
print(testDoc, 'classified as: ', classifyNB(thisDoc, p0Vect, p1Vect, pAbusive))