(本文会用到的所有代码都在这里)
本文主要介绍离散型朴素贝叶斯——MultinomialNB 的实现。对于离散型朴素贝叶斯模型的实现,由于核心算法都是在进行“计数”工作、所以问题的关键就转换为了如何进行计数。幸运的是、Numpy 中的一个方法:bincount
就是专门用来计数的,它能够非常快速地数出一个数组中各个数字出现的频率;而且由于它是 Numpy 自带的方法,其速度比 Python 标准库collections
中的计数器Counter
还要快上非常多。不幸的是、该方法有如下两个缺点:
- 只能处理非负整数型中数组
- 向量中的最大值即为返回的数组的长度,换句话说,如果用
bincount
方法对一个长度为 1、元素为 1000 的数组计数的话,返回的结果就是 999 个 0 加 1 个 1
所以我们做数据预处理时就要充分考虑到这两点
在综述中我们曾经提到过在这里可以找到将数据进行数值化的具体实现,该数据数值化的方法其实可以说是为bincount
方法“量身定做”的。举个栗子,当原始数据形如:
|
|
时,调用上述数值化数据的方法将会把数据数值化为:
|
|
单就实现这个功能而言、实现是平凡的:
|
|
不过考虑到离散型朴素贝叶斯需要的东西比这要多很多,所以才有了这里所实现的、相对而言繁复很多的版本。建议观众老爷们在看接下来的实现之前先把那个quantize_data
函数的完整版看一遍、因为我接下来会直接用(那你很棒棒哦)
当然,考虑到朴素贝叶斯的相关理论还是比较多的、我就不把实现一股脑扔出来了,那样估计大部分人(包括我自己在内)都看不懂……所以我决定把离散型朴素贝叶斯算法和对应的实现进行逐一讲解 ( σ’ω’)σ
计算先验概率
这倒是在将框架时就已经讲过了、但我还是打算重讲一遍以加深理解。首先把实现放出来:
|
|
其中的lb
为平滑系数(默认为 1、亦即拉普拉斯平滑),这对应的公式其实是带平滑项的、先验概率的极大似然估计:
所以代码中的self._cat_counter
的意义就很明确了——它存储着 K 个
(cat counter 是 category counter 的简称)(我知道我命名很差所以不要打我)
计算条件概率
同样先看核心实现:
|
|
这对应的公式其实就是带平滑项(lb
)的条件概率的极大似然估计:
其中
可以看到我们利用到了self._cat_counter
属性来计算。同时可以看出:
n_category
即为 Kself._n_possibilities
储存着 n 个self._con_counter
储存的即是各个的值。具体而言:
至于self._data
、就只是为了向量化算法而存在的一个变量而已,它将data
中的每一个列表都转成了 Numpy 数组、以便在计算后验概率时利用 Numpy 数组的 Fancy Indexing 来加速算法
聪明的观众老爷可能已经发现、其实self._con_counter
才是计算条件概率的关键,事实上这里也正是bincount
大放异彩的地方。以下为计算self._con_counter
的函数的实现:
|
|
可以看到、bincount
方法甚至能帮我们处理样本权重的问题
代码中有两个我们还没进行说明的属性:self._labelled_x
和self._label_zip
,不过从代码上下文不难推断出、它们储存的是应该是不同类别所对应的数据。具体而言:
self._labelled_x
:记录按类别分开后的、输入数据的数组self._label_zip
:比self._labelled_x
多记录了个各个类别的数据所对应的下标
这里就提前将它们的实现放出来以帮助理解吧:
|
|
计算后验概率
仍然先看核心实现:
|
|
这对应的公式其实就是决策公式:
所以不难看出代码中的p_category
存储着 K 个
整合封装模型
最后要做的、无非就是把上述三个步骤进行封装而已,首先是数据预处理:
|
|
然后利用上一章我们定义的框架的话、只需定义核心训练函数即可:
|
|
最后,我们需要定义一个将测试数据转化为模型所需的、数值化数据的方法:
|
|
至此,离散型朴素贝叶斯就全部实现完毕了(鼓掌!)
评估与可视化
我们可以先拿之前的气球数据集 1.0、1.5 来简单地评估一下我们的模型。首先我们要定义一个能够将文件中的数据转化为 Python 数组的类:
|
|
需要指出的是,今后获取各种数据的过程都会放在上述DataUtil
中的这个get_dataset
方法中,其完整版本可以参见这里。下面就放出 MultinomialNB 的评估用代码:
|
|
上面这段代码的运行结果如下图所示:
由于数据量太少、所以建模和评估的过程耗费的时间已是可以忽略不计的程度。同时正如前文所提及的,气球数据集1.5是“不太均衡”的数据集,所以朴素贝叶斯在其上的表现会比较差
仅仅在虚构的数据集上进行评估可能不太有说服力,我们可以拿 UCI 上一个比较出名(简单)的“蘑菇数据集(Mushroom Data Set)”来评估一下我们的模型。该数据集的大致描述如下:它有 8124 个样本、22 个属性,类别取值有两个:“能吃”或“有毒”;该数据每个单一样本都占一行、属性之间使用逗号隔开。选择该数据集的原因是它无需进行额外的数据预处理、样本量和属性量都相对合适、二类分类问题也相对来说具有代表性。更重要的是,它所有维度的特征取值都是离散的、从而非常适合用来测试我们的 MultinomialNB 模型
完整的数据集可以参见这里(第一列数据是类别),我们的模型在其上的表现则如下图所示:
其中第一、二行分别是训练集、测试集上的准确率,接下来三行则分别是建立模型、评估模型和总花费时间的记录
当然,仅仅看一个结果没有什么意思、也完全无法知道模型到底干了什么。为了获得更好的直观,我们可以进行一定的可视化,比如说将极大似然估计法得到的条件概率画出(如综述所示的那样)。可视化的代码实现如下:
|
|
由于蘑菇数据一共有 22 维,所以上述代码会生成 22 张图,从这些图可以非常清晰地看出训练数据集各维度特征的分布。下选出几组有代表性的图片进行说明
一般来说,一组数据特征中会有相对“重要”的特征和相对“无足轻重”的特征,通过以上实现的可视化可以比较轻松地辨析出在离散型朴素贝叶斯中这两者的区别。比如说,在离散型朴素贝叶斯里、相对重要的特征的表现会如下图所示(左图对应第 5 维、右图对应第 19 维):
可以看出,蘑菇数据集在第 19 维上两个类别各自的“优势特征”都非常明显、第 5 维上两个类别各自特征的取值更是基本没有交集。可以想象,即使只根据第 5 维的取值来进行类别的判定、最后的准确率也一定会非常高
那么与之相反的、在 MultinomialNB 中相对没那么重要的特征的表现则会形如下图所示(左图对应第 3 维、右图对应第 16 维):
可以看出,蘑菇数据集在第 3 维上两个类的特征取值基本没有什么差异、第 16 维数据更是似乎完全没有存在的价值。像这样的数据就可以考虑直接剔除掉
看到这里的观众老爷如果再回过头去看上一篇文章所讲的框架、想必会有些新的体会吧 ( σ’ω’)σ