机器学习实战 - 决策树

仅仅作为自己的学习笔记。学习内容原址:https://blog.csdn.net/c406495762/article/details/75663451 + https://blog.csdn.net/c406495762/article/details/76262487

简介

分类决策树模型是一种描述对实例进行分类的树形结构。决策树由结点(node)和有向边(directed edge)组成。结点有两种类型:内部结点(internal node)和叶结点(leaf node)。内部结点表示一个特征或属性,叶结点表示一个类(类别)

我们可以把决策树看成一个if-then规则的集合,将决策树转换成if-then规则的过程是这样的:由决策树的根结点(root node)到叶结点(leaf node)的每一条路径构建一条规则;路径上内部结点的特征对应着规则的条件,而叶结点的类对应着规则的结论。决策树的路径或其对应的if-then规则集合具有一个重要的性质:互斥并且完备。这就是说,每一个实例都被一条路径或一条规则所覆盖,而且只被一条路径或一条规则所覆盖。这里“所覆盖”是指实例的特征与它对应的路径上构建的规则一致实例满足路径上构建的规则的条件

使用决策树一个接一个的处理多元线性问题。所谓决策树学习算法,就是根据数据,找出决策边界。下图中每个节点都是一个决策边界,这些决策边界试图把所有训练点都正确分类,所以最终可能导致过拟合
在这里插入图片描述
在这里插入图片描述
使用决策树做预测需要以下过程:

  • 收集数据:可以使用任何方法。比如想构建一个相亲系统,我们可以从媒婆那里,或者通过参访相亲对象获取数据。根据他们考虑的因素和最终的选择结果,就可以得到一些供我们利用的数据了。
  • 准备数据:收集完的数据,我们要进行整理,将这些所有收集的信息按照一定规则整理出来,并排版,方便我们进行后续处理。
  • 分析数据:可以使用任何方法,决策树构造完成之后,我们可以检查决策树图形是否符合预期。
  • 训练算法:这个过程也就是构造决策树,同样也可以说是决策树学习,就是构造一个决策树的数据结构。
  • 测试算法:使用经验树计算错误率。当错误率达到了可接收范围,这个决策树就可以投放使用了。
  • 使用算法:此步骤可以使用适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义。

决策树构建的准备工作

使用决策树做预测的每一步骤都很重要:
数据收集不到位,将会导致没有足够的特征让我们构建错误率低的决策树;
数据特征充足,但是不知道用哪些特征好,将会导致无法构建出分类效果好的决策树模型。

因此从算法方面看,决策树的构建是我们的核心内容。

决策树要如何构建呢?通常,这一过程可以概括为3个步骤:特征选择、决策树的生成和决策树的修剪。

特征选择

特征选择在于选取对训练数据具有分类能力的特征,这样可以提高决策树学习的效率。如果利用一个特征进行分类的结果与随机分类的结果没有很大差别,则称这个特征是没有分类能力的,经验上扔掉这样的特征对决策树学习的精度影响不大。通常特征选择的标准是信息增益(information gain)信息增益比,为了简单,本文章使用信息增益作为选择特征的标准。

此时,请理解这样一段话:在数据分析领域,分类算法有很多,其原理千差万别,有基于样本距离的最近邻算法,有基于特征信息熵的决策树,有基于bagging的随机森林,有基于boosting的梯度提升分类树,其实现过程相差不大,如下图。

那么,什么是信息增益?在讲解信息增益之前,让我们看一组实例,贷款申请样本数据表。
在这里插入图片描述
希望通过所给的训练数据学习一个贷款申请的决策树,用以根据申请人的特征利用该决策树决定是否批准贷款申请。

特征选择就是决定用哪个特征来划分特征空间。比如,我们通过上述数据表得到两个可能的决策树,分别由两个不同特征的根结点构成:
在这里插入图片描述
图(a)所示的根结点的特征是年龄,有3个取值,对应于不同的取值有不同的子结点。图(b)所示的根节点的特征是工作,有2个取值,对应于不同的取值有不同的子结点。

问题是:究竟选择哪个特征更好些?这就要求确定选择特征的准则。直观上,如果一个特征具有更好的分类能力,或者说,按照某一特征将训练数据集分割成子集,使得各个子集在当前条件下有最好的分类,那么就更应该选择这个特征。信息增益就能够很好地表示这一直观的准则。

什么是信息增益呢?在划分数据集之前、之后信息发生的变化称为信息增益。

知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的信息增益,获得信息增益最的特征就是最的选择。

概念1:香农熵

集合信息的度量方式称为香农熵或者简称为熵(entropy),这个名字来源于信息论之父克劳德·香农。

如果看不明白什么是信息增益和熵,请不要着急,因为他们自诞生的那一天起,就注定会令世人十分费解。克劳德·香农写完信息论之后,约翰·冯·诺依曼建议使用"熵"这个术语,因为大家都不知道它是什么意思。

熵定义为信息的期望值。在信息论与概率统计中,熵是表示随机变量不确定性的度量熵越大,随机变量的不确定性就越大。

熵主要控制决策树决定在何处分隔数据。其定义——它是一系列样本中的不纯度的测量值。建立决策树的过程其实就是找到变量划分点,从而产生尽可能单一的子集,决策树做决策的过程,就是对这一过程的重复。如下图中,左右两侧的黄色区域相比较,右侧子集足够单一。
在这里插入图片描述

如果待分类的事务可能划分在多个分类之中,则符号 x i x_i 信息定义为:
在这里插入图片描述
其中 p ( x i ) p(x_i) 是选择该分类的概率。

  • 有人可能会问,信息为啥这样定义啊?答曰:前辈得出的结论。这就跟1+1等于2一样,记住并且会用即可。
  • 上述式中的对数以2为底,也可以e为底(自然对数)。

通过上式,我们可以得到所有类别的信息。

熵的计算公式:
在这里插入图片描述
其中n是类别的数目, P i P_i 是第 i 类中的样本占总样本数的比例。

从定义中可以看出,熵与数据单一性呈负相关的关系(数据越单一,熵就越小)。在一种极端情况下,所有样本属于同一类,此时熵为0;在另一种极端情况下,样本均匀分布在所有类中,此时熵达到最大值1(熵为1,是所能得到的单一性最差的样本)。

