(本文会用到的所有代码都在这里)
由前文的诸多讨论可知,Tree 结构需要做到如下几点:
- 定义好需要在各个 Node 上调用的“全局变量”
- 做好数据预处理的工作、保证传给 Node 的数据是合乎要求的
- 对各个 Node 进行合适的封装,做到:
- 生成决策树时能够正确地调用它们的生成算法
- 进行后剪枝时能够正确地调用它们的局部剪枝函数
- 定义预测函数和评估函数以供用户调用
既然 Node 我们可以抽象出一个基类CvDNode
,我们自然也能相应地对 Tree 结构抽象出一个基类CvDBase
下面先来看看如何搭建该基类的基本框架:
|
|
回忆朴素贝叶斯的实现,可以知道在第二章的混合型朴素贝叶斯中、我们要求用户告诉程序哪些维度的特征是连续的;这里我们介绍一种非常简易却有一定合理性的做法、从而可以让程序在进行数据预处理时自动识别出连续特征对应的维度:
- 将训练集中每个维度特征的所有可能的取值算出来,这一步可以用 Python 内置的数据结构
set
来完成 - 如果第 i 维可能的取值个数比上训练集总样本数 N 大于某个阈值,亦即若: 那么就认为第 i 维的特征是连续型随机变量
的具体取值需要视情况而定。一般来说在样本数 N 足够大时、可以取得比较小(比如取就是个不错的选择);但是样本数 N 比较小时,我们可能需要将取得大一些(比如取)。具体应该取什么值还是要看具体的任务和数据,毕竟这种自动识别的方法还是过于朴素了
以上所叙述的数据预处理的实现如下(注:我们在朴素贝叶斯的实现里用到过的quantize_data
方法中整合了如下代码中的核心部分):
|
|
最后一行我们对根节点调用了feed_tree
方法,该方法会做以下三件事:
- 让决策树中所有的 Node 记录一下它们所属的 Tree 结构
- 将自己记录在 Tree 中记录所有 Node 的列表
nodes
里 - 根据 Tree 的相应属性更新记录连续特征的列表
实现的时候同样利用上了递归:
|
|
注意:以上代码应定义在CvDNode
里面
接下来就是对生成算法的封装了。考虑到第二节会讲到的剪枝算法、我们需要做的是:
- 将类别向量数值化(和朴素贝叶斯里面的数值化类别向量的方法一样)
- 将数据集切分成训练集和交叉验证集、同时处理好样本权重
- 对根节点调用决策树的生成算法
- 调用自己的剪枝算法
具体的代码实现如下:
|
|
这里我们用到了np.random.permutation
方法,它其实可以看成两行代码的缩写、亦即:
|
|
从效果上来说等价于
|
|
不过前者不仅写起来更便利、而且运行速度也要稍微快一点,是故我们选择了前一种方法来进行实现
除了fit
这个函数以外,回忆 Node 中生成算法的实现过程、可知彼时我们调用了 Tree 的reduce_nodes
方法来将被剪掉的 Node 从nodes
中除去。该方法的实现如下:
|
|
注意:虽然该实现相当简单直观、不过其中却蕴含了一个具有普适意义的编程思想:如果要在遍历列表的同时进行当前列表元素的删除操作、就一定要从后往前遍历