基于贝叶斯推断的分类模型(代码篇)|机器学习你会遇到的“坑”
如果你已经仔细阅读了昨天的《基于贝叶斯推断的分类模型(理论篇)》,就会清楚地知道朴素贝叶斯是基于贝叶斯推断的生成式模型,假设l是我们的类别标记,x是我们的样本,我们为了获得样本类别的后验概率,根据贝叶斯定理,我们要计算的不仅是类条件概率P(x|l),还需要计算先验概率P(l):
朴素贝叶斯引入的属性独立性假设,使得先验概率变得简单可计算,因为在属性独立性假设下,我们会发现:
也就是说,类条件概率等于属性条件概率的连乘。在具体实现朴素贝叶斯的过程中,我们对条件概率和先验概率的估计会变成基于大数定理的计数问题。
我们首先要做的是对数据本身进行分析处理,用类别的频率比作为先验概率,将每一个属性在不同类下的条件概率作为条件概率,然后对新样本的后验概率进行估计。
我们以IRIS数据为例来展现整个过程,IRIS数据有150个样本,总共有三类:Setosa、versicolor、virginica,我们首先要计算类的先验概率,实际过程就是我们类别的比例:
from sklearn.datasets import load_iris
from collections import Counter
data=load_iris()
X=data.data
y=data.target
d=Counter(y)
P1=d[0]/len(y)
P2=d[1]/len(y)
P3=d[2]/len(y)
我们就会得到每个类别的先验概率:
P(setosa)
P(versicolor)
P(virginica)
0.3333
0.3333
0.3333
可以发现三种类别是均匀的,每个类别的样本数相等,均为50个。接下来,我们会注意到IRIS数据有4个属性:sepal length、speal width、petal length、petal width。我们要对每个属性的类条件概率进行估计,所谓的条件概率就是在某一类下某个属性出现的概率,实际的计算中,我们在同一类下,用带有属性A的样本除以该类的样本总数来得到属性A的类条件概率:
......
#计算sepal length在setosa类别下的条件概率,并将其保存为一个字典
D_sepallength_setosa={}
X_setosa=X[y==0]
d=Counter(X_setosa[:,0])#对sapal length的属性取值进行计数
for k,v in d.items():
D_sepallength_setosa[k]=v/X_setosa.shape[0]
......
就可以得到在setosa类别下,sepal length属性取值的条件概率:
In [1]: D_sepallength_setosa
Out[1]:
{4.2999999999999998: 0.02,
4.4000000000000004: 0.06,
4.5: 0.02,
4.5999999999999996: 0.08,
4.7000000000000002: 0.04,
4.7999999999999998: 0.1,
4.9000000000000004: 0.08,
5.0: 0.16,
5.0999999999999996: 0.16,
5.2000000000000002: 0.06,
5.2999999999999998: 0.02,
5.4000000000000004: 0.1,
5.5: 0.04,
5.7000000000000002: 0.04,
5.7999999999999998: 0.02}
字典的key对应的正是sepal length在setosa类中不同取值,而字典的values对应的则是相应的条件概率。于此类似,我们可以得到sepal length的不同取值在不同类别的条件概率,我们只需要:
#改动类别
X_versicolor=X[y==1]
X_virginica=X[y==2]
而我们要得到不同的属性在相同类别下的条件概率,也只需要:
#改动特征
d=Counter(X_setosa[:,1])
......
d=Counter(X_setosa[:,3])
其中我们使用了python的标准库collections中的Counter类,对其进行计数,我们要得到所有的特征取值在所有的类别上的条件概率,总共需要计算12次,我们当然可以写两个循环(一个用来循环类别,一个用来循环特征)将所有的数据都计算并保存,当遇到新样本的时候,我们直接提取我们需要的东西来估计后验概率。
但更优雅的方式是,我们为朴素贝叶斯分类器写作一个类,对它的计数功能实现封装。首先,我们要用一个函数进行数据的读取:
def getdata(data):
return(dict([(d,1) for d in data]))
其中,我们将每个特征作为一个列表传入我们的getdata函数,返回的则是一个字典,代表着数据对应的特征取值均出现了一次。接下来,我们要对传入的数据进行增量学习,我们的基本需求有三点:
• 获得类别数和各类别的样本数,并将其保存
• 获得特征的取值数和特征的不同取值在各类别下的出现次数,并将其保存
• 对每一组数据都要进行训练,更新以上两个目标
所以,我们可以很方便的写出:
class Bayes:
def __init__(self,getdata):
self.getdata=getdata
self.fn={}#统计各类别的特征取值数目
self.cn={}#统计各个类别的样本数目
def cn_change(self,label):
self.cn.setdefault(label,0)
self.cn[label]+=1
def fn_change(self,n,f,label):
self.fn.setdefault(n,{})
self.fn[n].setdefault(f,{})
self.fn[n][f].setdefault(label,0)
self.fn[n][f][label]+=1
def f_count(self,f,label):
if f in self.fn and label in self.fn[f]:
return(self.fn[f][label])
return(0)
def label_count(self,label):
if label in self.cn:
return(self.cn[label])
return(0)
def label_total(self):
return(sum(self.cn.values()))
def train(self,item,label):
data=self.getdata(item)
for n,f in zip(range(len(data)),data):
self.fn_change(n,f,label)
self.cn_change(label)
我们将IRIS数据依次传入我们的分类器并训练:
from sklearn.datasets import load_iris
data=load_iris()
X=data.data
y=data.target
bayes=Bayes(getdata)
for i in range(len(y)):
bayes.train(X[i],y[i])
我们调用类的实例bayes的方法cn就会得到:
In [2]: bayes.cn
Out[2]: {0: 50, 1: 50, 2: 50}
#表示,类别0、类别1、类别2的样本均有50个
我们调用bayes的方法fn就会得到一个字典,由于它过于庞大,不方便在此全部展开,但它的结构是:
{.....
3: {5.0999999999999996: {0:8, 1: 1, 2: 4}}
......}
#表示,在属性3上的取值5.0999在类别0上出现8次,在类别1上出现1次,在类别2上出现4次
到现在,我们实现了基本的计数功能,并且随着样本的进入,我们的计数会逐步更新,但我们要利用贝叶斯估计后验概率,所以要将我们上一步的计算转化为概率,为此,我们继续对我们的类增添功能:
def piror(self,label):
return(self.cn[label]/self.label_total())
def likeihood(self,n,f,label):
if f in self.fn[n]:
if label in self.fn[n][f]:
p=self.fn[n][f][label]/self.cn[label]
return(p)
return(0)
def test(self,item,label):
data=self.getdata(item)
p_lh=1
for n,f in zip(range(len(data)),data):
p_lh*=self.likeihood(n,f,label)
p_pir=self.piror(label)
return(p_lh*p_pir)
我们添加了三个函数,来分别获取测试样本的先验概率,似然,以及后验概率,如果我们调用Bayes的test方法,就会输出新样本的后验概率,比如,我们可以分别计算属性取值为[4.9,3.1,5.0,1.8]的样本在不同类别下的后验概率:
In [3]: bayes.test([4.9,3.1,5.0,1.8],0)
Out[3]: 0.0
In [4]: bayes.test([4.9,3.1,5.0,1.8],1)
Out[4]: 1.5999999999999998e-07
In [5]: bayes.test([4.9,3.1,5.0,1.8],2)
Out[5]: 7.04e-06
我们可以注意到,新样本在类别2(virginica)下的后验概率较大,我们就可以说,新样本更有可能属于virginica。但同时,我们还会注意到,新样本在类别0(setosa)下的后验概率为零,这说明新样本绝不可能是setosa吗?
当然不是,这其实是因为我们在用朴素贝叶斯方法时,需要计算多个属性的条件概率下的联合概率。但如果我们训练集的样本多样性不够丰富,很可能会出现我们的测试样本中的某些属性值并未在训练样本的某个类中出现,在概率连乘求解联合概率的前提下,某个属性的条件概率为零就会造成整体为零,这样的结果就不那么合理。
为了避免这种情况发生,我们引入拉普拉斯修正(Laplacian correction),简单的来说,就是强行使其条件概率不可能等于零,从而避免上述情况的发生。
为了解释清楚这个道理,考虑简单的二分类问题。我们用
表示数据集中标记为yes的样本数,D来表示总的样本数,
表示类别数,在拉普拉斯修正下,先验概率的变化为:
我们继续用
来表示数据集中,既被标记为yes,又在第一个属性上取值
的样本数,
表示第一个属性可能的取值数,那么在拉普拉斯修正下,似然的变化为:
实际情况中,训练样本多样性丰富程度我们并不清楚,我们最好引入拉普拉斯修正,而且拉普拉斯修正会随着训练集的增大,影响变得越来越小,所以完全不用担心拉普拉斯会影响后验概率的估值。要引入此修正,我们只需要改动:
def piror(self,label):
return((self.cn[label]+1)/(self.label_total()+len(self.cn)))
def likeihood(self,n,f,label):
if f in self.fn[n]:
if label in self.fn[n][f]:
p=(self.fn[n][f][label]+1)/(self.cn[label]+len(self.fn[n]))
return(p)
return(1/(self.cn[label]+len(self.fn[n])))
将我们的改动保存到Bayes类之中我们继续计算属性取值为[4.9,3.1,5.0,1.8]的样本在不同类别下的后验概率:
In [6]: bayes.test([4.9,3.1,5.0,1.8],0)
Out[6]: 2.406815330836021e-07
In [7]: bayes.test([4.9,3.1,5.0,1.8],1)
Out[7]: 2.5672696862250893e-07
In [8]: bayes.test([4.9,3.1,5.0,1.8],2)
Out[8]: 3.850904529337635e-06
我们可以惊喜的看到新样本在类别0下的概率不再是零,我们得到了一个较为健壮的朴素贝叶斯分类器,我们只需要将其应用到不同的数据之中去观察它的效果,值得注意的是,朴素贝叶斯分类器本身并没有超参数需要调整,它很简单的假设了属性独立性,但对垃圾邮件的过滤却非常有效,可以算得上是简单而且强大的一类分类器。
读芯君开扒
课堂TIPS
• 我们在用python构建朴素贝叶斯分类器时,并没有使用任何库(包括大家喜闻乐见的sklearn),仔细阅读本篇文章会对大家的代码能力有所提高。事实上,我们可以使用numpy来加快我们的运行速度,因为我为了结构上的易理解,将关系表示成了一个字典,事实上,我们完全可以表示为一个或多个矩阵。
• 我们可以通过后验概率的大小来判断样本是否属于某一个类别,但在实际中,两者的后验概率太过接近,我们什么都不判断会更好一点,转而用其他的模型或者集成学习来辅助判断,所以我们往往需要设置后验概率的绝对阈值,和不同类别后验概率的相对阈值。
• 可以看出,本文所采用的贝叶斯分类器只适用于处理离散属性值,并没有能力处理连续的属性值,因为离散的属性值才可以利用频率来估计概率,连续的属性值则会经常发生测试样本的属性值均未在训练集中出现,我们就需要假设一个分布来表示似然函数,同时,分布的参数也将会成为我们需要估计的任务,背后隐藏着极大似然估计为代表的一类参数估计方法,这将会是我们下一章节要解决的问题,敬请期待。
作者:唐僧不用海飞丝
如需转载,请后台留言,遵守转载规范