第三章:决策树
自说原理:决策数是用训练集训练出一棵树,树怎么分叉是由属性(特征决定),测试集的属性按照这个树一直走下去,自然就分类了。一般树的深度越小越好,那如何选属性作为根节点,又选择哪一个属性作为第二个分叉点尼?
这就用到了信息熵与信息增益的知识。ID3中选择信息增益大的,C4.5中选择增益率大的。以书中的数据集为例:
属性1 no surfacing | 属性2 flippers | 标签 | |
1. | 1 | 1 | Y |
2. | 1 | 1 | Y |
3. | 1 | 0 | N |
4. | 0 | 1 | N |
5. | 0 | 1 | N |
决策数的算法有很多,ID3,C4.5,CART等等,按照ID3的步骤如下:
1)算数据集的香农熵(也就是信息熵)。,这里其实计算只与标签这一列有关系,此处因为只有两类,所以n=2。H=-0.4log20.4-0.6log20.6=0.97(这个算下来相当于就是不用任何属性瞎猜的不确定度,越大越不好。)
2)这里有两个属性,先按照哪个属性划分尼?这得算各自属性的信息增益。选择信息增益大的。比如算属性1的信息增益,得按照属性1的值先将数据集划分,属性1有两个取值{1,0}。属性11中有{【1,Y】,【1,Y】,【0,N】}。属性10中有{【1,N】,【1,N】}。
然后计算属性1的信息增益,属性11返回的子数据集,算信息熵是Ent1=-2/3log22/3-1/3log21/3;属性10返回的子数据集,算信息熵是Ent2=-1log21;G=0.97-(3/5*Ent1+2/5*Ent2)
故这里程序中分为两步:1)求属性划分的子数据集
2)算信息增益选择最好的数据集划分方式
3)不断的递归,直到属性用完,或者每一个分支下都变成纯的数据。如果用完所有的属性,还不纯,该叶子节点得少数服从多数。
4)将1)~3)构建的树进一步操作变成分类器,即输入无标签的测试集,可以进行预测。
对于这些步骤,分别对应的代码(子函数)如下,首先为了调试方便,自营一个数据集:
1 def createDataSet(): 2 dataSet = [[1, 1, 'yes'], 3 [1, 1, 'yes'], 4 [1, 0, 'no'], 5 [0, 1, 'no'], 6 [0, 1, 'no']] 7 labels = ['no surfacing', 'flippers'] 8 return dataSet, labels
步骤1:
from math import log
1 def calcShannonEnt(dataSet): 2 ''' 3 算数据集的信息熵(也就是根据结果的类别占比,算不确定度) 4 :param dataSet:训练数据集(带标签) 5 :return:信息熵值H 6 ''' 7 # 求list的长度,表示计算参与训练的数据量 8 numEntries = len(dataSet) 9 # 计算分类标签label出现的次数 10 labelCounts = {} 11 for featVec in dataSet: 12 # 将当前实例的标签存储,即每一行数据的最后一个数据代表的是标签 13 currentLabel = featVec[-1] 14 # 为所有可能的分类创建字典,如果当前的键值不存在,则扩展字典并将当前键值加入字典。每个键值都记录了当前类别出现的次数。 15 if currentLabel not in labelCounts.keys(): 16 labelCounts[currentLabel] = 0 17 labelCounts[currentLabel] += 1 18 19 # 对于 label 标签的占比,求出 label 标签的香农熵 20 shannonEnt = 0.0 21 for key in labelCounts: 22 # 使用所有类标签的发生频率计算类别出现的概率。 23 prob = float(labelCounts[key])/numEntries 24 # 计算香农熵,以 2 为底求对数 25 shannonEnt -= prob * log(prob, 2) 26 return shannonEnt
步骤2)中的1):
1 def splitDataSet(dataSet, index, value): 2 ''' 3 求属性划分后的子数据集 4 :param dataSet: 数据集(带标签) 5 :param index: 列数(从0开始),比如按照第一个属性划分,那这个index=0 6 :param value:该属性(作为划分标准的属性)下的某一个值 7 :return: 8 ''' 9 # -----------切分数据集的第一种方式 start------------------------------------ 10 retDataSet = [] 11 for featVec in dataSet: 12 # index列为value的数据集【该数据集需要排除index列】 13 # 判断index列的值是否为value 14 if featVec[index] == value: 15 # chop out index used for splitting 16 # [:index]表示前index行,即若 index 为2,就是取 featVec 的前 index 行 17 reducedFeatVec = featVec[:index] 18 ''' 19 请百度查询一下: extend和append的区别 20 list.append(object) 向列表中添加一个对象object 21 list.extend(sequence) 把一个序列seq的内容添加到列表中 22 1、使用append的时候,是将new_media看作一个对象,整体打包添加到music_media对象中。 23 2、使用extend的时候,是将new_media看作一个序列,将这个序列和music_media序列合并,并放在其后面。 24 result = [] 25 result.extend([1,2,3]) 26 print result 27 result.append([4,5,6]) 28 print result 29 result.extend([7,8,9]) 30 print result 31 结果: 32 [1, 2, 3] 33 [1, 2, 3, [4, 5, 6]] 34 [1, 2, 3, [4, 5, 6], 7, 8, 9] 35 ''' 36 reducedFeatVec.extend(featVec[index+1:]) 37 # [index+1:]表示从跳过 index 的 index+1行,取接下来的数据 38 # 收集结果值 index列为value的行【该行需要排除index列】 39 retDataSet.append(reducedFeatVec) 40 # -----------切分数据集的第一种方式 end------------------------------------ 41 42 # # -----------切分数据集的第二种方式 start------------------------------------ 43 # retDataSet = [data for data in dataSet for i, v in enumerate(data) if i == axis and v == value] 44 # # -----------切分数据集的第二种方式 end------------------------------------ 45 return retDataSet
比如这里的测试的数据集传入,index=0,value=1,得到的结果是: [[1, 'y'], [1, 'y'], [0, 'n']]
步骤2)中的2):
1 def chooseBestFeatureToSplit(dataSet): 2 """chooseBestFeatureToSplit(选择最好的特征) 3 Args: 4 dataSet 数据集 5 Returns: 6 bestFeature 最优的特征列的序号(int) 7 """ 8 # 求有多少个属性 9 numFeatures = len(dataSet[0]) - 1 10 # label的信息熵 11 baseEntropy = calcShannonEnt(dataSet) 12 # 初始化最优的信息增益值(越大越好), 和最优的Featurn编号 13 bestInfoGain, bestFeature = 0.0, -1 14 # iterate over all the features 15 for i in range(numFeatures): 16 # 获取第i个属性下的所有值 17 featList = [example[i] for example in dataSet] 18 # 获取剔重后的集合,使用set对list数据进行去重 19 uniqueVals = set(featList) 20 # 创建一个临时的信息熵 21 newEntropy = 0.0 22 # 遍历某一列的value集合,计算该列的信息熵 23 # 遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集,计算数据集的新熵值,并对所有唯一特征值得到的熵求和。 24 for value in uniqueVals: 25 subDataSet = splitDataSet(dataSet, i, value) 26 prob = len(subDataSet)/float(len(dataSet)) 27 newEntropy += prob * calcShannonEnt(subDataSet) 28 # gain[信息增益]: 划分数据集前后的信息变化, 获取信息熵最大的值 29 # 信息增益是熵的减少或者是数据无序度的减少。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。 30 infoGain = baseEntropy - newEntropy 31 print('属性',i,'的信息增益infoGain=', infoGain, ' 标签的信息熵=', baseEntropy,' Ent和=', newEntropy) 32 if (infoGain > bestInfoGain): 33 bestInfoGain = infoGain 34 bestFeature = i 35 return bestFeature
步骤3):
1 import operator 2 from math import log 3 4 def majorityCnt(classList): 5 """当属性用完,叶节点还是不纯时,得少数服从多数 6 Args: 7 classList label列的集合 8 Returns: 9 bestFeature 最优的特征列 10 """ 11 classCount = {} 12 for vote in classList: 13 if vote not in classCount.keys(): 14 classCount[vote] = 0 15 classCount[vote] += 1 16 # 倒叙排列classCount得到一个字典集合,然后取出第一个就是结果(yes/no),即出现次数最多的结果 17 sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True) 18 # print 'sortedClassCount:', sortedClassCount 19 return sortedClassCount[0][0] 20 21 def createTree(dataSet, labels): 22 ''' 23 训练生成一棵数,只不过不是树状,是字典一层一层的 24 :param dataSet:训练数据集 25 :param labels:按顺序存放属性名字的列表 26 :return:一棵用一层一层字典表示的数 27 ''' 28 classList = [example[-1] for example in dataSet] 29 # 如果数据集的最后一列(标签列)的第一个值出现的次数=整个集合的数量,也就说只有一个类别,就只直接返回结果就行 30 # 第一个停止条件:所有的类标签完全相同,则直接返回该类标签。 31 if classList.count(classList[0]) == len(classList): 32 return classList[0] 33 # 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果 34 # 第二个停止条件:使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。 35 if len(dataSet[0]) == 1: 36 return majorityCnt(classList) 37 38 # 选择最优的列,得到最优列对应的label含义 39 bestFeat = chooseBestFeatureToSplit(dataSet) 40 # 获取label的名称 41 bestFeatLabel = labels[bestFeat] 42 # 初始化myTree 43 myTree = {bestFeatLabel: {}} 44 # 注:labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改 45 # 所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示'no surfacing' is not in list 46 del(labels[bestFeat]) 47 # 取出最优列,然后它的branch做分类 48 featValues = [example[bestFeat] for example in dataSet] 49 uniqueVals = set(featValues) 50 for value in uniqueVals: 51 # 求出剩余的标签label 52 subLabels = labels[:] 53 # 遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree() 54 myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels) 55 # print 'myTree', value, myTree 56 return myTree
测试:
data,lab = createDataSet()
mytree = createTree(data, lab)
print(mytree)
结果是:{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}这其实就是一棵树。
对于这个返回的结果不像实际的树状那么直观,而决策树的一大优点就是直观,书中3.2节中用matplotlib进行画数型图直观化。这里省略了。
步骤4):
def classify(inputTree, featLabels, testVec): """利用也训练出的树进行预测分类 Args: inputTree 决策树模型,也就是以有的树,这里是createTree函数返回的结果myTree featLabels Feature标签对应的名称列表 testVec 测试输入的数据 Returns: classLabel 分类的结果值,需要映射label才能知道名称 """ # 获取tree的根节点对于的key值 firstStr = list(inputTree.keys())[0] # 通过key得到根节点对应的value secondDict = inputTree[firstStr] # 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类 featIndex = featLabels.index(firstStr) # 测试数据,找到根节点对应的label位置,也就知道从输入的数据的第几位来开始分类 key = testVec[featIndex] valueOfFeat = secondDict[key] # print('+++', firstStr, 'xxx', secondDict, '---', key, '>>>', valueOfFeat) # 判断分枝是否结束: 判断valueOfFeat是否是dict类型 if isinstance(valueOfFeat, dict): classLabel = classify(valueOfFeat, featLabels, testVec) else: classLabel = valueOfFeat return classLabel
注意步骤3)中的labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改。在调试时注意。
data,lab = createDataSet()
mytree = createTree(data, lab)
x = classify(mytree,['no surfacing', 'flippers'],[1,1])
print(x)
结果是:yes
此处生出另一个问题就是,每次预测都要训练,是很费事的,所以想办法将训练好的树存储起来。
def storeTree(inputTree, filename): import pickle # -------------- 第一种方法 start -------------- fw = open(filename, 'wb') pickle.dump(inputTree, fw) fw.close() # -------------- 第一种方法 end -------------- # -------------- 第二种方法 start -------------- with open(filename, 'wb') as fw: pickle.dump(inputTree, fw) # -------------- 第二种方法 start -------------- def grabTree(filename): import pickle fr = open(filename,'rb') return pickle.load(fr)
>>trees.storeTree(myTree,'1.txt')
>>trees.grabTree('1.txt')
到此基本ID3搞定了。完整的可以看这里(Python3运行略有错误,在上面的代码我已经改正)。但是好像有缺少剪枝什么的。
以上是自写决策树模块,但是sklearn中也有决策数模块,具体使用方法看这里(别人的文章,哈哈)
这里我也简单用sklearn中的决策树模块预测了上一章中的花蕊数据集。
这之前与上一章基本一样,所以比较省略
主要是这一张截图部分不同
这里没有将数据归一化准确率就高达0.96666,上一章kNN没有归一化时准确率是0.9。这样看似乎是决策树好,但是用上一章的归一化方法处理后,决策树其它参数不变,其预测准确率又变成了0.9333333333333333