决策树算法听起来很陌生,但是一提到问问题游戏,大家肯定都玩过,游戏规则就是在脑海中想一个事物,其他人向此人提问题,回答也只能是是和否,从而不断缩小范围逼近正确答案。决策树就是这个原理。上一章介绍的K近邻算法能完成多分类问题,但是无法给出数据的内在含义,而决策树算法的优势就在于结果可视化,数据型式非常容易理解。如下图所示就是决策树的流程图型式。
优点:计算复杂度不高,结果易于理解,对中间值缺失不敏感,可以处理不相关特征数据。
缺点:可能会产生过度匹配问题。
适用范围:数值型和标称型
下面,我们就要进入决策树算法的大门了,首先就遇到了一个问题,我们要对一组数据进行分类,按照什么特征进行分类,怎样评判分类的好与坏。这貌似是个大问题,好在已经有人帮我们解决了,也就是信息论之父克劳德香浓提出的香农熵的概念。
香农熵:就是衡量一组数据混乱度的量。
比如说一群动物中基本都是狗,其中混有几只猫,则数据的混乱度很小,数据很有序,而如果一群动物中狗和猫的数目差不多,则这组数据的混乱度很大,很无序。而如何用式子来衡量这个现象呢,香浓直接给了我们一个公式:
S代表整个集合,Pi代表第i类出现的概率,m代表共有m个类别。如果一群动物中有29个猫与71个狗,则P1=29/100,P2=71/100。下面要讨论的一个问题是,按哪种特征分类是好的,试想一下,我们分类的目的就是想把每个类别的样本彻底分开,降低样本的香农熵。所以我们是向着熵减的方向进行,所以在每次分类的时候按照每个类别进行分类,找出使数据熵最小的特征。下面就是具体实现的代码。
计算每种分类的香农熵
def calcshannonEnt(dataset):
numEnt = len(dataset)
labelCounts1 = {}
for featVec in dataset:
currentLabel = featVec[-1]
if currentLabel not in labelCounts1.keys(): labelCounts1[currentLabel] = 0
labelCounts1[currentLabel] += 1
ShannonEnt = 0.0
for key in labelCounts1:
prob = float(labelCounts1[key])/numEnt
ShannonEnt -= prob *log(prob,2)
return ShannonEnt,labelCounts1
对数据进行分类
def splitDataSet(dataSet,axis,value):
retData = []
for featvec in dataSet:
if featvec[axis] == value:
reducedFeatVec = featvec[:axis] #将axis前的的值返回给变量
reducedFeatVec.extend(featvec[axis+1:]) #将axis后的值接到后面返回给变量,所以用extend函数。也就是将原来数组中的axis那一组值去掉。
retData.append(reducedFeatVec) #将分类后的数组赋给数组
return retData
计算每种分类的香农熵,并返回最大香农熵分类
def chooseBestFeatureToSplit(dataSet):
numFeature = len(dataSet[0])-1 #计算数据特征的个数
bestEntropy = calcShannonEnt(dataSet) #计算这组数据的香农熵
bestInfoGain = 0.0;bestFeature = -1
for i in range(numFeature): #遍历每个特征进行分类
featList = [example[i] for example in dataSet]#取该特征的所有取值生成一个数组
uniqueVals = set(featList) #将该特征的所有取值放到一个集合里(也就是去除重复值的作用)
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
多数决策代码,类似于K近邻中的classfy0程序,就是当分类到最后还没有完全分开的话,就取个数多的结果。
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),reversed = True)
return sortedclassCount[0][0]
创建决策树代码
def createTree(dataSet,label):
classList = [example[-1] for example in dataSet] #创建数组存放所有标签值
if classList.count(classList[0]) == len(classList): #判断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[:] #将labels赋给sublabels,此时的labels已经删掉了用于分类的特征的标签
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels) #递归创建树直到分类达到上述判断条件
return myTree
至此,已完成决策树算法的全部代码,下面我们将用数据来测试一下该代码。
数据一:
dataSet =
[[1,1,’yes’],
[1,1,’yes’],
[1,0,’no’],
[0,1,’no’],
[0,1,’no’]]
labels = [‘no surfacing’,’flippers’]
分类结果:
{‘no surfacing’: {0: ‘no’, 1: {‘flippers’: {0: ‘no’, 1: ‘yes’}}}}
数据二:
dataSet =
[[1,1,’yes’],
[1,1,’yes’],
[1,1,’no’]
[1,0,’no’],
[0,1,’no’],
[0,1,’no’]]
labels = [‘no surfacing’,’flippers’]
分类结果:{‘no surfacing’: {0: ‘no’, 1: {‘flippers’: {0: ‘no’, 1: ‘yes’}}}}
可以看出,数据一可以通过决策树将数据完全分开,但是数据二在分到最后一个特征时仍无法将数据完全分开,故最后调用了majorityCnt函数选择个数多的结果。由结果可以看出,以多重字典的表达形式很难懂,很晦涩,所以下面将介绍如何用matplotlib绘制树形图的方式表达结果。