朴素贝叶斯

前置知识

条件概率

我们规定$P(c)$代表事件c发生的概率(先验概率)

$P(x|c)$代表在事件c发生的前提下,事件$x$发生的概率。此时的$P(x|c)$就是最简单的条件概率。

先给出两者的计算公式。

令$D_c$表示训练集D中第c类样本的集合,令$D_{c,x_i}$表示$D_c$中第i个属性为$x_i$的样本的集合。当样本数量足够大的时候,可以估计出

$P(c)=\frac{|D_c|}{|D|}$

$P(x_i|c)=\frac{|D_{c,x_i}|}{|D_c|}$

同时根据这两个公式我们推出$P(x|c)=\frac{P(x,c)}{P(c)}$

让我们用最熟悉的🍉数据集举个例子:

首先估计先验概率$P(c)$,显然有

P(好瓜=是)=$\frac{8}{17}$ P(好瓜=否)=$\frac{9}{17}$

然后再为每个属性估计条件概率$P(x_i|c)$(举色泽的例子)

例如我们要求P(青绿|是),根据条件概率的定义,就相当于在好瓜里挑出色泽为青绿的瓜,总共有3个,好瓜又只有8个,所以P(青绿|是)=$\frac{3}{8}$

同理P(乌黑|否)=$\frac{2}{8}$

朴素贝叶斯理论

当我们拿到一个陌生的数据,例如一个西瓜={青绿,蜷缩,浊响,清晰,凹陷,硬滑,密度0.697,含糖0.460}(记该属性组合为$w$),我们需要分类其是否为好瓜。一种可行的方法是计算出P(w,是)和P(w,否)的概率,比较大小,哪个大就属于哪个类别。这就是贝叶斯决策理论。

普通的讲,贝叶斯决策理论要求计算两个概率p1(x, y)和p2(x, y):

如果p1(x, y) > p2(x, y),那么属于类别1;

如果p2(x, y) > p1(x, y),那么属于类别2。

回到那个西瓜的问题,我们其实无法直接求出P(w,是)的大小(因为这是一个全新的样本),但是根据刚刚条件概率的公式$P(x|c)=\frac{P(x,c)}{P(c)}$,我们可以得出P(w,是)=P(是)$\times$P(w|是)。P(是)容易得到,那么P(w|是)呢?当|w|=1时,我们也容易得到,那么当$|w|>1$时,容易想到概率论中$P(w|c)=\prod_{i=1}^{|w|}P(w_i|c)$

但这个公式成立的前提是$w_i$相互独立无关。那么当我们运用朴素贝叶斯时,特征之间是否相互无关呢?其实大多情况下特征之间都有着一定的联系,但是朴素贝叶斯分类器的假设就是特征之间相互独立且同等重要。这就是朴素贝叶斯中朴素一词的由来。因此,我们可以使用这个公式来求解。例如上文中的陌生西瓜,

P(w,是)=P(是)$\times$P(青绿|是)$\times$P(蜷缩|是)$\times$P(浊响|是)$\times$P(清晰|是)$\times$P(凹陷|是)$\times$P(硬滑|是)$\times$P(密度0.697|是)$\times$P(含糖0.460|是)=0.052

P(w,否)=P(否)$\times$P(青绿|否)$\times$P(蜷缩|否)$\times$P(浊响|否)$\times$P(清晰|否)$\times$P(凹陷|否)$\times$P(硬滑|否)$\times$P(密度0.697|否)$\times$P(含糖0.460|否)=0.000068

因此该西瓜样本经过朴素贝叶斯分类为好瓜。

至此朴素贝叶斯基本原理就介绍完毕了,接下来就进入代码实现部分吧。

代码实现

文本分类目标

给定论坛上的一些留言样本,每个留言有两种标签,正常语言和侮辱性语言。(1代表侮辱性文字,0代表正常语言),要求通过朴素贝叶斯分类器训练数据后,能对新的留言进行分类。

数据预处理

loadDataSet:创建训练数据

dataset:训练文档

classVec:文档依次对应标签(0表示文明/1表示不文明)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def loadDataSet():
    dataset=[['my','dog','has','flea',
                  'problems','help','please'],
                 ['maybe','not','take','him',
                  'to','dog','park','stupid'],
                 ['my','dalmation','is','so','cute',
                  'I','love','him'],
                 ['stop','posting','stupid','worthless','garbage'],
                 ['my','licks','ate','my','steak','how',
                  'to','stop','him'],
                 ['quit','buying','worthless','dog','food','stupid']]
    classVec=[0,1,0,1,0,1]
    return dataset,classVec

createVocab:创建词汇表

trainData:训练文档

Vocab:由训练文档中所有单词组成的词汇表

1
2
3
4
5
6
def createVocab(trainData):
    Vocab=set([])
    for document in trainData:
        Vocab=Vocab|set(document)
    return list(Vocab)
VocabList=createVocab(trainData)

createWordsVec:将inX转换成词向量

VocabList:创建好的词汇表

inX:需要转换的文档

1
2
3
4
5
6
def createWordsVec(VocabList,inX):
    outX=[0]*len(VocabList)
    for word in inX:
        if(word in VocabList):
            outX[VocabList.index(word)]+=1
    return outX

