决策树是机器学习中最基本的一种分类算法,主要用来根据训练数据集的特质来一步步往下划分,直到所有特质(属性)都已经使用完毕或者已得到的划分数据集中的数据全部为同一类型的数据。首先我们来看看信息熵和信息增益的定义:
信息:
其中p(xi)是当前在数据集中选择分类为xi的数据集的概率
信息熵:
信息熵表示数据集中数据类别的混乱程度,信息熵越小,表示这个数据集的数据纯度越高,反之就是越低。例如,假设有下面这一训练数据集:
[[1,1,'yes'],[1,0,'yes'],[0,0,'yes']],假设数据集中单个信息的前两列表示属性取值,最后一列表示类别,则这个数据集中所有信息类别相同,因此该数据集的的信息熵为0.
下面给出计算信息熵的python代码:
def calcShannonEnt(dataSet):
numEntries = len(dataSet)
labelCount = {}
for featVec in dataSet:
currentLabel = featVec[-1] //取出信息的类别
if currentLabel not in labelCount.keys():
labelCount[currentLabel] = 0
labelCount[currentLabel] += 1
shannonEnt = 0.0
for key in labelCount:
prob = float(labelCount[key])/numEntries
shannonEnt -= prob*log(prob,2)
return shannonEnt
前面我们说过,决策树主要是使用属性对数据集进行划分来,从而实现分类策略。因此关键在于如何选择属性来划分数据集。划分数据集合的最大原则就是将无序的数据集变得更加有序,在划分数据集之前之后信息发生的变化称为信息增益,信息增益越高,说明当前对数据集的划分就越佳,划分带来的纯度(也可以理解为类别一致性)提升也就越高。
信息增益:
因此我们现在的主要目标就是循环比较使用数据集中的属性来划分数据集以获得最佳的划分方式。python实现代码如下:
def chooseBestFeaturToSplit(dataSet):
numFeatures = len(dataSet[0])-1 # last column is used to specify the type.
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain = 0.0; bestFeature = -1
for i in range(numFeatures):
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) # fliter the repeat value
newEntropy = 0.0 # save the sum of all ShannonEnt of the splitedDataSet
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value) #using the feature to split the dataset
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob*calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy
if infoGain>bestInfoGain:
bestInfoGain = infoGain
bestFeature = i
return bestFeature # the best feature to split the dataSet that can get the bestInfoGain
通过这个函数,我们可以给定一个数据集,循环计算根据不同属性来划分当前数据集能获得最大信息增益,并返回当前可用来划分数据集的最佳属性
有了上面的基础,我们就可以开始构造一个完整的决策树了,代码如下:
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 = chooseBestFeaturToSplit(dataSet)
bestFeatLabel = labels[bestFeat] # pick up the feature's label
myTree = {bestFeatLabel:{}}
# delete the selected feature. if all feature have been selected,the labels will be empty
del(labels[bestFeat])
featValue = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValue)
for value in uniqueVals:
subLabels = labels[:] #copy the labels to sent to the function.
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)
return myTree
在这段代码中,第一行主要是获得数据集中的所有类别。由于构造一个决策树是一个递归的过程,因此我们必须定义递归出口,这里有两种情况:
1.当前节点所有的类别一致,此时该节点可以标记为叶节点
2.当前已经没有属性可以用来划分数据集了,此时只能通过投票来决定将该节点标记为何种类别的叶节点。(主要是通过多胜少的机制,详细代码就不贴了)
为什么这里判断当前数据集没有属性可以划分使用 DataSet[0]==1 来判断呢,因为对数据集的划分函数如下:
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
其中axis是属性在信息中的index值,value表示属性值,因为每次根据属性划分数据集时,返回的都是去除了当前属性列,其他列取值和原数据集中信息相同的数据集,因此当所有属性都已经用来划分了,此时,数据集中的信息只有一列,那就是最后一列,代表着当前信息的类别。因此DataSet[0]==1,表示当前节点可以标记为叶节点了。
接下来继续看CreateTree函数,如果两种出口都未执行,那么此时说明当前节点还有继续往下划分的必要。因此这里使用我们上面的选择最有属性划分的方法chooseBestFeaturToSplit来选择最佳划分属性。labels存储了所有属性的名称,因此bestFeatLabel就是当前最佳划分属性名。我们这里用一个字典myTree来存储决策树的信息. 接着我们求出数据集DataSet中所有信息在该属性上的取值,为了避免过滤掉重复的属性值,将List转为Set.。然后对Set中的每个值来划分当前数据并递归执行CreateTree
方法。整个方法执行完成,就构造一颗决策树。
下面,我们使用上面给出的决策树构造方法来构造一颗决策树,并预测一条信息的类别。首先给出一个数据集定义:
dataSet = [
['yes','no','yes','no','no','fish'],
['no','no','yes','no','yes','not fish'],
['no','yes','no','yes','no','fish'],
['yes','no','no','yes','yes','not fish'],
['yes','no','no','yes','no','fish'],
['yes','yes','no','yes','yes','fish'],
]
labels = ['no surfacing','flippers','has scale','breth by lungs','vivipation']
我们使用createTree方法,可以根据当前的数据集构造一个决策树:
decisionTree = createTree(myData,labels)
打印decisionTree , 我们就可以看到如下信息:
{'vivipation': {'yes': {'flippers': {'yes': 'fish', 'no': 'not fish'}}, 'no': 'fish'}}
对比上面数据集定义,此字典对应的决策树和数据集是相符的,然而光是构造出决策树是不够的,我们还必须能够运用决策树来进行测试数据的预测,下面给出预测代码:
# using the trained decisionTree to classify the testData
def classify(decisionTree,featLabels,testData):
currentFeature = decisionTree.keys()[0]
node = decisionTree[currentFeature] # the node may be the leaf or the branch
index = featLabels.index(currentFeature)
classLabel = ''
for value in node.keys():
if testData[index] == value:
if type(node[value]).__name__ == 'dict':
classLabel = classify(node[value],featLabels,testData)
else:
classLabel = node[value]
return classLabel
decision = classify(decisionTree,labels,['no','no','yes','no','no'])
执行这条代码,显示decison为‘fish’,而这条信息是决策树中没有的,也就是说决策树根据训练数据可以预测测试数据的类型。
其实,我们也可以想到,由于决策树是根据当前数据集来构造的,假如数据集过小,那么很可能将一些并不普遍的特性作为划分数据集的属性,导致预测结果和实际结果并不匹配,这其实就是发生了过拟合。预防过拟合有很多措施,主要是通过避免相关节点的进一步展开来进行的,这里就不再多讲。