机器学习 —— 决策树
一、决策树的基本概念
目录
三、在 Python 中使用 Matplotlib 注解绘制树形图
一、决策树的概述
1.1 决策树的概念
决策树(decision tree)是一种基本的分类与回归方法。决策树模型呈树形结构,在分类问题中,表示基于特征对实例进行分类的过程。它可以认为是if-then规则的集合,也可以认为是定义在特征空间与类空间上的条件概率分布。
决策树是一种描述对实例进行分类的树形结构,其中每个内部节点表示一个属性上的判断,每个分支代表一个判断结果的输出,最后每个叶节点代表一种分类结果,本质是一颗由多个判断节点组成的树。分类决策树模型是一种树形结构。 决策树由结点和有向边组成。结点有两种类型:内部结点和叶节点。内部结点表示一个特征或属性,叶节点表示一个类。
1.2 决策树分类举例
机器学习上两篇博客是讲k-近邻算法的分类任务,但是其最大的缺点是无法给出数据的内在含义,决策树的优势在于数据形式非常容易理解。
决策树分类的思想类似于我们找对象,嘿嘿(*^▽^*)。现想象一个女孩的母亲要给这个女孩介绍男朋友,于是有了下面的对话:
女儿:多大年纪了? (年龄)
母亲:26。
女儿:长的帅不帅? (长相)
母亲:挺帅的。
女儿:收入高不? (收入情况)
母亲:不算很高,中等情况。
女儿:是公务员不? (是否公务员)
母亲:是,在税务局上班呢。
女儿:那好,我去见见。
根据上面的对话,如下画出找对象的决策树,可以看到其中每个内部节点表示一个属性上的判断,每个分支代表一个判断结果的输出,最后每个叶节点代表一种分类结果。
从上面的对话中,可以看出这位女儿对于找对象的决策过程,她从年龄、长相、收入情况以及是不是公务员这四个方面就行决策选择的。决策过程中提出的每个判定问题都是对某个属性的“测试”,例如“年龄=?”“长相=?”;每个测试的结果或是导出最终结论,或是导出进一步的判定问题,其考虑范围是在上次决策结果的限定范围之内,例如若在“年龄=26,长相=帅”之后再判断“收入=?”,则仅在考虑年龄合适长得帅的对象。
决策过程中提出的每个判定问题都是对某个属性的“测试” ,每个测试的结果或是导出最终结论,或者导出进一步的判定问 题,其考虑范围是在上次决策结果的限定范围之内, 从根结点到每个叶结点的路径对应了一个判定测试序列。
决策树学习的目的是为了产生一棵泛化能力强, 即处理未见示例能力强的决策树。
1.3 决策树的步骤
决策树通常有三个步骤:特征选择、决策树的生成、决策树的修剪。
- 特征选择:从训练数据的特征中选择一个特征作为当前节点的分裂标准(特征选择的标准不同产生了不同的特征决策树算法)。
- 决策树生成:根据所选特征评估标准,从上至下递归地生成子节点,直到数据集不可分则停止决策树停止声场。
- 决策树剪枝:决策树容易过拟合,需要剪枝来缩小树的结构和规模(包括预剪枝和后剪枝)。
决策树学习的本质是从训练数据集中归纳出一组分类规则或者说是条件概率模型,与训练数据集不相矛盾的决策树可能有多个或者一个没有,我们需要找到一个与训练数据集矛盾较小的决策树,同时具有很好的泛化能力。换句话说,我们选择的条件概率模型应该不仅对现有的训练数据集有很好的拟合效果,而且能够对未知的数据有很好的预测(泛化能力)。实现的方法通过以上的三个方法。
1.4 决策树的优缺点
优点:
- 计算复杂度不高,输出结果易理解,对中间值缺失不敏感,可以处理不相关特征数据。
- 准确性高: 挖掘出来的分类规则准确性高, 便于理解, 决策树可以清晰的显示哪些字段比较重要, 即可以生成可以理解的规则。
- 可以处理连续和离散字段、不需要任何领域知识和参数假设、适合高维数据。
缺点:
- 对于各类别样本数量不一致的数据, 信息增益偏向于那些更多数值的特征。
- 容易过拟合、忽略属性之间的相关性。
适用数据类型:数值型和标称型
二、决策树的构造
2.1 决策树的一般流程
(1) 收集数据:可以使用任何方法。
(2) 准备数据:树构造算法只是用于标称型数据,因此数值型数据必须离散化。
(3) 分析数据:可以使用任何方法,决策树构造完成后,可以检查决策树图形是否符合预期。
(4) 训练算法:构造一个决策树的数据结构。
(5) 测试算法:使用经验树计算错误率。当错误率达到可接收范围,此决策树就可投放使用。
(6) 使用算法:此步骤可以使用适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义。
在构造决策树之前,我们先来看看若一个男生找对象相亲,那么他对白富美三个属性的决策是怎么样子的;下图是不同属性为根结点,其他结点属性次序不同生成的决策树:
从上面的图中我们可以看出,前面五个决策树的规模比最后一个决策树大得多,这说明特征选择会影响决策树的形状及规模大小,也会影响决策的快慢,从上面六个决策树里可以观察到,富是起主要作用的特征属性,再者是白,然后再到美,所以对于结点的特征选取(即也是属性划分)是很重要的。
决策树学习的关键在于如何选择最优划分属性。一般而言,随着划分过程不断进行,我们希望决策树的分支结点所包含的样本尽可能属于同一类别,即结点的“纯度 ”(purity)越来越高。
经典的属性划分方法:
- 信息增益: ID3
- 信息增益率:C4.5
- 基尼指数:CAR
2.2 信息增益
信息熵
信息熵是度量样本集合纯度最常用的一种指标,假定当前样本集合D中第k类样本所占的比例为 pk (K=1, 2, ..., |y|) ,则D的信息熵定义为:
- Ent(D)的值越小,则D的纯度越高
- 计算信息熵时约定:若p = 0,则plog2p=0 , Ent(D)的最小值为0,最大值为log2 |y|;
- 当随机变量只有两个值,例如1, 0时,即X的分布为 P(X=1)=p , P(X=0)=1-p , 0<=p<=1. 则熵可知,当p=0或p=1时,H(p)=0,随机变 量完全没有不确定性。
- 熵H(p)随概率p变化的曲线如下图:
- 可知,当p=0或p=1时,H(p)=0,随机变 量完全没有不确定性
# 计算给定数据集的熵
def calcShannonEnt(dataSet):
numEntries = len(dataSet) #获得数据集的行数
labelCounts = {} #用于保存每个标签出现次数的字典
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 #选择该标签的概率
shannonEnt -= prob*log(prob, 2) #以2为底求对数
return shannonEnt #返回经验熵
信息增益
离散属性a有V个可能的取值{ , , ..., },用a来进行划分,则会产生V个分支结点,其中第v个分支结点包含了D中所有在属性a上取值为 的样本,记为 。则可计算出用属性a对样本集D进行划分所获得的信息增益:
- 一般而言,信息增益越大,则意味着使用属性a来进行划分所获得的 “纯度提升”越大。
- ID3(Iterative Dichotomiser,迭代二分器)决策树学习算法[Quinlan, 1986] 以信息增益为准则来选择划分属性
信息增益实例
夏日炎炎,我们如何使用过去的经验来判断一个西瓜是不是好瓜呢?我们一般会根据它的色泽,根蒂,敲声,纹理,触感等等来判断它是不是一个好瓜。
该数据集包含 17个训练样本,结果有两种,一是好瓜,二是坏瓜,所以 , 其中正例占 , 反例占 ,计算得到根结点的信息熵为:
以属性“色泽”为例,其对应的3个数据子集分别为(色泽= 青绿),(色泽=乌黑), (色泽=浅白)
子集包含编号为{1, 4, 6, 10, 13, 17} 的6个样例,其中正例占 ,反例占 ,的信息熵为:
同理、的信息熵为:
属性“色泽”的信息增益为:
类似的,我们可以计算出其他属性的信息增益:
显然,属性“纹理”的信息增益最大,其被选为划分属性,下图给出了根据“纹理”属性划分之后的数据子集:
对每一个数据子集按照上面步骤继续划分下去得到最终的决策树,需要注意的是每次样例子集中的属性不包含父结点中划分所依赖的属性,如下图所示:
2.3 信息增益率
采用信息增益来进行划分属性的决策有一个潜在的问题,当某一个属性的取值种类非常多时,对应每一个属性取值的样本子集,其分类的信息熵可能会变得很小。为了说明,采用一种极端情况,假设我们对上面中要分类的西瓜数据进行决策树生成时,把“编号”也当作一种可以作为划分依据的属性。则在这种情况下,每一个编号属性对应一个实例,且其分类是确定的,那么对于每一个“编号”属性取值来说,其分类信息熵为 0,计算“编号”分类所带来的信息增益为:
为了减少这种偏好带来的不利影响,著名的C4.5决策树算法不直接使用信息增益,而使用增益率来选择最优划分属性。
信息增益率定义为:
其中
称为属性a的“固有值”(intrinsic value) [Quinlan, 1993]. 属性a的可能取值数目越多(即V越大),则IV(a) 的值通常会越大。
实例计算信息增益率
对西瓜数据,触感、色泽、编号:
我们注意到,信息增益率对属性值较少的属性比较偏好,因此,C4.5算法并不是直接选择增益率最大的作为候选划分属性,而是使用一个启发式:先从候选划分属性中找出信息增益高于平均水平的属性(保证属性值多的属性被选择),再从中选择增益率最高的(在属性值多的属性中挑属性值少的属性),这样是保证了相对准确。
2.4 基尼指数
基尼系数比信息熵要简单很多,基尼系数的计算公式如下所示:
其中,k是数据集中样本类型数量;是第 i 类样本的数量占总样本数量的比例。
基尼系数的性质与信息熵一样:度量随机变量的不确定度的大小
- G 越大,数据的不确定性越高;
- G 越小,数据的不确定性越低;
- G = 0,数据集中的所有样本都是同一类别;
直观上的理解为,Gini(D)反映了从数据集D中随机抽取两个样本,其类别标记不一致的概率。因此,Gini(D)越小,则数据集D纯度越高。于是产生了基尼指数(Gini index):
实例计算基尼指数
同样对于上面的对西瓜数据来计算色泽的基尼指数:
①求基尼值,青绿、乌黑、浅白
②求基尼指数
2.5 划分数据集
既然前面,我们已经讨论出了三种划分方式(信息增益、信息增益率、基尼指数),现在我们就开始划分数据,下面我以信息增益的示例来划分属性:
# 按照给定特征划分数据集
def splitDataSet(dataSet,axis,value): #dataSet:带划分数据集 axis:划分数据集的特征 value:需要返回的特征的值
retDataSet=[] #创建新的list对象
for featVec in dataSet: #遍历元素
if featVec[axis] == value: #符合条件的,抽取出来
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
知识点总结:
python中append()与extend()的区别
1、append():用于在列表末尾添加新的对象,列表只占一个索引位,在原有列表上增加
①语法:list.append()
②参数:向列表中添加一个对象,即添加到列表末尾的对象,任意对象都是可以的,直接将整个参数放入列表末尾。
③返回值:无返回值,但是会修改原来的列表。
2、extend():向列表尾部加一个列表,列表中的每个元素都追加进来,在原有列表上增加
①语法:list.extend()
②参数:把一个序列参数的内容添加到列表中,即元素列表,对象必须是一个可以迭代的序列,将参数打散后依次放入列表末尾。
③返回值:无返回值,但会在已存在的列表中添加新的列表内容。
# 选择最好的数据集划分方式
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 #信息熵
for value in uniqueVals: #循环特征的值
subDataSet = splitDataSet(dataSet, i, value) #subDataSet划分后的子集
prob = len(subDataSet) / float(len(dataSet)) #计算子集的概率
newEntropy += prob * calcShannonEnt((subDataSet))
infoGain = baseEntropy - newEntropy #计算信息增益
print("第%d个特征的信息增益为%.3f" % (i, infoGain)) #打印每个特征的信息增益
if (infoGain > bestInfoGain): #计算信息增益
bestInfoGain = infoGain #更新信息增益,找到最大的信息增益
bestFeature = i #记录信息增益最大的特征的索引值
return bestFeature #返回信息增益最大特征的索引值
2.5 递归构建决策树
伪代码:
显然,决策树的生成是一个递归过程,在决策树基本算法中,有三种情形会导致递归返回:
①当前结点包含的样本全部属于同一类别C:
无需划分,叶子节点标记为类别C
②当前属性集为空,或所有样本在所有属性上取值相同:
当前结点标记为叶子节点,类别=该结点所含样本最多的类别
③当前结点包含的样本集合为空:
当前结点标记为叶子节点,类别=该结点的父节点所含样本最多的类别
代码实现:
# 统计出现次数最多的元素(类标签)
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): #如果类别完全相同,则停止继续划分
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[:] #复制所有标签,这样树就不会弄乱现有标签
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
return myTree
小结:到这就可以捋清楚决策树从无到有的过程,首先在收集好数据的前提下,通过数据集选择最优特征值来划分数据集,然后根据划分后得数据集一层一层递归生成决策树。一开始老师讲解这个课的时候,不太能想象的用python怎么实现树的构造,学习到后面才了解是用字典结构,花了有点时间去学习用字典来表示树。
三、在 Python 中使用 Matplotlib 注解绘制树形图
现在我们已经能基本懂得来决策树的构造了,进一步拓展,实现决策树的可视化,同样还是和KNN算法一样用Matplotlib来实现可视化。
3.1 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.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',xytext=centerPt,
textcoords='axes fraction',va="center", ha="center", bbox=nodeType, arrowprops=arrow_args)
语句解析:
plt.annotate(str, xy=data_point_position, xytext=annotate_position,
va="center", ha="center", xycoords="axes fraction",
textcoords="axes fraction",bbox=annotate_box_type,arrowprops=arrow_style)str是给数据点添加注释的内容,如字符串
xy是要添加注释的数据点的位置
xytext是注释内容的位置
bbox是注释框的风格和颜色深度,fc越小,注释框的颜色越深,支持输入一个字典
va="center", ha="center"表示注释的坐标以注释框的正中心为准,而不是注释框的左下角(v代表垂直方向,h代表水平方向)
xycoords和textcoords可以指定数据点的坐标系和注释内容的坐标系,通常只需指定xycoords即可,textcoords默认和xycoords相同
arrowprops可以指定箭头的风格支持,输入一个字典
测试:
# 测试生成节点和箭头
fig = plt.figure(1, facecolor='white')
fig.clf()
font = {'family': 'MicroSoft YaHei'}
matplotlib.rc("font", **font)
createPlot.ax1 = plt.subplot(111, frameon=False)
plotNode('决策节点', (0.6, 0.1), (0.1, 0.5), decisionNode)
plotNode('叶子节点', (0.8, 0.1), (0.3, 0.8), leafNode)
plt.show()
测试结果:
3.2 构造注解树
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 = [{'有自己的房子': {0: {'有工作': {0: 'no', 1: {'年龄': {0: {'信贷情况': {'no': 'no', 'yes': 'yes'}}, 2: 'yes'}}}}, 1: {'年龄': {0: {'信贷情况': {'no': 'no', 'yes': 'yes'}}, 1: 'yes', 2: 'yes'}}}}]
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': 'MicroSoft YaHei'}
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()
测试:
# 测试结果
myTree = retrieveTree(0);
createPlot(myTree)
四、测试和存储分类器
4.1 测试算法:使用决策树执行分类
# 使用决策树进行分类
def classify(inputTree, featLabels, testVec):
firstStr = list(inputTree.keys())[0] #根节点
secondDict = inputTree[firstStr] #获得value值
featIndex = featLabels.index(firstStr) #特征值下标
key = testVec[featIndex]
valueOfFeat = secondDict[key]
if isinstance(valueOfFeat, dict):
classLabel = classify(valueOfFeat, featLabels, testVec) # 递归调用
else:
classLabel = valueOfFeat
return classLabel
4.2 实用算法:决策树的存储
# 存储
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)
4.3 算法结果:准确率
count = 0 #初始化,正确数目为0
numTest = len(testDate) #测试样本数
for test in testDate: #遍历测试每一个数集
result = classify(myTree, labels, test[0:4])
if(result == test[-1]):
print(result)
count += 1
print("准确率为:%f"%(count/numTest))
五、决策树的三种实现算法
5.1 ID3算法
(1)代码实现
上面的讲解,已经使用信息增益来划分属性的,即使用的是ID3算法,完整代码如下:
# 计算给定数据集的熵
def calcShannonEnt(dataSet):
numEntries = len(dataSet) #获得数据集的行数
labelCounts = {} #用于保存每个标签出现次数的字典
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 #选择该标签的概率
shannonEnt -= prob*log(prob, 2) #以2为底求对数
return shannonEnt #返回经验熵
# 按照给定特征划分数据集
def splitDataSet(dataSet,axis,value): #dataSet:带划分数据集 axis:划分数据集的特征 value:需要返回的特征的值
retDataSet=[] #创建新的list对象
for featVec in dataSet: #遍历元素
if featVec[axis] == value: #符合条件的,抽取出来
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
# 选择最好的数据集划分方式 ID3算法——信息增益
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 #信息熵
for value in uniqueVals: #循环特征的值
subDataSet = splitDataSet(dataSet, i, value) #subDataSet划分后的子集
prob = len(subDataSet) / float(len(dataSet)) #计算子集的概率
newEntropy += prob * calcShannonEnt((subDataSet))
infoGain = baseEntropy - newEntropy #计算信息增益
print("第%d个特征的信息增益为%.3f" % (i, infoGain)) #打印每个特征的信息增益
if (infoGain > bestInfoGain): #计算信息增益
bestInfoGain = infoGain #更新信息增益,找到最大的信息增益
bestFeature = i #记录信息增益最大的特征的索引值
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): #如果类别完全相同,则停止继续划分
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[:] #复制所有标签,这样树就不会弄乱现有标签
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
return myTree
(2)结果测试:
#ID3算法 生成的决策树
{'有自己的房子': {0: {'有工作': {0: 'no', 1: {'年龄': {0: {'信贷情况': {'no': 'no', 'yes': 'yes'}}, 2: 'yes'}}}}, 1: {'年龄': {0: {'信贷情况': {'no': 'no', 'yes': 'yes'}}, 1: 'yes', 2: 'yes'}}}}
(3)准确率
用信息增益来划分属性,ID3算法的准确率为83.3%
5.2 C4.5算法
(1)代码实现
C4.5使用信息增益比作为特征选择的度量,与ID3相比较,没有差多少,就是把信息增益改成信息增益率,即在ID3算法基础上,根据信息增益的公式修改 chooseBestFeatureToSplit() 函数
#选择最好的数据集划分方式 C4.5算法——信息增益率
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特征数量
baseEntropy = calcShannonEnt(dataSet) #获取信息熵
bestinfoGainratio = 0.0 #信息增益率初始化为0,
bestFeature = -1 #最优的划分特征,初始化为-1
for i in range(numFeatures): #遍历所有的特征
featList = [example[i] for example in dataSet] #创建list用来存每个样本在第i维度的特征值
uniqueVals = set(featList) #获取该特征下的所有不同的值
newEntropy = 0.0 #初始化信息熵
IV = 0.0
for value in uniqueVals: #遍历该特征维度下对应的所有特征值
subDataSet = splitDataSet(dataSet, i, value) #划分数据集
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
IV -= prob * log(prob,2) #计算信息增益率
infoGain = baseEntropy - newEntropy #计算信息增益
if IV == 0.0:
infoGainratio = 0.0
else:
infoGainratio = float(infoGain) / float(IV)
if (infoGainratio > bestinfoGainratio):
bestinfoGainratio = infoGainratio #找到最大的信息增益率
bestFeature = i #记录信息增益率最大的索引值
return bestFeature
(2)结果测试:
#C4.5算法 生成的决策树
{'有自己的房子': {0: {'有工作': {0: 'no', 1: {'年龄': {0: {'信贷情况': {'yes': 'yes', 'no': 'no'}}, 2: 'yes'}}}}, 1: {'有工作': {0: 'yes', 1: {'年龄': {0: {'信贷情况': {'yes': 'yes', 'no': 'no'}}, 1: 'yes'}}}}}}
(3)准确率
用信息增益率来划分属性,C4.5算法的准确率为83.3%,与ID3算法求得的准确率一样
5.3 CART算法
(1)代码实现
若用基尼指数来划分属性,相较于ID3算法和C4.5算法,基尼指数不需要计算信息熵,方便许多,即在ID3算法基础上,修改 chooseBestFeatureToSplit() 函数,并且还要添加一个每个属性的计算概率的函数calcProbabilityEnt()函数:
# 计算概率
def calcProbabilityEnt(dataSet):
numEntries = len(dataSet) # 数据集大小
feaCounts = 0 # 特征数量
feature = dataSet[0][len(dataSet[0]) - 1]
for feaVec in dataSet:
if feaVec[-1] == feature:
feaCounts += 1
probabilityEnt = float(feaCounts) / numEntries #概率= 特征数量/数据集大小
return probabilityEnt
# 选择最好的数据集划分方式 CAR算法——基尼指数
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特征数量
if numFeatures == 1:
return 0
bestGini = 1 #最佳基尼指数
bestFeature = -1 #最优的划分特征,初始化为-1
for i in range(numFeatures): # 遍历所有的特征
featList = [example[i] for example in dataSet]
feaGini = 0 # 定义特征的值的基尼系数
uniqueVals = set(featList) #获取该特征下的所有不同的值
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value) #划分数据集
prob = len(subDataSet) / float(len(dataSet))
probabilityEnt = calcProbabilityEnt(subDataSet)
feaGini += prob * (2 * probabilityEnt * (1 - probabilityEnt))
if (feaGini < bestGini):
bestGini = feaGini #基尼指数
bestFeature = i #记录基尼指数最小的索引值
return bestFeature
(2)结果测试:
#CAR算法 生成的决策树
{'有自己的房子': {0: {'有工作': {0: 'no', 1: {'年龄': {0: {'信贷情况': {1: 'yes'}}, 2: 'yes'}}}}, 1: {'年龄': {0: {'有工作': {1: {'信贷情况': {0: 'yes'}}}}, 1: 'yes', 2: 'yes'}}}}
(3)准确率
用基尼指数来划分属性,CAR算法的准确率为93.3%,在这三个算法中这个准确率是最高的了!
六、决策树实验分析与总结
6.1 三种决策树算法比较
ID3算法:
- ID3算法核心就是“最大信息熵增益” 原则选择划分当前数据集的最好特征。
- 而且对于连续型特征,比如长度,密度都是连续值,无法在ID3运用,因为一开始我用的鸢尾花实例数据,ID3不能判断连续型量。
- 前面也说到了,利用信息熵划分属性,会对倾向于可取值数目较多的属性。
- 没有考虑过拟合的问题。
C4.5算法:
- C4.5算法流程与ID3算法相类似,只不过将信息增益改为信息增益率,以解决偏向取值较多的属性的问题。
- 可以处理属性变量为连续型的,将连续的特征离散化。
CAR算法:
- CART算法使用基尼系数来代替信息增益比,基尼系数代表了模型的不纯度,基尼系数越小,则不纯度越低,特征越好。这和信息增益及信息增益率是相反的。
- 对于CART算法的连续值的处理问题,其思想和C4.5算法是相同的,都是将连续的特征离散化。
6.2 实验总结
决策树,其实和KNN算法的目的是一样的,就是做分类任务。对于我这次实验,最基本的就是懂得了ID3、C4.5、CART三种算法。一开始对信息熵、信息增益、信息增益率、基尼指数,不太理解,突然来的新东西需要时间消化,我比较困难的地方是用Matplotlib绘制决策树,那个代码比较难理解,而且对于python3的环境,有些函数已经更新了,也导致了过程中的报错,解决问题实在是太艰难了,不过好在实验做出来了!还有一个就是,python用字典结构存储决策树,是我没有想到的,花了一点点时间才看明白。
更多推荐
所有评论(0)