生成式学习算法(四)之----朴素贝叶斯分类器

朴素贝叶斯分类器(算法)与朴素贝叶斯假设

在高斯判别分析模型(GDA)中,特征向量$ x$ 是连续实值向量。现在我们来讨论分量$ x_j$ 取离散值的贝叶斯朴素贝叶斯模型。
在文本分类问题中,有一个问题是分出一个邮件是((y=1) )或者不是((y=1) )垃圾邮件。我们的训练数据集是一些标好是否是垃圾邮件的邮件。现在我们首先需要把每个邮件表示成特征向量的形式。
假设我们有一个固定长度的词表。这个词表不包括一些在每个文档中都出现的,高频的对判别垃圾邮件没有太大作用的停用词。然后对于每一个邮件。可以用一个长度为词表长度的0,1向量来表示。向量的分量$ x_j$ 就表示这个词表中的第$ j$项所代表的单词是否出现在这个邮件中,0表示没有出现,1表示出现。

把每个邮件表示成0,1特征向量$ x$ 后,现在我们要对条件概率(p(x | y)) 进行建模。肯定不能用多项分布来建模,因为参数量太大了。比如我们的词表长度为5000,特征向量的可能结果有$ 2^{5000}$ 种。用多项分布则需要确定$ 2^{5000}-1$ 个参数。因此,我们需要对模型做出一些简化假设。
朴素贝叶斯假设就是其中一种简化假设,在这个假设下得到的算法叫朴素贝叶斯分类器(算法)。虽然这个假设较强,但很多问题在这个假设下依然表现良好。朴素贝叶斯假设是指在给定(y) 的条件下,向量$ x$ 的各个分量$ x_j$ 取值是条件独立的。具体的,给定C的情况下,A和B是条件独立的是指$ p(A)=p(A|B,C)$ 。注意,这与A和B是独立的不同。A与B独立是指$ p(A)=p(A|B)$ 。在条件独立假设下,我们可以简化条件概率,将条件概率分解为如下形式,

[egin{array}{l} {pleft(x_{1}, ldots, x_{n} | y ight)} \ {quad=pleft(x_{1} | y ight) pleft(x_{2} | y, x_{1} ight) pleft(x_{3} | y, x_{1}, x_{2} ight) cdots pleft(x_{n} | y, x_{1}, ldots, x_{n-1} ight)} \ {quad=pleft(x_{1} | y ight) pleft(x_{2} | y ight) pleft(x_{3} | y ight) cdots pleft(x_{n} | y ight)} \ {quad=prod_{j=1}^{n} pleft(x_{j} | y ight)} end{array} ]

从现在起,我们应该把特征$x $ 看成是一个固定的东西。则模型的参数有垃圾邮件的先验概率(phi_{y}=p(y=1)) ,以及每个特征分量下的两个参数(phi_{j | y=1}=pleft(x_{j}=1 | y=1 ight)) ,和 (phi_{j | y=0}=pleft(x_{j}=1 | y=0 ight))。比如 (phi_{j | y=1}=pleft(x_{j}=1 | y=1 ight)) 是垃圾邮件中这个特征所代表的单词出现的概率。

然后我们在这些参数下可以写出数据的似然函数,

[mathcal{L}left(phi_{y}, phi_{j | y=0}, phi_{j | y=1} ight)=prod_{i=1}^{m} pleft(x^{(i)}, y^{(i)} ight) ]

关于每组参数最大化,可以得到各个参数的如下估计,

[egin{equation} egin{aligned} phi_{j | y=1} &=frac{sum_{i=1}^{m} 1left{x_{j}^{(i)}=1 wedge y^{(i)}=1 ight}}{sum_{i=1}^{m} 1left{y^{(i)}=1 ight}} \ phi_{j | y=0} &=frac{sum_{i=1}^{m} 1left{x_{j}^{(i)}=1 wedge y^{(i)}=0 ight}}{sum_{i=1}^{m} 1left{y^{(i)}=0 ight}} \ phi_{y} &=frac{sum_{i=1}^{m} 1left{y^{(i)}=1 ight}}{m} end{aligned} end{equation} ]

不难理解上面等式的意义(其中1表示计数向量,尖号表示逻辑与)。比如参数(phi_{j | y=1}=pleft(x_{j}=1 | y=1 ight)) 的估计(不严格地,上面我们还用同一个符号来表示)其实就是垃圾邮件中这个特征所代表的单词出现的频率,在一个邮件中每个词最多计数一次。
估计出这些参数,来了一个新邮件,我们就可以做出如下预测,

[egin{aligned} p(y=1 | x) &=frac{p(x | y=1) p(y=1)}{p(x)} \ &=frac{left(prod_{j=1}^{n} pleft(x_{j} | y=1 ight) ight) p(y=1)}{left(prod_{j=1}^{n} pleft(x_{j} | y=1 ight) ight) p(y=1)+left(prod_{j=1}^{n} pleft(x_{j} | y=0 ight) ight) p(y=0)} end{aligned} ]

