决策树定义
树想必大家都会比较熟悉,是由节点和边两种元素组成的结构。
理解树,就需要理解几个关键词:根节点、父节点、子节点和叶子节点。
父节点和子节点是相对的,说白了子节点由父节点根据某一规则分裂而来,然后子节点作为新的父亲节点继续分裂,直至不能分裂为止。而根节点是没有父节点的节点,即初始分裂节点,叶子节点是没有子节点的节点,如下图所示:
决策树利用如上图所示的树结构进行决策,每一个非叶子节点是一个判断条件,每一个叶子节点是结论。从根节点开始,经过多次判断得出结论。
从一个分类例子说起:
通过一个文件的信息(包括后缀、大小、来源、长度)来判断该文件是否是病毒,从而给出一定的参考建议。下表是现在能够掌握的信息,我们的目标是通过对下面的训练数据进行分析建立一个预测文件是否是病毒的模型。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
然后通过该模型来判断下表中的文件是否是病毒。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如何综合利用这些属性去判断该文件是否是病毒?决策树的做法是每次选择一个属性进行判断,如果不能得出结论,继续选择其他属性进行判断,直到能够“肯定地”判断出结果或者是上述属性都已经使用完毕。比如说我们要判断一个文件是否是病毒,我们可以先根据文件后缀进行判断,如果不能得出结论,再根据文件大小作判断,这样以此类推,直到可以得出结论为止。
决策树用树结构实现上述的判断流程,如图所示:
通过一开始的训练数据,我们可以建立下面的决策树。
其输入是文件的信息,输出是文件是否是病毒。
如果要判断某一文件是否是病毒,直接根据文件的文件后缀、文件大小、文件来源、文件名长度就可以分析得出文件是否是病毒。
如某文件的信息为:
[文件后缀,文件大小,文件来源,文件名长度]=[bat,15,下载,48]
将信息输入上述决策树,可以得到下列的分析步骤和结论。
决策树构建
如何构建上图所示一棵决策树呢?决策树的构建是数据逐步分裂的过程,构建的步骤如下:
2.1数据如何分割
假如我们已经选择了一个分裂的属性,那怎样对数据进行分裂呢?
分裂属性的数据类型分为离散型和连续性两种情况,对于离散型的数据,按照属性值进行分裂,每个属性值对应一个分裂节点;对于连续性属性,一般性的做法是对数据按照该属性进行排序,再将数据分成若干区间,如[0,10]、[10,20]、[20,30]…,一个区间对应一个节点,若数据的属性值落入某一区间则该数据就属于其对应的节点。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
上表可以表示成如下的决策树结构:
2.2如何选择分裂的属性
我们知道了分裂属性是如何对数据进行分割的,那么我们怎样选择分裂的属性呢?
决策树采用贪婪思想进行分裂,即选择可以得到最优分裂结果的属性进行分裂。那么怎样才算是最优的分裂结果?最理想的情况当然是能找到一个属性刚好能够将不同类别分开,但是大多数情况下分裂很难一步到位,我们希望每一次分裂之后子节点的数据尽量”单一”,以下图为例:
从上图可以明显看出,属性1分裂后的子节点比属性2分裂后的子节点更单一:属性2分裂后每个节点的两类的数量还是相同,跟根节点的分类结果相比几乎没有提高;按照属性1分裂后每个节点各类的数量相差比较大,可以很大概率认为第1个子节点的输出结果为类1,第2个子节点的输出结果为2。
选择分裂属性是要找出能够使所有子节点数据最单一的属性,决策树使用信息增益或者信息增益率作为选择属性的依据。
2.2.1信息增益
用信息增益表示分裂前后跟的数据复杂度和分裂节点数据复杂度的变化值。
在详细了解信息增益之前,还需对信息熵进行一定的了解
在信息论中,熵(entropy)是随机变量不确定性的度量,也就是熵越大,则随机变量的不确定性越大。设X是一个取有限个值得离散随机变量,其概率分布为:
则随机变量X的熵定义为:
其中Pi表示类i的数量占比。以二分类问题为例,如果两类的数量相同,此时分类节点的纯度最低,熵等于1;如果节点的数据属于同一类时,此时节点的纯度最高,熵等于0。
设有随机变量(X, Y),其联合概率分布为:
条件熵H(Y|X)表示在已知随机变量X的条件下,随机变量Y的不确定性。随机变量X给定的条件下随机变量Y的条件熵H(Y|X),定义为X给定条件下Y的条件概率分布的熵对X的数学期望:
当熵和条件熵中的概率由数据估计得到时(如极大似然估计),所对应的熵与条件熵分别称为经验熵和经验条件熵。
信息增益定义:信息增益表示由于得知特征A的信息时的数据集D的分类不确定性减少的程度,定义为:
即集合D的信息熵H(D)与特征A给定条件下D的条件熵H(H|A)之差。
信息增益说白了就是分裂前的数据复杂度减去子节点的数据复杂度的和,信息增益越大,分裂后的复杂度减小得越多,分类的效果越明显。
以熵作为节点复杂度的统计量,分别求出下面例子的信息增益,左图表示节点选择属性1进行分裂的结果,右图表示节点选择属性2进行分裂的结果,通过计算两个属性分裂后的信息增益,选择最优的分裂属性。
由于Gain1<Gain2,所以属性2与属性1相比是更优的分裂属性,故选择属性2作为分裂的属性。
使用信息增益作为选择分裂的条件有一个不可避免的缺点:倾向选择分支比较多的属性进行分裂。
同样是类1:30,类2:25
最理想的分割:[[30,0],[0,25]]
最糟糕的分割:
上面两个分割后的H(D|A)都等于0
2.2.2信息增益率
使用信息增益作为选择分裂的条件有一个不可避免的缺点:倾向选择分支比较多的属性进行分裂。为了解决这个问题,引入了信息增益率这个概念。
信息增益率是在信息增益的基础上除以分裂节点数据量的信息增益(听起来很拗口),其计算公式如下:
其中,Info_Gain 表示信息增益
InstrinsicInfo表示分裂子节点数据量的熵,其计算公式为:
其中m表示子节点的数量
ni表示第i个子节点的数据量
N表示父节点数据量
说白了,其实InstrinsicInfo是分裂节点相对父节点的数据量的熵
如果节点的数据链越接近, InstrinsicInfo越大
如果子节点越大, InstrinsicInfo越大
信息增益率越高,说明分裂的效果越好。
InstrinsicInfo = Entropy([20,35])= 0.994030211477
什么时候停止分裂。
决策树不可能不限制地生长,总有停止分裂的时候,最极端的情况是当节点分裂到只剩下一个数据点时自动结束分裂,但这种情况下树过于复杂。一般情况下为了降低决策树复杂度和提高预测的速度,会适当提前终止节点的分裂。
以下是决策树节点停止分裂的一般性条件:
(1)最小节点数
(2)熵或者基尼值小于阀值。
由上述可知,熵和基尼值的大小表示数据的复杂程度,当熵或者基尼值过小时,表示数据的纯度比较大,如果熵或者基尼值小于一定程度数,节点停止分裂。
(3)决策树的深度达到指定的条件
节点的深度可以理解为节点与决策树跟节点的距离,如根节点的子节点的深度为1,因为这些节点与跟节点的距离为1,子节点的深度要比父节点的深度大1。决策树的深度是所有叶子节点的最大深度,当深度到达指定的上限大小时,停止分裂。
(4)所有特征已经使用完毕,不能继续进行分裂。
被动式停止分裂的条件,当已经没有可分的属性时,直接将当前节点设置为叶子节点。
算法原理
根据决策树的输出结果,决策树可以分为分类树和回归树,分类树输出的结果为具体的类别,而回归树输出的结果为一个确定的数值。
决策树的构建算法主要有ID3、C4.5、CART三种,其中ID3和C4.5是分类树,CART是分类回归树,ID3、C4.5和CART将在下面的算法原理的中分别讲述。
其中ID3是决策树最基本的构建算法,而C4.5和CART是在ID3的基础上进行优化的算法。
ID3算法
创建数据集
def create_data_set():
dataset = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['usb', 'exe'] # 第一个代表是否从U盘下载来,第二个代表是否是exe文件后缀,第三个代表是否是病毒文件
print(dataset, labels)
return dataset, labels
熵
# 计算数据中的信息熵: H = -∑p(x)log2 p(x)
def calc_shannon_ent(dataset):
numentries = len(dataset)
labelcounts = {} # 创建标签字典
for featvec in dataset:
currentlabel = featvec[-1] # 数据的最后一列是标签作为字典的键值
if currentlabel not in labelcounts.keys(): # 记录属于该标签数据的次数,后面计算熵使用,
labelcounts[currentlabel] = 0 # 如果字典里没有这个标签即键值,则创建并赋值为0,如果存在该标签则加1
labelcounts[currentlabel] += 1
shannonent = 0.0 # 计算熵值
for key in labelcounts:
prob = float(labelcounts[key]) / numentries # 以每个标签出现的频次为概率计算熵值
shannonent -= prob * log(prob, 2)
return shannonent
划分数据集
# 按照给定特征划分数据集
def split_data_set(dataset, axis, value): # dataset为原始输入数据,axis是原始数据的特征位置,value是数据对应的特征值
retdataset = []
for featvec in dataset: # 迭代dataset中的数据
if featvec[axis] == value: # 通过迭代判断每个数据特征位置的数据和给定的特征值是否一致
reducedfeatvec = featvec[:axis] # 如果条件成立,那么就把该位置前面的数据保存
reducedfeatvec.extend(featvec[axis+1:]) # 把该位置后面的数据保存,通过这两句就可以把该特征位置删除
retdataset.append(reducedfeatvec) # 把保留下来的数据,存储在新的列表里作为一个元素
'''下面有例子解释一下上面的三句代码
trees.split_data_set(mydata, 0, 1),输入的数据为mydata, 特征位置为0即axis, 数据特征值为1
执行后的数据
原始数据: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
划分好的数据: [[1, 'yes'], [1, 'yes'], [0, 'no']]
原始数据是一个列表,这个列表有5个元素,而每个元素又是一个列表,每个元素列表中又有三个元素
featvec[axis] == value这句代码的意思是featvec从dataset中获得元素后,例如为第一个元素[1, 1, 'yes'],
此时featvec[axis]是指该元素的第零个位置数据即为1,value为给定的特征划分值为1,此时if成立,执行下面的程序
reducedfeatvec = featvec[:axis],列表的切片,不会的请查看列表的切片,featvec[:axis]是指把axis位置前面的数据传给reducedfeatvec
,而featvec[axis+1:]是把特征位置后面的数据都保存下来如[1, 'yes'],把保存下来,简单说 这两句意思就是把axis位置删除
把重新组建的数据传给retdata
'''
return retdataset
信息增益划分数据集
# 选择数据集划分方式,主要通过信息增益进行划分
def choose_bestfeature_to_split(dataset):
num_features = len(dataset[0]) - 1 # 计算每个元素有多少特征
print(num_features) # 只计算前两个数值特征
base_entropy = calc_shannon_ent(dataset) # 计算基础香浓熵
best_info_gain = 0.0
best_feature = -1
# 下面为计算信息增益做准备工作
for i in range(num_features):
feat_list = [example[i] for example in dataset] # 利用列表推倒,把每个元素数据的特征提取出来,并创建新列表
print('feat_list', feat_list) # 打印出来发现是feat_list [1, 1, 1, 0, 0],即表明把dataset的第0号位置的特征提取出来
unique_vals = set(feat_list) # 使用集合元素数据中重复的特征合并
print('unique_vals', unique_vals) # 把feat_list [1, 1, 1, 0, 0]重合的合并为unique_vals {0, 1}
new_entropy = 0.0
for value in unique_vals:
subdataset = split_data_set(dataset, i, value) # 根据特征把同类的数据归类,并计算香浓熵
prob = len(subdataset)/float(len(dataset)) # 计算概率
new_entropy += prob * calc_shannon_ent(subdataset) # 计算条件熵
info_gain = base_entropy - new_entropy # 计算信息增益
if (info_gain > best_info_gain): # 找出信息增益最大的特征
best_info_gain = info_gain
best_feature = i
return best_feature
创建决策树
# 创建决策树
def create_tree(dataset, labels):
classlist = [example[-1] for example in dataset]
# 该句代码的意思是,把元素数据的分类信息提取出来,此时classlist = ['yes', 'yes', 'no', 'no', 'no']
if classlist.count(classlist[0]) == len(classlist):
return classlist[0]
'''
# 第一个递归停止条件,如果classlist中的信息都为同一类,则说明分类已经是纯净的了,无须再划分,返回类别就好
# 如classlist = ['no', 'no', 'no'],此时classlist[0]出现三次,而列表长度也为3,等式成立执行if语句,即返回类别’no‘
'''
if len(dataset[0]) == 1: # 使用完了所有的特征,但还是无法将数据划分为唯一类别的分组,因此就挑选出现次数最多的类别进行分类
return max(classlist,key=classlist.count)
'''
# 第二个递归停止条件是根据特征判断,因为每次根据特征划分时,后面就会删除该特征,如果遍历完所有特征以后发现classlist还是不纯净,
# 那么就通过出现的次数划分。如刚开始dataset[0] = [1, 1, 'yes'],有两个特征,一个分类标签,当每根据一个特征进行分类结束时就会
# 删除该特征,最后只剩下dataset[0] = ['yes'],无法再根据特征进行分类,此时,根据分类列表中把该元素划分到出现类别最多的次数的
# 分类中,如classlist = ['no', 'yes', 'yes','yes'],此时‘no’出现一次,‘yes’出现三次,因此把该类分为‘yes’,其中通过函数
# majority_cnt()进行统计。
'''
bestfeat = choose_bestfeature_to_split(dataset) # 使用信息增益返回最好的划分特征的列表位置信息
# 分类的标准是通过ID3算法即信息增益进行划分,返回的是使信息增益增加最大的特征
bestfeatlabel = labels[bestfeat]
# 提出该最优特征 第一次特征为'U盘'
mytree = {bestfeatlabel: {}}
# 以当前使信息增益最大的特征创建以字典形式迭代的迭代器,此时为mytree = {U盘: {}}
del (labels[bestfeat])
# 删除该使用的特征
featvalues = [example[bestfeat] for example in dataset]
# 遍历所有元素的最优特征位置对应的特征值featvalues = [1, 1, 1, 0, 0]
unique_vals = set(featvalues)
# 转换成集合形式,即去除重复的特征值,集合的互异性可以使相同特征值合并,此时unique_vals = [1,0]
for value in unique_vals:
sublabels = labels[:]
# 复制一份 标签列表
mytree[bestfeatlabel][value] = create_tree(split_data_set(dataset, bestfeat, value), sublabels)
return mytree
使用决策树的分类函数进行预测
def classify(inputtree, featlabels, testvec):
firststr = list(inputtree.keys())[0]
second_dict = inputtree[firststr]
feat_index = featlabels.index(firststr)
for key in second_dict.keys():
if testvec[feat_index] == key:
if type(second_dict[key]) == dict:
classlabel = classify(second_dict[key], featlabels, testvec)
else:
classlabel = second_dict[key]
return classlabel
预测结果
data_set, labels = create_data_set()
decision_tree = create_tree(data_set, labels)
print "决策树:", decision_tree
print classify(decision_tree, labels, [1, 0])
print classify(decision_tree, labels, [1, 1])
print classify(decision_tree, labels, [0, 0])
output:
决策树: {'usb': {0: 'no', 1: {'exe': {0: 'no', 1: 'yes'}}}}
no
yes
no
应 用
#coding=utf-8
from sklearn import tree
import pydotplus
x=[[1,0,0],[0,1,1],[0,1,0],[1,1,1]]
y=[1,1,0,1]
clf = tree.DecisionTreeClassifier()
clf = clf.fit(x, y)
print(clf.predict([[1,0,1]])) # output:1
dot_data = tree.export_graphviz(clf, out_file=None)
graph = pydotplus.graph_from_dot_data(dot_data)
graph.write_png("./决策树.png")
原文始发于微信公众号(SAINTSEC):深入浅出决策树
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论