该部分内容所设计到的程序源码已经存在我的github上,地址奉上:
https://github.com/AdventureSJ/ML-Notes/tree/master/DecisionTree
欢迎各位大佬批评指正,也欢迎各位好友fock or star!
Thank You!
在前面所述的k-近邻算法可以完成很多分类任务,但是他最大的缺点就是无法给出数据的内部含义,决策树的主要优势就在于数据形式非常容易理解。
决策树(Decision Tree)及其变种是另一类将输入空间分成不同的区域,每个区域有独立参数的算法。决策树分类算法是一种基于实例的归纳学习方法,它能从给定的无序的训练样本中,提炼出树型的分类模型。树中的每个非叶子节点记录了使用哪个特征来进行类别的判断,每个叶子节点则代表了最后判断的类别。根节点到每个叶子节点均形成一条分类的路径规则。而对新的样本进行测试时,只需要从根节点开始,在每个分支节点进行测试,沿着相应的分支递归地进入子树再测试,一直到达叶子节点,该叶子节点所代表的类别即是当前测试样本的预测类别。
与其它机器学习分类算法相比较,决策树分类算法相对简单,只要训练样本集合能够使用特征向量和类别进行表示,就可以考虑构造决策树分类算法。预测分类算法的复杂度只与决策树的层数有关,是线性的,数据处理效率很高,适合于实时分类的场合。
机器学习中,决策树是一个预测模型。它代表的是对象属性与对象值之间的一种映射关系。树中每个节点表示某个对象,而每个分支叉路径则代表某个可能的属性值,而每个叶节点则对应从根节点到该叶节点所经历的路径所表示的对象的值。决策树仅有单一输出,若欲有复数输出,可以建立独立的决策树以处理不同输出。数据挖掘中决策树是一种经常要用到的技术,可以用于分析数据,同样也可以用来作预测。从数据产生决策树的机器学习技术叫做决策树学习,通俗说就是决策树。
本章构造的决策树算法能够读取数据集合,构建类似于下图所示的决策树,图中构造了一个假想的邮件分类系统,他首先检测发送邮件域名地址,如果地址为myEmpIoyer.com,则将其放在分类为“无聊时需要阅读的邮件”中。如果邮件不是来自这个域名,则检查邮件内容里是否包含单词曲棍球,如果包含则将邮件归类到“需要及时处理的朋友邮件",如果不包含则将邮件归类到“无需阅读的垃圾邮件"。决策树很多任务都是为了数据中所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,并从中提取出一系列 规则,机器学习算法最终将使用这些机器从数据集中创造的规则。专家系统中经常使用决策树,而且决策树给出结果往往可以匹敌在当前领域具有几十年工作经验的人类专家。
现在我们已经大致了解了决策树可以完成哪些任务,接下来我们将学习如何从一堆原始数据中构造决策树。首先我们讨论构造决策树的方法,以及如何编写构造树的代码;接着提出一些度量算法成功率的方法;最后使用递归建立分类器,并且使用matplotlib绘制决策树图。构造完成决策树分类器之后,我们将输人一些隐形眼镜的处方数据,并由决策树分类器预测需要的镜片类型 。
- 优点:计算复杂度不高,输出结果易于理解,对中间值不敏感,可以处理不相关特征数据。
- 缺点:可能会产生过度匹配的问题。
- 适用数据类型:数值型和标称型。
在构造决策树时,我们需要解决的第一个问题就是,当前数据集上哪个特征在划分数据分类时起决定性作用。为了找到决定性的特征,划分出最好的结果,我们必须评估每个特征。完成测试之后,原始数据集就被划分为几个数据子集。这些数据子集会分布在第一个决策点的所有分支上。如果某个分支下的数据属于同一类型,则当前无需阅读的垃圾邮件已经正确地划分数据分类,无需进一步对数据集进行分割。如果数据子集内的数据不属于同一类型,则需要重复划分数据子集的过程。如何划分数据子集的算法和划分原始数据集的方法相同,直到所有具有相同类型的数据均在一个数据子集内。
一些决策树算法采用二分法划分数据,我们并不采用这种方法。如果依据某个属性划分数据将会产生4个可能的值,我们将把数据划分成四块,并创建四个不同的分支。我们将使用ID3算法划分数据集,该算法处理如何划分数据集,何时停止划分数据集。(ID3是一种基于信息熵的决策树分类学习算法,以信息增益和信息熵,作为对象分类的衡量标准。ID3算法结构简单、学习能力强、分类速度快适合大规模数据分类。但同时由于信息增益的不稳定性,容易倾向于众数属性导致过度拟合,算法抗干扰能力差。)
信息增益
划分数据集的大原则是:将无序的数据变得更加有序。我们可以使用多种方法划分数据集,但是每种方法都有各自的优缺点。组织杂乱无章数据的一种方法就是使用信息论度量信息,信息论是量化处理信息的分支科学。我们可以在划分数据之前使用信息论量化度量信息的内容。
在划分数据集之前之后信息发生的变化称为信息增益,知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择。
在可以评测哪种数据划分方式是最好的数据划分之前,我们必须学习如何计算信息增益。集合信息的度量方式称为香农熵或者简称为熵,这个名字来源于信息论之父克劳德•香农。
熵定义为信息的期望值,在明晰这个概念之前,我们必须知道信息的定义。如果待分类的事务可能划分在多个分类之中, 则符号的信息定义为,其中是选择该分类的概率。
为了计算熵,我们需要计算所有类别所有可能值包含的信息期望值,通过下面的公式得到
其中n是分类的数目。创建名为tree.py文件,添加下述代码,此程序的功能是计算给定数据及的熵。
from math import log
import operator
"""
函数说明:计算给定数据集的经验熵
Parameters:
dataSet - 数据集
Returns:
shannonEnt - 经验熵(香农熵)
Modify:
2018/9/6
"""
def calcShannonEnt(dataSet):
numdataRows = len(dataSet) #返回数据集行数,即样本个数
labelCounts = {} #用于计算数据集最后一列标签向量
for featVec in dataSet:
currentLabel = featVec[-1]
if currentLabel not in labelCounts:
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 #获得数据集最后一列标签的对应的字典
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/numdataRows #选择该分类的概率
shannonEnt -= prob*log(prob,2) #按照经验熵公式计算经验熵
return shannonEnt
我们可以添加一个createDataSet()函数用来创建数据集,如下面的代码所示,该数据集的特征为一个人的信贷特征,结果为是否给一个人放贷。
"""
函数说明:创建测试数据集
Parameters:
无
Returns:
dataSet - 数据集
labels - 特征标签
Modify:
2018/9/6
"""
def createDataSet():
dataSet = [[0, 0, 0, 0, 'no'], #数据集
[0, 0, 0, 1, 'no'],
[0, 1, 0, 1, 'yes'],
[0, 1, 1, 0, 'yes'],
[0, 0, 0, 0, 'no'],
[1, 0, 0, 0, 'no'],
[1, 0, 0, 1, 'no'],
[1, 1, 1, 1, 'yes'],
[1, 0, 1, 2, 'yes'],
[1, 0, 1, 2, 'yes'],
[2, 0, 1, 2, 'yes'],
[2, 0, 1, 1, 'yes'],
[2, 1, 0, 1, 'yes'],
[2, 1, 0, 2, 'yes'],
[2, 0, 0, 0, 'no']]
labels = ['年龄','有工作','有自己的房子','信贷情况']
return dataSet,labels
if __name__=='__main__':
dataSet,labels = createDataSet()
print(calcShannonEnt(dataSet))
计算出的香农熵为0.9709505944546686, 熵越高,则混合的数据也越多,我们如果在数据集中添加更多的分类,则熵是也会变大的。
得到熵之后,我们就可以按照获取最大信息增益的方法划分数据集,下一节我们将具体学习如何划分数据集以及如何度量信息增益。
上节我们学习了如何度量数据集的无序程度,分类算法除了需要测量信息熵,还需要划分数据集,度量花费数据集的熵,以便判断当前是否正确地划分了数据集。我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照哪个特征划分数据集是最好的划分方式。想象一个分布在二维空间的数据散点图,需要在数据之间划条线,将它们分成两部分,我们应该按照x轴还是y轴划线呢?答案就是本节讲述的内容。
继续在tree.py文件中输入以下代码:
"""
函数说明:按照给定特征划分数据集
Parameters:
dataSet — 待划分的数据集
axis - 划分数据集的特征
value - 需要返回的特征的值
Returns:
无
Modify:
2018/9/6
"""
def splitDataSet(dataSet,axis,value):
retDataSet = [] #创建返回的数据集列表
for featVec in dataSet: #遍历数据集
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] #删除axis列的特征值
reducedFeatVec.extend(featVec[axis+1:])#将符合条件的添加到返回的数据集
retDataSet.append(reducedFeatVec)
return retDataSet
if __name__=='__main__':
dataSet,labels = createDataSet()
print(splitDataSet(dataSet,0,1))
输出结果应为:
[[0, 0, 0, 'no'], [0, 0, 1, 'no'], [1, 1, 1, 'yes'], [0, 1, 2, 'yes'], [0, 1, 2, 'yes']]
接下来我们将遍历整个数据集,循环计算香农熵和splitDataSet()函数,找到最好的特征划分方式。熵计算将会告诉我们如何划分数据集是最好的数据组织方式。打开文本编辑器,在tree.py文件中输人下面的程序代码。
"""
函数说明:选择最优特征
Parameter:
dataSet - 数据集
Returns:
bestFeature - 信息增益最大特征的索引值
Modify:
2018/9/6
"""
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] #返回某一特征的列向量
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 #信息增益
if (infoGain > bestInfoGain): #更新信息增益,找到最大的信息增益
bestFeature = i
bestInfoGain = infoGain
return bestFeature
if __name__=='__main__':
dataSet,labels = createDataSet()
print(chooseBestFeatureToSplit(dataSet))
输出结果为2,这就告诉我们第二个特征是最好的用于划分数据集的特征,即是否有自己的房子是最好的划分特征指标。
目前我们已经学习了从数据集构造决策树算法所需要的子功能模块,其工作原理如下:得到 原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点 上,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集。
递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有
相同的分类。如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。任何到达叶子节
点的数据必然属于叶子节点的分类。
在test.py文件中添加以下代码:
"""
函数说明:返回classlist中出现次数最多的元素
Parameter:
classList - 类标签列表
Returns:
sortedClassCount[0][0] - 出现此处最多的元素
Modify:
2018/9/6
"""
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]
"""
函数说明:创建决策树
Parameter:
dataSet - 训练数据集合
labels - 分类属性标签
featLabels - 存储选择的最优特征标签
Returns:
myTree - 决策树
Modify:
2018/9/6
"""
def createTree(dataSet,labels,featLabels):
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) #得到最优划分特征
bestFeatLabels = labels[bestFeat] #最优特征对应的标签
featLabels.append(bestFeatLabels)
myTree = {bestFeatLabels:{}} #初始化树
del(labels[bestFeat])
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals: #递归调用创建树函数
myTree[bestFeatLabels][value] = createTree(splitDataSet(dataSet,bestFeat,value),labels,featLabels)
return myTree
if __name__=='__main__':
dataSet,labels = createDataSet()
myTree = createTree(dataSet,labels,featLabels)
print(myTree)
变量myTree包含了很多代表树结构信息的嵌套字典,打印树我们得到的树结构如下所示:
{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进人叶子节点;最后将测试数据定义为叶子节点所属的类型。
为了验证算法的实际效果,将下述代码添加到tree.py文件中。
"""
函数说明:使用决策树分类
Parameters:
inputTree - 已经生成的决策树
featLabels - 存储选择的最优特征标签
testVec - 测试数据列表,顺序对应最优特征标签
Returns:
classLabels - 分类结果
Modify:
2018/9/6
"""
def classify(inputTree,featLabels,testVec):
firstStr = next(iter(inputTree))
secondDict = inputTree[firstStr] #根节点的分支
featIndex = featLabels.index(firstStr) #得到最优标签的索引
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]).__name__=='dict': #如果是字典,则继续递归决策,不是直接返回
classLabels = classify(secondDict[key],featLabels,testVec)
else:
classLabels = secondDict[key]
return classLabels
if __name__=='__main__':
dataSet,labels = createDataSet()
featLabels = []
myTree = createTree(dataSet,labels,featLabels)
testVec = [0,1]
result = classify(myTree,featLabels,testVec)
if result =='yes':
print('放贷')
if result =='no':
print('不放贷')
构造决策树是很耗时的任务,即使处理很小的数据集,如前面的样本数据,也要花费几秒的时间,如果数据集很大,将会耗费很多计算时间。然而用创建好的决策树解决分类问题,则可以很快完成。因此,为了节省计算时间,最好能够在每次执行分类时调用巳经构造好的决策树。为了解决这个问题,需要使用Python模块pickle序列化对象。 序列化对象可以在磁盘上保存对象,并在需要的时候读取出来。任何对象都可以执行序列化操作,字典对象也不例外。 添加以下代码实现上述功能。
'''
函数说明:存储和读取序列化的决策树
Parameters:
inputTree - 决策树
filename - 保存的文件名
Returns:
无
Modify:
2018/10/25
'''
def storeTree(inputTree,filename):
import pickle
fw = open(filename,'w')
pickle.dump(inputTree,fw)
fw.close()
def grab(filename):
import pickle
fr = open(filename)
return pickle.load(fr)
本节我们将通过一个例子讲解决策树如何预测患者需要佩戴的隐形眼镜类型。使用小数据集 ,我们就可以利用决策树学到很多知识:眼科医生是如何判断患者需要佩戴的镜片类型;一旦理解了决策树的工作原理,我们甚至也可以帮助人们判断需要佩戴的镜片类型。
在代码的最后部分,需要我们手动安装pydotplus库以及Graphviz软件,注意配置环境变量,测试结果如下所示:
下一章我们将学习另一个决策树构造算法CART,本章使用的算法是ID3, 它是一个好的算法但并不完美。103算法无法直接处理数值型数据,尽管我们可以通过量化的方法将数值型数据转化为标称型数值,但是如果存在太多的特征划分,ID3算法仍然会面临其他问题 。