wuyijia

导航

决策树decision tree

1.决策树是一种监督学习方法,既可以处理分类问题,也可以处理回归问题,甚至包括多输出(multioutput)任务.

2.熵泛指某些物质系统状态的一种量度,某些物质系统状态可能出现的程度。在信息论与概率统计中,熵(entropy)是表示随机变量不确定性的变量。熵越大随机变量的不确定性越强,那么从特征重要度的角度来说,该特征越不具备较强的表征分裂能力。 信息越有序,信息熵越低。

3.决策树的众多特性之一就是, 它不需要太多的数据预处理, 尤其是不需要进行特征的缩放或者归一化。

4.决策树模型呈树形结构,在分类问题中,表示基于特征对实例进行分类的过程。它可以认为是 if-then 规则的集合,也可以认为是定义在特征空间与类空间上的条件概率分布。

5.决策树学习通常包括 3 个步骤:特征选择、决策树的生成和决策树的修剪。

6.内部结点表示一个特征或属性(features),叶结点表示一个类(labels)。

7.信息增益(information gain):在划分数据集前后信息发生的变化称为信息增益。

  1. 缺点:容易过拟合。

8.为了防止“过拟合”造成较差的泛化能力,常用策略就是使用“剪枝“进行去噪,进而学习更加朴质的特征,学到数据的本质。

9.这种现象就是“过拟合”,本质原因在于学习器在学习的过程中把训练特征中的一个非朴素特质当做了潜在的真实分布造成的。

10.预剪枝:在树生成的阶段进行剪枝,后剪枝:在树生成后,在检查需要去掉哪些分支。

11.ID3算法起源于概念学习系统(CLS),以信息熵的下降速度为选取测试属性的标准,即在每个节点选取还尚未被用来划分的具有最高信息增益的属性作为划分标准,然后继续这个过程,直到生成的决策树能完美分类训练样例。

12.2.4.2 ID3算法优缺点总结

  • 模型结构:N叉树,如果特征的取值较多,计算量非常可观。

  • 模型目标:适合处理分类问题。

  • 特征相关:没有给出连续型特征的处理方法,适合处理离散型特征,对于缺失的特征没有需要在预处理阶段自行解决缺失值问题。

  • 模型优化:没有模型的剪纸优化操作,容易发生过拟合。

  • 内存消耗:随着样本的特征空间维度迅速膨胀。

  • 计算效率:N叉树,随着样本的特征空间进行分支,计算耗时较高。

  1 import operator
  2 from math import log
  3 import decisionTreePlot as dtPlot
  4 from collections import Counter
  5 
  6 
  7 def createDataSet():
  8     """
  9     Desc:
 10         创建数据集
 11     Args:
 12         无需传入参数
 13     Returns:
 14         返回数据集和对应的label标签
 15     """
 16     # dataSet 前两列是特征,最后一列对应的是每条数据对应的分类标签
 17     dataSet = [[1, 1, 'yes'],
 18                [1, 1, 'yes'],
 19                [1, 0, 'no'],
 20                [0, 1, 'no'],
 21                [0, 1, 'no']]
 22     # dataSet = [['yes'],
 23     #         ['yes'],
 24     #         ['no'],
 25     #         ['no'],
 26     #         ['no']]
 27     # labels  露出水面   脚蹼,注意: 这里的labels是写的 dataSet 中特征的含义,并不是对应的分类标签或者说目标变量
 28     labels = ['no surfacing', 'flippers']
 29     # 返回
 30     return dataSet, labels
 31 
 32 
 33 def calcShannonEnt(dataSet):
 34     """
 35     Desc: 
 36         calculate Shannon entropy -- 计算给定数据集的香农熵
 37     Args:
 38         dataSet -- 数据集
 39     Returns:
 40         shannonEnt -- 返回 每一组 feature 下的某个分类下,香农熵的信息期望
 41     """
 42     # -----------计算香农熵的第一种实现方式start--------------------------------------------------------------------------------
 43     # 求list的长度,表示计算参与训练的数据量
 44     numEntries = len(dataSet)
 45     # 下面输出我们测试的数据集的一些信息
 46     # 例如: <type 'list'> numEntries:  5 是下面的代码的输出
 47     # print(type(dataSet), 'numEntries: ', numEntries)
 48 
 49     # 计算分类标签label出现的次数
 50     labelCounts = {}
 51     # the the number of unique elements and their occurance
 52     for featVec in dataSet:
 53         # 将当前实例的标签存储,即每一行数据的最后一个数据代表的是标签
 54         currentLabel = featVec[-1]
 55         # 为所有可能的分类创建字典,如果当前的键值不存在,则扩展字典并将当前键值加入字典。每个键值都记录了当前类别出现的次数。
 56         if currentLabel not in labelCounts.keys():
 57             labelCounts[currentLabel] = 0
 58         labelCounts[currentLabel] += 1
 59         # print('-----', featVec, labelCounts)
 60 
 61     # 对于label标签的占比,求出label标签的香农熵
 62     shannonEnt = 0.0
 63     for key in labelCounts.keys():
 64         # 使用所有类标签的发生频率计算类别出现的概率。
 65         prob = float(labelCounts[key])/numEntries
 66         # log base 2
 67         # 计算香农熵,以 2 为底求对数
 68         shannonEnt -= prob * log(prob, 2)
 69         # print('---', prob, prob * log(prob, 2), shannonEnt)
 70     # -----------计算香农熵的第一种实现方式end--------------------------------------------------------------------------------
 71 
 72     # # -----------计算香农熵的第二种实现方式start--------------------------------------------------------------------------------
 73     # # 统计标签出现的次数
 74     # label_count = Counter(data[-1] for data in dataSet)
 75     # # 计算概率
 76     # probs = [p[1] / len(dataSet) for p in label_count.items()]
 77     # # 计算香农熵
 78     # shannonEnt = sum([-p * log(p, 2) for p in probs])
 79     # # -----------计算香农熵的第二种实现方式end--------------------------------------------------------------------------------
 80     return shannonEnt
 81 
 82 
 83 def splitDataSet(dataSet, index, value):
 84     """
 85     Desc: 
 86         划分数据集
 87         splitDataSet(通过遍历dataSet数据集,求出index对应的colnum列的值为value的行)
 88         就是依据index列进行分类,如果index列的数据等于 value的时候,就要将 index 划分到我们创建的新的数据集中
 89     Args:
 90         dataSet  -- 数据集                 待划分的数据集
 91         index -- 表示每一行的index列        划分数据集的特征
 92         value -- 表示index列对应的value值   需要返回的特征的值。
 93     Returns:
 94         index 列为 value 的数据集【该数据集需要排除index列】
 95     """
 96     # -----------切分数据集的第一种方式 start------------------------------------
 97     retDataSet = []
 98     for featVec in dataSet: 
 99         # index列为value的数据集【该数据集需要排除index列】
