一、决策树模型
决策树(decision tree) 是一类常见的机器学习方法,主要用作分类任务。一般一棵决策树包含一个根节点、若干个内部节点和若干个叶节点,叶节点对应于决策结果,其他每个节点则对应于一个属性测试。每个节点包含的样本集合根据属性测试的结果被划分到子节点中。根节点包含样本全集,从根节点到每个叶节点的路径对应了一个判定测试序列。决策树判定流程遵循简单且直观的分支策略。
决策树由上到下的每轮判定都会选择最佳属性,一般而言,随着划分过程不断进行,我们希望决策树的分支结点所包含的样本尽可能属于同一类别,信息熵就是度量样本集合纯度最常用的一种指标。
1 信息量和信息熵
信息数学理论以概率论和统计学为基础,用多个信息量来衡量信息。在信息论中,信息用来表示事物的不确定性,任何已经确定的事物都不含有信息,例如:“明天是晴天”包含信息,“太阳东升西落”不包含信息。换句话说,信息中蕴含的是一个服从某个概率分布的随机变量,随机变量的取值就是一个事件。
1.1 自信息量
自信息量(Self-information)由香农提出,是与概率空间中的单一事件或离散随机变量的值相关的信息量的量度,表示为:其中是从消息空间中所有可能的选择中选择消息的概率,对数的底只影响比例因子,从而影响表示测量的信息内容的单位。如果对数以2为底,则信息的度量以香农或更常见的比特(bit)为单位表示。
从公式可以看出,如果是一个确定的事件,则,,即不包含任何信息;不频繁出现的消息比频繁出现的消息包含更多信息。还可以证明,两个(或多个)不相关消息的复合消息将具有一定量的信息,该信息量是每个消息单独的信息度量的总和,假设和独立,根据,因此有:根据概率公式,可以得到条件自信息量,顾名思义,一个条件概率对应的自信息量即为条件自信息量。表示在确定的条件下,发生所带来的自信息量。
1.2 信息熵
自信息量只能反映一个随机事件拥有的信息量,并不能反映整体的不确定性。信息熵则被定义为来自消息空间中的消息的平均自信息量(数学期望)。通常用表示所拥有的不确定性,信息熵公式为:其中表示期望运算。
熵的一个重要属性是,当消息空间中的所有消息都是等概率时(比如),熵最大化,例如(1)式中最大化:。
另外一个重要的特例是二元熵函数:
1.3 条件熵
给定随机变量的特定值,此条件下的的条件熵为:其中,,即给定下发生的条件概率。条件熵的一个基本属性为:
1.4 互信息
互信息是通过观察另一个随机变量可以获得多少关于一个随机变量的信息的度量(在西瓜书中,也将其称为信息增益),它的原始定义如下:其中表示互信息,在概念上,它代表了通过观察可以获得的关于的平均信息量。互信息的一个基本属性是:也就是说,与不知道相比,知道,我们可以在编码时平均节省位。
1.5 机器学习中的定义
根据上文中的公式,下面推导决策树模型划分最优属性的方法。
在机器学习中,假设当前样本集合为,其中第类样本占所有样本的比例为,此时根据(1)式信息熵的定义,的信息熵为:其中代表标签的类别数(可能取值数),比如二分类问题中,。这里使用了以二为底的对数,也就是以比特为单位。另外约定如果,则,这一点通过计算零点的右极限可以得出。
假设离散属性有个可能的取值,如果使用对样本集进行划分,我们将表示为中所有在属性上取值为的样本。根据(2)式,互信息(西瓜书也叫信息增益)为:
其中,可以根据条件熵的定义计算:在这里,这个公式可以理解为在属性的取值已知后,样本类别这个随机变量的不确定性减小的程度。若根据某个属性计算得到的信息增益越大,则说明在知道其取值后样本集的不确定性减小的程度越大,也即为纯度提升越大。因此我们可以用互信息(信息增益)来进行决策树的划分属性选择。
2 决策树学习算法
2.1 通过示例理解ID3决策树算法
决策树算法是典型的分治算法。ID3决策树就是通过信息增益为准则来选择划分属性的算法,结合上一节的公式,通过一个具体的实例介绍整个算法的流程。
数据集为银行贷款数据,如图:

