机器学习之优化决策树

前言:

        之前已有一篇创建决策树的文章,本文是在此基础之上优化,建议不了解的看官先熟悉一下如何生成一颗决策树。上一篇文章地址:

                机器学习之构造决策树_俺从头开始的博客-CSDN博客

信息营地:

连续值处理:

        上一节中由于圆周长和重量两个数据属性属于连续性变量,致使最后的生成树没有用到它们。今天的任务之一就是解决连续型变量怎么处理的问题。

        对于连续特征,不能再根据连续属性的可取值对节点进行划分,而是应该使用取值范围进行划分。最简单的策略是二分法,即先将所有样本的取值排序,然后去两个样本取值的中点作为一个切分点,如有n个样本,则共计生成n-1个切分点。任选一个切分点,将样本按照小于切分点值和大于切分点值分为两部分去,计算划分后的信息增益,选取让信息增益最大的划分点作为最优切分点。

直接上代码操作:

# 选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet, n):   # n是连续性特征数,且在数据集中应排在前列
    numFeatures = len(dataSet[0]) - 1  # 特征数
    baseEntropy = calcShannonEnt(dataSet)  # 数据集整体信息熵
    bestInfoGain = 0.0; bestFeature = -1
    for i in range(numFeatures):
        if i < n:
            conVal = True
        else:
            featList = [example[i] for example in dataSet]
            uniqueVals = set(featList)  # 创建唯一的分类标签列表
            newEntropy = 0.0
            for value in uniqueVals:
                subDataSet = splitDataSet(dataSet, i, value)
                prob = len(subDataSet)/float(len(dataSet))
                newEntropy += prob * calcShannonEnt(subDataSet)
            infoGain = baseEntropy - newEntropy  # 计算每种方式的信息熵
            if(infoGain > bestInfoGain):
                bestInfoGain = infoGain
                bestFeature = i  # 计算最好的信息增益
    if conVal:
        return cal_information_gain_continuous(dataSet, i)
    else:
        return bestFeature
# 对于连续特征b,计算给定数据属性b的信息增益
def cal_information_gain_continuous(data, a):
    n = len(data)  # 共有n条数据,会产生n-1个划分点,选择信息增益最大的作为最优划分点
    temp = []
    for line in data:
        temp.append(line[a])
    Ent = calcShannonEnt(data)  # 原始数据集的信息熵Ent(D)
    select_points = []
    data_left = []
    data_right = []
    for i in range(n-1):
        val = int(float(temp[i]) + float(temp[i+1])) / 2  # 两个值中间取值为划分点
        if float(temp[i]) < val:
            data_left.append(data[i])
        else:
            data_right.append(data[i])
        ent_left = calcShannonEnt(data_left)
        ent_right = calcShannonEnt(data_right)
        result = Ent - len(data_left)/n * ent_left - len(data_right)/n * ent_right
        select_points.append([val, result])
        divpoint = i
    select_points.sort(key=lambda x: x[1], reverse=True)  # 按照信息增益排序
    return select_points[0][0], divpoint  # 返回信息增益最大的点, 以及对应的下标
# 创建树
def createTree(dataSet, labels):
    classList = [example[-1] for example in dataSet]
    if classList.count(classList[0]) == len(classList):  # classList只剩下一种值
        return classList[0]
    if len(dataSet[0]) == 1:  # dataSet中属性使用完毕,剩下一个类别标签
        return majorityCnt(classList)  # 取数量最多作为分类
    bestFeat = chooseBestFeatureToSplit(dataSet, 2)
    if isinstance(bestFeat, tuple):
        bestFeatLabel = str(labels[bestFeat[0]]) + '小于' + str(bestFeat[1])  # 修改分叉点信息
        midSeries = bestFeat[1]  # 得到当前的划分点
        bestFeat = bestFeat[0]  # 得到下标值
    else:
        bestFeatLabel = labels[bestFeat]  # 得到分叉点信息
    myTree = {bestFeatLabel: {}}
    del(labels[bestFeat])
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)  # 去掉重复特征值
    for value in uniqueVals:
        subLabels = labels[:]  # 剩余属性列表
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
    return myTree

只是对之前的部分代码做出了修改,现在可以将数据集前两列的连续值进行处理。

结果如下:

结果上看代码的效果仍旧不太好,由于数据过多但个属性类别鲜明使现在的树属性单一却复杂,接着继续解决这个问题。