当熵中的概率由数据估计(特别是最大似然估计)得到时,所对应的熵称为经验熵(empirical entropy)。什么叫由数据估计?比如有10个数据,一共有两个类别,A类和B类。其中有7个数据属于A类,则该A类的概率即为十分之七。其中有3个数据属于B类,则该B类的概率即为十分之三。浅显的解释就是,这概率是我们根据数据数出来的。

我们定义贷款申请样本数据表中的数据为训练数据集D,则训练数据集D的经验熵为H(D),|D|表示其样本容量,即样本个数。设有K个类别 C k ( k = 1 , 2 , 3 , , K ) C_k(k=1,2,3,···,K) C k |C_k| 为属于类别 C k C_k 的样本个数。则其对应的验熵公式可以写为:
在这里插入图片描述
根据此公式计算经验熵H(D),分析贷款申请样本数据表中的数据。最终分类结果只有两类,即放贷和不放贷。根据表中的数据统计可知,在15个数据中,9个数据的结果为放贷,6个数据的结果为不放贷。所以数据集D的经验熵H(D)为:
在这里插入图片描述
经过计算可知,数据集D的经验熵H(D)的值为0.971。

编写代码计算经验熵

在编写代码之前,我们先对数据集进行属性标注:

  • 年龄:0代表青年,1代表中年,2代表老年;
  • 有工作:0代表否,1代表是;
  • 有自己的房子:0代表否,1代表是;
  • 信贷情况:0代表一般,1代表好,2代表非常好;
  • 类别(是否给贷款):no代表否,yes代表是。

确定这些之后,我们就可以创建数据集,并计算经验熵了,代码编写如下:

from math import log

"""
函数说明:
	创建测试数据集
Parameters:
	无
Returns:
    dataSet - 数据集
    labels - 分类属性
"""
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   #返回数据集和分类属性


"""
函数说明:
	计算给定数据集的经验熵(香农熵)
Parameters:
    dataSet - 数据集
Returns:
    shannonEnt - 经验熵(香农熵)
"""
def calcShannonEnt(dataSet):
	numEntries = len(dataSet)
	# 保存每个标签(Label)出现次数的字典
	labelCounts = {}
	for featVec in dataSet:
		currentLabel = featVec[-1]

		# 如果标签(Label)还没有放入统计次数的字典, 则添加进去
		if currentLabel not in labelCounts.keys():
			labelCounts[currentLabel] = 0
		labelCounts[currentLabel] += 1

		# 返回指定键的值,如果键不存在于字典中,将会添加键并将值设为default
		#labelCounts.setdefault(currentLabel, 0) ?????

	# 计算香农熵
	shannonEnt = 0.0
	for key in labelCounts:
		# 选择该标签(Label)的概率
		prob = float(labelCounts[key]) / numEntries
		# 利用香农熵公式进行计算
		shannonEnt -= prob * log(prob, 2)

	return shannonEnt


if __name__ == '__main__':
	dataSet, features = createDataSet()
	print(dataSet)
	print(calcShannonEnt(dataSet))

运行效果:
在这里插入图片描述

概念2:信息增益

在上面,我们已经说过,如何选择特征,需要看信息增益。也就是说,信息增益是相对于特征而言的,信息增益越大,特征对最终的分类结果影响也就越大,我们就应该选择对最终分类结果影响最大的那个特征作为我们的分类特征。

熵是如何影响决策树确定其边界的?这涉及到一个新的术语——信息增熵(information gain)。信息增熵定义为父节点的熵减去子节点的熵的加权平均。这些子节点是划分父节点后生成的。决策树算法会最大化信息增熵,它通过这种方法来选择进行划分的特征,如果特征可以取多个不同值,该方法将帮助它找出在何处进行划分,总之,它会把信息增益最大化。
在这里插入图片描述

在讲解信息增益定义之前,我们还需要明确一个概念,条件熵:随机变量Y的条件熵(conditional entropy)H(Y|X)表示在已知随机变量X的条件下随机变量Y的不确定性。定义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)决策树学习中的信息增益等价于训练数据集中类与特征的互信息。

设特征A有n个不同的取值{a1, a2, ···, an},根据特征A的取值将D划分为n个子集D1, D2,···, Dn, D i |D_i| D i D_i 的样本个数。记子集 D i D_i 中属于 C k C_k 的样本的集合为 D i k D_ik (i、k均为下标),即 D i k D_ik = D i D_i C k C_k ,| D i k D_ik |为 D i k D_ik 的样本个数。于是经验条件熵的公式为:
在这里插入图片描述
说了这么多概念性的东西,没有听懂也没有关系,举几个例子,再回来看一下概念,就懂了。

以贷款申请样本数据表为例进行说明。看下年龄这一列的数据,也就是特征A1,一共有三个类别,分别是:青年、中年和老年。我们只看年龄是青年的数据,年龄是青年的数据一共有5个,所以年龄是青年的数据在训练数据集出现的概率是十五分之五,也就是三分之一。同理,年龄是中年和老年的数据在训练数据集出现的概率也都是三分之一。现在我们只看年龄是青年的数据的最终得到贷款的概率为五分之二,因为在五个数据中,只有两个数据显示拿到了最终的贷款,同理,年龄是中年和老年的数据最终得到贷款的概率分别为五分之三、五分之四。所以计算年龄的信息增益的过程如下:
在这里插入图片描述
同理,计算其余特征的信息增益g(D,A2)、g(D,A3)和g(D,A4)。分别为:
在这里插入图片描述
最后,比较所有特征对应的信息增益,由于特征A3(有自己的房子)的信息增益值最大,所以选择A3作为最优特征。

香农熵是直接对最终类别进行计算;信息增益是对某一特征情况下分类的类别结果进行计算。计算信息增益时需要使用到香农熵的计算公式。

编写代码计算信息增益

我们已经学会了通过公式计算信息增益,接下来编写代码,计算信息增益,选择最优特征。

from math import log


