这节我们来讲说一下决策树。介绍一下 决策树的基础知识、 决策树的基本算法、 决策树中的问题以及 决策树的理解和解释。
本文主要思路结构如下:先从直观上解释决策树的算法流程。之后针对在实际操作过程中会遇到的6个问题对其进行具体分析,其简要包括(后文会详细分析):
处理选择最佳划分属性:依据不纯度来选择最佳划分属性,而依据不纯度的不同,最佳划分的度量标准又可分为三种:熵减最大、基尼指数减最大、误分类减最大。而信息增益标准里面存在一个内在的偏置,它会偏好去选择具有较多属性值的属性,又提出了分裂信息的项作分母来惩罚具有较多属性值的属性。在分裂信息中,当划分属性在当前结点中几乎都取相同的属性值时,会导致增益率无定义或者非常大(分母可能为0或者非常小)的情况,提出了平均增益度量。将其组合,得到6种选择最佳划分的度量标准。之后再举实际例子,辅助解释上述概念。
处理缺失值的问题:当一些样本的特征为空时,主要有两种解决办法:舍弃这个样本和估计这个缺失值大小。而估计这个缺失值的方法又可以分为三种:依据该属性所有样本、依据该节点中该属性同类样本、依据可能值赋予概率对其估计。
处理连续属性值问题:主要采用数据离散化技术,介绍了有监督离散化和无监督离散化两大类。无监督离散化技术又可以分为等深分箱法和等宽分箱法两类;有监督离散化技术有:二分法,最小长度描述法。
处理叶子节点的判定问题:主要有空叶子、纯叶子、和属性被测试完的叶子。
处理过拟合问题:主要介绍了预剪枝和后剪枝技术。还介绍了最终确定正确树的规模的三种办法。
处理待测试样本的分类问题:将样本带入决策树,之后根据该叶子结点上的训练样本集计算其后验概率,最后把具有最大后验概率的类赋给待测样本。计算其后验概率的常用方法包括:投票法、加权投票法、局部概率模型法。在算后验概率的过程常采用的概率估计方法:基于频率的极大似然估计、拉普拉斯估计、基于相似度(距离)加权的拉普拉斯估计、m-估计,朴素贝叶斯估计等等。
决策树学习的基础知识
这种算法是从传统的手写规则演变而来的,直观上的理解就是:给你一个样本,然后问你,它是正类吗?你需要对这个问题进行决策跟判别。到这里不难发现,整个算法的核心就是:依据什么进行决策跟判别。
决策树学习顾名思义就是用来做决策的树。是一种逼近离散值目标函数的方法,学习到的函数被表示为一颗决策树。一颗决策树包含一个根结点,若干个内部结点,若干个叶子结点。
- 根结点包含了所有的训练样本。
- 内部结点包含对应的属性测试,每个内部结点包含的样本集合,依据属性测试的结果,被划分到它的子结点。
- 叶子结点对应于决策结界。
从根结点到每个叶子结点的路径对应了一条决策规则。核心任务是把样例分类到各可能的离散值对应的类别。是一个分类问题。
决策树学习的基本算法
决策树学习的目的是为了构造一颗泛化能力强,即在测试样本上就有很好表现的决策树。基本算法遵循自顶向下、分而治之的策略,具体步骤有以下几点:
- 选择最好的属性作为测试属性并创建树的根结点
- 为测试属性每个可能的取值产生一个分支
- 训练样本划分到适当的分支形成子结点
- 对每个子节点重复上面的过程,直到所有结点都是叶子结点。
第一个决策树算法是:CLS
(concept Learning System),而ID3
是助力决策树逐渐受到关注的关键,成为主流技术的算法。最常用的决策树算法是:C4.5
。而用于回归任务的决策树算法是:CART
(classification and Regression Tree)。
决策树学习中的常见问题
可见,决策树的学习是一个递归过程,过程的实现还需要解决以下六个方面的问题:
一、最佳划分的度量问题:
就是怎么产生子结点?从决策树学习基本算法的步骤可以看出,决策树学习的关键是如何选择最佳划分属性。一般而言,随着长树过程的不断进行,我们希望决策树的分支结点所包含的样本越来越归属于同一类别,即结点的“不纯度”(impurity) 越来越低。
因此,为了确定按某个属性划分的效果,我们需要比较划分前(父亲结点)和划分后(所有儿子结点)不纯度的降低程度,降低越多,划分的效果就越好。那么我们需要在数学上对这个不纯度进行量化:
- 若记不纯度的降低程度为Δ \DeltaΔ,则用来确定划分效果的度量标准可以用以下数学公式来定义:
其中,I ( p a r e n t ) I_{(parent)}I(parent)是父结点的不纯度度量,k 是划分属性取值的个数。N 是父亲结点上样本的总数,N ( j ) 是第j 个儿子结点上样本的数目,I ( j )是第j 个儿子节点的不纯度度量。
到这里,还有一个量没有说怎么来计算,就是不纯度I II怎么来算:给定任意结点t tt,如何来定义它的不纯度度量,令p ( i )为结点t 中第i 类样本所占有的比例,则结点t 的不纯度度量主要包括以下三种方式:
- 熵:
- 基尼指数:
- 误分类率:
其中,c cc为类别数目,并且在计算熵时,令。著名的I D 3 ID3ID3决策树算法就是以信息增益(熵)为准则来选择划分属性的。CART决策树
算法则使用的是“基尼指数”。基尼指数反应的是从数据集中随机抽取两个样本,其类别标记不一致的概率。因此Gini(t)越小,数据集纯度越高。
基于以上三种不纯度的度量方式,我们就可以得到以下3种选择最佳划分的度量标准:
- 熵减最大:
- 基尼指数减最大:
- 误分类率减最大:
上式中,熵减最大度量标准说的是信息增益最大度量标准,记作:
C4.5算法
但信息增益标准里面存在一个内在的偏置,它会偏好去选择具有较多属性值的属性,为了减少这种偏好带来的不利影响,著名的决策树算法C4.5算法并不直接使用信息增益,而使用“增益率”(Gain Ratio)来选择最佳划分属性。
那增益率如何定义的呢?增益率就是在信息增益度量中除以一个“分裂信息”(Split Information)的项作分母来惩罚具有较多属性值的属性:
其中SplitInfo是划分属性的分裂信息。其数学表达如下所示:
其中,p ( j )是当前结点中划分属性第j jj个属性值所占有样本的比例。分裂信息S p l i t I n f o SplitInfoSplitInfo度量了属性划分数据的广度和均匀性。分裂信息实际上就是当前结点关于划分属性各值的熵,它可以阻碍选择属性值均匀分布的属性。
在引入分裂信息SplitInfo的同时,也产生了一个新的实际问题:当划分属性在当前结点中几乎都取相同的属性值时,会导致增益率无定义或者非常大(分母可能为0或者非常小)。也可以理解为对可取值数目较少的属性有所偏好。
为了避免选择这种属性,C4.5决策树算法并不直接选择增益率最大的划分属性,而使用一个启发式方法:先计算每个属性的信息增益及平均值,然后仅对信息增益高于平均值的属性应用增益率度量。
平均增益度量
为了克服信息增益度量和增益率的问题,平均增益(AverGain
)度量被提出。平均增益度量用划分属性取值的个数来替换属性的分裂信息,不仅惩罚了属性较多的属性,还避免了增益率度量在实际操作过程中的问题。具体的度量公式如下:
其中,k 是划分属性取值的个数。
增益率和平均增益度量改进信息增益度量的方法同样适合于基尼指数和误分类率,由此,我们又可以得到6种选择最佳划分的度量标准。
- 熵减率最大:
- 基尼指数减率最大:
- 误分类率减率最大:
- 平均熵减最大:
- 平均基尼指数减最大:
- 平均误分类率减最大
到这里可能有点迷糊,我们举一个例子利用上述数学方法来实际演练一波:
给定训练集S SS,下面以信息增益度量作为最佳划分的标准,演示信息增益的计算和决策树生长的过程。
这是网上的一个样本数据集,每一天相当于是一个样本,总共是14个样本,每个样本有四个特征,一个标签。四个特征分别是:Outlook
、Temperature
、Humidity
、Wind
;标签是:PlayTennis
,标签里面只有两类类别。
假如Outlook
被选作划分属性,其图形表示如下所示:
那么它划分训练集S SS的信息增益,等于训练集S 的熵,减去它儿子结点上熵的加权和,其中的权值就是儿子结点上样本数目占父亲结点上样本的数目的比例。具体的表达公式如下所示:
因此,对于当前结点,用 “Outlook
”划分样本集S的信息增益最大,被选为划分属性。根节点选定为Ourlook
为划分属性。划分之后就如上图依据Outlook属性划分
所示。
对于生成的每一个儿子结点,重复上面的过程,直到所有的结点为叶子结点。
二、处理缺失属性值问题:
也即样本的一些特征为空。现实任务中常会遇到不完整样本,即样本的某些属性值缺失,尤其是在属性数目较多的情况下,往往会有大量样本出现缺失值。面对缺失属性值,决策树学习会面临两个方面的问题:
- 如何计算含缺失值属性的划分度量、并进行最佳划分的选择?
- 选择好最佳划分后,若样本在该属性上的值缺失,如何对样本进行划分?
- 处理这种属性值缺失的问题,通常有两个办法
1.放弃这个样本,使用无缺失样本进行学习。但这种方法会造成资源的浪费。
2.依据此属性值已知的其它样本来对其进行估计:a. 赋予其当前结点所有样本该属性最常见的值;b. 赋予它当前结点同类样本中该属性值最常见的值;c. 为缺失值属性的每个可能值赋予一个概率,而不是简单地将最常见的值赋给它。
三、处理连续属性值问题:
上文讨论的都是决策树学习中的离散属性问题,但是在实际操作过程中,通常会遇到连续属性,因此我们有必要讨论决策树如何来对连续属性进行处理。
由于连续属性的可取值数目不是有限的,当选定一个特征之后,它所对应的子节点无法展开,那么我们就需要将数据进行离散化。数据离散化技术大致可以分为有监督离散化和无监督离散化两大类。
无监督离散化技术又可以分为等深分箱法和等宽分箱法两类。在等深分箱法中,让每个分箱中的样本数目一致。在等宽分箱法中,让每个分箱中的取值范围一致,本质上就是将一个区间等分成若干段,每段附一个离散值。
有监督离散化技术有:二分法(C4.5算法采用的机制),最小长度描述法。
在二分法中将连续的属性按选定的阈值分割成布尔属性。它具体的做法是:
- 按照某个连续的属性T TT对样本进行排序;
- 找到类标记不同的相邻样本;
- 计算类标记不同的相邻样本的属性T TT的中间值,产生一组候选阈值。可以证明产生最大信息增益的阈值一定在这样的边界中(Fayyad, 1991);
- 计算与每个候选阈值关联的信息增益,选择具有最大信息增益的阈值来离散化连续属性T TT。
二分法的扩展是最小描述长度法(Minimum Description Length MDL)(Fayyad & Irani, 1993)。MDL法将连续取值的属性分割成多个区间,而不是单一阈值的两个区间。
四、 叶子结点的判定问题:
上文都是围绕决策树如何展开生长的讨论,那决策树什么时候停止生长呢?
如果我们暂且不考虑树的规模过大而导致的过拟合问题,在决策树学习基本算法中,有三种情形会判定为叶子结点:
- 当前结点中的样本集合为空,即空叶子;
- 当前结点中的所有样本全部归属于同一类别,即纯叶子;
- 当前结点中的所有样本在接下来划分的所有属性上取值相同,即属性被测试完的叶子。
2和3可合并等价为最佳划分的度量值为0的情况。
五、 怎样解决过拟合问题:
在决策树学习中,为了尽可能正确分类训练样本,结点划分过程将不断重复,有时会造成决策树分支过多,这时就可能因为训练样本学得“太好”了,上述对叶子结点判定的情形,都太过苛刻和完美,从而造成决策树的规模过大,以致于把训练集自身的一些特点当作所有数据都具有的一般性质而导致过拟合。
剪枝(pruning)是解决过拟合问题的主要手段,基本策略有“预剪枝”(prepruning)和“后剪枝” (post pruning) 。
- 预剪枝:在算法完美划分训练数据之前就停止树生长。在决策树展开之前,对每个节点在划分前先进行估计,若当前结点的划分不能带来决策树泛化性能提升,则停止划分,并将当前结点标记为叶节点;
- 后剪枝:允许树过度拟合训练数据,然后对树进行后修剪。从训练集生成一颗完整的决策树,然后自底向上地对非叶结点进行考察,若将该结点对应的子树替换为叶节点能带来决策树泛化性能提升,则将该子树替换为叶节点。
尽管预剪枝可能看起来更直接,但后剪枝方法在实践中往往更好。因为在预剪枝中精确地估计何时停止增长树是非常困难的。
无论是通过预剪枝还是后剪枝来得到正确规模的树,一个关键的问题是使用什么样的准则来确定最终正确树的规模?建立决策树的初衷是希望模型能够对测试样本具有足够强的泛化能力,所以有以下三种判断方法:
- 采用留出法,即预留一部分数据用作“验证集”以进行性能评估。
- 使用所有可用数据进行训练,但进行统计测试来估计生长或修剪一个特定的结点是否有可能改善在训练集以外的样例上的性能。
- 使用一个明确的标准来衡量训练样例和决策树的复杂度,当这个编码的长度最小时停止树增长。
上面的第一种方法是最普通的,常被称为训练和验证集法。它将可用数据分成两个样例集合:训练集用于形成学习到的假设;验证集用于评估这个假设在后续数据上的精度。
训练和验证集法的动机:即使学习器可能会被训练集误导,但验证集不大可能表现出同样的随机波动。通常的做法是,所有样例的三分之二作训练集,三分之一作验证集。
训练和验证集法主要包括:错误率降低修剪和规则后修剪。具体算法可参阅 [Mitchell, 1997]。
六、 待测试样本的分类问题:
到此,我们解决了决策树生长的相关问题,那么,决策树学习学到后,怎样应用决策树进行待测样本的分类?
分类待测样本的方法:从决策树的根结点开始,测试这个结点指定的划分属性,然后按照待测样本的该属性值对应的树枝向下移动。这个过程再在以新结点为根的子树上重复,直到将待测样本划分到某个叶子结点为止。然后根据该叶子结点上的训练样本集计算其后验概率,最后把具有最大后验概率的类赋给待测样本。
给定一个叶子结点(其本质就是一个训练样本的集合),计算其后验概率的常用方法包括:投票法、加权投票法、局部概率模型法。当计算得到的后验概率出现相同的情况下,可以采用随机分类或者拒判的方法进行处理。
在计算后验概率的过程经常会采用一些常用的概率估计方法:基于频率的极大似然估计、拉普拉斯估计、基于相似度(距离)加权的拉普拉斯估计、m-估计,朴素贝叶斯估计等等。
举个例子:
假设有一个样本,其特征分别是sunny
、hot
、normal
、weak
;真实标签是Yes
。将其运用于构建好的决策树,如上图所示。应用拉普拉斯估计(分子加1分母加2,背后的思想是:在实验之前已经有两次实验,一次正,一次反。为了防止出现零概率估计)得到待测试样本x属于Y e s 和N o 的概率分别为:
决策树学习的理解和解释
决策树学习是以样本为基础的归纳学习方法,它采用自顶向下的递归方式来生长决策树。随着树的生长,完成对训练样本集的不断细分,最终都被细分到了每个叶子结点上。
决策树的每个结点都是样本的集合,熵等度量刻画了样本集的不纯度,决策树的生长过程是一个熵降低、信息增益、从混沌到有序的过程。
决策树学习对噪声数据具有很好的鲁棒性,而且学习得到的决策树还能被表示为多条if-then形式的决策规则,因此具有很强的可读性和可解释性。
Numpy实现ID3算法
import os import numpy as np import pandas as pd def GetData(fileName = 'WatermelonOriginal.txt'): Data_FilePath = os.path.dirname(os.getcwd()) + '/Data/' + fileName df = pd.read_csv(Data_FilePath, sep=',', header=None, names=['x1', 'x2', 'x3', 'x4', 'x5', 'x6','y']) X_trainData = df[['x1', 'x2', 'x3', 'x4', 'x5', 'x6']].values Y_trainData = df['y'].values return X_trainData, Y_trainData def split_Data(DataX, DataY, ratio): """ 划分数据集 :param DataX: :param DataY: :param ratio: 训练集所占比例 :return: """ trainDataLen = int(len(DataX) * ratio) X_trainData = DataX[:trainDataLen] Y_trainData = DataY[:trainDataLen] X_testData = DataX[trainDataLen:] Y_testData = DataY[trainDataLen:] return X_trainData, Y_trainData, X_testData, Y_testData def ComputeEntropy(feature): """ 计算熵 :param feature: :return: """ entropy = 0 nums = len(feature) # 给定的某个特征下,所有的样本数 bincounts = np.bincount(feature) # 统计featrue数组中每个数字出现的次数 for count in bincounts: if count == 0 : continue # 如果特征出现的次数为0的话,直接跳过熵的计算 prob = count / nums entropy -= prob * np.log2(prob) # 熵 = p * log(p) * -1 return entropy def ComputeGain(DataX, DataY, feature_index): """ 计算某一列,也就是某一个特征的信息增益 :param DataX: :param DataY: :param feature_index: :return: """ if DataY.shape.__len__() == 1: # 如果label的shape是一维的,就增加一个纬度 DataY = np.expand_dims(DataY, axis=1) DataXY = np.concatenate((DataX, DataY), axis=1) feature_entropy = 0 for feature in set(DataXY[:, feature_index]): DataByfeature = DataXY[DataXY[:, feature_index] == feature] prob = len(DataByfeature) / len(DataXY) # 特征feature_index下的特征子集出现的概率 entropy = ComputeEntropy(DataByfeature[:, -1]) # 求数据子集下标签y的熵 feature_entropy += prob * entropy #这个feature_index的熵,等于这个式子的累计 # 计算信息增益,依据feature_index切分数据后,熵能下降多少,越大越好 gain = ComputeEntropy(DataXY[:, -1]) - feature_entropy # 用这个就是id3决策树,他倾向于选择可取值多的列 return gain def GetMaxGain_FeatureIndex(DataX, DataY): """ 计算信息增益最大的那个特征,并返回 :param DataX: :param DataY: :return: """ BestFeatureIndex = -1 BestGain = 0 for feature_index in range(0, DataX.shape[1]): gain = ComputeGain(DataX, DataY, feature_index) # print("feature index {} , Gain {}".format(feature_index, gain)) if gain > BestGain: BestFeatureIndex = feature_index BestGain = gain return BestFeatureIndex class Node(): def __init__(self, col): """ #创建节点和叶子对象,用来构建树 :param col: """ self.col = col self.children = {} def __str__(self): return 'Node col={}'.format(self.col) class Leaf(): def __init__(self, y): self.y = y def __str__(self): return 'Leaf y={}'.format(self.y) def print_tree(node, prefix='', subfix=''): """ # 打印树的方法 :param node: :param prefix: :param subfix: :return: """ prefix += '-' * 4 print(prefix, node, subfix) if isinstance(node, Leaf): return for i in node.children: subfix = 'value=' + str(i) print_tree(node.children[i], prefix, subfix) def CreateChildren(DataX, DataY, ParentNode): if DataY.shape.__len__() == 1: # 如果label的shape是一维的,就增加一个纬度 DataY = np.expand_dims(DataY, axis=1) DataXY = np.concatenate((DataX, DataY), axis=1) for feature_index in np.unique(DataXY[:, ParentNode.col]): sub_data = DataXY[DataXY[:,ParentNode.col] == feature_index] #首先根据父节点col列的取值分割数据 SubData_UniqueY = np.unique(sub_data[:, -1]) # 如果所有的y都是一样的,说明是个叶子节点 if len(SubData_UniqueY) == 1: ParentNode.children[feature_index] = Leaf(SubData_UniqueY[0]) continue MaxFeatureIndex = GetMaxGain_FeatureIndex(DataX=sub_data[:,:-1], DataY=sub_data[:, -1]) # 添加分支节点到父节点上 ParentNode.children[feature_index] = Node(col=MaxFeatureIndex) def main(DataX, DataY): MaxFeatureIndex = GetMaxGain_FeatureIndex(DataX=DataX, DataY=DataY) root = Node(MaxFeatureIndex) print(root) CreateChildren(DataX=DataX, DataY=DataY, ParentNode=root) print_tree(root) print('#---------------------------------------#') if DataY.shape.__len__() == 1: # 如果label的shape是一维的,就增加一个纬度 DataY = np.expand_dims(DataY, axis=1) DataXY = np.concatenate((DataX, DataY), axis=1) DataXY_0_0 = DataXY[DataXY[:,0] == 0] CreateChildren(DataX=DataXY_0_0[:,:-1], DataY=DataXY_0_0[:, -1], ParentNode=root.children[0]) print_tree(root) print('#---------------------------------------#') DataXY_0_1 = DataXY[DataXY[:, 0] == 1] CreateChildren(DataX=DataXY_0_1[:, :-1], DataY=DataXY_0_1[:, -1], ParentNode=root.children[1]) print_tree(root) print('#---------------------------------------#') # 继续创建,0=1,1=1的下一层 DataXY_0_1_and_1_1 = DataXY_0_1[DataXY_0_1[:, 1] == 1] CreateChildren(DataX=DataXY_0_1_and_1_1[:,:-1], DataY=DataXY_0_1_and_1_1[:,-1], ParentNode=root.children[1].children[1]) print_tree(root) return root def pred(DataX, node): """ #预测方法,测试 :param DataX: :param node: :return: """ col_value = DataX[node.col] node = node.children[col_value] if isinstance(node, Leaf): return node.y return pred(DataX, node) if __name__ == "__main__": Data_X, Data_Y = GetData(fileName='WatermelonOriginal.txt') trainData_X, trainData_Y, testData_X, testData_Y = split_Data(Data_X, Data_Y, ratio=0.6) print("label y entropy is : {}".format(ComputeEntropy(trainData_Y))) # 计算一下标签y的熵 print("feature index=0 entropy is : {}".format(ComputeGain(trainData_X, trainData_Y, 0))) # 计算一下特征index为0的信息增益 print("Best FeatureIndex Gain {} ".format(GetMaxGain_FeatureIndex(trainData_X, trainData_Y))) print(Node(0)) print(Leaf(1)) print_tree(Node(0)) root = main(DataX=trainData_X, DataY=trainData_Y) # 训练集上测试效果 correct = 0 for x, y in zip(trainData_X, trainData_Y): if pred(x, root) == y: correct += 1 print(correct / len(trainData_X)) print('-------------------------') # 测试集上测试效果 correct = 0 for x, y in zip(testData_X, testData_Y): if pred(x, root) == y: correct += 1 print(correct / len(trainData_X)) print('-------------------------') import pickle # 序列化保存下来,后面剪枝用 with open('tree.dump', 'wb') as fr: pickle.dump(root, fr)
在Python中使用决策树分类算法
在Scikit-Learn
库中,基于决策树这一大类的算法模型的相关类库都在sklearn.tree
包中。tree
包中提供了7
个类,但有3
个类是用于导出和绘制决策树,实际的决策树算法只有4
种,这4
种又分为两类,分别用于解决分类问题和回归问题。
DecisionTreeClassifier
类:经典的决策树分类算法,其中有一个名为“criterion”
的参数,给这个参数传入字符串“gini”
,将使用基尼指数;传入字符串“entropy”
,则使用信息增益。默认使用的是基尼指数。余下3
个决策树算法都有这个参数。DecisionTreeRegressor
类:用决策树算法解决反回归问题。ExtraTreeClassifier
类:这也是一款决策树分类算法,但与前面经典的决策树分类算法不同,该算法在决策条件选择环节加入了随机性,不是从全部的特征维度集中选取,而是首先随机抽取n
个特征维度来构成新的集合,然后再在新集合中选取决策条件。n
的值通过参数“max_features”
设置,当max_features
设置为1
时,相当于决策条件完全通过随机抽取得到。ExtraTreeRegressor
类:与ExtraTreeClassifier
类似,同样在决策条件选择环境加入随机性,用于解决回归问题。
# 导入决策树模型中的决策树分类算法 from sklearn.tree import DecisionTreeClassifier # 导入鸢尾花分类数据集 from sklearn.datasets import load_iris X, y = load_iris(return_X_y=True) # 训练模型 clf = DecisionTreeClassifier().fit(X, y) print(clf.predict(X)) ## 使用模型进行分类预测 print(clf.score(X,y)) ## 使用模型自带性能评估器
我的微信公众号名称:深度学习先进智能决策
微信公众号ID:MultiAgent1024
公众号介绍:主要研究深度学习、强化学习、机器博弈等相关内容!期待您的关注,欢迎一起学习交流进步!