a38dc792877322648f295facd1a74dd9.png

1.1 模型简介

朴素贝叶斯法(naive Bayes)是一种基于贝叶斯定理和特征条件独立的分类算法.它的实现较为简单且学习预测的效率也很高, 所以是实际中应用非常广泛的一类模型.

4723091761cd829e67639b79681a2453.png
贝叶斯公式

1.2 基本假设

1.2.1 联合概率分布

在朴素贝叶斯法中, 假设输入空间

维向量的集合, 输出的预测空间
.

算法中使用的

是定义在输入空间
的随机向量,
是定义在输出预测空间
的随机向量.
的联合概率分布.

假设训练集

是由
独立同分布产生的.

1.2.2 条件独立性

朴素贝叶斯的核心即是计算

先验分布

条件概率分布

但是条件概率分布有指数级的参数, 假设每一个维度

上有可取值数量
,
有可取值数量
个, 那么需要估计的条件概率分布参数有
个.

所以在朴素贝叶斯法中, 一个重要的假设就是条件独立性, 每一维度的分布是独立于其他维度的, 即

. 这一假设使
朴素贝叶斯法变得简单许多, "朴素"两字也因此而得名, 但有时会牺牲一定的分类准确性.

1.2.3 后验概率最大化

朴素贝叶斯法的分类原则是选取后验概率最大的类别, 即给定当前数据点

, 寻找
. 这等价于期望风险的最小化.

假设采用的是0-1损失函数, 则根据条件期望, 期望风险函数为

所以针对每一个取值

, 分别进行期望风险最小化:

如此, 根据期望风险最小化的目标, 就可以推导出朴素贝叶斯所采用的后验概率最大化准则:


1.3 参数估计与算法

1.3.1 极大似然估计

在朴素贝叶斯算法中, 给定训练集

, 我们需要先行计算出所有相关的先验概率和条件概率, 才能对新给定的数据点
进行预测. 在计算概率时, 我们采取的是极大似然估计的方法, 根据已有的数据实际信息(具体的数据数目)来估计逼近概率分布.

先验概率

的极大似然估计是
,
. 即在所有已知的样本中,
这个类别占多少比例.

条件概率

的极大似然估计是
,
;
;

需要解释一下的是,

是第几个维度,
是在维度
上所取的值,
是类别变量
的取值. 这个条件概率的极大似然估计即是, 在所有类别变量
的样本里, 第
维度
取值为
的占多少比例.

1.3.2 朴素贝叶斯分类算法

算法: 朴素贝叶斯分类输入: 训练数据
, 新实例点
输出: 新实例点
的预测分类
1. 用极大似然估计, 计算逼近所有的先验概率
2. 用极大似然估计,计算逼近所有的条件概率
3. 对于新的实例点
,计算出每一个
,
4. 确定新实例点
的类别
5. 返回

1.4 优化---贝叶斯估计

之前我们在估计参数先验概率条件概率时, 有一个隐患没有提及, 那就是如果每一个类或值并没有在训练集

中出现过,那么可能对于某个
有某个维度的某个值的拟合概率为
, 那么如果新实例点在维度
取值为
,
朴素贝叶斯算法则永远不会对新实例点
预测
, 这是有失公允和导致准确率下降的一大因素, 使得
朴素贝叶斯法非常依赖于训练集
的"
数据完整性".

由此, 一个优化的算法---贝叶斯估计便可以帮助解决这个问题.

具体来说, 条件概率的贝叶斯估计是:

,

即是在随机变量各个取值的频数上加上一个

项. 当
时就是之前的
朴素贝叶斯估计, 常用有
称为
拉普拉斯平滑(Laplacian smoothing). 在这样加入平滑项上, 我们获得了保证:

且作为一个概率分布, 仍然有概率收敛性质:

同样的, 先验概率的贝叶斯估计是:


2.1 Python代码实现

# -*- coding: utf-8 -*-
import numpy as np
import time