"""
函数说明:
	按照给定特征划分数据集
Parameters:
    dataSet - 待划分的数据集
    axis - 指定 划分数据集的特征在数据集中的索引
    value - 指定 划分数据集的特征下的某一类别
    	  - 比如年龄这一特征对应青年、中年、老年这三类,这里只及选择其中一类进行保存
Returns:
    retDataSet - 划分后的数据集(【以dataSet[axis]这一列数据作为特征】且【特征值仅为value时】、【去掉了那一列用来进行划分的特征数据】的数据集)
"""
def splitDataSet(dataSet, axis, value):
	retDataSet = []
	# 遍历数据集的每一行
	for featVec in dataSet:
		# 找出每一行中属于特征列的值等于value的情况,将这一行保存下来,但保存时不再保存作为特征的数据
		reducedFeatVec = []
		if featVec[axis] == value:
			# 去掉数据集中用来作为特征划分数据的那一列数据
			reducedFeatVec = featVec[:axis] # 向reducedFeatVec中添加axis前的数据
			reducedFeatVec.extend(featVec[axis+1:]) # 向reducedFeatVec中添加axis后的数据
			retDataSet.append(reducedFeatVec)
	return retDataSet


"""
函数说明:
	选择最优特征(计算信息增益)
Parameters:
    dataSet - 数据集
Returns:
    bestFeature - 信息增益最大的特征在数据集中的索引值
"""
def chooseBestFeatureToSplit(dataSet):
	# 减1是减去类别这一列,其他的列才可以作为数据集的特征, 所以共len(dataSet[0]) - 1个特征
	numFeatures = len(dataSet[0]) - 1 
	# 计算数据集的香农熵
	baseEntropy = calcShannonEnt(dataSet)
	#print("%.3f" % baseEntropy)
	# 初始化最优的信息增益值
	bestInfoGain = 0.0
	# 初始化最优特征的索引值
	bestFeature = -1
	for i in range(numFeatures):
		# 获取dataSet中索引为i的特征对应的所有内容(每个特征也包含多个类别)		
		featList = [example[i] for example in dataSet]
		# 比如年龄这一特征对应青年、中年、老年这三类, 所以下一步是进行去重,得到不同的类别
		uniqueVals = set(featList)
		# 初始化这一特征对应的经验条件熵
		newEntropy = 0.0
		# 计算这一特征下不同类别下的香农熵(进而得出这一特征下的信息增益)
		for value in uniqueVals:
			# 根据这一特征i下的这一类别value划分后的数据子集
			subDataSet = splitDataSet(dataSet, i, value)
			#print(subDataSet)
			# 计算子集的概率
			prob = len(subDataSet) / float(len(dataSet))
			# 根据公式计算经验条件熵
			newEntropy += prob * calcShannonEnt(subDataSet)
			#print("第%d个特征的%s类别的经验条件熵为%.3f" % (i, value, newEntropy))
		# 该特征下的信息增益
		infoGain = baseEntropy - newEntropy
		print("第%d个特征的增益为%.3f" % (i, infoGain))
		# 保存最大信息增益时的相关信息
		if (infoGain > bestInfoGain):
			bestInfoGain = infoGain       
			bestFeature = i  
	return bestFeature


"""
函数说明:
	创建测试数据集
Parameters:
	无
Returns:
    dataSet - 数据集
    labels - 分类属性
"""
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   #返回数据集和分类属性


"""
函数说明:
	计算给定数据集的经验熵(香农熵)
Parameters:
    dataSet - 数据集
Returns:
    shannonEnt - 经验熵(香农熵)
"""
def calcShannonEnt(dataSet):
	numEntries = len(dataSet)
	# 保存每个标签(Label)出现次数的字典
	labelCounts = {}
	for featVec in dataSet:
		currentLabel = featVec[-1]

		# 如果标签(Label)还没有放入统计次数的字典, 则添加进去
		if currentLabel not in labelCounts.keys():
			labelCounts[currentLabel] = 0
		labelCounts[currentLabel] += 1

		# 返回指定键的值,如果键不存在于字典中,将会添加键并将值设为default
		#labelCounts.setdefault(currentLabel, 0) ?????

	# 计算香农熵
	shannonEnt = 0.0
	for key in labelCounts:
		# 选择该标签(Label)的概率
		prob = float(labelCounts[key]) / numEntries
		# 利用香农熵公式进行计算
		shannonEnt -= prob * log(prob, 2)

	return shannonEnt


if __name__ == '__main__':
	dataSet, features = createDataSet()
	
	#print(dataSet)
	#print(calcShannonEnt(dataSet))

	print("最优特征的索引值:" + str(chooseBestFeatureToSplit(dataSet)))

上面代码中新加入的两个函数的介绍:

  • splitDataSet()函数是用来选择各个特征的子集的,比如选择特征为年龄(第0个特征)的青年类别(用0代表)的子集,我们可以调用splitDataSet(dataSet, 0, 0),这样返回的子集就是年龄为青年的5个数据集。
  • chooseBestFeatureToSplit()函数是选择选择最优特征的函数。需要调用splitDataSet()函数。

运行结果:
在这里插入图片描述
对比手动计算的结果,发现结果完全正确!最优特征的索引值为2,也就是特征A3(有自己的房子)。

决策树的生成

我们已经学习了从数据集构造决策树算法所需要的子功能模块,包括经验熵的计算和最优特征的选择,其工作原理如下:得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据集被向下传递到树的分支的下一个结点。在这个结点上,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集。

构建决策树的算法有很多,比如C4.5、ID3和CART,这些算法在运行时并不总是在每次划分数据分组时都会消耗特征。由于特征数目并不是每次划分数据分组时都减少,因此这些算法在实际使用时可能引起一定的问题。目前我们并不需要考虑这个问题,只需要在算法开始运行前计算列的数目,查看算法是否使用了所有属性即可。

ID3算法

ID3算法的核心是在决策树各个结点上对应信息增益准则选择特征,递归地构建决策树。具体方法是:从根结点(root node)开始,对结点计算所有可能的特征的信息增益,选择信息增益最大的特征作为结点的特征,由该特征的不同取值建立子节点;再对子结点递归地调用以上方法,构建决策树;直到所有特征的信息增益均很小或没有特征可以选择为止,最后得到一个决策树。ID3相当于用极大似然法进行概率模型的选择。

