From edddbf1b132aeaa81d07a8968717d51085649cf4 Mon Sep 17 00:00:00 2001 From: chenyyx Date: Thu, 17 Aug 2017 15:09:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=203.=E5=86=B3=E7=AD=96?= =?UTF-8?q?=E6=A0=91.md=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/3.决策树.md | 223 ++++++++++++---------- images/3.DecisionTree/DT_海洋生物数据.png | Bin 0 -> 8635 bytes 2 files changed, 123 insertions(+), 100 deletions(-) create mode 100644 images/3.DecisionTree/DT_海洋生物数据.png diff --git a/docs/3.决策树.md b/docs/3.决策树.md index 7863a28c..e4a57698 100644 --- a/docs/3.决策树.md +++ b/docs/3.决策树.md @@ -4,35 +4,29 @@ ![决策树_首页](/images/3.DecisionTree/DecisionTree_headpage_xy.png "决策树首页") -## 决策树简介 +## 决策树 概述 + +`决策树(Decision Tree)算法主要用来处理分类问题,是最经常使用的数据挖掘算法之一。` + +## 决策树 场景 + +一个叫做 "二十个问题" 的游戏,游戏的规则很简单:参与游戏的一方在脑海中想某个事物,其他参与者向他提问,只允许提 20 个问题,问题的答案也只能用对或错回答。问问题的人通过推断分解,逐步缩小待猜测事物的范围,最后得到游戏的答案。 + +一个邮件分类系统,大致工作流程如下: ![决策树-流程图](/images/3.DecisionTree/决策树-流程图.jpg "决策树示例流程图") -* 引入 +``` +首先检测发送邮件域名地址。如果地址为 myEmployer.com, 则将其放在分类 "无聊时需要阅读的邮件"中。 +如果邮件不是来自这个域名,则检测邮件内容里是否包含单词 "曲棍球" , 如果包含则将邮件归类到 "需要及时处理的朋友邮件", +如果不包含则将邮件归类到 "无需阅读的垃圾邮件" 。 +``` - 你是否玩过一个叫做 "二十个问题" 的游戏,游戏的规则很简单:参与游戏的一方在脑海中想某个事物,其他参与者向他提问,只允许提 20 个问题,问题的答案也只能用对或错回答。问问题的人通过推断分解,逐步缩小待猜测事物的范围。决策树的工作原理与 20 个问题类似,用户输入一系列数据,然后给出游戏的答案。 +## 决策树 原理 -* 简要介绍 +### 决策树 须知概念 - 根据一些 feature 进行分类,每个节点提一个问题,通过判断,将数据分为两类,再继续提问。这些数据是根据已有数据学习出来的,再投入新数据的时候,就可以根据这棵树上的问题,将数据划分到合适的叶子上。 - -* 决策树的任务 - - 第二章的 k-近邻算法可以完成很多分类任务,但是它最大的缺点就是无法给出数据的内在含义。决策树的主要优势就在于数据形式非常容易理解。 - - 接下来构造的决策树算法能够读取数据集合,构建类似于上图的决策树。决策树的一个重要任务是为了理解数据中所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,并从中提取出一系列规则,这些机器根据数据集创建规则的过程,就是机器学习的过程。 - - 专家系统中经常使用决策树,而且决策树给出结果往往可以匹敌在当前领域具有几十年工作经验的人类专家。 - -* 决策树的特点 - - 优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。 - - 缺点:可能会产生过度匹配问题。 - - 适用数据类型:数值型和标称型。 - -## 在数据集中度量一致性 +在数据集中度量一致性 划分数据集的最大原则是: 将无序的数据变得更加有序。我们可以使用多种方法划分数据集,但是每种方法都有各自的优缺点。组织杂乱无章数据的一种方法就是使用信息论度量信息,信息论是量化处理信息的分支科学。我们可以在划分数据之前或之后使用信息论量化度量信息的内容。 @@ -40,93 +34,122 @@ 学习了如何度量数据集的无序程度之后,分类算法除了需要测量信息熵,还需要划分数据集,度量划分数据集的熵,以便判断当前是否正确地划分了数据集。我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照哪个特征划分数据集是最好的划分方式。 -## 使用递归构造决策树 -> 构造决策树时需要解决的第一个问题 +### 决策树 工作原理 -``` - 在构造决策树时,我们需要解决的第一个问题就是,当前的数据集上哪个特征在划分数据分类时起决定性作用。 -为了找到决定性的特征,划分出最好的结果,我们必须评估每个特征。完成测试之后,原始数据集就被划分为几个数据子集。 -这些数据子集会分布在第一个决策点的所有分支上。如果某个分支下的数据属于同一类型,则当前无需阅读的垃圾邮件已经正确地划分数据分类, -无需进一步对数据集进行分割。如果数据子集内的数据不属于同一类型,则需要重复划分数据子集的过程。划分数据子集的算法和划分原始数据集的方法相同, -直到所有具有相同类型的数据均在一个数据子集内。 +1. 检测数据集中的每个子项是否属于同一分类: + 1. 如果属于同一分类,返回对应的类别标签 label + 2. 如果不属于同一分类: + 1. 寻找划分数据集的最好特征 + 2. 划分数据集 + 3. 创建分支节点 + * 对于每个划分的子集,调用函数 createBranch (创建分支的函数)并增加返回结果到分支节点中 + 4. return 分支节点 + +### 决策树 一般流程 + +1. 收集数据:可以使用任何方法。 +2. 准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。 +3. 分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。 +4. 训练算法:构造树的数据结构。 +5. 测试算法:使用经验树计算错误率。 +6. 使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义。 + +### 决策树 算法特点 + +* 优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。 +* 缺点:可能会产生过度匹配问题。 +* 适用数据类型:数值型和标称型。 + +## 决策树 项目实战 + +### 项目实战1: 判定鱼类和非鱼类 + +#### 概述 + +根据以下 2 个特征,将动物分成两类:鱼类和非鱼类。 + +特征: +1. 不浮出水面是否可以生存 +2. 是否有脚蹼 + +#### 已知数据 + +![海洋生物数据](/images/3.DecisionTree/DT_海洋生物数据.png) + +* 计算给定数据集的香农熵 + +```Python +def calcShannonEnt(dataSet): + # 求list的长度,表示计算参与训练的数据量 + numEntries = len(dataSet) + # 计算分类标签label出现的次数 + labelCounts = {} + # the the number of unique elements and their occurance + for featVec in dataSet: + # 将当前实例的标签存储,即每一行数据的最后一个数据代表的是标签 + currentLabel = featVec[-1] + # 为所有可能的分类创建字典,如果当前的键值不存在,则扩展字典并将当前键值加入字典。每个键值都记录了当前类别出现的次数。 + if currentLabel not in labelCounts.keys(): + labelCounts[currentLabel] = 0 + labelCounts[currentLabel] += 1 + + # 对于label标签的占比,求出label标签的香农熵 + shannonEnt = 0.0 + for key in labelCounts: + # 使用所有类标签的发生频率计算类别出现的概率。 + prob = float(labelCounts[key])/numEntries + # 计算香农熵,以 2 为底求对数 + shannonEnt -= prob * log(prob, 2) + return shannonEnt ``` -> 创建分支的伪代码函数createBranch() +* 按照给定特征划分数据集 -``` - 检测数据集中的每个子项是否属于同一分类: - If so return 类标签 - Else - 寻找划分数据集的最好特征 - 划分数据集 - 创建分支节点 - for 每个划分的子集 - 调用函数 createBranch 并增加返回结果到分支节点中 - return 分支节点 +```Python +def splitDataSet(dataSet, axis, value): + retDataSet = [] + for featVec in dataSet: + if featVec[axis] == value: + reducedFeatVec = featVec[:axis] + reducedFeatVec.extend(featVec[axis+1:]) + retDataSet.append(reducedFeatVec) + return retDataSet ``` -> 决策树的一般流程 +[完整代码地址](https://github.com/apachecn/MachineLearning/blob/master/src/python/3.DecisionTree/DecisionTree.py): -``` - (1)收集数据:可以使用任何方法。 - (2)准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。 - (3)分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。 - (4)训练算法:构造树的数据结构。 - (5)测试算法:使用经验树计算错误率。 - (6)使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义。 +### 项目实战2: 使用决策树预测隐形眼镜类型 + +#### 概述 + +隐形眼镜类型包括应材质、软材质以及不适合佩戴隐形眼镜。我们需要使用决策树预测患者需要佩戴的隐形眼镜类型。 + +#### 流程 + +1. 收集数据: 提供的文本文件。 +2. 解析数据: 解析 tab 键分隔的数据行 +3. 分析数据: 快速检查数据,确保正确地解析数据内容,使用 createPlot() 函数绘制最终的树形图。 +4. 训练算法: 使用 createTree() 函数。 +5. 测试算法: 编写测试函数验证决策树可以正确分类给定的数据实例。 +6. 使用算法: 存储树的数据结构,以便下次使用时无需重新构造树。 + +* 使用 pickle 模块存储决策树 + +```Python +def storeTree(inputTree, filename): + impory pickle + fw = open(filename, 'w') + pickle.dump(inputTree, fw) + fw.close() + +def grabTree(filename): + import pickle + fr = open(filename) + return pickle.load(fr) ``` -> 划分数据集时的数据路径 - -``` - 得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。 - 第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集。 - 递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。如果所有实例具有相同的分类, - 则得到一个叶子节点或者终止块。任何到达叶子节点的数据必然属于叶子节点的分类,如下图所示: -``` - -![决策树划分数据集时的数据路径](/images/3.DecisionTree/决策树划分数据集时的数据路径.png) - -## 使用Matplotlib绘制树形图 - -> Matplotlib绘制树形图示例 - -![Matplotlib绘制树形图示例](/images/3.DecisionTree/Matplotlib绘制树形图.png) - -## 决策树小结 - -* 决策树是什么? - * 顾名思义,是一种树,一种依托于策略抉择而建立起来的树。 - * 从数据产生决策树的机器学习技术叫做决策树学习, 通俗点说就是决策树。 -* 决策树目前的情况: - * 1.最经常使用的数据挖掘算法。(流行的原因:不需要了解机器学习的知识,就能搞明白决策树是如何工作的) - * 2.数据形式【决策过程只有:是/否】和数据内在含义非常容易理解。 - * 3.决策树给出的结果往往可以匹敌在当前领域具有几十年工作经验的人类专家。 -* 决策树的构造: - * 优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。 - * 缺点:可能会产生过度匹配问题。 - * 适用数据类型:数值型和标称型【标称型:其实就是离散型数据,变量的结果只在`有限`目标集中取值(例如:分类特征 A/B/C 类其中一种)】。 -* 如何找出第一个分支点呢? - * 信息增益: - * 划分数据集的最大原则是:将无序的数据变得更加有序。 - * 集合信息的度量称为`香农熵`或者简称`熵`(名字来源于信息论之父`克劳德·香农`) - * 公式: - * \\(p(x_i)\\) 表示该 label 分类的概率 - * \\(l(x_i) = - \log_2p(x_i)\\) 表示符号\\(x_i\\)的信息定义 - * \\(H = -\sum_{i=0}^np(x_i)\log_2p(x_i)\\) 表示香农熵,用于计算信息熵 - * 基尼不纯度(Gini impurity) [本书不做过多的介绍] - * 简单来说:就是从一个数据集中随机选取子项,度量其被错误分类到其他分组里的概率。 -* 流程介绍图 - -![决策树流程介绍图](/images/3.DecisionTree/决策树流程介绍图.jpg) - -``` - 决策树分类器就像带有终止块的流程图,终止块表示分类结果。 - 开始处理数据集时,我们首先需要测量集合中数据的不一致性,也就是熵,然后寻找最优方案划分数据集,直到数据集中的所有数据属于同一分类。 - ID3算法可以用于划分标称型数据集。构建决策树时,我们通常采用递归的方法将数据集转化为决策树。一般我们并不构造新的数据结构,而是使用 - Python 中内嵌的数据结构字典存储树节点信息。 -``` +[完整代码地址](https://github.com/apachecn/MachineLearning/blob/master/src/python/3.DecisionTree/DecisionTree.py): * * * diff --git a/images/3.DecisionTree/DT_海洋生物数据.png b/images/3.DecisionTree/DT_海洋生物数据.png new file mode 100644 index 0000000000000000000000000000000000000000..e9866068ab26d10aa039c0bf5ccdac1560ed7bd3 GIT binary patch literal 8635 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