机器学习——决策树2

决策树的预剪枝、后剪枝以及连续数据的离散化

    • 一、剪枝处理
      • 1、剪枝原因
      • 2、剪枝的基本策略
        • 1、预剪枝
        • 2、后剪枝
    • 二、连续值处理
      • 1、原因
      • 2、离散化方法(二分法)
      • 3、例子
    • 三、代码实现
      • 1、预剪枝
        • (1)预剪枝代码实现
        • (2)预剪枝测试结果
      • 2、后剪枝
        • (1)后剪枝代码
        • (2)后剪枝测试结果
      • 3、连续值处理

一、剪枝处理

1、剪枝原因

  • 剪枝是决策树学习算法解决“过拟合”问题的主要手段。
  • 为什么会产生“过拟合”问题:在决策树的学习过程中,为了尽可能正确地分类训练样本,结点划分过程不断重复,有时会造成决策树分支过多,于是可能将训练样本学得太好,把训练集数据中一些特点当作所有数据都具有的一般特点了(以偏概全),导致测试集预测效果不好,这就是过拟合现象。
  • “过拟合”会让决策树模型学习到并不具备普遍意义的分类决策条件,从而导致模型的分类效率、泛化能力降低。

2、剪枝的基本策略

剪枝后的效果如何判断:将数据集一部分作为训练集,一部分作为验证集来检验剪枝后的效果。

1、预剪枝

  • 概念: 顾名思义,就是在树长好之前就预先剪掉分支,将即将长出的分支扼杀,不进行该分支划分。
  • 主要方法:

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

  • 三大准则
    (1)树的高度是否超过阈值(10-15最合适)。
    (2)结点的样本量是否小于阈值(太少容易过拟合)。
    (3)划分前后验证集的精度是否提高。

  • 预剪枝过程
    生成决策树的过程中,在每个结点划分之前先进行计算,比较划分该结点前后决策树泛化性能是否提升,如果提升则划分该结点,如果不能提升则停止划分并将当前结点记为叶结点,其类别标记为该结点对应训练样例数最多的类别(好瓜or坏瓜)。

机器学习——决策树2_第1张图片

例子:
机器学习——决策树2_第2张图片

计算过程:(根蒂)
(1)不划分:将其标记为叶结点,类别标识为训练样例中最多的类别,即好瓜。则验证集中{4,5,8}被分类正确,得到验证集精度为
3/7100%=42.9%
(2)划分:如果划分,如图所示将三个结点进行标记。此时,验证集中编号为{4,5,8,11}的样例被划分正确,验证集精度为4/7
100%=57.1%
机器学习——决策树2_第3张图片
(3)进行判断:划分前=42.9%<划分后=57.1%,所以预剪枝决策为:划分。

  • 优缺点

  • 优点

在这里插入图片描述

  • 缺点
    因为预剪枝和贪心算法相似,都是注重局部最优,所以并不能保证在剪枝掉的分支的基础上进行的后续划分对性能的提高没有帮助,而是直接在最开始就遏制分支的展开,带来了欠拟合风险

2、后剪枝

  • 概念: 与预剪枝对应,后剪枝是在决策树完全生成之后才进行的剪枝操作。
  • 后剪枝过程

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

例子

机器学习——决策树2_第4张图片

  • 优缺点

机器学习——决策树2_第5张图片
· 从精度上来说,后剪枝会比预剪枝好

二、连续值处理

1、原因

  • 连续属性取值数目非有限,不能像处理离散属性取值一样对结点直接进行划分,所以需要将连续属性离散化。

  • 与离散属性不同,若当前结点划分属性为连续属性,该属性还可作为其后代结点的划分属性

2、离散化方法(二分法)

常用的离散化方法是二分法

机器学习——决策树2_第6张图片
机器学习——决策树2_第7张图片
简单来说:
(1)将样本集中连续属性的取值从小到大排序
(2)找到候选划分点(一般是中位点),将取值分为小于等于划分点的和大于划分点的
(3)计算候选划分点的增益,选择增益最大的作为最终的划分点。

3、例子

机器学习——决策树2_第8张图片机器学习——决策树2_第9张图片
(1)先对属性“密度”的取值进行排序(从小到大)
{0.243,0.245,0.343,0.360,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}
(2)计算候选划分点(16个:每两个取值之间有一个划分点,共17个取值,所以会有16个划分点)
(3)一个个计算候选划分点的增益进行比较

三、代码实现

1、预剪枝

(1)预剪枝代码实现

# 创建预剪枝决策树
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

# 将数据集分割为测试集和训练集
xgDataTrain, xgLabelTrain, xgDataTest, xgLabelTest = splitXgData20(xgData, xgLabel)
# 生成不剪枝的树
xgTreeTrain = createTree(xgDataTrain, xgLabelTrain, xgName, method = 'id3')
# 生成预剪枝的树
xgTreePrePruning = createTreePrePruning(xgDataTrain, xgLabelTrain, xgDataTest, xgLabelTest, xgName, method = 'id3')
# 画剪枝前的树
print("剪枝前的树")
createPlot(xgTreeTrain)
# 画剪枝后的树
print("剪枝后的树")
createPlot(xgTreePrePruning)