上面我们已经求得特征A3(有自己的房子)的信息增益值最大,所以选择特征A3作为根结点的特征。它将训练集D划分为两个子集D1(A3取值为”是”)和D2(A3取值为”否”)。由于D1只有同一类的样本点(子集D1的最终结果都是“发放贷款”),所以它成为一个叶结点,结点的类标记为“是”。对D2则再需要从特征A1(年龄)、A2(有工作)和A4(信贷情况)中选择新的特征作为分类的标准,所以再次计算各个特征的信息增益:
在这里插入图片描述
选择信息增益最大的特征A2(有工作)作为结点的特征。由于A2有两个可能取值,从这一结点引出两个子结点:一个对应”是”(有工作)的子结点,包含3个样本,它们属于同一类,所以这是一个叶结点,类标记为”是”;另一个是对应”否”(无工作)的子结点,包含6个样本,它们也属于同一类,所以这也是一个叶结点,类标记为”否”。

这样就生成了一个决策树,该决策树只用了两个特征(有两个内部结点)。生成的决策树如下图所示。
在这里插入图片描述
我们使用ID3算法,通过计算构建出决策树,接下来,让我们看看如何进行代实现。

编写代码实现决策树

说明:使用字典存储决策树的结构,比如上小节我们分析出来的决策树,用字典可以表示为:

{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}

在上面计算信息增益的代码中增加两个函数:

  • majorityCnt()统计classList中出现次数最多的元素(类标签).
  • createTree()用来递归构建决策树。

createTree()递归创建决策树时,递归有两个终止条件:第一个停止条件是数据集中所有最终分类标签(是否放贷: yes or no)完全相同,则直接返回该类标签;第二个停止条件是已经使用完了所有特征,仍然不能将数据划分仅包含唯一类别的分组,即决策树构建失败,特征不够用。此时说明数据纬度不够,由于第二个停止条件无法简单地返回唯一的类标签,这里挑选出现数量最多的类别作为返回值。

编写代码如下:

from math import log


"""
函数说明:
	统计classList中出现次数最多的元素(类标签)
Parameters:
    classList - 类标签列表
Returns:
    sortedClassCount[0][0] - 出现此处最多的元素(类标签)
"""
def majorityCnt(classList):
	classCount = {} # 保存classList中每个元素出现的次数
	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]


"""
函数说明:
	创建决策树
Parameters:
    dataSet - 训练数据集
    labels - 分类属性标签
    featLabels - 存储选择的最优特征标签
Returns:
	myTree - 决策树
"""
def createTree(dataSet, labels, featLabels):
	# 保存数据集中所有最终分类标签(是否放贷: yes or no)
	classList = [example[-1] for example in dataSet]

	# 如果classList中只有一种类别,则停止继续划分
	if classList.count(classList[0]) == len(classList):
		return classList[0]

	# 如果数据集中只有一种特征,
	# 则直接使用majorityCnt()函数遍历完数据集中这一所有特征的取值,
	# 然后返回出现次数最多的这一特征的取值。
	# 如年龄这一特征,还具有青年中年老年这三种类别/取值
	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)  
	# 遍历特征,创建决策树 【递归调用createTree()函数】
	for value in uniqueVals:
		myTree[bestFeatLabel][value] =  createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
	return myTree


"""
函数说明:
	按照给定特征划分数据集
Parameters:
    dataSet - 待划分的数据集
    axis - 指定 划分数据集的特征在数据集中的索引
    value - 指定 划分数据集的特征下的某一类别
    	  - 比如年龄这一特征对应青年、中年、老年这三类,这里只及选择其中一类进行保存
Returns:
    retDataSet - 划分后的数据集(【以dataSet[axis]这一列数据作为特征】且【特征值仅为value时】、【去掉了那一列用来进行划分的特征数据】的数据集)
"""
def splitDataSet(dataSet, axis, value):
	retDataSet = []
	# 遍历数据集的每一行
	for featVec in dataSet:
		# 找出每一行中属于特征列的值等于value的情况,将这一行保存下来,但保存时不再保存作为特征的数据
		reducedFeatVec = []
		if featVec[axis] == value:
			# 去掉数据集中用来作为特征划分数据的那一列数据
			reducedFeatVec = featVec[:axis] # 向reducedFeatVec中添加axis前的数据
			reducedFeatVec.extend(featVec[axis+1:]) # 向reducedFeatVec中添加axis后的数据
			retDataSet.append(reducedFeatVec)
	return retDataSet


"""
函数说明:
	选择最优特征(计算信息增益)
Parameters:
    dataSet - 数据集
Returns:
    bestFeature - 信息增益最大的特征在数据集中的索引值
"""
def chooseBestFeatureToSplit(dataSet):
	# 减1是减去类别这一列,其他的列才可以作为数据集的特征, 所以共len(dataSet[0]) - 1个特征
	numFeatures = len(dataSet[0]) - 1 
	# 计算数据集的香农熵
	baseEntropy = calcShannonEnt(dataSet)
	#print("%.3f" % baseEntropy)
	# 初始化最优的信息增益值
	bestInfoGain = 0.0
	# 初始化最优特征的索引值
	bestFeature = -1
	for i in range(numFeatures):
		# 获取dataSet中索引为i的特征对应的所有内容(每个特征也包含多个类别)		
		featList = [example[i] for example in dataSet]
		# 比如年龄这一特征对应青年、中年、老年这三类, 所以下一步是进行去重,得到不同的类别
		uniqueVals = set(featList)
		# 初始化这一特征对应的经验条件熵
		newEntropy = 0.0
		# 计算这一特征下不同类别下的香农熵(进而得出这一特征下的信息增益)
		for value in uniqueVals:
			# 根据这一特征i下的这一类别value划分后的数据子集
			subDataSet = splitDataSet(dataSet, i, value)
			#print(subDataSet)
			# 计算子集的概率
			prob = len(subDataSet) / float(len(dataSet))
			# 根据公式计算经验条件熵
			newEntropy += prob * calcShannonEnt(subDataSet)
			#print("第%d个特征的%s类别的经验条件熵为%.3f" % (i, value, newEntropy))
		# 该特征下的信息增益
		infoGain = baseEntropy - newEntropy
		#print("第%d个特征的增益为%.3f" % (i, infoGain))
		# 保存最大信息增益时的相关信息
		if (infoGain > bestInfoGain):
			bestInfoGain = infoGain       
			bestFeature = i  
	return bestFeature


"""
函数说明:
	创建测试数据集
Parameters:
	无
Returns:
    dataSet - 数据集
    labels - 分类属性
"""
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   #返回数据集和分类属性


