您的位置:首页 > 其它

读书笔记:机器学习实战【第3章 决策树】

2017-08-21 12:13 525 查看

读书笔记:机器学习实战【第3章 决策树】

决策树的优点:

计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。

缺点:

可能会产生过度匹配问题

适用数据类型:数值型,标称型。

3.1 决策树的构造

在构造决策树时,要解决的第一个问题就是,当前数据集上哪个特征在划分数据分类的时候起决定作用。为了找到决定性特征,划分出最好的结果,我们必须苹果每个特征,而完成测试之后,原始数据集就被划分为几个数据子集,这些数据子集会分布在第一个决策点的所有分支上。

如果某个分支下的数据属于同一类型,则当前分支所有数据都被划分完成,无需进一步分割数据;否则需要重复划分数据子集的过程,直到所有具有相同类型的数据均在一个数据子集内。

创建分支的伪代码函数createBranch()如下所示:

检测数据集中每一个子项是否属于同一分类:
If so return 类标签;
Else
寻找划分数据集的最好特征
划分数据集
创建分支节点
for 每个划分的子集
调用函数createBranch并增加返回结果到分支节点中
return 分支节点


上面的伪代码createBranch是一个递归函数,在倒数第二行直接调用了它自己,后面将要把上面的伪代码转换为Python代码,为此,需要进一步了解算法是如何划分数据集的。

决策树的一般流程:

收集数据:可以使用任何方法

准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。

分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。

训练算法:构造树的数据结构。

测试算法:使用经验树计算错误率。

使用算法:此步骤可以使用于任何监督学习算法,而使用决策树可以更好理解数据内在含义。

一些决策树算法采用二分法划分数据,这里并不采用这种方法:如果依据某个属性划分数据会产生4个可能的值,我们就把数据划分成4块,并创建4个不同的分支。

本书采用ID3算法划分数据集。

3.1.1 信息增益

哪个属性最适合用于划分数据集呢?为了回答这个问题我们需要知道划分数据集的目的——将无序的数据变得更加有序,比如说,一组全部为1元的硬币和一堆多少面值都有的硬币,显然全为1元硬币的那堆更加有序,而后者则显得更加混乱无序。

为了衡量数据是否有序,这里我们引入信息论进行度量:我们把划分数据集前后信息发生的变化称为信息增益,只要知道如何计算信息增益,我们就可以计算根据每个特征划分数据所带来的信息增益,然后,从中选择带来最高信息增益的特征就是了。

这里我们引入熵的定义:熵被定义为信息的期望值,而信息的含义是这样的,比如对于待分类的事物,如果该事物它可能划分在多个类别中和,则其所包含的信息如下:

l(x i )=−log 2 p(x i )

其中,p(x i ) 是事物属于x i 类的概率。

为了计算熵,我们需要计算所有类别的所有可能值包含的信息期望值,公式如下:

H=−∑ n i=1 p(x i )log 2 p(x i )

其中,n是可能分类的总数目,换言之,事物属于不同类别时所拥有的信息量是不同的,而根据不同类别下的信息量与对应概率,计算出来的期望值就是熵,熵衡量的是一组事物的信息量大小。熵越大,事物的信息量越大,也显得越混乱。

换句话说,如果在对数据集依照某个特征划分之后,能够使得数据集的熵显著下降,就说明这一特征相当有成效。

下述程序清单的功能是计算给定数据集的熵:

from math import log

def calcShannonEnt(dataSet):  #计算
numEntries = len(dataSet) #数据集长度,即子样本总数
labelCounts = {} #建立分类标签初始空字典
##以下五行:为所有可能分类创建字典
for featVec in dataSet: #逐行读取数据for循环
currentLabel = featVec[-1] #本行数据类别标签
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0 ##如果字典中不包含该标签,则向字典引入该标签,标记出现次数为0。
labelCounts[currentLabel] +=1 #本标签的出现次数+1
##字典创建完成,现在我们获得了一个字典LabelCounts

