一、决策树的构建
上篇博客已经讲到,构建决策树,我们有三种算法:ID3,C4.5和CART。这一篇,我们只介绍,ID3算法,其他两种后面介绍。
ID3算法
也就是使用使用信息增益作为判断指标,用于选择最优的划分特征。递归调用上述算法,不断产生分支,直到所有特征的信息增益均很小或没有特征可以选择为止。
1. 示例分析:
首先看我们要分析的数据:
通过上篇写到的信息增益的计算方法,计算各个特征的信息增益,我们发现是否有自己的房子,有最大的信息增益,所以我们把有自己的房子,作为根节点的划分标准。
将训练集D划分为两个子集D1(有自己的房子为”是”)和D2(有自己的房子为”否”)。由于D1只有同一类的样本点,所以它成为一个叶结点,结点的类标记为“是”。
然后,我们对D2的A1(年龄),A2(有工作)和A4(信贷情况)特征中选择最优划分特征,计算各个特征的信息增益:
可以看出A2信息增益最大,所以选择A2(有工作)作为划分特征。然后根据是否有工作,再次进行划分。一个对应”是”(有工作)的子结点,包含3个样本,它们属于同一类,所以这是一个叶结点,类标记为”是”;另一个是对应”否”(无工作)的子结点,包含6个样本,它们也属于同一类,所以这也是一个叶结点,类标记为”否”。
生成决策树:
2. 编写代码构建决策树
使用字典存储决策树的结构,上小节的决策树,用字典可以表示为:
{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
创建函数majorityCnt统计classList中出现此处最多的元素(类标签),创建函数createTree用来递归构建决策树。
递归创建决策树时,递归有两个终止条件:
一:所有的类标签完全相同,则直接返回该类标签;
二:使用完了所有特征,仍然不能将数据划分仅包含唯一类别的分组,即决策树构建失败,特征不够用。此时说明数据纬度不够,由于第二个停止条件无法简单地返回唯一的类标签,这里挑选出现数量最多的类别作为返回值。
- 创建决策树所需要用到的函数:
1、createDataSet():创建数据集
2、calShannoEnt(dataSet):计算香农熵
3、splitDataSet(dataSet,axis, value):根据特征划分数据集
4、chooseBestFeaturesToSplit(dataSet):选择最佳特征
5、majorityCnt(classList):确定类别
6、createTree(dataSet, labels, featLabels):创建决策树
创建数据:
from math import log
from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
import operator
#--------------------------------------------------------------
def createDataSet():
"""
函数说明:创建测试数据集
参数:无
return: dataSet-数据集
labels-分类属性
"""
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,1,'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
计算香农熵:
def calShannoEnt(dataSet):
"""
函数说明:
计算数据集的香农熵
param :dataSet-数据集
return: shannonEnt-香农熵
"""
numEntires = len(dataSet) #数据集行数
labelCounts = {} #该字典用于保存每个标签出现的次数
for featVec in dataSet:
currentLabel = featVec[-1] #for循环,来处理每一个数据的标签
if currentLabel not in labelCounts.keys(): #对字典进行初始化
labelCounts[currentLabel] = 0 #令两类初始值分别为0
labelCounts[currentLabel] += 1 #统计数据
shannonEnt = 0.0
for key in labelCounts: #根据公式,利用循环求香农熵
prob = float(labelCounts[key])/numEntires
shannonEnt -= prob*log(prob,2)
return shannonEnt
#-------------------------------------------------------------
划分数据:
def splitDataSet(dataSet,axis, value):
"""
函数说明:
对数据集根据某个特征进行划分
:param dataSet: 被划分的数据集
:param axis: 划分根据的特征
:param value: 需要返回的特征的值
:return: 无
"""
retDataSet = [] #建立空列表,存储返回的数据集
for featVec in dataSet: #遍历数据,一个对象一个对象的进行操作
if featVec[axis] == value: #找到axis特征,等于value值得数据
reducedFeatVec = featVec[:axis] #选出去除axis特征,只保留其他特征的数据
reducedFeatVec.extend(featVec[axis+1:]) #保留后面的
retDataSet.append(reducedFeatVec) #对数据进行连接,按照列表形式
return retDataSet #返回划分后被取出的数据集,即满足条件的数据
#---------------------------------------------------------------------------
选择最佳分类属性:
def chooseBestFeaturesToSplit(dataSet):
"""
函数说明:
选择最佳的分类属性
:param :dataSet-数据集
:return: bestFeature-最佳划分属性
"""
numFeatures = len(dataSet[0]) - 1 #特征数量,去除最后一个标签值
baseEntropy = calShannoEnt(dataSet) #计算原始数据的香农熵
bestInfoGain = 0.0 #信息增益初始化
bestFeature = -1 #最有特征索引
for i in range(numFeatures): #遍历所有特征
#该特征的所有值,组成的列表
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) #创建集合,元素不可重复
newEntropy = 0.0 #经验条件熵
for value in uniqueVals: #计算信息增益
#根据第i特征,value值对数据进行划分
subDataSet = splitDataSet(dataSet, i, value)
#求划分后数据的经验条件熵
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calShannoEnt(subDataSet)
infoGain = baseEntropy - newEntropy #求划分前后的信息增益
print("第%d个特征的增益是%.3f" %(i,infoGain))
if(infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
#--------------------------------------------------------------
选择类别:
在决策树构建过程中,我们通过不断地划分,可能一直到遍历所有特征,依然不能彻底划分数据,也就是说,划分到最后,包含的数据类别存在不一样。此时我们选择,这多个类别中,出现次数最多的作为该部分数据的类别。
def majorityCnt(classList):
"""
函数说明:
当划分结束,遍历所有特征,但是依然不能彻底划分数据时
将这多个类别中,出现次数最多的作为该类别
:param classList-类别列表
:return: sortedClassCount[0][0]-类别
"""
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]
#--------------------------------------------------------------
创建决策树:
def createTree(dataSet, labels, featLabels):
"""
函数说明:
创建决策树
:param dataSet-训练集
:param labels-训练集数据标签
:param featLabels:
:return:
"""
classList = [example[-1] for example in dataSet] #取分类标签(是否放贷:yes or no)
#注意条件,对列表的某一类计数,若为列表长度,则类别完全相同
if classList.count(classList[0]) == len(classList): #如果类别完全相同则停止继续划分
return classList[0]
if len(dataSet[0]) == 1: #遍历完所有特征时返回出现次数最多的类标签
return majorityCnt(classList)
bestFeat = chooseBestFeaturesToSplit(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: #遍历特征创建决策树
#通过此句实现字典的叠加
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),labels,featLabels)
return myTree
#-------------------------------------------------------------
主函数创建决策树:
if __name__ == '__main__':
dataSet, labels = createDataSet()
featLabels = []
myTree = createTree(dataSet, labels, featLabels)
print(myTree)
运行后我们可以得到,刚开始,字典类型的决策树。
二、测试决策树:
测试函数:
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':
classLabel = classify(secondDict[key], featLabels, testVec)
else:
classLabel = secondDict[key]
return classLabel
主函数:
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('不放贷')