数据集
决策树常用于处理分类问题,首先对本文实现的决策树训练时所用的数据集做简单介绍。
属性值: 也就是特征,通常是数据集除去最后一列剩余的部分。下面数据集中这部分数据为标称型(有限类别)。
预测目标: 也就是分类任务的目标,即样本的类别,通常是数据集中的最后一列,同样为标称型数据。
具体如下面例子:海洋生物数据
不浮出水面是否可以生存(1是0否) | 是否有脚撲(1是0否) | 属于鱼类 | |
---|---|---|---|
1 | 1 | 1 | 是 |
2 | 1 | 1 | 是 |
3 | 1 | 0 | 否 |
4 | 0 | 1 | 否 |
5 | 0 | 1 | 否 |
该数据集中前两列即为属性值(有限类别的标称型特征),最后一列“是否属于鱼类”即为所需预测的目标分类。
决策树的构造
决策树的构造过程实际上就是通过递归,不断地在当前节点处选择剩余的属性值(是否可浮出水面,是否有脚)来进行分支,使得最终叶子节点的纯度最优。对上面的数据集来讲:即尽可能地使最终的各个叶子节点均为鱼类,或均不为鱼类。
下面给出决策树构建的伪代码以理解构建过程:
# 创建分支的伪代码函数createBranch()如下所水:
检测数据集中的每个子项是否属于同一分类:
If so return 类标签;
Else
寻找划分数据集的最好特征
划分数据集
创建分支节点
for每个划分的子集
调用函数createBranch()并增加返回结果到分支节点中
return 分支节点
注意createBranch()是一个递归函数,在倒数第二行直接调用了它自己。
信息增益
决策树的关键步骤就是每一步对数据集的划分,也就是在当前的叶子节点上选择属性来将其继续分支。
而分支的最终目标是使得无序的数据变得有序,也就是使最终每个叶子节点的纯度尽可能高,即每个叶子节点都尽可能均为鱼类,或均不为鱼类。
那么,如何在当前的叶子节点上选择剩余属性中最优的属性来进行分支?这就需要定义一个指标来度量当前节点(即当前数据集)的无序程度,下面给出两个常见的度量指标:
- 信息熵:通过信息熵可以计算分支前后数据集的信息增益,信息增益越大,证明无序程度减小得越多,即能够使得划分后的叶子节点纯度最优
- 基尼不纯度:此处不讲
信息熵的定义
假设当前数据集(当前节点)为X,针对预测目标有n个分类,如上面数据集,则预测目标为“是否鱼类”,n=2。令其中的一个分类为Xi,P(Xi)为Xi所占的比例,则Xi的信息定义为:
,而当前数据集的信息熵为:
信息增益的定义
假设当前数据集(当前节点)的信息熵为H1,数据集中存在属性A,A有n个类别。我们使用属性A来划分当前数据集(分支),设划分后的每个新节点所占比例为Pn,且其熵为hn,令H2 = P1* h1 + P2* h2 + … + Pn* hn。
则有:使用属性A划分后的信息增益为H1-H2
从中我们可以看出,要在当前节点下选择最优属性来划分,则该属性应满足划分后的信息增益值应最大。
递归终止条件
- 当前节点下的类标签已经完全相同,则该处递归终止,并返回该类别标签。
- 当已经使用完所有特征时,该处递归终止。若此时不是唯一类别标签,则采用多数表决的方法,将占比最大的类别标签作为该叶子节点的分类,并返回。
代码实现
计算当前数据集的信息熵
输入的数据结构形如:
[[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
from math import log
def calcShannonEnt(dataSet) :
numEntries = len(dataSet) # 数据集的样本量
labeXCounts = {}
for featVec in dataSet: # 迭代数据集的每一行
currentLabel = featVec[-1] # 取最后一列(预测目标)
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 # 对目标列的每个类别进行计数
shannonEnt = 0.0
for key in labelCounts
prob = float (labelCounts [key] ) /numEntries # 目标列每个类别的占比
shannonEnt -= prob * log(prob # 信息熵公式
return shannonEnt
按照选定的特征划分数据集
# 得到根据特征axis划分后,值为value的叶子节点的数据集
# axis的值为0~n,即第1个~第n+1个特征
def splitDataSet(dataSet, axis, value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == valUe: # 当该属性为value时
# 去掉axis列
reducedFeatVec = featVec[: axis]
reducedFeatVec.extend{featVec[axis+l: ])
retDataSet.append(reducedFeatVec) # 添加到划分后的新数据集内(新节点)
return retDataSet
选择出划分数据集的最优特征
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 # 特征数-1
baseEntropy = calcShannonEnt(dataSet) # 初始信息熵
bestInfoGain = 0.0; bestFeature = -1
for i in range(numFeatures): # 迭代每一个属性
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) # set()函数创建一个无序不重复的元素集:{...}
newEntropy = 0.0
for value in uniqueVals: # 迭代该属性的每一个分类
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet) # 累计该属性每个分类的信息熵
infoGain = baseEntropy - newEntropy # 计算信息增益
if (infoGain > bestInfoGain): # 记录最好的信息增益
bestInfoGain = infoGai
bestFeature = i
return bestFeature
叶子节点的多数表决
# 利用多数表决选择该叶子节点应当返回的类别标签
def majorityCnt(classList):
classCount={}
for vote in classList:
if vote not in classCount.keys(): classCount fvote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
递归构建决策树的算法
# 利用了前面的功能函数
# 生成的决策树为字典形式如:{'no surfacing' : {o: 'no', 1: {'flippers': {0: 'no 1, 1: 'yes'}}}}
def createTree(dataSet,labels):
classList = [example [-1] for example in dataSet] # 当前目标变量的所有类别
if classList.count (classList [0] ) == len(classList): # 类别完全相同则停止划分
return classList[0]
if len{dataSet[0]) == 1: # 遍历完所有特征时返回出现次数最多的类别
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit{dataSet) # 最优属性下标
bestFeatLabel = labels[bestFeat] # 最优属性
myTree = {bestFeatLabel:{}} # 建立该节点下的子树
del{labels[bestFeat]) # 从labels中先剔除该节点
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set{featValues)
for value in uniqueVals:
subLabels = labels[:] # 切片,使用sublabels时不改变labels的内容
# 生成当前节点处的子树(进入递归)
myTree[bestFeatLabel] [value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
return myTree # 返回当前子树