shannonEnt = 0.0 #初始化香农熵
for key in labelCounts: #字典内的for循环是可以的
prob = float(labelCounts[key])/numEntries ##把频数转化为浮点,然后除以numEntries即总样本数求出本分类的概率
shannonEnt -= prob * log(prob,2) ##加总每个分类xi下的分类pi*log(xi)

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


接下来,就可以对这个数据集计算香农熵:

In [4]: myDat,labels=createDataSet()

In [5]: myDat
Out[5]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

In [6]: calcShannonEnt(myDat)
Out[6]: 0.9709505944546686


熵越高,则混合的数据越多,我们可以在数据集中增加更多的分类,观察熵的变化:

In [7]: myDat[0][-1] = 'maybe'

In [8]: myDat
Out[8]: [[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

In [9]: calcShannonEnt(myDat)
Out[9]: 1.3709505944546687


可以看到熵变大了。

3.1.2 划分数据集

为了对数据集进行划分,除了测量信息熵之外,还需要选择合适的特征,为了判断最合适用来划分数据集的特征,我们需要对每个特征的划分结果计算信息熵,然后判断按照哪个特征划分数据集是最好的划分方式。

以下代码是划分数据集的:

def splitDataSet(dataset,axis,value): #按照给定特征及取值划分数据集,dataset为待划分数据集,axis为用于划分数据集的特征的所在列,value为需要返回的特征的值
##1.创建新的list对象:
retDataSet = [] #初始化空数据集
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:]) #这两行加在一起,目的是获得除了划分特征外的特征向量,之所以要除掉划分特征,是因为该特征后面不会再用到。另外,注意extend和append的区别。
retDataSet.append(reducedFeatVec) ##这样就获得了不含划分特征,且均属于value类的数据集
return retDataSet
#这个函数获得的,是根据划分特征axis,从数据集中取出所有该特征的取值为value的块。


Python中列表的传递是不会新建表的,而只是获得视图,因此,对子列表的修改会反映到源列表上。这就是为什么这里要新建一个list的缘故。

关于append和extend的区别,在处理多个列表时,append(list)是将整个目标列表list添加入原列表,extend则是把list中的元素逐个填入列表。

仍然在前面生成的简单样本数据上测试函数splitDataSet():

e4bf
In [15]: myDat,lables = createDataSet()

In [16]: myDat
Out[16]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

In [17]: splitDataSet(myDat,0,1) #以第0列作为划分特征,取出所有第0列为1的行:
Out[17]: [[1, 'yes'], [1, 'yes'], [0, 'no']]


以上是根据特征及取值获得分割后的数据集,接下来我们将遍历整个数据集,循环计算香农熵和splitDataSet()函数,目标是找到最好的特征划分方式:

def chooseBestFeatureToSplit(dataSet): #找出最优特征并根据该特征划分数据集
numFeatures = len(dataSet[0]) -1 ##单个行的长度,就是总列数,-1后就是可用特征数了,因为减去的是分类标签所在列。
baseEntropy = calcShannonEnt(dataSet) #基准香农熵,计算信息增益要用各特征下的香农熵减去这个
bestInfoGain = 0.0;bestFeature = -1 #信息增益初始化为0,最优特征初始化为-1

for i in range(numFeatrues):
#创建唯一的分类标签列表:
featList = [example[i] for example in dataSet] #注意这种写法,其含义是从数据集dataSet中,提取出所有行的I列标签。
uniqueVals = set(featList) #建立不含重复值的feat集合。
newEntropy = 0.0 ##初始化本特征的信息熵

##计算每种划分方式的信息熵:
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value) #对每个value分割数据集
prob = len(subDataSet)/float(len(dataSet)) ##该部分数据集与总数据集样本量之比,是该value的权重
newEntropy += prob * calcShannonEnt(subDataSet) ##prob权重乘以本部分数据集,获得的期望值就是本value的熵。

