0
点赞
收藏
分享

微信扫一扫

机器学习——03决策树算法

pipu 2022-12-20 阅读 149

机器学习——03决策树算法

参考资料

  1. AIlearning
  2. Machine-Learning-in-Action
  3. 庞善民.西安交通大学机器学习导论2022春PPT

一、信息熵与信息增益

🔥信息熵

信息熵使信息得以量化

一条信息的信息量和它的不确定性有着直接的关系

比如,要搞清楚一件非常不确定的事,或是我们一无所知的事情,就需要了解大量信息。相反,如果我们对某件事已经有了较多了解,那么不需要太多信息就能把它搞清楚

信息熵是消除不确定性所需信息量的度量,也即未知事件可能含有的信息量。需要引入消除不确定性的信息量越多,则信息熵越高,反之则越低。

例如“中国男足进军2022年世界杯决赛圈”,这个因为确定性很高,几乎不需要引入信息,因此信息熵很低。

信息熵的计算

Shannon定义的信息熵的计算公式如下:

image-20220317113414164

其中X表示随机变量,随机变量的取值为{𝑥1,𝑥2,…,𝑥𝑛},𝑃(𝑥𝑖)表示事件𝑥𝑖发生的概率,且有 ∑ 𝑃 ( 𝑥 𝑖 ) = 1 \sum𝑃(𝑥𝑖)=1 P(xi)=1信息熵的单位为比特(bit)

熵越小表示概率分布的纯度越高,反之,熵越大表示概率分布的纯度越低。

数据集的信息熵

设数据集D中有m个不同的类C1,C2,C3,…,Cm

设Di是数据集D中Ci类的样本的集合,|D|和|Di|分别是D和Di中的样本个数

数据集D的信息熵
I n f o ( D ) = − ∑ i = 1 m p i log ⁡ 2 p i Info(D)=-\sum^m_{i=1}p_i\log_2p_i Info(D)=i=1mpilog2pi
其中𝑝𝑖是数据集D中任意样本属于类Ci的概率,用 ∣ D i ∣ ∣ D ∣ \frac{|D_i|}{|D|} DDi估计

使用熵衡量数据纯度

假设有一个数据集合D,其中只有两个类,一个是正例类,一个是负例类。计算D中正例类和负例类在三种不同的组分下熵的变化情况。
(1)D中包含有50%的正例和50%的负例。
I n f o ( D ) = − 0.5 ∗ log ⁡ 2 0.5 − 0.5 ∗ log ⁡ 2 0.5 = 1 Info(D) = -0.5 * \log_20.5 - 0.5 * \log_20.5 = 1 Info(D)=0.5log20.50.5log20.5=1
(2)D中包含有20%的正例和80%的负例。
I n f o ( D ) = − 0.2 ∗ log ⁡ 2 0.2 − 0.8 ∗ log ⁡ 2 0.8 = 0.722 Info(D) = -0.2 * \log_20.2 - 0.8 * \log_20.8 = 0.722 Info(D)=0.2log20.20.8log20.8=0.722
(3)D中包含有100%的正例和0%的负例。
I n f o ( D ) = − 1 ∗ log ⁡ 2 1 − 0 ∗ log ⁡ 2 0 = 0 Info(D) = -1 * \log_21 - 0 * \log_20 =0 Info(D)=1log210log20=0
当数据变得越来越“纯”时,熵的值变得越来越小:当D中正反例比例相同时,熵取最大值;当D中所有数据属于一个类时,熵取最小值。因此,熵可以作为数据纯净度的衡量指标。

信息增益

信息增益可以衡量划分数据集前后数据纯度提升程度。信息增益=原数据信息熵−数据划分之后的信息熵

image-20220317113905578

其中,离散属性𝑎有𝐾个可能的取值{𝑎1,𝑎2,…,𝑎𝐾},其中第𝑘个分支节点包含了𝐷中所有在属性𝑎上取值为 𝑎 𝑘 𝑎^𝑘 ak的样本,记为 𝐷 𝑘 𝐷^𝑘 Dk

image-20220317113946167

二、决策树简介

决策树(Decision Tree)是一种基于树结构的分类预测方法

决策树引入

决策树(Decision Tree)又称为判定树,是用于分类的一种树结构。其中每个内部结点(internal node)代表对某个属性的一次测试,叶结点(leaf)代表某个(class)或者类的分布(class distribution),最上面的结点是根结点

决策树提供了一种展示在什么条件下会得到什么类别的方法。

决策树组成

决策树的基本组成部分:根节点、决策结点、分枝和叶子结点。树是由节点和分枝组成的层次数据结构。

image-20220317112744114

决策树是描述分类过程的一种数据结构,从上端的根节点开始,各种分类原则被引用进来,并依这些分类原则将根节点的数据集划分为子集,这一划分过程直到某种约束条件满足而结束。

决策树基本原理

首先对数据进行处理,利用归纳法生成可读的规则和决策树,然后使用决策对新数据进行分析。

本质上决策树是通过一系列规则对数据进行分类的过程。

