目录
3.3在 Python 中使用 Matplotlib 注解绘制树形图
一、决策树介绍
1.1什么是决策树
决策树(decision tree)是一种基本的分类与回归方法。决策树模型呈树形结构,在分类问题中,表示基于特征对实例进行分类的过程。它可以认为是if-then规则的集合,也可以认为是定义在特征空间与类空间上的条件概率分布。
分类决策树模型是一种描述对实例进行分类的树形结构。决策树由结点和有向边组成。结点有两种类型:内部结点和叶结点。内部结点表示一个特征或属性,叶结点表示一个类。
1.2决策树实例
根据以上对话我们可以画出一棵决策树:
决策过程中提出的每个判定问题都是对某个属性的“测试”
例如年龄=?、长相=?、收入=?、是否是公务员
每个测试的结果或是导出最终结论,或者导出进一步的判定问题,其考察范围是在上次决策结果的限定范围之内。
例如 年龄<=30,之后再判断长相,则仅考虑年龄小于30的对象
从根结点到每个叶结点的路径对应了一个判定测试序列
例如 年龄小于30,长相帅,收入高的路径,最终判定结果为见
决策树学习的目的是为了产生一棵泛化能力强,即处理未见示例能力强的决策树。
1.3决策树的优缺点
优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。
缺点:可能会产生过度匹配问题。
适用数据类型:数值型和标称型。
二、划分选择
决策树学习的关键是如何选择最优划分属性。一般而言,随着划分过程不断进行,我们希望决策树的分支结点所包含的样本尽可能属于同一类别,即结点的“纯度”(puruty)越来越高。
2.1信息增益
“信息熵”是度量样本集合纯度最常用的一种指标,假定当前样本集合D中第k类样本所占的比例为(K=1,2,……,|y|),则D的信息熵定义为
Ent(D)的值越小,则D的纯度越高
计算信息熵时约定:若p=0,则plog2p=0
Ent(D)的最小值为0,最大值为log2|y|
离散属性a有V个可能的取值{ ,
, ...,
},用a来进行划分,则会产生V个分支结点,其中第v个分支结点包含了D中所有在属性a上取值为
的样本,记为
。则可计算出用属性a对样本集D进行划分所获得的信息增益:
再考虑到不同的分支结点所包含的样本数不同,给分支结点赋予权重,样本数越多的分支结点的影响越大
一般而言,信息增益越大,则意味着使用属性a来进行划分所获得的“纯度提升”越大
说了这么多可能还是有点难理解信息增益是什么,怎么用,下面我们用个例子来说明一下
这是西瓜书上的例子 ,我们根据一个西瓜的色泽、根蒂、敲声、纹理、触感等来判断它是不是一个好瓜。
该数据集包含17个训练样本,结果有两种,一是好瓜,二是坏瓜,所以|y|=2,其中正例(好瓜)占 ,反例(坏瓜)占
,可以算出根结点的信息熵为
以属性“色泽”为例其对应的3个数据子集分别为(色泽= 青绿),
(色泽=乌黑),
(色泽=浅白)
子集包含编号为{1, 4, 6, 10, 13, 17} 的6个样例,其中正例占
,反例占
,
的信息熵为:
同理、
的信息熵为:
那么,属性“色泽”的信息增益为
同理,我们可以计算出其他属性的信息增益
Gain(D,根蒂) = 0.143
Gain(D,敲声) = 0.141
Gain(D,纹理) = 0.381
Gain(D,脐部) = 0.289
Gain(D,触感) = 0.006
根据计算结果,我们可以看出属性“纹理”的信息增益最大,其被选为划分属性
继续对每个分支进行划分,计算出每个结点下各属性的信息增益,选择最大信息增益作为下次划分属性,最终得到的决策树如图:
2.2增益率
采用信息增益来进行划分属性的决策存在问题,当某一个属性的取值种类非常多是,对应每一个属性取值的样本子集,其分类的信息熵可能会变得非常小。例如,将“编号”作为一个候选划分属性,则“编号”的信息熵:
显然,这样的决策树不具有泛化能力,无法对新样本进行有效预测。
为了减少这种偏好带来的不利影响,著名的C4.5决策树算法不直接使用信息增益,而使用增益率来选择最优划分属性。
信息增益率定义为:
其中
称为属性a的“固有值”(intrinsic value) [Quinlan, 1993]. 属性a的可能取值数目越多(即V越大),则IV(a) 的值通常会越大。
计算上述数据的信息增益率:
a1表示触感,a2表示色泽,a3表示编号
增益率准则对可取值数目较少的属性有所偏好
C4.5采用了一个启发式方法:先从候选划分属性中找出信息增益高于平均水平的属性,再从中选取增益率最高的
2.3基尼指数
分类问题中,假设D有K个类,样本点属于第k类的概率为,则概率分布的基尼值定义为:
Gini反映了随机抽取两个样本,其类别标记不一致的概率
Gini(D)越小,数据集D的纯度越高
给定数据集D,属性a的基尼指数定义为:
同样使用上述数据集,计算属性色泽的基尼指数
色泽 | 好瓜数量 | 坏瓜数量 |
青绿 | 3 | 3 |
乌黑 | 4 | 2 |
浅白 | 1 | 1 |
v1青绿,v2乌黑,v3浅白
三、代码实现
以福建省选调生报考条件为例,选择其中三项:担任过一年以上学生干部,品学兼优,党员优先 判断学生能否得到学院的推荐名额,数据集如下:
学习成绩分为优秀(outstanding)和良好(good),结果有推荐(recommend)、犹豫(vacillate)、拒绝(refuse)
3.1以信息增益划分属性/ID3
数据加载
#数据加载
def loadData( ):
data = pd.read_csv("STUdata3.csv")
dataset = data.values.tolist()
#四个属性
labels=['担任班干一年以上','学习成绩','是否党员','result']
return dataset,labels
计算给定数据的香农熵
#计算给定数据的香农熵
def calShannonEnt(dataset):
numEntries = len(dataset) #获得数据集函数
labelCounts={} #用于保存每个标签出现的次数
for data in dataset:
#提取标签信息
classlabel = data[-1]
if(classlabel not in labelCounts.keys()): #如果标签未放入统计次数的字典,则添加进去
labelCounts[classlabel]=0
labelCounts[classlabel]+=1 #标签计数
shannonEnt=0.0 #熵初始化
for key in labelCounts:
p = float(labelCounts[key])/numEntries #选择该标签的概率
shannonEnt-= p*np.log2(p)
return shannonEnt #返回经验熵
根据某一特征划分数据集
#根据某一特征划分数据集
def splitDataset(dataset,axis,value):
# dataset 待划分的数据集 axis 划分数据集的特征 value 返回数据属性值为value
retDataSet = [] #创建新的list对象
for featVec in dataset: #遍历元素
if featVec[axis]==value: #符合条件的抽取出来
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
选择最佳属性划分数据集
#选择最佳属性划分数据集
def chooseBestFeatureToSplit(dataset):
numFeatures = len(dataset[0])-1 #计算特征数
baseEntropy = calShannonEnt(dataset)#计算信息熵
bestFeature = -1 #最优特征的索引值
bestInfoGain = 0 #信息增益
for i in range(numFeatures): #不断循环属性
featList = [example[i] for example in dataset] #获取数据集的第i个特征
uniqueVals = set(featList) #属性i的属性值有哪些
newEntropy = 0.0
for value in uniqueVals:
subDataSet = splitDataset(dataset,i,value) #按照属性i和属性i的值value进行数据划分
prob = len(subDataSet)/float(len(dataset))
newEntropy +=prob*calShannonEnt(subDataSet) #计算划分过数据集的信息熵
infoGain = baseEntropy-newEntropy #计算信息增益,也就是信息熵的变换量
#print("第%d个特征的信息增益为:%.3f" % (i, infoGain))
if(infoGain>bestInfoGain):
bestInfoGain = infoGain
bestFeature=i
#print("最优索引值:"+str(bestFeature))
return bestFeature #返回信息增益最大特征的索引值
# 统计出现次数最多的元素(类标签)
def majorityCnt(classList):
classCount={} #统计classList中每个类标签出现的次数
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):#条件1:classList只剩下一种值
return classList[0]
if len(dataset[0])==1:#条件2:数据dataset中属性已使用完毕,但没有分配完毕
return majorityCnt(classList)#取数量多的作为分类
bestFeat = chooseBestFeatureToSplit(dataset)#选择最好的分类点,即香农熵值最小的
labels2 = labels.copy()#复制一分labels值,防止原数据被修改。
bestFeatLabel = labels2[bestFeat]
myTree = {bestFeatLabel:{}}#选取获取的最好的属性作为
del(labels2[bestFeat])
featValues = [example[bestFeat] for example in dataset]#获取该属性下的几类值
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels2[:]#剩余属性列表
myTree[bestFeatLabel][value] = createTree(splitDataset(dataset,bestFeat,value),subLabels)
return myTree
添加主函数
if __name__ == '__main__':
dataSet, labels = loadData( )
print("数据集信息熵:"+str(calShannonEnt(dataSet)))
mytree = createTree(dataSet,labels)
print(mytree)
运行结果
运行代码时发现树的分支不能过多,过多会报错list index out of range,这个问题暂时还没有解决
小结
ID3优点:理论清晰,方法简单,学习能力较强
缺点:
- 信息增益的计算比较依赖于数目比较多的特征
- ID3为非递增算法
- ID3为单变量决策树
- 抗糙性差
3.2以信息增益率划分属性/C4.5
只需要修改选择最佳属性划分数据集 的代码即可
# 选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特征数量
baseEntropy = calcShannonEnt(dataSet) #计数数据集的香农熵
bestInfoGain = 0.0 #信息增益
bestFeature = -1 #最优特征的索引值
for i in range(numFeatures): #遍历数据集的所有特征
featList = [example[i] for example in dataSet] #获取dataSet的第i个所有特征
uniqueVals = set(featList) #创建set集合{},元素不可重复
newEntropy = 0.0
splitInfo = 0.0 #信息熵
for value in uniqueVals: #循环特征的值
subDataSet = splitDataSet(dataSet, i, value) #subDataSet划分后的子集
prob = len(subDataSet) / float(len(dataSet)) #计算子集的概率
newEntropy += prob * calcShannonEnt((subDataSet)) #计算划分过数据集的信息熵
splitInfo -= prob * np.log2(prob)
infoGain = (baseEntropy - newEntropy)/splitInfo #求出第i列属性的信息增益率
#print("第%d个特征的信息增益为%.3f" % (i, infoGain)) #打印每个特征的信息增益
if (infoGain > bestInfoGain): #计算信息增益
bestInfoGain = infoGain #更新信息增益,找到最大的信息增益
bestFeature = i #记录信息增益最大的特征的索引值
return bestFeature #返回信息增益最大特征的索引值
运行结果
小结
C4.5算法与ID3算法的区别是主要有4点改进
- 采用信息增益率作为最优划分属性
- 能够处理连续值类型的属性
- 能够处理缺失值属性
- 增加了剪纸处理,从而避免过拟合
3.3在 Python 中使用 Matplotlib 注解绘制树形图
import matplotlib
import matplotlib.pyplot as plt
# 定义文本框和箭头格式
decisionNode = dict(boxstyle="square", fc="0.8") #boxstyle文本框样式、fc=”0.8” 是颜色深度
leafNode = dict(boxstyle="round4", fc="0.8") #叶子节点
arrow_args = dict(arrowstyle="<-") #定义箭头
# 绘制带箭头的注解
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
#createPlot.ax1是表示: ax1是函数createPlot的一个属性
createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',xytext=centerPt,
textcoords='axes fraction',va="center", ha="center", bbox=nodeType, arrowprops=arrow_args)
# 获取叶节点的数目和树的层数
def getNumLeafs(myTree):
numLeafs = 0 # 初始化
firstStr = list(myTree.keys())[0] # 获得第一个key值(根节点)
secondDict = myTree[firstStr] # 获得value值
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] # 获得第一个key值(根节点)
secondDict = myTree[firstStr] # 获得value值
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': 'refuse', 'yes': {'学习成绩': {'outstanding': {'是否党员': {'no': 'vacillate', 'yes': 'recommend'}}, 'good': {'是否党员': {'no': 'refuse', 'yes': 'vacillate'}}}}}}]
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, va="center", ha="center", rotation=30)
# 画树
def plotTree(myTree, parentPt, nodeTxt):
numLeafs = getNumLeafs(myTree) # 获取树高
depth = getTreeDepth(myTree) # 获取树深度
firstStr = list(myTree.keys())[0] # 这个节点的文本标签
cntrPt = (plotTree.xOff + (1.0 + float(numLeafs)) / 2.0 / plotTree.totalW, plotTree.yOff) #plotTree.totalW, plotTree.yOff全局变量,追踪已经绘制的节点,以及放置下一个节点的恰当位置
plotMidText(cntrPt, parentPt, nodeTxt) #标记子节点属性
plotNode(firstStr, cntrPt, parentPt, decisionNode)
secondDict = myTree[firstStr]
plotTree.yOff = plotTree.yOff - 1.0 / plotTree.totalD #减少y偏移
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
# 绘制决策树
def createPlot(inTree):
fig = plt.figure(1, facecolor='white') # 创建一个新图形
fig.clf() # 清空绘图区
font = {'family': 'Songti SC'}
matplotlib.rc("font", **font)
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
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()
运行结果
完整代码
链接: https://pan.baidu.com/s/1HvKRDVmlV0J7Q87DIPDrpA?pwd=qfyr 提取码: qfyr