决策树
优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。
缺点:可能会产生过度匹配问题。
适用数据类型:数值型和标称型。
1.1 决策树的构造
创建分支的伪代码函数CreateBranch():
检测数据集中的每个子项是否属于同一分类:
If so return 类标签
Else
寻找划分数据集的最好特征
划分数据集
创建分支节点
for 每个划分的子集
调用函数createBranch并增加返回结果到分支节点中
return 分支节点
#第一个问题:当前数据集哪个特征在划分数据集时起决定性作用
决策树的一般流程
(1)收集数据:可以使用任何方法
(2)准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化
(3)分析数据:可以使用任何方法,构造树完成后,我们应该检查图形是否符合预期
(4)训练算法:构造树的数据结构
(5)测试算法:使用经验树计算错误率
(6)使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义
1.2 信息论
划分数据集的原则:将无序的数据变得更加有序
划分前后信息发生的变化称为信息增益,熵定义为信息的期望值
# 计算给定数据的香农熵
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
熵越高,则混合的数据越多。
还有一种度量集合无序程度的方法是基尼不纯度(Gini impurity)
从一个数据集中随机选取子项,度量其被错误分类到其他分组里的概率
1.3 划分数据集
将每个特征划分数据集的结果计算一次信息熵,然后判断按照哪一个特征划分数据集是最好的划分方式
# 按照给定特征划分数据集
def splitDataSet(dataSet, axis, value):
# 创建一个新的列表对象
retDataSet = []
# 数据集这个列表中的各个元素也是列表,遍历数据集中的每个元素
# 发现符合要求的值,就将其添加到新建列表中
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] #chop out axis used for splitting
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
# 选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #the last column is used for the labels
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain = 0.0; bestFeature = -1
for i in range(numFeatures): #iterate over all the features
featList = [example[i] for example in dataSet]#create a list of all the examples of this feature
uniqueVals = set(featList) #get a set of unique values
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 #calculate the info gain; ie reduction in entropy
if (infoGain > bestInfoGain): #compare this to the best gain so far
bestInfoGain = infoGain #if better than current best, set to best
bestFeature = i
return bestFeature #returns an integer
注意:先前在上一个程序中把prob打印出来,导致运行这个划分数据集的时候一直报错,不知道什么原因……
遍历当前特征中的所有唯一属性值,对每个唯一属性值互粉一次数据集,然后计算数据集的新熵值,并对所有唯一特征值得到的熵求和。
信息增益是熵的减少或者是数据无序度的减少,最后比较所有特征中的信息增益,返回最好特征划分的索引值。
1.4 递归构建决策树
第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。
因此,可以采用递归的原则进行处理数据。
递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。
# 创建树的函数代码
def createTree(dataSet,labels):
classList = [example[-1] for example in dataSet]
if classList.count(classList[0]) == len(classList):
return classList[0]#stop splitting when all of the classes are equal
if len(dataSet[0]) == 1: #stop splitting when there are no more features in dataSet
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet)
print(bestFeat)
bestFeatLabel = labels[bestFeat]
#print('bestFeatLabel')
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
1.5 在Python中使用Matplotlib注解绘制树形图
绘制一颗完整的树需要一些技巧,必须知道有多少个叶节点,以便正确确定x轴的长度;知道树有多少层,以便确定y轴的高度
# 使用文本注解绘制树节点
import matplotlib.pyplot as plt
# boxstyle 是文本框的类型,sawtooth是锯齿形/波浪线型,fc是边框粗细
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
# 上面定义的字典decisionNode和leafNode都是作为参数传递给函数nodeType,进而传递给内置函数annotate()的bbox参数,决定了注释文本框的特征
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 )
其中参数xy 表示标注位置
xytext表示文本位置
xycoords,textcoords表示选择的坐标系,用来指定xy及xytext的坐标系 ,默认是data(使用轴域数据坐标系)
axes fraction 表示(0,0)在轴的左下角,(1,1)为右上角
arrowprops 是提供能箭头属性字典,用来绘制文本到注释点的箭头,这里在上面的字典有定义
va和ha表示 标注的文本框相对于整个坐标轴水平或者垂直方向的位置
bbox在程序里有注释
(附:Matplotlib中文用户指南标注部分
https://blog.csdn.net/wizardforcel/article/details/54782628)
# 获取叶节点的数目和树的层数
def getNumLeafs(myTree):
numLeafs = 0
firstStr = 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
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':#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 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()
最后绘制出来的图形:
1.6 测试和存储分类器
(1)使用决策树的分类函数,使用决策树执行分类
def classify(inputTree,featLabels,testVec):
firstStr = inputTree.keys()[0]
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
(2)使用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)
可以将分类器存储在硬盘上,而不用每次对数据分类时重新学习一遍。
通过预先提炼并存储数据集中包含的知识信息,在需要对事物进行分类时再使用这些知识。
1.7 示例:使用决策树预测隐形眼镜类型
使用决策树如何预测患者需要佩戴的隐形眼镜类型
(1)收集数据:提供的文本文件
(2)准备数据:解析Tab键分隔的数据行
(3)分析数据:快速检查数据,确保正确地解析数据内容
使用createPlot()函数绘制最终的树形图
(4)训练算法:createTree()函数
(5)测试算法:编写测试函数验证决策树可以正确分类给定的数据实例
(6)使用算法:存储树的数据结构,以便下次使用时无需重新构造树
隐形眼镜数据集:
沿着决策树的不同分支,我们可以得到不同患者需要佩戴的隐形眼镜类型
医生最多需要问四个问题就能确定患者需要佩戴哪种类型的隐形眼镜
匹配选项如果太多,这个问题称之为过度匹配
下面这个是关于ID3算法的一些介绍:
https://blog.csdn.net/fly_time2012/article/details/70210725
ID3无法直接处理数值型数据,这里我们是通过量化的方法将数值型数据转化为标称型数值,但如果特征划分太多,也会出现一定的问题。