"""
函数说明:
	计算给定数据集的经验熵(香农熵)
Parameters:
    dataSet - 数据集
Returns:
    shannonEnt - 经验熵(香农熵)
"""
def calcShannonEnt(dataSet):
	numEntries = len(dataSet)
	# 保存每个标签(Label)出现次数的字典
	labelCounts = {}
	for featVec in dataSet:
		currentLabel = featVec[-1]

		# 如果标签(Label)还没有放入统计次数的字典, 则添加进去
		if currentLabel not in labelCounts.keys():
			labelCounts[currentLabel] = 0
		labelCounts[currentLabel] += 1

		# 返回指定键的值,如果键不存在于字典中,将会添加键并将值设为default
		#labelCounts.setdefault(currentLabel, 0) ?????

	# 计算香农熵
	shannonEnt = 0.0
	for key in labelCounts:
		# 选择该标签(Label)的概率
		prob = float(labelCounts[key]) / numEntries
		# 利用香农熵公式进行计算
		shannonEnt -= prob * log(prob, 2)

	return shannonEnt


if __name__ == '__main__':
	dataSet, labels = createDataSet()
	featLabels = []
	myTree = createTree(dataSet, labels, featLabels)
	print(myTree)

在这里插入图片描述

编写代码实现可视化的决策树

上面我们确实实现了决策树的构建,但你一定会觉得它太别扭、不够直观…所以下面我们使用强大的Matplotlib绘制决策树。

这里代码都是关于Matplotlib的,如果对于Matplotlib不了解的,可以先学习下,Matplotlib的内容这里就不再累述。

与上一程序相比,这里为实现可视化而增加的函数如下:

  • getNumLeafs:获取决策树叶子结点的数目
  • getTreeDepth:获取决策树的层数
  • plotNode:绘制结点
  • plotMidText:标注有向边属性值
  • plotTree:绘制决策树
  • createPlot:创建绘制面板

plotNode()函数的工作就是绘制各个结点,比如有自己的房子、有工作、yes、no,包括内结点和叶子结点(注意:这里为了显示中文,需要设置FontProperties)。plotMidText()函数的工作就是绘制各个有向边的属性,例如各个有向边的0和1。

代码编写如下:

from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
from math import log
import operator

"""
函数说明:
	获取决策树叶子结点的数目(递归)
Parameters:
    myTree - 决策树
Returns:
    numLeafs - 决策树的叶子节点的数目
"""
def getNumLeafs(myTree):
	# 初始取决策树叶子结点的数目
	numLeafs = 0 
	'''
	 iter()函数得到迭代器
	 	【注意:迭代器函数应用在字典上时,迭代器遍历的是字典的键(key)】
	 next()函数得到第一个元素(字典中的第一个键值对中的键key)
	'''
	firstKey = next(iter(myTree))
	# 获取第一个键值对中的值value
	firstValue = myTree[firstKey]
	'''
		遍历firstValue(字典类型)中的所有值(表示firstKey的子结点),
		然后判断它们是否仍为字典,
		如果不是字典,则表明此结点为叶子结点。
		比如:{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
	'''
	for key in firstValue.keys():
		if type(firstValue[key]).__name__ == 'dict':
			numLeafs += getNumLeafs(firstValue[key])
		else:
			numLeafs += 1
	return numLeafs

"""
函数说明:
	获取决策树的层数/深度(递归)
Parameters:
    myTree - 决策树
Returns:
    maxDepth - 决策树的层数
"""
def getTreeDepth(myTree):
	# 初始化决策树层数
	maxDepth = 0
	firstKey = next(iter(myTree))
	firstValue = myTree[firstKey]
	for key in firstValue.keys():
		if type(firstValue[key]).__name__ == 'dict':
			thisDepth = 1 + getTreeDepth(firstValue[key])
		else:
			thisDepth = 1
		if thisDepth > maxDepth:
			maxDepth = thisDepth
	return maxDepth

"""
函数说明:
	绘制节点
Parameters:
    nodeTxt - 节点名
    centerPt - 文本位置
    parentPt - 标注的箭头位置
    nodeType - 节点格式
Returns:
    无
"""
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
	# 定义箭头格式
	arrow_args = dict(arrowstyle="<-")
	# 设置中文字体
	font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)
	# 绘制结点
	createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords="axes fraction", 
						xytext=centerPt, textcoords='axes fraction',
						va="center", ha="center", bbox=nodeType, 
						arrowprops=arrow_args, FontProperties=font)

"""
函数说明:
	标注有向边属性值
Parameters:
    cntrPt、parentPt - 用于计算标注位置
    txtString - 标注的内容
Returns:
    无
"""
def plotMidText(cntrPt, parentPt, txtString):
	# 计算标注位置
	xMid = (parentPt[0] - cntrPt[0]) / 2.0 + cntrPt[0] 
	yMid = (parentPt[1] - cntrPt[1]) / 2.0 + cntrPt[1] 
	createPlot.ax1.text(xMid, yMid, txtString, va="center", ha="center", rotation=30)

"""
函数说明:
	绘制决策树
Parameters:
    myTree - 决策树(字典)
    parentPt - 标注的内容
    nodeTxt - 节点名
Returns:
    无
"""
def plotTree(myTree, parentPt, nodeTxt):
	# 设置结点格式
	decisionNode = dict(boxstyle="sawtooth", fc="0.8")
	# 设置叶节点格式
	leafNode = dict(boxstyle="round4", fc="0.8")

	# 获取决策树叶结点数目(决定树的宽度)
	numLeafs = getNumLeafs(myTree)
	# 获取决策树层数(决定树的深度)
	depth = getTreeDepth(myTree)

	# 得到第一个字典的键key
	firstKey = next(iter(myTree))	
	# 中心位置
	cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff)    
	# 标注有向边属性值
	plotMidText(cntrPt, parentPt, nodeTxt)
	# 绘制结点
	plotNode(firstKey, cntrPt, parentPt, decisionNode)

	# y偏移
	plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD	
	# 得到第二个字典
	secondDict = myTree[firstKey]
	# 遍历第二个字典中的所有内容,判断是否为叶子结点
	# 如果不是,则继续调用plotTree()函数绘制下一层的树
	# 如果已经达到叶子结点,则绘制叶结点,并标注有向边属性值
	for key in secondDict.keys():
		if type(secondDict[key]).__name__ == 'dict':
			plotTree(secondDict[key], cntrPt, str(key))
		else:
			plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
			plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
			plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
	plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD

