【Python】CART分类树与回归树实现

本文详细介绍了CART算法,包括决策树的构建过程、节点类定义、分类与回归树的区别、基尼指数计算、节点划分策略以及后剪枝技巧。通过实例演示了如何使用Python实现 CART,重点讲解了递归、离散与连续属性处理和精度评估。
摘要由CSDN通过智能技术生成

着重总结记录实验过程(属实把决策树玩明白了…)

重要的点

  1. CART是二叉树
  2. 分类树中处理离散属性,父节点用它划分过,子节点就不能使用;处理连续属性则在父节点划分后,子节点仍然可以使用
  3. 若使用递归建树,当判定节点为叶子时要return,此时一定搞清楚递归return的地方:必须进入子节点再return,而不能站在父节点的位置判断子节点是否是叶子就立即return。这样只会照顾到先判断的子节点而忽视了后面判断的子节点。
  4. 递归函数中,每个return的地方,都是生成叶子的地方

决策树说白了就是一连串的决策,最终决策的终点自然就是结果。尤其对CART这个二叉树来说,当要分类或者预测一个样本,那么就不断从样本的属性列表中取属性,样本的该属性和树的该节点的值做比较,小于就分到左子树,大于等于就分到右子树,这个样本最终到叶子的时候,这个叶子上存储的类别或者预测值就是这个样本的结果。
至于建树过程,就是对一大堆已有结果的样本,通过对属于某个类别下的所有样本的共有的突出属性的不断提取,最后到叶子时(停止条件),叶子分到的样本集中,多数样本的类别是什么或者所有样本的均值是什么,这个结果就作为叶子存储的结果。最终从根节点开始往下递归,形成这样一颗决策树。

节点类

既然是一颗树,那还是单独定义一个节点类比较直观。

但是网上有不少大佬也用字典或者元组来定义节点,也比较方便。github上大佬的机器学习代码

首先节点应该分为两类:

  1. 分支节点(包括根节点):功能就是存储一个划分属性,用该划分属性的值来划分左右子树
  2. 叶子节点:功能就是存储一个类别标签,当一个样本来到这个叶子节点,那么该样本对应的类别就是叶子存储的类别。

