一、决策树概念
1、概念以及适用场景
概念:
决策树(decision tree)是一类常见的机器学习方法。
适用场景:
决策树能够生成清晰的基于特征(feature)选择不同预测结果的树状结构,希望更好地理解手上的数据的时候,往往可以使用决策树,在实际应用中,受限于它的简单性,决策树更大的用处是作为一些更有用的算法的基石,例如随机森林。
2、算法介绍
决策树类似于数据结构中的二叉树,从上到下,依次进行判断,以西瓜书中对西瓜的判断为例,如下图所示:
最终,通过一次次的判断得到一个最终的判断结果,这就是一个简单形象的决策树。
3、决策树有哪些算法
- ID3
以信息增益作为树的分裂准则,该算法存在的不足:
- ID3没有考虑连续特征,比如长度,密度都是连续值,无法在ID3运行,如果一定要用ID3处理连续属性,一般来说都需要将连续特征离散化;
- 对于缺失值的情况没有做考虑;
- 偏向于多值属性。例如:如果存在唯一标识属性ID(每个要本的ID属性值都不相同),则ID3会选择它作为优先分裂属性,这种划分虽然充分纯净,但是这种划分对分类几乎毫无用处。
- C4.5
- 以基于信息增益的增益率(gain ratio)作为树的分裂准则,解决了ID3的偏向于多值属性问题;
- 内部自己考虑了连续属性离散化过程,所以克服了ID3的没有考虑连续特征的问题;
- 内部考虑了缺失值的自动处理策略。
- CART
ID3和C4.5只能处理分类问题,而CART可以处理分类和回归问题,CART考虑问题非常全面,有较多优点,可以自行深入研究。
具体内容可看决策树算法原理
二、决策树的构造
1、熵
什么是熵
熵全称“信息熵”(information entyopy),是度量样本集合纯度最常用的一种指标。表示随机变量的不确定性。信息熵是用于度量信息的混乱程度,信息越混乱说明能够包含的信息量越多,则熵越大;反之信息越有序,说明包含的信息量越少,则熵越小。
熵的公式如下:
H
(
p
)
=
−
∑
i
=
1
n
p
i
l
o
g
p
i
H(p)=-\sum_{i=1}^np_ilogp_i
H(p)=−i=1∑npilogpi
熵是一个表示随机变量的不确定性的一个值,在熵中有一个特殊的数据,当n=2时,我们可以做如下处理:
H
(
p
)
=
−
(
p
1
l
o
g
p
1
+
p
2
l
o
g
p
2
)
H(p)=-(p_1logp_1+p_2logp_2)
H(p)=−(p1logp1+p2logp2)
但是因为n=2,所以可知p1+p2=1,所以可以令p1=p,p2=1-p,此时,原式转化为:
H
(
p
)
=
−
p
l
o
g
p
−
(
1
−
p
)
l
o
g
(
1
−
p
)
H(p)=-plogp-(1-p)log(1-p)
H(p)=−plogp−(1−p)log(1−p)
则,可做如下图:
由图中可知:
- 当p=0或1时,H(p)=0,当p=0.5时,H( p)=1;
- 因为p表示的是概率,所以当H( p)=1时,p=0.5,则表示一半的可能,此时的随机变量的不确定性就会变得很大,则越不稳定。
- 反之,当H(p)=0时,p等于0或1,此时p相当于确定的是或者否,则随机变量变得极其稳定。
所以,可知:熵越大,随机变量的不确定性越大。
为了更加理解熵如何计算,有如下例子(以西瓜书中西瓜为例):
这里给我们呈现了17组数据,在这里,我们比如计算是否是好瓜的熵值,我们使用公式带入:
H
(
p
)
=
−
∑
i
=
1
n
p
i
l
o
g
p
i
=
∑
i
=
1
n
∣
C
i
∣
D
l
o
g
∣
C
i
∣
D
H(p)=-\sum_{i=1}^np_ilogp_i=\sum_{i=1}^n\frac{|C_i|}{D}log\frac{|C_i|}{D}
H(p)=−i=1∑npilogpi=i=1∑nD∣Ci∣logD∣Ci∣
我们很容易能够知道这里总共有17个瓜,其中8个是好瓜,9个是不好的瓜,所以按照公式,我们带入:
H
(
好
瓜
否
)
=
−
(
∣
8
∣
17
l
o
g
∣
8
∣
17
+
∣
9
∣
17
l
o
g
∣
9
∣
17
)
H(好瓜否)=-(\frac{|8|}{17}log\frac{|8|}{17}+\frac{|9|}{17}log\frac{|9|}{17})
H(好瓜否)=−(17∣8∣log17∣8∣+17∣9∣log17∣9∣)
由此我们就可以计算出好瓜与否的熵的值是多少。
2、条件熵
前面讲了熵怎么计算,在这个地方,我们将引入条件熵。条件熵指的是给定某一特征A后在算这个数据集的熵。条件熵的公式可以如下表示:
H
(
D
∣
A
)
=
∑
i
=
1
n
∣
D
i
∣
D
H
(
D
i
)
=
−
∑
i
=
1
n
∣
D
i
∣
D
∑
k
=
1
K
∣
D
i
k
∣
D
l
o
g
∣
D
i
k
∣
D
H(D|A)=\sum_{i=1}^n\frac{|D_i|}{D}H(D_i)=-\sum_{i=1}^n\frac{|D_i|}{D}\sum_{k=1}^K\frac{|D_{ik}|}{D}log\frac{|D_{ik}|}{D}
H(D∣A)=i=1∑nD∣Di∣H(Di)=−i=1∑nD∣Di∣k=1∑KD∣Dik∣logD∣Dik∣
条件熵其实就是在熵的基础上添加一些前置的条件,比如刚刚的示例,我们刚刚计算了最后一列好瓜与否的熵的值,我们现在计算,给定特征色泽之后,计算我们的条件熵,具体算法如下:
可知数据的总数应该是17,由色泽分为三类,每一类及其编号为:
{青绿| 1,4,6,10,13,17}–D1
{乌黑|2,3,7,8,9,15}–D2
{浅白|5,11,12,14,16}D3
由这三个分类每一个里面都有好瓜和不好瓜,具体的计算方式如下所示:
H ( 色 泽 ∣ 好 瓜 ) = ∣ D 1 ∣ D H ( D 1 ) + ∣ D 2 ∣ D H ( D 2 ) + ∣ D 3 ∣ D H ( D 3 ) = ∣ 5 ∣ 17 ∗ ( − ∣ 3 ∣ 6 l o g ∣ 3 ∣ 6 − ∣ 3 ∣ 6 l o g ∣ 3 ∣ 6 ) + ∣ 5 ∣ 17 ∗ ( − ∣ 4 ∣ 6 l o g ∣ 4 ∣ 6 − ∣ 2 ∣ 6 l o g ∣ 2 ∣ 6 ) + ∣ 5 ∣ 17 ∗ ( − ∣ 1 ∣ 5 l o g ∣ 1 ∣ 5 − ∣ 4 ∣ 5 l o g ∣ 4 ∣ 5 ) H(色泽|好瓜)=\frac{|D_1|}{D}H(D_1) + \frac{|D_2|}{D}H(D_2) + \frac{|D_3|}{D}H(D_3)\\ \,\,\,\,\,\,\,\,\,\,\,\,\,\,=\frac{|5|}{17}*(-\frac{|3|}{6}log\frac{|3|}{6} - \frac{|3|}{6}log\frac{|3|}{6}) + \frac{|5|}{17}*(-\frac{|4|}{6}log\frac{|4|}{6}-\frac{|2|}{6}log\frac{|2|}{6}) + \frac{|5|}{17}*(-\frac{|1|}{5}log\frac{|1|}{5}-\frac{|4|}{5}log\frac{|4|}{5}) H(色泽∣好瓜)=D∣D1∣H(D1)+D∣D2∣H(D2)+D∣D3∣H(D3)=17∣5∣∗(−6∣3∣log6∣3∣−6∣3∣log6∣3∣)+17∣5∣∗(−6∣4∣log6∣4∣−6∣2∣log6∣2∣)+17∣5∣∗(−5∣1∣log5∣1∣−5∣4∣log5∣4∣)
这样就可以计算出给定色泽特征后的条件熵。
3、信息增益
由上面的熵,我们可以引出新的概念,叫信息增益(information gain)。
信息增益(也叫互信息,mutual information),定义如下:
g ( D , A ) = H ( D ) − H ( D ∣ A ) g(D, A)=H(D)-H(D|A) g(D,A)=H(D)−H(D∣A)其中,D是训练数据集,A是某个特征
根据信息增益准则的特征选择方法是:
- 对训练数据集(或子集)D,计算其每个特征的信息增益
- 比较它们的大小,选择信息增益最大的特征
信息增益对于数据来说,越大越好,因此,在我们给定特征值进行信息增益计算之后,我们应当选取信息增益最大的那一个特征值进行一次划分。
在这个例子中,假如计算出色泽特征的信息增益最大,那么将按照色泽中的分类进行一次划分,比如将青绿放在左边,乌黑放在中间,浅白放在右边,然后再往下对剩下的特征进行信息增益的计算,整个过程就是一个循环递归的过程,直到最后全部特征计算完毕后,生成最终的决策树。(如下图的第一个简单的划分)
三、部分功能代码的实现
1、香农熵的计算
from math import log
def calcShannonEnt(dataSet):
numEntries = len(dataSet) # 返回数据集的行数
labelCounts = {} # 保存每个标签(label)出现的次数
for featVec in dataSet: # 对每组特征向量进行统计
currentLabel = featVec[-1] # 提取标签(label)信息
if currentLabel not in labelCounts.keys(): # 如果标签(label)没有放入统计次数的字典,则添加进去
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 # label计数
shannonEnt = 0.0 # 经验熵(香农熵)
for key in labelCounts: # 计算香农熵
prob = float(labelCounts[key] / numEntries) # 选择该标签(label)的概率
shannonEnt -= prob * log(prob, 2) # 利用公式计算
return shannonEnt # 返回经验熵(香农熵)
2、划分数据集
def splitDataSet(dataSet, axis, value):
retDataSet = [] # 创建返回的数据集列表
for featVec in dataSet: # 遍历数据集
if featVec[axis] == value:
reducedFeatVec = featVec[: axis] # 去掉axis特征
reducedFeatVec.extend(featVec[axis+1:]) # 将符合条件的添加到返回的数据集
retDataSet.append(reducedFeatVec)
return retDataSet
注意:该函数是对每一列特征进行切割,axis表示要切割的列号,value是对该列要进行切割的值,比如:
有一个数据集为:
myDat= [
[1, 1, ‘yes’],
[1, 1, ‘yes’],
[1, 0, ‘no’],
[0, 1, ‘no’],
[0, 1, ‘no’]]
若执行 splitDataSet(myDat, 0, 0),即axis=0, value=0则最后的结果为:
[[1, ‘no’],
[1, ‘no’]]
如下代码所示:
myDat = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
splitDataSet(myDat, 0, 0)
>>> [[1, 'no'], [1, 'no']]
所以在这里这个函数是将该数据集第axis列,即第0列的值为value,也就是0的所有的值去掉,并将其剩下的留下来,所以这个地方,第一列值为0的有[0, 1, ‘no’]和[0, 1, ‘no’],在将其第一列的数据去掉后,剩下的就是[1, ‘no’]和[1, ‘no’],所以最后的结果是[[1, ‘no’], [1, ‘no’]]。
3、选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 # 特征数量
baseEntropy = calcShannonEnt(dataSet) # 计算数据集的香农熵
bestInfoGain = 0.0 # 信息增益
bestFearture = -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) # subDataSet划分后的子集
prob = len(subDataSet) / float(len(dataSet)) # 计算子集的概率
newEntropy += prob * calcShannonEnt(subDataSet) # 根据公式计算经验条件熵
infoDain = baseEntropy - newEntropy # 信息增益
if infoDain > bestInfoGain: # 计算信息增益
bestInfoGain = infoDain # 更新信息增益,找到最大的信息增益
bestFearture = i # 记录信息增益最大的特征的索引值
return bestFearture # 返回的是信息增益最大的特征的索引值
4、核心程序-创建树的函数
def majorityCnt(classList): # 投票
classCount = {}
for vote in classList:
if vote not in classCount.key():
classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.item(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
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]) # 删除已经使用的特征标签
featValues = [example[bestFeat] for example in dataSet] # 得到训练集中所有最优特征的属性
uniqueVals = set(featValues) # 去掉重复的属性
for value in uniqueVals: # 遍历特征,创建决策树
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
return myTree
5、使用决策树的分类函数
# 递归寻找到最后的叶子节点获取分类
def classify(inputTree, featLabels, testVec):
firstStr = list(inputTree.keys())[0] # 获取决策树的节点
secondDict = inputTree[firstStr] # 写一个字典
featIndex = featLabels.index(firstStr)
for key in list(secondDict.keys()):
if testVec[featIndex] == key:
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key], featLabels, testVec)
else:
classLabel = secondDict[key]
return classLabel
6、PlotTree函数
这个函数主要是使用matplotlib对树进行绘制,这里就不做详细解释,有兴趣的同学可以对matplotlib库进行研究。
import matplotlib.pyplot as plt
def plotMidText(cntrPt, parentPt, txtString):
xMid = (parentPt[0] - cntrPt[0]/2.0 + cntrPt[0])
yMid = (parentPt[1] - cntrPt[1]/2.0 + cntrPt[1])
createPlot.ax1.text(xMid, yMid, txtString)
def plotTree(myTree, parentPt, nodeTxt):
numLeafs = getNumLeafs(myTree)
depth = getTreeDepth(myTree)
firstStr = list(myTree.keys())[0]
cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff)
plotMidText(cntrPt, parentPt, nodeTxt)
plotNode(firstStr, cntrPt, parentPt, decisionNode)
secondDict = myTree[firstStr]
plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
plotTree(secondDict[key], cntrPt, str(key))
else:
plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD
def createPlot(inTree):
fig = plt.figure(1, facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
plotTree.totalW = float(getNumLeafs(inTree))
plotTree.totalD = float(getTreeDepth(inTree))
plotTree.xOff = -0.5/plotTree.totalW
plotTree.yOff = 1.0
plotTree(inTree, (0.5, 1.0), '')
plt.show()
四、书上的示例:使用决策树预测隐形眼镜类型
《机器学习实战》一书为我们提供了一个实战案例,使用决策树预测隐形眼镜类型,在该示例中,运用的是前面的对决策树构造的代码,数据集则由作者为我们提供了,如果有兴趣的同学可以通过点击此处下载书中相关的数据集以及代码,同时里面也带有《机器学习实战》英文版和中文版PDF电子书(侵权请告知)。下面我演示一下这个示例,以及最后的结果。
整个示例的代码如下:
fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
lensesTree = createTree(lenses, lensesLabels)
createPlot(lensesTree)
其中,数据的大致样子如下:
最终跑出来的结果如下:
因为整个数据使用的核心代码都在前面展示了,所以该示例直接使用前面相应的代码就可以了,有兴趣的同学可以在自己电脑上跑一下。
五、总结
决策树适用于数据型和标称型数据集,构造决策树是很耗时的任务,即使在处理很小的数据集,也要花费几秒的时间,如果数据集很大,将会消耗很多计算时间。但是如果适用创建好的决策树解决分类问题,则可以很快完成,因此,为了节省时间,最好能够在每次执行分类是调用已经构造好的决策树。
优点:
- 计算复杂度不高,输出结果易于理解,对于中间值的缺失不敏感,可以处理不相关特征数据;
- 决策树的时间复杂度适用于训练决策树的数据点的对数;
- 能够处理数字和数据,而其他很多算法分析的数据集往往都只能是一种数据;
- 决策树能够处理一些多输出的问题。
缺点:
- 可能会产生过度匹配问题,即过拟合;
- 决策树的结果可能会出现不稳定的情况,因为数据中一个很小的变化就可能导致生成一个完全不一样的树;