"""
函数说明:
	创建绘制面板
Parameters:
    inTree - 决策树(字典)
Returns:
    无
"""
def createPlot(inTree):
	fig = plt.figure(1, facecolor="white")
	fig.clf()
	axprops = dict(xticks=[], yticks=[])
	createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) 
	plotTree.totalW = float(getNumLeafs(inTree))  
	plotTree.totalD = float(getTreeDepth(inTree))     
	plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0;
	plotTree(inTree, (0.5,1.0), '')   
	plt.show()



"""
函数说明:
	统计classList中出现次数最多的元素(类标签)
Parameters:
    classList - 类标签列表
Returns:
    sortedClassCount[0][0] - 出现此处最多的元素(类标签)
"""
def majorityCnt(classList):
	classCount = {} # 保存classList中每个元素出现的次数
	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]


"""
函数说明:
	创建决策树
Parameters:
    dataSet - 训练数据集
    labels - 分类属性标签
    featLabels - 存储选择的最优特征标签
Returns:
	myTree - 决策树
"""
def createTree(dataSet, labels, featLabels):
	# 保存数据集中所有最终分类标签(是否放贷: yes or no)
	classList = [example[-1] for example in dataSet]

	# 如果classList中只有一种类别,则停止继续划分
	if classList.count(classList[0]) == len(classList):
		return classList[0]

	# 如果数据集中只有一种特征,
	# 则直接使用majorityCnt()函数遍历完数据集中这一所有特征的取值,
	# 然后返回出现次数最多的这一特征的取值。
	# 如年龄这一特征,还具有青年中年老年这三种类别/取值
	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)  
	# 遍历特征,创建决策树 【递归调用createTree()函数】
	for value in uniqueVals:
		myTree[bestFeatLabel][value] =  createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
	return myTree


"""
函数说明:
	按照给定特征划分数据集
Parameters:
    dataSet - 待划分的数据集
    axis - 指定 划分数据集的特征在数据集中的索引
    value - 指定 划分数据集的特征下的某一类别
    	  - 比如年龄这一特征对应青年、中年、老年这三类,这里只及选择其中一类进行保存
Returns:
    retDataSet - 划分后的数据集(【以dataSet[axis]这一列数据作为特征】且【特征值仅为value时】、【去掉了那一列用来进行划分的特征数据】的数据集)
"""
def splitDataSet(dataSet, axis, value):
	retDataSet = []
	# 遍历数据集的每一行
	for featVec in dataSet:
		# 找出每一行中属于特征列的值等于value的情况,将这一行保存下来,但保存时不再保存作为特征的数据
		reducedFeatVec = []
		if featVec[axis] == value:
			# 去掉数据集中用来作为特征划分数据的那一列数据
			reducedFeatVec = featVec[:axis] # 向reducedFeatVec中添加axis前的数据
			reducedFeatVec.extend(featVec[axis+1:]) # 向reducedFeatVec中添加axis后的数据
			retDataSet.append(reducedFeatVec)
	return retDataSet


"""
函数说明:
	选择最优特征(计算信息增益)
Parameters:
    dataSet - 数据集
Returns:
    bestFeature - 信息增益最大的特征在数据集中的索引值
"""
def chooseBestFeatureToSplit(dataSet):
	# 减1是减去类别这一列,其他的列才可以作为数据集的特征, 所以共len(dataSet[0]) - 1个特征
	numFeatures = len(dataSet[0]) - 1 
	# 计算数据集的香农熵
	baseEntropy = calcShannonEnt(dataSet)
	#print("%.3f" % baseEntropy)
	# 初始化最优的信息增益值
	bestInfoGain = 0.0
	# 初始化最优特征的索引值
	bestFeature = -1
	for i in range(numFeatures):
		# 获取dataSet中索引为i的特征对应的所有内容(每个特征也包含多个类别)		
		featList = [example[i] for example in dataSet]
		# 比如年龄这一特征对应青年、中年、老年这三类, 所以下一步是进行去重,得到不同的类别
		uniqueVals = set(featList)
		# 初始化这一特征对应的经验条件熵
		newEntropy = 0.0
		# 计算这一特征下不同类别下的香农熵(进而得出这一特征下的信息增益)
		for value in uniqueVals:
			# 根据这一特征i下的这一类别value划分后的数据子集
			subDataSet = splitDataSet(dataSet, i, value)
			#print(subDataSet)
			# 计算子集的概率
			prob = len(subDataSet) / float(len(dataSet))
			# 根据公式计算经验条件熵
			newEntropy += prob * calcShannonEnt(subDataSet)
			#print("第%d个特征的%s类别的经验条件熵为%.3f" % (i, value, newEntropy))
		# 该特征下的信息增益
		infoGain = baseEntropy - newEntropy
		#print("第%d个特征的增益为%.3f" % (i, infoGain))
		# 保存最大信息增益时的相关信息
		if (infoGain > bestInfoGain):
			bestInfoGain = infoGain       
			bestFeature = i  
	return bestFeature


"""
函数说明:
	创建测试数据集
Parameters:
	无
Returns:
    dataSet - 数据集
    labels - 分类属性
"""
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   #返回数据集和分类属性


"""
函数说明:
	计算给定数据集的经验熵(香农熵)
Parameters:
    dataSet - 数据集
Returns:
    shannonEnt - 经验熵(香农熵)
"""
def calcShannonEnt(dataSet):
	numEntries = len(dataSet)
	# 保存每个标签(Label)出现次数的字典
	labelCounts = {}
	for featVec in dataSet:
		currentLabel = featVec[-1]

		# 如果标签(Label)还没有放入统计次数的字典, 则添加进去
		if currentLabel not in labelCounts.keys():
			labelCounts[currentLabel] = 0
		labelCounts[currentLabel] += 1

		# 返回指定键的值,如果键不存在于字典中,将会添加键并将值设为default
		#labelCounts.setdefault(currentLabel, 0) ?????

	# 计算香农熵
	shannonEnt = 0.0
	for key in labelCounts:
		# 选择该标签(Label)的概率
		prob = float(labelCounts[key]) / numEntries
		# 利用香农熵公式进行计算
		shannonEnt -= prob * log(prob, 2)

	return shannonEnt


