【机器学习】决策树
一、决策树的构造
优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。
缺点:可能会产生过度匹配问题
使用数据类型:数值型和标称型
创建分支的伪代码函数createBranch()如下:
If so return 类标签;
Else
寻找划分数据集的最好特征
划分数据集
创建分支节点
for 每个划分的子集
调用函数createBranch并增加返回结果道分支节点中
return 分支节点
上面伪代码是一个递归函数,在倒数第二行直接调用了它自己。后面我们将它转换为Python代码,here我们要进一步了解算法是如何划分数据集的。
决策树的一般流程
- 收集数据:可以使用任何方法
- 准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化
- 分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。
- 训练算法:构造树的数据结构
- 测试算法:使用经验树计算错误率
- 使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义
一些决策树算法采用二分法划分数据,本书并不采用这种方法。如果依据某个属性划分数据将会产生4个可能的值,我们将数据划分为四块,并创建四个不同的分支。本书将使用ID3算法划分数据集,该算法 处理如何划分数据集,何时停止划分数据集(进一步信息可以参见http://en.wikipedia.org/wiki/ID3_algorithm)。每次划分数据集时我们只选取一个特征属性。
1.1信息增益
划分数据集的大原则是:将无需的数据变得更加有序。我们可以使用多种方法划分数据集,但是每种方法都有各自的优缺点。组织杂乱无章数据的一种方法就是使用信息论量化度量信息的内容。
在划分数据集之前之后信息发生的变化成为信息增益,知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择。
在可以评测哪种数据划分方式是最好的数据划分之前,我们必须学习如何计算信息增益。集合信息的度量方式称为香农熵或简称熵,这个名字来源于信息论之父克劳德·香农。
熵定义为信息的期望值,那么信息是什么?xi的信息可定义为:l(xi) = -log(p(xi)),其中p(xi)是选择该分类的概率。
熵指的是所有类别所有可能值包含的信息期望值,可表示为:
from math import log
def calcShannonEnt(dataSet):
numEntries =len(dataSet)
labelCounts={}#创建字典
for featVec in dataSet:
currentLabel =featVec[-1]
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 shannonEnt
我们可以利用createDataSet函数进行测试
def creatDataSet():
dataSet = [[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no']]
labels = ['no surfacing','flippers']
return dataSet,labels
myData,labels = creatDataSet()
print("原数据为:",myData)
print("标签为:",labels)
shang = calShang(myData)
print("香农熵为:",shang)
得到熵之后,我们就可以按照获取最大增益的办法划分数据集。
1.2划分数据集
def splitDataSet(dataSet,axis,value):
retDataSet=[]
for featVec in dataSet:
if featVec[axis]==value:
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;beatFeature=-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):
bestInfoGain=infoGain
beatFeature=i
return beatFeature
在开始划分数据集之前,第三行代码计算了整个数据集的原始香农熵,我们保存最初的无序度量值,用于与划分完之后的数据集计算的熵值进行比较。第一个for循环遍历数据集中的所有特征。使用列表推导来创建新的列表,将数据集中所有第i个特征值或者所有可能存在的值写入这个新的list中。然后用python语言原生的集合set数据类型。集合数据类型和列表类型相似,不同之处在于集合类型中的每个值互不相同。从列表中创建集合是python得到列表中唯一元素值的最快方法
遍历当前特征中的所有唯一属性值,对每个特征划分一次数据集,然后计算数据集的新熵值,并对所有唯一特征值得到的熵求和。信息增益是熵的减少或者是数据无序度的减少,大家肯定对于将熵用于度量数据无序度的减少更容易理解。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。
1.3递归构建决策树
工作原理
得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将被向下传递道树分支的下一个节点,在这个节点熵,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集。
递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。任何到达叶子节点的数据必然属于叶子节点的分类。
def majorityCnt(classList):
classCount={}
for vote in classList:
if vote not in classCount.keys():
classCount[vote]=0
classCount[vote]+=1
sortedClassCount=sorted(classCount.iteritems(),key=operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0]
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:
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
二、在python中使用Matplotlib注释绘制树形图
2.1Matplotlib注释
本书将使用Matplotlib的注释功能绘制树形图,它可以对文字着色并提取多种形状以供选择,而且我们还可以反转箭头,将它指向文本框而不是数据点。
decisionNode = dict(boxstyle="sawtooth",fc="0.8")
leafNode = dict(boxstyle="round4",fc="0.8")
arrow_args=dict(arrowstyle="<-")
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)
plotNode(U'决策节点', (0.5,0.1), (0.1,0.5),decisionNode)
plotNode(U'叶节点', (0.8,0.1), (0.3,0.8),leafNode)
plt.show()
中文乱码
1. 点击此网页查看解决方法
2.2构造注解树
获取叶节点的数目和树的层数
def getNumLeafs(myTree):
numLeafs=0
firstStr=list(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
####
##python3改变了dict.keys,返回的是dict_keys对象,支持iterable 但不支持indexable,我们可以将其明确的转化成list。
firstStr=list(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 retrieveTrees(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]
代码报错
在2.7中,找到key所对应的第一个元素为:firstStr = myTree.keys()[0],这在3.4中运行会报错:‘dict_keys’ object does not support indexing,这是因为python3改变了dict.keys,返回的是dict_keys对象,支持iterable 但不支持indexable,我们可以将其明确的转化成list,则此项功能在3中应这样实现:
firstSides = list(myTree.keys())
firstStr = firstSides[0]#找到输入的第一个元素
绘制树
def plotMidText(cntrPt,parentPt,txtString):
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):
numLeafs=getNumLeafs(myTree)
depth=getTreeDepth(myTree)
firstStr=list(myTree.keys())[0]
cntrPt=(plotTree.xOff+(1.0+float(numLeafs))/2.0/plotTree.totalW,
plotTree.yOff)
plotMidText(cntrPt,parentPt,nodeTxt)
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:
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
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()
之前打错了一个字母啊啊啊啊,现在终于能画出来了
函数createPlot()是我们使用的主函数,它调用了plotTree(),函数plotTree又依次调用了前面介绍的函数和plotMidText()。绘制树形图的很多工作都是在函数plotTree()中完成的,函数plotTree()首先计算树的宽和高。全局变量plotTree.totalW存储树的宽度,全局变量plotTree.totalD存储树的深度,我们使用着两个变量计算树节点的摆放位置,这样可以将树绘制在水平方向和垂直方向的中心位置。
plotTree()是个递归函数,树的宽度用于计算放置判断节点的位置,主要的计算原则是将它放在所有叶子节点的中间,而不仅仅是它子节点的中间。同时我们使用两个全局变量plotTree.xOff和plotTree.yOff追踪已经绘制的节点位置,以及放置下一个节点的适当位置。
另一个需要说明的问题是,绘制图形的x轴有效范围是0.0到1.0,y轴有效范围也是0.0~1.0.为了方便期间,划分图形的宽度,从而计算得到当前节点的中心位置,也就是说,我们按照叶子节点的数目将x轴划分为若干部分。按照图形比例绘制树形图的最大好处是无需关心实际输出图形的大小,一大图形大小发生了变化,函数会自动按照图形大小重新绘制。
接着,绘出子节点具有的特征值,或者延此分支向下的数据实例必须具有的特征值。使用函数plotMidText()计算父节点和子节点的中间位置,并在此处添加简单的文本标签信息。
然后,按比例减少全局变量plotTree.yOff,并标注此处将要绘制子节点,这些节点既可以是叶子节点也可以是判断节点,此处需要只保存绘制图形的轨迹。因为我们是自顶向下绘制图形,因此需要依次递减y坐标值,而不是递增。然后程序采用函数getNumLeafs()和getTreeDepth()以相同的方式递归遍历整棵树。
三、测试和存储分类器
在本章中,我们首先使用决策树对实际数据进行分类,然后使用决策树预测隐形眼镜类型对算法进行验证。
3.1测试算法:使用决策树执行分类
使用决策树的分类函数
def classify(inputTree,featLabels,testVec):
global classLabel
firstStr=list(inputTree.keys())[0]
secondDict=inputTree[firstStr]
featIndex=featLabels.index(firstStr)
for key in secondDict.keys():
if testVec[featIndex]==key:
if type(secondDict[key]).__name__=='dict':
classLabel=classify(secondDict[key],featLabels,testVec)
else: classLabel=secondDict[key]
return classLabel
使用pickle模块存储决策树
#这里打开方式得写成'wb'和'rb',否则会出现异常。
def storeTree(inputTree,filename):
import pickle
fw=open(filename,'wb')
pickle.dump(inputTree,fw)
fw.close()
def grabTree(filename):
import pickle
fr=open(filename,'rb')
return pickle.load(fr)
问题报错
1.是’w’:TypeError: write() argument must be str, not bytes
2.是’r’:UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0x80 in position 0: illegal multibyte sequence
这里需要wb和rb同时
四、示例:使用决策树预测隐形眼镜类型
fr=open('lenses.txt')
lenses=[inst.strip().split('\t') for inst in fr.readlines()]
lensesLabels=['age','prescript','astigmatic','tearRate']
lensesTree=createTree(lenses,lensesLabels)
print(lensesTree)
createPlot(lensesTree)
着决策树的不同分支,我们可以得到不同患者需要佩戴的隐形眼镜类型,从该图中我们可以得到,医生最多需要问四个问题就可以确定出患者需要佩戴何种隐形眼镜。