ID3决策树中连续值的处理。

版权声明:本文为博主原创文章,转载请标明出处:http://blog.csdn.net/leafage_m https://blog.csdn.net/Leafage_M/article/details/80137305

在之前的基于信息增益的ID3决策树介绍。介绍了ID决策树的一些内容,但是其中所使用的特征值都是离散的,但有些特征值可能不是离散而是连续属性,比如在周志华老师的西瓜书中西瓜数据集3.0就包含了密度和含糖率这两个特征值。本文以西瓜书中的知识点为基础来大致讲解一下如何处理计算这种连续属性。


连续属性的介绍。

在西瓜数据集3.0中出现了密度和含糖率这两种连续属性。在之前介绍过决策树的划分条件,是选取一个最合适的特征属性,然后将集合按照这个特征属性的不同值划分为多个子集合,并且不断的重复这种操作的过程。

那么如果出现了连续属性呢?所谓的连续属性就是像:0.697、0.774、0.634等的数字。

比如我们想按照密度进行划分,那么就会有多个密度值,如果样本足够大那么就会有无数个密度值,显然我们不能以这些密度值直接进行分散集合。否则某个密度将会对应一种分类,比如0.697属于好瓜,那么划分结果就会认为密度为0.697的瓜是好瓜,但是密度这中属性变化性太多,所以不能直接根据单个密度来判断。

但是我们又想让密度这个特征属性参与到决策树的建立中,那么就可以思考一下如何利用密度这个属性呢?

打个比方,在直角坐标系中x轴上有无数个点,这些点就像对应的密度值一样,我们无法计算出其总数,所以不能用这个点进行划分。但是直角坐标系中还有y轴,在y轴左边的点x值都是负的,在y轴右边的点都是正的,所以可以说y轴将x轴划分为正负两个部分。那么我们就可以利用这种划分方法将密度值进行划分多个子区域,这样密度值的子区域是有限的,所以就可以进行计算了。

比如我们按照0.697进行划分,那么就可以将密度值划分为:密度值大于0.697和密度值小于0.697的两个部分,然后我们将这两个部分看为离散值,这样就可以利用密度值参加决策树的划分过程了。

这里写图片描述

下面将介绍如何将连续属性离散化。


连续属性离散化。

由于连续属性的可取值数目不再有限,因此,不能直接根据连续属性的可取值来对节点进行划分。此时,我们可以使用连续属性离散化的技术将连续属性转换为离散的。

最简单的策略是使用二分法对连续属性进行处理。下面是书中对该方法的介绍。

给定样本集 D 和连续属性 a ,假定 a D 上出现了 n 个不同的取值,将这些值从小到大进行排序,记为{ a1,a2,a3,...,an }。基于划分点 t 可将 D 分为子集 Dt D+t 。其中 Dt 包含那些在属性a上取值不大于 t 的样本,而 D+t 则包含那些在属性a上取值大于 t 的样本。显然,对相邻的属性取值 ai ai+1 来说, t 在区间 [ai,ai+1) 中取任意值所产生的划分结果相同。因此,对于连续属性 a ,我们可考察包含 n1 个元素的候选划分集合。

Ta={ai+ai+12|1<=i<=n1}

即把区间 [ai,ai+1) 的中位点 ai+ai+12 作为候选划分点。然后我们就可像离散属性值一样来考察这些划分点,选取最优的划分点进行样本集合的划分。

介绍了这么多的概念,下面用数据集中的数据对这些概念进行讲解。

利用书中的西瓜数据集3.0作为样本集 D ,连续属性 a 就是其中的密度。那么在数据集中密度一共有17(17也就是概念中的 n )中不同的取值,将这17个值从小到大进行排序为:

[0.243, 0.245, 0.343, 0.36, 0.403, 0.437, 0.481, 0.556, 0.593, 0.608, 0.634, 0.639, 0.657, 0.666, 0.697, 0.719, 0.774]

然后利用公式求出 Ta ,其中 Ta 每个元素为: ai+ai+12 ,也就是相邻两个数值的平均值,比如0.243和0.245的平均值为:0.244、0.245和0.343的平均值为:0.294。。。按照这种计算方法就可以将密度计算出16个候选值:

[0.244, 0.294, 0.351, 0.381, 0.420, 0.459, 0.518, 0.574, 0.600, 0.621, 0.636, 0.648, 0.661, 0.708, 0.746]

