7.2 贝叶斯估计#
在介绍完7.1节中的内容后,相信各位读者对朴素贝叶斯算法的原理应该有了清楚地认识,但还有一个不能忽略的问题就是,当训练集不充分的情况下,某个维度的条件概率缺失时该怎么处理。例如在7.1.3节的示例中,如果条件概率$P(X^{(3)}=D|Y=1)=0$,即训练集中不存在这一情况,而在测试的数据样本中却存在这种情况该如何处理呢?如果此时仍旧将这种情况下的条件概率看作0,则在预测的时候将会产生很大的错差。
7.2.1 平滑处理#
通常,解决这类问题的一个有效办法就是在各个估计中加入一个平滑项(Smoothing Parameter),则此时先验概率和条件概率的计算方法为
$$ {{P}_{\lambda }}(Y={{c}_{k}})=\frac{\sum\limits_{i=1}^{m}{I}({{y}_{i}}={{c}_{k}})+\lambda }{m+K\lambda }\tag{7-20} $$$$ {{P}_{\lambda }}({{X}^{(j)}}={{a}_{jl}}|Y={{c}_{k}})=\frac{\sum\limits_{i=1}^{m}{I}(x_{i}^{(j)}={{a}_{jl}},{{y}_{i}}={{c}_{k}})+\lambda }{\sum\limits_{i=1}^{m}{I}({{y}_{i}}={{c}_{k}})+{{S}_{j}}\lambda }\tag{7-21} $$其中$K$表示数据集分类的类别数;$S_j$表示第$j$维特征的取值情况数; $\lambda\geq0$,并且当$\lambda=1$时称为拉普拉斯平滑(Laplace Smoothing),这也是常用的做法。
同时,当$\lambda>0$时分别称式(7-20)和式(7-21)为先验概率和条件概率的贝叶斯估计,并且可以发现,当$\lambda=0$时,就是极大似然估计。
7.2.2 计算示例#
接下来,将第7.1.3节中的数据使用拉普拉斯平滑($\lambda=1 $)再来计算一次。在计算之前我们知道,此时类别数$K=2,S_1=2,S_2=2,S_3=3$。
根据表7-1和式(7-20)易知,各类别的先验概率分别为
$$ P(Y=0)=\frac{6}{15+2\cdot 1},\ \ P(Y=1)=\frac{11}{15+2\cdot1}\tag{7-22} $$条件概率为
$$ \begin{aligned} & P({{X}^{(1)}}=0|Y=0)=\frac{5}{5+2\cdot1},P({{X}^{(1)}}=1|Y=0)=\frac{2}{7} \\[1ex] & P({{X}^{(2)}}=0|Y=0)=\frac{5}{7},P({{X}^{(2)}}=1|Y=0)=\frac{2}{7} \\[1ex] & P({{X}^{(3)}}=D|Y=0)=\frac{2}{8},P({{X}^{(3)}}=S|Y=0)=\frac{2}{8} \\[1ex] & P({{X}^{(3)}}=T|Y=0)=\frac{4}{8},P({{X}^{(1)}}=0|Y=1)=\frac{4}{12} \\[1ex] & P({{X}^{(1)}}=1|Y=1)=\frac{8}{12},P({{X}^{(2)}}=0|Y=1)=\frac{5}{12} \\[1ex] & P({{X}^{(2)}}=1|Y=1)=\frac{7}{12},P({{X}^{(3)}}=D|Y=1)=\frac{3}{13} \\[1ex] & P({{X}^{(3)}}=S|Y=1)=\frac{4}{13},P({{X}^{(3)}}=T|Y=1)=\frac{6}{13} \end{aligned}\tag{7-23} $$计算出属于各个类别的后验概率为
$$ \begin{aligned} & P(Y=0|X=x) \\[1ex] & =P(Y=0)\cdot P({{X}^{(1)}}=0|Y=0)\cdot P({{X}^{(2)}}=1|Y=0)\cdot P({{X}^{(3)}}=D|Y=0) \\[1ex] & =\frac{6}{17}\cdot \frac{5}{7}\cdot \frac{2}{7}\cdot \frac{2}{8}\approx 0.02 \end{aligned}\tag{7-24} $$$$ \begin{aligned} & P(Y=1|X=x) \\[1ex] & =P(Y=1)\cdot P({{X}^{(1)}}=0|Y=1)\cdot P({{X}^{(2)}}=1|Y=1)\cdot P({{X}^{(3)}}=D|Y=1) \\[1ex] & =\frac{11}{17}\cdot \frac{4}{12}\cdot \frac{7}{12}\cdot \frac{3}{13}\approx 0.03 \\[1ex] \end{aligned}\tag{7-25} $$于是我们同样可以得出,样本$x=(0,1,D)$属于$y=1$的可能性最大。
至此,对于朴素贝叶斯算法的原理及计算过程就介绍完了。由于在不同的书中对于一些算法原理有着不同的称谓,这也导致读者在初学及翻阅各种资料时发现一会儿又多了这个概念,一会儿又多了那个概念并为此极为苦恼。不过名称并不太重要,重要的是要知道具体指代的概念。如图7-2所示是我们对遇到的各种“叫法”进行的总结,仅供参考。

