详解贝叶斯(二):朴素贝叶斯分类器的实现
原文转载自:码海拾遗 - 详解贝叶斯(二):朴素贝叶斯分类器的实现
朴素贝叶斯分类器(Naive Bayes Classifier)是一种基于贝叶斯定理的概率统计分类算法,它被广泛用于文本分类、垃圾邮件过滤、情感分析等领域。朴素贝叶斯分类器的"朴素"来源于其对特征之间的独立性的假设,即在给定类别的情况下,特征之间是相互独立的。尽管这个假设在实际情况中往往不成立,但朴素贝叶斯在实践中却往往表现得很好。
我们以周志华老师的西瓜书中的西瓜数据集为例,使用python实现一个朴素贝叶斯分类器。
色泽 | 根蒂 | 敲击 | 纹理 | 脐部 | 触感 | 好坏 |
---|---|---|---|---|---|---|
青绿 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 好瓜 |
乌黑 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 好瓜 |
乌黑 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 好瓜 |
青绿 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 好瓜 |
浅白 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 好瓜 |
青绿 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 软粘 | 好瓜 |
乌黑 | 稍蜷 | 浊响 | 稍糊 | 稍凹 | 软粘 | 好瓜 |
乌黑 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 硬滑 | 好瓜 |
乌黑 | 稍蜷 | 沉闷 | 稍糊 | 稍凹 | 硬滑 | 坏瓜 |
青绿 | 硬挺 | 清脆 | 清晰 | 平坦 | 软粘 | 坏瓜 |
浅白 | 硬挺 | 清脆 | 模糊 | 平坦 | 硬滑 | 坏瓜 |
浅白 | 蜷缩 | 浊响 | 模糊 | 平坦 | 软粘 | 坏瓜 |
青绿 | 稍蜷 | 浊响 | 稍糊 | 凹陷 | 硬滑 | 坏瓜 |
浅白 | 稍蜷 | 沉闷 | 稍糊 | 凹陷 | 硬滑 | 坏瓜 |
乌黑 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 软粘 | 坏瓜 |
浅白 | 蜷缩 | 浊响 | 模糊 | 平坦 | 硬滑 | 坏瓜 |
青绿 | 蜷缩 | 沉闷 | 稍糊 | 稍凹 | 硬滑 | 坏瓜 |
可以看到数据集里西瓜的特征有色泽、根蒂、敲击、纹理、脐部、触感等六个维度,每个维度都有三种或者两种取值,最后的标签有好瓜和坏瓜两种情况。
定义数据集:
dataset = np.array([
# 色泽,根蒂,敲击,纹理,脐部,触感, 好坏
['青绿', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '好瓜'],
# ...
['青绿', '蜷缩', '沉闷', '稍糊', '稍凹', '硬滑', '坏瓜']
])
features = {
'色泽': ['青绿', '乌黑', '浅白'],
'根蒂': ['蜷缩', '稍蜷', '硬挺'],
'敲击': ['浊响', '沉闷', '清脆'],
'纹理': ['清晰', '稍糊', '模糊'],
'脐部': ['凹陷', '稍凹', '平坦'],
'触感': ['硬滑', '软粘']
}
labels = ["好瓜", "坏瓜"]
将特征与标签拆分开来:
X = dataset[:, :6] #前6列为特征
Y = dataset[:, 6] #最后一列为标签
现在我们构造一个Bayes类,这个类的实例代表一个朴素贝叶斯分类器,其设计如下:
class Bayes:
def __init__(self, features: dict, labels: list) -> None:
pass # 初始化模型
def bayes_prob(self, arr):
pass # 计算贝叶斯概率
def train(self, X, Y):
pass # 训练模型
def predic(self, X):
pass # 模型推理
def show_params(self) -> str:
pass # 辅助函数,用来观察模型内部
首先我们实现一下构造函数,这个函数里要完成模型必要参数的初始化。那么模型需要一些什么参数呢?首先特征的维度、标签的种类肯定需要保存。贝叶斯公式里的核心,先验概率和似然度也需要保存。考虑到方方面面,最后构造函数设计如下:
def __init__(self, features: dict, labels: list) -> None:
self.features = features # 保存特征的维度、取值范围
self.labels = labels # 保存标签的类别
self.feature_classes = list(self.features.keys()) # 为了方便
self.condi_prob = {} # 保存条件概率/似然度的字典
self.prior_prob = {} # 保存先验概率的字典
# 保存计算概率用的训练样本统计信息,具体保存形式如下:
# {
# '好瓜':8,
# '坏瓜':9,
# '坏瓜.敲击.沉闷': 3,
# '好瓜.根蒂.稍蜷': 3,
# ...
# }
self._statistic = {}
self._samples = 0 # 计数
for i in self.labels:
# 先验概率的初始值认为各标签是等概率的
self.prior_prob[i] = 1 / len(self.labels)
# 每个标签初始化一个空的字典储存各标签下的条件概率
self.condi_prob[i] = {}
# 每个标签下的样本数清零
self._statistic[i] = 0
for j in self.feature_classes:
# i标签下j维度的的条件概率初始化
self.condi_prob[i][j] = {}
# 遍历j维度的取值范围
for k in self.features[j]:
# 初始化时假设j维度的每个特征等概率出现
self.condi_prob[i][j][k] = 1 / len(self.features[j])
# 'i.j.k'储存在i标签下,j维度的k取值,出现的次数,初始值为0
self._statistic['.'.join([i, j, k])] = 0
为了方便观察内部情况,设置一个辅助函数打印内部参数:
def show_params(self) -> str:
pprint(self.prior_prob)
pprint(self.condi_prob)
# pprint(self._statistic)
初始化完成后打印一下可以看到,目前的模型参数:
{'坏瓜': 0.5, '好瓜': 0.5}
{'坏瓜': {'敲击': {'沉闷': 0.3333333333333333,
'浊响': 0.3333333333333333,
'清脆': 0.3333333333333333},
'根蒂': {'硬挺': 0.3333333333333333,
'稍蜷': 0.3333333333333333,
'蜷缩': 0.3333333333333333},
'纹理': {'模糊': 0.3333333333333333,
'清晰': 0.3333333333333333,
'稍糊': 0.3333333333333333},
'脐部': {'凹陷': 0.3333333333333333,
'平坦': 0.3333333333333333,
'稍凹': 0.3333333333333333},
'色泽': {'乌黑': 0.3333333333333333,
'浅白': 0.3333333333333333,
'青绿': 0.3333333333333333},
'触感': {'硬滑': 0.5, '软粘': 0.5}},
'好瓜': {'敲击': {'沉闷': 0.3333333333333333,
'浊响': 0.3333333333333333,
'清脆': 0.3333333333333333},
'根蒂': {'硬挺': 0.3333333333333333,
'稍蜷': 0.3333333333333333,
'蜷缩': 0.3333333333333333},
'纹理': {'模糊': 0.3333333333333333,
'清晰': 0.3333333333333333,
'稍糊': 0.3333333333333333},
'脐部': {'凹陷': 0.3333333333333333,
'平坦': 0.3333333333333333,
'稍凹': 0.3333333333333333},
'色泽': {'乌黑': 0.3333333333333333,
'浅白': 0.3333333333333333,
'青绿': 0.3333333333333333},
'触感': {'硬滑': 0.5, '软粘': 0.5}}}
接下来需要完成最核心的训练函数。在编写训练函数前,再回顾一下贝叶斯公式:
P
(
A
∣
B
)
=
P
(
B
∣
A
)
P
(
A
)
P
(
B
)
P(A|B) = \frac{P(B|A)P(A)}{P(B)}
P(A∣B)=P(B)P(B∣A)P(A)
在模型中的self.prior_prob变量其实就是先验概率
P
(
A
)
P(A)
P(A),而似然度
P
(
B
∣
A
)
P(B|A)
P(B∣A)就是self.condi_prob。推理的过程就是已知特征B的情况下,求标签A的概率。
取一条数据代入贝叶斯公式就是:
P
(
好瓜
∣
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
)
=
P
(
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
∣
好瓜
)
P
(
好瓜
)
P
(
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
)
P
(
坏瓜
∣
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
)
=
P
(
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
∣
坏瓜
)
P
(
坏瓜
)
P
(
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
)
P(好瓜|青绿,蜷缩,浊响,清晰,凹陷,硬滑) = \frac{P(青绿,蜷缩,浊响,清晰,凹陷,硬滑|好瓜)P(好瓜)}{P(青绿,蜷缩,浊响,清晰,凹陷,硬滑)} \\ P(坏瓜|青绿,蜷缩,浊响,清晰,凹陷,硬滑) = \frac{P(青绿,蜷缩,浊响,清晰,凹陷,硬滑|坏瓜)P(坏瓜)}{P(青绿,蜷缩,浊响,清晰,凹陷,硬滑)}
P(好瓜∣青绿,蜷缩,浊响,清晰,凹陷,硬滑)=P(青绿,蜷缩,浊响,清晰,凹陷,硬滑)P(青绿,蜷缩,浊响,清晰,凹陷,硬滑∣好瓜)P(好瓜)P(坏瓜∣青绿,蜷缩,浊响,清晰,凹陷,硬滑)=P(青绿,蜷缩,浊响,清晰,凹陷,硬滑)P(青绿,蜷缩,浊响,清晰,凹陷,硬滑∣坏瓜)P(坏瓜)
对于特征为[青绿,蜷缩,浊响,清晰,凹陷,硬滑]时,计算好瓜还是坏瓜的概率时分母是一样的,因此可以直接不计算分母,最后直接比较分子的大小输出结果。
公式展开后, P ( 青绿 , 蜷缩 , 浊响 , 清晰 , 凹陷 , 硬滑 ∣ 好瓜 ) P(青绿,蜷缩,浊响,清晰,凹陷,硬滑|好瓜) P(青绿,蜷缩,浊响,清晰,凹陷,硬滑∣好瓜)、 P ( 好瓜 ) P(好瓜) P(好瓜)都应该怎么计算呢?事实是无法计算!虽然无法计算,但是当有很多样本可以观察时,可以统计出这两者的近似值,这个过程就是机器学习的过程。
其中
P
(
好瓜
)
P(好瓜)
P(好瓜)和
P
(
坏瓜
)
P(坏瓜)
P(坏瓜)只要统计训练数据中,对应标签出现的比例即可。而
P
(
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
∣
好瓜
)
P(青绿,蜷缩,浊响,清晰,凹陷,硬滑|好瓜)
P(青绿,蜷缩,浊响,清晰,凹陷,硬滑∣好瓜)的计算就要用到朴素贝叶斯的朴素性假设,即在给定类别的情况下,特征之间是相互独立的。既然每个特征可以看作是一个个独立事件,那整体的概率就等于一个个独立事件的概率的乘积:
P
(
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
∣
好瓜
)
=
P
(
青绿
∣
好瓜
)
×
P
(
蜷缩
∣
好瓜
)
×
P
(
浊响
∣
好瓜
)
×
P
(
清晰
∣
好瓜
)
×
P
(
凹陷
∣
好瓜
)
×
P
(
硬滑
∣
好瓜
)
P(青绿,蜷缩,浊响,清晰,凹陷,硬滑|好瓜) = \\ P(青绿|好瓜) \times P(蜷缩|好瓜) \times P(浊响|好瓜) \times P(清晰|好瓜) \times P(凹陷|好瓜) \times P(硬滑|好瓜)
P(青绿,蜷缩,浊响,清晰,凹陷,硬滑∣好瓜)=P(青绿∣好瓜)×P(蜷缩∣好瓜)×P(浊响∣好瓜)×P(清晰∣好瓜)×P(凹陷∣好瓜)×P(硬滑∣好瓜)
而
P
(
青绿
∣
好瓜
)
P(青绿|好瓜)
P(青绿∣好瓜)的概率只要简单统计所有好瓜样本中,色泽为青绿的样本数量即可。
在实际操作中,可能会出现某个特征的某个取值一次都没出现的情况,比如训练数据里没有出现色泽为浅白的情况。这会导致其中一项特征的条件概率变为0,乘起来后全部为0,导致计算出现异常。为了解决这个问题可以引入拉普拉斯平滑。简单的讲,也就是分子直接加上 λ \lambda λ,分母加上 λ × N \lambda \times N λ×N,这里N是特征可取值的个数, λ \lambda λ是拉普拉斯平滑系数,一般取1。这种方法避免了零概率问题,在数据集较大时,也几乎不会对准确度产生影响。
经过上面的分析可以写出训练函数的代码如下:
def train(self, X, Y):
rows, cols = X.shape
for i in range(rows):
# 好瓜、坏瓜计数
self._statistic[Y[i]] += 1
for j in range(cols):
# 这里为Y[i]为好瓜坏瓜
# self.feature_classes[j]为色泽、根蒂等特征维度
# X[i, j]就是上面的某个特征维度的具体取值
# 比如 self._statistic['好瓜.色泽.青绿'] +=1
self._statistic['.'.join(
[Y[i], self.feature_classes[j], X[i, j]])] += 1
# 总的样本数量
self._samples += 1
# 统计完了开始算概率
for i in self.labels:
# 好瓜坏瓜的数量 / 总的样本的数量
# 这里分子加了1,分母加了2 (len(self.labels))是因为拉普拉斯平滑
self.prior_prob[i] = (self._statistic[i] + 1) / (self._samples+len(self.labels))
for j in self.feature_classes:
for k in self.features[j]:
# 分子:好瓜或坏瓜里面色泽、根蒂等特征维度里取值为K的样本的数量
# 分母:好瓜或者坏瓜的数量
# 分子分母额外加的数是因为拉普拉斯平滑
self.condi_prob[i][j][k] = (self._statistic['.'.join([i, j, k])] + 1) / (self._statistic[i] + len(self.features[j]))
用西瓜书中的数据集训练一下,再调用打印内部参数的函数可以得到下面的结果:
{'坏瓜': 0.5263157894736842, '好瓜': 0.47368421052631576}
{'坏瓜': {'敲击': {'沉闷': 0.3333333333333333, '浊响': 0.4166666666666667, '清脆': 0.25},
'根蒂': {'硬挺': 0.25, '稍蜷': 0.4166666666666667, '蜷缩': 0.3333333333333333},
'纹理': {'模糊': 0.3333333333333333, '清晰': 0.25, '稍糊': 0.4166666666666667},
'脐部': {'凹陷': 0.25, '平坦': 0.4166666666666667, '稍凹': 0.3333333333333333},
'色泽': {'乌黑': 0.25, '浅白': 0.4166666666666667, '青绿': 0.3333333333333333},
'触感': {'硬滑': 0.6363636363636364, '软粘': 0.36363636363636365}},
'好瓜': {'敲击': {'沉闷': 0.2727272727272727,'浊响': 0.6363636363636364,'清脆': 0.09090909090909091},
'根蒂': {'硬挺': 0.09090909090909091,'稍蜷': 0.36363636363636365,'蜷缩': 0.5454545454545454},
'纹理': {'模糊': 0.09090909090909091,'清晰': 0.7272727272727273,'稍糊': 0.18181818181818182},
'脐部': {'凹陷': 0.5454545454545454,'平坦': 0.09090909090909091,'稍凹': 0.36363636363636365},
'色泽': {'乌黑': 0.45454545454545453,'浅白': 0.18181818181818182,'青绿': 0.36363636363636365},
'触感': {'硬滑': 0.7, '软粘': 0.3}}}
接下来是预测函数,前面已经展开了贝叶斯公式,可以看出计算时只要将各个维度的概率相乘即可。而贝叶斯公式分数线下方的内容完全可以忽略不计算。
朴素贝叶斯分类器(Naive Bayes Classifier)是一种基于贝叶斯定理的概率统计分类算法,它被广泛用于文本分类、垃圾邮件过滤、情感分析等领域。朴素贝叶斯分类器的"朴素"来源于其对特征之间的独立性的假设,即在给定类别的情况下,特征之间是相互独立的。尽管这个假设在实际情况中往往不成立,但朴素贝叶斯在实践中却往往表现得很好。
我们以周志华老师的西瓜书中的西瓜数据集为例,使用python实现一个朴素贝叶斯分类器。
| 色泽 | 根蒂 | 敲击 | 纹理 | 脐部 | 触感 | 好坏 |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| 青绿 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 好瓜 |
| 乌黑 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 好瓜 |
| 乌黑 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 好瓜 |
| 青绿 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 好瓜 |
| 浅白 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 好瓜 |
| 青绿 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 软粘 | 好瓜 |
| 乌黑 | 稍蜷 | 浊响 | 稍糊 | 稍凹 | 软粘 | 好瓜 |
| 乌黑 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 硬滑 | 好瓜 |
| 乌黑 | 稍蜷 | 沉闷 | 稍糊 | 稍凹 | 硬滑 | 坏瓜 |
| 青绿 | 硬挺 | 清脆 | 清晰 | 平坦 | 软粘 | 坏瓜 |
| 浅白 | 硬挺 | 清脆 | 模糊 | 平坦 | 硬滑 | 坏瓜 |
| 浅白 | 蜷缩 | 浊响 | 模糊 | 平坦 | 软粘 | 坏瓜 |
| 青绿 | 稍蜷 | 浊响 | 稍糊 | 凹陷 | 硬滑 | 坏瓜 |
| 浅白 | 稍蜷 | 沉闷 | 稍糊 | 凹陷 | 硬滑 | 坏瓜 |
| 乌黑 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 软粘 | 坏瓜 |
| 浅白 | 蜷缩 | 浊响 | 模糊 | 平坦 | 硬滑 | 坏瓜 |
| 青绿 | 蜷缩 | 沉闷 | 稍糊 | 稍凹 | 硬滑 | 坏瓜 |
可以看到数据集里西瓜的特征有色泽、根蒂、敲击、纹理、脐部、触感等六个维度,每个维度都有三种或者两种取值,最后的标签有好瓜和坏瓜两种情况。
定义数据集:
```python
dataset = np.array([
# 色泽,根蒂,敲击,纹理,脐部,触感, 好坏
['青绿', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '好瓜'],
# ...
['青绿', '蜷缩', '沉闷', '稍糊', '稍凹', '硬滑', '坏瓜']
])
features = {
'色泽': ['青绿', '乌黑', '浅白'],
'根蒂': ['蜷缩', '稍蜷', '硬挺'],
'敲击': ['浊响', '沉闷', '清脆'],
'纹理': ['清晰', '稍糊', '模糊'],
'脐部': ['凹陷', '稍凹', '平坦'],
'触感': ['硬滑', '软粘']
}
labels = ["好瓜", "坏瓜"]
将特征与标签拆分开来:
X = dataset[:, :6] #前6列为特征
Y = dataset[:, 6] #最后一列为标签
现在我们构造一个Bayes类,这个类的实例代表一个朴素贝叶斯分类器,其设计如下:
class Bayes:
def __init__(self, features: dict, labels: list) -> None:
pass # 初始化模型
def bayes_prob(self, arr):
pass # 计算贝叶斯概率
def train(self, X, Y):
pass # 训练模型
def predic(self, X):
pass # 模型推理
def show_params(self) -> str:
pass # 辅助函数,用来观察模型内部
首先我们实现一下构造函数,这个函数里要完成模型必要参数的初始化。那么模型需要一些什么参数呢?首先特征的维度、标签的种类肯定需要保存。贝叶斯公式里的核心,先验概率和似然度也需要保存。考虑到方方面面,最后构造函数设计如下:
def __init__(self, features: dict, labels: list) -> None:
self.features = features # 保存特征的维度、取值范围
self.labels = labels # 保存标签的类别
self.feature_classes = list(self.features.keys()) # 为了方便
self.condi_prob = {} # 保存条件概率/似然度的字典
self.prior_prob = {} # 保存先验概率的字典
# 保存计算概率用的训练样本统计信息,具体保存形式如下:
# {
# '好瓜':8,
# '坏瓜':9,
# '坏瓜.敲击.沉闷': 3,
# '好瓜.根蒂.稍蜷': 3,
# ...
# }
self._statistic = {}
self._samples = 0 # 计数
for i in self.labels:
# 先验概率的初始值认为各标签是等概率的
self.prior_prob[i] = 1 / len(self.labels)
# 每个标签初始化一个空的字典储存各标签下的条件概率
self.condi_prob[i] = {}
# 每个标签下的样本数清零
self._statistic[i] = 0
for j in self.feature_classes:
# i标签下j维度的的条件概率初始化
self.condi_prob[i][j] = {}
# 遍历j维度的取值范围
for k in self.features[j]:
# 初始化时假设j维度的每个特征等概率出现
self.condi_prob[i][j][k] = 1 / len(self.features[j])
# 'i.j.k'储存在i标签下,j维度的k取值,出现的次数,初始值为0
self._statistic['.'.join([i, j, k])] = 0
为了方便观察内部情况,设置一个辅助函数打印内部参数:
def show_params(self) -> str:
pprint(self.prior_prob)
pprint(self.condi_prob)
# pprint(self._statistic)
初始化完成后打印一下可以看到,目前的模型参数:
{'坏瓜': 0.5, '好瓜': 0.5}
{'坏瓜': {'敲击': {'沉闷': 0.3333333333333333,
'浊响': 0.3333333333333333,
'清脆': 0.3333333333333333},
'根蒂': {'硬挺': 0.3333333333333333,
'稍蜷': 0.3333333333333333,
'蜷缩': 0.3333333333333333},
'纹理': {'模糊': 0.3333333333333333,
'清晰': 0.3333333333333333,
'稍糊': 0.3333333333333333},
'脐部': {'凹陷': 0.3333333333333333,
'平坦': 0.3333333333333333,
'稍凹': 0.3333333333333333},
'色泽': {'乌黑': 0.3333333333333333,
'浅白': 0.3333333333333333,
'青绿': 0.3333333333333333},
'触感': {'硬滑': 0.5, '软粘': 0.5}},
'好瓜': {'敲击': {'沉闷': 0.3333333333333333,
'浊响': 0.3333333333333333,
'清脆': 0.3333333333333333},
'根蒂': {'硬挺': 0.3333333333333333,
'稍蜷': 0.3333333333333333,
'蜷缩': 0.3333333333333333},
'纹理': {'模糊': 0.3333333333333333,
'清晰': 0.3333333333333333,
'稍糊': 0.3333333333333333},
'脐部': {'凹陷': 0.3333333333333333,
'平坦': 0.3333333333333333,
'稍凹': 0.3333333333333333},
'色泽': {'乌黑': 0.3333333333333333,
'浅白': 0.3333333333333333,
'青绿': 0.3333333333333333},
'触感': {'硬滑': 0.5, '软粘': 0.5}}}
接下来需要完成最核心的训练函数。在编写训练函数前,再回顾一下贝叶斯公式:
P
(
A
∣
B
)
=
P
(
B
∣
A
)
P
(
A
)
P
(
B
)
P(A|B) = \frac{P(B|A)P(A)}{P(B)}
P(A∣B)=P(B)P(B∣A)P(A)
在模型中的self.prior_prob变量其实就是先验概率
P
(
A
)
P(A)
P(A),而似然度
P
(
B
∣
A
)
P(B|A)
P(B∣A)就是self.condi_prob。推理的过程就是已知特征B的情况下,求标签A的概率。
取一条数据代入贝叶斯公式就是:
P
(
好瓜
∣
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
)
=
P
(
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
∣
好瓜
)
P
(
好瓜
)
P
(
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
)
P
(
坏瓜
∣
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
)
=
P
(
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
∣
坏瓜
)
P
(
坏瓜
)
P
(
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
)
P(好瓜|青绿,蜷缩,浊响,清晰,凹陷,硬滑) = \frac{P(青绿,蜷缩,浊响,清晰,凹陷,硬滑|好瓜)P(好瓜)}{P(青绿,蜷缩,浊响,清晰,凹陷,硬滑)} \\ P(坏瓜|青绿,蜷缩,浊响,清晰,凹陷,硬滑) = \frac{P(青绿,蜷缩,浊响,清晰,凹陷,硬滑|坏瓜)P(坏瓜)}{P(青绿,蜷缩,浊响,清晰,凹陷,硬滑)}
P(好瓜∣青绿,蜷缩,浊响,清晰,凹陷,硬滑)=P(青绿,蜷缩,浊响,清晰,凹陷,硬滑)P(青绿,蜷缩,浊响,清晰,凹陷,硬滑∣好瓜)P(好瓜)P(坏瓜∣青绿,蜷缩,浊响,清晰,凹陷,硬滑)=P(青绿,蜷缩,浊响,清晰,凹陷,硬滑)P(青绿,蜷缩,浊响,清晰,凹陷,硬滑∣坏瓜)P(坏瓜)
对于特征为[青绿,蜷缩,浊响,清晰,凹陷,硬滑]时,计算好瓜还是坏瓜的概率时分母是一样的,因此可以直接不计算分母,最后直接比较分子的大小输出结果。
公式展开后, P ( 青绿 , 蜷缩 , 浊响 , 清晰 , 凹陷 , 硬滑 ∣ 好瓜 ) P(青绿,蜷缩,浊响,清晰,凹陷,硬滑|好瓜) P(青绿,蜷缩,浊响,清晰,凹陷,硬滑∣好瓜)、 P ( 好瓜 ) P(好瓜) P(好瓜)都应该怎么计算呢?事实是无法计算!虽然无法计算,但是当有很多样本可以观察时,可以统计出这两者的近似值,这个过程就是机器学习的过程。
其中
P
(
好瓜
)
P(好瓜)
P(好瓜)和
P
(
坏瓜
)
P(坏瓜)
P(坏瓜)只要统计训练数据中,对应标签出现的比例即可。而
P
(
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
∣
好瓜
)
P(青绿,蜷缩,浊响,清晰,凹陷,硬滑|好瓜)
P(青绿,蜷缩,浊响,清晰,凹陷,硬滑∣好瓜)的计算就要用到朴素贝叶斯的朴素性假设,即在给定类别的情况下,特征之间是相互独立的。既然每个特征可以看作是一个个独立事件,那整体的概率就等于一个个独立事件的概率的乘积:
P
(
青绿
,
蜷缩
,
浊响
,
清晰
,
凹陷
,
硬滑
∣
好瓜
)
=
P
(
青绿
∣
好瓜
)
×
P
(
蜷缩
∣
好瓜
)
×
P
(
浊响
∣
好瓜
)
×
P
(
清晰
∣
好瓜
)
×
P
(
凹陷
∣
好瓜
)
×
P
(
硬滑
∣
好瓜
)
P(青绿,蜷缩,浊响,清晰,凹陷,硬滑|好瓜) = \\ P(青绿|好瓜) \times P(蜷缩|好瓜) \times P(浊响|好瓜) \times P(清晰|好瓜) \times P(凹陷|好瓜) \times P(硬滑|好瓜)
P(青绿,蜷缩,浊响,清晰,凹陷,硬滑∣好瓜)=P(青绿∣好瓜)×P(蜷缩∣好瓜)×P(浊响∣好瓜)×P(清晰∣好瓜)×P(凹陷∣好瓜)×P(硬滑∣好瓜)
而
P
(
青绿
∣
好瓜
)
P(青绿|好瓜)
P(青绿∣好瓜)的概率只要简单统计所有好瓜样本中,色泽为青绿的样本数量即可。
在实际操作中,可能会出现某个特征的某个取值一次都没出现的情况,比如训练数据里没有出现色泽为浅白的情况。这会导致其中一项特征的条件概率变为0,乘起来后全部为0,导致计算出现异常。为了解决这个问题可以引入拉普拉斯平滑。简单的讲,也就是分子直接加上 λ \lambda λ,分母加上 λ × N \lambda \times N λ×N,这里N是特征可取值的个数, λ \lambda λ是拉普拉斯平滑系数,一般取1。这种方法避免了零概率问题,在数据集较大时,也几乎不会对准确度产生影响。
经过上面的分析可以写出训练函数的代码如下:
def train(self, X, Y):
rows, cols = X.shape
for i in range(rows):
# 好瓜、坏瓜计数
self._statistic[Y[i]] += 1
for j in range(cols):
# 这里为Y[i]为好瓜坏瓜
# self.feature_classes[j]为色泽、根蒂等特征维度
# X[i, j]就是上面的某个特征维度的具体取值
# 比如 self._statistic['好瓜.色泽.青绿'] +=1
self._statistic['.'.join(
[Y[i], self.feature_classes[j], X[i, j]])] += 1
# 总的样本数量
self._samples += 1
# 统计完了开始算概率
for i in self.labels:
# 好瓜坏瓜的数量 / 总的样本的数量
# 这里分子加了1,分母加了2 (len(self.labels))是因为拉普拉斯平滑
self.prior_prob[i] = (self._statistic[i] + 1) / (self._samples+len(self.labels))
for j in self.feature_classes:
for k in self.features[j]:
# 分子:好瓜或坏瓜里面色泽、根蒂等特征维度里取值为K的样本的数量
# 分母:好瓜或者坏瓜的数量
# 分子分母额外加的数是因为拉普拉斯平滑
self.condi_prob[i][j][k] = (self._statistic['.'.join([i, j, k])] + 1) / (self._statistic[i] + len(self.features[j]))
用西瓜书中的数据集训练一下,再调用打印内部参数的函数可以得到下面的结果:
{'坏瓜': 0.5263157894736842, '好瓜': 0.47368421052631576}
{'坏瓜': {'敲击': {'沉闷': 0.3333333333333333, '浊响': 0.4166666666666667, '清脆': 0.25},
'根蒂': {'硬挺': 0.25, '稍蜷': 0.4166666666666667, '蜷缩': 0.3333333333333333},
'纹理': {'模糊': 0.3333333333333333, '清晰': 0.25, '稍糊': 0.4166666666666667},
'脐部': {'凹陷': 0.25, '平坦': 0.4166666666666667, '稍凹': 0.3333333333333333},
'色泽': {'乌黑': 0.25, '浅白': 0.4166666666666667, '青绿': 0.3333333333333333},
'触感': {'硬滑': 0.6363636363636364, '软粘': 0.36363636363636365}},
'好瓜': {'敲击': {'沉闷': 0.2727272727272727,'浊响': 0.6363636363636364,'清脆': 0.09090909090909091},
'根蒂': {'硬挺': 0.09090909090909091,'稍蜷': 0.36363636363636365,'蜷缩': 0.5454545454545454},
'纹理': {'模糊': 0.09090909090909091,'清晰': 0.7272727272727273,'稍糊': 0.18181818181818182},
'脐部': {'凹陷': 0.5454545454545454,'平坦': 0.09090909090909091,'稍凹': 0.36363636363636365},
'色泽': {'乌黑': 0.45454545454545453,'浅白': 0.18181818181818182,'青绿': 0.36363636363636365},
'触感': {'硬滑': 0.7, '软粘': 0.3}}}
接下来是预测函数,前面已经展开了贝叶斯公式,可以看出计算时只要将各个维度的概率相乘即可。而贝叶斯公式分数线下方的内容完全可以忽略不计算。
# 输入一个特征,输出每种标签的概率
def bayes_prob(self, arr):
plist = []
for i in self.labels:
idx = 0
# 先验概率P(A)
p = self.prior_prob[i]
# 各维度的条件概率连乘
for j in self.feature_classes:
p *= self.condi_prob[i][j][arr[idx]]
idx += 1
# 算完一个标签的概率
plist.append(p)
return plist
# 返回预测标签
def predic(self, X):
rows, cols = X.shape
res = []
for i in range(rows):
# 对每个样本计算各标签概率
y = self.bayes_prob(X[i])
res.append(y)
# 找出概率最大的标签的索引值
res = np.argmax(res, axis=1)
# 转换为标签
res = [self.labels[x] for x in res]
return np.array(res)
直接那训练的数据去跑预测结果如下:
原始:['好瓜' '好瓜' '好瓜' '好瓜' '好瓜' '好瓜' '好瓜' '好瓜' '坏瓜' '坏瓜' '坏瓜' '坏瓜' '坏瓜' '坏瓜'
'坏瓜' '坏瓜' '坏瓜']
预测:['好瓜' '好瓜' '好瓜' '好瓜' '好瓜' '好瓜' '坏瓜' '好瓜' '坏瓜' '坏瓜' '坏瓜' '坏瓜' '好瓜' '坏瓜'
'好瓜' '坏瓜' '坏瓜']
准确率:0.8235294117647058
完整代码如下:
from pprint import pprint
import numpy as np
dataset = np.array([
# 色泽,根蒂,敲击,纹理,脐部,触感, 好坏
['青绿', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '好瓜'],
['乌黑', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑', '好瓜'],
['乌黑', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '好瓜'],
['青绿', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑', '好瓜'],
['浅白', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '好瓜'],
['青绿', '稍蜷', '浊响', '清晰', '稍凹', '软粘', '好瓜'],
['乌黑', '稍蜷', '浊响', '稍糊', '稍凹', '软粘', '好瓜'],
['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '硬滑', '好瓜'],
['乌黑', '稍蜷', '沉闷', '稍糊', '稍凹', '硬滑', '坏瓜'],
['青绿', '硬挺', '清脆', '清晰', '平坦', '软粘', '坏瓜'],
['浅白', '硬挺', '清脆', '模糊', '平坦', '硬滑', '坏瓜'],
['浅白', '蜷缩', '浊响', '模糊', '平坦', '软粘', '坏瓜'],
['青绿', '稍蜷', '浊响', '稍糊', '凹陷', '硬滑', '坏瓜'],
['浅白', '稍蜷', '沉闷', '稍糊', '凹陷', '硬滑', '坏瓜'],
['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '软粘', '坏瓜'],
['浅白', '蜷缩', '浊响', '模糊', '平坦', '硬滑', '坏瓜'],
['青绿', '蜷缩', '沉闷', '稍糊', '稍凹', '硬滑', '坏瓜']
])
features = {
'色泽': ['青绿', '乌黑', '浅白'],
'根蒂': ['蜷缩', '稍蜷', '硬挺'],
'敲击': ['浊响', '沉闷', '清脆'],
'纹理': ['清晰', '稍糊', '模糊'],
'脐部': ['凹陷', '稍凹', '平坦'],
'触感': ['硬滑', '软粘']
}
labels = ["好瓜", "坏瓜"]
class Bayes:
def __init__(self, features: dict, labels: list) -> None:
self.features = features
self.labels = labels
self.feature_classes = list(self.features.keys())
self.condi_prob = {}
self.prior_prob = {}
self._statistic = {}
self._samples = 0
for i in self.labels:
self.prior_prob[i] = 1 / len(self.labels)
self.condi_prob[i] = {}
self._statistic[i] = 0
for j in self.feature_classes:
self.condi_prob[i][j] = {}
for k in self.features[j]:
self.condi_prob[i][j][k] = 1 / len(self.features[j])
self._statistic['.'.join([i, j, k])] = 0
def bayes_prob(self, arr):
plist = []
for i in self.labels:
idx = 0
p = self.prior_prob[i]
for j in self.feature_classes:
p *= self.condi_prob[i][j][arr[idx]]
idx += 1
plist.append(p)
return plist
def train(self, X, Y):
rows, cols = X.shape
for i in range(rows):
self._statistic[Y[i]] += 1
for j in range(cols):
self._statistic['.'.join(
[Y[i], self.feature_classes[j], X[i, j]])] += 1
self._samples += 1
for i in self.labels:
self.prior_prob[i] = (self._statistic[i] + 1) / (self._samples+len(self.labels))
for j in self.feature_classes:
for k in self.features[j]:
self.condi_prob[i][j][k] = (self._statistic['.'.join([i, j, k])] + 1) / (self._statistic[i] + len(self.features[j]))
def predic(self, X):
rows, cols = X.shape
res = []
for i in range(rows):
y = self.bayes_prob(X[i])
res.append(y)
res = np.argmax(res, axis=1)
res = [self.labels[x] for x in res]
return np.array(res)
def show_params(self) -> str:
pprint(self.prior_prob)
pprint(self.condi_prob , sort_dicts=False)
# pprint(self._statistic)
if __name__ == "__main__":
X = dataset[:, :6]
Y = dataset[:, 6]
print(X)
print(Y)
clf = Bayes(features, labels)
clf.show_params()
clf.train(X, Y)
clf.show_params()
Yp = clf.predic(X)
print(f"原始:{Y}\n预测:{Yp}\n准确率:{np.sum(Y==Yp)/len(Y)}")