得到了候选值之后我们就把它们看作为离散值,可以利用这些候选值将数据集划分为“大于”和“小于”两个部分。比如说使用0.244可以将数据集划分为:“密度小于0.244”和“密度大于0.244”这两个部分,然后进行接下来的计算。


连续属性离散后的信息增益计算。

上述部分已经使用二分法的技术将连续属性离散化。接下来就需要计算出来离散值后的信息增益了。

将计算信息增益的公式改造为:

$Gain(D,a) = $

公式主要由两部分构成,其前半部分的: Ent(D) 还是当前集合的信息熵,这一点的计算过程与之前的离散属性计算一致。所以这里的值依然为: Ent(D)=0.998

后半部分的计算与之前的略有不同。后半段公式中使用到了上述得到的候选值,也就是上述的16个值,而且对于每个候选值我们用 λ 将集合分为两部分,一种是小于当前候选值的部分,另一种是大于当前候选值的部分。比如我们取第五个候选值0.420,然后将集合中密度值小于0.420的划分为一部分,将大于0.420的划分为另一部分。

所以小于0.420的密度值有:{0.243, 0.245, 0.343, 0.36, 0.403},这些密度值对应的样本编号为:{10, 11, 12, 15, 6},对应的标签为:{坏瓜,坏瓜, 坏瓜, 坏瓜, 好瓜 }。
所以这部分计算的 |Dλt||D| 的值为: 517
这部分计算的 Ent(Dλt) 为: Ent(Dλt)=(15×log215+45×log245)=0.72

大于0.420的密度值有:{0.437, 0.481, 0.556, 0.593, 0.608, 0.634, 0.639, 0.657, 0.666, 0.697, 0.719, 0.774},对应的标签为:{好瓜,好瓜, 好瓜, 坏瓜, 好瓜, 好瓜, 坏瓜, 坏瓜, 坏瓜, 好瓜, 坏瓜, 好瓜 }。
这部分计算的 |Dλt||D| 的值为: 1217
这部分计算的 Ent(Dλt) 为: Ent(Dλt)=(712×log2712+512×log2512)=0.980

所以在0.420这个划分点计算的信息增益为:
Ent(D)λ{,+}|Dλt||D|Ent(Dλt)=0.998(517×0.72+1217×0.980)=0.094

这样我们就通过计算得到了在候选划分点0.420的信息增益,同时在公式的前面还有一个 max(tTa) ,也就是说我们需要计算出所有候选划分点 Ta 的各个信息增益,然后取其中的最大最为该密度属性的信息增益。

使用代码很容易算出 Ta 中的所有信息增益,能够看到属性“密度”的信息增益为0.262,对应的划分点为:0.381:

这里写图片描述


创建决策树。

至此,我们已经够计算出连续值的信息增益了,那么我们就需要创建决策树了。创建的过程还是与之前的一样,首先计算出各特征属性的信息增益,然后取其中最大的那个作为划分点,所以过程这里就不再累述,但也有一些需要注意的地方。

首先,如果是连续属性的话,比如说“密度”,我们计算出信息增益之后,我们需要得到该信息增益对应的划分点,然后将集合划分为“小于。。。”和“大于。。。”两部分,所以在决策树该划分点的左右两部分分别就是这两个子集。

其次,与离散值不同,若当前划分属性为连续属性,该属性还可作为其后代节点的划分属性。比如在父节点上使用了“密度小于等于0.381”,不会禁止在子节点上使用“密度小于等于0.294”的。

最后创建的决策树为:

这里写图片描述


核心代码。

创建决策树的主要流程与之前的文章西瓜书中ID3决策树的实现。中介绍的一致。不过多了对连续值的处理过程。

在选择划分属性的时候,需要判断一下当前的属性是属于离散值还是连续值,如果是离散值则按照之前的流程走,如果是连续值的话就需要特殊处理了,可以使用如下代码计算连续值的信息增益:

def calcInfoGainForSeries(dataSet, i, baseEntropy):
    """
    计算连续值的信息增益
    :param dataSet:整个数据集
    :param i: 对应的特征值下标
    :param baseEntropy: 基础信息熵
    :return: 返回一个信息增益值,和当前的划分点
    """

    # 记录最大的信息增益
    maxInfoGain = 0.0

    # 最好的划分点
    bestMid = -1

    # 得到数据集中所有的当前特征值列表
    featList = [example[i] for example in dataSet]

    # 得到分类列表
    classList = [example[-1] for example in dataSet]

    dictList = dict(zip(featList, classList))

    # 将其从小到大排序,按照连续值的大小排列
    sortedFeatList = sorted(dictList.items(), key=operator.itemgetter(0))

    # 计算连续值有多少个
    numberForFeatList = len(sortedFeatList)

    # 计算划分点,保留三位小数
    midFeatList = [round((sortedFeatList[i][0] + sortedFeatList[i+1][0])/2.0, 3)for i in range(numberForFeatList - 1)]

    # 计算出各个划分点信息增益
    for mid in midFeatList:
        # 将连续值划分为不大于当前划分点和大于当前划分点两部分
        eltDataSet, gtDataSet = splitDataSetForSeries(dataSet, i, mid)

        # 计算两部分的特征值熵和权重的乘积之和
        newEntropy = len(eltDataSet)/len(sortedFeatList)*calcShannonEnt(eltDataSet) + len(gtDataSet)/len(sortedFeatList)*calcShannonEnt(gtDataSet)

        # 计算出信息增益
        infoGain = baseEntropy - newEntropy
        # print('当前划分值为:' + str(mid) + ',此时的信息增益为:' + str(infoGain))
        if infoGain > maxInfoGain:
            bestMid = mid
            maxInfoGain = infoGain

    return maxInfoGain, bestMid

创建决策树的时候也需要进行判断:

def createTree(dataSet, labels):
    """
    创建决策树
    :param dataSet: 数据集
    :param labels: 特征标签
    :return:
    """
    # 拿到所有数据集的分类标签
    classList = [example[-1] for example in dataSet]

    # 统计第一个标签出现的次数,与总标签个数比较,如果相等则说明当前列表中全部都是一种标签,此时停止划分
    if classList.count(classList[0]) == len(classList):
        return classList[0]

    # 计算第一行有多少个数据,如果只有一个的话说明所有的特征属性都遍历完了,剩下的一个就是类别标签
    if len(dataSet[0]) == 1:
        # 返回剩下标签中出现次数较多的那个
        return majorityCnt(classList)

    # 选择最好的划分特征,得到该特征的下标
    bestFeat = chooseBestFeatureToSplit(dataSet=dataSet, labels=labels)

    # 得到最好特征的名称
    bestFeatLabel = ''

    # 记录此刻是连续值还是离散值,1连续,2离散
    flagSeries = 0

    # 如果是连续值,记录连续值的划分点
    midSeries = 0.0

    # 如果是元组的话,说明此时是连续值
    if isinstance(bestFeat, tuple):
        # 重新修改分叉点信息
        bestFeatLabel = str(labels[bestFeat[0]]) + '小于' + str(bestFeat[1]) + '?'
        # 得到当前的划分点
        midSeries = bestFeat[1]
        # 得到下标值
        bestFeat = bestFeat[0]
        # 连续值标志
        flagSeries = 1
    else:
        # 得到分叉点信息
        bestFeatLabel = labels[bestFeat]
        # 离散值标志
        flagSeries = 0

    # 使用一个字典来存储树结构,分叉处为划分的特征名称
    myTree = {bestFeatLabel: {}}

    # 得到当前特征标签的所有可能值
    featValues = [example[bestFeat] for example in dataSet]

    # 连续值处理
    if flagSeries:
        # 将连续值划分为不大于当前划分点和大于当前划分点两部分
        eltDataSet, gtDataSet = splitDataSetForSeries(dataSet, bestFeat, midSeries)
        # 得到剩下的特征标签
        subLabels = labels[:]
        # 递归处理小于划分点的子树
        subTree = createTree(eltDataSet, subLabels)
        myTree[bestFeatLabel]['小于'] = subTree

        # 递归处理大于当前划分点的子树
        subTree = createTree(gtDataSet, subLabels)
        myTree[bestFeatLabel]['大于'] = subTree

        return myTree

    # 离散值处理
    else:
        # 将本次划分的特征值从列表中删除掉
        del (labels[bestFeat])
        # 唯一化,去掉重复的特征值
        uniqueVals = set(featValues)
        # 遍历所有的特征值
        for value in uniqueVals:
            # 得到剩下的特征标签
            subLabels = labels[:]
            # 递归调用,将数据集中该特征等于当前特征值的所有数据划分到当前节点下,递归调用时需要先将当前的特征去除掉
            subTree = createTree(splitDataSet(dataSet=dataSet, axis=bestFeat, value=value), subLabels)
            # 将子树归到分叉处下
            myTree[bestFeatLabel][value] = subTree
        return myTree

详细的代码请移至:具有连续属性的决策树实现

猜你喜欢

转载自blog.csdn.net/Leafage_M/article/details/80137305