if __name__ == '__main__':
	dataSet, labels = createDataSet()
	featLabels = []
	myTree = createTree(dataSet, labels, featLabels)
	print(myTree)
	createPlot(myTree)

运行效果:
在这里插入图片描述

这部分内容,个人感觉可以选择性掌握,能掌握最好,不能掌握可以放一放,因为后面会介绍一个更简单的决策树可视化方法。看到这句话,是不是想偷懒不仔细看这部分的代码了?。。。

编写代码应用决策树

依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类/预测。

在执行数据分类时,需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子结点;最后将测试数据定义为叶子结点所属的类型。

在上面构建决策树的代码中,可以看到,有个featLabels参数。它是用来干什么的?它就是用来记录各个分类结点的。在用决策树做预测的时候,我们按顺序输入需要的分类结点的属性值即可。举个例子,比如我用上述已经训练好的决策树做分类,那么我只需要提供这个人是否有房子,是否有工作这两个信息即可得出最终的结论(可参考上面的决策树图,该图上只涉及了这两个分支结点,说明只需要判断这两个标签即可),无需提供冗余的信息。

使用决策树做分类的代码很简单(classify()函数),如下:

主要是注意如何进行节点的选择,其实还是把它考虑成字典来存储这个树,那么进行节点的选择是,只有两种情况,而这两种情况位于一个字典中【{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}】,所以就是遍历这个字典,看里面的哪个值等于要预测的这个标签。具体请看下面的代码。

from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
from math import log
import operator

"""
函数说明:
	使用决策树进行预测(递归)
Parameters:
    inputTree - 已经生成的决策树
    featLabels - 构造决策树时用到的所有最优特征标签
	testVec - 测试数据列表,顺序对应最优特征标签
Returns:
	classLabel - 分类结果
"""
def classify(inputTree, featLabels, testVec):
	# 根结点
	firstKey = next(iter(inputTree))
	# 根结点对应的下一节点(可能又是一个字典,也可能直接是最终结果)
	# 实际对应0/1两种结果
	secondDict = inputTree[firstKey]
	# 根结点对应的标签所在的索引
	featIndex = featLabels.index(firstKey)
	# 循环遍历根结点对应的下一节点,看哪个与所预测的数据对应的此标签一致
	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

"""
函数说明:
	统计classList中出现次数最多的元素(类标签)
Parameters:
    classList - 类标签列表
Returns:
    sortedClassCount[0][0] - 出现此处最多的元素(类标签)
"""
def majorityCnt(classList):
	classCount = {} # 保存classList中每个元素出现的次数
	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]


"""
函数说明:
	创建决策树
Parameters:
    dataSet - 训练数据集
    labels - 分类属性标签
    featLabels - 存储选择的最优特征标签
Returns:
	myTree - 决策树
"""
def createTree(dataSet, labels, featLabels):
	# 保存数据集中所有最终分类标签(是否放贷: yes or no)
	classList = [example[-1] for example in dataSet]

	# 如果classList中只有一种类别,则停止继续划分
	if classList.count(classList[0]) == len(classList):
		return classList[0]

	# 如果数据集中只有一种特征,
	# 则直接使用majorityCnt()函数遍历完数据集中这一所有特征的取值,
	# 然后返回出现次数最多的这一特征的取值。
	# 如年龄这一特征,还具有青年中年老年这三种类别/取值
	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)  
	# 遍历特征,创建决策树 【递归调用createTree()函数】
	for value in uniqueVals:
		myTree[bestFeatLabel][value] =  createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
	return myTree


"""
函数说明:
	按照给定特征划分数据集
Parameters:
    dataSet - 待划分的数据集
    axis - 指定 划分数据集的特征在数据集中的索引
    value - 指定 划分数据集的特征下的某一类别
    	  - 比如年龄这一特征对应青年、中年、老年这三类,这里只及选择其中一类进行保存
Returns:
    retDataSet - 划分后的数据集(【以dataSet[axis]这一列数据作为特征】且【特征值仅为value时】、【去掉了那一列用来进行划分的特征数据】的数据集)
"""
def splitDataSet(dataSet, axis, value):
	retDataSet = []
	# 遍历数据集的每一行
	for featVec in dataSet:
		# 找出每一行中属于特征列的值等于value的情况,将这一行保存下来,但保存时不再保存作为特征的数据
		reducedFeatVec = []
		if featVec[axis] == value:
			# 去掉数据集中用来作为特征划分数据的那一列数据
			reducedFeatVec = featVec[:axis] # 向reducedFeatVec中添加axis前的数据
			reducedFeatVec.extend(featVec[axis+1:]) # 向reducedFeatVec中添加axis后的数据
			retDataSet.append(reducedFeatVec)
	return retDataSet


"""
函数说明:
	选择最优特征(计算信息增益)
Parameters:
    dataSet - 数据集
Returns:
    bestFeature - 信息增益最大的特征在数据集中的索引值
"""
def chooseBestFeatureToSplit(dataSet):
	# 减1是减去类别这一列,其他的列才可以作为数据集的特征, 所以共len(dataSet[0]) - 1个特征
	numFeatures = len(dataSet[0]) - 1 
	# 计算数据集的香农熵
	baseEntropy = calcShannonEnt(dataSet)
	#print("%.3f" % baseEntropy)
	# 初始化最优的信息增益值
	bestInfoGain = 0.0
	# 初始化最优特征的索引值
	bestFeature = -1
	for i in range(numFeatures):
		# 获取dataSet中索引为i的特征对应的所有内容(每个特征也包含多个类别)		
		featList = [example[i] for example in dataSet]
		# 比如年龄这一特征对应青年、中年、老年这三类, 所以下一步是进行去重,得到不同的类别
		uniqueVals = set(featList)
		# 初始化这一特征对应的经验条件熵
		newEntropy = 0.0
		# 计算这一特征下不同类别下的香农熵(进而得出这一特征下的信息增益)
		for value in uniqueVals:
			# 根据这一特征i下的这一类别value划分后的数据子集
			subDataSet = splitDataSet(dataSet, i, value)
			#print(subDataSet)
			# 计算子集的概率
			prob = len(subDataSet) / float(len(dataSet))
			# 根据公式计算经验条件熵
			newEntropy += prob * calcShannonEnt(subDataSet)
			#print("第%d个特征的%s类别的经验条件熵为%.3f" % (i, value, newEntropy))
		# 该特征下的信息增益
		infoGain = baseEntropy - newEntropy
		#print("第%d个特征的增益为%.3f" % (i, infoGain))
		# 保存最大信息增益时的相关信息
		if (infoGain > bestInfoGain):
			bestInfoGain = infoGain       
			bestFeature = i  
	return bestFeature


