1.决策树基本概念
顾名思义,决策树就是一棵树,一颗决策树包含一个根节点、若干个内部结点和若干个叶结点;叶结点对应于决策结果(也就是一个类别),其他每个结点则对应于一个属性测试,每个分支代表一个测试输出;每个结点包含的样本集合根据属性测试的结果被划分到子结点中;根结点包含样本全集,从根结点到每个叶子结点的路径对应了一个判定测试序列。
2. ID3算法原理
决策树学习的关键如何选择最优划分属性。一般而言,随着划分过程不断进行,我们希望决策树的分支结点所包含的样本尽可能属于同一类别,即结点的“ 纯度”(purity)越来越高.
2.1信息熵
那么如何来度量特征的纯度呢? “信息熵”
信息熵”(informationentropy)是度量样本集合纯度最常用的一种指标。假定当前样本集合D中第k类样本所占的比例为pk (k= 1,2…|K|) ,K为类别的总数(对于二元分类来说,K=2),则样本集D 的信息熵定义为:
Ent的值越小,则D的纯度越高。
2.2 信息增益
假定离散属性a有V个可能的取值{a1,a2…,av} ,如果使用特征a来对数据集D进行划分,则会产生V个分支结点, 其中第v个结点包含了数据集D中所有在特征a上取值为aᵛ的样本总数,记为Dᵛ。因此可以根据上面信息熵的公式计算出信息熵,再考虑到不同的分支结点所包含的样本数量不同,给分支节点赋予权重|Dᵛ|/|D|,即样本数越多的分支结点的影响越大,因此,能够计算出特征a对样本集D进行划分所获得的“信息增益”:
一般而言,信息增益越大,则表示使用特征 a对数据集划分所获得的“纯度提升”越大。因此,我们可以用信息增益来进行决策树的划分属性选择,其实就是选择信息增益最大的属性。
ID3决策树学习算法就是以信息增益为准则来划分属性。
前面留了一个问题,应该怎么构建一棵决策树呢?应该把哪个属性放在根节点呢?
构造树的基本想法是随着树深度的增加,节点的熵迅速地降低。熵值越低,说明模型越纯。哪些结点当做根结点我们不知道,那我们是不是可以拿熵值当做一个衡量的标准?哪些结点能使熵值下降的越大,我们就用哪个结点当做根节点。
熵降低的速度越快越好,这样我们有望得到一棵高度最矮的决策树。
2.3 构建决策树
决策树可以分成两个阶段:
-
1.训练阶段
从给定的训练数据集DB,构造出一棵决策树。
class=DecisionTree(DB) -
2.分类阶段
从根开始,按照决策树的分类属性逐层往下划分,直到叶节点,获得概念(决策、分类)结果。
y=DecisionTree(x)
2.4 ID3算法基本流程
ID3算法的核心是在决策树各个结点上对应信息增益准则选择特征,递归地构建决策树。
具体方法是:
step1:从根结点(root node)开始,对结点计算所有可能的特征的信息增益,选择信息增益最大的特征作为结点的特征。
step2: 由该特征的不同取值建立子节点,再对子结点递归地调用以上方法,构建决策树;直到所有特征的信息增益均很小或没有特征可以选择为止;
step3:最后得到一个决策树。
2.5 例子
以实际的例子对ID3算法进行阐述。
我们统计了14天的气象数据(指标包括outlook,temperature,humidity,windy),并已知这些天气是否逛街(play)。如果给出新一天的气象指标数据:sunny,cool,high,TRUE,我们的任务就是构建一棵决策树来进行判断会不会去逛街。
首先我们给定一张数据表(14行数据,每个数据4个特征outlook、temperature、humidity、windy),数据表中记录的是一些信息,如下图所示:
令weather数据集为s,其中有14个样本,目标属性play ball有2个值{C1=yes,C2=no}。14个样本的分布为:9个样本的类标号取值为yes, 5个样本的类标号取值为No。C1=yes 在所有样本s中出现的概率为9/14. C2=no 在所有样本s中出现的概率为5/14.
在没有给任何天气信息时,根据历史数据,我们只知道新的一天逛街的概率是9/14,不逛街的概率是5/14。此时的熵为:
分别基于天气、温度、湿度、是否有风进行划分,结果如下:
可以看出,属性有4个: outlook , temperature , humidity , windy。我们首先要决定哪个属性作树的根结点。
对每项指标分别统计:在不同的取值下逛街和不逛街的次数。
下面我们计算当已知变量outlook的值时,信息熵为多少。
outlook=sunny时, 2/5的概率逛街, 3/5的概率不逛街,entropy=0.971;
outlook=overcast时, entropy=0;
outlook=rainy时, entropy=0.971;
而根据历史统计数据, outlook取值为sunny、overcast. rainy的概率分别是5/14、 4/14、 5/14 ,所以当已知变量outlook的值时,信息熵为: 5/14x 0.971 + 4/14x0+ 5/14x 0.971 = 0.693。
这样的话系统熵就从0.940下降到了0.693 ,信息增溢gain(outlook)为0.940-0.693=0.247。
同理,如果以humidity作为根节点:
Entropy(high)=0.985 ; Entropy(normal)=0.592;
gain(humidity)=0.940-(7/14)*Entropy(high)-(7/14)*Entropy(normal)=0.152;
以temperature作为根节点:
Entropy(cool)=0.811 ; Entropy(hot)=1.0 ; Entropy(mild)=0.918
gain(temperature)=0.940-(4/14)*Entropy(cool)-(4/14)*Entropy(hot)-(6/14)*Entropy(mild)=0.029;
以windy作为根节点:
Entropy(false)=0.811;Entropy(true)=1.0;
gain(windy)=0.940-(8/14)*0.811-(6/14)*1.0=0.048;
这样我们就得到了以上四个属性相应的信息增益值:
gain(temperature)=0.029 , gain(humidity)=0.152 , gain(windy)=0.048, gain(outlook)=0.247。
可以看出,gain(outlook)最大。
因为gain(outlook)最大(即outlook在第一步使系统的信息熵下降得最快) , 所以决策树的根节点就取outlook。
因此可以确定,当分支特征为outlook(即天气)时带来的信息增益最大,因此根节点的分支特征选择为outlook,分为sunny、overcast、rainy三支,左子节点中的数据集为sunny的样本,中间结点的数据集为overcast,右子节点的数据集为rainy,之后以此类推便构建出了决策树模型。
得到的决策树如下:
所以,如果给出新一天的气象指标数据:sunny,cool,high,TRUE,则根据得到的决策树可以判断出:今天不会去逛街。
3. 代码实现
3.1 ID3算法
from math import log
import tree_plotter
"""
函数说明:创建测试数据集
Parameters:无
Returns:
dataSet:数据集
labels:分类属性
Modify:
2021-03-20
"""
# 加载数据
def creatDataSet():
# 数据集
dataSet = [['sunny', 'hot', 'high', 'FALSE', 'no'],
['sunny', 'hot', 'high', 'TRUE', 'no'],
['overcast', 'hot', 'high', 'FALSE', 'yes'],
['rainy', 'mild', 'high', 'FALSE', 'yes'],
['rainy', 'cool', 'normal', 'FALSE', 'yes'],
['rainy', 'cool', 'normal', 'TRUE', 'no'],
['overcast', 'cool', 'normal', 'TRUE', 'yes'],
['sunny', 'mild', 'high', 'FALSE', 'no'],
['sunny', 'cool', 'normal', 'FALSE', 'yes'],
['rainy', 'mild', 'normal', 'FALSE', 'yes'],
['sunny', 'mild', 'normal', 'TRUE', 'yes'],
['overcast', 'mild', 'high', 'TRUE', 'yes'],
['overcast', 'hot', 'normal', 'FALSE', 'yes'],
['rainy', 'mild', 'high', 'TRUE', 'no']]
#分类属性
labels=['outlook','temperature','humidity','windy']
#返回数据集和分类属性
return dataSet,labels
"""
函数说明:计算给定数据集的经验熵(香农熵)
Parameters:
dataSet:数据集
Returns:
shannonEnt:经验熵
Modify:
2021-03-20
shannonEnt=-∑pk*logpk
"""
# 计算标准熵
def calcShannonEnt(dataSet):
# 返回数据集行数
num_data = len(dataSet)
shannonEnt = 0.0 # 经验熵
# print(num_data)
label_count = {} # 保存每个标签(label)出现次数的字典
# 对每组特征向量进行统计
for i in dataSet:
# 当使用负数索引时,python将从右开始往左数,因此 -1 是最后一个元素的位置
label = i[-1] # 提取标签信息
if label not in label_count.keys(): # 如果标签没有放入统计次数的字典,添加进去
label_count[label] = 0
label_count[label]+=1 # label计数
# print(label)
# print(label_count) # 输出每个标签(label)的出现次数
# 计算经验熵
for key in label_count.keys():
Prob = float(label_count[key])/num_data # 选择该标签的概率
shannonEnt-=Prob*log(Prob,2) #利用公式计算
return shannonEnt #返回经验熵
"""
函数说明:按照给定特征划分数据集
Parameters:
dataSet:待划分的数据集
axis:划分数据集的特征
value:需要返回的特征的值
Returns:
retDataSet:划分后的数据集
Modify:
2021-03-20
"""
def splitDataSet(dataSet, axis, value):#划分子集,来求条件熵
# 创建返回的数据集列表
retDataSet = []
# 遍历数据集
for featVec in dataSet:
if featVec[axis] == value:
# 去掉axis特征
reducedFeatVec = featVec[:axis]
# print("reducedFeatVec",reducedFeatVec)
# 将符合条件的添加到返回的数据集
reducedFeatVec.extend(featVec[axis + 1:])
# print("reducedFeatVec=",reducedFeatVec)
retDataSet.append(reducedFeatVec)
# print("retDataSet=",retDataSet)
# 返回划分后的数据集
return retDataSet
"""
函数说明:计算给定数据集的经验熵(香农熵)
Parameters:
dataSet:数据集
labels:分类属性
Returns:
shannonEnt:信息增益最大特征的索引值
Modify:
2021-03-20
condition_Ent=∑(|D^v|/|D|)*Ent(D^v)
信息增益 Gain(D,a)=Ent(D)-∑(|D^v|/|D|)*Ent(D^v)
"""
def chooseBestFeatureToSplit(dataSet, labels):
num_data = float(len(dataSet)) # 数据集行数
num_label = len(dataSet[0]) - 1 # 特征数量
# 计数数据集的香农熵,即什么都没有做时根据已知数据集计算出来的熵
shannonEnt = calcShannonEnt(dataSet)
best_information_value = 0.0 # 将最佳信息增益初始化为0
best_label_axis = -1 # 最优特征的索引值
# 遍历所有特征
for i in range(num_label):
# 获取dataSet的第i个所有特征
label_list = [example[i] for example in dataSet]
#创建set集合{},元素不可重复
label_set = set(label_list)
# print(f'label_list = {label_list}')
# print(f'label_set = {label_set}')
condition_Ent = 0.0 # #经验条件熵,初始化条件熵为0
# 计算信息增益
for label in label_set:
# set_after_split划分后的子集
set_after_split = splitDataSet(dataSet,i,label)
# 计算子集的概率
Prob = len(set_after_split)/num_data
# 根据公式计算经验条件熵
condition_Ent += Prob*calcShannonEnt((set_after_split))
# 计算信息增益的公式
imformation_value = shannonEnt-condition_Ent
# 打印出每个特征的信息增益
print("第%d个特征%s的增益为%.3f" % (i, labels[i], imformation_value))
# print("第%d个特征的增益为%.3f" % (i,imformation_value))
if imformation_value > best_information_value:#比较出最佳信息增益
# 更新信息增益,找到最大的信息增益
best_information_value = imformation_value
# 记录信息增益最大的特征的索引值
best_label_axis = i
# 返回信息增益最大特征的索引值
return best_label_axis
"""
函数说明:创建决策树
Parameters:
dataSet:训练数据集
labels:分类属性标签
featLabels:存储选择的最优特征标签
Returns:
myTree:决策树
Modify:
2021-03-20
"""
# ID3算法核心:以每个结点上的信息增益为选择的标准来递归的构建决策树
def createTree(dataSet,label):
# 取分类标签(是否逛街:yes or no)
class_list = [example[-1] for example in dataSet]
# 如果类别完全相同,则停止继续划分
if class_list.count(class_list[0]) == len(class_list): #count() 方法用于统计某个元素在列表中出现的次数。
return class_list[0]
bestFeat = chooseBestFeatureToSplit(dataSet,label) # 选择最优特征
bestFeatLabel = labels[bestFeat] # 最优特征的标签
# print(bestFeat,bestFeatLabel)
# 根据最优特征的标签生成树
mytree = {bestFeatLabel:{}}
# 删除已经使用的特征标签
del(label[bestFeat])
# 得到训练集中所有最优特征的属性值
clasify_label_value = [example[bestFeat] for example in dataSet]
# set 可以去掉重复的属性值
set_clasify_label_value = set(clasify_label_value)
# 遍历特征,创建决策树
for value in set_clasify_label_value:
new_label = label[:] # 子集合
# 构建数据的子集合,并进行递归
mytree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),new_label)
return mytree
"""
决策树分类
:param input_tree: 决策树
:param test_vec: 测试的数据
:return: class_label 类别标签 是否可以去逛街(yes or no)
"""
def classify(inputTree,testVec):
dataSet, labels = creatDataSet()
#获取决策树节点
firstStr=next(iter(inputTree)) #iter() 函数用来生成迭代器。iter(object[, sentinel])object -- 支持迭代的集合对象。
#下一个字典
secondDict=inputTree[firstStr]
featIndex=labels.index(firstStr)
for key in secondDict.keys():
if testVec[featIndex]==key:
if type(secondDict[key]).__name__=='dict':
classLabel=classify(secondDict[key],testVec)
else: classLabel=secondDict[key]
return classLabel
if __name__=='__main__':
dataSet,labels=creatDataSet()
# print(dataSet,labels)
mytree = createTree(dataSet,labels)
print(f'决策树:{mytree}')
tree_plotter.create_plot(mytree)
#测试数据
print('测试数据:')
testVec=['sunny','cool','high','TRUE']
result=classify(mytree,testVec)
if result=='yes':
print( f'当outlook、temperature、humidity、windy={testVec}时:逛街')
if result=='no':
print(f'当outlook、temperature、humidity、windy={testVec}时:不逛街')
3.2 画出决策树
import matplotlib.pyplot as plt
decision_node = dict(boxstyle="sawtooth", fc="0.8")
leaf_node = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
def plot_node(node_txt, center_pt, parent_pt, node_type):
create_plot.ax1.annotate(node_txt, xy=parent_pt, xycoords='axes fraction', \
xytext=center_pt, textcoords='axes fraction', \
va="center", ha="center", bbox=node_type, arrowprops=arrow_args)
def get_num_leafs(my_tree):
num_leafs = 0
first_str = list(my_tree.keys())[0]
second_dict = my_tree[first_str]
for key in second_dict.keys():
if type(second_dict[key]).__name__ == 'dict':
num_leafs += get_num_leafs(second_dict[key])
else:
num_leafs += 1
return num_leafs
def get_tree_depth(my_tree):
max_depth = 0
first_str = list(my_tree.keys())[0]
second_dict = my_tree[first_str]
for key in second_dict.keys():
if type(second_dict[key]).__name__ == 'dict':
thisDepth = get_tree_depth(second_dict[key]) + 1
else:
thisDepth = 1
if thisDepth > max_depth:
max_depth = thisDepth
return max_depth
def plot_mid_text(cntr_pt, parent_pt, txt_string):
x_mid = (parent_pt[0] - cntr_pt[0]) / 2.0 + cntr_pt[0]
y_mid = (parent_pt[1] - cntr_pt[1]) / 2.0 + cntr_pt[1]
create_plot.ax1.text(x_mid, y_mid, txt_string)
def plot_tree(my_tree, parent_pt, node_txt):
num_leafs = get_num_leafs(my_tree)
depth = get_tree_depth(my_tree)
first_str = list(my_tree.keys())[0]
cntr_pt = (plot_tree.x_off + (1.0 + float(num_leafs)) / 2.0 / plot_tree.total_w, plot_tree.y_off)
plot_mid_text(cntr_pt, parent_pt, node_txt)
plot_node(first_str, cntr_pt, parent_pt, decision_node)
second_dict = my_tree[first_str]
plot_tree.y_off = plot_tree.y_off - 1.0 / plot_tree.total_d
for key in second_dict.keys():
if type(second_dict[key]).__name__ == 'dict':
plot_tree(second_dict[key], cntr_pt, str(key))
else:
plot_tree.x_off = plot_tree.x_off + 1.0 / plot_tree.total_w
plot_node(second_dict[key], (plot_tree.x_off, plot_tree.y_off), cntr_pt, leaf_node)
plot_mid_text((plot_tree.x_off, plot_tree.y_off), cntr_pt, str(key))
plot_tree.y_off = plot_tree.y_off + 1.0 / plot_tree.total_d
def create_plot(in_tree):
fig = plt.figure(1, facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[])
create_plot.ax1 = plt.subplot(111, frameon=False, **axprops)
plot_tree.total_w = float(get_num_leafs(in_tree))
plot_tree.total_d = float(get_tree_depth(in_tree))
plot_tree.x_off = -0.5 / plot_tree.total_w
plot_tree.y_off = 1.0
plot_tree(in_tree, (0.5, 1.0), '')
plt.show()
3.3 实现结果
详情可以查看我上传的PPT资源:https://download.youkuaiyun.com/download/ckc_csdn/16184237