决策树
决策树简介
很多人可能玩过或见过一个猜字游戏,游戏规则很简单,就是两个参与者,一个是提问者,一个是回答者,提问者可以不断的提问题,而回答者根据提问者的问题来回答是或不是,通过不断缩小猜测事务范围提问者最后确定了答案。
图1-1构造了一个假设的三好生分类系统,黑框代表判断模块,蓝框代表终止模块,表示已得出结论,可以停止运行。分类系统中,它首先判断学生是否尊敬老师,如果判断不是则归类到不是三好生,如果判断是再继续判断是否友爱同学,如果不是则归类到不是三好生中,如果是再判断成绩是否优异,如果不是则归类到不是三好生中,如果是则归类到时三好生中。
决策树的构造
在构造决策树时,我们需要解决的第一个问题就是,当前数据集上哪个特征在划分数据分类时起决定性作用。为了找到决定性特征,划分出最好的结果,需要评估每个特征。完成测试之后,原始数据集会被划分为几个数据子集。这些数据子集会分布在第一个决策点的所有分支上,如果某个分支下的数据属于同一类型,则当前无需对数据集进行分割,如果数据子集内的数据不属于同一类型,则需要重复划分数据子集。
信息增益
划分数据集的原则是:将无序的数据变得更加有序,阻止杂乱无章数据的一种方法就是使用信息论度量信息,我们可以在划分数据之前或之后使用信息论量化度量信息的内容。在划分数据集之前之后信息发生的彼岸花称为信息增益,知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的增益信息,获得信息增益最高的特征就是最好的选择。集合信息的度量方式称为熵,熵的定义为信息的期望值。如果待分类的事务可能划分在多个分类中,则符号xi 的信息定义为:
其中P(xi)为选择该分类的概率。
为了计算熵需要计算所有类别所有可能值包含的期望值,通过下面的公式得到:
其中n是分类的数目。
代码1-2为创建数据集和根据数据集计算熵
from numpy import * from math import log import operator # 生成数据集 def createDataSet(): dataSet = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']] lables = ['no surfacing', 'flippers'] return dataSet, lables def calcShannonEnt(dataSet): numEntries = len(dataSet) lableCounts = {} for featVec in dataSet: curLable = featVec[-1] lableCounts[curLable] = lableCounts.get(curLable, 0) + 1 shannonEnt = 0.0 for key in lableCounts: prob = float(lableCounts[key]) / numEntries shannonEnt -= prob * log(prob, 2) return shannonEnt myData,lables = createDataSet() calcShannonEnt(myData)
代码1-2创建数据集然后根据数据集计算熵的结果:
>>> myData,lables = createDataSet() >>> myData [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']] >>> calcShannonEnt(myData) 0.9709505944546686
熵越高,则混合的数据越多,我们可以在数据集中添加更多的分类,观察熵是如何变化的,这里我们增加第三个名为maybe的分类,测试熵的变化,如:
>>> myData[0][-1] = 'maybe' >>> calcShannonEnt(myData) 1.3709505944546687
得到熵之后,我们就可以按照获取最大信息增益的方法划分数据集。
划分数据集
分类算法除了需要测量信息熵,还需要划分数据集,度量划分数据集的熵,以便判断当前是否正确划分了数据集。我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照哪个特征划分数据集是最好的划分方式。
在划分数据集时,我们需要用到Python语言列表类型自带的extend()和append()方法。这两个方法功能类似,但处理多个列表时,结果不同。
假定有a和b两个列表,如果执行a.append(b),则列表得到第四个元素,而且第四个元素也是一个列表。
>>> a = [1, 2, 3] >>> b = [4, 5, 6] >>> a.append(b) >>> a [1, 2, 3, [4, 5, 6]]
如果执行a.extend(b),则a列表包含b列表所有元素。
>>> a = [1, 2, 3] >>> b = [4, 5, 6] >>> a.extend(b) >>> a [1, 2, 3, 4, 5, 6]
代码1-3为按照给定特征划分数据集
def splitDataSet(dataSet, axis, value): retDataSet = [] for featVec in dataSet: if featVec[axis] != value: continue reducedFeatVec = featVec[:axis] reducedFeatVec.extend(featVec[axis + 1:]) retDataSet.append(reducedFeatVec) return retDataSet
通过代码1-3来划分数据集:
>>> myData,lables = createDataSet() >>> myData [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']] >>> splitDataSet(myData, 0, 1) [[1, 'yes'], [1, 'yes'], [0, 'no']] >>> splitDataSet(myData, 0, 0) [[1, 'no'], [1, 'no']]
代码1-4为选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet): numFeatures = len(dataSet[0]) - 1 baseEntropy = calcShannonEnt(dataSet) bestInfoGain = 0.0 bestFeature = -1 for i in range(numFeatures): # 计算唯一的分类标签 featList = [example[i] for example in dataSet] uniqueVals = set(featList) newEntropy = 0.0 # 计算每种划分方式的信息熵 for value in uniqueVals: subDataSet = splitDataSet(dataSet, i, value) prob = len(subDataSet) / float(len(dataSet)) newEntropy += prob * calcShannonEnt(subDataSet) infoGain = baseEntropy - newEntropy # 选择最好的信息增益 if (infoGain <= bestInfoGain): continue bestInfoGain = infoGain bestFeature = i return bestFeature
代码1-4给出了函数chooseBestFeatureToSplit(),该函数实现了选取特征,划分数据集,计算得出最好的划分数据集特征,在划分数据之前,先计算整个数据集的原始熵,保证了最初的无序度量值,用于与划分完之后的数据集计算的熵值进行比较。遍历当前特征中所有唯一的属性值,对每个特征划分一次数据集,然后计算数据集的新熵值,并对所有唯一特征值得到的熵求和。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。
调用代码1-4得到的结果:
>>> myData, lables = createDataSet() >>> chooseBestFeatureToSplit(myData) 0
代码运行结果告诉我们,第0个特征是最好的用于划分数据集的特征。
递归构建决策树
构造决策树算法的原理:得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大雨2两个分支的数据集划分。第一次划分之后,数据被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。
递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。如果所有实例具有相同的分类,则得到一个叶子节点或终止模块。任何达到叶子节点的数据必然属于叶子节点的分类。
代码1-5为返回出现次数最好的分类名称
def majorityCnt(classList): classCount = {} for vote in classList: classCount[vote] = classCount.get(vote, 0) + 1 sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True) return sortedClassCount[0][0]
代码1-6为创建决策树的代码
def createTree(dataSet, lables): # 类别相同则停止继续划分 classList = [example[-1] for example in dataSet] if classList.count(classList[0]) == len(classList): return classList[0] # 遍历完所有特征时返回出现次数最多的分类 if len(dataSet[0]) == 1: return majorityCnt(classList) bestFeat = chooseBestFeatureToSplit(dataSet) bestFeatLable = lables[bestFeat] myTree = {bestFeatLable: {}} del (lables[bestFeat]) # 得到列表包含的所有属性值 featValues = [example[bestFeat] for example in dataSet] uniqueValues = set(featValues) for value in uniqueValues: subLables = lables[:] myTree[bestFeatLable][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLables) return myTree
代码1-6的函数包含了两个输入参数:数据集和标签列表。标签列表包含了数据集中所有特征的标签。上述代码首先创建了一个名为classList的列表变量,用于存储数据集的所有类标签。递归函数的第一个停止条件是所有的类标签完全相同,则直接返回该类的类标签。递归函数的第二个停止条件是使用完使用的特征,仍然不能将数据集划分到仅包含唯一类别的分组,则将数据集进行投票,推选出票数最高的分类作为该类的类标签。
下一步开始创建树,当前数据集选取最好的特征存储在变量bestFeat中,得到列表包含的所有属性值。最后,代码遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用createTree(),得到的返回值将插入到字典变量myTree中,因此函数终止执行时,字典中将会潜逃很多代表叶子节点信息的字典数据。
调用代码1-6函数执行结果:
>>> myData, lables = createDataSet() >>> myTree = createTree(myData, lables) >>> myTree {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
代码1-7为获取叶子节点的树木和树的层数
# 决策树数据 def retrieveTree(i): listOfTrees = [{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}, {'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}} ] return listOfTrees[i] # 获取叶子节点的数目 def getNumLeafs(myTree): numLeafs = 0 firstStr = myTree.keys()[0] secondDict = myTree[firstStr] for key in secondDict.keys(): if type(secondDict[key]).__name__ == 'dict': numLeafs += getNumLeafs(secondDict[key]) else: numLeafs += 1 return numLeafs # 获取树的层数 def getTreeDepth(myTree): maxDepth = 0 firstStr = myTree.keys()[0] secondDict = myTree[firstStr] for key in secondDict.keys(): if type(secondDict[key]).__name__ == 'dict': thisDepth = 1 + getTreeDepth(secondDict[key]) else: thisDepth = 1 if thisDepth > maxDepth: maxDepth = thisDepth return maxDepth
代码1-7中,两个函数都具有类似的结构。使用Python的type()函数可以判断子节点是否是字典类型,如果子节点是字典类型,则该节点需要递归调用getNumLeafs()函数,如果子节点是叶子节点,则对叶子节点数加1,并返回该值。getTreeDepth()同getNumLeafs()一样,会遍历所有的叶子节点,一旦到达叶子节点,则从递归调用中返回,并将树的层数加1,最后选取层数最大的值作为树的层数。
调用代码1-7的计算树的叶子节点和层数:
>>> myTree = retrieveTree(1) >>> myTree {'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}} >>> getNumLeafs(myTree) 4 >>> getTreeDepth(myTree) 3
代码1-8使用决策树的分类函数
def classify(inputTree, featLables, testVec): firstStr = inputTree.keys()[0] secondDict = inputTree[firstStr] # 将标签字符串转换为索引 featIndex = featLables.index(firstStr) for key in secondDict.keys(): if testVec[featIndex] != key: continue if type(secondDict[key]).__name__ == 'dict': classLable = classify(secondDict[key], featLables, testVec) else: classLable = secondDict[key] return classLable
代码1-8定义的函数也是一个递归函数,使用index方法查找当前列表中第一个匹配firstStr变量的元素,然后代码递归遍历整棵树,比较testVec变量中的值与树节点的值,如果到达叶子节点,则返回当前节点的分类标签。
调用代码1-8分类函数
>>> myData, lables = createDataSet() >>> lables ['no surfacing', 'flippers'] >>> myTree = retrieveTree(0) >>> myTree {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}} >>> classify(myTree, lables, [1, 0]) 'no' >>> classify(myTree, lables, [1, 1]) 'yes'
代码1-9为本章代码整合
# coding:utf-8 from numpy import * from math import log import operator # 生成数据集 def createDataSet(): dataSet = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']] lables = ['no surfacing', 'flippers'] return dataSet, lables def calcShannonEnt(dataSet): numEntries = len(dataSet) lableCounts = {} for featVec in dataSet: curLable = featVec[-1] lableCounts[curLable] = lableCounts.get(curLable, 0) + 1 shannonEnt = 0.0 for key in lableCounts: prob = float(lableCounts[key]) / numEntries shannonEnt -= prob * log(prob, 2) return shannonEnt def splitDataSet(dataSet, axis, value): retDataSet = [] for featVec in dataSet: if featVec[axis] != value: continue reducedFeatVec = featVec[:axis] reducedFeatVec.extend(featVec[axis + 1:]) retDataSet.append(reducedFeatVec) return retDataSet def chooseBestFeatureToSplit(dataSet): numFeatures = len(dataSet[0]) - 1 baseEntropy = calcShannonEnt(dataSet) bestInfoGain = 0.0 bestFeature = -1 for i in range(numFeatures): # 计算唯一的分类标签 featList = [example[i] for example in dataSet] uniqueVals = set(featList) newEntropy = 0.0 # 计算每种划分方式的信息熵 for value in uniqueVals: subDataSet = splitDataSet(dataSet, i, value) prob = len(subDataSet) / float(len(dataSet)) newEntropy += prob * calcShannonEnt(subDataSet) infoGain = baseEntropy - newEntropy # 选择最好的信息增益 if (infoGain <= bestInfoGain): continue bestInfoGain = infoGain bestFeature = i return bestFeature def majorityCnt(classList): classCount = {} for vote in classList: classCount[vote] = classCount.get(vote, 0) + 1 sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True) return sortedClassCount[0][0] def createTree(dataSet, lables): # 类别相同则停止继续划分 classList = [example[-1] for example in dataSet] if classList.count(classList[0]) == len(classList): return classList[0] # 遍历完所有特征时返回出现次数最多的分类 if len(dataSet[0]) == 1: return majorityCnt(classList) bestFeat = chooseBestFeatureToSplit(dataSet) bestFeatLable = lables[bestFeat] myTree = {bestFeatLable: {}} del (lables[bestFeat]) # 得到列表包含的所有属性值 featValues = [example[bestFeat] for example in dataSet] uniqueValues = set(featValues) for value in uniqueValues: subLables = lables[:] myTree[bestFeatLable][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLables) return myTree # 决策树数据 def retrieveTree(i): listOfTrees = [{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}, {'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}} ] return listOfTrees[i] # 获取叶子节点的数目 def getNumLeafs(myTree): numLeafs = 0 firstStr = myTree.keys()[0] secondDict = myTree[firstStr] for key in secondDict.keys(): if type(secondDict[key]).__name__ == 'dict': numLeafs += getNumLeafs(secondDict[key]) else: numLeafs += 1 return numLeafs # 获取树的层数 def getTreeDepth(myTree): maxDepth = 0 firstStr = myTree.keys()[0] secondDict = myTree[firstStr] for key in secondDict.keys(): if type(secondDict[key]).__name__ == 'dict': thisDepth = 1 + getTreeDepth(secondDict[key]) else: thisDepth = 1 if thisDepth > maxDepth: maxDepth = thisDepth return maxDepth def classify(inputTree, featLables, testVec): firstStr = inputTree.keys()[0] secondDict = inputTree[firstStr] # 将标签字符串转换为索引 featIndex = featLables.index(firstStr) for key in secondDict.keys(): if testVec[featIndex] != key: continue if type(secondDict[key]).__name__ == 'dict': classLable = classify(secondDict[key], featLables, testVec) else: classLable = secondDict[key] return classLable
总结
决策树分类器就像带有终止块的流程图,终止块代表分类结果。开始处理数据集时,我们首先需要测量集合中数据的不一致性,也就是熵,然后寻找最优方案划分数据集,直到数据集中的所有数据属于同一类。构建决策树时,我们通常采用递归的方法将数据集转化为决策树,一般我们并不构造新的数据结构,而是使用Python语言内嵌的数据结构字段存储树节点的信息。