剪枝:

       创建决策树后会出现某些数据单独成一类的情况。这样执行分类时,很容易造成“过拟合”现象。即本身就不存在该类,但因为数据采集错误或标注错误等会使一个数据单独成类,这种情况下,有类似的错误样本进来时一定会分类成功;相反,真实的数据反而可能找不到属于自己的类别。

        为了解决这类“过拟合”问题,有专家学者提出了剪枝策略。那怎么减呢?举个栗子:

        剪枝指的是剪去根部节点,换个词应该叫替换。因为你不能凭空的剪去某一部分。比如上面A4的左孩子类A,直接剪去会发生什么呢?如果分类为A4的Yes情况,机器将不知道该给出什么样子的分类结果。所以剪枝只能是直接将A4根节点替换为类A或是类B的一种。

        那怎样验证一下剪枝之后的效果呢?或者说,你如何保证剪枝操作是有必要的。我们选择直接用数据说话,保留出一组验证集来计算剪枝前后的增益效果。效果好证明当前节点有必要进行剪枝。

预剪枝:

        剪枝策略一般可以分为预剪枝和后剪枝两种,先看预剪枝。预剪枝是发生在树的创建过程中,创建树的同时以某种衡量指标确定是否要建立当前分支。

        一般方法有下面几种:

1.当决策树达到预设的高度时就停止决策树的生长。
2.达到某个节点的实例具有相同的特征向量,即使这些实例不属于同一类,也可以停止决策树的生长。
3.定义一个阈值,当达到某个节点的实例个数小于阈值时就可以停止决策树的生长。
4.通过计算每次扩张对系统性能的增益,决定是否停止决策树的生长。
上述不足:阈值属于超参数,很难找到过拟合--欠拟合的tradeoff。

        优缺点:

优点一是降低过拟合风险。二是显著减少训练时间和测试时间开销。

缺点主要是欠拟合风险:有些分支的当前划分虽然不能提升泛化性能,但在其基础上进行的后续划分却有可能显著提高性能。预剪枝基于“贪心”本质禁止这些分支展开,带来了欠拟合风险。

代码展示:

# 创建预剪枝决策树
def createTreePrePruning(dataTrain, labelTrain, dataTest, labelTest, names, method='id3'):、
    trainData = np.asarray(dataTrain)
    labelTrain = np.asarray(labelTrain)
    testData = np.asarray(dataTest)
    labelTest = np.asarray(labelTest)
    names = np.asarray(names)
    # 如果结果为单一结果
    if len(set(labelTrain)) == 1:
        return labelTrain[0]
        # 如果没有待分类特征
    elif trainData.size == 0:
        return voteLabel(labelTrain)
    # 其他情况则选取特征
    bestFeat, bestEnt = bestFeature(dataTrain, labelTrain, method=method)
    # 取特征名称
    bestFeatName = names[bestFeat]
    # 从特征名称列表删除已取得特征名称
    names = np.delete(names, [bestFeat])
    # 根据最优特征进行分割
    dataTrainSet, labelTrainSet = splitFeatureData(dataTrain, labelTrain, bestFeat)

    # 预剪枝评估
    # 划分前的分类标签
    labelTrainLabelPre = voteLabel(labelTrain)
    labelTrainRatioPre = equalNums(labelTrain, labelTrainLabelPre) / labelTrain.size
    # 划分后的精度计算
    if dataTest is not None:
        dataTestSet, labelTestSet = splitFeatureData(dataTest, labelTest, bestFeat)
        # 划分前的测试标签正确比例
        labelTestRatioPre = equalNums(labelTest, labelTrainLabelPre) / labelTest.size
        # 划分后 每个特征值的分类标签正确的数量
        labelTrainEqNumPost = 0
        for val in labelTrainSet.keys():
            labelTrainEqNumPost += equalNums(labelTestSet.get(val), voteLabel(labelTrainSet.get(val))) + 0.0
        # 划分后 正确的比例
        labelTestRatioPost = labelTrainEqNumPost / labelTest.size

        # 如果没有评估数据 但划分前的精度等于最小值0.5 则继续划分
    if dataTest is None and labelTrainRatioPre == 0.5:
        decisionTree = {bestFeatName: {}}
        for featValue in dataTrainSet.keys():
            decisionTree[bestFeatName][featValue] = createTreePrePruning(dataTrainSet.get(featValue),
                                                                         labelTrainSet.get(featValue)
                                                                         , None, None, names, method)
    elif dataTest is None:
        return labelTrainLabelPre
        # 如果划分后的精度相比划分前的精度下降, 则直接作为叶子节点返回
    elif labelTestRatioPost < labelTestRatioPre:
        return labelTrainLabelPre
    else:
        # 根据选取的特征名称创建树节点
        decisionTree = {bestFeatName: {}}
        # 对最优特征的每个特征值所分的数据子集进行计算
        for featValue in dataTrainSet.keys():
            decisionTree[bestFeatName][featValue] = createTreePrePruning(dataTrainSet.get(featValue),
                                                                         labelTrainSet.get(featValue)
                                                                         , dataTestSet.get(featValue),
                                                                         labelTestSet.get(featValue)
                                                                         , names, method)
    return decisionTree