考虑节点需要的属性与方法:

  1. 用来划分的属性
  2. 用来划分的属性的值
  3. 节点的属性(或者说是父节点分下来的树枝。其余父节点的属性名对应)
  4. 左孩子
  5. 右孩子
  6. 父节点(作用不大,但是可以用来辨别根节点)
  7. 待选择的属性列表(在遇到离散属性作为划分属性时,使用之后就从列表中删除)
  8. 样本索引列表(用来得到本节点拥有的样本集合)
  9. 节点描述方法
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File   :   node.py
@Time   :   2021/11/18 10:49:33
@Author :   WYJ 
@Desc   :   构建一个决策树的节点类
'''


class Node:
    def __init__(self, attr_name2split_value=None, attr_value=None, parent=None, left_child=None, right_child=None,
                 leaf_type=None, remain_attrs=None, data_indexes=None):
        """
        节点类初始化函数
        节点类别标识:
                    分支节点:attr_name2split_value != {} and leaf_type == None
                    叶子节点:attr_name2split_value == {} and leaf_type != None
                    判断叶子节点优先使用leaf_type,防止忘记更新attr_name2split_value导致出错
        
        param attr_value: 本节点中样本的属性值,与父节点的属性名相对应。
        param attr_name2split_value: 本节点进行划分子节点的属性名与值的字典 {name:value}
        param parent: 本节点的父节点
        param left_child: 本节点的左孩子
        param right_child: 本节点的右孩子
        param leaf_type: 本节点若是叶节点,该值标识样本类型
        param remain_attrs: 剩余未划分的属性
        param data_indexes: 划分到本节点的样本数据下标列表
        """
        self.attr_value = attr_value
        self.attr_name2split_value = {} if attr_name2split_value is None else attr_name2split_value
        self.parent = parent
        self.left_child = left_child
        self.right_child = right_child
        self.leaf_type = leaf_type
        self.remain_attrs = [] if remain_attrs is None else remain_attrs
        self.data_indexes = [] if data_indexes is None else data_indexes


    def node_to_string(self):
        """
        将节点相关属性用字符串描述
        
        return: 返回描述字符串
        """
        info = ''
        if self.leaf_type is None:
            if self.parent == None:
                info += '【根节点】'
                info += '【分支节点】:\n划分属性:' + str(list(self.attr_name2split_value.keys())[0])\
                        + '\t划分点:' + str(list(self.attr_name2split_value.values())[0]) + '\n'
            else:
                info += '【分支节点】:\n'
                info += '所属分支:' + str(list(self.parent.attr_name2split_value.keys())[0])\
                        + '(' + self.attr_value + ')' + '\n'
                info += '划分属性:' + str(list(self.attr_name2split_value.keys())[0])\
                        + '\t划分点:' + str(list(self.attr_name2split_value.values())[0]) + '\n'
        else:
            info += '【叶节点】:' + '\n'
            info += '所属分支:' + str(list(self.parent.attr_name2split_value.keys())[0])\
                    + '(' + self.attr_value + ')' + '\n'
            info += '类别:' + str(self.leaf_type) + '\n'
        return info
        

CART类

  1. 类结构是这样的:
    使用时可以先new一个cart类,同时选择要生成的决策树是分类树还是回归树。在CART类的初始化方法中,就根据传入的数据集等调用生成树的方法去建树了。所以当CART对象初始化结束,一颗树就建好了,可以直接调用相应的test方法去测试了。
  2. 首先CART类肯定需要一个root节点,其他的属性解释见代码:
from node import Node

class CART:
    def __init__(self, data_set=None, discrete_attrs=None, tree_type=1, min_samples_leaf=2, max_deepth=9):
        """
        CART类的初始化函数,可以生成分类树与回归树(默认生成分类树)
        
        param data_set: 训练集的字典列表,以{属性:值}的形式存储,样本类别为最后一个键值对
        param discrete_attrs: 离散属性集合
        param tree_type: 构造树的类型 1为分类树,0为回归树
        param min_samples_leaf: 叶子节点的最小样本量,默认为2
        param max_deepth: 递归深度(树的深度),默认位9
        return: 得到一颗树的根节点
        """
        self.root = Node()
        self.data_set = {} if data_set is None else data_set
        self.attr_names = list(data_set[0].keys())[0:-1]
        self.discrete_attrs = [] if discrete_attrs is None else discrete_attrs
        self.tree_type = tree_type
        self.min_samples_leaf = min_samples_leaf
        self.max_deepth = max_deepth
        if tree_type == 1:
            self.grow_classify_CART()
        else:
            self.grow_regression_CART()
  1. 接下来自顶向下去描述一棵树的构造过程,这样思路会更流畅,根据顶层的需要去构建底层提供的功能。
    放上流程图,重点关注三个递归return的地方,也就是生成叶子节点的地方
    在这里插入图片描述

CART分类树

分类树是比较容易理解的,就是对样本的一连串属性做一连串决策,就得到样本的类别。

建树过程

  1. 建树过程是一个递归算法,在调用递归函数之前,首先对训练集中的样本生成下标,初始化一个根节点,然后将根节点传入递归函数去建树。
    def grow_classify_CART(self):
        """
        生成一颗分类树
        
        return: 最终得到一颗分类树,可以沿着根节点走完整棵树
        """
        # 为数据集样本添加下标
        root_data_indexes = []
        for i in range(len(self.data_set)):
            root_data_indexes.append(i)
        # 初始化根节点
        root_node = Node(remain_attrs=copy.deepcopy(self.attr_names), data_indexes=root_data_indexes)
        self.root = root_node
        self.build_classify_tree(root_node, parent_leaf_type=0, deepth=1)
  1. 当一个节点进入build_classify_tree()函数,首先就判断当前节点拥有的样本集是否为空(流程图中第三个return),若空,标记该孩子节点为父节点中最多的类别(叶节点),因为实在没有数据,没办法继续判断了,只好将父样本中最多的类别当成孩子的类别。其中这个父节点中最多的类别会和叶子节点一起传递进递归函数
        # 得到当前节点的训练集
        data_indexes = curr_node.data_indexes
        # 判断当前节点样本集是否为空
        if len(data_indexes) == 0:
            curr_node.leaf_type = parent_leaf_type # 空则标记该孩子节点为父节点中最多的类别(叶节点)
            return
  1. 接下来统计该节点拥有的样本集中的类别,得到样本最多的类是什么类。判断当前节点拥有的样本是否都是同一类别(流程图第一个return),以及控制一个最大递归深度,超过最大深度就return。
        node_data_set = self.split_data(data_indexes)
        # 得到当前训练集样本对应的类别
        node_data_set_targets = []
        for item in node_data_set:
            node_data_set_targets.append(item['target'])
        # 得到当前训练集样本最多的类别
        leaf_type = max(node_data_set_targets, key=node_data_set_targets.count)
        # 控制递归深度
        if deepth >= self.max_deepth:
            curr_node.leaf_type = leaf_type
            return
        # 判断当前节点拥有的样本是否都是同一类别
        if len(set(node_data_set_targets)) == 1: # 【!!!】忘记加len导致无法从同类节点return
            curr_node.leaf_type = node_data_set_targets[0] # 是则标记当前节点为该类别(叶节点)
            return
  1. 对于流程图中的第二个return,判断待划分属性是否为空or样本属性值都一样(类别可能不一样),这个return其实实际跑到的样本不多,尤其是same_flag,但也是必要的。这里注意一点,因为**要对可变对象list做删除操作,但又不希望原list有变化,就必须用深拷贝。**比如此处的remain_attrs(后面要做删除)和temp_node_data_set
        # 判断待划分属性是否为空or样本属性值都一样(类别可能不一样)
        remain_attrs = copy.deepcopy(curr_node.remain_attrs) # 得到当前节点的剩余待划分属性
        # 判断待划分属性是否为空
        empty_flag = 0
        if len(remain_attrs) == 0:
            empty_flag = 1
        # 判断样本属性值是否都一样
        temp_node_data_set = copy.deepcopy(node_data_set) # 先复制一份数据
        for item in temp_node_data_set:
            del item['target'] # 因为类别可能不一样,就先删除类别的键值对
        temp_item = temp_node_data_set[0] # 便于比较
        same_flag = 1
        for item in temp_node_data_set:
            if item != temp_item: # 只要有一个不同,数据集中的样本就不一样
                same_flag = 0
        # 是则标记当前节点为最多样本的类别(叶节点)
        if empty_flag or same_flag:
            curr_node.leaf_type = leaf_type 
            return
  1. 几个return点判断完毕,那么该节点确定是分支节点,就需要选择最优划分属性、去生成左右子节点,然后分别递归建左右子树了。这里还需要做两个操作,其一是判断是否移除属性,其二是填充当前节点的划分属性与划分属性值。至于如何选择划分属性,先不关注。
        # 选择最优划分属性
        selected_attr, split_value = self.get_best_divide_attr_classify(curr_node)
        # 若被选择属性是离散属性,那么本次用完就移除
        if selected_attr in self.discrete_attrs:
            # 从剩余属性列表中移除被选择的属性
            remain_attrs.remove(selected_attr)
        # print(selected_attr)
        # 填充{划分属性:划分属性值}
        curr_node.attr_name2split_value[selected_attr] = split_value
  1. 现在需要生成当前节点的左右孩子,先做一些必要的节点初始化,再分别调用build_classify_tree()函数生成左右子树。
        # 为选择的属性生成左孩子和右孩子
        # filter函数得到比划分值小、比划分值大的两部分数据集
        left_data_set_indexes = list(filter(lambda index: self.data_set[index][selected_attr] <= curr_node.attr_name2split_value[selected_attr], data_indexes))
        right_data_set_indexes = list(filter(lambda index: self.data_set[index][selected_attr] > curr_node.attr_name2split_value[selected_attr], data_indexes))
        # 左右孩子所划到的样本的属性值,左孩子的属性值为'<=划分值'
        left_attr_value = '<=' + str(curr_node.attr_name2split_value[selected_attr])
        right_attr_value = '>' + str(curr_node.attr_name2split_value[selected_attr])
        curr_node.left_child = Node(attr_value=left_attr_value, parent=curr_node, remain_attrs=remain_attrs, data_indexes=left_data_set_indexes)
        curr_node.right_child = Node(attr_value=right_attr_value, parent=curr_node, remain_attrs=remain_attrs, data_indexes=right_data_set_indexes)

        # 递归构建左右子树,同时深度+1,并且传参当前节点样本中最多的类别进入子节点的递归
        self.build_classify_tree(curr_node.left_child, leaf_type, deepth+1)
        self.build_classify_tree(curr_node.right_child, leaf_type, deepth+1)

选择划分属性

递归的事情做完了,那么现在需要考虑怎么从当前还剩下的属性中,选择最优的划分属性。做选择的标准就是基尼指数,哪个属性的基尼指数最小,就选哪个属性。同时也要将该属性对应的划分值传递出去方便划分样本集。

    def get_best_divide_attr_classify(self, curr_node):
        """
        基于基尼指数从剩余属性中选择最优划分属性
        
        param curr_node: 当前节点
        return: 返回选择的划分属性与对应的划分值
        """
        attrs_gini_dict = {} # 统计每个属性的基尼指数
        attrs_split_value = {} # 统计每个属性的划分值
        remain_attrs = curr_node.remain_attrs # 从当前节点中得到剩余属性
        # 遍历每个剩余属性,求得基尼指数字典与划分值字典
        for attr in remain_attrs:
            data_set_a = self.split_data(curr_node.data_indexes)
            gini_value_a, split_value_a = self.Gini_index(data_set_a, attr)
            attrs_gini_dict[attr] = gini_value_a
            attrs_split_value[attr] = split_value_a
        # 选择基尼指数最小(优)的划分属性
        selected_attr = min(attrs_gini_dict, key=attrs_gini_dict.get)
        # 得到该划分属性对应的划分值
        split_value = attrs_split_value[selected_attr]
        return selected_attr, split_value

如何计算基尼指数,交给Gini_index(data_set_a, attr)考虑。

计算基尼指数

一个属性的基尼指数计算公式如下:
在这里插入图片描述
属性a有V个属性值,每个属性值拥有一部分样本,需要计算每个拥有相同属性值的样本集,也就是Dv的基尼值,做一个加权求和,才能得到属性a的基尼指数。

但是这里牵扯两种属性的处理方式:

离散属性的处理

知乎一个文章说的比较清楚当在决策树CART中有连续的和离散的预测变量时,如何计算预测变量的重要度?
因为CART是一个二叉树,那一个离散属性有多个取值时,就不能生成一个多叉树,而是要选择最好的两个分叉,即去遍历属性值的不同组合情况,每种组合都有可能成为这两个分叉。就要计算不同组合的基尼值,选最小的那个组合作为属性a的基尼指数。
在这里插入图片描述

连续属性的处理

对于连续属性,划分上述的组合就比较容易,采用连续数值离散化的方法,首先要排序去重
在这里插入图片描述
为了方便,对样本中的离散属性进行数字化,在这里计算基尼指数时直接采用连续属性的处理方法:
(虽然对离散属性来说丧失了一些组合的选择,但如果数字化的合理的话,也是可以采用的,比如离散属性值有一个趋势,西瓜色泽{浅白,青绿,乌黑}={1,2,3}。另一个原因是实际情况中离散属性并不多,多是连续的属性。)

    def Gini_index(self, data_set, divide_attr):
        """
        计算输入属性的基尼指数
        
        param data_set: 当前节点数据集的字典列表 [{'sepal_length':7.7}, {'sepal_width':6.8}...{'target':1}]
        param divide_attr: 当前用于划分数据集的属性
        return: 输入属性的基尼指数与最优划分点
        """
        data_num = len(data_set)
        gini_index_value = 0 # 最终返回的基尼指数
        divide_attr_value_list = [] # 存储划分属性的值
        split_points = [] # 存储候选划分点
        split_res = [] # 存储候选划分结果
        # 统计划分属性的值
        for item in data_set:
            divide_attr_value_list.append(item[divide_attr])
        # 对划分属性的值进行排序
        sorted_attr_list = divide_attr_value_list.copy()
        sorted_attr_list.sort()
        # 统计候选划分点集合
        for i in range(data_num-1):
            split_points.append((sorted_attr_list[i]+sorted_attr_list[i+1])/2)
        
        s_split_points = set(split_points)
        # 求每个候选划分结果
        for point in s_split_points:
            data_set_v1 = list(filter(lambda item: item[divide_attr]<=point, data_set))
            data_set_v2 = list(filter(lambda item: item[divide_attr]>point, data_set))
            gini_index_value1 = (self.Gini(data_set_v1) * len(data_set_v1)) / data_num
            gini_index_value2 = (self.Gini(data_set_v2) * len(data_set_v2)) / data_num
            split_res.append(gini_index_value1 + gini_index_value2)
        # 求最优划分点与对应的基尼指数
        gini_index_value = min(split_res)
        split_value = split_points[split_res.index(gini_index_value)]
        return gini_index_value, split_value

如何计算一个数据集的基尼值,交给Gini(data_set_v1)去考虑

计算基尼值

基尼值的计算公式如下,基尼值反应了一个不纯度,那么自然是越小越好:
在这里插入图片描述

    def Gini(self, data_set_v):
        """
        计算输入数据集的基尼值
        
        param data_set_v: 输入数据集的字典列表,每一个样本对应一个dict,存储{属性:值},所有样本组成一个list
        return: 基尼值
        """
        data_num = len(data_set_v)
        # 统计每个类别的数量
        type_cnt_dict = {} 
        for item in data_set_v:
            type_cnt_dict[item['target']] = \
                            type_cnt_dict.get(item['target'], 0) + 1
        # 基于公式4.5计算基尼值
        P_k_sum = 0
        for key in type_cnt_dict.keys():
            P_k_sum += pow(type_cnt_dict[key] / data_num, 2)
        gini_value = 1 - P_k_sum
        return gini_value

广度优先打印一颗树

到现在为止,一颗分类树就建好了,可以用层序遍历(广度优先)打印出每一层节点的描述信息:

    def print_tree(self):
        """
        打印这棵树以便直观的观察分类规则
        """
        # 使用队列,以广度优先的方法打印每一层的节点
        node_queue = [copy.deepcopy(self.root)]
        node_cnt = 1
        while(len(node_queue)>0):
            node_cnt += 1
            curr_node = node_queue[0]
            # print(curr_node.node_to_string()) # 调用Node的描述方法
            # 不能用if not curr_node.leaf_type: 因为0也是其中一个类型,这样会把叶子节点误判为分支节点
            if curr_node.leaf_type is None:
                node_queue.append(curr_node.left_child)
                node_queue.append(curr_node.right_child)
            node_queue.remove(curr_node)
            # print('---------------------------------------')
        print('节点总数为:', node_cnt)

测试准确率

从root开始,一直进行决策,找到叶子节点也就找到了对应的类别。

    def test_classify(self, test_dict_list):
        """
        对输入的测试集调用决策树进行预测分类
        
        param test_dict_list: 测试集,和训练集一样是字典列表
        return acc: 准确率
        """
        target = []
        pre_target = []
        cnt = 0
        for item in test_dict_list:
            curr_node = copy.deepcopy(self.root)
            while curr_node.leaf_type is None:
                if item[list(curr_node.attr_name2split_value.keys())[0]] <= list(curr_node.attr_name2split_value.values())[0]:
                    curr_node = curr_node.left_child
                else:
                    curr_node = curr_node.right_child
            if item['target'] == curr_node.leaf_type:
                cnt += 1
            target.append(item['target'])
            pre_target.append(curr_node.leaf_type)
        acc = cnt / len(test_dict_list)
        # print('实际类别:', target)
        # print('预测类别:', pre_target)
        # print('准确率=', acc)
        return acc

在这里插入图片描述

CART回归树

对于回归树刚开始不容易理解,决策树天然的可以进行分类,但是如何预测呢?
其实也相当于是一个分类问题,只不过叶子节点存储的不再是类别,而是现在叶子节点拥有样本集的平均分,这个平均分就可以看做是这个样本的预测值。因为根据样本的属性,做了一系列决策之后,判定这个样本和现在这个叶子节点拥有的样本最类似,那么这个未知样本的分数自然也和已有的样本均值接近。

建树过程

为了代码的可读性,便于理解,就写了另一套建回归树的代码,其实内部逻辑和分类树是一样的。
两点不同:

  1. 流程图中第三个return点,回归树判定当前节点拥有的样本集数量小于一个阈值时,例如2,就return。因为叶子节点样本太小了没意义。
        # 判断当前节点样本集是否小于预定值
        if len(data_indexes) <= self.min_samples_leaf:
            curr_node.leaf_type = parent_mean_score
            return

  1. 不再做连续和离散属性的区分,采用的策略是,父节点使用的划分属性,子节点不可以再次使用。
        # 选择最优划分属性
        selected_attr, split_value = self.get_best_divide_attr_reg(curr_node)
        # 从剩余属性列表中移除被选择的属性
        remain_attrs.remove(selected_attr)

选择划分属性

和分类树相同

选择划分点

对应分类树的计算基尼指数,也是相同的逻辑

计算MSE

这里是计算当前样本集的分数均值与真实分数的均方误差,当然也是越小越好,证明这个样本集中的样本更接近。
调用sklearn的库计算MSE

    def cal_mse(self, data_set_v):
        """
        计算输入数据集的mse
        
        param data_set_v: 输入数据集的字典列表,每一个样本对应一个dict,存储{属性:值},所有样本组成一个list
        return: mse
        """
        data_num = len(data_set_v)
        # 统计每个类别的数量
        true_scores = []
        for item in data_set_v:
            true_scores.append(item['target'])
        mean_score = sum(true_scores) / data_num
        pred_scores = [mean_score] * data_num
        # 计算mse
        mse = mean_squared_error(true_scores, pred_scores) # 调用sklearn的MSE计算函数
        return mse

使用R2指标测试回归树性能

    def test_reg_R2(self, test_dict_list):
        """
        对输入的测试集调用决策树进行预测回归
        
        param test_dict_list: 测试集,和训练集一样是字典列表
        return R2: R2值,越大越好,最大为1
        """
        target = []
        pre_target = []
        for item in test_dict_list:
            curr_node = copy.deepcopy(self.root)
            while curr_node.leaf_type is None:
                if item[list(curr_node.attr_name2split_value.keys())[0]] <= list(curr_node.attr_name2split_value.values())[0]:
                    curr_node = curr_node.left_child
                else:
                    curr_node = curr_node.right_child
            target.append(item['target'])
            pre_target.append(curr_node.leaf_type)
        mse = mean_squared_error(target, pre_target)
        R2=1-mse/np.var(target)
        # print('R2=', R2)
        return R2

波士顿房价预测测试:
在这里插入图片描述

后剪枝

直接生成的决策树可能会过拟合,后剪枝的也避免了预剪枝的欠拟合情况,所以使用后剪枝策略。
后剪枝就是考虑叶子节点的父节点,如果把这个父节点设为叶节点,准确度会不会提高,会提高就剪,不会提高就恢复原样。而如果确定对这个父节点剪枝,那它就变成了叶节点,再考虑这个叶节点的父节点要不要剪枝。

  1. 首先得到一个待判断节点列表,也就是树的倒数第二层,连着两个叶节点的分支节点。
        nodes_waiting_judge = [] # 待剪枝节点列表
        node_queue = [] # 节点队列
        node_queue.append(self.root)
        # 首先使用广度优先遍历将最后一层分支节点加入待剪枝列表
        while len(node_queue) > 0:
            curr_node = node_queue[0]
            # 首先判断是分支节点
            if curr_node.leaf_type is None:
                node_queue.append(curr_node.left_child)
                node_queue.append(curr_node.right_child)
                # 其次判断左右孩子都是叶节点
                if curr_node.left_child.leaf_type and curr_node.right_child.leaf_type:
                    nodes_waiting_judge.append(curr_node)
            node_queue.remove(curr_node)
  1. 随后开始剪枝,先记录剪枝前的准确率,再更改当前分支节点的leaf_type属性值,将它变成叶节点。**这里要注意,test函数里判定是叶节点的依据一定是leaf_type不为空。**然后计算剪枝后的准确率,相对比,不剪就leaf_type=None;剪就清空左右孩子。还要再判断这个节点的父节点要不要加入待判断节点列表。
        while len(nodes_waiting_judge) > 0:
            curr_node = nodes_waiting_judge.pop() # 获取当前节点,同时从列表中pop出去
            # 得到剪枝前的准确率orR2值
            if self.tree_type:
                acc_before_prune = self.test_classify(test_dict_list)
            else:
                acc_before_prune = self.test_reg_R2(test_dict_list)
            node_data_set = self.split_data(curr_node.data_indexes)
            # 得到当前训练集样本对应的类别
            node_data_set_targets = []
            for item in node_data_set:
                node_data_set_targets.append(item['target'])
            # 得到剪枝后的准确率orR2值
            if self.tree_type:
                leaf_type = max(node_data_set_targets, key=node_data_set_targets.count)
                curr_node.leaf_type = leaf_type # 这里注意,在test函数中判断是否到达叶子节点使用leaf_type而不是attr_name2split_value
                acc_after_prune = self.test_classify(test_dict_list)
            else:
                mean_score = sum(node_data_set_targets) / len(node_data_set_targets)
                curr_node.leaf_type = mean_score
                acc_after_prune = self.test_reg_R2(test_dict_list)
            # 判断是否剪枝
            if acc_before_prune > acc_after_prune: # 不剪枝
                curr_node.leaf_type = None # 回退leaf_type值
            else: # 剪枝
                curr_node.attr_name2split_value = {} # 保持叶子节点的属性同步
                curr_node.left_child = None
                curr_node.right_child = None
                # 判断父节点是否也要加入待剪枝列表
                parent_node = curr_node.parent
                if parent_node.left_child.leaf_type and parent_node.right_child.leaf_type:
                    nodes_waiting_judge.append(parent_node)

一些注意的问题与方法习惯

python的三种赋值方式

grow_classify_CART的copy.deepcopy()
Python赋值的机制是名字绑定
有三张图(源自陈波老师讲义):分别是a=1、a=2、b=a的解释:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
所以对只读数据类型,

  • 若数据值不变,那么赋值操作就只是让这个数据有了不同的名字而已,不同的名字指向的内存是一样的。如a=1,b=a,a和b指向的都是1这个对象
  • 若数据值改变,那么赋值操作就会new一个新的数据对象,将名字绑定在这个新对象上。如a=1,b=a,b=2,b从指向1变为指向2
    其实对于只读类型,编程者完全可以忽略名字绑定这件事,任何赋值操作的最终结果都会跟期望一致。
    只读类型包括:int、str、float、bool、tuple、None

而对可修改数据类型list、dict、set、自定义数据类型,当他们被绑定到多个名字时,如果用这些名字修改了数据,就会导致意料之外的结果,**要非常小心!**因为不管这个list的名字是什么,都指向的是同一个list,不注意就会出错,比如函数传参的时候。

这个时候就要考虑用到深拷贝b=copy.deepcopy(a),下面是常用的三种赋值方式:

  • =赋值:仅仅给对象起了一个别名,新名字仍然指向该对象,修改old与new其实修改的都是同一个对象

  • copy浅拷贝赋值:浅拷贝,指的是重新分配一块内存,创建一个新的对象,但里面的元素是原对象中各个子对象的引用。即仅仅表层的对象是新的,内部的对象还是原对象的引用。浅拷贝对于不可变对象无伤大雅,但是如果内部存储可变对象,就容易出问题,比如嵌套的list

    • 切片属于浅拷贝。
  • deepcopy深拷贝赋值:完全new一片新的地址用来存储新对象,old和new完全分离,修改old和new无关

函数的入参类型设置–>python是传对象引用(非传值或传引用)

cart的init
问题:python的函数传参是传值还是传引用?
都不是!
传对象引用,因为Python中一切皆对象,包括数据、函数、类等。
由于上述提到的名字绑定问题:

  • 当函数入参是个只读数据类型,函数内部对入参做了修改的话,就会new一个新的对象;而外部的变量不变
  • 当函数入参是个可修改数据类型,函数内部对入参做了修改,外部的变量会跟着一起变。【尤其注意递归的时候】

一处引用
结论:python不允许程序员选择采用传值还是传引用。Python参数传递采用的肯定是“传对象引用”的方式。这种方式相当于传值和传引用的一种综合。如果函数收到的是一个可变对象(比如字典或者列表)的引用,就能修改对象的原始值--相当于通过“传引用”来传递对象。如果函数收到的是一个不可变对象(比如数字、字符或者元组)的引用,就不能直接修改原始对象--相当于通过“传值’来传递对象。

以及一个例子:
在这里插入图片描述

python中set的使用方法

set是一个无重复元素的集合,运用于两点:

  • 查找效率set>dict>>list,大规模查找时,可以用set来存
  • 可以用来做list的去重,判断list当中是否所有元素都相同
    使用方法

统计list中出现次数最多的元素

仅仅是一点记录

a = [1,2,3,4,2,3,2]
maxlabel = max(a,key=a.count)
print(maxlabel)

filter函数的使用方法

这个使用得当会非常好用,filter()函数可以对一个可迭代对象,运用给定的规则去过滤得到一个新的可迭代对象。如可以从list中过滤出给定条件的元素。

filter()接受两个参数,第一个为过滤规则(函数),第二个为可迭代对象,即列表、字典等。过滤规则一般可以用匿名函数编写。最终将其过滤结果转换为list即可。Python filter() 函数

对递归的思考

递归需要有return的地方,也就是结束递归的判断,同时递归的结果也往往是在return的时候生成的,那么return的位置就很关键
如果递归生成一棵二叉树,那么一定是要分别进入 伪叶子节点 看一看(也就是说要先调用build(left_child)),是不是真的可以return,(也就是说在这次build内部判断是否return),而不可以站在分支节点的位置去“眺望”是否是叶子节点(也就是说不能在调用build(left_child)之前,判断left_child是否就是叶子节点,要return出去。)因为这样做,不管if条件写的多么好,总是会漏掉right_child也是叶子节点的情况。
所以,要在递归函数的开头,判断完毕是否需要return,当断定是分支节点,再去构造子节点,递归子节点。

同一类别有不同标识的辨识思考

在节点类的定义中,判定分支节点和叶子节点可以有两种办法:

  • 分支节点:attr_name2split_value != {} and leaf_type == None
  • 叶子节点:attr_name2split_value == {} and leaf_type != None
    那么在后面需要判定节点类型的时候,一定要统一规则,同时同步更新两个值,如当进行剪枝,将分支节点标记为叶子节点,那么不但要更新leaf_type!=None,还要更新attr_name2split_value ={}。
    同时判定叶子节点优先使用leaf_type这个属性

其他

Sklearn提供的常用数据集
Python编程实现基于基尼指数进行划分选择的决策树(CART决策树)算法
完整代码链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值