决策树简介
很多人可能玩过或见过一个猜字游戏,游戏规则很简单,就是两个参与者,一个是提问者,一个是回答者,提问者可以不断的提问题,而回答者根据提问者的问题来回答是或不是,通过不断缩小猜测事务范围提问者最后确定了答案。
图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语言内嵌的数据结构字段存储树节点的信息。