infoGain = baseEntropy - newEntropy #计算信息增益
if (infoGain > bestinfoGain) : #根据特征i计算出来的信息增益是否高过当前最优信息增益,来判断是否最优
bestinfoGain = infoGain
bestFeature = i
return bestFeature


现在,测试上面的代码的实际输出结果:

In [27]: chooseBestFeatureToSplit(myDat)
Out[27]: 0


说明第0组变量是最适合的。

3.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) ##使用operator操作键值排列字典
##operator.itemgetter(i):定义一个函数,该函数的含义:获取对象第i个维度的值。
##sort:排序函数
##iteritems:获得的是键值对,key:定义函数,因此key所定义的取维度1的函数,实际上取了iteritems键值对中的值(即排在1维的那个)
##reverse:True为倒序
return sortedClassCount


该函数返回的是投票排序列表。

接下来,就要写创建决策树的函数代码:

def createTree(dataSet,labels):   #这个labels是特征名,和分类标签不是一个意思,不知道为什么要用同一个词
classList = [example[-1] for example in dataSet] #创建分类标签列表

if classList.count(classList[0]) == len(classList): ##IF条件;如果列表列表中第一类的频数等于类别列表的样本总数
return classList[0] ##那么不用继续划分了,直接返回分类标签

if len(dataSet[0]) == 1: ##IF条件:行长度为1,即已经遍历完毕所有特征,只剩下个分类标签列。
return majorityCnt(classList) ##那么不继续划分了,直接返回分类标签

bestFeat = chooseBestFeatureToSplit(dataSet) #最优特征编号
bestFeatLable = labels[bestFeat] #最优特征名
myTree = {bestFeatLable:{}} ##初始化决策树字典,把已经获得的最优特征列加入其中的key,子字典为空。实际上,子字典依然会是决策树字典的形式,大概是这样:
## {根节点:{节点1:{...},节点2:{...}}

del(labels[bestFeat]) #把已经用于划分的bestFeat特征从labels中删去,获得新的用于下一轮迭代的labels。
featValues = [example[bestFeat] for example in dataSet] ##bestFeat向量,也就是每一行的第bestFeat列的值组成的列表。
uniqueVals = set(featValues) #建立该bestFeat列取值的唯一值集合

for value in uniqueVals:
subLabels = labels[:]
myTree[bestFeatLable][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)
##字典的value其实就是下层决策树,下层决策树是新的字典。
##根据bestFeat(最优特征列),value(特征的每个可能取值)划分出若干个子数据集,每个子数据集都是bestFeatLable下的第value个子树,递归直到返回分类标签。

return myTree


接下来,测试创建决策树的代码:

In [57]: myDat,labels = createDataSet()

In [58]: myTree = createTree(myDat,labels)

In [59]: myTree
Out[59]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}


这样,就获得了一个嵌套字典形式的决策树。

3.2 在Python中使用Matplotlib注解绘制属性图

决策树的主要优点就是直观易于理解,所以如果不能将其直观地显示出来,就无法发挥其优势,本节学习如何编写代码绘制树形图。

3.2.1 Matplotlib注解

Matplotlib提供了一个注解工具annotations,非常有用,它可以再数据图形上添加文本注释。注解常用语解释数据的内容,由于数据上面直接存在文本描述非常丑陋,因此工具内嵌支持带箭头的画线工具。

输入下面的程序代码,使用文本注解绘制树形图:

import matplotlib.pyplot as plt

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)
##以上函数,绘制带箭头的注解
##annotate为文本注释,create.Plot看下面、

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()


注意,以上的createPlot()尚不完善,后面会继续完善。

测试程序的输出结果:

In [79]: createPlot()




3.2.2 构造注解树

为了构造一颗完整的树,我们必须知道有多少个叶节点,以便正确确定x轴的长度;我们还需要知道树有多少层,以便正确确定y轴的高度,这里定义两个新函数getNumLeafs()和getTreeDepth(),来获取节点数目和树的层数:

def getNumLeafs(myTree):
numLeafs = 0 #初始化叶节点数为0
firstStr = myTree.keys()[0] #根节点
secondDict = myTree[firstStr] #根节点的子字典,其实就是根节点下的子树

