决策树进阶(连续值处理和剪枝处理)

目录

1 连续值处理

1.1 处理方法

1.2 实例分析 

1.3 Python代码实现

 2 剪枝处理

2.1 为什么要剪枝

2.2 预剪枝

 2.2.1 基本思想

2.2.2 实例分析

2.2.3预剪枝的优缺点

2.3 后剪枝

2.3.1基本思想

2.3.2代码实现

2.3.3后剪枝的优缺点

1 连续值处理

1.1 处理方法

在上一篇博客(http://t.csdn.cn/DmFiz)是否能找到工作的例子中,使用 ID3决策树算法实现了基于离散属性的决策树构造 。在上述例子中,对绩点的处理是将其划分成了三个等级,实际上绩点是连续值。应该如何实现连续属性的离散化呢?

C4.5算法中策略是采用二分法将连续属性离散化处理 ,具体步骤如下:

第一步:将连续属性 a 在样本集 D 上出现 n 个不同的取值从小到大排列,记为a^1,a^2,...a^n。基于划分点t,可将D分为子集D_{t}^{+}D_{t}^{-},其中D_{t}^{-}包含那些在属性a上取值不大于t的样本,D_{t}^{+}包含那些在属性a上取值大于t的样本。考虑包含n-1个元素的候选划分点集合

                T_a =\left \{ \frac{a^i + a^{i+1}}{2} | 1\leq i\leq n-1 \right \}

即把区间[a^i,a^{i-1})的中位点作为候选划分点

第二步:采用离散属性值方法,计算这些划分点的增益,选取最优的划分点进行样本集合的划分:

对于每个划分点t,按如下公式计算其信息增益值,然后选择使信息增益值最大的划分点进行样本集合的划分

                

1.2 实例分析 

数据集如图所示时:

对属性平均成绩进行离散化处理:

首先对平均成绩从小到大排序:

{56,58,64,65,66,68,72,75,76,78,82,85,86,87,92,93,95} 

我们可以取任意相邻取值的中位点,作为划分点,例如:我们可以使用56和58的中位点进行划分,则

 T_a = \frac{56+58}{2} = 57

T_a = \left \{ 57,61,64.5,65.5,67,70,73.5,75.5,77,80,83.5,85.5,86.5,89.5,92.5,94 \right \}

信息熵 Ent(D) = \sum_{k=1}^{2}p_klog_2p_k = -\left ( \frac{7}{19} log_{2}\frac{7}{19} + \frac{12}{19} log_{2}\frac{12}{19}\right ) = 0.949

当t = 57时:

D_{t}^{-} = \left \{ 56 \right \}D_{t}^{+} =\left \{ 58,64,65,66,68,...87,92,93,95 \right \}

Ent(D_t^{+}) = -\left ( \frac{6}{18} log_{2}\frac{6}{18} + \frac{12}{18} log_{2}\frac{12}{18}\right ) = 0.918

Ent(D_t^{-}) = -(0log_20+1log_21) = 0

Gain(D,a,57) = 0.949-(\frac{1}{19}*0 +\frac{18}{19} * 0.918)=0.079

按上述方法依次计算t=58,64,...时的信息增益率,当t = 67时,Gain(D,a,t)最大,因此选择该划分点

再由前面介绍的方法得到其他属性的信息增益值,选择信息增益值最大的属性为根结点划分属性。

需要注意的是:与离散属性不同,若当前结点划分属性为连续属性,该属性还可作为其后代结点的划分属性

1.3 Python代码实现

1.划分数据集

# 划分数据集, axis:按第几个特征划分, value:划分特征的值, LorR: value值左侧(小于)或右侧(大于)的数据集
def splitDataSet_c(dataSet, axis, value, LorR='L'):
    retDataSet = []
    featVec = []
    if LorR == 'L':
        for featVec in dataSet:
            if float(featVec[axis]) < value:
                retDataSet.append(featVec)
    else:
        for featVec in dataSet:
            if float(featVec[axis]) > value:
                retDataSet.append(featVec)
    return retDataSet

2.选择最好的数据集划分方式

# 选择最好的数据集划分方式
def chooseBestFeatureToSplit_c(dataSet, labelProperty):
    numFeatures = len(labelProperty)  # 特征数
    baseEntropy = calcShannonEnt(dataSet)  # 计算根节点的信息熵
    bestInfoGain = 0.0
    bestFeature = -1
    bestPartValue = None  # 连续的特征值,最佳划分值
    for i in range(numFeatures):  # 对每个特征循环
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList)  # 该特征包含的所有值
        newEntropy = 0.0
        bestPartValuei = None
        if labelProperty[i] == 0:  # 对离散的特征
            for value in uniqueVals:  # 对每个特征值,划分数据集, 计算各子集的信息熵
                subDataSet = splitDataSet(dataSet, i, value)
                prob = len(subDataSet) / float(len(dataSet))
                newEntropy += prob * calcShannonEnt(subDataSet)
        else:  # 对连续的特征
            sortedUniqueVals = list(uniqueVals)  # 对特征值排序
            sortedUniqueVals.sort()
            listPartition = []
            minEntropy = inf
            for j in range(len(sortedUniqueVals) - 1):  # 计算划分点
                partValue = (float(sortedUniqueVals[j]) + float(
                    sortedUniqueVals[j + 1])) / 2
                
                # 对每个划分点,计算信息熵
                dataSetLeft = splitDataSet_c(dataSet, i, partValue, 'L')
                
                dataSetRight = splitDataSet_c(dataSet, i, partValue, 'R')
                probLeft = len(dataSetLeft) / float(len(dataSet))
                probRight = len(dataSetRight) / float(len(dataSet))
                Entropy = probLeft * calcShannonEnt(
                    dataSetLeft) + probRight * calcShannonEnt(dataSetRight)
                # print(Entropy)
                if Entropy < minEntropy:  # 取最小的信息熵
                    minEntropy = Entropy
                    bestPartValuei = partValue
            newEntropy = minEntropy
        infoGain = baseEntropy - newEntropy  # 计算信息增益
        if infoGain > bestInfoGain:  # 取最大的信息增益对应的特征
            bestInfoGain = infoGain
            bestFeature = i
            bestPartValue = bestPartValuei
    return bestFeature, bestPartValue

