(本文会用到的所有代码都在这里)
在实现完计算各种信息量的函数之后,我们就可以着手实现决策树本身了。由前文的讨论可知、组成决策树主体的是一个个的 Node,所以我们接下来首先要实现的就是 Node 这个结构。而且由于我们所关心的 ID3、C4.5 和 CART 分类树的 Node 在大多数情况下表现一致、只有少数几个地方有所不同,因此我们可以写一个统一的 Node 结构的基类CvDNode
来囊括我们所有关心的决策树生成算法,该基类需要实现如下功能:
- 根据离散型特征划分数据(ID3、C4.5、CART)
- 根据连续型特征划分数据(C4.5、CART)
- 根据当前的数据判定所属的类别
虽然说起来显得轻巧,但这之中的抽象还是比较繁琐的
我们先看看这个基类的基本框架该如何搭建:
|
|
可以看到,除了重载 getitem 方法以外、我们还重载 lt、str 和 repr 方法。这是因为决策树模型的结构比起朴素贝叶斯模型而言要复杂一些,为了开发过程中的调试和最后的可视化更加便利、通常来说最好让我们模型的表现更贴近内置类型的表现
然后我们需要定义几个 property 以使开发过程变得便利:
|
|
以上就是CvDNode
的基本框架,接下来就可以在这个框架的基础上实现决策树的各种生成算法了。首先需要指出的是,由于 Node 结构是会被 Tree 结构封装的、所以我们应该将数据预处理操作交给 Tree 来做。其次,由于我们实现的是抽象程度比较高的基类,所以我们要做比较完备的分情况讨论
注意:把 ID3、C4.5 和 CART 这三种算法分开实现是可行且高效的、此时各个部分的代码都会显得更加简洁可读一些;但这样做的话,整体的代码量就会不可避免地骤增。具体应当选择何种实现方案需要看具体的需求
我们先来看看实现生成算法所需要做的一些准备工作,比如定义停止继续生成的准则、定义停止后该 Node 的行为等
|
|
接下来就要实现生成算法的核心了,我们可以将它分成三步以使逻辑清晰:
- 定义一个方法使其能将一个有子节点的 Node 转化为叶节点(局部剪枝)
- 定义一个方法使其能挑选出最好的划分标准
- 定义一个方法使其能根据划分标准进行生成
我们先来看看如何进行局部剪枝:
|
|
第 16 行的 mark_pruned 方法用于给各个被局部剪枝剪掉的 Node 打上一个标记、从而今后 Tree 可以根据这些标记将被剪掉的 Node 从它记录所有 Node 的列表nodes
中删去。该方法同样是通过递归实现的,代码十分简洁:
|
|
有了能够进行局部剪枝的方法后,我们就能实现拿来挑选最佳划分标准的方法了。开发时需要时刻注意分清楚二分(连续 / CART)和多分(其它)的情况
|
|
根据划分标准进行生成的方法相当冗长、因为需要进行相当多的分情况讨论。它的实现用到了递归的思想,真正写起来就会发现其实并不困难、只不过会有些繁琐。囿于篇幅、我们略去它的实现细节,其算法描述则如下:
- 根据划分标准将数据划分成若干份
- 依次用这若干份数据实例化新 Node(新 Node 即是当前 Node 的子节点),同时将当前 Node 的相关信息传给新 Node。这里需要注意的是,如果划分标准是离散型特征的话:
- 若算法是 ID3 或 C4.5,需将该特征对应的维度从新 Node 的
self.feats
属性中除去 - 若算法是 CART,需要将二分标准从新 Node 的二分标准取值集合中除去
- 若算法是 ID3 或 C4.5,需将该特征对应的维度从新 Node 的
- 最后对新 Node 调用
fit
方法、完成递归
我个人实现的版本可以参见这里
以上我们就实现了 Node 结构并在其上实现了决策树的生成算法,接下来我们要做的就是实现 Tree 结构来将各个 Node 封装起来