100         # 判断index列的值是否为value
101         if featVec[index] == value: #一行一行读
102             # chop out index used for splitting
103             # [:index]表示前index行,即若 index 为2,就是取 featVec 的前 index 行
104             reducedFeatVec = featVec[:index]
105             '''
106             请百度查询一下:  extend和append的区别
107             list.append(object) 向列表中添加一个对象object
108             list.extend(sequence) 把一个序列seq的内容添加到列表中
109             1、使用append的时候,是将new_media看作一个对象,整体打包添加到music_media对象中。
110             2、使用extend的时候,是将new_media看作一个序列,将这个序列和music_media序列合并,并放在其后面。
111             result = []
112             result.extend([1,2,3])
113             print(result)
114             result.append([4,5,6])
115             print(result)
116             result.extend([7,8,9])
117             print(result)
118             结果: 
119             [1, 2, 3]
120             [1, 2, 3, [4, 5, 6]]
121             [1, 2, 3, [4, 5, 6], 7, 8, 9]
122             '''
123             reducedFeatVec.extend(featVec[index+1:])  #一个样本数据
124             # [index+1:]表示从跳过 index 的 index+1行,取接下来的数据
125             # 收集结果值 index列为value的行【该行需要排除index列】
126             retDataSet.append(reducedFeatVec)  #子数据集
127     # -----------切分数据集的第一种方式 end------------------------------------
128 
129     # # -----------切分数据集的第二种方式 start------------------------------------
130     # retDataSet = [data[:index] + data[index + 1:] for data in dataSet for i, v in enumerate(data) if i == index and v == value]
131     # # -----------切分数据集的第二种方式 end------------------------------------
132     return retDataSet
133 
134 
135 def chooseBestFeatureToSplit(dataSet):
136     """
137     Desc:
138         选择切分数据集的最佳特征
139     Args:
140         dataSet -- 需要切分的数据集
141     Returns:
142         bestFeature -- 切分数据集的最优的特征列
143     """
144 
145     # -----------选择最优特征的第一种方式 start------------------------------------
146     # 求第一行有多少列的 Feature, 最后一列是label列嘛
147     numFeatures = len(dataSet[0]) - 1
148     # label的信息熵
149     baseEntropy = calcShannonEnt(dataSet)
150     # 最优的信息增益值, 和最优的Featurn编号
151     bestInfoGain, bestFeature = 0.0, -1
152     # iterate over all the features
153     for i in range(numFeatures):
154         # create a list of all the examples of this feature
155         # 获取每一个实例的第i+1个feature,组成list集合
156         featList = [example[i] for example in dataSet]  #[example[0] for example in dataSet]:[1, 1, 1, 0, 0]
157         # get a set of unique values
158         # 获取剔重后的集合,使用set对list数据进行去重
159         uniqueVals = set(featList)
160         # 创建一个临时的信息熵
161         newEntropy = 0.0
162         # 遍历某一列的value集合,计算该列的信息熵 
163         # 遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集,计算数据集的新熵值,并对所有唯一特征值得到的熵求和。
164         for value in uniqueVals:
165             subDataSet = splitDataSet(dataSet, i, value)
166             prob = len(subDataSet)/float(len(dataSet))
167             newEntropy += prob * calcShannonEnt(subDataSet)
168         # gain[信息增益]: 划分数据集前后的信息变化, 获取信息熵最大的值
169         # 信息增益是熵的减少或者是数据无序度的减少。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。
170         infoGain = baseEntropy - newEntropy
171         print('infoGain=', infoGain, 'bestFeature=', i, baseEntropy, newEntropy)
172         if (infoGain > bestInfoGain):
173             bestInfoGain = infoGain
174             bestFeature = i
175     return bestFeature
176     # -----------选择最优特征的第一种方式 end------------------------------------
177 
178     # # -----------选择最优特征的第二种方式 start------------------------------------
179     # # 计算初始香农熵
180     # base_entropy = calcShannonEnt(dataSet)
181     # best_info_gain = 0
182     # best_feature = -1
183     # # 遍历每一个特征
184     # for i in range(len(dataSet[0]) - 1):
185     #     # 对当前特征进行统计
186     #     feature_count = Counter([data[i] for data in dataSet])
187     #     # 计算分割后的香农熵
188     #     new_entropy = sum(feature[1] / float(len(dataSet)) * calcShannonEnt(splitDataSet(dataSet, i, feature[0])) \
189     #                    for feature in feature_count.items())
190     #     # 更新值
191     #     info_gain = base_entropy - new_entropy
192     #     print('No. {0} feature info gain is {1:.3f}'.format(i, info_gain))
193     #     if info_gain > best_info_gain:
194     #         best_info_gain = info_gain
195     #         best_feature = i
196     # return best_feature
197     # # -----------选择最优特征的第二种方式 end------------------------------------
198 
199 
200 def majorityCnt(classList):
201     """
202     Desc:
203         选择出现次数最多的一个结果
204     Args:
205         classList label列的集合
206     Returns:
207         bestFeature 最优的特征列
208     """
209     # -----------majorityCnt的第一种方式 start------------------------------------
210     classCount = {}
211     for vote in classList:
212         if vote not in classCount.keys():
213             classCount[vote] = 0
214         classCount[vote] += 1
215     # 倒叙排列classCount得到一个字典集合,然后取出第一个就是结果(yes/no),即出现次数最多的结果
216     sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
217     print('sortedClassCount:', sortedClassCount)
218     return sortedClassCount[0][0]
219     # -----------majorityCnt的第一种方式 end------------------------------------
220 
221     # # -----------majorityCnt的第二种方式 start------------------------------------
222     # major_label = Counter(classList).most_common(1)[0]
223     # return major_label
224     # # -----------majorityCnt的第二种方式 end------------------------------------
225 
226 
227 def createTree(dataSet, labels):
228     """
229     Desc:
230         创建决策树
231     Args:
232         dataSet -- 要创建决策树的训练数据集
233         labels -- 训练数据集中特征对应的含义的labels,不是目标变量
234     Returns:
235         myTree -- 创建完成的决策树
236     """
237     classList = [example[-1] for example in dataSet]
238     # 如果数据集的最后一列的第一个值出现的次数=整个集合的数量,也就说只有一个类别,就只直接返回结果就行
239     # 第一个停止条件: 所有的类标签完全相同,则直接返回该类标签。
240     # count() 函数是统计括号中的值在list中出现的次数
241     if classList.count(classList[0]) == len(classList):
242         return classList[0]
243     # 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果
244     # 第二个停止条件: 使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。
245     if len(dataSet[0]) == 1:
246         return majorityCnt(classList)
247 
248     # 选择最优的列,得到最优列对应的label含义
249     bestFeat = chooseBestFeatureToSplit(dataSet)
250     # 获取label的名称
251     bestFeatLabel = labels[bestFeat]
252     # 初始化myTree
253     myTree = {bestFeatLabel: {}}
254     # 注: labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改
255     # 所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示'no surfacing' is not in list
256     del(labels[bestFeat])
257     # 取出最优列,然后它的branch做分类
258     featValues = [example[bestFeat] for example in dataSet]
259     uniqueVals = set(featValues)
260     for value in uniqueVals:
261         # 求出剩余的标签label
262         subLabels = labels[:]
263         # 遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree()
264         myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
265         print('myTree',bestFeat, value, myTree)
266     return myTree
267 
268 
269 def classify(inputTree, featLabels, testVec):
270     """
271     Desc:
272         对新数据进行分类
273     Args:
274         inputTree  -- 已经训练好的决策树模型  myTree 0 1 {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
275         featLabels -- Feature标签对应的名称,不是目标变量
276         testVec    -- 测试输入的数据
277     Returns:
278         classLabel -- 分类的结果值,需要映射label才能知道名称
279     """
280     # 获取tree的根节点对于的key值
281     firstStr = list(inputTree.keys())[0] #0
282     # 通过key得到根节点对应的value
283     secondDict = inputTree[firstStr] #{0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}
284     # 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类
285     featIndex = featLabels.index(firstStr) #0
286     # 测试数据,找到根节点对应的label位置,也就知道从输入的数据的第几位来开始分类
287     key = testVec[featIndex] #1
288     valueOfFeat = secondDict[key]
289     print('featIndex',featIndex,'key',key) #{'flippers': {0: 'no', 1: 'yes'}}
290     print('+++', firstStr, 'xxx', secondDict, '---', key, '>>>', valueOfFeat)
291 
292     # 判断分枝是否结束: 判断valueOfFeat是否是dict类型
293     if isinstance(valueOfFeat, dict):
294         classLabel = classify(valueOfFeat, featLabels, testVec)
295     else:
296         classLabel = valueOfFeat
297     return classLabel
298 
299 """
300 featIndex 0 key 1
301 +++ no surfacing xxx {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}} --- 1 >>> {'flippers': {0: 'no', 1: 'yes'}}
302 featIndex 1 key 1
303 +++ flippers xxx {0: 'no', 1: 'yes'} --- 1 >>> yes
304 yes
305 """
306 
307 def storeTree(inputTree, filename):
308     """
309     Desc:
310         将之前训练好的决策树模型存储起来,使用 pickle 模块
311     Args:
312         inputTree -- 以前训练好的决策树模型
313         filename -- 要存储的名称
314     Returns:
315         None
316     """
317     import pickle
318     # -------------- 第一种方法 start --------------
319     fw = open(filename, 'wb')
320     pickle.dump(inputTree, fw)
321     fw.close()
322     # -------------- 第一种方法 end --------------
323 
324     # -------------- 第二种方法 start --------------
325     with open(filename, 'wb') as fw:
326         pickle.dump(inputTree, fw)
327     # -------------- 第二种方法 start --------------
328 
329 
330 def grabTree(filename):
331     """
332     Desc:
333         将之前存储的决策树模型使用 pickle 模块 还原出来
334     Args:
335         filename -- 之前存储决策树模型的文件名
336     Returns:
337         pickle.load(fr) -- 将之前存储的决策树模型还原出来
338     """
339     import pickle
340     fr = open(filename, 'rb')
341     return pickle.load(fr)
342 
343 
344 def fishTest():
345     """
346     Desc:
347         对动物是否是鱼类分类的测试函数,并将结果使用 matplotlib 画出来
348     Args:
349         None
350     Returns:
351         None
352     """
353     # 1.创建数据和结果标签
354     myDat, labels = createDataSet()
355     # print(myDat, labels)
356 
357     # 计算label分类标签的香农熵
358     # calcShannonEnt(myDat)
359 
360     # # 求第0列 为 1/0的列的数据集【排除第0列】
361     # print('1---', splitDataSet(myDat, 0, 1))
362     # print('0---', splitDataSet(myDat, 0, 0))
363 
364     # # 计算最好的信息增益的列
365     # print(chooseBestFeatureToSplit(myDat))
366 
367     import copy
368     myTree = createTree(myDat, copy.deepcopy(labels))
369     print(myTree)
370     print('================================')
371     # [1, 1]表示要取的分支上的节点位置,对应的结果值
372     print(classify(myTree, labels, [1, 1]))
373 
374     # 画图可视化展现
375     dtPlot.createPlot(myTree)
376     
377     
378 fishTest()
379 
380 """
381 infoGain= 0.4199730940219749 bestFeature= 0 baseEntropy= 0.9709505944546686 newEntropy= 0.5509775004326937
382 infoGain= 0.17095059445466854 bestFeature= 1 baseEntropy= 0.9709505944546686 newEntropy= 0.8
383 infoGain= 0.9182958340544896 bestFeature= 0 baseEntropy= 0.9182958340544896 newEntropy= 0.0
384 {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
385 ================================
386 +++ no surfacing xxx {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}} --- 1 >>> {'flippers': {0: 'no', 1: 'yes'}}
387 +++ flippers xxx {0: 'no', 1: 'yes'} --- 1 >>> yes
388 yes
389 """

 

posted on 2023-05-10 20:51  小吴要努力  阅读(52)  评论(0)    收藏  举报