决策树(Decision Tree)算法是一种基本的分类与回归方法,是最经常使用的数据挖掘算法之一。书中只讨论用于分类的决策树。
决策树模型呈树形结构,在分类问题中,表示基于特征对实例进行分类的过程。它可以认为是 if-then 规则的集合,也可以认为是定义在特征空间与类空间上的条件概率分布。
决策树的定义:
分类决策树模型是一种描述对实例进行分类的树形结构。决策树由结点(node)和有向边(directed edge)组成。结点有两种类型:内部结点(internal node)和叶结点(leaf node)。内部结点表示一个特征或属性(features),叶结点表示一个类(labels)。
用决策树对需要测试的实例进行分类:从根节点开始,对实例的某一特征进行测试,根据测试结果,将实例分配到其子结点;这时,每一个子结点对应着该特征的一个取值。如此递归地对实例进行测试并分配,直至达到叶结点。最后将实例分配到叶结点的类中。
决策树学习通常包括 3 个步骤:特征选择、决策树的生成和决策树的修剪。
一个叫做 "二十个问题" 的游戏,游戏的规则很简单:参与游戏的一方在脑海中想某个事物,其他参与者向他提问,只允许提 20 个问题,问题的答案也只能用对或错回答。问问题的人通过推断分解,逐步缩小待猜测事物的范围,最后得到游戏的答案。
书中提到一个邮件分类系统,大致工作流程如下:
首先检测发送邮件域名地址。如果地址为 myEmployer.com, 则将其放在分类 "无聊时需要阅读的邮件"中。
如果邮件不是来自这个域名,则检测邮件内容里是否包含单词 "曲棍球" , 如果包含则将邮件归类到 "需要及时处理的朋友邮件",
如果不包含则将邮件归类到 "无需阅读的垃圾邮件" 。
在构造决策树时,第一个需要解决的问题就是,如何确定出哪个特征在划分数据分类是起决定性作用,或者说使用哪个特征分类能实现最好的分类效果。这样,为了找到决定性的特征,划分川最好的结果,我们就需要评估每个特征。当找到最优特征后,依此特征,数据集就被划分为几个数据子集,这些数据自己会分布在该决策点的所有分支中。此时,如果某个分支下的数据属于同一类型,则该分支下的数据分类已经完成,无需进行下一步的数据集分类;如果分支下的数据子集内数据不属于同一类型,那么就要重复划分该数据集的过程,按照划分原始数据集相同的原则,确定出该数据子集中的最优特征,继续对数据子集进行分类,直到所有的特征已经遍历完成,或者所有叶结点分支下的数据具有相同的分类。
创建分支的伪代码函数createBranch()如下:
检测数据集中的所有数据的分类标签是否相同:
If so return 类标签
Else:
寻找划分数据集的最好特征(划分之后信息熵最小,也就是信息增益最大的特征)
划分数据集
创建分支节点
for 每个划分的子集
调用函数 createBranch (创建分支的函数)并增加返回结果到分支节点中
return 分支节点
特征选择在于选取对训练数据具有分类能力的特征。这样可以提高决策树学习的效率,如果利用一个特征进行分类的结果与随机分类的结果没有很大差别,则称这个特征是没有分类能力的。经验上扔掉这样的特征对决策树学习的精度影响不大。通常特征选择的标准是信息增益(information gain)或信息增益比,为了简单,本文章使用信息增益作为选择特征的标准。那么,什么是信息增益?
在划分数据集之前之后信息发生的变化成为信息增益,知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择。
在可以评测哪个数据划分方式是最好的数据划分之前,我们必须学习如何计算信息增益。集合信息的度量方式成为香农熵或者简称为熵(entropy),这个名字来源于信息论之父克劳德·香农。
如果看不明白什么是信息增益和熵,请不要着急,因为他们自诞生的那一天起,就注定会令世人十分费解。克劳德·香农写完信息论之后,约翰·冯·诺依曼建议使用”熵”这个术语,因为大家都不知道它是什么意思。
熵定义为信息的期望值。在信息论与概率统计中,熵是表示随机变量不确定性的度量。如果待分类的事务可能划分在多个分类之中,则符号xi的信息定义为
其中p(xi)是选择该分类的概率。
为了计算熵,我们需要计算所有类别所有可能值包含的信息期望值(数学期望),通过下面的公式得到:
其中n是分类的数目。熵越大,随机变量的不确定性就越大。
使用Python计算信息熵,创建名为tree.py的文件,代码如下:
"""
计算给定数据集的香农熵
"""
def calcShannonent(dataSet):
numEntries = len(dataSet) # 计算数据集总实例的总数
labelCounts = {} # 保存每个标签(Label)出现次数的字典
for featVec in dataSet: # 对每组特征向量进行统计
currentLabel = featVec[-1] # 提取标签(label)信息
if currentLabel not in labelCounts.keys(): # 如果标签(Label)没有放入统计次数的字典,添加进去
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 # 统计label 次数
shannonEnt = 0.0 # 经验熵(香农熵)
for key in labelCounts: # 计算香农熵
prob = float(labelCounts[key]) / numEntries # 选择该label的概率
shannonEnt -= prob * log(prob,2) # 利用相对应的公式计算
return shannonEnt
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
if __name__ == '__main__':
myDat,labels = createDataset()
print(myDat)
print(calcShannonent(myDat))
结果如下图:
在上面,我们已经说过,如何选择特征,需要看信息增益。也就是说,信息增益是相对于特征而言的,信息增益越大,特征对最终的分类结果影响也就越大,我们就应该选择对最终分类结果影响最大的那个特征作为我们的分类特征。
在讲解信息增益定义之前,我们还需要明确一个概念,条件熵。
熵我们知道是什么,条件熵又是个什么鬼?条件熵H(Y|X)表示在已知随机变量X的条件下随机变量Y的不确定性,随机变量X给定的条件下随机变量Y的条件熵(conditional entropy) H(Y|X),定义X给定条件下Y的条件概率分布的熵对X的数学期望:
同理,当条件熵中的概率由数据估计(特别是极大似然估计)得到时,所对应的条件熵成为条件经验熵(empirical conditional entropy)。
明确了条件熵和经验条件熵的概念。接下来,让我们说说信息增益。前面也提到了,信息增益是相对于特征而言的。所以,特征A对训练数据集D的信息增益g(D,A),定义为集合D的经验熵H(D)与特征A给定条件下D的经验条件熵H(D|A)之差,即
一般地,熵H(D)与条件熵H(D|A)之差成为互信息(mutual information)。决策树学习中的信息增益等价于训练数据集中类与特征的互信息。
通过上面讲到的信息增益公式得到划分数据集的最有特征,从而划分数据集,
"""
按照给定特征划分数据集
"""
def splitDataSet(dataSet,axis,valus):
"""
:param dataSet: 待划分的数据集
:param axis: 划分数据集的特征
:param valus: 需要返回的特征的值
:return:
"""
retDataSet = [] # 创建返回的数据集列表
for featVec in dataSet: # 遍历整个数据集
# axis列为value的数据集【该数据集需要排除index列】
# 判断axis列的值是否为valu
if featVec[axis] == valus:
reduceFeatVec = featVec[:axis] # 去掉axis特征
'''
extend和append的区别
music_media.append(object) 向列表中添加一个对象object
music_media.extend(sequence) 把一个序列seq的内容添加到列表中 (跟 += 在list运用类似, music_media += sequence)
1、使用append的时候,是将object看作一个对象,整体打包添加到music_media对象中。
2、使用extend的时候,是将sequence看作一个序列,将这个序列和music_media序列合并,并放在其后面。
music_media = []
music_media.extend([1,2,3])
print music_media
#结果:
#[1, 2, 3]
music_media.append([4,5,6])
print music_media
#结果:
#[1, 2, 3, [4, 5, 6]]
music_media.extend([7,8,9])
print music_media
#结果:
#[1, 2, 3, [4, 5, 6], 7, 8, 9]
'''
reduceFeatVec.extend(featVec[axis+1:]) # 将符合条件的特征添加到返回的数据集中
retDataSet.append(reduceFeatVec)
return retDataSet
接下来,我们再看选取最优特征的代码:
"""
选择最好的数据集划分方式
"""
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 # 特征数量
baseEntropy = calcShannonent(dataSet) # 计算香农熵
bestInfoGain = 0.0 # 信息增益
bestFeature = -1 # 最优特征的索引值
for i in range(numFeatures): # 遍历所有特征
featList = [example[i] for example in dataSet] # 获取dataset数据集第i个所有特征
uniqueVals = set(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 # 得到信息增益
print("第%d个特征的增益为%.3f" % (i, infoGain)) # 打印每个特征的信息增益
if infoGain > bestInfoGain: # 求出最大信息增益得到信息增益最大的特征索引值
bestInfoGain = infoGain
bestFeature = i
return bestFeature
if __name__ == '__main__:
myDat, labels = createDataset()
print(myDat)
print("最优特征的索引值:" + str(chooseBestFeatureToSplit(myDat)))
结果如下:
在函数调用中,数据必须满足一定的要求,首先,数据必须是由列表元素组成的列表,而且所有的列表元素具有相同的数据长度;其次,数据的最后一列或者每个实例的最后一个元素是当前实例的类别标签。这样,我们才能通过程序统一完成数据集的划分。
我们已经学习了从数据集构造决策树算法所需要的子功能模块,包括经验熵的计算和最优特征的选择,其工作原理如下:得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据集被向下传递到树的分支的下一个结点。在这个结点上,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集。
构建决策树的算法有很多,比如C4.5、ID3和CART,这些算法在运行时并不总是在每次划分数据分组时都会消耗特征。由于特征数目并不是每次划分数据分组时都减少,因此这些算法在实际使用时可能引起一定的问题。目前我们并不需要考虑这个问题,只需要在算法开始运行前计算列的数目,查看算法是否使用了所有属性即可。
递归的条件是:程序遍历完所有划分数据集的属性,或者每个分之下的所有实例都具有相同的分类。如果所有的实例具有相同的分类,则得到一个叶子结点或者终止块。
当然,我们可能会遇到,当遍历完所有的特征属性,但是某个或多个分支下实例类标签仍然不唯一,此时,我们需要确定出如何定义该叶子结点,在这种情况下,通过会采取多数表决的原则选取分支下实例中类标签种类最多的分类作为该叶子结点的分类
这样,我们就需要先定义一个多数表决函数majorityCnt()
"""
统计classList中出现此处最多的元素(类标签)
"""
def majorityCnt(classList):
"""
classList - 类标签列表
Returns:
sortedClassCount[0][0] - 出现此处最多的元素(类标签)
"""
classColunt = {} # 统计classList中每个元素出现的次数
for vote in classList:
if vote not in classColunt.keys():
classColunt[vote] = 0
classColunt[vote] += 1
sortedClassCount = sorted(classColunt.items(),key = operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0] # 返回classList中出现次数最多的元素
考虑了这种情况后,我们就可以通过递归的方式写出决策树的构建代码了。
"""
创建树
"""
def createTree(dataSet,labels,featLabels):
"""
:param dataSet: 数据集
:param labels: 标签列表,包含了数据集中所有特征的标签
:return:
"""
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] # 获得最优特征的标签
featLabels.append(bestFeatLabel)
myTree = {bestFeatLabel:{}} # 根据最优特征的标签生成树
del (labels[bestFeat]) # 删除已经使用特征标签
featValues = [example[bestFeat] for example in dataSet] # 得到训练集中所有最优特征的属性值
uniqueVals = set(featValues) # 去掉重复的属性值
for value in uniqueVals: # 遍历特征,创建决策树。
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels,featLabels)
return myTree
if __name__ == '__main__:
dataSet, labels = createDataset()
featLabels = []
myTree = createTree(dataSet, labels,featLabels)
print(myTree)
我们可以通过决策树进行实际的分类了,利用构建好的决策树,输入符合要求的测试数据,比较测试数据与决策树上的数值,递归执行该过程直到叶子结点,最后将测试数据定义为叶子结点所有的分类,输出分类结果
决策树分类函数代码为:
"""
使用决策树的分类函数
"""
def classify(inputTree,featLabels,testVec):
"""
classify 给输入的节点,进行分类
:param inputTree: 决策树模型
:param featLabels: 标签对应的名称
:param testVec: 测试输入的数据
:return:
classLabel 分类的结果,需要映射label才能知道名称
"""
firstStr = next(iter(inputTree)) # 获取tree的根节点对于的key值
secondDict = inputTree[firstStr] # 通过key得到根节点对应的value
featIndex = featLabels.index(firstStr) # 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key],featLabels,testVec)
else:
classLabel = secondDict[key]
return classLabel
if __name__ == '__main__':
dataSet, labels = createDataset()
featLabels = []
myTree = createTree(dataSet, labels,featLabels)
print(classify(myTree,featLabels,[1,0]))
首先,我们知道构建决策树是非常耗时的任务,即使很小的数据集,也要花费几秒的时间来构建决策树,这样显然耗费计算时间。所以,我们可以将构建好的决策树保存在磁盘中,这样当我们需要的时候,再从磁盘中读取出来使用即可。
如何进行对象的序列化操作,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)
项目概述
隐形眼镜类型包括硬材质、软材质以及不适合佩戴隐形眼镜。我们需要使用决策树预测患者需要佩戴的隐形眼镜类型。
"""
使用决策树预测隐形眼镜类型
"""
fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
featLables = []
lensesTree = createTree(lenses,lensesLabels,featLables)
print(lensesTree)
{'tearRate': {'normal': {'astigmatic': {'yes': {'prescript': {'myope': 'hard', 'hyper': {'age': {'presbyopic': 'no lenses', 'pre': 'no lenses', 'young': 'hard'}}}}, 'no': {'age': {'presbyopic': {'prescript': {'myope': 'no lenses', 'hyper': 'soft'}}, 'pre': 'soft', 'young': 'soft'}}}}, 'reduced': 'no lenses'}}
决策树算法可能或出现的过度匹配(过拟合)的问题,当决策树的复杂度较大时,很可能会造成过拟合问题。此时,我们可以通过裁剪决策树的办法,降低决策树的复杂度,提高决策树的泛化能力。比如,如果决策树的某一叶子结点只能增加很少的信息,那么我们就可将该节点删掉,将其并入到相邻的结点中去,这样,降低了决策树的复杂度,消除过拟合问题。后面会讲到。
决策树的一些优点:
决策树的一些缺点:
1 https://blog.csdn.net/c406495762/article/details/75663451
2 https://blog.csdn.net/sinat_17196995/article/details/55670932
3 https://www.cnblogs.com/zy230530/p/6813250.html
4 https://github.com/apachecn/MachineLearning/blob/master/docs/3.%E5%86%B3%E7%AD%96%E6%A0%91.md