3.创建树

# 创建树
# dataSet:样本集 
# labels:特征 
# labelProperty:特征属性(0 离散, 1 连续)
def createTree_c(dataSet, labels, labelProperty):
    # print dataSet, labels, labelProperty
    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, bestPartValue = chooseBestFeatureToSplit_c(dataSet,
                                                        labelProperty)  # 最优分类特征的索引
    if bestFeat == -1:  # 如果无法选出最优分类特征,返回出现次数最多的类别
        return majorityCnt(classList)
    if labelProperty[bestFeat] == 0:  # 对离散的特征
        bestFeatLabel = labels[bestFeat]
        myTree = {bestFeatLabel: {}}
        labelsNew = copy.copy(labels)
        labelPropertyNew = copy.copy(labelProperty)
        del (labelsNew[bestFeat])  # 已经选择的特征不再参与分类
        del (labelPropertyNew[bestFeat])
        featValues = [example[bestFeat] for example in dataSet]
        uniqueValue = set(featValues)  # 该特征包含的所有值
        for value in uniqueValue:  # 对每个特征值,递归构建树
            subLabels = labelsNew[:]
            subLabelProperty = labelPropertyNew[:]
            myTree[bestFeatLabel][value] = createTree_c(
                splitDataSet(dataSet, bestFeat, value), subLabels,
                subLabelProperty)
    else:  # 对连续的特征,不删除该特征,分别构建左子树和右子树
        bestFeatLabel = labels[bestFeat] + '<' + str(bestPartValue)
        myTree = {bestFeatLabel: {}}
        subLabels = labels[:]
        subLabelProperty = labelProperty[:]
        # 构建左子树
        valueLeft = '是'
        myTree[bestFeatLabel][valueLeft] = createTree_c(
            splitDataSet_c(dataSet, bestFeat, bestPartValue, 'L'), subLabels,
            subLabelProperty)
        # 构建右子树
        valueRight = '否'
        myTree[bestFeatLabel][valueRight] = createTree_c(
            splitDataSet_c(dataSet, bestFeat, bestPartValue, 'R'), subLabels,
            subLabelProperty)
    return myTree

4. 读取数据集,打印创建的树

data = pd.read_csv("D:/syy/MachineLearning/data/data_td.csv")
dataSet = data.values.tolist()

labels = ['平均绩点', '竞赛等级', '实习经历']  

labelProperties = [1, 0, 0]  # 属性的类型,0表示离散,1表示连续
Trees = createTree_c(dataSet, labels, labelProperties)

print(Trees)

 5.决策树的可视化

 2 剪枝处理

2.1 为什么要剪枝

  • “剪枝”是决策树学习算法对付“过拟合”的主要手段
  • 可通过“剪枝”来一定程度避免因决策分支过多,以致于把训练集自身的一些特点当做所有数据都具有的一般性质而导致的过拟合

剪枝的基本策略:

  • 预剪枝
  • 后剪枝

判断决策树泛化性能是否提升的方法:

        留出法:预留一部分数据用作“验证集”以进行性能评估

2.2 预剪枝

 2.2.1 基本思想

决策树生成过程中,对每个结点在划分前先进行估计,若当前结点的划分不能带来决策树泛化性能提升,则停止划分并将当前结点记为叶结点,其类别标记为该节点对应训练样例数最多的类别。

通过提前停止树的构建而对树剪枝,主要方法有:

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

上述不足:阈值属于超参数,很难找到过拟合--欠拟合的平衡

2.2.2 实例分析

如图所示的数据集(黄色部分为测试集,其他为验证集)

 C4.5算法生成的未剪枝决策树如图所示:

