K近邻可以完成很多分类任务,但其无法给出数据的内在含义。
这次来阐述下决策树的原理及python实现,另有实例剖析。
决策树可以使用不熟悉的数据集合,从中提取一系列规则。
原理:在构造决策树时,需要找到当前数据集上哪个特征在划分数据分类时起决定作用。为了找到决定性的特征,划分出最好的结果,我们必须评估每个特征。找到最好的划分特征时,完成第一次分类,数据集被划分成几个数据子集,这些数据子集会分布在第一个决策点的所有分支上。如果某个分支下的数据属于同一类型,则无需再分割。若子集内的数据不属于同一类型,则要重新划分数据子集,直到在一个数据子集下的数据类型相同。
一般流程:收集数据后,因为树构造算法只适用于标称型数据,因此数值型数据必须离散化。构造树完成之后,应该检查图形是否符合预期。使用训练算法构造树的数据结构。使用经验树计算错误率用于测试算法。
一般决策树采用二分法,这里使用ID3算法划分数据集。以信息熵和信息增益度为衡量标准,从而实现对数据的归纳分类。每次划分数据集时我们只选取一个特征属性。
决策树学习采用的是自顶向下的递归方法,其基本思想是以信息熵为度量构造一棵熵值下降最快的树,到叶子节点处的熵值为零,此时每个叶节点中的实例都属于同一类。
事件ai发生的概率用p(ai)来表示,而-log2(p(ai))表示为事件ai的不确定程度,称为ai的自信息量,sum(p(ai)*I(ai))称为信源S的平均信息量—信息熵。
ID3的原理是基于信息熵增益达到最大,设原始问题的标签有正例和负例,p和n表示其相应的个数。则原始问题的信息熵为
其中N为该特征所取值的个数,比如{rain,sunny},则N即为2
Gain = BaseEntropy – newEntropy
ID3的原理即使Gain达到最大值。信息增益即为熵的减少或者是数据无序度的减少。
ID3易出现的问题:如果是取值更多的属性,更容易使得数据更“纯”(尤其是连续型数值),其信息增益更大,决策树会首先挑选这个属性作为树的顶点。结果训练出来的形状是一棵庞大且深度很浅的树,这样的划分是极为不合理的。 此时可以采用C4.5来解决
C4.5的思想是最大化Gain除以下面这个公式即得到信息增益率:
其中底为2
python实现:
1:计算给定数据集的香农熵.
from math import log #导入数学运算log
import operator
def calcShannonEnt(dataSet):
numEntries = len(dataSet)#得到数据集的实例个数
labelCounts = {}#初始化一个字典用于存储键值和对应的出现次数
for featVec in dataSet:
currentLabel = featVec[-1]#获取每行最后一个数值,作为键值
if currentLabel not in labelCounts.keys():#如果当前键值不存在,则将键值添加进字典,键值对应的数值为0,意思是出现零次,若存在,则数值加1,代表出现次数多一次
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
shannonEnt = 0.0#用所有标签的发生概率计算香农熵
for key in labelCounts:#使用所有类标签的发生频率计算类别出现的概率。我们将用这个概率计算香农熵,统计所有类标签发生的次数
prob = float(labelCounts[key])/numEntries
shannonEnt -= prob*log(prob,2)# #以2为底数求对数
return shannonEnt
上面大致是求数据集中每个实例标签出现的频率,然后用这个频率 计算香农熵。
这里作为测试构建一个测试数据集:
def createDataSet():
dataSet = [[1,1,'yes'],#最后一列出现不同标签的数量越高,则熵越大,代表无序程序越高,我们在数据集中添加的分类就越多
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no']]
labels = ['no surfacing','flippers']
return dataSet,labels#在这里,数据集是针对标签的,第一个数据对应第一个标签,最后一个数据代表判断标签
2:得到香农熵后,就可以按照最大信息增益的方法划分数据集。
划分数据集,然后计算划分数据集的熵,对每个特征划分数据集的结果计算一次信息熵,判断按照哪个特征划分数据集是最好的划分方式。
def splitDataSet(dataSet,axis,value):#输入:待划分的数据集,划分数据集的特征,需要返回的特征的值
retDataSet = []#python不考虑内存问题,在函数中传递的是列表的引用,在函数内部是对列表对象的更改,将会影响该列表对象的整个生存周期。为了消除影响,新建一个列表对象。
for featVec in dataSet:
if featVec[axis] == value:#数据集中每个元素都是列表,遍历每个元素,发现符合的就添加到列表中;当按照某个特征划分数据集时,需要将所有符合的元素抽取出来。感觉运行结果是第axis个元素的值为value时,抽取这个元素。
reducedFeatVec = featVec[:axis]#当axis为0时,0:0是空;0:1是0的值
reducedFeatVec.extend(featVec[axis+1:])#extend是把两个列表合并
retDataSet.append(reducedFeatVec)#append是把后一个列表直接当作一个元素添加进前一个列表
return retDataSet
这里只是选择了一个特征及特征值的子集合,是一个小程序,若划分整个数据集,则要反复调用这个函数,得到每一种情况的子集合。
3:遍历整个数据集,循环计算香农熵和splitDataSet()函数,找到最好的特征划分方式。
"""选取特征值,划分数据集,计算出最好的划分数据集的特征"""
def chooseBestFeatureToSplit(dataSet):#dataSet需是一种由列表元素组成的列表,所有的列表元素都要具有相同的数据长度;数据的最后一列或每个元素的做后一列都是当前元素的标签。list中数据类型不限,不影响。
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]#把第i个索引所对应的值提取出来
uniqueVals = set(featList)#把提出来的值唯一化,set是集合数据类型,值不相同
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):#比较信息增益,得到最大值
bestInfoGain = infoGain
bestFeature = i
return bestFeature#返回最好特征划分的索引值
这里就实现了一层树的最好划分方式,得到最好的划分特征的索引值。
4:递归构建决策树,就是一层层的进行划分,对原始数据集进行划分后,由于特征值可能大于两个,得到第二层的数据子集,再次划分,从而实现全划分,直到遍历完所有划分集的属性,或者每个分支下的实例都具有相同的分类,则得到一个叶子节点或者终止块。我们也可以设置算法可以划分的最大分组数目,若最后得到的数据子集中类标签依旧不唯一,则使用下面的程序,计算在这个子集中标签出现次数最多的,作为该子集的标签。
"""得到每个类标签出现的次数,返回出现次数最多的分类名称"""
def majorityCnt(classList):
classCount={}
for vote in classCount:
if vote not in classCount.keys():classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.iteritem(),key=operator.itemgetter(1),reverse = true) #对标签出现的频率按从大到小进行排序
return sortedClassCount[0][0] #返回出现频率最大的那个标签作为子集的标签。
5:创建树的函数代码,这应该是一个主函数,调用以上的函数,对一个原始数据集创建决策树。
#递归构建决策树,这是这个文件的主函数,对已有的数据集,知调用这一个函数就创建了决策树******************************************
def createTree(dataSet,labels):#输入:数据集和标签列表,标签列表中集中所有特征的标签,算法本身不需要,只作为输入参数提供
classList = [example[-1] for example in dataSet]#提取数据集最后一列数据
if classList.count(classList[0]) == len(classList):#当计算在最后一列数据中与第一个值相同的元素个数与最后一列数据个数相同时,直接返回第一个元素值,意思是所有类标签都相同
return classList[0]
if len(dataSet[0]) == 1:#当数据集中第一个也代表所有元素的长度为1时,仍然类标签不相同,就挑选出现次数最多的作为返回值
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet)#返回最佳特征值划分的索引
bestFeatLabel = labels[bestFeat]#得到最佳特征值索引的标签
myTree = {bestFeatLabel:{}}#使用字典类型存储树的信息
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#返回最终的字典信息
6:使用决策树进行分类,依靠训练数据构造了决策树之后,可以将它用于实际数据的分类。在执行数据分类时,需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行直到进入叶节点,最终给出测试数据的类型。
使用决策树的分类函数.
def classify(inputTree,featLabels,testVec):#根据已有的决策树,对给出的数据进行分类
firstStr = inputTree.keys()[0]
secondDict = inputTree[firstStr]
featIndex = featLabels.index(firstStr)#这里是将标签字符串转换成索引数字
for key in secondDict.keys():
if testVec[featIndex] == key:#如果key值等于给定的标签时
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key],featLabels,testVec)#递归调用分类
else: classLabel = secondDict[key]#此数据的分类结果
return classLabel
存储决策树:
#由于构建决策树是很耗时的,但用创建好的决策树就可以很快解决分类问题,最好每次次执行分类时调用已构造好的决策树,pickle可以存储对象,也可以读出对象,字典对象也不例外,k近邻不能持久分类,必须每次都计算
def storeTree(inputTree,filename):
import pickle
fw = open(filename,'w')
pickle.dump(inputTree,fw)
fw.close()
def grabTree(filename):
import pickle
fr = open(filename)
return pickle.load(fr)
8:以下是使用Matplotlib注解绘制树形图的代码,包括我的注释
#-*- coding:utf-8 -*-
import matplotlib.pyplot as plt
decisionNode = dict(boxstyle="sawtooth",fc="0.8")#设置文本框的格式
leafNode = dict(boxstyle="round4",fc="0.8")
arrow_args = dict(arrowstyle="<-")#设置箭头
#annotate:第一个是要显示的文字,第二个是点的位置,第三个和第五个是表示坐标轴左下方是0,0;第四个是文字的中心位置,第六个和七个是表明文字相对于文字放置位置中心点的偏移,第八个是给文字画个什么样的边框,最后一个是箭头。
def plotNode(nodeTxt,centerPt,parentPt,nodeType):
createPlot.ax1.annotate(nodeTxt,xy=parentPt,xycoords='axes fraction',\
xytext=centerPt,textcoords='axes fraction',va="center",ha="center",bbox=nodeType,arrowprops=arrow_args)
def createPlot():
fig = plt.figure(1,facecolor='white')#新建一个绘图区
fig.clf()#清空绘图区
createPlot.ax1 = plt.subplot(111,frameon=False)#ax是createPlot的一个属性,这里是定义这个属性
plotNode('decisionNode',(0.5,0.1),(0.1,0.5),decisionNode)#调用
plotNode('leafNode',(0.8,0.1),(0.3,0.8),leafNode)
plt.show()
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 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 plotMidText(cntrPt,parentPt,txtString):#找到父节点和字节点之间的中间位置,放置0或1
xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0]
yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1]
createPlot.ax1.text(xMid,yMid,txtString)
def plotTree(myTree,parentPt,nodeTxt):#计算所有叶节点的位置,并绘制叶节点以及0和1的位置
numLeafs = getNumLeafs(myTree)#首先计算宽和高
depth = getTreeDepth(myTree)
firstStr = myTree.keys()[0]
cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW,plotTree.yOff)#计算字节点的位置
plotMidText(cntrPt,parentPt,nodeTxt)#绘制0或者1
plotNode(firstStr,cntrPt,parentPt,decisionNode)#绘制最开始的父节点
secondDict = myTree[firstStr]
plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD#因为父节点在最上面,则需要往下减去偏移量
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
plotTree(secondDict[key],cntrPt,str(key))#如果是字典则递归调用
else:#如果不是字典,则计算x偏移,就是叶节点的位置,绘制叶节点以及0或者1
plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
plotNode(secondDict[key],(plotTree.xOff,plotTree.yOff),cntrPt,leafNode)
plotMidText((plotTree.xOff,plotTree.yOff),cntrPt,str(key))
plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD#把所有的叶节点都计算完之后,将把y偏移加回来,使最后的y在父节点上
def createPlot(inTree):
fig = plt.figure(1,facecolor='white')
fig.clf()
axprops = dict(xticks=[],yticks=[])
createPlot.ax1 = plt.subplot(111,frameon=False,**axprops)
plotTree.totalW = float(getNumLeafs(inTree))#这都是全局变量
plotTree.totalD = float(getTreeDepth(inTree))
plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0
plotTree(inTree,(0.5,1.0),'')#绘制节点树形图
plt.show()
总结:有些决策树非常好的匹配了实验数据,然而这些匹配选项可能太多了,这样的称为过渡匹配。为了减少过度匹配的问题,我们可以裁剪决策树,去掉一些不必要的叶子节点,如果叶子节点只能增加少许信息,则可以删除该节点,将它并入到其他叶子节点中。
ID3无法直接处理数值型数据,尽管可以通过量化的方法将数值型数据转化为标称型数值,但若存在太多的特征划分,ID3仍然存在其他问题。
决策树开始处理数据时,我们首先需要测量集合中数据的一致性,也就是熵,然后寻找最优方案划分数据集,直到数据集中的所有数据归于一类。