1 引言

各位朋友大家好,欢迎来到月来客栈,我是掌柜空字符。

前面一篇文章中,掌柜详细介绍了ID3与C4.5决策树算法的原理与计算示例,并且还介绍了如何借助开源的sklearn框架来完成整个建模的搭建流程。在接下来的这篇文章中,掌柜将会详细地来介绍如何从零一步步地实现ID3与C4.5这两种决策树算法。

所有示例代码均可从此仓库获取:https://github.com/moon-hotel/MachineLearningWithMe

2 决策树实现

由ID3与C4.5决策树算法的原理可知,两则的唯一差别就是体现在对于特征划分标准的不同上,前者采用的信息增益,而后者则采用的是信息增益比来进行判断。因此,两则在代码实现时只需要将这部分内容单独抽象成一个函数即可,其它部分的代码可以保持不变。下面,掌柜开始来一步步的进行介绍。

2.1 节点定义

在实现决策树之前,我们需要先来定义决策树中每个节点的构成。参考第8章 决策树与集成学习 第8.3.2节)中决策树可视化例子中的节点信息,这里我们将决策树的节点定义为如下形式:

在上述代码中,第3行sample_index用来保存当前节点中对应样本在数据集中的索引,这样我们在需要的时候可以直接通过索引去取到对应的样本而不是保存到每个节点中,同时也方便根据索引来取对应的样本标签;第4行values用来保存每个类别的数量,例如[10,4,6]则表示第0、1、2这三个类别在当前节点中的数量分别是10、4和6,其作用是根据这一结果可以知道当前叶子节点所代表的类别;第5行features用于保存在当前节点状态时特征集中剩余特征维度(即还剩下哪些特征没有被用于前面的划分中),例如[0,2,3]则表示对于当前节点来说,其备选特征为第0、2和3个;第6行feature_id用于保存当前节点对应划分特征的id,因为在决策树预测阶段时需要知道当前节点是用哪个特征来进行划分的;第7行label用来保存当前节点对应的类别标签(叶子节点才有),不过这个可要可不要,因为通过前面的values就已经能够得到当前叶子节点的类别;第8行n_samples用来保存当前节点对应的样本数量,用于分析观察;第9行children用来保存当前节点对应的所有孩子节点,因为利用ID3和C4.5生成的决策树为n叉树,所以掌柜这里定义了一个字典来进行存储,其中key为特征取值,value为对应的孩子节点,值得一提的是在sklearn框架中采用的是二叉树来进行实现;第10行criterion_value则是用来保存当前节点对应的信息熵;第11行是记录以当前节点为根节点时其叶子节点的个数;第12行是记录以当前节点为根节点时其所有叶子节点的损失和,这两行主要用于后续剪枝部分的代码实现。

同时,为了在打印输出构建好的决策树的能够更加方便,我们需要定义如下方法,代码如下:

这里值得一提的是__str__方法是Python中每个类对象都有的一个方法,只是默认情况下没有进行实现。__str__方法的作用是在通过print函数打印类的实例化对象时输出的便是上面我们定义的信息,而不是默认情况下的对象内存地址。

例如:

2.2 实现思路

在介绍完节点定义部分的内容后,我们便可以根据前面介绍的决策树生成步骤递归地来构建决策树。

输入:训练数据集D,特征集A,阈值ε;

输出:决策树。

(1) 若D中所有样本属于同一类Ck,则T为单节点树,并将Ck作为该节点的类标记,返回T;

(2) 若A=,则T为单节点树,并将D中样本数最多的类Ck作为该 节点的类标记,返回T

(3) 否则,计算A中各特征对D的信息增益(比),选择信息增益(比)最大的特征Ag

(4) 如果Ag的信息增益比小于阈值ε,则置T为单节点树,并将D中样本数最多的类Ck作为该节点的类标记,返回T

(5) 否则,对Ag的每一个可能值ai,以Ag=ai将D分割为若干非空子集,并建立为子节点;

(6) 对于第i个子节点,以Di为训练集,以A{Ag}为特征集,递归地调用(1)-(5),得到子树Ti,并返回Ti

根据上述生成步骤,下面我们先来实现其中需要用到的一些计算方法。

2.3 信息熵与条件熵实现

