以机器学习实战决策树为例,实现具体的决策树算法:
1.信息增益的实现
2.划分数据集
3.递归构建决策树
4.使用matplotlib构造决策树
5.测试和存储决策树
6.实例--隐形眼镜类型
1.信息增益的实现
集合D中类别数y,各种类别概率为pk,则集合D的信息熵为Ent(D)
属性a的取值有a1,a2...av,取值为av的样本集合为Dv,则由于属性a划分而引起的集合信息增益为:
具体实现算法如下:
def calcShannonEnt(dataSet):
#计算样本数
numEntries = len(dataSet)
#字典 key 类别 value 该类别的样本数
labelCounts = {}
for featVec in dataSet: #the the number of unique elements and their occurance
#取出最后一个元素获得类别信息
currentLabel = featVec[-1]
#字典中不存在则新增置0,若存在则更新数量+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
输入数据为样本数据,例如:
dataSet = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'],[0, 1, 'no']]
矩阵的行数为样本数目,每一行元素前面为特征属性取值,最后一个为类别信息。
计算结果如下:
2.划分数据集
前面实现了集合D信息熵的计算,当基于某个属性进行特征划分时,计算其信息增益。然后选择使信息增益最大的属性来进行特征划分。
返回样本第axis个特征属性取值为value的样本集合,同时去除掉axis该属性值
def splitDataSet(dataSet, axis, value):
#存放特征划分后的集合
retDataSet = []
for featVec in dataSet:
#每一个样本的第axis个特征属性取值为value,则返回该样本,同时去除掉axis这一列
if featVec[axis] == value:
#取0--axis 和 axis+1---length
reducedFeatVec = featVec[:axis] #chop out axis used for splitting
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
extend用于数组元素的拼接,append用于数组元素的增加.
输入样本集合,取出第0个特征属性值为1的样本集合,测试如下:
处理样本集合,获取所有的特征属性,对于每一种属性,首先获取该属性的所有取值可能,然后计算该属性进行划分引起的集合信息增益,依次计算所有的特征属性,最后返回信息增益最大的特征属性,来作为集合的划分选择。
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
#取出该特征属性所有取值,即矩阵第i列的所有取值set
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
其中featList是矩阵第i列的所有取值构成的list,然后进行set去重,即可得到该特征属性所有可能的取值。
3.递归构建决策树
当对每一个样本集合进行划分时,
1.如果所有集合样本同属于同一类,则应停止划分,返回该类别作为叶子节点类别(矩阵最后一列取值只有一种)
2.如果该样本集合已经没有特征属性,则可以返回类别中最多的类别信息作为该叶子节点类别(矩阵只有最后一列,返回最后一列中取值最多的数值信息)
3.有多个特征属性,则根据特征属性来进行划分,并递归调用。
定义函数处理第二种情况,已经没有特征属性,返回类别中最多的类别信息
def majorityCnt(classList):
#key 类别 value类别次数
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]#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)
#获取该特征属性的标签含义
bestFeatLabel = labels[bestFeat]
#字典存放决策树 key 划分特征属性的标签 value具体的子节点信息
myTree = {bestFeatLabel:{}}
#删除该特征属性标签
del(labels[bestFeat])
#该特征属性所有可能取值set
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
测试如下:
4.matplotlib构造决策树
返回决策树的叶子节点数目,便于进行x坐标的节点绘制以及x坐标偏移。递归遍历二叉树,当某个节点不是字典类型时,叶子节点数加一。
#返回决策树的叶子数目
def getNumLeafs(myTree):
numLeafs = 0
#取出根节点
#python3中dict.keys不支持索引,需要转换成list
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
numLeafs += getNumLeafs(secondDict[key])
else: numLeafs +=1
return numLeafs
计算决策树的高度,便于进行y坐标的绘制,高度即为所有叶子节点的高度的最大值
#返回决策树的深度
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
进行决策树的绘制,坐标范围x[0-1],y[0-1],需要根据决策树的叶子数以及高度决定x和y方向上移动间距。起始坐标位于(0.5,1)处,绘制某一个节点时,首先计算该节点的叶子数以及高度,绘制该节点以及文本信息,然后遍历该节点的子节点信息,如果子节点为地点则递归调用继续绘制,否则说明该节点为叶子节点,则直接进行绘制。每次进行绘制时,都要注意节点坐标的偏移计算。
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 = list(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]
#树高度增加 调整y大小,减少
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
#画叶子结点 x坐标
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))
#画完该节点y坐标上升至父节点高度,以便递归调用产生父节点其他子节点
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))
#x,y坐标的偏移
plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0;
#起始点位于(0.5,1.0)处,即画布的最上方中间处
plotTree(inTree, (0.5,1.0), '')
plt.show()
测试如下:
5.测试,存储决策树
测试决策树,给定决策树,给定测试样本,即特征属性向量,输出该样本的类别信息
def classify(inputTree,featLabels,testVec):
#决策树的第一个划分特征属性标签
firstStr = list(inputTree.keys())[0]
#该节点的叶子结点dict
secondDict = inputTree[firstStr]
#特征属性标签在特征向量中的索引
featIndex = featLabels.index(firstStr)
#测试向量在该特征属性上的值
key = testVec[featIndex]
#测试向量位于哪一颗子树
valueOfFeat = secondDict[key]
#如果该子树为dict,则继续递归查找,否则返回该节点类别信息
if isinstance(valueOfFeat, dict):
classLabel = classify(valueOfFeat, featLabels, testVec)
else: classLabel = valueOfFeat
return classLabel
划分过程类似有B树的查找过程,根据测试样本向量在某特征属性上的取值决定测试样本位于哪一颗子数,当该子树为字典类型,继续递归调用,进行属性值的再次判断,当子树不是字典类型,这说明是叶子节点,节点所放数据即为该测试样本的类别信息,返回即可。
决策树的存储和加载过程,训练好决策树可以存储到硬盘等,这样可以避免每次使用都临时计算构造决策树。
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)
6.实例--隐形眼镜类型判断
给定训练样本数据如下所示,
特征属性向量标签为['age','prescript','astigmatic','tearRate']
类别信息为['no lenses','soft','hard']
读取数据文件,取出特征向量和类别信息
特征向量标签,构建决策树
绘出决策树:
treePlotter.createPlotter(lenseTree)
总结:
本节使用信息增益来作为特征属性的划分选择标准,ID3算法,能够处理离散属性的特征划分,对于连续属性不太友好。没有考虑过拟合的情况。
优点:计算复杂度不高,输出结果便于理解 对于中间值缺失不敏感,可以处理不相关特征数据
缺点:容易过拟合