上述的三个函数作用是载入训练数据并将原来的文档转换为一个统一的矩阵,矩阵每一行代表一条留言,这条留言的词向量每一元素表示该单词出现的次数。这种表示方法采用的是词袋模型,区别于词集模型(只表示该次是否出现在文档中)。

这样我们就将原来的训练数据转换成一个文档矩阵,便于后面的运算操作。

训练函数

基于上面的理论,我们需要求出所有在文档中出现过的词汇$w$的$P(w|0)$和$P(w|1)$.

根据$P(w|0)=\frac{P(w,0)}{P(0)}$和$P(w|1)=\frac{P(w,1)}{P(1)}$,可以设计出最初版本

bayesTrain:训练朴素贝叶斯分类器

trainMat:将训练文档转换为词向量组成的矩阵

trainclass:训练文档对应的标签

p0v:p(w|0)组成的向量

p1v:p(w|1)组成的向量

pAbusive:文档属于侮辱类的概率

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
def bayesTrain(trainMat,trainclass):
    n=len(trainMat[0])
    pAbusive=sum(trainclass)/len(trainMat)
    p0v=np.zeros(n)
    p1v=np.zeros(n)
    sum0=sum1=0
    for i in range(len(trainMat)):
        if(trainclass[i]):
            p1v+=trainMat[i]
            sum1+=sum(trainMat[i])
        else:
            p0v+=trainMat[i]
            sum0+=sum(trainMat[i])
    p0v=p0v/sum0
    p1v=p1v/sum1
    return p0v,p1v,pAbusive
trainMat=[]
for document in trainData:
    trainMat.append(createWordsVec(VocabList,document))
p0v,p1v,pAbusive=bayesTrain(trainMat,trainclass)

代码优化

在实现函数的时候我们会碰到两个问题:

  1. 如果待分类的数据中出现了词汇表中没有出现过的单词$w_i$,那么其$P(w_i|0)$和$P(w_i|1)$均为0,导致最终结果连乘也为0。显然这与现实是不符的。

  2. 另一个问题是下溢出,这是由于太多很小的数相乘造成的。当计算乘积$\prod_{i=1}^{|w|}P(w_i|c)$ 时,由于大部分因子都非常小,所以程序会下溢出或者 得到不正确的答案。(读者可以用Python尝试相乘许多很小的数,最后四舍五入后会得到0)

拉普拉斯平滑

针对问题1出现的零概率,法国数学家拉普拉斯最早提出用加1的方法估计没有出现过的现象的概率。具体来说,令N表示训练集中D中可能的类别数,$N_i$表示第$i$个属性可能的取值数,则原来的公式可以修正为$P(c)=\frac{|D_c|+1}{|D|+N}$

$P(x_i|c)=\frac{|D_{c,x_i}|+1}{|D_c|+N_i}$

对原来的代码作如下修改:

1
2
3
p0v=np.ones(n)
p1v=np.ones(n)
sum0=sum1=n

对数优化

针对问题2,由于$lnx$和$x$单调性相同,且$ln(a*b)=lna+lnb$。所以在实践中常通过取对数的方式将连乘转化为连加以避免数值下溢。

对原来的代码作如下修改:

1
2
p0v=np.log(p0v/sum0)
p1v=np.log(p1v/sum1)

最终版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
def bayesTrain(trainMat,trainclass):
    n=len(trainMat[0])
    pAbusive=sum(trainclass)/len(trainMat)
    p0v=np.ones(n)
    p1v=np.ones(n)
    sum0=sum1=n
    for i in range(len(trainMat)):
        if(trainclass[i]):
            p1v+=trainMat[i]
            sum1+=sum(trainMat[i])
        else:
            p0v+=trainMat[i]
            sum0+=sum(trainMat[i])
    p0v=np.log(p0v/sum0)
    p1v=np.log(p1v/sum1)
    return p0v,p1v,pAbusive
trainMat=[]
for document in trainData:
    trainMat.append(createWordsVec(VocabList,document))
p0v,p1v,pAbusive=bayesTrain(trainMat,trainclass)

分类预测

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def classify(inX,p0v,p1v,pclass1):
    p1=sum(inX*p1v)+log(pclass1)
    p0=sum(inX*p0v)+log(1-pclass1)
    if(p1>p0):
        return 1
    else:
        return 0
test=[['love','my','dalmation'],
          ['stupid','garbage']]
for document in test:
    label=classify(createWordsVec(VocabList,document),p0v,p1v,pAbusive)
    print(f"{','.join(document)} classified as :{label}")

运行结果如下:

可以看到test中的预测结果都是正确的。

总结

朴素贝叶斯有着如下优点和缺点:

优点

  • 朴素贝叶斯模型发源于古典数学理论,有着坚实的数学基础,以及稳定的分类效率;

  • 对大数量训练和查询时具有较高的速度。即使使用超大规模的训练集,针对每个项目通常也只会有相对较少的特征数,并且对项目的训练和分类也仅仅是特征概率的数学运算而已;

  • 对小规模的数据表现很好,能个处理多分类任务,适合增量式训练(即可以实时的对新增的样本进行训练);

  • 对缺失数据不太敏感,算法也比较简单,常用于文本分类;

缺点

  • 由于使用了样本属性独立性的假设,所以如果样本属性有关联时其效果不好。

  • 对输入数据的表达形式很敏感,需要使用标称性数据

应用领域

  • 文本分类
  • 垃圾邮件过滤
  • 情感判别