无论是ID3还是C4.5生成算法都需要计算样本的信息熵、特征的信息熵(在C4.5中)以及在某一特征取值下的条件熵。同时,需要说明的是,在本篇文章中掌柜是以离散型输入特征为例进行的实现讲解,在下一篇文章中再来介绍能够同时处理离散型和连续型特征的实现方式。

根据信息熵计算公式

H(D)=k=1K|Ck||D|log2|Ck||D|(1)

可知,其代码实现如下:

在上述代码中,第2-10行是定义类DecisionTree的初始化函数,其中第6行是保存决策树的根节点,第7行用来指定节点中停止分裂的最少样本数;第8行用来指定节点停止分裂的最小阈值,第9行指定采用ID3还是C4.5进行特征划分,第10行是后续实现剪枝过程时的惩罚参数;第12行则是定义一个方法来计算信息熵,主要用于ID3和C4.5中计算样本的信息熵,以及C4.5中特征的信息熵;第13行是计算得到当前输入有多少重类别(或特征取值);第14-15行则是判断如果只有一个类别则信息熵为0;第17-19行是分别遍历么一个类别,然后根据式(1)来计算信息熵。

进一步,在实现完信息熵的计算后,可以根据条件熵的计算公式

H(D|A)=i=1n|Di||D|H(Di)=i=1n|Di||D|k=1K|Dik||Di|log2DikDi(2)

来编码实现条件熵的计算,代码如下:

在上述代码中,第1行X_feature表示数据集中的某一列特征,y_class表示样本的类别标签;第2行是计算得到特征维度中的取值情况;第4行开始是遍历每一个特征取值;第5-6行是取当前特征类别对应的索引以及标签;第7-8行是先计算样本对应的信息熵,然后再计算条件熵。

例如对于如下数据集来说

表 1. 示例计算数据

信息熵为

H(D)=(515log2515+1015log21015)0.918(3)

各特征取值下的条件熵为

H(D|A1)=[715H(D1)+815H(D2)]=715(37log237+47log247)815(78log278+18log218)0.75H(D|A2)=[815H(D1)+715H(D2)]=815(48log248+48log248)715(17log217+67log267)0.81H(D|A3)=[315H(D1)+415H(D2)+815H(D3)]=315(13log213+23log223)415(14log214+34log234)815(38log238+58log258)0.91(4)

以上具体计算过程可以参考文章XXX第8.2节内容

所有示例代码均可从此仓库获取:https://github.com/moon-hotel/MachineLearningWithMe

对于上述计算过程,我们可以通过上面介绍的代码来进行完成,如下所示:

2.4 特征划分标准实现

在实现完信息熵和条件熵的计算过程后,下面我们再来封装一个方法来根据指定参数返回信息增益或者是信息增益比的结果来作为节点划分时的标注,实现代码如下所示:

在上述代码中,第1行的3个参数分别是传入的信息熵、对应的特征列和样本标签;第2行是计算每个特征下的条件熵;第3行是计算对应特征下的信息增益;第4-8行是根据参数来返回对应的划分指标,其中第7行是计算特征对应的信息熵;第10行则是进行错误提示。

2.5 决策树构建实现

在做完前面的整个铺垫过程后,下面就可以正式的来递归构建生成决策树了。由于这部分代码较长,掌柜这里就分成两部分来进行介绍,第一部分实现代码如下所示:

在上述代码中,第1行中data表示当前节点中的所有样本点,f_ids表示当前节点中还要哪些特征是可用的(剩余特征);第2行是得到每个样本在原始数据集中的索引,因此在调用_build_tree方法前需要在训练集的最后一列追加上样本索引;第3行是根据样本索引取对应的标签;第4-8行是先定义当前节点并保存对应的节点信息;第9-10行是判断根节点是否为空,如果为空则将当前节点作为根节点;第11-15行是2.2节生成步骤中第(1)和(2)步的体现,同时还增加了最小样本数的判断条件;第14行是根据多数原则确定当前节点对应的类别;第16行是计算当前节点所有样本对应的信息熵。

紧接着,第二部分实现代码如下所示:

在上述代码中,第3-7行是遍历当前节点可进行选择的划分特征,然后计算各个特征划分下的信息增益(比)来选择最佳特征,这便是2.2节生成步骤后第(3)步的体现;第8-10则是判断当前计算得到的最大划分指标是否小于阈值,如果是则直接返回当前节点,这是生成步骤中第(4)步的体现;第12行是取最佳划分特征中特征的取值情况;第13-14则是从一开始传入的特征列表f_ids中去处此时选中的最佳特征,而之所以要先复制是因为后续要依次遍历最佳特征中的每个特征取值并递归的构建决策树(第19行),而f_ids是一个列表传入函数_build_tree时本质上是传入的地址,这就会导致每次递归操作都是改变之中的值,因此需要先进行复制;第16-19行是依次遍历每个取值情况,然后根据当前特征维度的取值,来取对应的样本索引,并递归的进行节点划分,这是生成步骤中第(5)步的体现。

到此,对于整个决策树的递归构建过程就介绍完了,我们可以通过如下函数来进行决策树的拟合:

在上述代码中,第1行X,y便是训练时所用到的数据和标签,需要注意的是输入的特征X必须为categorical类型,同时X的形状必须为i[n_samples, n_features]y的形状必须为[n_samples,];第3行是得到当前数据集的类别数量;第4行是得到特征的序号;第5行是在X的右侧追加一列作为每个样本的索引;第6行是开始递归构建决策树;第7行是进行剪枝步骤,这部分内容将在第3节中进行介绍。

2.6 决策树遍历实现

为了便于查看决策树的构建结果,以及在剪枝过程中需要从下往上按层遍历所有节点,所以这里需要实现一个层次遍历的方法。遍历的思路大致是用一个队列来保存当前层的所有节点,然后遍历当前层的所有节点时再将当前层所有节点的孩子节点依次放入到队列中,同时在这一过程中再将每一层的遍历结果保存起来即是最后层次遍历的结果。实现代码如下所示:

在上述代码中,第1行return_node是用来判断返回所有层次遍历的结果(后续剪枝时会用到)还是直接输出遍历结果;第7-14行是按层来遍历所有节点,然后再将所有节点的孩子节点放入到队列中再次循环遍历这些节点,其中tmp是用来保存当前层的所有节点;第15-16行是返回所有层次遍历的结果;第18-21行是直接输出整个层次遍历的结果。

所有示例代码均可从此仓库获取:https://github.com/moon-hotel/MachineLearningWithMe

对于表1中的示例数据来说,在其拟合结束后便可以得到如下所示的层次遍历结果(由于篇幅有限这里就只展示部分输出信息,大家可以自己运行源码进行查看):

在上述运行结果中,特征ID取值0,1,2分别表示表1中的X1,X(2),X(3)这3个特征,即“有无工作”、“有无房屋”和“学历等级”。根据上述运行结果,我们便可以得到如图1所示的决策树。

图 1. 决策树拟合结果图

如图1所示便是根据上述代码拟合得到的决策树,其中每个节点中第2行表示样本索引,第3行表示每个类别中样本的数量。例如对于最后一层左边的叶子节点来说,[1,2]表示该节点中包含有原始训练样本中的第1和2行样本,[0,2]表示在这个节点中,类别为0的样本有0个,类别为1的样本后2个。

同时,可以发现,这与我们在前面通过手动计算生成的决策树是一样,如图2所示。

图 2. 决策树计算结果图

2.7 样本预测实现

在完成决策树的拟合过程后,下一步便是如何来实现决策树的预测过程。总的来讲,新样本的预测思路为:从根节点开始,根据当前节点的划分维度取测试样本中对应的特征维度,然后再根据特征维度的取值进入到当前节点的孩子节点中;然后再迭代进行这一步骤,直到进入到叶子节点或当前节点的孩子节点中不存在满足特征取值的节点时停止,此时取当前节点对应对应的类别作为该测试样本的类别即可。

具体地,我们先来实现一个样本的预测过程,实现代码如下:

在上述代码中,第2行是初始化根节点为当前节点;第4-9行则开始按照上述思路进行遍历;第4-5行是取当前节点进行分裂时所使用到的特征ID,并取新样本x汇总对应的特征值;第6-8行是判断如果当前节点为叶子节点,或者由于数据集不充分当前节点的孩子节点不存在下一个划分节点的某一个取值(例如根据上面测试数据集构造得到的id3树,对于特征['0','1','D']来说,在遍历最后一个特征维度时,取值'D'就不存在于孩子节点中)时,则返回当前节点中各个类别的分布情况,即图1中每个节点的第3行结果;第9行是根据当前新样本x的特征取值进入到当前节点对应的孩子节点中。

