本博客是用jupyter notebook写的,在CSDN上是用其转换的markdown,想要获取本篇博客的笔记与源码,请访问:我的github
最近在看《机器学习实战》这本书,主要是为了给自己的机器学习中没有学过的知识补充一下,比如这个决策树我就没学过,但是这本书写的吧……还行,有些地方说得不是很清楚,正好学完了决策树也想巩固一下,于是写下此文。
其实我觉得决策树很像一种专家系统,有各种条件判断的分支,来判断符不符合标准、属于哪个类别……我在某人的博客上找到了一个非常典型的图,银行可以根据这张图来判断是否可以放贷给这个人(当然,实际肯定比这幅图复杂得多):
好了,这就是决策树了,我们根据各个分支,层层筛选,最后判断这个人是否有偿还贷款的能力,没有的话,就不贷款给他了。现在我给你一组数据(书上的数据),我要求,根据不浮出水面是否能生存、有没有脚蹼这两个特征来判断是否属于鱼类,用决策树来表现出来。
id | 不浮出水面是否能生存 | 有没有脚蹼 | 鱼类 |
---|---|---|---|
0 | 是 | 是 | 是 |
1 | 是 | 是 | 是 |
2 | 是 | 否 | 否 |
3 | 否 | 是 | 否 |
4 | 否 | 是 | 否 |
这个时候可能大家就会有疑惑了,到底第一个特征是用能否浮出水面生存来划分好呢,还是用有没有脚蹼来划分好?有什么评价标准吗?答案是用信息增益。
信息熵与信息增益
先来信息熵的公式亮瞎眼:
H=−∑ni=1p(xi)log2(xi)
伟大的香农提出来的(本人学通信的),n代表的是分类的数目,比如上面的表格中,只有两种类别,是鱼类,不是鱼类,我们用1表示是,0表示否,则不是鱼类的概率为:
p(x0)=35
,是鱼类的概率为:
p(x1)=25
我们手算一下目前的信息熵:
H=−35log2(35)−25log2(25)≈0.971
那么信息熵和我们设计决策树有什么关系呢?我们每一次选择某一特征作为节点进行划分的时候,都有一个目标——使得这一节点(注意,不是整体)的熵下降最多,也就是信息增益最大。
假设我们以能浮出水面生存作为第一个节点来划分,那么1,2,3作为左子树,4,5作为右子树。在左子树中,是鱼类占
23
,不是鱼类占
13
,所以左子树的信息熵
H=−23log2(23)−13log2(13)≈0.918
;在右子树中,是鱼类占0%,不是鱼类占100%,所以右子树的信息熵为:
H=−1log2(1)=0
;左子树有1,2,3三个条目,占当前节点的
35
,右子树有4,5两个条目,占当前节点的
25
,所以按照能否浮出水面生存来划分的信息熵为
H=35∗0.918+25∗0≈0.551
相对于未划分前,信息增益为0.420。
那如果以有没有脚蹼作为划分标准,则1,2,4,5作为左子树,3作为右子树。在左子树中,是鱼类和不是鱼类各占
12
,左子树的信息熵
H=−2∗12log2(12)=1
;右子树不是鱼类为100%,则右子树的信息熵为
H=−1log2(1)=0
;左子树有4个条目,占当前节点的
45
,右子树有1个条目,占当前节点的
15
,所以按照能否浮出水面生存来划分的信息熵为
H=45∗1+15∗0≈0.8
相对于未划分前,信息增益为0.171。
因此,我们选择信息增益最大的划分方案,划分结果如下图:
递归和停止条件
如果学过数据结构与算法的人应该知道,通常我们画树都是用递归来画的,那么递归截止的条件是啥?可以分两种情况:1、所有的分类都一样了,比如我们上图的右子树,都是鱼类,所有这个右子树可以停止继续递归了;2所有的特征都用完了,假如我们继续对左子树分类,把脚蹼作为分类特征,分完后已经没有其他特征可以继续分类了,这时候也不得不结束递归,但是又有一个问题了,如果我特征用完了,在当前子树下还是有不同类别咋办,那只能选择数量最多的了作为分类。
好了,手工推导已经结束,接下来让代码来实现。
首先我们想一想,需要哪些功能模块:计算信息熵、根据特征划分、选择最佳特征划分、创建树、使用树来分类。不过在此之前,先把数据格式定下来,这样才方便,在这里我使用的是pandas,作为一款数据分析包,其使用起来比较方便。
import numpy as np
import pandas as pd
data=pd.DataFrame({
'No Surfacing':[1,1,1,0,0],
'Flippers':[1,1,0,1,1],
'Fish':['Y','Y','N','N','N']
})
data
Fish | Flippers | No Surfacing | |
---|---|---|---|
0 | Y | 1 | 1 |
1 | Y | 1 | 1 |
2 | N | 0 | 1 |
3 | N | 1 | 0 |
4 | N | 1 | 0 |
信息熵计算模块
思路:把数据放入后要统计分类的比例,根据比例计算信息熵并返回。注意,这里的category的意思是指最终想要分成什么类,不是特征,我们这里是想分辨是否是鱼,所以category就填Fish
def calcShannonEnt(data,category):
labels=data[category].value_counts()#统计有几类,每个类有多少个
numEntries=np.sum(labels)#计算总数
shannonEnt = 0
for label in labels:
prob = label/numEntries#计算当前类的概率
shannonEnt -= prob * np.log2(prob)
return shannonEnt
calcShannonEnt(data,'Fish')#测试正确
0.97095059445466858
根据特征划分模块
def splitData(data,feature):
labels=set(data[feature])#该特征一共有几种分类
retdata=[]
for index,f in enumerate(labels):
retdata.append(pd.DataFrame(data[data[feature]==f]))
del retdata[index][feature]
return retdata,labels
sp,labels=splitData(data,'No Surfacing')#测试正确
print(sp[0])
print(labels)
Fish Flippers
3 N 1
4 N 1
{0, 1}
选择最佳特征划分
这里的category不是特征,是最终要划分的类,在这里我们用的是Fish
def chooseBestFeatureToSplit(data,category):
baseEntropy = calcShannonEnt(data,category)#计算分类前的信息熵
features = data.columns.drop(category)#有哪些特征
numEntries = data[category].count()#总数
bestInfoGain = 0
bestFeature = features[0]
for feature in features:
splitdata,_ = splitData(data,feature)
newEntropy = 0
for item in splitdata:
prob = item[category].count()/numEntries#计算当前类的概率
newEntropy += prob * calcShannonEnt(item,category)
infoGain = baseEntropy - newEntropy
#print(infoGain)
if (infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = feature
return bestFeature
chooseBestFeatureToSplit(data,'Fish')#测试正确
'No Surfacing'
递归构建决策树
def createTree(data,category):
#所有分类都一样
if len(set(data[category])) == 1:
return data[category].iloc[0]
#遍历完所有特征时返回出现次数最多的
if len(set(data.columns)) == 1:
temp=pd.DataFrame(data[category].value_counts())
return temp.sort(columns=category).index[-1]
#以上两点都不符合,继续递归
bestFeat = chooseBestFeatureToSplit(data,category)
splitdata,labels = splitData(data,bestFeat)
#print(bestFeat)
myTree = {bestFeat:{}}
for subdata,sublabel in zip(splitdata,labels):
#print(sublabel)
myTree[bestFeat][sublabel]=createTree(subdata,category)
#print(myTree)
return myTree
myTree=createTree(data,'Fish')
myTree
{'No Surfacing': {0: 'N', 1: {'Flippers': {0: 'N', 1: 'Y'}}}}
用决策树分类
还是会用到递归
def classify(tree,data):
key=list(tree.keys())
subtree=tree[key[0]][data[key[0]]]
if type(subtree) == dict:#说明不在叶子结点
subdata = data.copy()
return classify(subtree,subdata)
return subtree#说明在叶子结点
testdata=data.loc[4,['Flippers','No Surfacing']]
classify(myTree,testdata)
'N'
testdata = data.loc[:,['Flippers','No Surfacing']]
print(testdata)
for index,row in testdata.iterrows():
print(classify(myTree,row))
Flippers No Surfacing
0 1 1
1 1 1
2 0 1
3 1 0
4 1 0
Y
Y
N
N
N
结尾
这样ID3决策树就完成了,至于书上用matplotlib画图,这种锦上添花的内容,高兴做就做吧。不过用matplotlib画树状图,简直是大炮打蚊子,我倒是发现有专门画流程图的软件,叫graphviz,这个软件也提供了Python接口,可以用比较简单清爽的语句来描述流程图,这个软件我没研究,需要画图了再研究吧,谁叫我是懒人呢……