简介
朴素贝叶斯(naive Bayes)算法是基于贝叶斯定理与特征条件独立假设的分类方法。对于给定的训练数据集,首先基于特征条件独立假设学习输入输出的联合概率分布。然后基于此模型,对给定的输入x,利用贝叶斯定理求出后验概率最大的输出y。不同于其他分类器,朴素贝叶斯是一种基于概率理论的分类算法;总体来说,朴素贝叶斯原理和实现都比较简单,学习和预测效率较高,是一种经典而常用的分类算法。其中的朴素(naive)是指的对于数据集中的各个特征(feature)都有较强的独立性假设,并未将特征之间的相关性考虑其中。
贝叶斯公式
这里首先不加证明地给出贝叶斯定理:
其中,称为后验(posterior)概率; 是类先验(prior)概率;是样本相对于类标记的类条件概率(class-conditional proability),或称为似然(likelihood)概率;是用于归一化的证据(evidence)因子。对于给定样本,证据因子与类别标记无关,因此估计后验概率的问题就转化为如何基于训练数据D来估计先验概率和似然。
这里补充一下关于先验概率和后验概率的概念:
先验概率: 根据以往经验或经过数据统计得到的概率。例如我们可以通过统计训练数据集来计算得到先验概率。
后验概率:事情已经发生,求这件事情发生的原因是由某个因素引起的可能性的大小。比如称为的后验概率,即它是在观察到事件发生之后计算得到的。
可参考:https://zhuanlan.zhihu.com/p/26464206
贝叶斯定义之所以有用,是因为我们通常很难直接得到后验概率,但是先验概率和似然概率反而比较好得到,通过这两者我们可以计算得到后验概率。实际上,机器学习所要实现的就是基于有限的训练样本集合尽可能准确地估计出后验概率。一般来说有两种策略:给定,直接建模来预测,这样得到的是“判别式模型”(discriminative models),常见的包括决策树模型、神经网络、支持向量机、逻辑回归模型等等;也可以对联合概率分布建模,然后再由此获取,这样得到就是“生成式模型”(generative models),比如本文介绍的朴素贝叶斯模型。
极大似然估计
贝叶斯公式确定了,现在的问题变成了如何从训练样本集合中去估计先验概率和似然。类先验概率表达了样本空间中各类样本所占的比例,根据大数定律,当训练集包含充足的独立同分布样本时,可通过各类样本出现的频率来进行估计。但是对于类条件概率来说,由于它涉及到了关于所有属性的联合概率,实际上是无法进行估计的。比如每个样本多具有个属性,每个属性都是二值的,那么样本空间将有种可能的取值,在实际应用中,样本一般都具有多个特征,并且每个特征的取值也各不相同,这样组合下来的取值会远训练样本的数量。这会使得条件概率分布具有指数级数量的参数,造成组合爆炸的问题。
朴素贝叶斯对条件概率分布做了条件独立性的假设,也正是因为这一假设,朴素贝叶斯因此得名。具体来说,条件独立性假设是:
朴素贝叶斯实际上学习到生成数据的机制,所以属于生成模型。条件独立假设等于是说用于分类的特征在类别确定的条件下是条件独立的。这一假设使得朴素贝叶斯变得简单,但是特征实际上一般是互相有依赖的,并不完全满足这个假设,因此朴素贝叶斯也牺牲了一定的准确率。
有了上述前提之后,我们现在使用极大似然估计的方法来估计先验概率和似然。
先验概率的极大似然估计是:
其中是样本集的总数,K是类别的总数,表示事件。离散特征下,条件概率的极大似然估计是:
其中,代表第个样本的第个特征;是第个特征可能取的第个值。在连续特征下,这里首先假设概率密度函数,则参数和的极大似然估计为:
也就是说,通过极大似然估计得到的正态分布均值就是样本均值,方差就是的均值,这显然是一个符合直觉的结果。在离散特征属性下,也可以通过类似的方式来估计类条件概率。
需要注意的是,这种参数化的方法虽然能使类条件概率估计变得相对简单,但是估计的准确性严重依赖所假设的概率分布形式是否符合潜在的真实数据分布。在现实应用中,欲做出能较好地接近潜在真实分布的假设,往往需在一定程度上利用关于应用任务本身的经验知识,否则若仅凭“猜测”来假设概率分布形式,很可能会导致错误的结果。
朴素贝叶斯算法步骤
下面给出朴素贝叶斯算法的具体步骤:
输入:训练数据, 其中,是第个样本的第个特征,,是第个特征可能取的第个值,,。
输出:实例的分类。
计算先验概率和条件概率:
对于给定的实例,计算:
确定的分类:
实例解析
下面使用西瓜书中的西瓜数据集3.0来演示一下朴素贝叶斯的整体计算流程。训练样本数据集如下:
import pandas as pd
from io import StringIO
data = '编号,色泽,根蒂,敲声,纹理,脐部,触感,密度,含糖率,好瓜\n\
1,青绿,蜷缩,浊响,清晰,凹陷,硬滑,0.697,0.46,是\n\
2,乌黑,蜷缩,沉闷,清晰,凹陷,硬滑,0.774,0.376,是\n\
3,乌黑,蜷缩,浊响,清晰,凹陷,硬滑,0.634,0.264,是\n\
4,青绿,蜷缩,沉闷,清晰,凹陷,硬滑,0.608,0.318,是\n\
5,浅白,蜷缩,浊响,清晰,凹陷,硬滑,0.556,0.215,是\n\
6,青绿,稍蜷,浊响,清晰,稍凹,软粘,0.403,0.237,是\n\
7,乌黑,稍蜷,浊响,稍糊,稍凹,软粘,0.481,0.149,是\n\
8,乌黑,稍蜷,浊响,清晰,稍凹,硬滑,0.437,0.211,是\n\
9,乌黑,稍蜷,沉闷,稍糊,稍凹,硬滑,0.666,0.091,否\n\
10,青绿,硬挺,清脆,清晰,平坦,软粘,0.243,0.267,否\n\
11,浅白,硬挺,清脆,模糊,平坦,硬滑,0.245,0.057,否\n\
12,浅白,蜷缩,浊响,模糊,平坦,软粘,0.343,0.099,否\n\
13,青绿,稍蜷,浊响,稍糊,凹陷,硬滑,0.639,0.161,否\n\
14,浅白,稍蜷,沉闷,稍糊,凹陷,硬滑,0.657,0.198,否\n\
15,乌黑,稍蜷,浊响,清晰,稍凹,软粘,0.36,0.37,否\n\
16,浅白,蜷缩,浊响,模糊,平坦,硬滑,0.593,0.042,否\n\
17,青绿,蜷缩,沉闷,稍糊,稍凹,硬滑,0.719,0.103,否'
df = pd.read_csv(StringIO(data))
df
我们的测试样本为训练集的第一个样本:
test = df.loc[:0,:]
test["好瓜"] = '?'
test
首先计算先验概率,直接观察表格可得:
其次计算条件概率,分为离散特征和连续特征两步,首先是离散特征。这里出于篇幅原因,只举一个例子,其余类似。以第一个离散属性"色泽"为例,测试样本的色泽为”青绿“,那么分别计算在”是好瓜“和”不是好瓜“这两类事件发生的条件下,并且色泽是”青绿“的条件概率,如下:
其他离散属性同理,这里不再赘述。
接着是连续属性,我们假设其条件概率符合均值为且方差为的高斯分布,我们首先在数据集中计算出对应的均值和方差。以"密度"为例,将训练集按照是否为好瓜拆成两部分,分别计算两种类别下属性”密度“的均值和方差,如下:
haogua = df['密度'][:8] # 取前8个样本,求密度的均值和标准差
mean, std = haogua.mean(), haogua.std()
print("好瓜的密度均值和标准差:\n", mean, std)
langua = df['密度'][8:] # 取前后9个样本,求密度的均值和标准差
mean, std = langua.mean(), langua.std()
print("烂瓜的密度均值和标准差:\n", mean, std)
结果为:
则条件概率计算如下:
接着计算:
对比两个概率的大小,由于,所以这个测试样本被朴素贝叶斯分类器判定为”好瓜“。
拉普拉斯平滑
仔细观察上述例子会发现一个问题,若某个属性值在训练集中没有与某个类别同时出现的时候,如果直接使用频率统计来进行概率估计会出现问题。例如,如果某个测试用例中包含了"敲声=清脆"这一属性,那么:
由于极大似然估计使用的是连乘,这就会导致估计出的的条件概率为0,这样显然是不合理的。
为了避免其他属性携带的信息被训练集中未出现的属性值给抹去,在估计概率值时通常要进行”平滑“(smoothing)操作,常用的但是拉布拉斯修正(Laplacian correction)。具体来说,条件概率的贝叶斯估计为:
其中,这等价于在随机变量各个取值的频数上赋予一个正数,当时就是极大似然估计。常取,这时候称为拉普拉斯平滑。显然有:
同理,对于先验概率的贝叶斯估计为:
对于上述的例子,类的先验概率的贝叶斯估计为:
类似地, 条件概率的贝叶斯估计为:
显然,拉普拉斯平滑避免了因训练集样本不充分而导致的概率估值为0的情况,并且在训练集变大的时候,平滑过程中引入的先验的影响也会逐渐变得可忽略,使得估值趋向于实际概率值。
代码实践
本文不打算自己手写一个朴素贝叶斯的实现,sklean包中包含了各种贝叶斯算法模型,我们依旧是基于西瓜数据集3.0,来使用sklearn中集成的贝叶斯模型来预测上一节的测试样本。
由于原始的离散特征属性是字符串型的,模型无法处理,我们首先需要进行数据预处理,使用LabelEncoder将其转换成数值类型。代码如下:
from sklearn import preprocessing
le = preprocessing.LabelEncoder()
df['色泽'] = le.fit_transform(df['色泽'].values)
df['根蒂'] = le.fit_transform(df['根蒂'].values)
df['敲声'] = le.fit_transform(df['敲声'].values)
df['纹理'] = le.fit_transform(df['纹理'].values)
df['脐部'] = le.fit_transform(df['脐部'].values)
df['触感'] = le.fit_transform(df['触感'].values)
df['好瓜'] = le.fit_transform(df['好瓜'].values)
df
接着划分数据集:
# 划分训练集和测试集
train_x, train_y = df[['色泽', '根蒂', '敲声', '纹理', '脐部', '触感', '密度', '含糖率']], df[['好瓜']]
test_x, test_y = df[:1][['色泽', '根蒂', '敲声', '纹理', '脐部', '触感', '密度', '含糖率']], df[:1][['好瓜']]
test_x
模型训练和预测:
from sklearn.naive_bayes import GaussianNB
gnb = GaussianNB()
# training
gnb.fit(x, y.values.ravel())
# predicting
pred_y = gnb.predict(test_x)
print(pred_y)
输出为1.完整代码如下:
import pandas as pd
from io import StringIO
data = '编号,色泽,根蒂,敲声,纹理,脐部,触感,密度,含糖率,好瓜\n\
1,青绿,蜷缩,浊响,清晰,凹陷,硬滑,0.697,0.46,是\n\
2,乌黑,蜷缩,沉闷,清晰,凹陷,硬滑,0.774,0.376,是\n\
3,乌黑,蜷缩,浊响,清晰,凹陷,硬滑,0.634,0.264,是\n\
4,青绿,蜷缩,沉闷,清晰,凹陷,硬滑,0.608,0.318,是\n\
5,浅白,蜷缩,浊响,清晰,凹陷,硬滑,0.556,0.215,是\n\
6,青绿,稍蜷,浊响,清晰,稍凹,软粘,0.403,0.237,是\n\
7,乌黑,稍蜷,浊响,稍糊,稍凹,软粘,0.481,0.149,是\n\
8,乌黑,稍蜷,浊响,清晰,稍凹,硬滑,0.437,0.211,是\n\
9,乌黑,稍蜷,沉闷,稍糊,稍凹,硬滑,0.666,0.091,否\n\
10,青绿,硬挺,清脆,清晰,平坦,软粘,0.243,0.267,否\n\
11,浅白,硬挺,清脆,模糊,平坦,硬滑,0.245,0.057,否\n\
12,浅白,蜷缩,浊响,模糊,平坦,软粘,0.343,0.099,否\n\
13,青绿,稍蜷,浊响,稍糊,凹陷,硬滑,0.639,0.161,否\n\
14,浅白,稍蜷,沉闷,稍糊,凹陷,硬滑,0.657,0.198,否\n\
15,乌黑,稍蜷,浊响,清晰,稍凹,软粘,0.36,0.37,否\n\
16,浅白,蜷缩,浊响,模糊,平坦,硬滑,0.593,0.042,否\n\
17,青绿,蜷缩,沉闷,稍糊,稍凹,硬滑,0.719,0.103,否'
df = pd.read_csv(StringIO(data), index_col=0)
from sklearn import preprocessing
le = preprocessing.LabelEncoder()
df['色泽'] = le.fit_transform(df['色泽'].values)
df['根蒂'] = le.fit_transform(df['根蒂'].values)
df['敲声'] = le.fit_transform(df['敲声'].values)
df['纹理'] = le.fit_transform(df['纹理'].values)
df['脐部'] = le.fit_transform(df['脐部'].values)
df['触感'] = le.fit_transform(df['触感'].values)
df['好瓜'] = le.fit_transform(df['好瓜'].values)
# 划分训练集和测试集
train_x, train_y = df[['色泽', '根蒂', '敲声', '纹理', '脐部', '触感', '密度', '含糖率']], df[['好瓜']]
test_x, test_y = df[:1][['色泽', '根蒂', '敲声', '纹理', '脐部', '触感', '密度', '含糖率']], df[:1][['好瓜']]
from sklearn.naive_bayes import GaussianNB
gnb = GaussianNB()
# training
gnb.fit(x, y.values.ravel())
# predicting
pred_y = gnb.predict(test_x)
print(pred_y)