所有示例代码均可从此仓库获取:https://github.com/moon-hotel/MachineLearningWithMe

进一步,便可以根据_predict_one_sample方法来循环预测得到测试集中所有样本的类别,实现代码如下:

在上述代码中,第1行X为新输入的测试样本,需要注意的是其形状必须为[n_samples,n_features];第3-4行是循环遍历每个测试样本,并将预测后的结果放到results中;第7行是根据预测得到的样本类别分布结果来计算得到每个样本真实的预测类别。

例如对于如下原始结果来说

其真实的预测结果为:

2.8 使用示例

以表1中的示例数据为例,DecisionTree 的使用方法如下:

上述代码运行结束后,其输出结果便是2.7节末的示例结果。

下面,掌柜再以一个垃圾邮件分类数据集进行示例,该数据采用的是不考虑词频的词袋模型表示方法。

首先我们需要载入数据集,代码如下所示:

在上述代码中,第2行为载入原始分词后的样本,第5-7行则是分别将训练集和测试集转换为不考虑词频的词袋向量表示。

进一步,便可以构建一个决策树模型来对垃圾邮件进行分类了,代码如下:

在上述代码中,掌柜还特意加入了sklearn中的决策树来进行对比。待上述代码运行结束后便可得到如下所示结果:

从上述结果可以看出,两者在准确率上仅仅只有微小的差别。

3 决策树剪枝

在介绍完ID3和C4.5的决策树算法实现过程后,下面我们再来看最后一部分内容,那就是ID3和C4.5的剪枝过程。

3.1 剪枝原理

根据文章第8章 决策树与集成学习第8.4节内容可知,ID3和C4.5决策树的剪枝过程是通过最小化决策树整体的损失函数或者代价函数来实现。设树T的叶节点个数为|T|t是树T的一个叶节点,该叶节点有Nt个样本点,其中类别k的样本点有Ntk个,k=1,2,...,K,Ht(T)为叶节点t上的经验熵,α0为参数,则决策树的损失函数可以定义为

Cα(T)=t=1|T|NtHt(T)+α|T|(5)

其中经验熵为

Ht(T)=kNtkNtlogNtkNt(6)

进一步有

C(T)=t=1|T|NtHt(T)=t=1|T|k=1KNtklogNtkNt(7)

此时损失函数可以写为

Cα(T)=C(T)+α|T|(8)

其中C(T)表示模型对训练数据的分类误差,即模型与训练集的拟合程度,|T|表示模型复杂度,参数α0控制两者之间的平衡。较大的α促使选择较简单的模型(树),较小的α促使选择较复杂的模型(树)。α=0意味着只考虑模型与训练集的拟合程度,不考虑模型的复杂度。可以发现,这里α的作用就类似于正则化中惩罚系数的作用。

具体地,决策树的剪枝过程可以总结成如下步骤:

输入:生成算法产生的整个树T,参数α

输出:修剪后的子树Tα

(1) 计算每个叶节点的经验(信息)熵;

(2) 递归地从树的叶节点往上回溯;设一组叶节点回溯到其父节点之前与之后的整体树分别为TB,TA,其对应的损失函数值分别是Cα(TB),Cα(TA),如果Cα(TA)Cα(TB)则进行剪枝,即将父节点变为新的叶节点。

(3) 返回(2),直到不能继续为止,得到损失函数最小的子树Tα

在正式实现剪枝部分的代码前,掌柜先来通过一个计算示例带着大家回顾一下剪枝地过程, 详细内容也可以参见文章决策树与集成学习第8.4节内容)

3.2 剪枝示例

如图3所示,在考虑是否要减掉“学历等级”这个节点时,首先需要计算的就是剪枝前的损失函数数值Cα(TB)。由于剪枝时,每次只考虑一个节点,所以在计算剪枝前和剪枝后的损失函数值时,仅考虑该节点即可。因为其它叶节点的经验熵对于剪枝前和剪枝后都没有变化。

图 3 决策树剪枝图

根据表2可知,“学历等级”这个节点对应的训练数据如表2所示(D12)。

表 2. 学历等级样本分布表