以work_experience结点为例说明预剪枝的过程:

  • 对结点work_experience若不划分,则将其标记为叶结点,类别标记为训练样例中最多的类别,即找到工作,此时验证集中编号20、22、23、25分类正确,则验证集的精度为4/8=50%
  • 若划分,根据训练样例,work_experience=1时,训练集中有10组数据的标签为‘yes’,1组数据标签为‘no’,因此work_experience=1被标记为‘yes’;work_experience=0时,训练集中4组数据的标签为‘no’,3组为‘yes’,因此work_experience=1被标记为‘no’。此时,验证集中编号为26、27分类正确,验证集的精度为2/8 =25%
  • 验证集的精度下降,则预剪枝决策为不划分

从上述例子可以看出预剪枝有欠拟合的风险。

2.2.3预剪枝的优缺点

  • 优点:降低过拟合风险,显著减少训练时间和测试时间开销。
  • 缺点:欠拟合风险:有些分支的当前划分虽然不能提升泛化性能,但在其基础上进行的后续划分却有可能显著提高性能。预剪枝基于“贪心”本质禁止这些分支展开,带来了欠拟合风险。

2.3 后剪枝

2.3.1基本思想

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

2.3.2代码实现

def postPruningTree(inputTree, dataSet, data_test, labels, labelProperties):
    """ 
    type: (dict, list, list, list, list) -> dict
    inputTree: 已构造的树
    dataSet: 训练集
    data_test: 验证集
    labels: 属性标签
    labelProperties: 属性类别
    """
    firstStr = list(inputTree.keys())[0]
    secondDict = inputTree[firstStr]
    classList = [example[-1] for example in dataSet]
    featkey = copy.deepcopy(firstStr)
    if '<' in firstStr:  # 对连续的特征值,使用正则表达式获得特征标签和value
        featkey = re.compile("(.+<)").search(firstStr).group()[:-1]
        featvalue = float(re.compile("(<.+)").search(firstStr).group()[1:])
    labelIndex = labels.index(featkey)
    temp_labels = copy.deepcopy(labels)
    temp_labelProperties = copy.deepcopy(labelProperties)
    if labelProperties[labelIndex] == 0:  # 离散特征
        del (labels[labelIndex])
        del (labelProperties[labelIndex])
    for key in secondDict.keys():  # 对每个分支
        if type(secondDict[key]).__name__ == 'dict':  # 如果不是叶子节点
            if temp_labelProperties[labelIndex] == 0:  # 离散的
                subDataSet = splitDataSet_c(dataSet, labelIndex, key)
                subDataTest = splitDataSet_c(data_test, labelIndex, key)
            else:
                if key == 'Y':
                    subDataSet = splitDataSet_c(dataSet, labelIndex, featvalue,
                                               'L')
                    subDataTest = splitDataSet_c(data_test, labelIndex,
                                                featvalue, 'L')
                else:
                    subDataSet = splitDataSet_c(dataSet, labelIndex, featvalue,
                                               'R')
                    subDataTest = splitDataSet_c(data_test, labelIndex,
                                                featvalue, 'R')
            if len(subDataTest) > 0:
                inputTree[firstStr][key] = postPruningTree(secondDict[key],
                                                       subDataSet, subDataTest,
                                                       copy.deepcopy(labels),
                                                       copy.deepcopy(
                                                           labelProperties))
 
    if testing(inputTree, data_test, temp_labels,
               temp_labelProperties) <= testingMajor(majorityCnt(classList),
                                                     data_test):
        return inputTree
    return majorityCnt(classList)
data = pd.read_csv("D:/syy/MachineLearning/data/data_td.csv")
dataSet = data.values.tolist()

labels = ['grade', 'match', 'work_experience']  

labelProperties = [1, 0, 0]  # 属性的类型,0表示离散,1表示连续

Trees = createTree_c(dataSet, labels, labelProperties)
data_test = [[70, 1, 0, 'yes'],[72, 1, 0, 'yes']]

classSet = ["yes","no"]
# Trees = createTree_c(dataSet, labels, labelProperties)

# 构建决策树
trees = createTree_c(dataSet, labels, labelProperties)
# 绘制决策树
createPlot(trees)
# 利用验证集对决策树剪枝
postPruningTree(trees, dataSet, data_test, labels, labelProperties)

print(trees)
# 绘制剪枝后的决策树
createPlot(trees)

训练集如图所示:

 为了观察剪枝效果,这里的验证集为我设置的两组数据。实际应将数据划分为训练集和验证集。

剪枝前的决策树为:

剪枝后的树为:

 

2.3.3后剪枝的优缺点

  • 优点:后剪枝比预剪枝保留了更多的分支,欠拟合风险小,泛化性能往往优于预剪枝决策树
  • 缺点:训练时间开销大:后剪枝过程是在生成完全决策树之后进行的,需要自底向上对所有非叶结点逐一计算

完整代码:链接:https://pan.baidu.com/s/1sCKpoVKIRgpsmv3vaUHgrg 
提取码:mogz

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值