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

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

在高斯判别分析模型(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)$ 。在条件独立假设下,我们可以简化条件概率,将条件概率分解为如下形式,

\[\begin{array}{l} {p\left(x_{1}, \ldots, x_{n} | y\right)} \\ {\quad=p\left(x_{1} | y\right) p\left(x_{2} | y, x_{1}\right) p\left(x_{3} | y, x_{1}, x_{2}\right) \cdots p\left(x_{n} | y, x_{1}, \ldots, x_{n-1}\right)} \\ {\quad=p\left(x_{1} | y\right) p\left(x_{2} | y\right) p\left(x_{3} | y\right) \cdots p\left(x_{n} | y\right)} \\ {\quad=\prod_{j=1}^{n} p\left(x_{j} | y\right)} \end{array} \]

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

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

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

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

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

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

\[\begin{aligned} p(y=1 | x) &=\frac{p(x | y=1) p(y=1)}{p(x)} \\ &=\frac{\left(\prod_{j=1}^{n} p\left(x_{j} | y=1\right)\right) p(y=1)}{\left(\prod_{j=1}^{n} p\left(x_{j} | y=1\right)\right) p(y=1)+\left(\prod_{j=1}^{n} p\left(x_{j} | y=0\right)\right) 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} 1\left\{z^{(i)}=j\right\}}{m} \]

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

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

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

\[\begin{aligned} \phi_{j | y=1} &=\frac{\sum_{i=1}^{m}{ 1\left\{x_{j}^{(i)}=1 \wedge y^{(i)}=1\right\}}+1}{\sum_{i=1}^{m}{ 1\left\{y^{(i)}=1\right\}}+2} \\ \phi_{j | y=0} &=\frac{\sum_{i=1}^{m} 1\left\{x_{j}^{(i)}=1 \wedge y^{(i)}=0\right\}+1}{\sum_{i=1}^{m} 1\left\{y^{(i)}=0\right\}+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}=p\left(x_{j}=k | y=0\right)\)\(\phi_{k | y=1}=p\left(x_{j}=k | y=1\right)\) 。注意,我们这里假设不管哪一个位置j,产生此位置单词的多项分布的参数都是一样的(比较之前\(\phi_{j | y=0}=p\left(x_{j}=1 | y=0\right)\) ) 。

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

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

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

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

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

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

\[\begin{aligned} \phi_{k | y=1} &=\frac{\sum_{i=1}^{m} \sum_{j=1}^{n_{i}} 1\left\{x_{j}^{(i)}=k \wedge y^{(i)}=1\right\}+1}{\sum_{i=1}^{m} 1\left\{y^{(i)}=1\right\} n_{i}+|V|} \\ \phi_{k | y=0} &=\frac{\sum_{i=1}^{m} \sum_{j=1}^{n_{i}} 1\left\{x_{j}^{(i)}=k \wedge y^{(i)}=0\right\}+1}{\sum_{i=1}^{m} 1\left\{y^{(i)}=0\right\} 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\times C$ 二维数组,每一行表示每个样本的独热编码。因此, self.class_count_ = y_train.sum(axis=0) 表示每个类的文档个数。注意求和后self.class_count_ 遭到降维打击变成一位数组。

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

self.feature_count_= np.dot(y_train.T, X) 是一个 $ C\times 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

posted @ 2019-09-22 16:22  客忆安排  阅读(593)  评论(2编辑  收藏  举报