根据式(7)

C(TB)=t=12k=12NtklogNtkNt=((2log222+0)+(1log212+1log212))=2(9)

进一步,根据式(8)

Cα(TB)=C(TB)+α|TB|=2+2α(10)

同理可得,剪枝完成后树TA损失为

Cα(TA)=(3log234+1log214)+α3.25+α(11)

(10)(11)的结果可知,当设定α1.25时决策树便会执行剪枝操作,因为此时Cα(TA)=3.25+αCα(TB)=2+2α满足剪枝条件。

3.3 剪枝判断实现

根据上述过程可知,在是否要进行剪枝之前需要计算剪枝前和剪枝后对应节点的损失值,然后再判断是否进行剪枝。因此,根据式(7)可知我们首先需要实现的就是计算任意一个节点内部的损失。实现代码如下所示:

在上述代码中,第2行是计算得到当前节点中的样本类别数,即每个类别有多少个样本;第3行是计算当前节点一共有多少样本;第5-8行是计算整个节点内部的损失值。

所有示例代码均可从此仓库获取:https://github.com/moon-hotel/MachineLearningWithMe

例如对于某个节点来说,其样本标签为[1,1,1,0],那么其对应的损失为:

进一步,我们再来实现对于决策树剪枝之前和剪枝之后的损失比较,实现代码如下:

在上述代码中,第2-4行是在当前节点为叶子节点时,计算叶子节点对应的损失;第5行是计算当前点中所有样本的损失,也就是剪枝后的损失值;第6-7行是计算以当前节点为根节点其所有叶子节点的损失和,也就是剪枝前的损失值;第8-11行是判断是否满足剪枝条件,并返回相应的结果。

例如对于表2中"学历等级"这个节点来说,其剪枝前和剪枝后的损失为:

在上述结果中,第7-8行的输出便是对应式(10)(11)的计算过程。

3.3 剪枝过程实现

在实现完剪枝判断过程后,便可以依次遍历层次遍历的结果然后来判断是否对当前节点进行剪枝,而剪枝的做法也很简单直接将该节点children设置为空即可。整体实现代码如下所示:

在上述代码中,第2行是得到决策树的层次遍历结果;第3行是从下往上依次遍历每一层节点;第4行是取第i层的所有节点;第5-6行是开始遍历当前层中的每一个节点;第7-11行是用来累计以当前节点为根节点时其叶子节点的个数;第12-14行是判断是否需要剪枝操作。

3.4 使用示例

在实现完剪枝过程后,我们再来以3.2节中的例子进行示例。对于样本['0','1','S']['0','1','T']来说,根据图1可知其分别属于1和0这两个类别;而如果对“学历等级”这个极点进行剪枝操作,那么这两个样本的预测类别都将是1这个类别。整个实现过程如下所示:

上述代码运行结束后的结果如下所示:

4 总结

在这篇文章中,掌柜首先介绍了节点的定义以及回顾了ID3和C4.5决策树生成的原理;然后详细地分别介绍了信息熵与条件熵的实现、特征划分标准的实现、决策树的构建以及样本预测等;最后,一步一步介绍了决策树剪枝过程的代码实现,并通过具体的示例进行详细地解释。

本次内容就到此结束,感谢您的阅读!如果你觉得上述内容对你有所帮助,欢迎点赞分享!若有任何疑问与建议,请添加掌柜微信nulls8(备注来源)或文末留言进行交流。青山不改,绿水长流,我们月来客栈见

引用

[1] https://scikit-learn.org/stable/modules/naive_bayes.html#naive-bayes

[2] 决策树与集成学习

[3] 示例代码:https://github.com/moon-hotel/MachineLearningWithMe

推荐阅读

[1] 第1章 机器学习入门之环境安装配置(附高清PDF)

[2] 第2章 从零认识线性回归(附高清PDF与教学PPT)

[3] 第3章 从零认识逻辑回归(附高清PDF与教学PPT)

[4] 第4章 模型的改善与泛化(附高清PDF与教学PPT)

[5] 第5章 K近邻算法(附高清PDF与教学PPT)

[6] 第6章 朴素贝叶斯算法(附高清PDF与教学PPT)

[7] 第7章 文本特征提取与模型复用(附高清PDF与教学PPT)

[8] 第8章 决策树与集成学习