一,引言:python
上一章咱们讲的kNN算法,虽然能够完成不少分类任务,但它最大的缺点是没法给出数据的内在含义,而决策树的主要优点就在于数据形式很是容易理解。决策树算法可以读取数据集合,决策树的一个重要任务是为了数据所蕴含的知识信息,所以,决策树可使用不熟悉的数据集合,并从中提取一系列规则,在这些机器根据数据集建立规则是,就是机器学习的过程。算法
二,相关知识数据结构
1 决策树算法app
在构造决策树时,第一个须要解决的问题就是,如何肯定出哪一个特征在划分数据分类是起决定性做用,或者说使用哪一个特征分类能实现最好的分类效果。这样,为了找到决定性的特征,划分川最好的结果,咱们就须要评估每一个特征。当找到最优特征后,依此特征,数据集就被划分为几个数据子集,这些数据本身会分布在该决策点的全部分支中。此时,若是某个分支下的数据属于同一类型,则该分支下的数据分类已经完成,无需进行下一步的数据集分类;若是分支下的数据子集内数据不属于同一类型,那么就要重复划分该数据集的过程,按照划分原始数据集相同的原则,肯定出该数据子集中的最优特征,继续对数据子集进行分类,直到全部的特征已经遍历完成,或者全部叶结点分支下的数据具备相同的分类。机器学习
建立分支的伪代码函数createBranch()以下:函数
检测数据集中的每个子项是否属于同一分类: 工具
if so return 类标签; else 寻找划分数据集的最好特征 划分数据集 建立分支结点 for 每一个分支结点 调用函数createBranch并增长返回结点到分支结点中//递归调用createBranch() return 分支结点
了解了如何划分数据集后,咱们能够总结出决策树的通常流程:学习
(1)收集数据测试
(2)准备数据:构造树算法只适用于标称型数据,所以数值型数据必须离散化spa
(3)分析数据
(4)训练数据:上述的构造树过程构造决策树的数据结构
(5)测试算法:使用经验树计算错误率
(6)使用算法:在实际中更好地理解数据内在含义
2 最好特征选取的规则:信息增益
划分数据集的大原则是:将无序的数据变得更加有序。在划分数据集先后信息发生的变化称为信息增益,若是咱们知道如何计算信息增益,就能够计算每一个特征值划分数据集得到的信息增益,而获取信息增益最高的特征就是最好的特征。
接下来,咱们讲学习如何计算信息增益,而提到信息增益咱们又不得不提到一个概念"香农熵",或者简称熵。熵定义为信息的指望值。
若是待分类的事物可能会出现多个结果x,则第i个结果xi发生的几率为p(xi),那么咱们能够由此计算出xi的信息熵为l(xi)=p(xi)log(1/p(xi))=-p(xi)log(p(xi))
那么,对于全部可能出现的结果,事物所包含的信息但愿值(信息熵)就为:H=-Σp(xi)log(p(xi)),i属于全部可能的结果
这样,假设利用数据集中某一特征A对数据集D(D的分类类别有n种)进行分类,而特征A取值有k种,那么此时,利用特征A对数据集进行分类的信息增益为:
信息增益H(D,A)=原始数据集的信息熵H(D)-特征A对数据集进行划分后信息熵H(D/A)
其中H(D/A)=∑|Aj|/|D|*H(Aj),j属于A的k种取值,|Aj|和|D|分别表示,特征A第j种取值的样本数占全部取值样本总数的比例,以及数据集的样本总数
三,构造决策树
在知道了如何选取划分数据的最优特征后,咱们就能够依据此来构建决策树了。
1 因为要屡次使用香农熵的公式,因此咱们写出计算给定数据集的熵的公式:
#计算给定数据集的熵 #导入log运算符 from math import log def calEnt(dataSet): #获取数据集的行数 numEntries=len(dataSet) #设置字典的数据结构 labelCounts={} #提取数据集的每一行的特征向量 for featVec in dataSet: #获取特征向量的最后一列的标签 currentLabel=featVec[-1] #检测字典的关键字key中是否存在该标签 #若是不存在keys()关键字 if currentLabel not in labelCounts.keys(): #将当前标签/0键值对存入字典中 labelCounts[currentLabel]=0 #不然将当前标签对应的键值加1 labelCounts[currentLabel]+=1 #初始化熵为0 Ent=0.0 #对于数据集中全部的分类类别 for key in labelCounts: #计算各个类别出现的频率 prob=float(labelCounts[key])/numEntries #计算各个类别信息指望值 Ent-=prob*log(prob,2) #返回熵 return Ent
2 咱们固然须要构建决策树的数据集:
#建立一个简单的数据集 #数据集中包含两个特征'no surfacing','flippers'; #数据的类标签有两个'yes','no' 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
须要说明的是,熵越高,那么混合的数据就越多,若是咱们在数据集中添加更多的分类,会致使熵结果增大
3 接下来咱们就要经过上面讲到的信息增益公式获得划分数据集的最有特征,从而划分数据集
首先划分数据集的代码:
#划分数据集:按照最优特征划分数据集 #@dataSet:待划分的数据集 #@axis:划分数据集的特征 #@value:特征的取值 def splitDataSet(dataSet,axis,value): #须要说明的是,python语言传递参数列表时,传递的是列表的引用 #若是在函数内部对列表对象进行修改,将会致使列表发生变化,为了 #不修改原始数据集,建立一个新的列表对象进行操做 retDataSet=[] #提取数据集的每一行的特征向量 for featVec in dataSet: #针对axis特征不一样的取值,将数据集划分为不一样的分支 #若是该特征的取值为value if featVec[axis]==value: #将特征向量的0~axis-1列存入列表reducedFeatVec reducedFeatVec=featVec[:axis] #将特征向量的axis+1~最后一列存入列表reducedFeatVec #extend()是将另一个列表中的元素(以列表中元素为对象)一一添加到当前列表中,构成一个列表 #好比a=[1,2,3],b=[4,5,6],则a.extend(b)=[1,2,3,4,5,6] reducedFeatVec.extend(featVec[axis+1:]) #简言之,就是将原始数据集去掉当前划分数据的特征列 #append()是将另一个列表(以列表为对象)添加到当前列表中 ##好比a=[1,2,3],b=[4,5,6],则a.extend(b)=[1,2,3,[4,5,6]] retDataSet.append(reducedFeatVec) return retDataSet
须要说明的是:
(1)在划分数据集函数中,传递的参数dataSet列表的引用,在函数内部对该列表对象进行修改,会致使列表内容发生改变,因而,为了消除该影响,咱们应该在函数中建立一个新的列表对象,将对列表对象操做后的数据集存入新的列表对象中
(2)须要区分一下append()函数和extend()函数
这两种方法的功能相似,都是在列表末尾添加新元素,可是在处理多个列表时,处理结果有所不一样:
好比:a=[1,2,3],b=[4,5,6]
那么a.append(b)的结果为:[1,2,3,[4,5,6]],即便用append()函数会在列表末尾添加人新的列表对象b
而a.extend(b)的结果为:[1,2,3,4,5,6],即便用extend()函数
接下来,咱们再看选取最优特征的代码:
#如何选择最好的划分数据集的特征 #使用某一特征划分数据集,信息增益最大,则选择该特征做为最优特征 def chooseBestFeatureToSplit(dataSet): #获取数据集特征的数目(不包含最后一列的类标签) numFeatures=len(dataSet[0])-1 #计算未进行划分的信息熵 baseEntropy=calEnt(dataSet) #最优信息增益 最优特征 bestInfoGain=0.0;bestFeature=-1 #利用每个特征分别对数据集进行划分,计算信息增益 for i in range(numFeatures): #获得特征i的特征值列表 featList=[example[i] for example in dataSet] #利用set集合的性质--元素的惟一性,获得特征i的取值 uniqueVals=set(featList) #信息增益0.0 newEntropy=0.0 #对特征的每个取值,分别构建相应的分支 for value in uniqueVals: #根据特征i的取值将数据集进行划分为不一样的子集 #利用splitDataSet()获取特征取值Value分支包含的数据集 subDataSet=splitDataSet(dataSet,i,value) #计算特征取值value对应子集占数据集的比例 prob=len(subDataSet)/float(len(dataSet)) #计算占比*当前子集的信息熵,并进行累加获得总的信息熵 newEntropy+=prob*calEnt(subDataSet) #计算按此特征划分数据集的信息增益 #公式特征A,数据集D #则H(D,A)=H(D)-H(D/A) infoGain=baseEntropy-newEntropy #比较此增益与当前保存的最大的信息增益 if (infoGain>bestInfoGain): #保存信息增益的最大值 bestInfoGain=infoGain #相应地保存获得此最大增益的特征i bestFeature=i #返回最优特征 return bestFeature
在函数调用中,数据必须知足必定的要求,首先,数据必须是由列表元素组成的列表,并且全部的列表元素具备相同的数据长度;其次,数据的最后一列或者每一个实例的最后一个元素是当前实例的类别标签。这样,咱们才能经过程序统一完成数据集的划分
4,在经过以上的各个模块学习以后,咱们接下来就要真正构建决策树,构建决策树的工做原理为:首先获得原始数据集,而后基于最好的属性划分数据集,因为特征值可能多于两个,所以可能存在大于两个分支的数据集划分。第一次划分以后,数据将向下传递到树分支的下一个结点,在该结点上,咱们能够再次划分数据。所以,咱们能够采用递归的方法处理数据集,完成决策树构造。
递归的条件是:程序遍历完全部划分数据集的属性,或者每一个分之下的全部实例都具备相同的分类。若是全部的实例具备相同的分类,则获得一个叶子结点或者终止块。
固然,咱们可能会遇到,当遍历完全部的特征属性,可是某个或多个分支下实例类标签仍然不惟一,此时,咱们须要肯定出如何定义该叶子结点,在这种状况下,经过会采起多数表决的原则选取分支下实例中类标签种类最多的分类做为该叶子结点的分类
这样,咱们就须要先定义一个多数表决函数majorityCnt()
#当遍历完全部的特征属性后,类标签仍然不惟一(分支下仍有不一样分类的实例) #采用多数表决的方法完成分类 def majorityCnt(classList): #建立一个类标签的字典 classCount={} #遍历类标签列表中每个元素 for vote in classList: #若是元素不在字典中 if vote not in classCount.keys(): #在字典中添加新的键值对 classCount[vote]=0 #不然,当前键对于的值加1 classCount[vote]+=1 #对字典中的键对应的值所在的列,按照又大到小进行排序 #@classCount.items 列表对象 #@key=operator.itemgetter(1) 获取列表对象的第一个域的值 #@reverse=true 降序排序,默认是升序排序 sortedClassCount=sorted(classCount.items,\ key=operator.itemgetter(1),reverse=true) #返回出现次数最多的类标签 return sortedClassCount[0][0]
好了,考虑了这种状况后,咱们就能够经过递归的方式写出决策树的构建代码了
#建立树 def createTree(dataSet,labels): #获取数据集中的最后一列的类标签,存入classList列表 classList=[example[-1] for example in dataSet] #经过count()函数获取类标签列表中第一个类标签的数目 #判断数目是否等于列表长度,相同表面全部类标签相同,属于同一类 if classList.count(classList[0])==len(classList): return classList[0] #遍历完全部的特征属性,此时数据集的列为1,即只有类标签列 if len(dataSet[0])==1: #多数表决原则,肯定类标签 return majorityCnt(classList) #肯定出当前最优的分类特征 bestFeat=chooseBestFeatureToSplit(dataSet) #在特征标签列表中获取该特征对应的值 bestFeatLabel=labels[bestFeat] #采用字典嵌套字典的方式,存储分类树信息 myTree={bestFeatLabel:{}} ##此位置书上写的有误,书上为del(labels[bestFeat]) ##至关于操做原始列表内容,致使原始列表内容发生改变 ##按此运行程序,报错'no surfacing'is not in list ##如下代码已改正 #复制当前特征标签列表,防止改变原始列表的内容 subLabels=labels[:] #删除属性列表中当前分类数据集特征 del(subLabels[bestFeat]) #获取数据集中最优特征所在列 featValues=[example[bestFeat] for example in dataSet] #采用set集合性质,获取特征的全部的惟一取值 uniqueVals=set(featValues) #遍历每个特征取值 for value in uniqueVals: #采用递归的方法利用该特征对数据集进行分类 #@bestFeatLabel 分类特征的特征标签值 #@dataSet 要分类的数据集 #@bestFeat 分类特征的标称值 #@value 标称型特征的取值 #@subLabels 去除分类特征后的子特征标签列表 myTree[bestFeatLabel][value]=createTree(splitDataSet\ (dataSet,bestFeat,value),subLabels) return myTree
须要说明的是,此时参数dataSet为列表的引用,咱们不能在函数中直接对列表进行修改,可是在书中代码中有del(labels[bestFeat])的删除列表某一列的操做,显然不可取,应该建立新的列表对象subLabels=labels[:],再调用函数 del(subLabels[bestFeat])
好了,接下来运行代码:
5 接下来,咱们能够经过决策树进行实际的分类了,利用构建好的决策树,输入符合要求的测试数据,比较测试数据与决策树上的数值,递归执行该过程直到叶子结点,最后将测试数据定义为叶子结点全部的分类,输出分类结果
决策树分类函数代码为:
#------------------------测试算法------------------------------ #完成决策树的构造后,采用决策树实现具体应用 #@intputTree 构建好的决策树 #@featLabels 特征标签列表 #@testVec 测试实例 def classify(inputTree,featLabels,testVec): #找到树的第一个分类特征,或者说根节点'no surfacing' #注意python2.x和3.x区别,2.x可写成firstStr=inputTree.keys()[0] #而不支持3.x firstStr=list(inputTree.keys())[0] #从树中获得该分类特征的分支,有0和1 secondDict=inputTree[firstStr] #根据分类特征的索引找到对应的标称型数据值 #'no surfacing'对应的索引为0 featIndex=featLabels.index(firstStr) #遍历分类特征全部的取值 for key in secondDict.keys(): #测试实例的第0个特征取值等于第key个子节点 if testVec[featIndex]==key: #type()函数判断该子节点是否为字典类型 if type(secondDict[key]).__name__=='dict': #子节点为字典类型,则从该分支树开始继续遍历分类 classLabel=classify(secondDict[key],featLabels,testVec) #若是是叶子节点,则返回节点取值 else: classLabel=secondDict[key] return classLabel
输入实例,经过分类函数获得预测结果,能够与实际结果比对,计算错误率
6 咱们说一个好的分类算法要可以完成实际应用的须要,决策树算法也不例外,一个算法好很差,仍是须要实际应用的检验才行,接下来咱们会经过一个实例来使用决策树预测隐形眼镜的类型
首先,咱们知道构建决策树是很是耗时的任务,即便很小的数据集,也要花费几秒的时间来构建决策树,这样显然耗费计算时间。因此,咱们能够将构建好的决策树保存在磁盘中,这样当咱们须要的时候,再从磁盘中读取出来使用便可。
如何进行对象的序列化操做,python的pickle模块足以胜任该任务,任何对象均可以经过pickle模块执行序列化操做,字典也不例外,使用pickle模块存储和读取决策树文件的代码以下:
#决策树的存储:python的pickle模块序列化决策树对象,使决策树保存在磁盘中 #在须要时读取便可,数据集很大时,能够节省构造树的时间 #pickle模块存储决策树 def storeTree(inputTree,filename): #导入pickle模块 import pickle #建立一个能够'写'的文本文件 #这里,若是按树中写的'w',将会报错write() argument must be str,not bytes #因此这里改成二进制写入'wb' fw=open(filename,'wb') #pickle的dump函数将决策树写入文件中 pickle.dump(inputTree,fw) #写完成后关闭文件 fw.close() #取决策树操做 def grabTree(filename): import pickle #对应于二进制方式写入数据,'rb'采用二进制形式读出数据 fr=open(filename,'rb') return pickle.load(fr)
这里,文件的写入操做为'wb'或'wb+',表示以byte的形式写入数据,相应'rb'以byte形式读入数据
接下来,咱们将经过隐形眼镜数据集构建决策树,从而预测患者须要佩戴的隐形眼镜的类型,步骤以下:
(1)收集数据:文本数据集'lenses.txt'
(2)准备数据:解析tab键分隔开的数据行
(3)分析数据:快速检查数据,确保正确地解析数据内容
(4)训练算法:构建决策树
(5)测试算法:经过构建的决策树比较准确预测出分类结果
(6)算法的分类准确类知足要求,将决策树存储下来,下次须要时读取使用
其中,解析文本数据集和构建隐形眼镜类型决策树的函数代码以下:
#------------------------示例:使用决策树预测隐形眼镜类型---------------- def predictLensesType(filename): #打开文本数据 fr=open(filename) #将文本数据的每个数据行按照tab键分割,并依次存入lenses lenses=[inst.strip().split('\t') for inst in fr.readlines()] #建立并存入特征标签列表 lensesLabels=['age','prescript','astigmatic','tearRate'] #根据继续文件获得的数据集和特征标签列表建立决策树 lensesTree=createTree(lenses,lensesLabels) return lensesTree
固然,咱们还能够经过python的matplotlib工具绘制出决策树的树形图,因为内容太多,就不一一讲解啦
接下来,补充一下决策树算法可能或出现的过分匹配(过拟合)的问题,当决策树的复杂度较大时,极可能会形成过拟合问题。此时,咱们能够经过裁剪决策树的办法,下降决策树的复杂度,提升决策树的泛化能力。好比,若是决策树的某一叶子结点只能增长不多的信息,那么咱们就可将该节点删掉,将其并入到相邻的结点中去,这样,下降了决策树的复杂度,消除过拟合问题。