详解贝叶斯算法(二):朴素贝叶斯分类器的实现

详解贝叶斯(二):朴素贝叶斯分类器的实现

原文转载自:码海拾遗 - 详解贝叶斯(二):朴素贝叶斯分类器的实现

朴素贝叶斯分类器(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(AB)=P(B)P(BA)P(A)
在模型中的self.prior_prob变量其实就是先验概率 P ( A ) P(A) P(A),而似然度 P ( B ∣ A ) P(B|A) P(BA)就是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(AB)=P(B)P(BA)P(A)
在模型中的self.prior_prob变量其实就是先验概率 P ( A ) P(A) P(A),而似然度 P ( B ∣ A ) P(B|A) P(BA)就是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)}")
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值