本篇博客整理了李航的《统计学习方法》第五章,其中关于决策树的知识,涉及决策树的构建原理,决策树的生成只用了ID3算法,后续会把C4.5算法和CART算法也整理出来。最后结合《机器学习实战》进行算法的实现。
一、决策树原理
在进行原理阐述之前,我们要搞清楚一个问题:为什么会有决策树的方法? 我们之前用过的K-近邻算法原理简单,实现方便,不是挺好的吗?原因: KNN算法虽然也能完成很多分类的任务,但无法给出数据的内在含义,而决策树的优势在于数据形式非常易于理解,决策树算法能够读取数据的集合,找到数据所蕴含的的知识信息。决策树可以利用不熟悉的数据,提取一系列的规则。
1、决策树模型
决策树是一种基本的分类与回归方法,这里讨论的是分类的方法。分类决策树模型是一种根据特征对实例进行分类的树形结构。决策树由结点和有向边组成。结点分为内部结点和叶结点。内部结点表示特征或属性,叶结点表示分类结果。
决策树分类过程:从根结点开始,对实例的某一特征进行测试,根据测试结果,将实例分配到其子结点,此时,子结点对应该特征的一个取值。递归的对实例进行测试和分配,直至到达叶结点,该实例就属于这个叶结点的类。注意: 这是一颗已经训练好的决策树,刚才的过程相当于对测试集进行分类的过程,而不是训练决策树的过程。
2、决策树学习过程
决策树学习本质上从训练数据集中归纳出一组分类规则,根据这个规则得到的决策树,不仅对训练数据有很好的拟合,而且要有很好的泛化能力。
决策树悬系的算法通常是一个递归的选择最优特征的过程,并根据该特征对数据集进行划分,使得各个子数据集都有一个最好的分类。具体过程: 1、构造根结点,将所有的训练数据放在根结点上,选择一个最佳特征(怎么选择?),按照这一特征将训练数据集分割成子集,使得各个子集有一个在当前条件下最好的分类。如果 这些子集已经能够被基本上正确分类,则构建叶结点,子集被分配到对应的叶结点上去;2、如果还有子集不能被正确的分类,那么就选择这些子集的新的最优特征,继续对对其进行分割,构建相应的结点,如此递归,直到所有的训练数据子集被基本正确分类或者无新的特征为止,最后每个特征都被分到叶结点上。
#创建分支的伪代码函数 createBranch():
# 检测数据集中的每一项是否属于同一类
# If so return 类标签;
# Else
# 寻找划分数据集的最好特征
# 划分数据集
# 创建分支节点
# for 每个划分的子集
# 调用函数createBranch()并增加返回结果到分支节点去
# return 分支节点
3、特征选择
选择的意义:在于选取出对数据具有分类能力的特征。
选择的方法:信息增益或信息增益比
信息增益
-
熵—表示随机变量不确定性的度量,定义为:
H ( p ) = ∑ i = 1 n p i   l o g   p i H(p)=\sum\limits_{i=1}^n{p_i\,log\,p_i} H(p)=i=1∑npilogpi
其中, p i p_i pi表示随机变量的概率分布。 -
条件熵—给定条件下的熵,定义X在给定的条件下Y的条件熵为:
H ( Y ∣ X ) = ∑ i = 1 n   p i   H ( Y ∣ X = x i ) H(Y|X)=\sum\limits_{i=1}^n\,p_i\,H(Y|X=x_i) H(Y∣X)=i=1∑npiH(Y∣X=xi)
其中, p i = P ( X = x i )    i = 1 , 2 , . . . , n . p_i=P(X=x_i)\;i=1,2,...,n. pi=P(X=xi)i=1,2,...,n.
特征A对训练数据集D的信息增益g(D,A),定义为集合D的经验熵H(D)与特征A在给定条件下D的经验条件熵H(D|A)之差,即
g
(
D
,
A
)
=
H
(
D
)
−
H
(
D
,
A
)
g(D,A)=H(D)-H(D,A)
g(D,A)=H(D)−H(D,A)
决策树应用信息增益准则选择特征。经验熵H(D)表示对数据集D进行分类的不确定性。经验条件熵H(D|A)表示在特征A给定的条件下对数据集D进行分类的不确定性。这两者的差,即信息熵,表示的是由于特征A而对数据集D进行分类的不确定性减少的程度。
根据信息增益准则的特征选择方法是:对训练数据集(或子集)D,计算其每个特征的信息增益,然后比较大小,选择信息增益最大的特征。
信息增益算法:
输入:训练数据集D和特征A:
输出:特征A对训练数据集D的信息增益g(D,A)
(1) 计算数据集D的经验熵H(D)
H
(
D
)
=
−
∑
i
=
k
K
∣
C
k
∣
∣
D
∣
l
o
g
2
 