(2)预剪枝测试结果

  • 预剪枝前:
    机器学习——决策树2_第10张图片

  • 预剪枝后:
    机器学习——决策树2_第11张图片

2、后剪枝

(1)后剪枝代码

# 创建决策树 带预划分标签
def createTreeWithLabel(data, labels, names, method='id3'):
    data = np.asarray(data)
    labels = np.asarray(labels)
    names = np.asarray(names)
    # 如果不划分的标签为
    votedLabel = voteLabel(labels)
    # 如果结果为单一结果
    if len(set(labels)) == 1:
        return votedLabel
        # 如果没有待分类特征
    elif data.size == 0:
        return votedLabel
    # 其他情况则选取特征
    bestFeat, bestEnt = bestFeature(data, labels, method=method)
    # 取特征名称
    bestFeatName = names[bestFeat]
    # 从特征名称列表删除已取得特征名称
    names = np.delete(names, [bestFeat])
    # 根据选取的特征名称创建树节点 划分前的标签votedPreDivisionLabel=_vpdl
    decisionTree = {bestFeatName: {"_vpdl": votedLabel}}
    # 根据最优特征进行分割
    dataSet, labelSet = splitFeatureData(data, labels, bestFeat)
    # 对最优特征的每个特征值所分的数据子集进行计算
    for featValue in dataSet.keys():
        decisionTree[bestFeatName][featValue] = createTreeWithLabel(dataSet.get(featValue), labelSet.get(featValue),
                                                                    names, method)
    return decisionTree


# 将带预划分标签的tree转化为常规的tree
def convertTree(labeledTree):
    labeledTreeNew = labeledTree.copy()
    nodeName = list(labeledTree.keys())[0]
    labeledTreeNew[nodeName] = labeledTree[nodeName].copy()
    for val in list(labeledTree[nodeName].keys()):
        if val == "_vpdl":
            labeledTreeNew[nodeName].pop(val)
        elif type(labeledTree[nodeName][val]) == dict:
            labeledTreeNew[nodeName][val] = convertTree(labeledTree[nodeName][val])
    return labeledTreeNew


# 后剪枝 训练完成后决策节点进行替换评估  这里可以直接对xgTreeTrain进行操作
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])
    # print("当前节点划分的数据维度:" + str(names))
    # print("当前节点划分的数据:" )
    # print(dataTest)
    # print(labelTest)
    # 该特征下所有值的字典
    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
            # print("当前节点无划分数据!直接转换树:"+str(featValueDict[featValue]))
            newTree[featName][featValue] = convertTree(featValueDict[featValue])
            # print("转换结果:" + str(convertTree(featValueDict[featValue])))
    # 如果全为叶子节点, 评估需要划分前的标签,这里思考两种方法,
    #     一是,不改变原来的训练函数,评估时使用训练数据对划分前的节点标签重新打标
    #     二是,改进训练函数,在训练的同时为每个节点增加划分前的标签,这样可以保证评估时只使用测试数据,避免再次使用大量的训练数据
    #     这里考虑第二种方法 写新的函数 createTreeWithLabel,当然也可以修改createTree来添加参数实现
    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
        # print("当前节点预划分标签的准确率:" + str(ratioPreDivision))
        # print("当前节点划分后的准确率:" + str(ratioAfterDivision))
        # 如果划分后的测试数据准确率低于划分前的,则划分无效,进行剪枝,即使节点等于预划分标签
        # 注意这里取的是小于,如果有需要 也可以取 小于等于
        if ratioAfterDivision < ratioPreDivision:
            newTree = featPreLabel
    return newTree


xgTreeBeforePostPruning = {"脐部": {"_vpdl": "是"
                                   , '凹陷': {'色泽':{"_vpdl": "是", '青绿': '是', '乌黑': '是', '浅白': '否'}}
                                   , '稍凹': {'根蒂':{"_vpdl": "是"
                                                  , '稍蜷': {'色泽': {"_vpdl": "是"
                                                                  , '青绿': '是'
                                                                  , '乌黑': {'纹理': {"_vpdl": "是"
                                                                               , '稍糊': '是', '清晰': '否', '模糊': '是'}}
                                                                  , '浅白': '是'}}
                                                  , '蜷缩': '否'
                                                  , '硬挺': '是'}}
                                   , '平坦': '否'}}
xgTreePostPruning = treePostPruning(xgTreeBeforePostPruning, xgDataTest, xgLabelTest, xgName)
createPlot(convertTree(xgTreeBeforePostPruning))
createPlot(xgTreePostPruning)

(2)后剪枝测试结果

  • 后剪枝前:
    机器学习——决策树2_第12张图片

  • 后剪枝后:
    机器学习——决策树2_第13张图片

3、连续值处理

`

你可能感兴趣的:(决策树,算法)