决策树的每一次判定都是对某一属性的测试,每个测试的结果或是导出最终结论,或是导出进一步的判定问题。决策的最终结论则对应最终的判定结果。
一般的,一棵决策树包含一个根结点,若干内部结点和若干个叶结点:
- 每个叶结点对应于一个决策结果,存放一个类别;
- 每个非叶结点表示一个特征属性测试;
- 每个分支代表这个特征属性在某个域上的输出;
- 每个结点包含的样本集合通过属性测试被划分到子结点中;根结点包含样本全集;
决策树的构造:
构造决策树的关键是——当前数据集上哪个特征在划分数据分类时起决定性作用。为了找到决定性的特征,划分出最好的结果,我们要评估每个特征。完成评估测试后,原始数据集(根结点)就被划分为几个数据子集。这些数据子集会分布在第一个决策点的所有分支上。如果某个分支下的数据属于同一类型,则无需进一步对该数据子集进行分割。如果数据子集内的数据不属于同一类型,则需要重复划分数据子集的过程(即对当前数据子集再划分)。划分数据子集的算法和划分原始数据集的方法相同,直到所有具有相同类型的数据均在一个数据子集内。
伪代码如下:
def creatBranch():
检测数据集中的每个子项是否属于同一分:
if so:
return 类标签
else:
寻找划分数据集的最好特征
划分数据集
创建分支节点
for 每个划分的子集:
递归调用createBranch并增加返回结果到分支节点中
return 分支节点
划分选择:
划分选择,即选择最优划分属性。属性划分的目的是让各个划分出来的子节点尽可能“纯度”越高,即该数据子集中的样本尽可能属于同一类别。主要有以下三种算法:ID3,C4.5和CART。本文使用ID3算法。
计算信息熵(香农熵):熵即为信息的期望,信息定义为-log2p(xi),则数据集D的熵为:Ent(D)=-∑(i=1,n)log2p(xi)
"""计算信息熵"""
def calcShannonEnt(dataSet):
numEntries=len(dataSet) #数据集中样本(实例)的总数 len(numpy.array)输出array的第一维中的个数,这里就是样本的个数
labelCounts={} #样本标记字典初始化
#为所有可能的分类创建字典
for featVec in dataSet:
currentLabel=featVec[-1] #字典的键值就是样本矩阵的最后一列数值(即样本标签)
if currentLabel not in labelCounts.keys(): #如果当前键值不在字典中,则扩展字典并将该键值存入
labelCounts[currentLabel]=0
labelCounts[currentLabel]+=1 #然后记录当前类别出现的次数
shannonEnt=0.0 #初始化信息熵
#Ent=-∑pk*log2pk
for key in labelCounts:
prob=float(labelCounts[key])/numEntries
shannonEnt-=prob*log(prob,2) #以2为底求对数
return shannonEnt
计算信息增益:假定离散属性a有V个可能的取值{a1,a2,..,av},若使用a来对样本集D进行划分,会产生V个分支节点,其中第v个分支节点包含了D中所有在属性a上取值为av的样本,记为Dv。则Dv的信息熵为Ent(Dv),考虑到不同的分支结点所包含的样本数不同,给该分支结点赋予权重|Dv|/|D|,于是可以计算属性a对样本集D进行划分所获得的信息增益为:
Gain(D,a)=Ent(D)-∑(v=1,V)|Dv|/|D|*Ent(Dv)
信息增益越大,意味着使用属性a来进行划分所获得的“纯度提升”越大。根据信息增益进行特征划分的代码如下:
"""按照给定特征划分数据集"""
def splitDataSet(dataSet,axis,value): #三个输入参数:待划分的数据集,划分数据集的特征(即用第几个特征划分),需要返回的特征的值(即划分条件)
retDataSet=[]
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 #样本的特征数,dataSet[0]表示第一个样本,因为包括一个标签所以要减一
baseEntropy=calcShannonEnt(dataSet) #计算数据集信息熵
bestInfoGain=0.0 #初始化最优信息增益
bestFeature=-1 #初始化最好的特征(下标)
for i in range(numFeatures):
#创建为一个分类标签列表
featList=[example[i] for example in dataSet]
uniqueVals=set(featList) #用featList,set()函数创建一个无序不重复元素集
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 #计算信息增益
#选择最好的信息增益
if(infoGain>bestInfoGain):
bestInfoGain=infoGain
bestFeature=i
return bestFeature
递归创建决策树:
递归结束的条件有三个:
- 当前结点包含的样本全属于同一类别,无需划分;
- 当前属性集为空,或者所有样本在所有属性上取值相同,无法划分;
- 当前结点包含的样本集合为空,不能划分;
在第二种情形下,我们把当前结点标记为叶结点,并将其类别设定为该结点所含样本最多的类别。第三种情形下,把当前结点标记为叶结点,但其类别设定为其父节点所含样本最多的类别。
实际上,递归结束的条件简单地说就是遍历完所有划分数据集的属性,或者每个分支下的所有样本实例都具有相同的分类。如果所有样本实例具有相同的分类,则得到一个叶子节点或者终止块。条件2意味着,程序处理了所有的属性后,但类标签依然不是唯一的,这时对叶结点进行投票法,将其类别设定为该结点所含样本最多的类别。
代码如下:
"""多数表决(投票法)决定叶子节点的分类,用于已经划分了所有属性,但叶子节点中样本仍然完全属于同一类别"""
def majorityCnt(classList):
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] #初始化样本标签列表
#递归返回条件1:所有样本属于同一类别
if classList.count(classList[0])==len(classList): #判断整个数据集的类别是否属于同一类别,若属于同一类别则停止划分
return classList[0]
#递归返回条件2,特征全部遍历完,没有将样本分为同一个类别
if len(dataSet[0])==1: #数据集中不再有样本特征后停止遍历
return majorityCnt(classList) #返回包含样本数最多的类别
#若特征没有全部遍历完,选择最优特征划分
bestFeat=chooseBestFeatureToSplit(dataSet) #当前数据集选取的最优特征(选择bestFeat来划分数据集)这里是一个索引下标
bestFeatLabel=labels[bestFeat] #用索引下标对应着找出相应的特征标签
#开始创建树,myTree保存了树的所有信息
myTree={bestFeatLabel:{}} #用字典形式存储返回值
del(labels[bestFeat]) #将分类过了的分类标签删除
#创建树,遍历当前选取的特征包含的所有属性值
featValues=[example[bestFeat] for example in dataSet]
uniqueValues=set(featValues) #该特征包含的所有的属性值放入集合,属性值是不重合的
#在每一个属性值上递归调用createTree
for value in uniqueValues:
subLabels=labels[:] #保存labels数据,防止因为引用方式传递导致原始数据变化
#递归生成决策树
myTree[bestFeatLabel][value]=createTree(splitDataSet(dataSet,bestFeat,value),subLabels) #得到的返回值插入字典myTree
return myTree