blue吖
2021/09/27阅读:96主题:灵动蓝
决策树——ID3算法和C4.5算法的理论基础与python实现
1.前言
决策树是一种常用的机器学习算法,既可以做分类任务也可以做回归任务,本文主要讨论用于分类任务的决策树。决策树与数据结构中的树相同,也是由一个根结点、若干个内部结点和若干个叶结点构成。这里我们以在相亲过程中女生的决策过程来直观的理解决策树,如下图所示,图片来自于《百面机器学习》。在决策树中根结点和内部结点对应于一个特征测试,如图中的年龄就是一个特征,通过测试年龄是不是大于30将实例分配到其不同的子结点中,如此自上而下一直循环下去,直到走到叶结点,给出最后的决策结果,即见或不见。

最著名的决策树学习算法有**ID3算法、C4.5算法和CART算法,**本文主要讨论ID3算法和C4.5算法,CART将会在后面一篇文章中讨论。决策树模型的学习过程可以分为3个步骤:特征选择、决策树的生成和决策树的剪枝。 特征选择是为了选取对训练数据有分类能力的特征,决策树的生成其实就是在不断进行特征选择并划分训练数据的过程,直到叶结点结束。比如在上图中,首先根据年龄这个特征,将训练数据分为大于30的子集和小于30的子集,年龄小于30的子集又根据长相继续将数据分为很丑、一般和很帅三个子集。最后,因为生成的决策树往往会对训练数据分类准确,而对测试数据分类效果不好,即出现过拟合现象,所以要对生成的决策树进行简化,即减少叶结点的数量,这就需要对决策树进行剪枝。
2. ID3算法
2.1 信息增益
不同的算法进行特征选择的准则不同,ID3算法采用的是最大信息增益准则,C4.5算法采用的是最大信息增益比,而CART算法采用的是最大基尼指数。
为了说明信息增益是什么,首先要给出经验熵和经验条件熵的定义。在信息论与概率统计中,熵(entropy)是表示随机变量不确定性的度量。假设数据集为
,
表示数据集中的样本个数,
中的样本共有
个类别,则数据集
的经验熵
的计算公式为:
其中 是数据集 中属于第 类的样本子集, 则表示该子集的样本个数。其代码实现如下:
from collections import Counter
from math import log
#计算数据集的经验熵
def calc_HD(self,dataset):
dataset = np.array(dataset)
data_length = len(dataset)
label = dataset[:,-1] #要求数据集的最后一列为类别
label_count = Counter(label) #得到一个字典,key为类别,value为对应的类的数量
HD = -sum([(p/data_length)*log(p/data_length,2) for p in label_count.values()]) #课本式(5.7)
return HD
假设样本的某一个特征为 ,且特征 有 个不同的取值 ,根据特征 将数据集 划分为 个子集 为子集 中的样本个数,子集 中属于 类的样本集合为 , 为 中的样本个数,则特征 对于数据集 的经验条件熵的计算公式为:
计算经验条件熵的代码实现如下:
#计算特征A对数据集的经验条件熵
def calc_HDA(self,dataset,A=0):
data_length = len(dataset)
feature_sets = {}
#构建以特征A的取值划分的各个数据子集
for i in range(data_length):
feature = dataset[i][A]
if feature not in feature_sets:
feature_sets[feature] = []
feature_sets[feature].append(dataset[i])
HDA = 0
#课本式(5.8)
for D in feature_sets.values():
HDA += (len(D)/data_length)*self.calc_HD(D)
return HDA
有了经验熵和经验条件熵之后便可以得到信息增益(information gain)了,信息增益 即为二者之差:
2.2 决策树生成
ID3算法选择特征的过程就是计算出所有特征对数据集
的信息增益,然后选择信息增益最大的特征。
上面的定义有点套娃的感觉,有点绕,下面以贷款申请的例子来说明经验熵和经验条件熵的计算过程,假设有15个人在申请贷款,最后的类别即为申请成功与否,如下表所示。
ID | 年龄 | 有工作 | 有自己的房子 | 信贷情况 | 类别 |
---|---|---|---|---|---|
1 | 青年 | 否 | 否 | 一般 | 否 |
2 | 青年 | 否 | 否 | 好 | 否 |
3 | 青年 | 是 | 否 | 好 | 是 |
4 | 青年 | 是 | 是 | 一般 | 是 |
5 | 青年 | 否 | 否 | 一般 | 否 |
6 | 中年 | 否 | 否 | 一般 | 否 |
7 | 中年 | 否 | 否 | 好 | 否 |
8 | 中年 | 是 | 是 | 好 | 是 |
9 | 中年 | 否 | 是 | 非常好 | 是 |
10 | 中年 | 否 | 是 | 非常好 | 是 |
11 | 老年 | 否 | 是 | 非常好 | 是 |
12 | 老年 | 否 | 是 | 好 | 是 |
13 | 老年 | 是 | 否 | 好 | 是 |
14 | 老年 | 是 | 否 | 非常好 | 是 |
15 | 老年 | 否 | 否 | 一般 | 否 |
在这个数据集中,样本的类别数
,即贷款申请是否成功两种类别,每个样本有4个特征,分别为年龄、是否有工作、是否有自己的房子和信贷情况,这里分别用
来表示这几个特征。在上面的定义中,我觉得最绕的就是样本子集
和样本子集
,一定要区分清楚,在上面这个数据集中,
是根据样本类别来分的,即
,
,而
是根据特征来分的,比如根据年龄这个特征的取值老和年轻就将数据集
分成了
、
和
三个子集。而在
中又可以根据样本类别将
分成
两个子集,在
中可以根据样本类别将
分成
两个子集,在
中可以根据样本类别将
分成
两个子集。
ID3算法生成决策树的过程为:从根结点开始,对结点计算所有特征的信息增益,选择信息增益最大的特征作为结点的特征,由该特征的不同取值建立子结点;再对子结点递归的执行以上过程,构建决策树;直到所有特征的信息增益均很小或者没有特征可以选择为止。ID3算法的具体过程如下:
接下来就通过上面贷款申请的数据来讲解利用ID3算法建立决策树的过程。首先,计算数据集
经验熵:
然后计算各个特征对数据集 的信息增益:
同理可计算出 对数据集 的信息增益:
可以发现,特征 即是否有自己的房子的信息增益最大,所以选择特征 作为根结点的特征。特征 将数据集 划分为两个子集,即两个子结点: 和 。由于 的样本都属于同一类,所以它成为一个叶结点,结点类标记为“是”。对于 ,则需要继续从 中选择新的特征对其进行划分,计算 对 的的信息增益:
所以我们选择信息最大的特征 来作为此结点的特征,特征 将数据集 划分为两个子结点 和 ,由于两个结点中的样本都分别属于同一类,所以这两个结点都是叶结点,因为没有可以再进行划分的结点了,所以决策树就生成完毕了,生成的决策树如下图所示:

ID3算法的代码实现如下:
#找出信息增益最大的特征
def calcBestFeature(self,dataset):
feature_count = len(dataset[0]) - 1 #特征的个数
GDA = [0]*feature_count
for i in range(feature_count):
#计算信息增益,课本式(5.9)
GDA[i] = self.calc_HD(dataset) - self.calc_HDA(dataset,A=i)
max_GDA = max(GDA) #最大的信息增益
best_feature = GDA.index(max_GDA) #最大的信息增益对应的特征索引
return best_feature,max_GDA
#利用ID3算法递归生成决策树
def createTree(self,train_data):
label_train = train_data.iloc[:,-1] #要求输入的数据为pd.DataFrame类型
features = train_data.columns[:-1] #并要求最后一列为类别标记列
# 1,若D中实例属于同一类Ck,则T为单结点树,并将类Ck作为结点的类标记,返回T
if len(label_train.value_counts()) == 1:
return Node(root=True, label=label_train.iloc[0])
# 2, 若A为空,则T为单结点树,将D中实例树最大的类Ck作为该结点的类标记,返回T
if len(features) == 0:
return Node(root=True, label=label_train.value_counts().index[0])
# 3,计算最大信息增益 ,Ag为信息增益最大的特征
best_feature, max_GDA = self.calcBestFeature(np.array(train_data))
best_feature_name = features[best_feature]
# 4,Ag的信息增益小于阈值eta,则置T为单结点树,并将D中是实例数最大的类Ck作为该结点的类标记,返回T
if max_GDA < self.threshold:
return Node(root=True, label=label_train.value_counts().index[0])
# 5,根据Ag的取值构建数据子集
node_tree = Node(root=False, feature_name=best_feature_name, feature=best_feature)
# 得到特征的取值列表
feature_list = train_data[best_feature_name].value_counts().index
for f in feature_list:
sub_train_df = train_data.loc[train_data[best_feature_name] ==
f].drop([best_feature_name], axis=1)
# 6, 递归生成决策树
sub_tree = self.createTree(sub_train_df)
node_tree.add_node(f, sub_tree)
return node_tree
3. C4.5算法
3.1 信息增益比
ID3算法采用的最大信息增益准则存在偏向于选择取值较多的特征的问题,信息增益比则是对这个问题进行了改进,信息增益比记为 ,其计算公式如下:
其中, 为数据集 关于特征 的取值的熵,其计算公式如下:
其中 为特征 的取值个数。
3.2 决策树生成
C4.5算法与ID3算法很相似,只是选择特征的准则不同,C4.5算法根据最大增益比准则来选择特征,其算法过程如下:
我们还是以上面的贷款申请的例子来讲解C4.5算法生成决策树的过程,各个特征的信息增益我们已经在上文得到了,要求得信息增益比,则只需求得每个特征的取值对数据集的熵即可,计算过程如下:
于是便可得到各个特征对数据集 的信息增益比:
根据最大增益比准则,选择特征 作为根结点的特征,接下来与ID3算法相似,继续求 对 的信息增益比, 的样本集合为 ,首先求特征 的取值对 对子数据集 的熵:
同理可求得:
所以可求得 对 的信息增益比为:
因为特征
的信息增益比最大,所以选择
作为此结点的特征,可以发现,C4.5算法与ID3算法对于此贷款申请数据集生成的决策树是一样的,但是通过比较特征的信息增益比和信息增益可以发现,特征取值多的特征(比如年龄和信贷情况)的信息增益比相较于其信息增益有了一定程度的下降,而特征取值少的特征(比如是否有工作和是否有房子)的信息增益比相较于其信息增益有了一定程度的上升,这就展示了信息增益比确实对信息增益所存在的偏向于选择取值较多的特征的问题进行了改进。
C4.5算法与ID3算法只有特征选择的准则不同,其特征选择的代码如下:
# 计算数据集D关于特征A的值的熵
def calc_HAD(self,dataset,A=0):
dataset = np.array(dataset)
data_length = len(dataset)
feature = dataset[:,A]
feature_values = Counter(feature) #得到一个字典,key为类别,value为对应的类的数量
HD = -sum([(p/data_length)*log(p/data_length,2) for p in feature_values.values()])
return HAD
#找出信息增益比最大的特征
def calcBestFeature(self,dataset):
feature_count = len(dataset[0]) - 1 #特征的个数
GRDA = [0]*feature_count
for i in range(feature_count):
#计算信息增益,课本式(5.9)
GDA[i] = self.calc_HD(dataset) - self.calc_HDA(dataset,A=i)
#计算信息增益比,课本式(5.10)
GRDA[i] = GDA[i] / self.calc_HAD(dataset, A=i)
max_GRDA = max(GRDA) #最大的信息增益比
best_feature = GRDA.index(max_GRDA) #最大的信息增益比对应的特征索引
return best_feature,max_GRDA
4.决策树的剪枝
为了提高决策树模型的泛化能力,避免过拟合,往往需要对生成的决策树进行剪枝。 决策树的剪枝通常有两种方法,分别为预剪枝(pre-pruning)和后剪枝(post-pruning)。
预剪枝的核心思想是在树中结点进行扩展之前,先计算当前的划分是否能够带来模型泛化能力的提升,如果不能则不在继续生长子树。此时可能存在不同类别的样本同时存在与结点中,按照多数投票的原则判断该结点所属类别。预剪枝对于何时停止树的生长有以下几种方法:
-
当树到达一定深度的时候,停止树的生长; -
当到达当前结点的样本数量小于某个阈值的时候,停止树的生长; -
计算每次分裂对测试集的准确度提升,当小于某个阈值的时候,不再继续扩展。
后剪枝的核心思想是让算法生成一颗完全生长的决策树,然后自下而上计算是否剪枝。剪枝的过程是将子树删除,用一个叶结点替代,该结点的类别同样按照多数投票的原则进行判断。相比于预剪枝,后剪枝通常可以得到泛化能力更强的决策树,但时间开销会更大。
常见的后剪枝方法有错误率降低剪枝、悲观剪枝、代价复杂度剪枝和最小误差剪枝等等。下面我们主要讨论代价复杂度剪枝(Cost Complexity Pruning,CCP)的实现过程。如果想了解其他剪枝方法可以去B站https://space.bilibili.com/406882224/search/video?keyword=%E5%89%AA%E6%9E%9D观看,我感觉这个UP主讲的很好。
决策树的剪枝通常通过极小化决策树整体的损失函数或代价函数来实现。设完全生长的决策树为
,其叶结点个数为
,
为决策树
的叶结点,且该叶结点上有
个样本点,其中属于第
类的样本点有
个,
,则决策树的损失函数可以定义为:
其中 为叶结点 的经验熵,其计算公式如下:
损失函数中的第一项表示模型对训练数据的拟合程度,第二项中的
表示模型的复杂度,即叶结点越多,模型越复杂。参数
是用来平衡模型拟合能力与复杂度的参数,较大的
促使选择比较简单的模型,但是拟合能力相对较弱,较小的
促使选择相对复杂的模型,此时拟合能力更强。损失函数就是为了在保证拟合能力的基础上来降低模型的复杂度。值得注意的是,在ID3算法和C4.5算法的剪枝中,参数
是人为指定的,而在CART算法中,则不再是人为指定,详细的内容会在下一篇文章中介绍。
代价复杂度剪枝的实现过程如下:
-
输入:ID3算法或C4.5算法生成的决策树,参数 ; -
输出:剪枝后的子树 。
(1) 计算每个结点的经验熵。
(2) 递归的从树的叶结点向上回缩。设剪枝前后的树分别为
和
,如果:
则进行剪枝。
(3) 重复步骤(2),直到不能继续为止(只剩下根结点和叶结点),此时就得到了损失函数最小的子树
。 剪枝过程的示意图如下所示:

5.参考资料
1.李航《统计学习方法》
2.诸葛越《百面机器学习》
3.周志华《机器学习》
4.https://www.zhihu.com/question/22697086
5.https://www.bilibili.com/video/BV1mf4y1L7Ko?spm_id_from=333.999.0.0
6.https://github.com/fengdu78/lihang-code
作者介绍