for key in secondDict.keys():#对每个key,也就是每个子树for循环:
if type(secondDict[key]).__name__=='dict': #监测类别是否是字典
numLeafs += getNumLeafs(secondDict[key]) #因为类别是字典,说明还能继续向下遍历,则继续向本子树key下遍历计算叶节点数
else: numLeafs +=1 ##类别不是字典,说明到头,叶节点数+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


进行测试,首先,保存先前生成的树:

myDat,labels = createDataSet()
myTree = createTree(myDat,labels)


然后测试:

In [82]: getTreeDepth(myTree)
Out[82]: 2

In [83]: getNumLeafs(myTree)
Out[83]: 3


接下来,把前面的方法组合在一切,绘制一棵完整的树:

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,va='center',ha='center',rotation=30)

def plotTree(myTree, parentPt, nodeTxt):#if the first key tells you what feat was split on
numLeafs = getNumLeafs(myTree)  #this determines the x width of this tree
depth = getTreeDepth(myTree)
firstStr = myTree.keys()[0]     #the text label for this node should be this
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':#test to see if the nodes are dictonaires, if not they are leaf nodes
plotTree(secondDict[key],cntrPt,str(key))        #recursion
else:   #it's a leaf node print the leaf node
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
#if you do get a dictonary you know it's a tree, and the first element will be another dict

def createPlot(inTree):
fig = plt.figure(1, facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)    #no ticks
#createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses
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()


到此,树的绘制完成,尝试对现有的树来测试该代码:

In [103]: createPlot(myTree)


绘图如下:



3.3 测试和存储分类器

构造使用决策树的分类函数:

def classify(inputTree,featLabels,testVec): #构造使用决策树的分类函数
firstStr = inputTree.keys()[0]
secondDict = inputTree[firstStr]
#将标签字符串转化为索引:
featIndex = featLabels.index(firstStr) #.index函数,用于检索字符串中是否含有firstStr,并返回首次出现的起始位置,这里就是从featLabels中找到根结点特征序号
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


进行测试:

In [131]: myDat,labels = createDataSet()

In [132]: labels
Out[132]: ['no surfacing', 'flippers']

In [133]: myTree = createTree(myDat,labels)

In [134]: myTree
Out[134]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

In [135]: myDat,labels = createDataSet()

In [136]: myTree
Out[136]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

In [137]: labels
Out[137]: ['no surfacing', 'flippers']

In [138]: classify(myTree,labels,[1,0])
Out[138]: 'no'

In [139]: classify(myTree,labels,[1,1])
Out[139]: 'yes'


3.3.2 使用算法:决策树的存储

构造决策树是很耗时的任务,而用创建好的决策树解决分类问题,却可以很快完成。因此,为了节省计算时间,最好能够在每次执行分类时,就调用已经构造好的决策树,为了解决这个问题,需要使用Python模块pickle序列化对象,序列化对象可以在磁盘上保存对象,并在需要的时候读取出来:

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)


进行测试:

In [145]: storeTree(myTree,'classifierStorage.txt')

In [146]: grabTree('classifierStorage.txt')
Out[146]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}


3.4 示例:使用决策树预测隐形眼镜模型

In [152]: lenses = [inst.strip().split('\t') for inst in fr.readlines()]

In [153]: lensesLabels = ['age','prescript','astigmatic','tearRate']

In [154]: lensesTree = createTree(lenses,lensesLabels)

In [155]: lensesTree
Out[155]:
{'tearRate': {'normal': {'astigmatic': {'no': {'age': {'pre': 'soft',
'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}},
'young': 'soft'}},
'yes': {'prescript': {'hyper': {'age': {'pre': 'no lenses',
'presbyopic': 'no lenses',
'young': 'hard'}},
'myope': 'hard'}}}},
'reduced': 'no lenses'}}


可以看到,上述代码已经生成了决策树,而为了更为直观,使用createplot()画图如下:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息