在构造决策树时,我们需要解决的第一个问题就是,当前数据集上哪个特征在划分数据分类时起决定性作用。为了找到决定性的特征,划分出好的结果,我们必须评估每个特征。
构建决策树的一般步骤是:先找到一个划分结果最好的特征,作为根节点,也就是最上面的一个节点,然后数据集就被划分成了几个数据子集,所谓数据子集也就是其他特征集合。这些数据子集分布在第一个决策点的子节点上。如果某个子节点的数据属于同一类型,则无需再进行分类,即无需进一步对数据集进行分割。如果数据子集内的数据不属于同一类型,则需要重复划分数据子集的过程。
创建分支的伪代码函数createBranch()如下所示:
上面的伪代码createBranch是一个递归函数,在倒数第二行直接调用了它自己。后面我们将把上面的伪代码转换为Python代码,这里我们需要进一步了解算法是如何划分数据集的。
决策树的一般流程:
划分数据集的大原则是:将无序的数据变得更加有序。我们可以使用多种方法划分数据集, 但是每种方法都有各自的优缺点。组织杂乱无章数据的一种方法就是使用信息论度量信息,信息论是量化处理信息的分支科学。我们可以在划分数据之前使用信息论量化度量信息的内容。
集合信息的度量方式称为香农熵或者简称为熵,这个名字来源于信息论之父克劳德·香农。
熵定义为信息的期望值,在明晰这个概念之前,我们必须知道信息的定义。如果待分类的事 务可能划分在多个分类之中,则符号xi的信息定义为 :
Pxi代表信息发生的可能性,发生的可能性越大,概率越大,则信息越少,通常将这种可能性叫为不确定性,越有可能则越能确定则信息越少。
为了计算熵,我们需要计算所有类别所有可能值包含的信息期望值,也就是信息熵了。通过下面的公式得到:
其中n是分类的数目。
信息增益就是用整体的信息熵减掉以按某一特征分裂后的条件熵,结果越大,说明这个特征越能消除不确定性。所以获得信息增益高的特征就是好的选择。
H(D)是总的信息熵,也就是所有样本的分类结果的信息熵。H(D|A)是在某个特征条件下的分类结果的信息熵。举个例子,如下图,关于动物的分类样本:
此样本集有“饮食习性”、“胎生动物”、“水生动物”、“会飞”四个 属性可作为特征条件,而“哺乳动物”作为样本的分类属性,有“是”与 “否”两种分类,也即正例与负例。共有14个样本,其中8个正例,6个反 例,设此样本集为 S,则分裂前的熵值为
现在我要以’饮食习性’这个特征划分样本集的话,会有三个节点,如下图:
接下来计算以’饮食习性’这个特征划分样本集后的信息熵。
“饮食习性”为“肉食动物”的分支中有3个正例、5个反例,其熵值为:
同理,计算出“饮食习性”分类为“草食动物”的分支与分类为“杂食动 物”的分支中的熵值分别为
设“饮食习性”属性为Y,由此可以计算得出,作为分支属性进行分裂之后的 信息增益为
通过这个案例应该能很好理解信息增益的计算过程了
下面我们将学习如何使用Python计算信息熵。
首先,我们的数据集是有特征和分类结果组成。这里我们先自己创建一个数据用于测试:
def createDataSet():
'''
自己创建数据集,用于测试
'''
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['no surfacing','flippers']
return dataSet, labels
注意代码中的labels并不是我们的分类结果标签,而是给特征取得名字,方便理解。
接着我们看一下如何利用上面的测试集来计算信息熵,数据集中既有特征,又有分类结果(也就是标签),但是我们计算信息熵时,只需要计算每个类标签出现的概率,这里概率计算是标签出现的次数除以总标签数量,计算出各个标签出现的概率就可以利用我们的公式进行信息熵的计算了,代码如下:
def calcShannonEnt(dataSet):
'''
计算信息熵
'''
numEntries = len(dataSet) # 获取样本数量
labelCounts = {} # 初始化标签字典,用于统计存储不同分类的数量
for featVec in dataSet: # 遍历每行数据
currentLabel = featVec[-1] # 获取当前行的标签,也就是分类,在最后一列
if currentLabel not in labelCounts.keys(): # 如果这个标签第一次出现,就在标签字典中初始化数量为0
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 # 否则给和这个标签数量加1
shannonEnt = 0.0 # 初始化信息熵为0
for key in labelCounts: # 遍历所有标签
# 计算每个种类的熵,并相加(其实是先算正的,然后相减)
prob = float(labelCounts[key])/numEntries
shannonEnt -= prob * log(prob,2)
return shannonEnt
下面我们来测试一下:
#test1
myDat, labels = createDataSet()
print(calcShannonEnt(myDat))
#运行结果为0.9709505944546686
熵越高,则混合的数据也越多,我们可以在数据集中添加更多的分类,观察熵是如何变化的。
得到熵之后,我们就可以按照获取大信息增益的方法划分数据集,下一节我们将具体学习 如何划分数据集以及如何度量信息增益。
另外还有C4.5和CART决策树算法分别用了信息增益率和基尼指数。后续再做讲解介绍。
划分数据集的说法,在看到后面代码之前,书中说的不是很明白,其实就是给定一个数据集和特征,然后我需要按照这个特征指定的值去得到标签还有此特征值下的其他特征的数据集。
还用上面那个动物分类举例子。假如我要计算’饮食习性’这个特征的信息增益,因为’饮食习性’有三种可能,那么每一种可能我都需要再去计算信息熵。
那么划分数据集的过程就是:现在我要计算’饮食习性’为肉食动物的信息熵。则我需要在数据集中先找到’饮食习性’为肉食动物的所有行,得到数据集1。因为我已经用了’饮食习性’作为上一个节点,所以要把’饮食习性’这列特征删除,得到数据集2。这里的数据集2就是我们划分的数据集,他包含了标签和除’饮食习性’的其他特征数据。其中标签用来计算信息熵,其他特征用来继续划分数据集。
然后再计算’饮食习性’为其他情况的信息熵,我们又需要划分一次数据集。然后用划分好的数据集计算信息熵。全部计算完成后,则可以用公式计算’饮食习性’的信息增益了。
下面看一下代码:
def splitDataSet(dataSet, axis, value):
'''
划分数据集,即根据每个特征的不同,去划分数据集
比如第一列特征有0、1两个值,则分别获取为0时的所有数据集,为1时所有的数据集
获取的数据集中,把这个特征去除了,保留了其他特征和结果标签,
其中保留的特征用于下一次筛选最好的特征,标签用来计算信息熵
'''
retDataSet = [] # 初始化根据某个特征值划分得到的数据集
for featVec in dataSet: # 遍历每行数据集
if featVec[axis] == value: # 如果这个特征等于传入的特征值,则获取该特征值下的数据
reducedFeatVec = featVec[:axis] # 获取这个特征列的前面的所有特征列
reducedFeatVec.extend(featVec[axis+1:]) # extend加上这个特征列后面的所有列
retDataSet.append(reducedFeatVec) # 把获取的当前行数据加入到总数据集列表中
return retDataSet
代码使用了三个输入参数:待划分的数据集、划分数据集的特征、特征值。
我们来测试一下效果:
#test2
myDat, labels = createDataSet()
splitDat = splitDataSet(myDat,0,1) #这里的意思就是我要以第1列特征为1时划分数据集
print(splitDat)
# 划分结果为
[[1, 'yes'], [1, 'yes'], [0, 'no']]
接下来我们将遍历整个数据集,循环计算信息熵和splitDataSet()函数,找到好的特征划分方式。熵计算将会告诉我们如何划分数据集是好的数据组织方式。
下面寻找最优划分特征和划分数据集的函数:
def chooseBestFeatureToSplit(dataSet):
'''
选取特征,划分数据集,计算最好的划分数据集的特征
'''
numFeatures = len(dataSet[0]) - 1 # 去除最后一列标签,统计特征数量
baseEntropy = calcShannonEnt(dataSet) # 计算总样本的信息熵
# 初始化信息增益和最好的特征列
bestInfoGain = 0.0
bestFeature = -1
for i in range(numFeatures): # 遍历所有特征
# 获取当前特征的所有特征值,比如第一列的特征的特征值可能为[1,1,0,1,0]
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) # 利用集合方法去重,得到所有可能的特征值,比如去重后为[1,0]
newEntropy = 0.0 # 初始化熵为0
for value in uniqueVals: # 遍历当前特征所有可能的值,即遍历[1,0]
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
下面我们测试一下代码:
#test3
myDat, labels = createDataSet()
bestfeature = chooseBestFeatureToSplit(myDat)
print(bestfeature)
# 结果如下,所以第一列特征是我们目前数据集中划分标签最好的特征
0
目前我们已经学习了从数据集构造决策树算法所需要的子功能模块,其工作原理如下:得到原始数据集,然后基于好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集。 递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有 相同的分类。如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。任何到达叶子节 点的数据必然属于叶子节点的分类,参见下图:
第一个结束条件使得算法可以终止,我们甚至可以设置算法可以划分的大分组数目。有一种情况是:如果数据集已经处理了所有属性,但是类标签依然不是唯一的,此时我们需要决定如何定义该叶子节点,在这种情况下,我们通常会采用多数表决的方法决定该叶子节点的分类。 下面我们定义一个函数完成这个工作:
def majorityCnt(classList):
'''
多数表决方法决定分类
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]
接下来我们就可以开始用上面封装的几个函数来构建我们的决策树了。整体思路是:1.首先递归函数要写递归结束的条件,这里有两个,一个是数据集标签相同,一个是遍历完了所有特征集;2.然后写递归的过程,首先利用chooseBestFeatureToSplit函数找到当前数据集的最好特征;3.把最好的特征添加到树结构中,这里树结构形式是{bestFeat:{ }};4.获取最好特征的所有特征值,把特征值加到树结构中,形式如{bestFeat:{feat1:{ }, feat2:{ }}}; 5.利用feat1和feat2,使用函数splitDataSet((dataSet, bestFeat, feat1))划分的数据集再递归重复此过程,直到结束。代码如下:
def createTree(dataSet,labels):
'''
创建树
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 = chooseBestFeatureToSplit(dataSet) # 选择当前划分类别最好的特征
bestFeatLabel = labels[bestFeat] # 最好的特征标签加到标签列表里
myTree = {bestFeatLabel:{}} # 初始化tree
subLabels = labels[:] # 复制类标签,为了每次调用createTree()函数不改变原始的列表标签内容
del(subLabels[bestFeat]) # 从特征标签列表中删掉已经计算后的标签名称
featValues = [example[bestFeat] for example in dataSet] # 获取目前最好特征的所有特征值
uniqueVals = set(featValues) # 去重获取所有可能特征值
for value in uniqueVals: # 遍历上面所有特征值
# 添加树结构,调用递归
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
return myTree
上面函数使用两个输入参数:数据集和标签列表。标签列表包含了数据集中所有特征的标签,算法本身并不需要这个变量,但是为了给出数据明确的含义,我们将它作为一个输入参数提供。比如我们createDataSet()创建的数据集的labels。
我们来测试一下代码:
#test4
myDat, labels = createDataSet()
mytree = createTree(myDat, labels)
print(mytree)
# 结果如下
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
变量myTree包含了很多代表树结构信息的嵌套字典,从左边开始,第一个关键字no surfacing是第一个划分数据集的特征名称,该关键字的值也是另一个数据字典。第二个关键字是no surfacing特征划分的数据集,这些关键字的值是no surfacing节点的子节点。这些值可能是类标签,也可能是另一个数据字典。如果值是类标签,则该子节点是叶子节点;如果值是另一个数据字典,则子节点是一个判断节点,这种格式结构不断重复就构成了整棵树。本节的例子中,这棵树包含了3个叶子节点以及2个判断节点。
依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时, 需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子节点;后将测试数据定义为叶子节点所属的类型。 代码如下:
def classify(inputTree,featLabels,testVec):
'''
使用构建好的决策树进行分类
inputTree:构建好的树
featLabels:特征标签,便于知道该标签在数据集中的列索引位置
testVec:测试数据
'''
firstStr = list(inputTree.keys())[0] # 获得第一个树节点标签
secondDict = inputTree[firstStr] # 获取第一个节点下面的树结构
featIndex = featLabels.index(firstStr) # 获取读一个树节点特征的列索引位置
key = testVec[featIndex] # 获取测试数据在当前索引下(当前特征)的特征值
valueOfFeat = secondDict[key] # 获得第二层树节点下的树结构
# 如果第二层是字典结构,则还有叶子结构,继续递归寻找下个节点
if isinstance(valueOfFeat, dict):
classLabel = classify(valueOfFeat, featLabels, testVec)
# 如果没有树结构,即到达了叶子节点,则直接返回改叶子节点额值
else:
classLabel = valueOfFeat
return classLabel
该函数也是一个递归函数,在存储带有特征的数据会面临一个问题:程序无法确定特征在数据集中的位置,例如前面例子的第一个用于划分数据集的特征是no surfacing属性,但是在实际数据集中该属性存储在哪个位置?是第一个属性还是第二个属性? 特征标签列表将帮助程序处理这个问题。使用index方法查找当前列表中第一个匹配firstStr变量的元素 。然后代码递归遍历整棵树,比较testVec变量中的值与树节点的值,如果到达叶子节点,则返回当前节点的分类标签。
下面来测试一下代码:
#test5
myDat, labels = createDataSet()
mytree = createTree(myDat, labels)
result = classify(mytree, labels,[1, 0])
print(result)
# 结果为 no
构造决策树是很耗时的任务,即使处理很小的数据集,如前面的样本数据,也要花费几秒的时间,如果数据集很大,将会耗费很多计算时间。然而用创建好的决策树解决分类问题,则可以很快完成。因此,为了节省计算时间,好能够在每次执行分类时调用已经构造好的决策树。为 了解决这个问题,需要使用Python模块pickle序列化对象。序列化对象可以在磁 盘上保存对象,并在需要的时候读取出来。任何对象都可以执行序列化操作,字典对象也不例外。
def storeTree(inputTree,filename):
'''
存储树结构
'''
import pickle
fw = open(filename,'wb')
pickle.dump(inputTree,fw)
fw.close()
def grabTree(filename):
'''
读取树结构
'''
import pickle
fr = open(filename,'rb')
return pickle.load(fr)
决策树分类器就像带有终止块的流程图,终止块表示分类结果。开始处理数据集时,我们首先需要测量集合中数据的不一致性,也就是熵,然后寻找优方案划分数据集,直到数据集中的所有数据属于同一分类。ID3算法可以用于划分标称型数据集。构建决策树时,我们通常采用递归的方法将数据集转化为决策树。一般我们并不构造新的数据结构,而是使用Python语言内嵌的数据结构字典存储树节点信息。
还有其他的决策树的构造算法,流行的是C4.5和CART,后面将会介绍。