决策树——ID3、C4.5、CART、剪枝

本文详细介绍了决策树的学习过程,包括ID3、C4.5算法的生成和剪枝策略。通过对特征选择的探讨,如信息增益与信息增益比,以及CART算法中的回归树和分类树,阐述了决策树的构建和优化。同时,讨论了决策树剪枝的CCP方法,旨在提高模型的泛化能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

决策树(decision tree)是一种基本的分类与回归方法,属于判别模型。《统计学习方法》重点讨论分类决策树。
优点:计算复杂度不高,对于中间值的缺失不敏感,可以处理不相关特征数据(特征选择的时候发现计算的数值不高会pass掉)。
缺点:可能过度匹配问题(咱们需要去剪枝泛化)。

一.模型

分类决策树模型是一种描述对实例进行分类的树形结构。决策树由结点和有向边组成。结点类型包括:内部结点和叶结点。内部结点表示一个特征或属性,叶结点表示一个类。每一个实例的特征都只会与一条路径上的特征一致。
决策树表示给定特征条件下类的条件概率分布(图c),决策树在给实例分类时,强制划分到条件概率大的那一个类。
这一条件概率分布定义在特征空间的一个划分上(如图a和图b)。决策树的一条路径对应于划分中的一个单元(图a和图c)。
在这里插入图片描述
在这里插入图片描述

二.策略

我们需要的是一个与训练数据矛盾较小的决策树,同时具有很好的泛化能力。从另一个角度看,决策树的学习是由训练数据集估计条件概率模型,我们选择的条件概率模型是希望不仅对训练数据有很好的拟合,而且对未知数据也有很好的预测。
我们用损失函数来表示这一目标,针对条件概率模型我们大多使用对数似然函数损失函数。损失函数通常是正则化的极大似然函数。策略就是以损失函数为目标函数的最小化

(个人观点)那它的条件概率就要取大些的才行,所以后面的特征选择都是在挑大的。

三.算法

决策树学习的算法通常是一个递归地选择最优特征,并且根据这个特征对训练数据进行分割,直到所有训练数据子集被正确分类或者没有合适的特征为止。当然这样还会形成过拟合的问题,就需要我们去剪枝泛化。
所以决策树学习的算法包括三个过程:特征选择、决策树的生成、决策树的剪枝。

1. 特征选择

在说明具体的算法之前我们必须了解特征是如何选择的,所有决策树学习算法都逃不开特征选择的问题。
**通常的特征选择的准则是:信息增益或者信息增益比。**当然还有基尼指数、平法误差最小化。

信息增益与信息增益比

了解信息增益之前先要了解什么是熵和条件熵,经验熵和条件经验熵。

  1. 熵表示随机变量不确定性的度量。熵越大,不确定性就越大。
    在这里插入图片描述

  2. 条件熵H(Y|X)表示在已知随机变量X的条件下随机变量Y的不确定性。
    在这里插入图片描述
    当熵和条件熵中的概率有数据估计得到就变成了经验熵和经验条件熵。

信息增益:特征A对训练数据集D的信息增益g(D,A),定义为集合D的经验熵H(D)与特征A给定条件下D的经验条件熵H(D|A)之差。在这里插入图片描述
信息增益表示得知特征X的信息而使得类Y的信息不确定性减少的程度,越大越好。
在这里插入图片描述
在这里插入图片描述
其中|D|表示样本个数;|C_k|表示属于第k类的样本个数;A表示特征,A1、A2…表示不同的特征;D_i表示根据不同特征把D划分的子集,|D_i|表示子集的样本个数。
信息增益比在这里插入图片描述

2. ID3树生成算法

以信息增益准则选择特征。

  1. 如果所有实例属于一个类,T为单结点树,把这个类作为结点的类标记;或者特征为空,T为单结点树,把D中实例数最大的类作为该结点的类标记。
  2. 否则,计算不同特征A对D的信息增益,选择信息增益最大的特征A_g,如果A_g小于阈值,设置为单结点数,把D中实例数最大的类作为该结点的类标记。(阈值不是通过计算得到而是靠经验,我们可以看别人的论文来设置它)
  3. 否则对A_g的每一个可能值a_i划分非空子集D_i,把D_i中实例数最大的类作为标记,构建子结点。
  4. 对子结点,以D_i为训练集,以A-{A_g}为特征集,递归调用1-3步。
3. C4.5树生成算法

与ID3算法相同只不过是以信息增益比来选择特征的。

4. 决策树剪枝算法

决策树的剪枝通过极小化决策树整体的损失函数来实现。
整体树T损失函数定义为:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

