决策树典型算法ID3原理与代码实现

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.        1.训练阶段
           从给定的训练数据集DB,构造出一棵决策树。
                  class=DecisionTree(DB)

  2.        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.csdn.net/download/ckc_csdn/16184237

  • 11
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值