目录
1、决策树
你是否玩过二十个问题的游戏,游戏的规则很简单:参与游戏的一方在脑海里想某个事物,其他参与者向他提问题,只允许提20个问题,问题的答案也只能用对或错回答。问问题的人通过推断分解,逐步缩小待猜测事物的范围。决策树的工作原理与20个问题类似,用户输人一系列数据,然后给出游戏的答案。
我们经常使用决策树处理分类问题,近来的调查表明决策树也是最经常使用的数据挖掘算法R。它之所以如此流行,一个很重要的原因就是使用者基本上不用了解机器学习算法,也不用深究它是如何工作的。
本文构造的决策树算法能够读取数据集合,构建类似于图下的决策树。
①优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。
②缺点:可能会产生过度匹配问题。
③适用数据类型:数值型和标称型。
④决策树的一般流程:
(1)决策树的一般流程
(2)准备数据;树构造算法只适用于标称型数据,因此数值型数据必须离散化。
(3)分析数据:可以使用任何方法,构造树完成之后我们应该检查图形是否符合预期。
(4)训练算法:构造树的数据结构。
(5)测试算法:使用经验树计算错误率。
(6)使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据
的内在含义。
2、决策树的构造
在构造决策树时,我们需要解决的第一个问题就是,当前数据集上哪个特征在划分数据分类时起决定性作用。为了找到决定性的特征,划分出最好的结果,我们必须评估每个特征。完成测试之后,原始数据集就被划分为几个数据子集。这些数据子集会分布在第一个决策点的所有分支上。如果某个分支下的数据属于同一类型,则当前无需阅读的垃圾邮件已经正确地划分数据分类,无需进一步对数据集进行分割。如果数据子集内的数据不属于同一类型,则需要重复划分数据子集的过程。如何划分数据子集的算法和划分原始数据集的方法相同,直到所有具有相同类型的数据均在一个数据子集内。
下表的数据包含5个海洋动物,特征包括:不浮出水面是否可以生存,以及是否有脚践。我们可以将这些动物分成两类:鱼类和非鱼类。现在我们想要决定依据第一个特征还是第二个特征划分数据。在回答这个问题之前,我们必须采用量化的方法判断如何划分数据。
划分数据集的大原则是:将无序的数据变得更加有序。我们可以使用多种方法划分数据集,但是每种方法都有各自的优缺点。组织杂乱无章数据的一种方法就是使用信息论度量信息,信息论是量化处理信息的分支科学。我们可以在划分数据之前使用信息论量化度量信息的内容。
在划分数据集之前之后信息发生的变化称为信息增益,知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择。
在可以评测哪种数据划分方式是最好的数据划分之前,我们必须学习如何计算信息增益。集合信息的度量方式称为香农嫡或者简称为嫡,这个名字来源于信息论之父克劳德·香农。
为了计算嫡,我们需要计算所有类别所有可能值包含的信息期望值,通过下面的公式得到:
1、计算给定数据集的熵
先利用createDataSet()函数得到简单鱼的鉴定数据集。
#创建数据
def createDataSet():
dataSet = [[1,1,1],[1,1,1],[1,0,0],[0,1,0],[0,1,0]]#最后一列数据1代表为鱼类,0代表非鱼类
labels = ['no surfacing','flippers']
return dataSet,labels
首先,计算数据集中实例的总数。我们也可以在需要时再计算这个值,但是由于代码中多次用到这个值,为了提高代码效率,我们显式地声明一个变量保存实例总数。然后,创建一个数据字典,它的键值是最后一列的数值。如果当前键值不存在,则扩展字典并将当前键值加人字典。每个键值都记录了当前类别出现的次数。最后,使用所有类标签的发生频率计算类别出现的概率。我们将用这个概率计算香农嫡,统计所有类标签发生的次数。
#计算给定数据的香农熵
def calShannonEnt(dataSet):
numEntries = len(dataSet)#返回数据集的行数
labelCounts = {}#保存每个标签(Label)出现次数的字典
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#选择该标签(Label)的概率
shannonEnt = shannonEnt - prob*log(prob,2)#香农熵
return shannonEnt
2、划分数据集
我们学习了如何度量数据集的无序程度,分类算法除了需要测量信息嫡,还需要划分数据集,度量花费数据集的嫡,以便判断当前是否正确地划分了数据集。我们将对每个特征划分数据集的结果计算一次信息嫡,然后判断按照哪个特征划分数据集是最好的划分方式。
#划分数据集
#我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照那个特征划分数据集是最好的划分方式
def splitDataSet(dataSet,axis,value):
retDataSet = []#创建返回的数据集列表
for featVec in dataSet:
if featVec[axis] ==value:
reduceFeatVec = featVec[:axis] #去掉axis特征
reduceFeatVec.extend(featVec[axis+1:])#将符合条件的添加到返回的数据集
retDataSet.append(reduceFeatVec)
return retDataSet#返回划分后的数据集
我们输出检查一下:
dataSet,labels = createDataSet()#创建数据
ans1 = splitDataSet(dataSet,0,1)
print(ans1)
可以得到输出的结果为:
在开始划分数据集之前,我们需要计算整个数据集的原始香农嫡,我们保存最初的无序度量值,用于与划分完之后的数据集计算的嫡值进行比较。第1个for循环遍历数据集中的所有特征。使用列表推导(List Comprehension)来创建新的列表,将数据集中所有第i个特征值或者所有可能存在的值写入这个新list中。然后使用Python语言原生的集合(set)数据类型。集合数据类型与列表类型相似,不同之处仅在于集合类型中的每个值互不相同。从列表中创建集合是Python语言得到列表中唯一元素值的最快方法。
遍历当前特征中的所有唯一属性值,对每个特征划分一次数据集,然后计算数据集的新嫡值,并对所有唯一特征值得到的嫡求和。信息增益是嫡的减少或者是数据无序度的减少,大家肯定对于将嫡用于度量数据无序度的减少更容易理解。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。
#选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
numFeature = len(dataSet[0])-1#特征数量,-1是因为最后一个是类别
baseEntopy = calShannonEnt(dataSet)#计算数据集的香农熵
bestInfoGain = 0.0#信息增益
bestFeature = -1 #最优特征的索引值
for i in range(numFeature):
#获取dataSet的第i个所有特征
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) #创建set集合{},元素不可重复
newEntropy = 0.0
#计算每种划分方式的信息熵
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value)#subDataSet划分后的子集
prob = len(subDataSet)/float(len(dataSet)) #计算子集的概率
newEntropy += prob*calShannonEnt(subDataSet)
InfoGain = baseEntopy -newEntropy #信息增益
if (bestInfoGain<InfoGain):
bestInfoGain = InfoGain
bestFeature = i
return bestFeature
输出检查一下:
dataSet,labels = createDataSet()#创建数据
# ans1 = splitDataSet(dataSet,0,1)
bestFeature = chooseBestFeatureToSplit(dataSet)
print(bestFeature)
得到的输出结果为:
代码运行结果告诉我们,第0个特征是最好的用于划分数据集的特征。结果是否正确呢?这个结果又有什么实际意义呢?数据集中的数据来源于表3-1,如果我们按照第一个特征属性划分数据,也就是说第一个特征是1的放在一个组,第一个特征是0的放在另一个组,数据一致性如何?按照上述的方法划分数据集,第一个特征为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.items(),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 valus in uniqueVals: #遍历特征,创建决策树。
subLabels = labels[:] #复制类标签
myTree[bestFeatLabel][valus] = createTree(splitDataSet(dataSet,bestFeat,valus),subLabels)
return myTree
dataSet,labels = createDataSet()#创建数据
myTree = createTree(dataSet,labels)
print(myTree)
构造的树为:
至此,我们的决策树已经构造完成了,接下来讲的是怎么样将决策树进行可视化处理。
3、决策树的可视化
本节我们将使用Matplotlib库创建树形图。决策树的主要优点就是直观易于理解,如果不能将其直观地显示出来,就无法发挥其优势。Python并没有提供绘制树的工具,因此我们必须自己绘制树形图。可视化这里我就不做过多的讲解了,大家可以直接看代码部分。
import matplotlib.pyplot as plt
import operator
#定义文本框和箭头格式
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(inTree):
fig = plt.figure(1,facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) #no ticks
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()
#获取叶节点的数目
def getNumLeafs(myTree):
numLeafs = 0
firstStr = list(myTree.keys())[0] #python3中myTree.keys()返回的是dict_keys,不是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
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 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):
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,nodeTree):
#计算宽与高
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,nodeTree)
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
Tree = retrieveTree(0)
createPlot(Tree)
我们建造出来的树形图像为:
4、测试和存储决策树
依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子节点;最后将测试数据定义为叶子节点所属的类型。
#分类函数
def classify(inputTree,featLabel,TestVec):
firstStr = next(iter(inputTree))
secondDict = inputTree[firstStr]
featIndex = featLabel.index(firstStr)
for key in secondDict.keys():
if TestVec[featIndex] == key:
if type(secondDict[key]).__name__=='dict':
classLabel = classify(secondDict[key],featLabel,TestVec)
else :
classLabel = secondDict[key]
return classLabel
测试代码:
dataSet,labels = createDataSet()#创建数据
T = labels[:] #复制labels
myTree = createTree(dataSet,labels)
ans = classify(myTree,T,[1,0])
print(ans)
输出为:
构造决策树是很耗时的任务,即使处理很小的数据集,如前面的样本数据,也要花费几秒的时间,如果数据集很大,将会耗费很多计算时间。然而用创建好的决策树解决分类问题,则可以很快完成。因此,为了节省计算时间,最好能够在每次执行分类时调用已经构造好的决策树。为了解决这个问题,需要使用Python模块pickle序列化对象。序列化对象可以在磁盘上保存对象,并在需要的时候读取出来。任何对象都可以执行序列化操作,字典对象也不例外。
#决策树的存储
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)
完整代码:
import numpy as np
from math import log
import operator
#创建数据
def createDataSet():
dataSet = [[1,1,1],[1,1,1],[1,0,0],[0,1,0],[0,1,0]]#最后一列数据1代表为鱼类,0代表非鱼类
labels = ['no surfacing','flippers']
return dataSet,labels
#计算给定数据的香农熵
def calShannonEnt(dataSet):
numEntries = len(dataSet)#返回数据集的行数
labelCounts = {}#保存每个标签(Label)出现次数的字典
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#选择该标签(Label)的概率
shannonEnt = shannonEnt - prob*log(prob,2)#香农熵
return shannonEnt
#划分数据集
#我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照那个特征划分数据集是最好的划分方式
def splitDataSet(dataSet,axis,value):
retDataSet = []#创建返回的数据集列表
for featVec in dataSet:
if featVec[axis] ==value:
reduceFeatVec = featVec[:axis] #去掉axis特征
reduceFeatVec.extend(featVec[axis+1:])#将符合条件的添加到返回的数据集
retDataSet.append(reduceFeatVec)
return retDataSet#返回划分后的数据集
#选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
numFeature = len(dataSet[0])-1#特征数量,-1是因为最后一个是类别
baseEntopy = calShannonEnt(dataSet)#计算数据集的香农熵
bestInfoGain = 0.0#信息增益
bestFeature = -1 #最优特征的索引值
for i in range(numFeature):
#获取dataSet的第i个所有特征
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) #创建set集合{},元素不可重复
newEntropy = 0.0
#计算每种划分方式的信息熵
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value)#subDataSet划分后的子集
prob = len(subDataSet)/float(len(dataSet)) #计算子集的概率
newEntropy += prob*calShannonEnt(subDataSet)
InfoGain = baseEntopy -newEntropy #信息增益
if (bestInfoGain<InfoGain):
bestInfoGain = InfoGain
bestFeature = i
return bestFeature
#出现次数最多的分类名称
def majorityCnt(classList):
classCount = {}
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote]+=1
sortedClassCount = sorted(classCount.items(),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 valus in uniqueVals: #遍历特征,创建决策树。
subLabels = labels[:] #复制类标签
myTree[bestFeatLabel][valus] = createTree(splitDataSet(dataSet,bestFeat,valus),subLabels)
return myTree
#分类函数
def classify(inputTree,featLabel,TestVec):
firstStr = next(iter(inputTree))
secondDict = inputTree[firstStr]
featIndex = featLabel.index(firstStr)
for key in secondDict.keys():
if TestVec[featIndex] == key:
if type(secondDict[key]).__name__=='dict':
classLabel = classify(secondDict[key],featLabel,TestVec)
else :
classLabel = secondDict[key]
return classLabel
#决策树的存储
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)
dataSet,labels = createDataSet()#创建数据
T = labels[:] #复制labels
myTree = createTree(dataSet,labels)
ans = classify(myTree,T,[1,0])
print(ans)
storeTree(myTree,'store.txt')
AnsTree = grabTree('store.txt')