决策树之剪枝原理与CART算法
来源:互联网 发布:软件企业退税文件 编辑:程序博客网 时间:2024/05/07 17:35
决策树学习笔记(二)
前言
继续关于决策树的内容,本篇文章主要学习了决策树的剪枝理论和基于二叉树的CART算法。主要内容:
- 理解决策树损失函数的定义以及物理含义
- 基尼指数的主要两个作用
- 理解CART剪枝原理,以及它的基本假设和核心思想
决策树的剪枝
上篇关于决策树的博文实现了ID3和C4.5算法,但我们并没有实现预测函数,以及进行准确率测试。接下来我们将把数据分为训练数据和测试数据集,来看看决策树的分类效果。
数据准备:
访问:https://archive.ics.uci.edu/ml/datasets/Car+Evaluation,下载car.data数据集。
| class valuesunacc, acc, good, vgood| attributesbuying: vhigh, high, med, low.maint: vhigh, high, med, low.doors: 2, 3, 4, 5more.persons: 2, 4, more.lug_boot: small, med, big.safety: low, med, high.
新建loadData.py,加载数据集:
dataSet = []labels =[]def createDataSet(fileName): with open(fileName) as ifile: for line in ifile: tokens = line.strip().split(',') dataSet.append(tokens) labels =['buying','maint','doors','persons','lug_boot','safety'] return dataSet,labels
新建test.py,编写predict函数:
import operatorfrom math import logdef splitDataSet(dataSet,axis,value): """ 按照给定特征划分数据集 :param axis:划分数据集的特征的维度 :param value:特征的值 :return: 符合该特征的所有实例(并且自动移除掉这维特征) """ # 循环遍历dataSet中的每一行数据 retDataSet = [] # 找寻 axis下某个特征的非空子集 for featVec in dataSet: if featVec[axis] == value: reduceFeatVec = featVec[:axis] # 删除这一维特征 reduceFeatVec.extend(featVec[axis+1:]) retDataSet.append(reduceFeatVec) return retDataSet# 计算的始终是类别标签的不确定度def calcShannonEnt(dataSet): """ 计算训练数据集中的Y随机变量的香农熵 :param dataSet: :return: """ numEntries = len(dataSet) # 实例的个数 labelCounts = {} for featVec in dataSet: # 遍历每个实例,统计标签的频次 currentLabel = featVec[-1] # 表示最后一列 # 当前标签不在labelCounts map中,就让labelCounts加入该标签 if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] =0 labelCounts[currentLabel] +=1 shannonEnt = 0.0 for key in labelCounts: prob = float(labelCounts[key]) / numEntries shannonEnt -= prob * log(prob,2) # log base 2 return shannonEntdef calcConditionalEntropy(dataSet,i,featList,uniqueVals): """ 计算x_i给定的条件下,Y的条件熵 :param dataSet: 数据集 :param i: 维度i :param featList: 数据集特征列表 :param unqiueVals: 数据集特征集合 :return: 条件熵 """ ce = 0.0 for value in uniqueVals: subDataSet = splitDataSet(dataSet,i,value) prob = len(subDataSet) / float(len(dataSet)) # 极大似然估计概率 ce += prob * calcShannonEnt(subDataSet) #∑pH(Y|X=xi) 条件熵的计算 return ce# 计算信息增益def calcInformationGain(dataSet,baseEntropy,i): """ 计算信息增益 :param dataSet: 数据集 :param baseEntropy: 数据集中Y的信息熵 :param i: 特征维度i :return: 特征i对数据集的信息增益g(dataSet | X_i) """ featList = [example[i] for example in dataSet] # 第i维特征列表 uniqueVals = set(featList) # 换成集合 - 集合中的每个元素不重复 newEntropy = calcConditionalEntropy(dataSet,i,featList,uniqueVals) infoGain = baseEntropy - newEntropy # 信息增益 return infoGaindef majorityCnt(classList): """ 返回出现次数最多的分类名称 :param classList: 类列表 :retrun: 出现次数最多的类名称 """ classCount = {} for vote in classList: if vote not in classCount.keys(): classCount[vote] = 0 classCount[vote] +=1 sortedClassCount = sorted(classCount.items(),key = operator.itemgetter(1),reverse = True) return sortedClassCount[0][0]def chooseBestFeatureToSplitByID3(dataSet): """ 选择最好的数据集划分 :param dataSet: :return: """ numFeatures = len(dataSet[0]) -1 # 最后一列是分类 baseEntropy = calcShannonEnt(dataSet) bestInfoGain = 0.0 bestFeature = -1 for i in range(numFeatures): # 遍历所有维度特征 infoGain = calcInformationGain(dataSet,baseEntropy,i) if(infoGain > bestInfoGain): bestInfoGain = infoGain bestFeature = i return bestFeature # 返回最佳特征对应的维度def calcInformationGainRate(dataSet,baseEntropy,i): """ 计算信息增益比 :param dataSet: 数据集 :param baseEntropy: 数据集中Y的信息熵 :param i: 特征维度i :return: 特征i对数据集的信息增益g(dataSet|X_i) """ numEntries = len(dataSet) labelCounts = {} for featVec in dataSet: currentLabel = featVec[i] if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] =0 labelCounts[currentLabel] +=1 shannonEnt = 0.0 for key in labelCounts: prob = float(labelCounts[key]) / numEntries shannonEnt -= prob * log(prob,2) return calcInformationGain(dataSet,baseEntropy,i) / shannonEntdef chooseBestFeatureToSplitByC45(dataSet): """ 选择最好的数据集划分方式 :param dataSet: :return: """ numFeatures = len(dataSet[0]) -1 # 最后一列是分类 baseEntropy = calcShannonEnt(dataSet) bestInfoGainRate =0.0 bestFeature = -1 for i in range(numFeatures): infoGainRate = calcInformationGainRate(dataSet,baseEntropy,i) if (infoGainRate > bestInfoGainRate): bestInfoGainRate = infoGainRate bestFeature = i return bestFeaturedef noInformationGainToSplitByID3(dataSet): """ 不使用信息增益概念,而是直接判断条件熵的大小 """ numFeatures = len(dataSet[0]) -1 bestConditionEntropy = 1.0 bestFeature =-1 for i in range(numFeatures): featList = [example[i] for example in dataSet] uniqueVals = set(featList) conditionEntropy = calcConditionalEntropy(dataSet,i,featList,uniqueVals) if (conditionEntropy < bestConditionEntropy): bestConditionEntropy = conditionEntropy bestFeature =i return bestFeaturedef createTree(dataSet,labels,chooseBestFeatureToSplitFunc = chooseBestFeatureToSplitByID3): """ 创建决策树 :param dataSet: 数据集 :param labels: 数据集每一维的名称 :return: 决策树 """ classList = [example[-1] for example in dataSet] # 类别列表 if classList.count(classList[0]) == len(classList): # 统计属于列别classList[0]的个数 return classList[0] # 当类别完全相同则停止继续划分 if len(dataSet[0]) ==1: # 当只有一个特征的时候,遍历所有实例返回出现次数最多的类别 return majorityCnt(classList) # 返回类别标签 bestFeat = chooseBestFeatureToSplitFunc(dataSet) bestFeatLabel = labels[bestFeat] myTree ={bestFeatLabel:{}} # map 结构,且key为featureLabel del (labels[bestFeat]) # 找到需要分类的特征子集 featValues = [example[bestFeat] for example in dataSet] uniqueVals = set(featValues) for value in uniqueVals: subLabels = labels[:] # 复制操作 myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels) return myTree# change map feature to label's indexdef mapFeatureToLabelIndex(map,labels): for key in map.keys(): for i in range(len(labels)): if key == labels[i]: return key,i# 决策树预测函数def predict(testData,decisionTree,labels): # 获得决策树结点的下标 feature_label,feature_index = mapFeatureToLabelIndex(decisionTree,labels) tree = decisionTree[feature_label][testData[feature_index]] # 判断该树是叶子结点还是子结点 if (~isinstance(tree,dict)): # 如果是叶子结点,则直接返回结果 return tree else: # 子结点则继续递归 return predict(testData,tree,labels)# 决策树准确率判断def calPrecision(dataSet,predictSet): length = len(dataSet) count = 0 for i in range(length): if dataSet[i][-1] == predictSet[i]: count +=1 return count / length *100# 测试不同数据集的决策树构建import loadData as lddataSet,labels = ld.createDataSet('car.data')import copypredict_labels = copy.copy(labels)# split dataSet into trainingSet and testSetimport numpy as npnp.random.shuffle(dataSet)m = len(dataSet)# 定义交叉验证比例rate = 0.7training_len = int(rate * m);trainingSet,testSet = dataSet[0:training_len],dataSet[training_len:-1]myTree = createTree(trainingSet,labels,chooseBestFeatureToSplitByC45)# 预测训练集predict_result =[]for data in testSet: result = predict(data[0:-1],myTree,predict_labels) predict_result.append(result)# 测试训练集准确率print("decision Tree predict precision: %.2f"%calPrecision(testSet,predict_result),"%")
经过多次测试结果得:
decision Tree predict precision: 35.71%decision Tree predict precision: 32.24%decision Tree predict precision: 35.14%decision Tree predict precision: 33.78%
决策树生成算法递归地产生决策树,直到不能继续下去为止。这样产生的树往往对训练数据的分类很准确,但对未知的测试数据的分类却没那么准确,即出现过拟合现象。过拟合的原因在于学习时过多地考虑如何提高对训练数据的正确分类,从而构建出过于复杂的决策树。解决这个问题的办法是考虑决策树的复杂度,对已生成的决策树进行简化。(如何证明?数学能否证明?)
以下是数学与程序的分割线
损失函数的定义
书中所表述的内容可以简单理解为:训练集分类越准确,决策树的复杂度越高。训练集分类模糊,决策树的复杂度就相对比较低。我们先前写的所有ID3和C4.5算法的分类准确率都是100%。如果拿来一堆测试数据,往往测试结果告诉我们分类准确率并不高。上述测试集结果为35%附近。模型复杂度和损失函数为何成反比?由经验得?为何要调和分类模糊和决策树的复杂度即能提升准确率?
先来看看书中关于损失函数的定义。决策树的剪枝往往通过极小化决策树整体的损失函数或代价函数来实现。设树T的叶结点个数为
其中经验熵为
在这里我存在两个疑问:
- 叶结点上为什么会有有限个
Nt 个样本点?且为何存在k类样本点?即在该叶结点上,所有类别标签应该都是一致的。(可能解释:1.在该叶结点上没有足够的特征信息再把不一致的标签分开。2.存在噪声点,何谓噪声,在类群中有极少个与类群不符的标签。) - 假如上述问题成立,那么叶结点存在的噪声是不需要做处理的,因为这种噪声天然的被大量类群所包围,而决策树生成过程中,在叶结点上取条件概率大的那一类作为该叶结点的类别标签。由此,难道在决策树生成时,某个维度上的特征是可以自动被切分的?如果此条件成立,那么上述式子区分噪声点才有了意义。
损失函数的物理含义
在理解损失函数时,首先明确两个物理含义,信息熵和叶结点个数
那么叶结点个数有反映了决策树什么样的性质呢?为何把这两个量构成的新模型(损失函数)就能够提升决策时的预测准确率呢?首先我们来画一个最简单的决策树模型。见下图:
从图中我们能看到,根结点为特征x,且分类规则为
我们再来看一个稍微复杂的例子,自带噪声(且噪声点能够被该决策树自动学习到)。
这个决策树中,在rule3中只有一个点,且类别标签为“是”。在它附近的点都属于“否”标签,它很有可能就是一个噪声点。但由于决策树能够自动提取特征,所以该空间被4条规则所瓜分,对应的,决策树叶结点个数为4。有了这幅,我们便可以分析损失函数是如何有效的去除噪声点的。目前该损失函数中
在这个特殊的情况下,根节点选取x或者y为特征向量都无关紧要,因为它们的不确定度都是一样的。我们以x为根节点进行分析。这里的一个最大疑问是,在进行决策树剪枝运算时,计算的是子结点下,各样本点的不确定度啊,为什么书中说是叶结点呢?(可能是已经假设了这些样本点已经归并到上一层子结点上了。)即现在的决策树形态为,如下图:
在这种情况下,我们可以计算出rule1空间中,即左侧叶结点的不确定次数。如果是噪声点,那么它的不确定次数是很低的。看书中信息熵的那张图,不信自己算一下也行。
噪声点出现的概率极低,那么
剪枝的过程必然导致叶结点个数的减少,因此由该损失函数得,必然选择叶结点树少的那类决策树,也就是选择模型简单的决策树。当然,如果
正如书中的结论:剪枝,就是当
个人疑云:
这些理论都建立在噪声影响了模型的复杂度之上,这好像是被大家广泛认可的公理。但为什么受噪声影响,任何一种模型的复杂度就随着增加嘛?是什么内在的因素影响了模型的复杂度?它们之间就没有任何联系可以用来解释嘛?就因为噪声使得系统变得混沌?现有模型无法很好的表达不确定因素,或者说现有模型都是建立在确定因素上,而噪声的引入,让确定变成不确定,模型为了找到规律,在确定因素中需找更加复杂的表现形式来拟合不确定因素。而确定的东西是没法表达不确定因素,由此带来了复杂度。呵呵,得看看哲学了。
最近,在知乎上提了这个问题,大神给出了他关于这个问题的理解。并从另外一个视角理解什么是信息熵,这种信息熵的理解更加贴合计算机领域知识。详见【如何理解机器学习中噪声影响模型复杂度问题?】
有了损失函数的表达式,我们就能够生成我们想要的决策树了。
算法(树的剪枝算法):
输入:生成算法产生的整个树T,参数
α :
输出:修建后的子树Tα
(1) 计算每个结点的经验熵
(2) 递归地从树的叶结点向上回缩
设一组叶结点回缩到其父结点之前与之后的整体树分别为TB与TA ,其对应的损失函数值分别为Cα(TB)与Cα(TA) ,如果
Cα(TA)≤Cα(TB)
则进行剪枝,即将父结点变为新的叶结点。
(3) 返回(2),直至不能继续为止,得到损失函数最小的子树Tα
只需要考虑两个树的损失函数的差,其计算可以在局部进行。所以,决策树的剪枝算法可以由一种动态规划的算法来实现。我们暂且不实现剪枝算法,在介绍完CART算法后,我们在此基础上实现。
CART算法
分类与回归树模型是由Breiman等人在1984年提出,是应用广泛的决策树学习方法。CART同样由特征选择、树的生成及剪枝组成,即可以用于分类也可以用于回归。以下将用于分类与回归的树统称为决策树。
CART是在给定输入随机变量X条件下输出随机变量Y的条件概率分布的学习方法。CART假设决策树是二叉树,内部结点特征的取值为“是”和“否”,左分支是取值为“是”的分支,右分支是取值为“否”的分支。这样的决策树等价于递归地二分每个特征,将输入空间即特征空间划分为有限个单元,并在这些单元上确定预测的概率分布,也就是输入给定的条件下输出的条件概率分布。
疑问:
- 为何是二叉树?不是其他形式的树?
CART算法由以下两步组成:
1. 决策树生成:基于训练数据集生成决策树,生成的决策树要尽量大;
2. 决策树剪枝:用验证数据集最已生成的树进行剪枝并选择最优子树,这时用损失函数最小作为剪枝的标准。
CART算法主要分为两大部分:
1. 回归数的生成,针对Y是连续变量。
2. 分类树的生成,针对Y是离散变量。
本篇文章主要理解分类树的生成。
分类树的生成
分类树用基尼指数选择最优特征,同时决定该特征的最优二值切分点。存在两个疑问:
- 基尼指数和信息熵有何不同?为何CART算法选择基尼指数而不是信息熵。
- 基尼指数是如何选择最优二值切分点的。(CART的前提条件,决策树必须为二叉树。)
定义(基尼指数)
分类问题中,假设有K个类,样本点属于第k类的概率为
对于二类分类问题,若样本点属于第1个类的概率是p,则概率分布的基尼指数为
对于给定的样本集合D,其基尼指数为
这里,
如果样本集合D根据特征A是否取某一可能值a被分割成D_1
D_1=\{(x,y) \in D| A(x) =a\},D_2 = D-D_1
则在特征A的条件下,集合D的基尼指数定义为
基尼指数Gini(D)表示集合D的不确定性,基尼指数Gini(D,A)表示A =a 分割后集合D的不确定性。基尼指数值越大,样本集合的不确定性也越大,这一点与熵相似。
上述就是基尼指数的定义了,定义很简单,但书中同样没有解释为什么使用基尼指数而不是信息熵作为CART算法。这一部分我也无法理解为什么不用信息熵来解决,而用基尼系数。但书中给了我们一张二类分类中基尼指数,熵之半和分类误差率的关系。起码在二元分类中,信息熵和基尼指数对特征选取的作用是等价的。见图:
简单来说,基尼指数反映了数据集合的混乱程度。当基尼指数越大时,当前数据越混沌的,分布均匀时取极值。
举个简单的例子,来分析基尼指数是如何一并选择特征向量和寻找最佳切分点的。
下表是一个由15个样本组成的贷款申请训练数据。数据包括贷款申请人的4个特征:第1个特征是年龄,有三个可能值:青年,中年,老年;第2个特征是有工作,有2个可能值:是,否;第3个特征是有自己的房子,有两个可能值:是,否;第四个特征是信贷情况,有3个可能值:非常好,好,一般。表的最后一列是类别,是否同意贷款,取二个值:是,否。
希望通过所给的训练数据学习一个贷款申请的模型,用以对未来的贷款申请进行分类,即当新的客户提出贷款申请时,根据申请人的特征利用该模型决定是否批准贷款申请。
解:首先计算各特征的基尼指数,选择最优特征以及最优切分点。分别以
求特征
基尼指数在选取最优切分点的过程中,会分为当前特征标签和其他特征标签两类。所以
简单说明下,第一部分是青年标签里能否贷款的数据混沌度,第二部分是中年和老年加在一起的数据混沌度。同理:
由于
求特征
由于
求特征
在
简单总结
从上述解题过程中,我们发现Gini指数不仅用来选取最优特征,还用来选择最优切分点。而在ID3和C4.5算法中,信息熵并没有用此来计算最优切分点而仅仅用来选择最优特征。当然,我们也可以自己实现信息熵的最优切分点来看看决策树的生成效果如何。
算法(CART生成算法)
输入:训练数据集D,停止计算的条件;
输出:CART决策树;
根据训练数据集,从根节点开始,递归地对每个结点进行以下操作,构建二叉决策树:
(1)设结点的训练数据集为D,计算现有特征对该数据集的基尼指数。此时对每一个特征A,对其可能取的每个值a,根据样本点对A=a的测试为“是”或“否”将D分割成D1 和D2 两部分,利用基尼指数计算公式计算。
(2)在所有可能的特征A以及它们所有可能的切分点a中,选择基尼指数最小的特征及其对应的切分点作为最优特征与最优切分点。依最优特征与最优切分点,从现结点生成两个子结点,将训练数据集依特征分配到两个子结点中去。
(3)对两个子结点递归地调用(1),(2),直至满足停止条件。
(4)生成CART决策树
算法停止计算的条件是结点中的样本个数小于预定阈值,或样本集的基尼指数小于预定阈值,或者没有更多特征。
Code Time
终于到了我们编代码的时间了,我们先模仿ID3算法,构建贷款申请样本数据的分类树。
数据准备
新建cart.py文件,生成数据:
def createDataSet(): """ 创建数据集 """ dataSet = [[u'1000',u'青年', u'否', u'否', u'一般', u'拒绝'], [u'2000',u'青年', u'否', u'否', u'好', u'拒绝'], [u'7000',u'青年', u'是', u'否', u'好', u'同意'], [u'7100',u'青年', u'是', u'是', u'一般', u'同意'], [u'3000',u'青年', u'否', u'否', u'一般', u'拒绝'], [u'3500',u'中年', u'否', u'否', u'一般', u'拒绝'], [u'3600',u'中年', u'否', u'否', u'好', u'拒绝'], [u'8000',u'中年', u'是', u'是', u'好', u'同意'], [u'9000',u'中年', u'否', u'是', u'非常好', u'同意'], [u'9200',u'中年', u'否', u'是', u'非常好', u'同意'], [u'8600',u'老年', u'否', u'是', u'非常好', u'同意'], [u'7800',u'老年', u'否', u'是', u'好', u'同意'], [u'10000',u'老年', u'是', u'否', u'好', u'同意'], [u'6500',u'老年', u'是', u'否', u'非常好', u'同意'], [u'3000',u'老年', u'否', u'否', u'一般', u'拒绝'], ] labels = [u'工资',u'年龄', u'有工作', u'有房子', u'信贷情况'] predict_labels = [u'工资',u'年龄', u'有工作', u'有房子', u'信贷情况'] # 返回数据集和每个维度的名称 return dataSet, labels,predict_labels
计算数据集的基尼指数:
# 计算数据集的基尼指数def calcGini(dataSet): numEntries = len(dataSet) labelCounts ={} # 给所有可能分类创建字典 for featVec in dataSet: currentLabel = featVec[-1] if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] =0 labelCounts[currentLabel]+=1 Gini =1.0 for key in labelCounts: prob = float(labelCounts[key])/numEntries Gini -= prob * prob return Gini
划分数据集,便于构造决策树的子结点:
def splitDataSet(dataSet,axis,value): """ 按照给定特征划分数据集 :param axis:划分数据集的特征的维度 :param value:特征的值 :return: 符合该特征的所有实例(并且自动移除掉这维特征) """ # 循环遍历dataSet中的每一行数据 retDataSet = [] # 找寻 axis下某个特征的非空子集 for featVec in dataSet: if featVec[axis] == value: reduceFeatVec = featVec[:axis] # 删除这一维特征 reduceFeatVec.extend(featVec[axis+1:]) retDataSet.append(reduceFeatVec) return retDataSet
划分指定特征标签数据集,便于计算在某特征下的基尼指数:
def splitOtherDataSetByValue(dataSet,axis,value): """ 按照给定特征划分数据集 :param axis:划分数据集的特征的维度 :param value:特征的值 :return: 不符合该特征的所有实例(并且自动移除掉这维特征) """ # 循环遍历dataSet中的每一行数据 retDataSet = [] # 找寻 axis下某个特征的非空子集 for featVec in dataSet: if featVec[axis] != value: reduceFeatVec = featVec[:axis] # 删除这一维特征 reduceFeatVec.extend(featVec[axis+1:]) retDataSet.append(reduceFeatVec) return retDataSet
辅助方法:
def majorityCnt(classList): """ 返回出现次数最多的分类名称 :param classList: 类列表 :retrun: 出现次数最多的类名称 """ classCount = {} for vote in classList: if vote not in classCount.keys(): classCount[vote] = 0 classCount[vote] +=1 sortedClassCount = sorted(classCount.items(),key = operator.itemgetter(1),reverse = True) return sortedClassCount[0][0]
根据最优特征的最优切分点二值化dataSet:
def binaryZationDataSet(bestFeature,bestSplitValue,dataSet): # 求特征标签数 featList = [example[bestFeature] for example in dataSet] uniqueValues = set(featList) # 特征标签输超过2,对数据集进行二值划分 为了看出决策树构造时的区别,这里特征标签为2时也进行处理 if len(uniqueValues) >= 2: for i in range(len(dataSet)): if dataSet[i][bestFeature] == bestSplitValue: # 不做处理 pass else: dataSet[i][bestFeature] = '其他'
计算最好的数据集划分:
def chooseBestFeatureToSplitByCART(dataSet): numFeatures = len(dataSet[0]) -1 bestGiniIndex = 1000000.0 bestSplictValue =[] bestFeature = -1 # 计算Gini指数 for i in range(numFeatures): featList = [example[i] for example in dataSet] # 这里只针对离散变量 & 特征标签 uniqueVals = set(featList) bestGiniCut = 1000000.0 bestGiniCutValue =[] Gini_value =0.0 # 计算在该特征下每种划分的基尼指数,并且用字典记录当前特征的最佳划分点 for value in uniqueVals: # 计算subDataSet的基尼指数 subDataSet = splitDataSet(dataSet,i,value) prob = len(subDataSet) / float(len(dataSet)) Gini_value = prob * calcGini(subDataSet) # 计算otherDataSet的基尼指数 otherDataSet = splitOtherDataSetByValue(dataSet,i,value) prob = len(otherDataSet) / float(len(dataSet)) Gini_value = Gini_value + prob * calcGini(otherDataSet) # 选择最优切分点 if Gini_value < bestGiniCut: bestGiniCut = Gini_value bestGiniCutValue = value # 选择最优特征向量 GiniIndex = bestGiniCut if GiniIndex < bestGiniIndex: bestGiniIndex = GiniIndex bestSplictValue = bestGiniCutValue bestFeature = i print(bestFeature,bestSplictValue) # 若当前结点的划分结点特征中的标签超过3个,则将其以之前记录的划分点为界进行二值化处理 binaryZationDataSet(bestFeature,bestSplictValue,dataSet) return bestFeature
构造决策二叉树:
def createTree(dataSet,labels,chooseBestFeatureToSplitFunc = chooseBestFeatureToSplitByID3): """ 创建决策树 :param dataSet: 数据集 :param labels: 数据集每一维的名称 :return: 决策树 """ classList = [example[-1] for example in dataSet] # 类别列表 if classList.count(classList[0]) == len(classList): # 统计属于列别classList[0]的个数 return classList[0] # 当类别完全相同则停止继续划分 if len(dataSet[0]) ==1: # 当只有一个特征的时候,遍历所有实例返回出现次数最多的类别 return majorityCnt(classList) # 返回类别标签 bestFeat = chooseBestFeatureToSplitFunc(dataSet) bestFeatLabel = labels[bestFeat] myTree ={bestFeatLabel:{}} # map 结构,且key为featureLabel del (labels[bestFeat]) # 找到需要分类的特征子集 featValues = [example[bestFeat] for example in dataSet] uniqueVals = set(featValues) for value in uniqueVals: subLabels = labels[:] # 复制操作 myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels,chooseBestFeatureToSplitFunc) return myTree
测试:
dataSet,labels,predict_labels= createDataSet()myTree = createTree(dataSet,labels,chooseBestFeatureToSplitByCART)
可视化:
from pylab import *mpl.rcParams['font.sans-serif'] = ['SimHei'] # 指定默认字体mpl.rcParams['axes.unicode_minus'] = False # 解决保存图像时负号'-'显示为方块的问题treePlotter.createPlot(myTree)
关于可视化的代码,参看博文【 决策树之理解ID3算法和C4.5算法】
实验结果如下:
输出
myTree{'有房子': {'是': '同意', '其他': {'有工作': {'是': '同意', '其他': '拒绝'}}}}
在还未出实验结果前,个人以为决策二叉树第二个子结点的特征标签为薪资结构,然而并非如我想象的那样,顿时有一种感悟。基尼指数天然的解决了ID3算法所提出的问题。为什么?因为基尼指数不管特征标签个数是多少,它都把特征分为两个标签,即在薪资结构中,可能会分为的特征标签为:“1000”,“其他”。在计算该基尼指数时,用二分法来衡量它们的不确定性,就变成了信息熵在二元分类中最本质的问题。即不会出现统计上因为特征标签一一对应于分类标签,而导致统计失效的问题。
我们再来加载car.data数据,来看看CART实验效果。数据即为博文之初的测试数据,这里不再重复。
修改预测函数,使得测试数据符合二分法:
# change map feature to label's indexdef mapFeatureToLabelIndex(map,labels): for key in map.keys(): for i in range(len(labels)): if key == labels[i]: return key,i# 决策树预测函数def predict(testData,decisionTree,labels): # 获得决策树结点的下标 feature_label,feature_index = mapFeatureToLabelIndex(decisionTree,labels) # 判断feature_index所指的特征标签是否在该类中 if testData[feature_index] not in decisionTree[feature_label]: testData[feature_index] = '其他' tree = decisionTree[feature_label][testData[feature_index]] # 判断该树是叶子结点还是子结点 if (~isinstance(tree,dict)): # 如果是叶子结点,则直接返回结果 return tree else: # 子结点则继续递归 return predict(testData,tree,labels)# 决策树准确率判断def calPrecision(dataSet,predictSet): length = len(dataSet) count = 0 for i in range(length): if dataSet[i][-1] == predictSet[i]: count +=1 return count / length *100
测试实验数据准确率:
################# 实验数据的测试 测试文件 car.data ###########################import loadData as lddataSet,labels = ld.createDataSet('car.data')import copypredict_labels = copy.copy(labels)# 训练集交叉验证抽取算法import numpy as npnp.random.shuffle(dataSet)m = len(dataSet)rate = 0.7training_len = int(rate * m);trainingSet,testSet = dataSet[0:training_len],dataSet[training_len:-1]myTree = createTree(trainingSet,labels,chooseBestFeatureToSplitByCART)# 数据可视化from pylab import * mpl.rcParams['font.sans-serif'] = ['SimHei'] # 指定默认字体mpl.rcParams['axes.unicode_minus'] = False # 解决保存图像时负号'-'显示为方块的问题treePlotter.createPlot(myTree)# 根据生成的最优切分点和最优特征,生成相应数据格式的testDatapredict_result =[]for data in testSet: result = predict(data[0:-1],myTree,predict_labels) predict_result.append(result)print("decision Tree predict precision: %.2f"%calPrecision(testSet,predict_result),"%")
测试结果如下:
decision Tree predict precision: 33.98 %decision Tree predict precision: 34.17 %decision Tree predict precision: 34.36 %
使用未剪枝的CART算法,也没有提高预测的准确率。
但我们发现,决策树变成了二叉树,同样的实验效果,可视化图变得简单,且易于呈现。
CART剪枝实现
书中基本思路:CART剪枝算法从“完全生长”的决策树的底端剪去一些子树,使决策树变小(模型变简单),从而能够对未知数据有更准确的预测。CART剪枝算法由两步组成:首先从生成算法产生的决策树
以下是数学与程序的分割线
当看到上述定义时,并不知道他在说什么,也不理解为何这么做。因此,我们还是从头到尾,按照一般人的思维来慢慢逼近CART剪枝。这个过程必不可少,从中我们也能挖掘出它实现CART最基本的假设和核心的思想是什么。
前文已经提到了决策树的剪枝算法了,理所当然,我们是顺着这个思路来讲解下决策树剪枝的关键步骤。我们定义了
该定义表示了决策树的损失函数。whaterver它是什么,现在有了损失函数这个衡量标准,并且假设我们已经根据training set生成了一棵复杂的决策树,且参数
首先明确一个概念,原本躺在那的一堆数据集,在没有决策规则被挖掘时,我们只能知道数据集中的分类标签的情况。如银行贷款问题中,银行只知道有多少人可以贷款,有多少人不可以贷款,所以躺在那的数据集的不确定度是最高的。关于这部分的理解可以参看博文【 决策树之理解ID3算法和C4.5算法】,由此决策树越复杂,整体数据的不确定度越低。(数据被规则所明确,即银行在得知用户有房子的情况下,能根据训练数据统计出所有用户都是可以贷款的,这样的规则被银行挖掘出来了。)那么,显而易见,规则越多,数据分类的不确定性将大大降低。
咱们来看看决策树损失函数的定义。其中第一部分
1. 降低第一部分的不确定次数,但我们知道这是不可能的了,因为降低不确定次数的办法是再找寻一个特征,或者找寻特征的最优切分点。这在生成决策时就已经决定了,无法改变。
2. 进行剪枝操作,这是可以的。剪枝最明显地变化就是叶结点个数变少。假设是一个三叉树,那么一次剪枝它的叶结点数减少2个。变化量为
因为
在上述剪枝过程中,还需要注意一个有趣的问题。对应于每一个参数
那么问题来了,参数
CART剪枝核心思想
刚才的思路是什么?从最宏观的角度去考虑的话,就是利用
咱们先来看看决策树损失函数的定义:
做一些数学变换得:
所以说,衡量损失函数大小的真正贡献在于每个叶结点,叶结点不确定次数的累加并加个常数
为了得到树T的所有子序列
为什么要这么做?接下来的思路是什么?因为我们刚才说了,是通过最优子树来求解参数
这公式是最初的决策树损失函数变化而来的!它是其中一个子结点【吞并】它的子树,所得到【叶结点后】的损失函数。还需要强调下,因为在最初理解这个C(t)含义时,自己也被搞混了。该公式已经是剪枝完毕后的表达式了,也就是说原先的子结点已经变成了当前的叶结点。接下来会给剪枝前的表达式!
那么【剪枝前】是什么情况呢?剪枝前是以t为根结点的子树
也就是说,剪枝前该子结点的损失函数如上。具体的含义之前都有解释过,就不再叙述了。接下来我们要明确哪些是已知参数,因为决策树已经生成了,所以每个结点的不确定次数都是知道的,即
假设1:必然发生剪枝!
从中我们便能求得
当
决策树叶结点越多,不确定性越低,不解释。
当增大
当继续增大
不等式反向,所以我们只要取
假设2:剪枝发生后,当前决策树是我们的最优子树
最优子树?现在t变成了一个变量,因为我们并不知道到底剪枝哪个子结点后决策树是最优的。不急,再来看看,公式:
剪枝已经发生,此时,对应于每一个子结点t会生成不同的
剪枝的决策树什么时候最优?对于当前参数
然而在这里为了能够求得
找的
后面的思路就很简单了,根据生成的子树序列,用测试集去测试这些子树,谁的测试准确率最高,谁就获胜。
算法(CART剪枝算法)
输入:CART算法生成的决策树
T0
输出:最优决策树Tα
(1)设k=0,T=T0
(2)设α=+∞
(3)自下而上地对各个内部结点t计算C(Tt),|Tt| 以及
α(t)=C(t)−C(Tt)|Tt|−1
α=min(α,α(t))
这里,Tt 表示t为根结点的子树,C(Tt) 是对训练数据的预测误差,|Tt| 是Tt 的叶结点个数。
(4)对α(t)=α 的内部结点t进行剪枝,并对叶结点t以多数表决法决定其类,得到树T。
(5)设k=k+1,αk=α,Tk=T .
(6)如果Tk 不是由根结点及两个叶结点构成的树,则回到步骤3;否则令Tk=Tn 。
采用交叉验证法在子树序列T0,T1,...,Tn 中选取最优子树Tα
总的来说,CART剪枝算法的核心在于用【有限个子树
CART剪枝算法实现
原本想亲自实现一把CART的剪枝算法,但发现在现有代码的基础上,实现它需要比较好的python基础,然而本人在代码上的造诣尚浅,先占个位,待日后补上剪枝算法实现。
参考文献:
1. 决策树之理解ID3算法和C4.5算法。
2.李航. 统计学习方法[M]. 北京:清华大学出版社,2012
- 决策树之剪枝原理与CART算法
- 决策树之CART算法
- 决策树之CART算法
- 决策树之CART算法
- 决策树之cart算法
- 决策树之CART算法
- 决策树之CART算法
- 决策树之CART算法
- 决策树之CART算法
- 决策树之CART算法
- 决策树之CART算法
- 决策树的剪枝和CART算法
- 决策树-Cart生成和剪枝算法
- 决策树剪枝算法原理
- CART决策树剪枝
- 决策树之CART算法原理及python实现
- 机器学习算法的Python实现 (3):CART决策树与剪枝处理
- 机器学习算法的Python实现 (3):CART决策树与剪枝处理
- 环信
- XML基础知识总结
- 集合框架(collection)
- ECMAScript与JavaScript有什么关系
- emgu对图片的像素进行直接操作
- 决策树之剪枝原理与CART算法
- JavaScript基础知识总结
- 到底买不买(20)
- ftp文件上传及下载工具类
- 第三章、栈的基本操作
- Spring整合ActiveMQ
- Opengl 坐标转换\视图变换
- 条件编译
- ipad mini 无法上网