说了这么久的思想和概念性的东西,我们终于可以开始叙述一个具体的机器学习算法—朴素贝叶斯。
在朴素贝叶斯这个名字中,
“朴素”二字对应着“独立性假设”这一个朴素的假设
“贝叶斯”则对应“后验概率最大化”这一贝叶斯学派的思想。
算法陈述与基本架构的搭建
朴素贝叶斯算法一个非常重要的基本假设称为独立性假设。
该假设从直观上来说是可以接受的,不过在实际任务中该假设一般而言会显得太牵强。
在朴素贝叶斯算法思想下,一般来说会衍生出以下三种不同的模型。
- 离散型朴素贝叶斯( MultinomialNB):所有维度的特征都是离散型随机变量。
- 连续型朴素贝叶斯( GaussianNB):所有维度的特征都是连续型随机变量。
- 混合型朴素贝叶斯( MergedNB):各个维度的特征有离散型也有连续型。
由浅入深,我们先用离散型朴素贝叶斯来说明一些普适性的概念,连续型和混合型的相关定义是类似的
从上述定义出发,可以利用上一节讲解的两种参数估计方法导出离散型朴素贝叶斯的算法
输入:训练数据集D={(x1,y1),…,(xn,yn)} 过程:(利用ML估计导出模型的具体参数) (1)计算先验概率p(y=ck)的极大似然估计; (2)计算条件概率的极大似然估计; 输出:(利用MAP估计进行决策) 朴素贝叶斯模型,能够估计数据的类别
由上述算法可以清晰地梳理出朴素贝叶斯算法背后的数学思想
- 使用极大似然估计导出模型的具体参数(先验概率、条件概率)
- 使用极大后验概率估计作为模型的决策(输出使得数据后验概率最大化的类别)
接下来我们在一个简单、虚拟的数据集上应用离散型朴素贝叶斯算法以加深对算法的理解,该数据集如下(1.0):
黄色,小,成人,用手打,不爆炸
黄色,小,成人,用脚踩,爆炸
黄色,小,小孩,用手打,不爆炸
黄色,小,小孩,用脚踩,不爆炸
黄色,大,成人,用手打,爆炸
黄色,大,成人,用脚踩,爆炸
黄色,大,小孩,用手打,不爆炸
黄色,大,小孩,用脚踩,爆炸
紫色,小,成人,用手打,不爆炸
紫色,小,小孩,用手打,不爆炸
紫色,大,成人,用脚踩,爆炸
紫色,大,小孩,用脚踩,爆炸
如果是下面这组数据,则需要加入平滑项(1.5):
黄色,小,成人,用手打,不爆炸
黄色,小,成人,用脚踩,爆炸
黄色,小,小孩,用手打,不爆炸
黄色,小,小孩,用脚踩,爆炸
黄色,小,小孩,用脚踩,爆炸
黄色,小,小孩,用脚踩,爆炸
黄色,大,成人,用手打,爆炸
黄色,大,成人,用脚踩,爆炸
黄色,大,小孩,用手打,不爆炸
紫色,小,成人,用手打,不爆炸
紫色,小,小孩,用手打,不爆炸
紫色,大,小孩,用手打,不爆炸
PS:需要加入平滑项(lambda=1时称作拉普拉斯平滑),这是最常见的,实现中也会优先默认使用,详情不再赘述。
考虑到代码重用和可拓展性,需要搭建
一个基本架构,它应该定义好三种模型都会用到的通用的功能,
例如:
- 定义获取训练集里类别先验概率的函数;
- 将核心训练步骤以外的训练步骤进行定义,其中核心训练步骤需要训练出一个决策
- 函数,该决策函数能够输出给定数据的后验概率;
- 利用决策函数定义预测函数和评估函数;
我们先来看看这个基本架构的基本框架:
1 #导入需要用到的库 2 import numpy as np 3 # 定义朴素贝叶斯模型的基类,方便以后的拓展 4 class NaiveBayes: 5 """ 6 初始化结构 7 self._x,self._y:记录训练集的变量 8 self._data:核心数组,存储实际使用的条件概率的相关信息 9 self._func:模型核心—决策函数,能够根据输入的x、y输出对应的后验概率 10 self._n_possibilities:记录各个维度特征取值个数的数组 11 self._labelled_x:记录按类别分开后的输入数据的数组 12 self._label_zip:记录类别相关信息的数组,视具体算法,定义会有所不同 13 self._cat_counter:核心数组,记录第i类数据的个数(cat是category的缩写) 14 self._con_counter:核心数组,用于记录数据条件概率的原始极大似然估计 15 self._con_counter[d][c][p]= (con是conditional的缩写) 16 self._label_dic:核心字典,用于记录数值化类别的转换关系 17 self._feat_dics:核心字典,用于记录数值化各维度特征(feat)时的转换关系 18 """ 19 def __init__(self): 20 self._x = self._y =None 21 self._data = self._func = None 22 self._n_possibilities = None 23 self._labelled_x = self._label_zip = None 24 self._cat_counter = self._con_counter = None 25 self._label_dic = self._feat_dics = None 26 27 # 重载 __getitem__运算符以避免定义大量property 28 def __getitem__(self, item): 29 if isinstance(item, str): 30 return getattr(self,"_" + item) 31 32 # 留下抽象方法让子类定义,这里的tar_idx 参数和self._tar_idx的意义一致 33 def feed_data(self,x,y,sample_weight=None): 34 pass 35 36 # 留下抽象方法让子类定义,这里的sample_weight参数代表着样本权重 37 #让模型支持输入样本权重,更多的是为了使模型能够应用在提升方法中 38 def feed_samle_weight(self,sample_weight=None): 39 pass
1 #定义计算先验概率的函数,lb就是各个估计中的平滑项lambda 2 #lb的默认值为1,即采用默认的拉普拉斯平滑 3 def get_prior_probability(self,lb=1): 4 return [(_c_num + lb) / (len(self._y) + lb * len(self._cat_counter)) 5 for _c_num in self.cat_counter] 6 7 #定义具有普适性的训练函数 8 def fit(self,x=None,y=None,sample_weight=None,lb=1): 9 #如果有传入的x,y,那么就用传入的数据 10 if x is not None and y is not None: 11 self.func_data(x,y,sample_weight) 12 #调用核心函数得到决策数 13 self._gunc = self._fit(lb) 14 15 #留下抽象核心算法让子类定义 16 def _fit(self,lb): 17 pass
以上是模型训练相关的过程,下面就是模型的预测和评估过程。先进行一个“朴素”的实现:
1 #定义预测单一样本的函数 2 #参数get_raw_result控制该函数是输出预测的类别还是输出相应的后验概率 3 #False则输出类别,True则输出后验概率 4 def predict_one(self,x,get_raw_result=False): 5 #在进行预测之前,要先把新的输入数据数值化 6 #如果输入的Numpy数组,要先将它转换成python数组 7 #因为python数组在数值化这个操作上更快 8 if isinstance(x,np.ndarray): 9 x=x.tolist() 10 #否则,对数组进行拷贝 11 else: 12 x=x[:] 13 #调用相关方法进行数值化,该方法随具体模型的不同而不同 14 x = self.transfer_x(x) 15 m_arg , m_probability = 0,0 16 #遍历各类别、找到能使后验概率最大化的类别 17 for i in range(len(self._cat_counter)): 18 p = self.func(x,i) ################################################ 19 if p>m_probability: 20 m_arg,m_probability = i,p 21 if not get_raw_result: 22 return self.label_dic[m_arg] 23 return m_probability 24 25 #定义预测多样本的函数,本质是不断调用上面定义的predict_one 函数 26 def predict(self,x,get_raw_result = False): 27 return np.array([self.predict_one(xx,get_raw_result) for xx in x]) 28 29 #定义能对新数据进行评估的方法,这里暂以简单地输出标准率作为演示 30 def evaluate(self,x,y): 31 y_pred = self.predict(x) 32 print('Acc: {:12.6} %'.format(100 * np.sum(y_pred == y) / len(y)))
注意:之所以称上述实现是“朴素的”,
是因为预测单一样本的函数只是在算法没有向量化时的一个临时产物。
在算法完成向量化后,模型就能进行批量预测,该函数就可以删去了。
至此,一个完整的朴素贝叶斯基类就定义完了,我们接下来要做的就是将那些留下来的抽象方法根据不同的需求进行补充定义。
v、