首先将数据集编码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| def load_data(): X = np.array([ [0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 1, 0, 1, 1], [0, 1, 1, 0, 1], [0, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 1, 0], [1, 1, 1, 1, 1], [1, 0, 1, 2, 1], [1, 0, 1, 2, 1], [2, 0, 1, 2, 1], [2, 0, 1, 1, 1], [2, 1, 0, 1, 1], [2, 1, 0, 2, 1], [2, 0, 0, 0, 0], ]) return X
|
下面根据(3)式,计算,由于本例的数据集用于预测是否进行贷款,显然,因此整个完整数据集中,正例为,反例为,代入公式可得:代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| def entropy(X): """ 计算X的信息熵 :param X: :return: """ Dn = X.shape[0] value_arr, count_arr = np.unique(X[:, -1], return_counts=True) ent = 0 for value, count in zip(value_arr, count_arr): print(f"value:{value},count:{count},all_count:{Dn},rate:{count / Dn}") ent -= count / Dn * np.log2(count / Dn) return ent
|
下面要计算每个特征的信息增益,然后选择最优特征进行划分,例如我们要计算“年龄”的信息增益,首先计算条件熵,根据公式(5):根据公式(4),可得年龄的信息增益为:同理可以得到其余所有特征的信息增益,分别为:0.324,0.412,0.363,因此我们选择第三个特征作为最优特征。
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| def choose_best_feature(X): """ 计算互信息(信息增益),选择信息增益最大的最佳特征 :param X: 数据集 :return: """ Fn = X.shape[1] - 1 all_ent = entropy(X) gain = None feature = None for i in range(Fn): value_arr = np.unique(X[:, i]) feature_ent = 0 for value in value_arr: data_part = np.delete(X[np.where(X[:,i] == value)], i, axis=1) feature_ent += data_part.shape[0]/X.shape[0]*entropy(data_part) cur_gain = all_ent - feature_ent print(f"特征:{i},信息增益:{cur_gain}") if gain is None or cur_gain > gain: gain = cur_gain feature = i return feature
|
有了选择最优特征的方法后,就可以构建决策树了,这是一个递归过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| def fit(X, feature_map): """ 构建决策树 :param X: 数据集 :param feature_map: 特征中文标签,方便查看 :return: tree """ y_uniq = np.unique(X[:, -1]) if y_uniq.shape[0] == 1: return y_uniq[0] feature = choose_best_feature(X) tree = {feature_map[feature]: {}} value_arr = np.unique(X[:, feature]) if value_arr.shape[0] == 1: return np.unique(X[:, -1])[-1] for value in value_arr: data_part = np.delete(X[np.where(X[:, feature] == value)], feature, axis=1) if data_part.shape[0] == 0: return np.unique(X[:, -1])[-1] else: tree[feature_map[feature]][value] = fit(data_part, feature_map[:feature]+feature_map[feature+1:]) return tree
|
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| import numpy as np
def load_data(): feature_map = ["年龄", "是否有工作", "是否有房", "信贷情况"] X = np.array([ [0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 1, 0, 1, 1], [0, 1, 1, 0, 1], [0, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 1, 0], [1, 1, 1, 1, 1], [1, 0, 1, 2, 1], [1, 0, 1, 2, 1], [2, 0, 1, 2, 1], [2, 0, 1, 1, 1], [2, 1, 0, 1, 1], [2, 1, 0, 2, 1], [2, 0, 0, 0, 0], ]) return X, feature_map
def entropy(X): """ 计算X的信息熵 :param X: :return: """ Dn = X.shape[0] value_arr, count_arr = np.unique(X[:, -1], return_counts=True) ent = 0 for value, count in zip(value_arr, count_arr): ent -= count / Dn * np.log2(count / Dn) return ent
def choose_best_feature(X): """ 计算互信息(信息增益),选择信息增益最大的最佳特征 :param X: 数据集 :return: """ Fn = X.shape[1] - 1 all_ent = entropy(X) gain = None feature = None for i in range(Fn): value_arr = np.unique(X[:, i]) feature_ent = 0 for value in value_arr: data_part = np.delete(X[np.where(X[:, i] == value)], i, axis=1) feature_ent += data_part.shape[0] / X.shape[0] * entropy(data_part) cur_gain = all_ent - feature_ent print(f"特征:{i},信息增益:{cur_gain}") if gain is None or cur_gain > gain: gain = cur_gain feature = i return feature
def fit(X, feature_map): """ 构建决策树 :param X: 数据集 :param feature_map: 特征中文标签,方便查看 :return: tree """ y_uniq = np.unique(X[:, -1]) if y_uniq.shape[0] == 1: return y_uniq[0] feature = choose_best_feature(X) tree = {feature_map[feature]: {}} value_arr = np.unique(X[:, feature]) if value_arr.shape[0] == 1: return np.unique(X[:, -1])[-1] for value in value_arr: data_part = np.delete(X[np.where(X[:, feature] == value)], feature, axis=1) if data_part.shape[0] == 0: return np.unique(X[:, -1])[-1] else: tree[feature_map[feature]][value] = fit(data_part, feature_map[:feature]+feature_map[feature+1:]) return tree
X, feature_map = load_data() tree = fit(X, feature_map) print()
|
2.2 使用增益率选择划分属性的C4.5算法
在使用ID3算法时,我们有意忽略掉了数据集中的“ID”列,如果把它也看作一个属性,则可以计算出它的信息增益为0.998,远大于其他候选划分属性。这是因为“ID”是连续递增的,它将产生17个分支,每个分支只包含一个样本,这些分支节点的纯度已经达到最大值。但是这样的决策树不具备泛化能力,无法对新样本进行有效预测。
造成这种结果的原因是以信息增益作为准则,对取值数目较多的属性有所偏好,为了减少这种偏好带来的不利影响,C4.5决策树算法改进了这一策略。具体来说,C4.5决策树算法改为使用增益率(gain ratio)来选择最优划分属性,增益率定义为:其中:称为特征的固有值(intrinsic value),特征可能取值的数目越多(越大),则通常会越大。增益率使用信息增益和固有值的比值定义,它对取值数目较少的特征有所偏好。
计算信息增益率的代码实现和之前区别不大:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| def choose_best_feature(X): """ 计算互信息(信息增益),选择信息增益最大的最佳特征 :param X: 数据集 :return: """ Fn = X.shape[1] - 1 all_ent = entropy(X) gain = None feature = None ratio = None for i in range(Fn): value_arr = np.unique(X[:, i]) feature_ent = 0 iv = 0 for value in value_arr: data_part = np.delete(X[np.where(X[:, i] == value)], i, axis=1) feature_ent += data_part.shape[0] / X.shape[0] * entropy(data_part) iv -= data_part.shape[0] / X.shape[0]*np.log2(data_part.shape[0] / X.shape[0]) cur_gain = all_ent - feature_ent cur_gain_ratio = cur_gain/iv print(f"特征:{i},信息增益:{cur_gain},信息增益率:{cur_gain_ratio}") if ratio is None or cur_gain_ratio > ratio: ratio = cur_gain feature = i return feature
|
2.3 使用基尼系数选择划分属性的CART决策树
另一种划分最优属性的方法通过基尼系数(Gini index),注意这与根据洛伦兹曲线所定义的判断年收入分配公平程度的指标的基尼系数(Gini coefficient)不是同一概念。
基尼系数同样可以用来度量数据集的纯度:其中,表示第类样本占所有样本的比例,从直观上理解就是:从数据集中随机抽取两个样本,其类别标记不一致的概率。因此,基尼系数越小,则数据集的纯度越高。
采用之前的符号表示,当给定数据集时,特征的基尼系数定义为:基尼系数越大,则说明样本的不确定性越高。另外,以此方法生成的决策树一般是二叉树。
使用基尼系数的核心代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| def gini(X): """ 计算X基尼系数 :param X: :return: """ Dn = X.shape[0] gini = 1 value_arr, count_arr = np.unique(X[:, -1], return_counts=True) for value, count in zip(value_arr, count_arr): gini -= pow(count / Dn, 2) return gini
def choose_best_feature(X): """ 计算互信息(信息增益),选择信息增益最大的最佳特征 :param X: 数据集 :return: """ Fn = X.shape[1] - 1 all_ent = entropy(X) gain = None feature = None ratio = None Gini = None for i in range(Fn): value_arr = np.unique(X[:, i]) feature_ent = 0 iv = 0 g = 0 for value in value_arr: data_part = np.delete(X[np.where(X[:, i] == value)], i, axis=1) feature_ent += data_part.shape[0] / X.shape[0] * entropy(data_part) iv -= data_part.shape[0] / X.shape[0] * np.log2(data_part.shape[0] / X.shape[0]) g += data_part.shape[0] / X.shape[0] * gini(data_part) cur_gain = all_ent - feature_ent cur_gain_ratio = cur_gain / iv print(f"特征:{i},信息增益:{cur_gain},信息增益率:{cur_gain_ratio},基尼系数:{g}") if Gini is None or g < Gini: Gini = g feature = i print(f"选择特征:{feature_map[feature]}") return feature
|
3 剪枝
剪枝是决策树学习算法中用于处理过拟合的主要方法,当特征和样本复杂时,学得的决策树模型分支可能会越来越多,这时会出现过拟合现象。剪枝就是主动去除一些分支来降低过拟合风险,剪枝可以分为预剪枝和后剪枝两种方式。
3.1 预剪枝
预剪枝指的是在决策树生成过程中,对每个节点进行划分前先行估计,若当前节点的划分不能带来决策树泛化性能提升,则停止划分并将当前节点标记为叶节点。
问题的关键就在于如何判断决策树泛化能力提升,这里可以采用一些模型评价的方法,比如“留出法”,即预留一部分数据作为验证集进行性能评估,剩余的部分作为训练集使用。假设决策树基于信息增益准则进行最优特征划分,我们可以使用验证集对划分后和划分前的决策树进行评估,如果划分后验证集精度下降了,那么就禁止划分,反之允许划分。
通过这种策略,决策树的很多分支都没有展开,这不仅降低了过拟合的风险,还显著减少了决策树的训练时间开销和测试时间开销。但是在另一方面,有些分支的当前划分虽然不能提升泛化性能或导致泛化能力下降,但在其基础上进行的后续划分却有可能导致性能显著提高。预剪枝基于贪心本质禁止这些分支展开,给决策树带来了欠拟合的风险。
3.2 后剪枝
后剪枝是通过训练集先生成一棵完整的决策树,然后自底向上地对非叶子结点进行考察,若将该节点对应的子树替换为叶节点能够带来决策树泛化能力的提升,则将该子树替换为叶节点。
后剪枝策略通常比预剪枝策略保留的分支数量更多,因此一般情形下欠拟合的风险很小,泛化性能往往优于预剪枝决策树。但是后剪枝是在完全生成一颗决策树之后进行的,并且需要自底向上对树种所有非叶子节点逐一进行考察,因此其训练时间开销往往比预剪枝决策树要大。
4 连续值与缺失值处理
4.1 连续值处理
到目前为止讨论的仅仅是离散特征来生成决策树,现实中通常还会遇到连续属性,有必要讨论如何在决策树学习中使用连续属性。
由于连续属性的可取值数目不再有限, 因此,不能直接根据连续属性的可能取值来对节点进行划分。此时需要对连续属性进行离散化。C4.5决策树算法中采用最简单的策略就是二分法。具体来说就是,假设样本集上的特征为连续属性,其具有个不同的取值,将这些值从小到大进行排序,记为,基于划分点可将样本集分为两部分,和。其中,前者包含了那些在特征上取值不大于的样本,后者则包含了在特征上取值大于的样本。显然,对于相邻的取值,在区间中取任意值所产生的的划分结果相同,因此,对于连续的特征,我们可以考察包含了个元素的候选划分点集合:即把区间的中位点作为候选划分点,然后,就可以像离散值那样通过各种方式选择最优划分属性了。
需要注意的是,与离散属性不同,若当前的划分是连续属性,该属性还可以被子树中的节点当做划分属性。
4.2 缺失值处理
现实任务中常会遇到不完整样本,即样本的某些属性值缺失。尤其是在属性数目较多的情况下,往往会有大量样本出现缺失值。如果简单地放弃不完整样本,仅使用无缺失值的样本来进行学习,显然是对数据信息极大的浪费。因此,有些时候,有必要考虑利用有缺失属性值的训练、样例来进行学习。
假设样本集上的特征上有缺失值,令表示中在上没有缺失值的样本子集,显然我们可以仅根据来判断的优劣。假设属性上可选值有个:,令表示中在属性上取值为的样本子集,表示中属于第类()的样本子集,显然有。我们为每个样本赋予一个权重,并定义:直观地看,对属性,表示无缺失值样本所占的比例,表示无缺失值样本中第类所占的比例,则表示无缺失值样本中在属性上取值的样本所占的比例。因此,此时的信息增益计算式可以写作:其中:我们解决了属性值缺失时如何进行最优属性的选择问题。
接下来的问题是给定划分属性,若样本在该属性上的值缺失,如何对样本进行划分。对于这个问题,若样本在划分属性上的取值已知,则将划入与其取值对应的子节点即可,且样本权值保持为即可。若样本在划分属性上的取值未知,则将同时划入所有子节点,且样本权值在与属性值对应的子节点中调整为。直观地看,这就是让同一个样本以不同的概率划分到不同的子节点中去。C4.5决策树算法就采用的上述策略。
5 多变量决策树
若我们把每个属性视为坐标空间中的一个坐标轴,则个属性描述的样本就对应了维空间中的一个数据点。对样本分类就相当于在这个坐标空间中寻找不同类样本之间的分类边界,决策树的分类边界有一个明显的特点就是轴平行,即它的分类边界由若干个与坐标轴平行的分段组成。
参考西瓜书中的数据集:

将它作为训练集可以学得如下的决策树和分类边界:

显然,分类边界的每一段都是与坐标轴平行的,这样的分类边界使得学习结果有较好的可解释性,因为每一段划分都直接对应了某个属性取值。但在学习任务的真实分类边界比较复杂时,必须使用很多段划分才能获得较好的近似,此时的决策树会相当复杂,由于要进行大量的属性测试,预测时间开销会很大。
多变量决策树还可以通过斜边界进行划分,如图中红色线段所示:

在此类决策树中,非叶子节点不再是仅对某个属性,而是对属性的线性组合进行测试,每个非叶子节点是一个形如的线性分类器,其中是属性的权重,与传统的单变量决策树不同,此时的划分过程不再是为每个非叶子节点寻找一个最优的划分属性,而是试图建立一个合适的线性分类器。
6 使用sklearn实现决策树
在sklearn中,DecisionTreeClassifier 是能够对数据集执行多类分类的类。与其他分类器一样, DecisionTreeClassifier 将两个数组作为输入:一个数组 X(稀疏或密集,shape: (n_samples, n_features) 保存训练样本)和整数值数组 Y,shape: (n_samples,) 保存训练样本的类标签。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import numpy as np from sklearn import tree import matplotlib.pyplot as plt def load_data(): X = np.array([ [0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 1, 0, 1, 1], [0, 1, 1, 0, 1], [0, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 1, 0], [1, 1, 1, 1, 1], [1, 0, 1, 2, 1], [1, 0, 1, 2, 1], [2, 0, 1, 2, 1], [2, 0, 1, 1, 1], [2, 1, 0, 1, 1], [2, 1, 0, 2, 1], [2, 0, 0, 0, 0], ]) return X
data = load_data()
X = data[:,:-1] Y = data[:,-1:]
clf = tree.DecisionTreeClassifier() clf.fit(X,Y)
tree.plot_tree(clf) plt.show()
|
DecisionTreeClassifier的一些常用参数:
- criterion:划分节点时选用的策略,可选值基尼系数
gini、信息增益entropy和log_loss,默认是gini。 - splitter:特征划分点选择标准,支持的策略是
best最佳分割,和random选择最佳随机分割,默认是best。 - max_depth:树的最大深度,可以用于防止过拟合。可传入一个
int类型,或者None。默认为None,表示不限制。 - min_samples_split:分裂内部节点所需的最小样本数。可以是一个
float/int或者None,默认为2。 - min_samples_leaf:叶子节点所需的最小样本数,只有在左右分支中至少留下
min_samples_leaf 个训练样本时,才会考虑任何深度的分割点,默认为1。 - min_impurity_decrease:表示节点的不纯度(基尼系数或信息增益),只有大于该值的时候,才会继续分裂子树。
- min_weight_fraction_leaf:叶节点处所需的(所有输入样本的)权重总和的最小加权分数,如果小于这个值则会和兄弟节点一起被剪枝。当未提供样本权重时,样本具有相同的权重。默认为
0。 - class_weight:类别权重。与
{class_label: weight} 形式的类关联的权重。如果没有,则所有类别的权重都应该为一。对于多输出问题,可以按照与 y 的列相同的顺序提供字典列表。也可以选择balanced“平衡”模式,则算法会自己计算权重,样本量少的类别所对应的样本权重会高。
对于更多参数,以官方文档为准。