后剪枝:

        先从训练集生成一棵完整的决策树,然后自底向上地对非叶结点进行分析计算,若将该结点对应的子树替换为叶结点能带来决策树泛化性能提升,则将该子树替换为叶结点。

图例:

        与预剪枝的区别在于:预剪枝策略是在创建树的时候剪枝,或者说创建树的时候不合适点就不去建;后剪枝是按照正常的步骤建造一棵树,然后再去评估剪枝。

        优缺点:

优点是后剪枝比预剪枝保留了更多的分支,欠拟合风险小,泛化性能往往优于预剪枝决策树。

缺点是训练时间开销大:后剪枝过程是在生成完全决策树之后进行的,需要自底向上对所有非叶结点逐一计算。

代码如下:

def treePostPruning(labeledTree, dataTest, labelTest, names):
    newTree = labeledTree.copy()
    dataTest = np.asarray(dataTest)
    labelTest = np.asarray(labelTest)
    names = np.asarray(names)
    # 取决策节点的名称 即特征的名称
    featName = list(labeledTree.keys())[0]
    # print("\n当前节点:" + featName)
    # 取特征的列
    featCol = np.argwhere(names == featName)[0][0]
    names = np.delete(names, [featCol])
    newTree[featName] = labeledTree[featName].copy()
    featValueDict = newTree[featName]
    featPreLabel = featValueDict.pop("_vpdl")
    # print("当前节点预划分标签:" + featPreLabel)
    # 是否为子树的标记
    subTreeFlag = 0
    # 分割测试数据 如果有数据 则进行测试或递归调用  np的array我不知道怎么判断是否None, 用is None是错的
    dataFlag = 1 if sum(dataTest.shape) > 0 else 0
    if dataFlag == 1:
        # print("当前节点有划分数据!")
        dataTestSet, labelTestSet = splitFeatureData(dataTest, labelTest, featCol)
    for featValue in featValueDict.keys():
        # print("当前节点属性 {0} 的子节点:{1}".format(featValue ,str(featValueDict[featValue])))
        if dataFlag == 1 and type(featValueDict[featValue]) == dict:
            subTreeFlag = 1
            # 如果是子树则递归
            newTree[featName][featValue] = treePostPruning(featValueDict[featValue], dataTestSet.get(featValue),
                                                           labelTestSet.get(featValue), names)
            # 如果递归后为叶子 则后续进行评估
            if type(featValueDict[featValue]) != dict:
                subTreeFlag = 0
        if dataFlag == 0 and type(featValueDict[featValue]) == dict:
            subTreeFlag = 1
            newTree[featName][featValue] = convertTree(featValueDict[featValue])
    if subTreeFlag == 0:
        ratioPreDivision = equalNums(labelTest, featPreLabel) / labelTest.size
        equalNum = 0
        for val in labelTestSet.keys():
            equalNum += equalNums(labelTestSet[val], featValueDict[val])
        ratioAfterDivision = equalNum / labelTest.size
        if ratioAfterDivision < ratioPreDivision:
            newTree = featPreLabel
    return newTree
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
数据挖掘是指从数据库中抽取隐含的、具有潜在使用价值信息的 过程,是一种新型的数据分析技术,已被广泛应用于金融、保险、政 府、教育、运输以及国防等领域。 数据分类是数据挖掘中一个重要的内容。分类存在很多方法,其 中决策树算法是以实例为基础的归纳学习算法,以其易于提取显式规 则、计算量相对较小、可以显示重要的决策属性和较高的分类准确率 等优点而得到广泛的应用。据统计,目前决策树算法是利用最广泛的 数据挖掘算法之一。 然而在实际应用过程中,现存的决策树算法也存在着很多不足之 处,如计算效率低下、多值偏向等。因此,进一步改进决策树,提高 决策树的性能,使其更加适合数据挖掘技术的应用要求具有重要的理 论和实际意义。 本文针对上述数据库知识发现的不足,进行深入的研究,探索数 据挖掘中决策树分类的组合优化算法,以便更好地提高分类的准确 性。应用于实际工作中,主要研究工作如下: 首先,从宏观上介绍了数据挖掘和分类技术的理论基础,并重点 对决策树算法进行了分析和比较。 然后,提出了一种新的适合于高维数据库的组合优化决策树算 法。相比于传统的分类算法,该算法从降维、属性选择、可扩展性和 剪枝等方面进行了改进。其中最主要是提出基于加权属性协调度并结 合简化预剪枝策略的决策树算法一DTBAC算法,以及加强算法可扩 展性的FAVC集。 最后,着重介绍了所研发的组合优化决策树分类器系统。它以 DTBAC算法为核心算法生成分类器,并应用到医学领域对病人进行 分类。通过对比分析发现,DTBAC算法在总体性能上要优于目前被 广泛采用的ID3算法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

俺从头开始

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值