决策树
同样是完成多分类任务:k-近邻算法无法给出数据的内在含义,决策树的优势在于形式易于理解!
决策树正如他的名字,可以用于决策,例如专家系统。
决策树的重要任务是为了数据中所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,从中提取出一系列规则,提取规则的过程就是机器学习的过程。
- 讨论构造决策树的方法
- 如何编写构造树的Python代码
- 提出一些度量算法成功率的方法
- 使用递归建立分类器
- 使用Matplotlib绘制决策树图
01决策树的构造
划分数据集可以选择很多特征,那么关键问题在于现在使用哪个特征能得到最好的划分结果,因此需要对特征进行评估!
根据该特征进行数据集划分会得到很多分支,如果某个分支下的数据属于同一类型,则已经正确分类,如果不属于同一类型,继续选取此时的最好特征继续划分!
最后:所有具有相同类型的数据均在同一个数据子集内。
对决策树构造过程进行总结:
- 评估最好特征
- 划分分支
- 对分支继续评估最好特征+划分分支
什么时候结束:相同类型数据在同一个子集中
我们采用ID3算法划分数据集,如果依据某个属性可能会产生4个可能的值,我们就把数据划分成4块,并创建4个不同的分支,每次划分数据集我们只选用1个特征属性。现在我们该考虑的问题就是:我们首先选择哪个特征去划分数据集呢?也就是如何评估最好特征?
01.1信息增益——评估最好特征的依据
划分数据集的最大原则:将无序数据变得更加有序。
在划分数据之前之后信息发生的变化称为信息增益,那么信息增益怎么用呢?
信息增益的计算:
熵entropy:信息的期望值(其实就是类似均值)
符号xi的信息定义为:l(xi) = -log2p(xi)
上式中p(xi)是选择该分类的概率
所有类别所有可能值包含的信息期望值:
n是指按照这个特征分类产生的类数目。
计算熵选取特征
- 计算不同特征带来的信息增益
- 选择信息增益最高的特征值
#计算给定数据集的香农熵
def calcShannonEnt(dataSet):
numEntries = len(dataSet)
labelCounts = {}
for featVec in dataSet: #the the number of unique elements and their occurance
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) #log base 2
return shannonEnt
对于以上海洋生物数据,我们可以将数据构造成数据集和标签。再利用该数据验证calcShannonEnt()的准确性。
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
#用来本地测试
if __name__ == "__main__":
myDat, lable = createDataSet()
print(myDat)
print(calcShannonEnt(myDat))
如果我们给数据集添加更多的分类,熵会增加,因为混乱度增加了!
例如,再本地测试时增加这行代码:myDat[0][-1] = 'maybe'
分类除了yes和no之外,又多了maybe,混乱都增加,熵也从原来的0.9709505944546686变成了1.3709505944546687
0.1.2 划分数据集
entropy可以度量数据集的无序程度,我们将对每个特征划分数据集的结果计算一次信息熵,然后判断哪个特征划分数据集是最好的!
#按照给定特征划分数据集
def splitDataSet(dataSet, axis, value):
retDataSet = [] #创建新的list对象
for featVec in dataSet:
if featVec[axis] == value:
#将符合特征的数据抽取出来加入retDataSet
reducedFeatVec = featVec[:axis] #chop out axis used for splitting
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
这个划分数据集的函数使用了3个参数:
- 待划分的数据集dataSet
- 划分数据集的特征axis
- 需要返回的特征的值value
value的含义是:dataSet中某条数据的axis位置的值等于value时,就将这条数据加入某一个分支。
需要注意的是,Python语言不用考虑内存分配问题。Python语言在函数中传递的是列表的引用,在函数内部对列表对象的修改,将会影响该列表对象的整个生存周期。为了消除这个不良影响,我们需要在函数的开始声明一个新列表对象。
接下来,如何选择最好的数据集划分方式呢?
# 选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特征数,最后一列是标签
baseEntropy = calcShannonEnt(dataSet) #计算初始熵,为计算信息增益做准备
bestInfoGain = 0.0; bestFeature = -1 #初始信息增益记为0,最佳特征暂时是-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 #更新最大的信息增益
bestFeature = i
return bestFeature #返回整数:最佳特征的列号
0.1.3 递归构建决策树
工作原理:得到原始数据集,然后基于最好的属性划分数据集,由于特征值可能多于2个,因此可能存在大于两个分支节点的数据集划分。第一次划分之后,数据将被传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据,因此我们可以采取递归的原则处理数据集。
递归出口:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类
如果数据集已经处理了所有的属性,但是类标签依然不是唯一的,此时我们需要决定如何定义该叶子节点,在这种情况下,我们会采用多数表决的方法决定该叶子节点的分类。
# 多数表决
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[:] #copy all of labels, so trees don't mess up existing labels
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
return myTree
0.2 在Python中使用Matplotlib注解绘制树形图
0.1节,我们学习了从数据集中创建树,然而字典的形式非常不易于理解,直接绘图也很难,本节我们使用Matplotlib库创建树形图——直观且易于理解
0.2.1Matplotlib注解+绘制树节点
提供注解工具类:annotations,可以在数据图形上添加文本注释,并且可以指定注释的位置。如图所示,在坐标(0.2, 0.1)的位置有一个点,我们将对该点的描述信息放在(0.35,0.3)的位置,并用箭头指向数据点(0.2, 0.1):
使用Matplotlib的注解功能绘制树形图,它可以对文字着色并提供多种形状以供选择,而且我们还可以反转箭头, 将它指向文本框而不是数据点。
# 使用文本注解绘制树节点
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 )
def createPlot():
fig = plt.figure(1, facecolor='white')
fig.clf()
createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses
plotNode('a decision node', (0.5, 0.1), (0.1, 0.5), decisionNode)
plotNode('a leaf node', (0.8, 0.1), (0.3, 0.8), leafNode)
plt.show()
0.2.2 构造注解树
我们有x和y坐标,但是如何放置树节点是个问题,我们需要知道有多少叶节点以确定x轴的长度;我们还需要知道树有多少层,以便确定y轴的高度。因此,我们构造两个新函数getNumLeafs()和getTreeDepth(),来获取叶节点的数目和树的层数。
def getNumLeafs(myTree):
numLeafs = 0
firstStr = list(myTree.keys())[0] #请注意,python3在这里需要转list类型
secondDict = myTree[firstStr]
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
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':#test to see if the nodes are dictonaires, if not they are leaf nodes
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]
我们在用书中代码运行时,会报错
TypeError: 'dict_keys' object is not subscriptable
遇到这个问题,其实是python3中keys不允许切片,先转list再切片就好了!
0.3 测试和存储分类器
我们学习机器学习是为了能够获得预测能力。前面学的这些东西只是解决了如何构建决策树+如何使用python函数库绘制树形图。为了了解数据的真实含义,下面将重点转移到如何利用决策树执行数据分类上。
本节内容:
- 使用决策树构建分类器
- 实际应用中如何存储分类器。
0.3.1 使用分类器执行分类
依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子节点;最后将测试数据定义为叶子节点所属的类型。
# 执行分类
def classify(inputTree,featLabels,testVec):
firstStr = list(inputTree.keys())[0] #转list类型
secondDict = inputTree[firstStr]
featIndex = featLabels.index(firstStr)
key = testVec[featIndex]
valueOfFeat = secondDict[key]
if isinstance(valueOfFeat, dict):
classLabel = classify(valueOfFeat, featLabels, testVec)
else: classLabel = valueOfFeat
return classLabel
第一节点名为no surfacing,它有两个子节点:一个是名字为0的叶子节点,类标签为no;另一个是名为flippers的判断节点,此处进入递归调用,flippers节点有两个子节点。以前绘制的树形图和此处代表树的数据结构完全相同。
0.3.2 使用算法:决策树的存储
决策树构造好后,需要存储,避免每次使用都重新构造,如何存储?
使用python模块pickle序列化对象,序列化对象可以在磁盘上保存对象,并在需要的时候读取出来,任何对象都可以执行序列化操作,包括字典对象。
def storeTree(inputTree, filename):
import pickle
fw = open(filename, 'wb') #TypeError: write() argument must be str, not bytes 改成wb能否行?
pickle.dump(inputTree, fw)
fw.close()
def grabTree(filename):
import pickle
fr = open(filename,'rb') #改成"rb",即可正常运行
return pickle.load(fr)
使用pycharm,一开始可能存在进制错误。类似这个 python提示错误TypeError: write() argument must be str, not bytes
当我们将写入模式更改为二进制之后,那么读取也应该以二进制格式读取!
这时候咱们可以补充补充python文件相关知识。Python3 输入和输出
这样,我们可以将分类器存储在硬盘上,而不用每次对数据分类时重新学习一遍,这也是决策树的优点之一!
0.4 示例:使用决策树预测隐形眼镜类型
使用小数据集,我们就可以来利用决策树学到很多知识:眼科医生如何判断患者需要佩戴的镜片类型。一旦了解了决策树的工作原理,我们甚至也可以帮助人们判断需要佩戴的镜片类型。
步骤如下:
- 收集数据:提供文本文件
- 准备数据:解析tab键分隔的数据行
- 分析数据:快速检查数据,确保正确解析数据内容,使用creatPlot()函数绘制最终的树形图
- 训练算法:使用createTree()函数
- 测试算法:编写测试函数验证决策树可以正确分类给定的数据实例
- 使用算法:存储树的数据结构,以便下次使用树时无需重新构造树
隐形眼镜包括硬材质、软材质以及不适合佩戴隐形眼镜。数据存储在lenses.txt中,最后一行调用creatPlot()函数绘制了树形图。
if __name__ == "__main__":
fr = open("lenses.txt")
lenses = [inst.strip().split("\t") for inst in fr.readlines()]
lensesLables = ['age', 'prescript', 'astigmatic', 'tearRate']
lensesTree = createTree(lenses,lensesLables)
print(lensesTree)
treePlotter.createPlot(lensesTree)
决策树非常好的匹配了实验数据,但是可能这些匹配选项太多了,我们将这个问题称为overfitting!为了减少过度匹配,我们可以裁剪掉一些不必要的叶子节点,如果叶子节点只能增加少量信息,则可以删除这些节点,并将它加入到其他叶子节点中!
决策树算法总结
决策树分类器就像流程图一样,终止块表示分类结果,开始处理数据集时,我们首先需要测量集合中数据的不一致,也就是熵(计算不同分类的信息期望),然后寻找最优方案划分数据集(需要信息增益最高),经过多次递归划分数据集,直到所有数据属于同一分类。ID3算法可以用于划分标称型数据集。一般我们并不构造新的数据结构,而是使用Python语言内嵌的数据结构字典存储树节点信息。
使用Matplotlib的注解功能,我们可以将存储的树结构转化为容易理解的图形。Python语言的pickle模块可用于存储决策树的结构。隐形眼镜的例子表明决策树可能会产生过多的数据集划分,从而产生过度匹配数据集的问题。我们可以通过裁剪决策树,合并相邻的无法产生大量信息增益的叶节点,消除过度匹配问题。
更精简总结:
- 计算熵(entropy)
- 计算不同特征划分数据集带来的信息增肌(information gain)
- 划分处数据子集后,递归前两步操作
- 递归边界:没有更多的特征可以使用 / 子集中的数据标签已经一样
- 划分数据集,构造决策树,图形化决策树
- 存储决策树使用pickle模块
- 决策树存在overfitting问题待解决