决策树技术发现数据模式和规则的核心是采用递归分割的贪婪算法

使用 createBranch() 方法,如下所示:

def createBranch():
    检测数据集中的所有数据的分类标签是否相同:
        If so return 类标签
        Else:
            寻找划分数据集的最好特征(划分之后信息熵最小,也就是信息增益最大的特征)
            划分数据集
            创建分支节点
                for 每个划分的子集
                    调用函数 createBranch (创建分支的函数)并增加返回结果到分支节点中
            return 分支节点

决策树基本流程

1️⃣收集待分类的数据,这些数据的所有属性应该是完全标注的。

2️⃣设计分类原则,即数据的哪些属性可以被用来分类,以及如何将该属性量化。

3️⃣分类原则的选择,即在众多分类准则中,每一步选择哪一准则使最终的树更令人满意。

4️⃣设计分类停止条件,实际应用中数据的属性很多,真正有分类意义的属性往往是有限几个,因此在必要的时候应该停止数据集分裂:

决策树开发流程

  • 收集数据: 可以使用任何方法。
  • 准备数据: 树构造算法 (下面案例使用的是ID3算法,只适用于标称型数据,这就是为什么数值型数据必须离散化。 还有其他的树构造算法,比如CART) ,详情参考置顶博客。
  • 分析数据: 可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。
  • 训练算法: 构造树的数据结构。
  • 测试算法: 使用训练好的树计算错误率。
  • 使用算法: 此步骤可以适用于任何监督学习任务,而使用决策树可以更好地理解数据的内在含义。

决策树算法特点

优点: 计算复杂度不高,输出结果易于理解,数据有缺失也能跑,可以处理不相关特征。

缺点: 容易过拟合。 适用数据类型: 数值型和标称型。

三、决策树项目案例——判断鱼类和非鱼类

ID3算法原理

ID3算法在决策树各个节点上使用信息增益准则选择特征(属性)进行数据划分,从而递归地构建决策树。

具体方法

选择划分属性示例

以买电脑为例进行决策树划分说明

年龄收入学生信用买了电脑
<30一般
<30
30-40一般
>40中等一般
>40一般
>40
30-40
<30一般
<30一般
>40一般
<30
30-40
30-40一般
>40

1️⃣ 确立初始的信息熵

2️⃣ 确立第一次分裂的属性

🍎 如果按照年龄划分

🍌 如果按照收入划分

🍑 如果按照学生划分

🥝 如果按照信用划分

综上,“年龄”属性具有最高信息增益,成为分裂属性

3️⃣ 确立第二次分裂的属性

image-20220321162200499

按照上述方法,可以确定第二次分裂的属性为学生

4️⃣ 划分到不可划分为止

ID3算法总结

算法流程

优点

缺点

项目概述

根据以下 2 个特征,将动物分成两类: 鱼类和非鱼类。

特征:

  1. 不浮出水面是否可以生存
  2. 是否有脚蹼

开发流程

收集数据

不浮出水面是否可以生存?是否有脚蹼?是🐟吗?
YesYesYes
YesYesYes
YesNoNo
NoYesNo
NoYesNo

使用createDataSet()函数输入数据

def createDataSet():
    dataSet = [[1, 1, '是🐟'], [1, 1, '是🐟'], [1, 0, '不是🐟'], [0, 1, '不是🐟'],
               [0, 1, '不是🐟']]
    labels = ['不浮出水面是否可以生存', '是否有脚蹼']
    return dataSet, labels

准备数据

此处,由于我们输入的数据本身就是离散化数据,所以这一步就省略了。

分析数据

使用如下公式计算计算给定数据集的信息熵:
I n f o ( D ) = − ∑ i = 1 m p i log ⁡ 2 p i Info(D)=-\sum^m_{i=1}p_i\log_2p_i Info(D)=i=1mpilog2pi

def calInfoEntropy(dataSet):
    """计算信息熵  
    Args:
        dataSet 数据集
    Returns:
        返回 每一组feature下的某个分类下的信息熵
    """    
    # 统计标签出现的次数
    label_count = Counter(data[-1] for data in dataSet)
    # 计算概率
    probs = [p[1] / len(dataSet) for p in label_count.items()]
    # 计算香农熵
    infoEntropy = sum([-p * log(p, 2) for p in probs])
    return infoEntropy   

按照给定特征划分数据集

def splitDataSet(dataSet, index, value):
    """就是依据index列进行分类,如果index列的数据等于 value的时候,就要将 index 划分到我们创建的新的数据集中
    Args:
        dataSet 数据集                 待划分的数据集
        index 表示每一行的index列        划分数据集的特征
        value 表示index列对应的value值   需要返回的特征的值。
    Returns:
        index列为value的数据集【该数据集需要排除index列】
    """
    retDataSet = []
    for featVec in dataSet:
        # index列为value的数据集【该数据集需要排除index列】
        # 判断index列的值是否为value
        if featVec[index] == value:
            reducedFeatVec = featVec[:index]
            reducedFeatVec.extend(featVec[index+1:])
            # 收集结果值 index列为value的行【该行需要排除index列】
            retDataSet.append(reducedFeatVec)
    return retDataSet