不要害怕第二个分母里一坨东西,它只是计算了$ x$ 和$ y$ 的不同取值的联合概率,然后归一化。
之前我们说特征向量$ x$ 是0,1向量。可以自然推广到特征向量取$k $ 个值的情况。对于连续的变量不是太服从多元正态分布时,可以将其离散化为$k $ 值离散特征向量,然后利用这个模型,也往往能取得比高斯判别分析模型更好的结果。

拉普拉斯平滑

2019年9月21日19:03:05
上面算法的一个最大问题就是对于在词表中出现过,但训练集中未出现过的单词,计算出后验概率0:0,无法判断是垃圾邮件,还是非垃圾邮件。采用拉普拉斯平滑可以大大改善这个算法。
更广泛一点来说,对于训练集中没有见到的事件,我们就估计它出现的概率为零,不是一个好的做法。对于一个取k个值的多项分布。把最大似然估计有,

[phi_{j}=frac{sum_{i=1}^{m} 1left{z^{(i)}=j ight}}{m} ]

拉普拉斯平滑机制则在此基础上在分母加$k $,分子加一,即,

[phi_{j}=frac{sum_{i=1}^{m} 1left{z^{(i)}=j ight}+1}{m+k} ]

回到朴素贝叶斯参数估计上,再给定标签$y $ 的情况下,表示词出现与否的每个特征分量 $x_{j} $ 是一个二项分布,因此就有如下拉普拉斯平滑估计,

[egin{aligned} phi_{j | y=1} &=frac{sum_{i=1}^{m}{ 1left{x_{j}^{(i)}=1 wedge y^{(i)}=1 ight}}+1}{sum_{i=1}^{m}{ 1left{y^{(i)}=1 ight}}+2} \ phi_{j | y=0} &=frac{sum_{i=1}^{m} 1left{x_{j}^{(i)}=1 wedge y^{(i)}=0 ight}+1}{sum_{i=1}^{m} 1left{y^{(i)}=0 ight}+2} end{aligned} ]

注意,我们这里是对特征分量进行拉普拉斯平滑。标签本身也是个二项分布,但一般不需要拉普拉斯平滑。

文本分类事件模型

虽然朴素贝叶斯算法在分类中一般表现较好,有一类算法在文本分类中可以表现得更好。

朴素贝叶斯用的是多元伯努利事件模型(multi-variate Bernoulli event model)。邮件发送者先一定概率决定发送垃圾邮件和非垃圾邮件,然后在给定条件概率下。独立的以相应概率发送词表中的每个单词。
现在我们来看一下多项事件模型(multinomial event model)。他对每个邮件的特征表示与之前不同。他将第j个特征分量表示邮件中的第j个单词(之前的是词表中第j个单词出现与否0,1变量)。这个特征分量取值于({1, ldots,|V|})(|V|) 为词表的大小。然后一个邮件由不固定长度的(n) 个词组成。
在多项事件模型中,每个邮件是否是垃圾邮件产生方法和之前相同。在决定了邮件是否是垃圾邮件后,再根据相应的条件概率产生第一个单词,注意是从多项分布中产生一个单词,然后再独立的产生第二个单词,一直产生n个单词。这里分布是多项分布,和之前的伯努利分布是不一样的。

这个模型的参数同样有垃圾邮件的先验概率(phi_{y}=p(y=1))(phi_{k | y=0}=pleft(x_{j}=k | y=0 ight))(phi_{k | y=1}=pleft(x_{j}=k | y=1 ight)) 。注意,我们这里假设不管哪一个位置j,产生此位置单词的多项分布的参数都是一样的(比较之前(phi_{j | y=0}=pleft(x_{j}=1 | y=0 ight)) ) 。

给定训练集(left{left(x^{(i)}, y^{(i)} ight) ; i=1, ldots, m ight}) ,其中(x^{(i)}=left(x_{1}^{(i)}, x_{2}^{(i)}, dots, x_{n_{i}}^{(i)} ight)),这里$n_{(i)} $ 是第${i} $ 个邮件的单词个数。然后我们在这些参数下可以写出数据的似然函数,

[egin{aligned} mathcal{L}left(phi_{y}, phi_{k | y=0}, phi_{k | y=1} ight) &=prod_{i=1}^{m} pleft(x^{(i)}, y^{(i)} ight) \ &=prod_{i=1}^{m}left(prod_{j=1}^{n_{i}} pleft(x_{j}^{(i)} | y ; phi_{k | y=0}, phi_{k | y=1} ight) ight) pleft(y^{(i)} ; phi_{y} ight) end{aligned} ]

关于每组参数最大化,可以得到各个参数的如下估计,

[egin{aligned} phi_{k | y=1} &=frac{sum_{i=1}^{m} sum_{j=1}^{n_{i}} 1left{x_{j}^{(i)}=k wedge y^{(i)}=1 ight}}{sum_{i=1}^{m} 1left{y^{(i)}=1 ight} n_{i}} \ phi_{k | y=0} &=frac{sum_{i=1}^{m} sum_{j=1}^{n_{i}} 1left{x_{j}^{(i)}=k wedge y^{(i)}=0 ight}}{sum_{i=1}^{m} 1left{y^{(i)}=0 ight} n_{i}} end{aligned} ]

