决策树常用来处理分类问题,也是最经常使用的数据挖掘算法。它之所以如此流行,一个很重要的原因就是使用者基本上不用了解机器学习算法,也不用深究它是如何工作的。下图所示的流程图就是一个决策树,正方形代表判断模块(decision block),椭圆形代表终止模块(terminating block),表示已经得出结论,可以终止运行。从判断模块引出的左右箭头称作分支(branch),它可以到达另一个判断模块或者终止模块。下图构造了一个假想的邮件分类系统,它首先检测发送邮件域名地址,如果地址为myEmployer,则将其放在分类“无聊时需要阅读的邮件”中。如果邮件不是来自这个域名,则检查邮件内容是否包含单词曲棒球,如果包含则将邮件归类到“需要及时处理的朋友邮件”,如果不包含则将邮件归类为“无需阅读的垃圾邮件”。
k-近邻算法可以完成很多分类任务,但是它最大的缺点就是无法给出数据的内在含义,决策树的主要优势就在于数据形式容易理解。
决策树算法能够读取数据集合,它的很多任务都是为了数据中所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,并从中提取出一系列规则,机器学习算法最终将使用这些机器从数据集中创造的规则。专家系统中经常使用决策树,而且决策树给出结果往往可以匹敌在当前领域具有几十年工作经验的人类专家。
3.1决策树的构建
决策树:
优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。
缺点:可能会产生过度匹配问题。
适用数据类型:数值型和标称型。
在构建决策树时,需要解决的第一个问题就是,当前数据集上哪个特征在划分数据分类时起决定性作用。为了找到决定性的特征,划分出最好的结果,必须评估每个特征。完成测试之后,原始数据集就被划分为几个数据子集。这些数据子集会分布在第一个决策点的所有分支上。如果某个分支下的数据属于同一类型,则当前无需阅读的垃圾邮件已经正确地划分数据分类,无需进一步对数据集进行分割。如果数据子集内的数据不属于同一类型,则需要重复划分数据子集的过程。如果划分数据子集的算法和划分原始数据集的方法相同,直到所有具有相同类型的数据均在一个数据子集内。
创建分支的伪代码函数createBranch()如下所示:
上述的伪代码createBranch是一个递归函数,在倒数第二行直接调用了它自己。
决策树的一般流程:
(1)收集数据:可以使用任何方法。
(2)准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。
(3)分析数据:可以使用任何方法,构造树完成之后,应该检查图形是否符合预期。
(4)训练算法:构造树的数据结构。
(5)测试算法:使用经验树计算错误率。
(6)使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义。
划分数据的方法有二分法,ID3算法等。如果依据某个属性划分数据将会产生4个可能的值,我们将把数据划分成4块,并创建四个不同的分支。这里使用ID3算法(ID3算法是一种贪心算法,它起源于概念学习系统,以信息熵的下降速度为选取测试属性的标准,即在每个节点选取还尚未被用来划分的具有最高信息增益的属性作为划分标准,然后继续这个过程,直到生成的决策树能完美分类训练样例)划分数据集,该算法处理如何划分数据集,何时停止划分数据集。每次划分数据集时只选取一个特征属性,如果训练集中存在20个特征,第一次选择哪个特征作为划分的参考属性呢?
下表的数据包含5个海洋动物,特征包括:不浮出水面是否可以生存,以及是否有脚蹼。可以将这些动物分成两类:鱼类和非鱼类。现在想要决定依据第一个特征还是第二个特征划分数据。在回答这个问题之前,必须采用量化的方法判断如何划分数据。
3.1.1信息增益
划分数据集的大原则是:将无序的数据变得更加有序。可以使用多种方法划分数据集,但是每种方法都有各自的优缺点。组织杂乱无章数据的一种方法就是使用信息论度量信息,信息论是量化处理信息的分支科学。可以在划分数据之前使用信息论量化度量信息的内容。
在划分数据集之前之后信息发生的变化称为信息增益,知道如何计算信息增益,就可以计算每个特征值划分数据值获得的信息增益,获得信息增益最高的特征就是最好的选择。
在可以评测哪种数据划分方式是最好的数据划分之前,必须学习如何计算信息增益。集合信息的度量方式称为香农熵或者称为熵,这个名字来源于信息论之父克劳德·香农。
熵定义为信息的期望值,在明晰这个概念之前,必须知道信息的定义。如果待分类的事务可能划分在多个分类之中,则符号xi的信息定义为:
其中p(xi)是选择该分类的概率。
为了计算熵,需要计算所有类别所有可能值包含的信息期望值,通过下面的公式得到:
其中n是分类的数目。
使用python计算信息熵,下面代码的功能是计算给定数据集的熵:
from math import log
#计算给定数据集的香农熵
def calxns(dataset):
num=len(dataset)
labels={}
for fv in dataset:
label=fv[-1]
if label not in labels:
labels[label]=1
else:
labels[label]+=1
xns=0
for key in labels:
prob=float(labels[key])/num
xns-=prob*log(prob,2)
return xns
代码解释:首先,计算数据集中实例的总数。由于代码中多次用到这个值,为了提高代码效率,显式地声明一个变量保存实例总数。然后,创建一个数据字典,它的键是最后一列的数值。如果当前键值不存在,则扩展字典并将当前键值加入字典。每个键值都记录了当前类别出现的次数。最后,使用所有类标签的发生频率计算类别出现的概率,用这个概率计算香农熵,统计所有类标签发生的次数。
使用熵划分数据集:
利用createDataSet()函数得到分类鱼的数据集:
#生成分类数据集
def creatDataSet():
dataset=[[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no']]
labels=['no surfacing','flippers']
return dataset,labels
调用函数,计算生成数据集的香农熵:
if __name__=='__main__':
dataset,labels=creatDataSet()
xns=calxns(dataset)
print(dataset,labels)
print(xns)
结果:
熵越高,则混合的数据也越多,可以在数据集中添加更多的分类,观察熵是如何变化的。这里增加第三个名为maybe的分类,测试熵的变化:
if __name__=='__main__':
dataset,labels=creatDataSet()
dataset[0][-1]='maybe'
xns=calxns(dataset)
print(dataset,labels)
print(xns)
结果:
可以看出当类别增多后,香农熵增大。
得到熵之后,就可以按照获取最大信息增益的方法划分数据集。
另一个度量集合无序程度的方法是基尼不纯度(Gini impurity),简单地说就是从一个数据集中随机选取子项。这里不做进一步介绍。
3.1.2划分数据集
分类算法除了需要测量信息熵,还需要划分数据集,度量花费数据集的熵,以便判断当前是否正确地划分了数据集。将每个特征划分数据集的结果计算一次信息熵,然后判断按照哪个特征划分数据集是最好的划分方式。想象一个分布在二维空间的数据散点图,需要在数据之间画条线,将它们分成两部分,应该按照x轴还是y轴划线呢?
#按照给定特征划分数据集
def splitdataset(dataset,axis,value):
rdataset=[]
for fv in dataset:
if fv[axis]==value:
rfv=fv[:axis]
rfv.extend(fv[axis+1:])
rdataset.append(rfv)
return rdataset
上述代码使用了三个输入参数:待划分的数据集、划分数据集的特征、特征的返回值。python语言不用考虑内存分配问题。python语言在函数中传递的是列表的引用,在函数内部对列表对象的修改,将会影响该列表对象的整个生存周期。为了消除这个不良影响,需要在函数的开始声明一个新列表对象。因为该函数代码在同一数据集上被调用多次,为了不修改原始数据集,创建一个新的列表对象。数据集这个列表中的各个元素也是列表,要遍历数据集中的每个元素,一旦发现符合要求的值,则将其添加到新创建的列表中。在if语句中,程序将符合特征的数据抽取出来。可以这样理解这段代码:当我们按照某个特征划分数据集时,就需要将所有符合要求的元素抽取出来。代码中使用了python语言列表类型自带的extend()和append()方法。这两个方法功能类似,但是在处理多个列表时,这两个方法的处理结果是完全不同的。
a1=[1,2,3,4]
a2=[1,2,3,4]
b=[5,6,7,8]
a1.append(b)
a2.extend(b)
print('append:',a1)
print('extend:',a2)
结果:
从结果可以看出append是加一个元素,该元素可以是数,可以是列表等各种形式;而extend是在一个列表的末尾一次性追加另一个序列中的多个值。
在前面的简单样本数据上测试函数splitdataset()。
if __name__=='__main__':
dataset,labels=creatDataSet()
print(dataset)
print(splitdataset(dataset,0,1))
print(splitdataset(dataset,0,0))
结果:
接下来遍历整个数据集,循环计算香农熵和splitdataset()函数,找到最好的特征划分方式,熵计算将会告诉我们如何划分数据集是最好的数据组织方式。
def choosebestplitdataset(dataset):
numFeature=len(dataset[0])-1
baseEntroy=calxns(dataset)
bestinfo=0
bestfeature=-1
for i in range(numFeature):
featureList=[example[i] for example in dataset]
uniqueVals=set(featureList)
newEntroy=0
for value in uniqueVals:
subdataset=splitdataset(dataset,i,value)
prob=len(subdataset)/float(len(dataset))
newEntroy+=prob*calxns(subdataset)
infoGain=baseEntroy-newEntroy
if(infoGain>bestinfo):
bestinfo=infoGain
bestfeature=i
return bestfeature
choosebestsplitdataset()函数实现选取特征,划分数据集,计算得出最好的划分数据集的特征。在函数中调用的数据需要满足一定的要求:第一个要求是数据必须是一种由列表元素组成的列表,而且所有的列表元素都要有相同的数据长度;第二个要求是数据的最后一列或者每个实例的最后一个元素是当前实例的类别标签。数据集一旦满足上述要求,就可以在函数的第一行判定当前数据集包含多少特征属性。无需限定list中数据的类型,它们既可以是数字也可以是字符串,并不影响实际计算。
结果:
从结果可以看出第0个特征是最好的用于划分数据集的特征。
3.1.3递归构建决策树
从数据集构造的决策树算法需要的子功能模块的工作原理为:得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,可以再次划分数据。因此可以采用递归的原则处理数据集。
递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。任何到达叶子节点的数据必须属于叶子节点的分类,如下图所示:
划分数据集时的数据路径:
第一个结束条件使得算法可以终止,甚至可以设置算法可以划分的最大分组数目。由于特征数目并不是在每次划分数据分组时都减少,因此这些算法在实际使用时可能引起一定的问题。目前并不需要考虑这个问题,只需要在算法开始运行前计算列的数目,查看算法是否使用了所有属性即可。如果数据集已经处理了所有属性,但是类标签依然不是唯一的,此时需要决定如何定义该叶子节点,在这种情况下,通常会采用多数表决的方法决定该叶子节点的分类。
在tree.py文件顶部增加一行代码:import operator,然后增加下面的代码到tree.py文件中:
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]
该函数使用分类名称的列表,然后创建键值为classList中唯一值的数据字典,字典对象存储了classList中每个类标签出现的频率,最后利用operator操作键值排序字典,并返回出现次数最多的分类名称。
再在tree.py文件中添加以下代码:
##创建树的函数代码
def createTree(dataSet,label):
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=choosebestplitdataset(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
上述代码使用两个输入参数:数据集和标签列表。标签列表包含了数据集中所有特征的标签,算法本身并不需要这个变量,但是为了给出数据明确的含义,将它作为一个输入参数提供。此外,前面提到的对数据集的要求这里依然需要满足。上述代码首先创建了名为classList的列表变量,其中包含了数据集的所有类标签。递归函数的第一个停止条件是所有的类标签完全相同,则直接返回该类标签。递归函数的第二个停止条件是使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。由于第二个条件无法简单地返回唯一的类标签,挑选出现次数最多的类别作为返回值。
下一步程序开始创建树,这里使用python语言的字典类型存储树的信息,当然也可以声明特殊的数据类型存储树,但是这里完全没有必要。字典变量myTree存储了树的所有信息,这对于其后绘制树形图非常重要。当前数据集选取的最好特征存储在变量beatFeat中,得到列表包含的所有属性值。
最后代码遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree(),得到的返回值将被插入到字典变量myTree中,因此函数终止执行时,字典中将会嵌套很多代表叶子节点信息的字典数据。在解释这个嵌套数据之前,先看一下循环的第一行subLabels=labels[:],这行代码复制了类标签,并将其存储在新列表变量subLabels中。之所以这样做,是因为在python语言中函数参数是列表类型时,参数是按照引用方式传递的。为了保证每次调用函数creatTree()时不改变原始列表的内容,使用新变量subLabels代替原始列表。
输入一下命令测试代码的实际输出结果:
if __name__=='__main__':
dataset,labels=creatDataSet()
myTree=createTree(dataset,labels)
print(myTree)
结果:
来源:https://blog.csdn.net/qq_36850381/article/details/97099770