∣
C
k
∣
∣
D
∣
H(D)=-\sum\limits_{i=k}^K\frac{|C_k|}{|D|}log_2\,\frac{|C_k|}{|D|}
H(D)=−i=k∑K∣D∣∣Ck∣log2∣D∣∣Ck∣
(2)计算特征A对数据集D的经验条件熵H(D|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
i
∣
l
o
g
2
 
∣
D
i
k
∣
∣
D
i
∣
\begin{aligned}H(D|A) &=\sum\limits_{i=1}^n\frac{|D_i|}{|D|}H(D_i)\\ &=-\sum\limits_{i=1}^{n}\frac{|D_i|}{|D|}\sum\limits_{k=1}^K\frac{|D_{ik|}}{|D_i|}log_2\,\frac{|D_{ik}|}{|D_i|} \end{aligned}
H(D∣A)=i=1∑n∣D∣∣Di∣H(Di)=−i=1∑n∣D∣∣Di∣k=1∑K∣Di∣∣Dik∣log2∣Di∣∣Dik∣
(3)计算信息增益
g
(
D
,
A
)
=
H
(
D
)
−
H
(
D
∣
A
)
g(D,A)=H(D)-H(D|A)
g(D,A)=H(D)−H(D∣A)
信息增益比
以信息增益作为划分数据集的特征,存在偏向于选择取值较多的特征(因为信息增益公式是加和的形式),使用信息增益比可以对这一问题进行校正。
定义:
特征A对训练数据集D的信息增益比
g
R
(
D
,
A
)
g_{_R}(D,A)
gR(D,A)的定义为信息增益
g
(
D
,
A
)
g(D,A)
g(D,A) 与训练数据集D关于特征A的信息熵
H
A
(
D
,
A
)
H_A(D,A)
HA(D,A)之比,即
g
R
(
D
,
A
)
=
g
R
(
D
,
A
)
H
A
(
D
)
g_{_R}(D,A)=\frac{g_{_R}(D,A)}{H_{_A}(D)}
gR(D,A)=HA(D)gR(D,A)
其中, H A ( D ) = − ∑ i = 1 n ∣ D i ∣ ∣ D ∣ l o g 2 ( ∣ D i ∣ ∣ D ∣ ) H_{_A}(D)=-\sum\limits_{i=1}^{n}\frac{|D_i|}{|D|}log_{_2}(\frac{|D_i|}{|D|}) HA(D)=−i=1∑n∣D∣∣Di∣log2(∣D∣∣Di∣),n是特征A取值的个数。
二、决策树生成算法之ID3算法
ID3算法的核心是在决策树的各个结点上应用信息增益准则选择特征,过程为:从根结点开始,对结点计算所有可能的信息增益,选择信息增益最大的特征作为结点的特征,然后根据该特征不同的取值来构建子结点;再对子结点递归的调用以上的方法,构建决策树;直到满足:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。
相关代码:
(1)计算给定数据集的香农值:
from math import log
import operator
# 计算给定数据的香农值
def calshannoEnt(dataSet):
numEntries=len(dataSet)# 获取数据集的长度
labelsCount={}# 设置字典来统计各种类标签的个数
for feat in dataSet:# 提取数据每一行的特征向量
featLabels=feat[-1]# 获取特征向量的最后一列的标签
if featLabels not in labelsCount.keys():# 字典中的键key是否有该标签
labelsCount[featLabels]=0# 如果没有,将当前的标签和其对应的键值0存入该字典
labelsCount[featLabels]+=1# 将字典中该标签对应的键值+1
shannoVal=0# 初始化香农值为0
for key in labelsCount:# 提取字典中的每一个关键字,即分类的类别
prob=float(labelsCount[key])/numEntries# 计算每一个类别出现的频率
shannoVal-=prob*log(prob,2)# 计算各个类别信息的期望值
return shannoVal # 返回熵值
(2)添加数据集dataSet,进行测试:
# 创建一个特定的数据集
def createDataSet():
dataSet=[[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no']]
labels=['no surfacing','flippers']
return dataSet,labels
(3)根据信息熵最大的特征,划分数据集:
# —划分数据集,得到该特征对应的标签—
# dataSet—待划分的数据集
# axis—当前最好特征对应的索引
# value—当前最好特征的取值
def splitData(dataSet,axis,value):
# python在传递参数列表时,传递的是列表的引用
# 如果在函数内部对列表对象进行修改时,将会导
# 致列表的变化,为了不修改原始数据,创建一个
# 新的列表对象进行操作
retData=[]
for vote in dataSet:# 提取数据集的每一行特征向量
# 针对axis的不同取值,会将数据集划分成不同的分支
if vote[axis]==value:# 如果该特征值为value
reduceData=vote[:axis]# 将该特征向量的0~axis-1列给reduceData
# extend()是将另一个列表中的元素(以列表中的元素为对象),添加到当前列表中,构成新的列表
# 比如 a=[1,2,3],b=[1,2],则a.extend(b)=[1,2,3,1,2]
reduceData.extend(vote[axis+1:])# 将该特征向量的第axis+1~最后一列添加到reduceData
# append()是将另一个列表(以列表为元素)添加到当前列表中
#比如 a=[1,2,3],b=[1,2],则a.append(b)=[1,2,3,[1,2]]
retData.append(reduceData)
return retData
(4)寻找信息熵最大的特征:
# 选择最好的特征
def chooseBestFeatureToSplit(dataSet):
numFeature=len(dataSet[0])-1# 获取数据集特征的数目
baseshannoEntry=calshannoEnt(dataSet)# 计算整个数据集的香农值
bestInfoEntry=0 # 初始最优信息熵
bestFeature=-1 # 初始最优特征的索引
for i in range(numFeature):# 计算每一个特征的信息增益
featlist=[example[i] for example in dataSet]# 得到当前特征对应的特征值
uniquefeatValue=set(featlist)# 利用set的性质—元素的唯一性,得到当前特征不重复的取值
NewEntries=0 # 当前特征的信息熵
for value in uniquefeatValue:# 根据特征的取值,划分不同的子集
retData=splitData(dataSet,i,value)
prob=float(len(retData))/len(dataSet)# 特征为i,值为value时占整个数据集的比例
NewEntries += prob*calshannoEnt(retData)# 计算当前子集对应的信息熵
InfoEntries=baseshannoEntry-NewEntries# 信息增益
if bestInfoEntry < InfoEntries:# 比较此时的信息增益与当前保存的最大信息增益的关系
bestInfoEntry=InfoEntries # 保存最大的信息增益
bestFeature=i # 保存最大增益对应的最优特征的索引
return bestFeature
(5)在通过以上的各个模块学习之后,我们接下来就要真正构建决策树,构建决策树的工作原理为:首先得到原始数据集,然后基于最好的属性划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将向下传递到树分支的下一个结点,在该结点上,我们可以再次划分数据。因此,我们可以采用递归的方法处理数据集,完成决策树构造。
递归的条件是:程序遍历完所有划分数据集的属性,或者每个分之下的所有实例都具有相同的分类。如果所有的实例具有相同的分类,则得到一个叶子结点或者终止块。
当然,我们可能会遇到,当遍历完所有的特征属性,但是某个或多个分支下实例类标签仍然不唯一,此时,我们需要确定出如何定义该叶子结点,在这种情况下,通过会采取多数表决的原则选取分支下实例中类标签种类最多的分类作为该叶子结点的分类,这样,我们就需要先定义一个多数表决函数majorityCnt():
#少数服从多数原则挑选类标签
def majorityCnt(classList):
classCount={}# 创建一个类标签的字典
for vote in classList:# 遍历类标签列表中的每一个类
if vote not in classCount.keys():# 如果这个类不在字典当中
classCount[vote]=0 # 则将关键字为vote,值为0,添加到字典中
classCount[vote]+=1# 将字典中关键字为vote的值加1
# classCount.items—列表对象
# operator.itemetter(1)—获取列表对象的第一个域的值
# reverse=True—降序排列,默认是升序
SelectLabel=sorted(classCount.items(),
key=operator.itemgetter(1),
reverse=True)
return SelectLabel[0][0]# 返回出现次数最多的类标签
(6)创建树的函数:
# 创建树的函数
def createTree(dataSet,labels):
classList=[example[-1] for example in dataSet]# 获取最后一列的类标签
# 通过count()函数获取类标签中第一个类标签的数目
if classList.count(classList[0])==len(classList):# 判断第一个类标签的数目是否等于列表长度
return classList[0]# 如果相等,说明数据集中的实例属于同一类
if len(dataSet[0])==1:# 遍历完所有的特征,此时数据集的列长为1
return majorityCnt(classList) # 多数表决原则,确定类标签
bestFeature=chooseBestFeatureToSplit(dataSet)# 选择当前数据集的最好特征索引
bestfeat=labels[bestFeature]# 在特征标签列表中获得对应的标签
myTree={bestfeat:{}}# 采用字典的形式,存储分类信息
## 此处注意:1.用del(),将遍历过的特征从列表中清除,但后面可能会出现'no surfacing' is not in list'
## 正确做法:在后面把labels重新定义一次
## 2.直接以下做法
sublabel = labels[:]
del (labels[bestFeature])
featvalues=[example[bestFeature] for example in dataSet]# 获取最优特征对应的列的值
uniquefeatvalues=set(featvalues)# 利用set性质,去掉重复值
for value in uniquefeatvalues:# 遍历每一个特征的值
#sublabel=labels[:]
myTree[bestfeat][value]=createTree(splitData(dataSet,bestFeature,value),
sublabel)
return myTree
(7)创建决策树的分类函数:
# 使用决策树的分类函数
def classify(inputTree, featLabel, testVec):
firstStr=list(inputTree.keys())[0]# 取出树中的第一个键
secondDict=inputTree[firstStr]# 树中第一个键对应的值
featIndex=featLabel.index(firstStr)# list.index(obj) 用于从列表中找出第一个匹配项的位置
for key in secondDict.keys():# 取出secondDict中的每一个键
if testVec[featIndex]==key:# 测试集中的特征值和secondDict的键进行比较
if type(secondDict[key])==dict:# 如果secondDict的键值是一个字典,则继续分类
classLabel=classify(secondDict[key],featLabel,testVec)
else:
classLabel=secondDict[key] # 如果secondDict的键值不是一个字典,则分类完成
return classLabel # 返回类标签
(8) 使用pickle模块存储决策树:
# 使用pickle模块存储决策树
def storeTree(inpuTree,filename):
import pickle
fw=open(filename,'wb')
pickle.dump(inpuTree,fw)
fw.close()
def grabTree(filename):
import pickle
fr=open(filename,'rb')
return pickle.load(fr)
(9)使用决策树预测隐形眼镜类型:
# 使用决策树预测隐形眼镜类型
# 1.收集数据:提供的文本文件
# 2.准备数据:解析tab键分隔的数据行
# 3.分析数据:快速的检查数据,确保正确的解析数据内容。
# 4.训练算法:使用createTree()函数
# 5.测试函数
# 6.使用算法:存储树的数据结构,以便下次使用时无需重新构造树
def predictLenseesType(filename):
# 打开文本数据
fr=open(filename)
# 将文本数据的每一个数据行按照tab键分隔,并依次存入lenses
lenses=[inst.strip().split('\t') for inst in fr.readlines()]
# 创建并存入特征标签
lensesLabels=['age','prescript','astigmatic','tearRate']
# 根据文件得到的数据集和特征标签列表创建决策树
lensesTree=createTree(lenses, lensesLabels)
return lensesTree
lensesTree=predictLenseesType('lenses.txt')
print(lensesTree)
可以输出结果,我在这里就不粘贴了。
关于 .strip() 和 .split() 用法可以参考后面的资料列表。
参考资料列表:
1.《统计学习方法》李航
2.《机器学习实战》Peter Harrington
3.csdn博文等号对齐问题:https://blog.csdn.net/qq_33039859/article/details/89253578
4.csdn博文中公式下标太大问题:https://blog.csdn.net/wnsfzf/article/details/9389125
5.决策树算法代码理解: https://www.cnblogs.com/zy230530/p/6813250.html
6..strip() 和 .split() 用法:https://www.cnblogs.com/yyxayz/p/4034299.html