我们耐心从一个邮件开始分析,慢慢理顺就不难理解上面等式的意义(其中1表示计数向量,尖号表示逻辑与)。比如 (phi_{k | y=1}) 是垃圾邮件中第(k) 个单词出现的频率,在一个邮件中每个词可能多次计数(注意比较与前面不同)。

如果我们用拉普拉斯平滑,标签$y $ 的情况下,表示第 $j $ 个位置哪个词出现的特征分量 $x_{j} $ 是一个多项分布(取值个数 (|V|) ),因此就有如下拉普拉斯平滑估计,

[egin{aligned} phi_{k | y=1} &=frac{sum_{i=1}^{m} sum_{j=1}^{n_{i}} 1left{x_{j}^{(i)}=k wedge y^{(i)}=1 ight}+1}{sum_{i=1}^{m} 1left{y^{(i)}=1 ight} n_{i}+|V|} \ phi_{k | y=0} &=frac{sum_{i=1}^{m} sum_{j=1}^{n_{i}} 1left{x_{j}^{(i)}=k wedge y^{(i)}=0 ight}+1}{sum_{i=1}^{m} 1left{y^{(i)}=0 ight} n_{i}+|V|} end{aligned} ]

尽管朴素贝叶斯算法可能不是最好的,但它通常都有不俗的表现。由于这个算法实现起来简单明了。绝对是首先值得一试的算法。

实现细节

2019年9月25日12:15:38
代码源于这里,精简浓缩了sklearn实现,精华,推荐。

类定义,

class BernoulliNB():
    def __init__(self, alpha=1.0):
        self.alpha = alpha #平滑参数,事实上可以设为任意大于零值

下面都是类函数,

  • _xxx 表示protected类型变量。只允许这个类本身与它的子类进行访问。不能用 from module import * 引入名字。

  • __xxx 表示私有类型变量。只允许这个类本身进行访问,子类不可以访问。

总结,python 里面的单下划线与双下划线的区别是前者保护和后者私有

#这里的X都是训练数据
def _encode(self, y):
    classes = np.unique(y)
    y_train = np.zeros((y.shape[0], len(classes)))
    for i, c in enumerate(classes):
        y_train[y == c, i] = 1 #每一行是 y 的one-hot表示
    return classes, y_train

def fit(self, X, y):
    self.classes_, y_train = self._encode(y)
    self.feature_count_ = np.dot(y_train.T, X)   # C类*n词 一行代表是不是某一类,X一j列代表出现了没第j个词,中间m个文档正好累和消去
    self.class_count_ = y_train.sum(axis=0)
    smoothed_fc = self.feature_count_ + self.alpha #给定类别下,避免各个词频为零出现,最小
    smoothed_cc = self.class_count_ + 2 * self.alpha #某类别下特征出现与否概率,与类别个数无关,与特征个数无关,只与出现与否有关,因此加2,使得出现和不出现加起来概率为1
    self.feature_log_prob_ = (np.log(smoothed_fc) -
                              np.log(smoothed_cc.reshape(-1, 1)))    #reshape(-1, 1)升维变成C*1的2维数组,可参考之前numpy广播博客
    self.class_log_prior_ = np.log(self.class_count_) - np.log(self.class_count_.sum())
    return self

输入 y 是$ m$ 个整数标签,由函数 _encode 知道, self.classes_ 是一个长为$ C$ 的表示类别一维数组, y_train 为$ m imes C$ 二维数组,每一行表示每个样本的独热编码。因此, self.class_count_ = y_train.sum(axis=0) 表示每个类的文档个数。注意求和后self.class_count_ 遭到降维打击变成一位数组。

$ X$ 表示由$ m$ 个样本,$ n$ 个特征组成的$ m imes n$ 数据矩阵。 里面表示词表中词出现与否的0,1特征向量意义在之前讲过。

self.feature_count_= np.dot(y_train.T, X) 是一个 $ C imes n$ 的矩阵,$ (i,j)$ 元素表示第$ i$ 个类别下第$ j$ 个特征词出现的文档个数(自己理一理,向量化代码学习一下)。

#这里的X都是新来的数据
def _joint_log_likelihood(self, X): #第二坨是计算未出现词项的概率,第一项出现词项概率,向量大法好,简洁又明了(可能不太明了?)
    return (np.dot(X, self.feature_log_prob_.T) +
            np.dot(1 - X, np.log(1 - np.exp(self.feature_log_prob_)).T) +
            self.class_log_prior_)

def predict(self, X):#如前所述,预测不需要归一化
    joint_log_likelihood = self._joint_log_likelihood(X)
    return self.classes_[np.argmax(joint_log_likelihood, axis=1)]

def predict_proba(self, X):#归一化概率
    joint_log_likelihood = self._joint_log_likelihood(X)
    log_prob = joint_log_likelihood - logsumexp(joint_log_likelihood, axis=1)[:, np.newaxis] #[:, np.newaxis]升维变成m*1的2维数组,可参考之前numpy广播博客
    return np.exp(log_prob)

img

原文地址:https://www.cnblogs.com/qizhien/p/11567887.html