决策树的概念非常简单,即使不知道它也可以通过简单的图形了解其工作原理,下图就是一个简单的决策树:
- 优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。
- 缺点:可能会产生过度匹配问题。
- 适用数据类型:数值型和标称型。
数据
下表包含5个海洋动物,有两个特征:不浮出水面是否可以生存,是否有脚蹼,分成两类:鱼类和非鱼类
不浮出水面是否可以生存(No Surfacing?) | 是否有脚蹼(Flippers?) | 是否是鱼类 |
---|---|---|
是 | 是 | 是 |
是 | 是 | 是 |
是 | 否 | 否 |
否 | 是 | 否 |
否 | 是 | 否 |
决策树的构造
决策树构造可以用下面伪代码来表示:1
2
3
4
5
6
7
8
9def createBranch():
if so return 类标签;
else
寻找划分数据集的最好特征
划分数据集
创建分支节点
for 每个划分的子集
调用函数createBranch()并增加返回结果到分支节点中
return 分支节点
本文使用ID3算法划分数据集,该算法处理如何划分数据集及何时停止划分数据集。
我们应该选择哪个特征作为划分的参考属性呢?
信息增益
划分数据集前后信息发生的变化称为信息增益,一般用香农熵来度量。熵定义为信息的期望值:
1 | H=-Sum(i=1=>n)(p(xi)log2p(xi)) |
如何理解:
假设现在待分数据集有两类,其熵H大于0,经过第一次分类后,分成的两类中刚好n都为1,即分类后的熵为0,此时的信息增益即为H。其实这里说成“信息减益”更合适,我们就是要找到使得分类后信息越少的属性进行分类。
计算信息熵的代码:
1 | from math import log |
测试不同数据的熵大小
1 | print tree.calcShannonEnt([ |
由结果可见之前的分析是对的
划分数据集
从原始数据中根据某特征划分出特定的数据子集的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26def splitDataSet(dataSet, axis, value):
"""
划分数据集
dataSet =
[[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
axis = 0
value = 1
=>
[[1, 'yes'], [1, 'yes'], [0, 'no']]
:param dataSet: 元素数据
:param axis: 划分的属性下标
:param value: 所要提取的属性值
:return:
"""
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] #chop out axis used for splitting
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
有了上述代码,现在可以选择最好的数据集划分方式了:
1 | def chooseBestFeatureToSplit(dataSet): |
递归构建决策树
基于最好的属性值划分数据集,划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,递归再次划分数据。直到程序遍历完所有划分数据集的属性或者每个分支下的所有实例都具有相同的分类。
如果处理完所有属性后类标签依然不是唯一的,则采取多数表决的方法决定叶子节点的分类:
1 | def majorityCnt(classList): |
有了以上代码,可以得到创建树的代码如下:
1 | def createTree(dataSet,labels): |
还是用刚才的数据测试
1 | print tree.createTree([ |
测试算法
基于上面的分类树,可以得到分类函数如下:
1 | def classify(inputTree, featLabels, testVec): |
决策树的存储
为了避免每次分类都要重新构建决策树,可以使用pickle
来序列化或反序列化决策树:
1 | def storeTree(inputTree, filename): |
小结
本文介绍了ID3
算法构建决策树的过程,但该算法并不完美:
- 可能会产生过度匹配
- 无法直接处理数值型数据,需要经过转化
后续会继续学习其他决策树的构造方法,如C4.5
和CART