其中|T|表示树T的叶结点个数,N_t表示该叶结点的样本个数,N_tk表示叶结点样本个数中属于k类的样本个数。C(T)表示模型对训练数据的预测误差,|T|表示模型复杂程度(叶结点的个数),α用于调节两者之间的影响。
算法

  1. 计算每个结点的经验熵;
  2. 递归的从树的叶结点开始向上回缩:
    在这里插入图片描述
    假设是如下示例,现在的这个树叫做Ta:
    在这里插入图片描述
    (1. Ta时C(T)为0,但是α|T|是大的,假设α=1,那么α|T|=11,C_α(Ta)=0+α|T|=11;
    (2. 如果我们减去5号内部结点,变成叶结点,整个树Ta变成Tb。那么C(T)变成C(T4),因为其他地方的经验熵H_t(T)还是0,只有这里开始变化。计算的C_α(Tb)=C(T4)+α|Tb|;
    (3. 当然你可以剪去2和5,那么C_α(Tc)=C(T5+T2)+α|Tc|。
  3. 重复2直到不能继续为止。减到无叶减。

其实《统计学习方法》中的剪枝属于后剪枝中的代价-复杂度剪枝方法。

ID3和C4.5决策树代码
# 剪枝部分不是完全代码
import math
import numpy as np
from queue import LifoQueue, Queue


class item:
    def __init__(self, name=''):
        self.feature_name = name
        self.classify = {}      # ‘是’:[NTK,item],‘否’:[NTK,item]


# 创建数据集 备注 李航《统计学习方法》中表5.1 贷款申请数据数据
def createDataLH():
    data = np.array([['青年', '否', '否', '一般']])
    data = np.append(data, [['青年', '否', '否', '好']], axis=0)
    data = np.append(data, [['青年', '是', '否', '好']
                            , ['青年', '是', '是', '一般']
                            , ['青年', '否', '否', '一般']
                            , ['中年', '否', '否', '一般']
                            , ['中年', '否', '否', '好']
                            , ['中年', '是', '是', '好']
                            , ['中年', '否', '是', '非常好']
                            , ['中年', '否', '是', '非常好']
                            , ['老年', '否', '是', '非常好']
                            , ['老年', '否', '是', '好']
                            , ['老年', '是', '否', '好']
                            , ['老年', '是', '否', '非常好']
                            , ['老年', '否', '否', '一般']
                           ], axis=0)
    label = np.array(['否', '否', '是', '是', '否', '否', '否', '是', '是', '是', '是', '是', '是', '是', '否'])
    name = np.array(['年龄', '有工作', '有房子', '信贷情况'])
    return data, label, name


def createDataXG20():
    data = np.array([['青绿', '蜷缩', '浊响', '清晰', '凹陷', '硬滑']
                    , ['乌黑', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑']
                    , ['乌黑', '蜷缩', '浊响', '清晰', '凹陷', '硬滑']
                    , ['青绿', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑']
                    , ['浅白', '蜷缩', '浊响', '清晰', '凹陷', '硬滑']
                    , ['青绿', '稍蜷', '浊响', '清晰', '稍凹', '软粘']
                    , ['乌黑', '稍蜷', '浊响', '稍糊', '稍凹', '软粘']
                    , ['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '硬滑']
                    , ['乌黑', '稍蜷', '沉闷', '稍糊', '稍凹', '硬滑']
                    , ['青绿', '硬挺', '清脆', '清晰', '平坦', '软粘']
                    , ['浅白', '硬挺', '清脆', '模糊', '平坦', '硬滑']
                    , ['浅白', '蜷缩', '浊响', '模糊', '平坦', '软粘']
                    , ['青绿', '稍蜷', '浊响', '稍糊', '凹陷', '硬滑']
                    , ['浅白', '稍蜷', '沉闷', '稍糊', '凹陷', '硬滑']
                    , ['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '软粘']
                    , ['浅白', '蜷缩', '浊响', '模糊', '平坦', '硬滑']
                    , ['青绿', '蜷缩', '沉闷', '稍糊', '稍凹', '硬滑']])
    label = np.array(['是', '是', '是', '是', '是', '是', '是', '是', '否', '否', '否', '否', '否', '否', '否', '否', '否'])
    name = np.array(['色泽', '根蒂', '敲声', '纹理', '脐部', '触感'])
    return data, label, name

# 定义一个常用匿名函数 用来求numpy array中数值等于某值的元素数量
# 传入x,y,x表示特征集,y表示某个特征。numpy的一个用法x[x == y].size
# 解释:x == y 是得到取x中所有元素值等于y的元素的索引(类似这样理解)
equalNums = lambda x, y: 0 if x is None else x[x == y].size


# 定义计算经验熵的函数
def singleEntropy(x):
    """计算一个输入序列的经验熵"""
    # 转换为 numpy 矩阵
    x = np.asarray(x)
    # 取所有不同值
    xValues = set(x)
    # 计算熵值
    entropy = 0
    for xValue in xValues:
        p = equalNums(x, xValue) / x.size
        entropy -= p * math.log(p, 2)   # log函数
    return entropy


# 定义计算经验条件熵的函数
def conditionnalEntropy(feature, y):
    """计算 某特征feature 条件下y的经验熵"""
    # 转换为numpy
    feature = np.asarray(feature)
    y = np.asarray(y)
    # 取特征的不同值
    featureValues = set(feature)
    # 计算熵值
    entropy = 0
    # 计算某个特征下不同的取值的经验条件熵
    for feat in featureValues:
        # y[feature == feat]是取y中feature元素值等于feat的元素索引的y的元素的子集
        p = equalNums(feature, feat) / feature.size     # 某个特征值在所有特征的个数,数据估计得到的概率
        entropy += p * singleEntropy(y[feature == feat])    # 根据特征值划分得到的子集,求子集的经验熵
    return entropy


# 定义信息增益
def infoGain(feature, y):
    return singleEntropy(y) - conditionnalEntropy(feature, y)


# 定义信息增益比
def infoGainRatio(feature, y):
    return 0 if singleEntropy(feature) == 0 else infoGain(feature, y) / singleEntropy(feature)


# 特征选取
def bestFeature(data, labels, method='id3'):
    assert method in ['id3', 'c45'], "method 须为id3或c45"
    data = np.asarray(data)
    labels = np.asarray(labels)

    # 根据输入的method选取 评估特征的方法:id3 -> 信息增益; c45 -> 信息增益率
    def calcEnt(feature, labels):
        if method == 'id3':
            return infoGain(feature, labels)
        elif method == 'c45':
            return infoGainRatio(feature, labels)

    # 特征数量  即 data 的列数量
    featureNum = data.shape[1]
    # 计算最佳特征
    bestEnt = 0
    bestFeat = -1
    for feature in range(featureNum):
        ent = calcEnt(data[:, feature], labels)
        if ent >= bestEnt:
            bestEnt = ent
            bestFeat = feature
    return bestFeat, bestEnt


# 根据特征及特征值分割原数据集  删除data中的feature列,并根据feature列中的值分割 data和label
def splitFeatureData(data, labels, feature):
    """feature 为特征列的索引"""
    # 取特征列
    features = np.asarray(data)[:, feature]
    # 数据集中删除特征列
    data = np.delete(np.asarray(data), feature, axis=1)
    # 标签
    labels = np.asarray(labels)

    uniqFeatures = set(features)    # 获得特征列的特征值
    dataSet = {}
    labelSet = {}
    for feat in uniqFeatures:
        dataSet[feat] = [len(data[features == feat]), data[features == feat]]
        labelSet[feat] = [len(labels[features == feat]), labels[features == feat]]
    # print(dataSet, labelSet)
    return dataSet, labelSet


# 多数投票
def voteLabel(labels):
    uniqLabels = list(set(labels))
    labels = np.asarray(labels)

    labelNum = []
    for label in uniqLabels:
        # 统计每个标签值得数量
        labelNum.append(equalNums(labels, label))
    # 返回数量最大的标签
    return uniqLabels[labelNum.index(max(labelNum))]


# 计算某类的样本个数,剪枝需要用
def cal_labelSet(label):
    labelclassfiy = {}
    label = np.asarray(label)
    labels = set(label)
    for feat in labels:
        labelclassfiy[feat] = equalNums(label, feat)
    return labelclassfiy


# 创建决策树
def createTree(data, labels, names, method='id3'):
    data = np.asarray(data)
    labels = np.asarray(labels)
    names = np.asarray(names)
    # 如果结果为单一结果
    if len(set(labels)) == 1:
        return labels[0]
    # 如果没有待分类特征
    elif data.size == 0:
        return voteLabel(labels)
    # 其他情况则选取特征
    bestFeat, bestEnt = bestFeature(data, labels, method=method)
    # 取特征名称
    bestFeatName = names[bestFeat]
    # 从特征名称列表删除已取得特征名称
    names = np.delete(names, [bestFeat])
    # 根据选取的特征名称创建树节点
    decisionTree = item(bestFeatName)
    # 根据最优特征进行分割
    dataSet, labelSet = splitFeatureData(data, labels, bestFeat)
    # 对最优特征的每个特征值所分的数据子集进行计算,多层嵌套的字典
    for featValue in dataSet.keys():
        decisionTree.classify[featValue] = [cal_labelSet(labelSet[featValue][1]),
                                            createTree(dataSet[featValue][1], labelSet[featValue][1], names, method)]
        # decision第三个可以是item子节点,也可以是label叶结点
    return decisionTree


ht = []


# 计算每个结点的经验熵
def cal_ht(classify):
    Nt = 0
    for i in classify.keys():    # 得到特征的样本数
        Nt += classify[i]
    Ht = 0
    for i in classify.keys():
        q = classify[i]/Nt
        Ht -= q * math.log(q, 2)  # log函数
    return Ht
# 递归计算
def cal(p):
    h = []
    for i in p.classify.keys():
        h.append(cal_ht(p.classify[i][0]))
        if isinstance(p.classify[i][1], type(p)):
            h.append(cal(p.classify[i][1]))
    return h

# 简单的剪枝算法
def pruning(decisonTree,labelclassify):
    # 得到每个结点经验熵
    p = decisonTree
    ht.append(cal_ht(labelclassify))      # 第一个结点
    ht.append(cal(p))
    '''
        计算次剪枝一部分,得到的C(T),书上说用动态规划存储???怎么弄
        
    '''
    print(ht)
    return decisonTree


if __name__ == '__main__':
    # 使用李航数据测试函数 p62
    # lhData, lhLabel, lhName = createDataLH()
    # lhTree = createTree(lhData, lhLabel, lhName, method='id3')
    # pruning(lhTree, cal_labelSet(lhLabel))
    # print(lhTree)
    # 西瓜书
    xgData, xgLabel, xgName = createDataXG20()
    xgTree = createTree(xgData, xgLabel, xgName, method='id3')
    pruning(xgTree, cal_labelSet(xgLabel))
    print(xgTree)
    #

参考:https://blog.youkuaiyun.com/ylhlly/article/details/93213633?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522160378892119725222464193%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=160378892119725222464193&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v28-5-93213633.pc_first_rank_v2_rank_v28&utm_term=%E5%86%B3%E7%AD%96%E6%A0%91%E5%89%AA%E6%9E%9Dpython%E4%BB%A3%E7%A0%81&spm=1018.2118.3001.4187

CART算法

分类与回归树:假设决策树是二叉树,内部结点特征的取值为是或者否。

回归树

使用平方误差最小化准则来进行特征选择。

  1. 给你一堆数据集{(x_i,y_i)},先取第一个切分变量j(就是x1,y1)的x作为切分点s,得到两个区域在这里插入图片描述
    接着利用在这里插入图片描述
    分别求出两个区域的值带入
    在这里插入图片描述
  2. 递归的遍历所有的x作为s,得到组m(s),取上面式子最小的那一个的s作为切分点,当然它的(x,y)也是切分变量。这是第一个棵树,如果不完美我们还可以接着子区域分。
  3. 两个子区域重复1,2步骤,最终完美停止,生成决策树。
分类树

使用基尼指数选择最优特征。

  1. 根据下面式子计算每个特征不同特征值的基尼指数,也就是说如果A特征有多个特征值都需要计算在这里插入图片描述
    在这里插入图片描述
  2. 计算完一个特征,找到其最小的基尼指数的特征值,他就是A1的最优切分点。
  3. 计算玩所有的特征,在每一个特征的最优切分点之间比较,选择基尼指数最小的特征,它为最优特征,它的特征值为最优切分点。
  4. 于是生成两个子结点。对子结点递归进行1,2,3步骤,直到我们可以把类别分干净。
CCP剪枝
  1. 在T0时(也就是树还没有开始减的时候),计算每个内部节点的g(t)在这里插入图片描述
    (1. 其中C(t)表示假设t是内部叶子节点时的节点误差率,它是由在这里插入图片描述
    变形得到,注意|T|=1,只有一个叶结点,但是!!!,这个叶结点里面包含多个样本,而且这些样本的类可以不同,也就是说算C(T)和算H_t(T)差不多。
    (2. 然后找到最小的g(t),其对应的结点减去形成新的叶子结点。
    (3. 重复1,2步骤,直到变成单结点树
    (4. 在形成的树中通过交叉验证(2/3做训练集,1/3做验证集)中的验证集来测数据的准确率(正确数/总数),选择准确率最高的树。

参考剪枝示例1:https://zhuanlan.zhihu.com/p/30296061
参考剪枝示例2:https://blog.youkuaiyun.com/am290333566/article/details/81187562

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值