选择最好的数据集划分方式:具有最高信息增益

def chooseBestFeatureToSplit(dataSet):
    """chooseBestFeatureToSplit(选择最好的特征)
    Args:
        dataSet 数据集
    Returns:
        bestFeature 最优的特征列
    """
    # 求特征数
    numFeatures = len(dataSet[0]) - 1
    # 数据集的原始信息熵
    baseEntropy = calInfoEntropy(dataSet)
    # 最优的信息增益值, 和最优的Featurn编号
    bestInfoGain, bestFeature = 0.0, -1
    # 遍历所有的特征
    for i in range(numFeatures):
        # 获取对应的feature下的所有数据
        featList = [example[i] for example in dataSet]
        # 获取剔重后的集合,使用set对list数据进行去重
        uniqueVals = set(featList)
        # 创建一个临时的信息熵
        newEntropy = 0.0
        # 遍历某一列的value集合,计算该列的信息熵
        # 遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集
        # 计算数据集的新熵值,并对所有唯一特征值得到的熵求和。
        for value in uniqueVals:
            subDataSet = splitDataSet(dataSet, i, value)
            # 计算概率
            prob = len(subDataSet)/float(len(dataSet))
            # 计算信息熵
            newEntropy += prob * calInfoEntropy(subDataSet)。
        infoGain = baseEntropy - newEntropy
        print('infoGain=', infoGain, 'bestFeature=', i, baseEntropy, newEntropy)
        if (infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature

训练算法

创建树的函数代码如下:

def majorityCnt(classList):
    """majorityCnt(选择出现次数最多的一个结果)
    Args:
        classList label列的集合
    Returns:
        bestFeature 最优的特征列
    """
    major_label = Counter(classList).most_common(1)[0]
    return major_label
    
def createTree(dataSet, labels):
    classList = [example[-1] for example in dataSet]
    # 如果数据集的最后一列的第一个值出现的次数=整个集合的数量,也就说只有一个类别,就只直接返回结果就行
    # 第一个停止条件: 所有的类标签完全相同,则直接返回该类标签。
    # count() 函数是统计括号中的值在list中出现的次数
    if classList.count(classList[0]) == len(classList):
        return classList[0]
    # 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果
    # 第二个停止条件: 使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。
    if len(dataSet[0]) == 1:
        return majorityCnt(classList)

    # 选择最优的列,得到最优列对应的label含义
    bestFeat = chooseBestFeatureToSplit(dataSet)
    # 获取label的名称
    bestFeatLabel = labels[bestFeat]
    # 初始化myTree
    myTree = {bestFeatLabel: {}}
    # 注: labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改
    # 所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示'no surfacing' is not in list
    del(labels[bestFeat])
    # 取出最优列,然后它的branch做分类
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    for value in uniqueVals:
        # 求出剩余的标签label
        subLabels = labels[:]
        # 遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree()
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
        # print ('myTree', value, myTree)
    return myTree

算法得出的决策树如下:

image-20221216120351798

测试算法

def classify(inputTree, featLabels, testVec):
    """classify(给输入的节点,进行分类)

    Args:
        inputTree  决策树模型
        featLabels Feature标签对应的名称
        testVec    测试输入的数据
    Returns:
        classLabel 分类的结果值,需要映射label才能知道名称
    """
    # 获取tree的根节点对于的key值
    firstStr = list(inputTree.keys())[0]
    # 通过key得到根节点对应的value
    secondDict = inputTree[firstStr]
    # 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类
    featIndex = featLabels.index(firstStr)
    # 测试数据,找到根节点对应的label位置,也就知道从输入的数据的第几位来开始分类
    key = testVec[featIndex]
    valueOfFeat = secondDict[key]
    print('+++', firstStr, 'xxx', secondDict, '---', key, '>>>', valueOfFeat)
    # 判断分枝是否结束: 判断valueOfFeat是否是dict类型
    if isinstance(valueOfFeat, dict):
        classLabel = classify(valueOfFeat, featLabels, testVec)
    else:
        classLabel = valueOfFeat
    return classLabel

def fishTest():
    # 1.创建数据和结果标签
    myDat, labels = createDataSet()
    myTree = createTree(myDat, copy.deepcopy(labels))
    # [1, 1]表示要取的分支上的节点位置,对应的结果值
    print(classify(myTree, labels, [1, 1]))

+++ 不浮出水面是否可以生存 xxx {0: '不是🐟', 1: {'是否有脚蹼': {0: '不是🐟', 1: '是🐟'}}} --- 1 >>> {'是否有脚蹼': {0: '不是🐟', 1: '是🐟'}}  
+++ 是否有脚蹼 xxx {0: '不是🐟', 1: '是🐟'} --- 1 >>> 是🐟
是🐟

使用算法:

此步骤可以适用于任何监督学习任务,而使用决策树可以更好地理解数据的内在含义。

举报

相关推荐

0 条评论