"""
函数说明:
	创建测试数据集
Parameters:
	无
Returns:
    dataSet - 数据集
    labels - 分类属性
"""
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   #返回数据集和分类属性


"""
函数说明:
	计算给定数据集的经验熵(香农熵)
Parameters:
    dataSet - 数据集
Returns:
    shannonEnt - 经验熵(香农熵)
"""
def calcShannonEnt(dataSet):
	numEntries = len(dataSet)
	# 保存每个标签(Label)出现次数的字典
	labelCounts = {}
	for featVec in dataSet:
		currentLabel = featVec[-1]

		# 如果标签(Label)还没有放入统计次数的字典, 则添加进去
		if currentLabel not in labelCounts.keys():
			labelCounts[currentLabel] = 0
		labelCounts[currentLabel] += 1

		# 返回指定键的值,如果键不存在于字典中,将会添加键并将值设为default
		#labelCounts.setdefault(currentLabel, 0) ?????

	# 计算香农熵
	shannonEnt = 0.0
	for key in labelCounts:
		# 选择该标签(Label)的概率
		prob = float(labelCounts[key]) / numEntries
		# 利用香农熵公式进行计算
		shannonEnt -= prob * log(prob, 2)

	return shannonEnt


if __name__ == '__main__':
	dataSet, labels = createDataSet()
	featLabels = []
	myTree = createTree(dataSet, labels, featLabels)
	testVec = [0, 1] # 测试集
	result = classify(myTree, featLabels, testVec)
	if result == 'yes':
		print("放贷")
	elif result == 'no':
		print("不放贷")

运行效果:
在这里插入图片描述
看到这里,细心的朋友可能就会问了,每次做预测都要训练一次决策树?这也太麻烦了吧?有什么好的解决吗?当然!我们需要把这个决策树存起来~

编写代码存储决策树

构造决策树是很耗时的任务,即使处理很小的数据集,如前面的样本数据,也要花费几秒的时间,如果数据集很大,将会耗费很多计算时间。然而用创建好的决策树解决分类问题,则可以很快完成。因此,为了节省计算时间,最好能够在每次执行分类时调用已经构造好的决策树。

为了解决这个问题,需要使用Python模块pickle序列化对象。序列化对象可以在磁盘上保存对象,并在需要的时候读取出来。

假设我们已经得到决策树{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}},下面使用pickle.dump存储决策树:

import pickle

"""
函数说明:
	存储决策树
Parameters:
    inputTree - 已经生成的决策树
    filename - 保存此决策树的文件名
Returns:
	无
"""
def storeTree(inputTree, filename):
	with open(filename, 'wb') as fw:
		pickle.dump(inputTree, fw)


if __name__ == '__main__':
	myTree = {'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
	storeTree(myTree, 'classifierStorage.txt')

运行代码,在该Python文件的相同目录下,会生成一个名为classifierStorage.txt的txt文件,这个文件以二进制进行存储:
在这里插入图片描述
将决策树存储后,使用pickle.load进行载入即可使用。编写代码如下:

import pickle

"""
函数说明:
	读取存储在文件中的决策树
Parameters:
    filename - 保存此决策树的文件名
Returns:
	pickle.load(fr) - 决策树字典
"""
def grabTree(filename):
	fr = open(filename, 'rb')
	return pickle.load(fr)


if __name__ == '__main__':
	myTree = grabTree('classifierStorage.txt')
	print(myTree)

运行效果:
在这里插入图片描述

利用这种方式对决策树进行应用的情况不再赘述。如果要进行应用,不仅仅要保存决策树,还要保存最优标签~

决策树的修剪

决策树生成算法递归地产生决策树,直到不能继续下去未为止。 这样产生的树往往对训练数据的分类很准确,但对未知的测试数据的分类却没有那么准确,即出现过拟合现象。

上述出现过拟合现象的原因在于学习时过多地考虑如何提高对训练数据的正确分类,从而构建出过于复杂的决策树。解决这个问题的办法是考虑决策树的复杂度,对已生成的决策树进行简化。

实战:使用Sklearn中决策树算法

实战题目

预测隐形眼镜类型

实战背景

眼科医生是如何判断患者需要佩戴隐形眼镜的类型的?一旦理解了决策树的工作原理,我们甚至也可以帮助人们判断需要佩戴的镜片类型。

隐形眼镜数据集是非常著名的数据集,它包含很多患者眼部状态的观察条件以及医生推荐的隐形眼镜类型。隐形眼镜类型包括硬材质(hard)、软材质(soft)以及不适合佩戴隐形眼镜(no lenses)。

一共有24组数据,数据的Labels依次是age(年龄)、prescript(症状)、astigmatic(是否散光)、tearRate(眼泪数量)、class(最终的分类结果)。数据如下图所示:
在这里插入图片描述
可以使用已经写好的Python程序手动构建决策树,不过出于继续学习的目的,本文使用Sklearn实现。下面介绍Sklearn模块中的决策树算法。

使用Sklearn构建决策树

sklearn.tree模块提供了决策树模型,用于解决分类问题和回归问题(官网)。方法如下图所示:
在这里插入图片描述
本次实战内容使用的是DecisionTreeClassifierexport_graphviz,前者用于决策树构建,后者用于决策树可视化

DecisionTreeClassifier构建决策树

构建决策树算法的参数:
在这里插入图片描述

  • min_samples_split参数:表示分割到的最小样本为多少。下图中左侧效果图对应min_samples_split=2,表示分割的更加精细。右侧min_samples_split=5,分割的不那么精细,从而避免过拟合,准确率会更高一些。

在这里插入图片描述

发布了53 篇原创文章 · 获赞 33 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/jy_z11121/article/details/103241869