class NaiveBayes:
    """
    算法 : 朴素贝叶斯
    主要步骤为: 
    1. 根据训练集数据和标签, 确定维度、特征的数量
    2. 根据训练集计算出 先验概率 和 条件概率
    3. 对测试集进行测试, 取后验概率最大化的标签, 符合期望风险最小化
    Default: lambda=1 采用贝叶斯平滑处理, 防止某标签或数据从未出现而使概率为0
    """
    
    def __init__(self, trainData, trainLabel, _lambda=1):
        """
        param: trainData [numpy ndarray] 训练集数据
        param: trainLabel [numpy 1darray] 训练集标签
        param: _lambda [int] 平滑项 采用 Default=1 拉普拉斯平滑
        """
        t0 = time.time()
        self.trainData = trainData
        self.trainLabel = trainLabel
        self._lambda = _lambda
        self.labelNum = len(np.unique(trainLabel)) # 可取的标签总数量
        self.featureNum = len(trainData[0]) # 特征的维度
        self.featureCount = [len(np.unique([trainData[i][k] for i in 
                            range(len(trainData))]))for k in range(self.featureNum)]# 特征的每个维度可取值的数量
        t1 = time.time()
        print("完成初始化: 耗时 {:.2f}s".format(t1-t0))
    
    def get_all_prob(self):
        """
        根据提供已有的训练数据集和标签集,
        构造拟合出 先验概率分布 和 条件概率分布
        
        @ret: None
        """
        trainData, trainLabel, t0 = self.trainData, self.trainLabel, time.time()
        labelNum, featureNum, maxFeatureCount, _lambda = self.labelNum, self.featureNum, np.max(self.featureCount), self._lambda
        Py = np.zeros((labelNum, 1)) # 初始化先验概率分布向量, 全部为0
        for i in range(labelNum): # 采用平滑化, 每一类的分子计数加一个lambda平滑, 分母加lambda*总标签数 
            Py[i] = (np.sum(trainLabel == i) + _lambda)/(len(trainLabel) + labelNum * _lambda)
        Py = np.log(Py) # 取对数化, 防止太多的低小数相乘而导致浮点数下溢floating point underflow
        Px_y = np.zeros((labelNum, featureNum, maxFeatureCount)) 
        # 初始化条件概率分布: 第一维labelNum--y取哪个标签
        #                   第二维featureNum--第几维度的数据
        #                   第三维maxFeatureCount--当前维度所能取几个值
        #
        # 先验概率分布计算
        for i in range(len(trainLabel)):
            x = trainData[i]
            label = trainLabel[i]
            for j in range(featureNum):
                Px_y[label][j][x[j]] += 1
        # 条件概率分布计算       
        for label in range(labelNum):
            for j in range(featureNum):
                value_count = list()
                for k in range(maxFeatureCount): # 计数:在标签取label, 第j维度取值为k的数据有多少个
                    value_count.append(Px_y[label][j][k])
                for k in range(maxFeatureCount): # 除法:加入平滑化后, 拟合条件概率分布
                    Px_y[label][j][k] = np.log((Px_y[label][j][k] + _lambda) / (np.sum(value_count) + _lambda * maxFeatureCount))
        t1 = time.time()
        print("完成先验/条件概率分布计算: 耗时 {:.2f}s".format(t1-t0))
        self.Py, self.Px_y = Py, Px_y 
    
    def predict(self,new_x):
        """
        朴素贝叶斯: 
        根据已计算拟合过了的先验概率和条件概率分布
        预测出新数据点new_x的类别
        采取后验概率最大化的准则, 理论上符合期望风险最小化的目标
        
        param: [lst/np.1darray] new_x 新的待预测的数据实例点
        @ret:  [int] new_x 的预测类别
        """
        Py, Px_y, featureNum, labelNum = self.Py, self.Px_y, self.featureNum, self.labelNum
        prob = [0.0] * labelNum # 初始化每个类别的预测概率
        for label in range(labelNum):
            curr_prob = 0.0
            for j in range(featureNum): # 因为概率分布已为Log对数化
                curr_prob += Px_y[label][j][new_x[j]] # 所以连乘变成了连加
            prob[label] = curr_prob + Py[label]
        return prob.index(max(prob)) # 返回后验概率最大化的类别为预测值
    
    def test(self, testData, testLabel):
        """
        对外界输入的测试数据集进行预测,
        并对比测试标签集, 得出测试准确率以及平均预测耗时
        
        param: [np.ndarry/lst[lst]] testData 测试数据集
        param: [np.1darray/lst] testLabel 测试标签集
        @ret: [float] accu 测试准确率
        """
        t0 = time.time()
        correct = 0
        for i in range(len(testLabel)):
            if self.predict(testData[i]) == testLabel[i]:
                correct += 1
        t1 = time.time()
        print("平均每次预测计算: 耗时{:.5f}s".format((t1-t0)/len(testData)))
        accu = correct / len(testData)
        print("测试成绩: {}/{} 准确率{:.2f}%".format(correct, len(testData), 100*accu))
        return accu

我们在Mnist手写数字集合上进行了测试:

数据集:Mnist
训练集数量:60000
测试集数量:10000
完成初始化: 耗时 26.60s
完成先验/条件概率分布计算: 耗时 56.30s
平均每次预测计算: 耗时0.00526s
测试成绩: 8433/10000 准确率84.33%

欢迎大家指出不足或错误, 进行提问和讨论.

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