7.2.3 基于贝叶斯的垃圾邮件分类#
由于sklearn中并没有内置用于类别型变量分类的数据集,且sklean中也没有实现不考虑词频的词袋模型,所以这里需要我们自己实现一个类方法来完成整个向量化过程。因为这部分核心代码我们在第6.1.4节中已经介绍过,下面只需要将其整理封装成一个类即可。为了方便后续其它地方调用,将这部分代码放在utils下的text_feature_extraction模块中,完整代码可参见 AllBooKCode/utils/text_feature_extraction.py 文件。
1. 实现VectWithoutFrequency
首先定义类VectWithoutFrequency及其相应的初始化方法,示例代码如下:
1 class VectWithoutFrequency(object):
2 def __init__(self, top_k_words=500):
3 self.top_k_words = top_k_words在上述代码中,第2行表示在实例化类VectWithoutFrequency时,指定取语料中出现频率最高的前top_k_words词来构造词表。
接着,定义一个方法来根据语料得到词表,示例代码如下:
1 def _get_vocab(self, raw_documents):
2 c = Counter()
3 for sample in raw_documents:
4 words_list = sample.split()
5 for x in words_list:
6 if len(x) > 1 and x != '\r\n':
7 c[x] += 1
8 vocab = []
9 for (k, v) in c.most_common(self.top_k_words): # 输出词频最高的前top_k_words个词
10 vocab.append(k)
11 return vocab在上述代码中,第1行raw_documents为输入的原始语料,为一个列表,其中的每个元素为一个分词后以空格分割的样本。第2行是初始化一个计数器,用于统计词频。第3~7行开始遍历每个样本中的每一个词,并通过计数器进行计数。第9~11行是取计数器中词频最高的top_k_words个词作为整个语料的词表并返回。
进一步,这里模仿sklearn中的接口风格来实现fit()、transform()和fit_transform()3个方法,实现代码如下:
1 def fit(self, raw_documents):
2 self.vocabulary = self._get_vocab(raw_documents)
3
4 def transform(self, raw_documents):
5 x_vec = []
6 for item in raw_documents:
7 tmp = [0] * len(self.vocabulary)
8 for i, w in enumerate(self.vocabulary):
9 if w in item:
10 tmp[i] = 1
11 x_vec.append(tmp)
12 return np.array(x_vec)在上述代码中,第1~2行是实现模型的拟合,这里就是通过语料得到词表。第4~12行则是根据词表将原始分词后的语料转换为向量,详细介绍可参见6.1.4节内容。
同时,fit_transform()方法则是将上面两个方法进行整合,示例代码如下:
1 def fit_transform(self, raw_documents):
2 self.fit(raw_documents)
3 x = self.transform(raw_documents)
4 return x在实现完类VectWithoutFrequency的所有方法后,便可以通过如下方式进行使用,示例代码如下:
1 def test_VectWithoutFrequency():
2 s = ['文本 分词 工具 可 用于 对 文本 进行 分词 处理',
'常见 的 用于 处理 文本 的 分词 处理 工具 有 很多']
3 vec = VectWithoutFrequency(8)
4 print(vec.fit_transform(s))
5 if __name__ == '__main__':
6 test_VectWithoutFrequency()2. 构建数据集
在有了VectWithoutFrequency模块化,下一步则是构建整个任务所需要的数据集,示例代码如下:
1 def load_data():
2 x, y = load_cut_spam()
3 x_train, x_test, y_train, y_test \
4 = train_test_split(x, y, test_size=0.3, random_state=2020)
5 vect = VectWithoutFrequency(top_k_words=1000)
6 x_train = vect.fit_transform(x_train)
7 x_test = vect.transform(x_test)
8 return x_train, x_test, y_train, y_test在上述代码中,第2行是复用6.2.2节中实现的函数,载入原始分词后的垃圾邮件样本。第5~6行是先实例化类VectWithoutFrequency并在训练集上取得词表并将训练集转换为向量。第7行是用在训练集上得到的词表来对测试集进行向量化。第8行是返回最后的结果。
3. 垃圾邮件分类任务
在完成前期所有准备工作后,通过如下代码便可以完成基于CategoricalNB的垃圾邮件分类任务:
1 def test_spam_classification():
2 x_train, x_test, y_train, y_test = load_data()
3 model = CategoricalNB()
4 model.fit(x_train, y_train)
5 y_pred = model.predict(x_test)
6 logging.info(f"CategoricalNB 运行结果:")
7 logging.info(classification_report(y_test, y_pred))上述代码便是一个基于朴素贝叶斯模型的垃圾邮件分类示例,代码运行结束后便可以看到如下所示的结果:
CategoricalNB 运行结果:
precision recall f1-score support
0 0.97 0.96 0.97 1504
1 0.96 0.97 0.97 1497
accuracy 0.97 3001
macro avg 0.97 0.97 0.97 3001
weighted avg 0.97 0.97 0.97 3001可以看到,相较于6.2节中基于K近邻算法的垃圾邮件分类模型,朴素贝叶斯模型在各个指标上均有显著提升。
7.2.4 小结#
在本节中,我们介绍了如何处理在贝叶斯算法中条件概率为0时的处理方法,即贝叶斯估计,然后辨析了几个在贝叶斯算法中容易混淆的概念。值得一提的是,其实平滑处理这种做法不仅可以用于此处,在其他任何类似的情况中都可以借鉴这种做法。例如在第6章中我们介绍TFIDF的计算过程时就用到了类似的平滑处理方法。抑或是编写含有除运算的程序中,为了防止分母出现0的情况,都可以采用这样的做法。