着重总结记录实验过程(属实把决策树玩明白了…)
文章目录
重要的点
- CART是二叉树。
- 分类树中处理离散属性,父节点用它划分过,子节点就不能使用;处理连续属性则在父节点划分后,子节点仍然可以使用。
- 若使用递归建树,当判定节点为叶子时要return,此时一定搞清楚递归return的地方:必须进入子节点再return,而不能站在父节点的位置判断子节点是否是叶子就立即return。这样只会照顾到先判断的子节点而忽视了后面判断的子节点。
- 递归函数中,每个return的地方,都是生成叶子的地方。
决策树说白了就是一连串的决策,最终决策的终点自然就是结果。尤其对CART这个二叉树来说,当要分类或者预测一个样本,那么就不断从样本的属性列表中取属性,样本的该属性和树的该节点的值做比较,小于就分到左子树,大于等于就分到右子树,这个样本最终到叶子的时候,这个叶子上存储的类别或者预测值就是这个样本的结果。
至于建树过程,就是对一大堆已有结果的样本,通过对属于某个类别下的所有样本的共有的突出属性的不断提取,最后到叶子时(停止条件),叶子分到的样本集中,多数样本的类别是什么或者所有样本的均值是什么,这个结果就作为叶子存储的结果。最终从根节点开始往下递归,形成这样一颗决策树。
节点类
既然是一颗树,那还是单独定义一个节点类比较直观。
但是网上有不少大佬也用字典或者元组来定义节点,也比较方便。github上大佬的机器学习代码
首先节点应该分为两类:
- 分支节点(包括根节点):功能就是存储一个划分属性,用该划分属性的值来划分左右子树。
- 叶子节点:功能就是存储一个类别标签,当一个样本来到这个叶子节点,那么该样本对应的类别就是叶子存储的类别。
考虑节点需要的属性与方法:
- 用来划分的属性
- 用来划分的属性的值
- 节点的属性(或者说是父节点分下来的树枝。其余父节点的属性名对应)
- 左孩子
- 右孩子
- 父节点(作用不大,但是可以用来辨别根节点)
- 待选择的属性列表(在遇到离散属性作为划分属性时,使用之后就从列表中删除)
- 样本索引列表(用来得到本节点拥有的样本集合)
- 节点描述方法
#!/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类
- 类结构是这样的:
使用时可以先new一个cart类,同时选择要生成的决策树是分类树还是回归树。在CART类的初始化方法中,就根据传入的数据集等调用生成树的方法去建树了。所以当CART对象初始化结束,一颗树就建好了,可以直接调用相应的test方法去测试了。 - 首先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()
- 接下来自顶向下去描述一棵树的构造过程,这样思路会更流畅,根据顶层的需要去构建底层提供的功能。
放上流程图,重点关注三个递归return的地方,也就是生成叶子节点的地方。
CART分类树
分类树是比较容易理解的,就是对样本的一连串属性做一连串决策,就得到样本的类别。
建树过程
- 建树过程是一个递归算法,在调用递归函数之前,首先对训练集中的样本生成下标,初始化一个根节点,然后将根节点传入递归函数去建树。
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)
- 当一个节点进入
build_classify_tree()
函数,首先就判断当前节点拥有的样本集是否为空(流程图中第三个return),若空,标记该孩子节点为父节点中最多的类别(叶节点),因为实在没有数据,没办法继续判断了,只好将父样本中最多的类别当成孩子的类别。其中这个父节点中最多的类别会和叶子节点一起传递进递归函数。
# 得到当前节点的训练集
data_indexes = curr_node.data_indexes
# 判断当前节点样本集是否为空
if len(data_indexes) == 0:
curr_node.leaf_type = parent_leaf_type # 空则标记该孩子节点为父节点中最多的类别(叶节点)
return
- 接下来统计该节点拥有的样本集中的类别,得到样本最多的类是什么类。判断当前节点拥有的样本是否都是同一类别(流程图第一个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
- 对于流程图中的第二个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
- 几个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
- 现在需要生成当前节点的左右孩子,先做一些必要的节点初始化,再分别调用
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回归树
对于回归树刚开始不容易理解,决策树天然的可以进行分类,但是如何预测呢?
其实也相当于是一个分类问题,只不过叶子节点存储的不再是类别,而是现在叶子节点拥有样本集的平均分,这个平均分就可以看做是这个样本的预测值。因为根据样本的属性,做了一系列决策之后,判定这个样本和现在这个叶子节点拥有的样本最类似,那么这个未知样本的分数自然也和已有的样本均值接近。
建树过程
为了代码的可读性,便于理解,就写了另一套建回归树的代码,其实内部逻辑和分类树是一样的。
两点不同:
- 流程图中第三个return点,回归树判定当前节点拥有的样本集数量小于一个阈值时,例如2,就return。因为叶子节点样本太小了没意义。
# 判断当前节点样本集是否小于预定值
if len(data_indexes) <= self.min_samples_leaf:
curr_node.leaf_type = parent_mean_score
return
- 不再做连续和离散属性的区分,采用的策略是,父节点使用的划分属性,子节点不可以再次使用。
# 选择最优划分属性
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
波士顿房价预测测试:
后剪枝
直接生成的决策树可能会过拟合,后剪枝的也避免了预剪枝的欠拟合情况,所以使用后剪枝策略。
后剪枝就是考虑叶子节点的父节点,如果把这个父节点设为叶节点,准确度会不会提高,会提高就剪,不会提高就恢复原样。而如果确定对这个父节点剪枝,那它就变成了叶节点,再考虑这个叶节点的父节点要不要剪枝。
- 首先得到一个待判断节点列表,也就是树的倒数第二层,连着两个叶节点的分支节点。
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)
- 随后开始剪枝,先记录剪枝前的准确率,再更改当前分支节点的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决策树)算法
完整代码链接