本内容将介绍用于分类的决策树(decision tree),以及 ID3、C4.5 和 CART 算法。
决策树(decision tree)是一种基本的分类与回归方法。决策树模型呈树形结构,分为分类树、回归树和模型树,前者用于分类,后者用于预测实数值。其主要优点是模型具有可读性,分类速度快。决策树学习通常包括 3 个步骤:特征选择、决策树的生成和决策树的剪枝。这些决策树学习的思想主要来源于由 Quinlan 在 1986 年提出的 ID3 算法和 1993 年提出的 C4.5 算法,以及由 Breiman 等人在 1984 年提出的 CART 算法。
本内容主要介绍分类决策树,不涉及回归树和模型树。可以参阅 机器学习系列:决策树 02 - CART 算法 了解 CART 算法和回归树、模型树。
一、决策树模型和学习
1.1 决策树模型
一棵决策树包含一个根节点、若干个内部节点和若干个叶节点。图-1 为一棵决策树,椭圆形为判断模块,进行特征判断;矩形为终止模块,进行结果决策。从根节点到每个叶节点的路径对应了一个判断测试序列。
用决策树进行预测,从根节点开始,对实例的某一特征进行判断,根据判断结果,将实例分配到其子节点。如此递归地对实例进行判断并分配,直至到达叶节点。最后根据叶节点对实例进行决策,分类树输出类别,回归树输出预测值。
图-1 决策树模型
1.2 决策树学习
决策树的学习,也称为决策树的生成或决策树的构造。决策树学习过程:得到原始数据集,选择最优划分特征,并根据此特征划分数据集,数据将被向下传递到子节点。如果子节点上的数据满足特定条件,将构建叶节点;否则,再次选择最优特征划分数据集。如此递归地划分数据集,最终将形成一棵决策树。决策树的生成就是递归地构建树的过程。
以分类树进行说明,上面提到的特定条件为:
- 当前节点包含的样本全属于同一类别,无需划分。
- 当前可划分属性集为空,或是所有样本在所有属性上取值相同,无法划分。
- 当前节点包含的样本集合为空,不能划分。
对上面提到的情况,构造叶节点的类别设定方法:
- 设定为该节点所含样本的类别(所有样本为同一类别)。
- 设定为该节点所含样本最多的类别(即采用多数表决法决定叶节点的类别)。
- 设定为其父节点所含样本最多的类别。
决策树学习的目的是为了产生一棵泛化能力强,即处理未见实例能力强的决策树。其基本流程如下:
函数 create_tree() 的伪代码大致如下:
create_tree():
if 数据集满足特定条件:
将该节点存为叶节点
return
end if
根据最优特征划分数据集,产生多个子集
for 每个划分的子集:
if 子集为空:
构造叶结点,其类别为父节点所含样本最多的类别
else:
调用函数 create_tree() 递归划分子集
end if
end for
决策树是一种贪心算法,它要在给定时间内做出最佳选择,但并不关心能否达到全局最优。
接下来我们需要了解如何选择最优划分特征,以及如何划分数据集。
二、特征选择
特征选择在于选取对训练数据具有分类能力的特征。这样可以提高决策树学习的效率。通常特征选择的准则有信息增益、信息增益比和基尼指数等。在具体介绍它们之前,我们先来了解一下熵和条件熵。
2.1 熵与条件熵
2.1.1 熵
在信息论与概率统计中,熵(entropy,又被称为信息熵)是表示随机变量不确定性的度量。假设 是一个取有限个值的离散随机变量,其概率分布为
则随机变量 的熵定义为
约定若 时,。 的最小值为 0,最大值为 。通常,式(1)中的对数以 2 、自然常数或 10 为底,熵的单位分别称为比特(bit)、纳特(nat)或 Hart。熵越大,随机变量的不确定性就越大。
2.1.2 条件熵
假设随机变量 的联合概率分布为
条件熵 表示在已知随机变量 的条件下随机变量 的不确定性。随机变量 给定条件下 的条件熵(conditional entropy),定义为 给定条件下 的条件概率分布的熵对 的数学期望
当熵和条件熵中的概率由数据估计(特别是极大似然估计)得到时,所对应的熵和条件熵分别称为经验熵(empirical entropy)和经验条件熵(empirical conditional entropy)。
2.2 信息增益
信息增益(information gain)表示得知特征 的信息而使得类 的信息的不确定性减少的程度。
特征 对训练数据集 的信息增益 ,定义为集合 的经验熵 与特征 给定条件下 的经验条件熵 之差,即
假设训练数据集为 ,存在以下设定:
- 为其样本容量,即样本个数。
- 训练数据集 有 个类 ,, 为属于类 的样本个数。
- 特征 有 个不同的取值 ,根据特征 的取值将 划分为 个子集 , 为 的样本个数。
- 子集 中属于类 的样本的集合为 , 为 的样本个数。
则使用特征 划分训练数据集 获得的信息增益为
其中, 为
ID3 算法就是使用信息增益来选择划分特征。
2.3 信息增益率
信息增益准则对可取值数目较多的特征有所偏好,为减少这种偏好可能带来的不利影响,可使用信息增益率(information gain ratio)来选择最优划分特征。
其中, 为
称为特征 的“固有值”(intrinsic value)。特征 的可能取值数目越多(即 越大),则 的值通常会越大。
C4.5算法就是使用信息增益率来选择划分特征。 需注意的是,增益率准则对可取值数目较少的特征有所偏好。因此,C4.5 算法并不是直接选择增益率最大的候选划分特征,而是使用了一个启发式:先从候选划分特征中找出信息增益高于平均水平的特征,再从中选择增益率最高的。
2.4 基尼指数
基尼指数(Gini index)定义为
从上面的公式可知,基尼指数有两种不同理解形式:
- 从式(8)来看, 表示从数据集 中随机抽取两个样本,其类别标记不一致的概率。
- 从式(9)来看, 表示从数据集 中随机选中的一个样本被分错的概率。
因此, 越小,则数据集 的纯度越高。
则使用特征 划分训练数据集后的基尼指数为
其中 为
CART 算法进行分类时,就是使用基尼指数来选择划分特征。
三、划分数据集方法
3.1 离散属性
针对离散属性,可以采用多分法或者二分法划分数据集。假设有数据集 ,其中属性 有 个不同的取值,根据属性 的取值将数据集划分为 个子集,就是多分法;选择其中的某个取值 ,根据该值将数据集划分为 2 个子集 和 , 包含属性 不大于 的样本, 包含属性 大于 的样本,就是二分法。
注意:如果采用多分法后,该属性不能作为其后代节点的划分属性;如果采用二分法,该属性可以继续作为其后代节点的划分属性。
3.2 连续属性
由于连续性属性的可取值不再有限,因此,不能直接根据连续型属性的可取值来对节点进行划分。此时,连续型属性离散化技术可派上用场。最简单的策略是采用二分法(bi-partition)对连续属性进行处理,C4.5 和 CART 决策树算法都使用该方法。
注意:因为连续属性只能采用二分法,所以该属性还可作为其后代节点的划分属性。
ID3 算法不能处理连续属性,C4.5 和 CART 算法可以处理连续型属性。
四、决策树的剪枝
在决策树学习中,为了尽可能对数据集进行正确分类,从而构建出过于复杂的决策树。这样产生的树往往对训练数据集的分类很正确,但对未知的测试数据的分类却没有那么准确,即出现过拟合现象。可以通过降低对决策树的复杂度(即去掉一些分支)来解决这个问题,这个过程称之为剪枝(pruning)。
决策树剪枝有预剪枝(prepruning)和后剪枝(postpruning)两种。预剪枝是指在决策树生成过程中,对每个节点在划分前后进行估计,若当前节点的划分不能带来决策树泛化性能提升,则停止划分并将当前节点标记为叶节点。后剪枝则是先从训练集生成一棵完整的决策树;然后从上到下找到叶节点,用测试集来判断将这些叶节点合并是否能降低测试误差,如果能就进行合并。
决策树的生成对应于模型的局部选择,决策树的剪枝对应于模型的全局选择。决策树的生成只考虑局部最优,相对地,决策树的剪枝则考虑全局最优。
在 机器学习系列:决策树 02 - CART 算法 里面会讲到剪枝的具体实现。
五、代码实现
在生成分类决策树时,ID3、C4.5 和 CART 三种算法的流程基本相同,基本上只需要在特征选择和特征划分时进行不同处理即可。下面给出 ID3 算法的 Python 代码实现(Python 3.x):
from math import log
class DecisionTreeID3:
def __init__(self):
pass
def creat_tree(self, data_set, feature_labels):
"""
创建决策树
data_set:创建决策树的数据集
feature_labels:特征名列表
"""
labels_list = [sample[-1] for sample in data_set]
# 数据集中的样本属于同一个类别,返回这个类别
if labels_list.count(labels_list[0]) == len(labels_list):
return labels_list[0]
# 当数据集中已经没有可划分的属性,采用多数表决法返回类别
if len(data_set[0]) == 1:
return self._majority_cnt(labels_list)
# 选择最优划分属性
best_feature = self._choose_best_feature_to_split(data_set)
best_feature_label = feature_labels[best_feature]
# 初始化树
tree = {best_feature_label: {}}
# 移除已经选择的划分属性
del feature_labels[best_feature]
# 获取当前划分属性的所有取值
feature_values = [sample[best_feature] for sample in data_set]
# set() 可以去除 feature_values 中存在的相同值
unique_values = set(feature_values)
for value in unique_values:
sub_feature_labels = feature_labels[:]
tree[best_feature_label][value] = self.creat_tree(
self._split_data_set(data_set, best_feature, value),
sub_feature_labels)
return tree
def _calc_shannon_ent(self, data_set):
"""
计算信息熵
"""
# 计算数据集中样本数量
samples_num = len(data_set)
labels_count = {}
# 遍历数据集中所有样本,计算每种类别的样本数量
for sample in data_set:
# 获取样本的标签值,即类别
cur_label = sample[-1]
if cur_label not in labels_count.keys():
labels_count[cur_label] = 0
labels_count[cur_label] += 1
shannon_ent = 0.0
# 求信息熵
for count in labels_count.values():
prob = float(count)/samples_num
shannon_ent -= prob*log(prob, 2)
return shannon_ent
def _split_data_set(self, data_set, axis, value):
"""
按照给定特征划分数据集,返回数据集中满足特征值为 value 的子集
data_set:待划分的数据集
axis:划分数据集的特征
value:划分数据集的特征值
"""
sub_data_set = []
for feat_vec in data_set:
if feat_vec[axis] == value:
reduced_feat_vec = feat_vec[:axis]
reduced_feat_vec.extend(feat_vec[axis+1:])
sub_data_set.append(reduced_feat_vec)
return sub_data_set
def _choose_best_feature_to_split(self, data_set):
"""
选择最好的划分属性
data_set:待划分的数据集
"""
# 获取数据集的样本数量
samples_num = len(data_set)
# 获取样本特征值数量
feature_num = len(data_set[0]) - 1
# 计算数据集未划分前的信息熵
base_entropy = self._calc_shannon_ent(data_set)
best_info_gain = 0.0
best_feature = -1
# 遍历每一个特征
for i in range(feature_num):
feature_values = [sample[i] for sample in data_set]
unique_vals = set(feature_values)
new_entropy = 0.0
# 遍历当前特征的每一个值,并计算出划分数据集后信息熵
for value in unique_vals:
sub_data_set = self._split_data_set(data_set, i, value)
prob = float(len(sub_data_set))/samples_num
new_entropy -= prob*self._calc_shannon_ent(sub_data_set)
info_gain = base_entropy - new_entropy
# 获取信息增益最高的划分特征和特征值
if info_gain > best_info_gain:
best_info_gain = info_gain
best_feature = i
return best_feature
def _majority_cnt(self, labels_list):
"""
返回数量最多的类型
labels_list:类型列表
"""
labels_count = {}
for label in labels_list:
if label not in labels_count.keys():
labels_count[label] = 0
labels_count[label] += 1
sorted_labels_count = \
sorted(labels_count.items(), key=lambda x: x[1], reverse=True)
return sorted_labels_count[0][0]
def get_training_datas():
data_set = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
feature_labels = ['no surfacing', 'flippers']
return data_set, feature_labels
if __name__ == "__main__":
data_set, feature_labels = get_training_datas()
decision_tree_mode = DecisionTreeID3()
decision_tree = decision_tree_mode.creat_tree(data_set, feature_labels)
print(decision_tree)
疑问:
- C4.5 对离散型属性是采用二分还是多分?
参考:
[1] 周志华《机器学习》
[2] 李航《统计学习方法》
[3] 《机器学习实战》