目录
8.3 过拟合出现的原因以及我们用什么方法来防止过拟合的产生?
1 决策树
最火的两类算法莫过于神经网络算法(CNN、RNN、LSTM等)与树形算法(随机森林、GBDT、XGBoost等),树形算法的基础就是决策树。决策树因其易理解、易构建、速度快的特性,被广泛应用于统计学、数据挖掘、机器学习领域。因此,对决策树的学习,是机器学习之路必不可少的一步。
根据处理数据类型的不同,决策树又分为两类:分类决策树与回归决策树,前者输出的结果为具体的类别,可用于处理离散型数据;后者输出的结果为一个确定的数值,可用于处理连续型数据。
决策树的构建算法主要有ID3、C4.5、CART三种,其中ID3和C4.5是分类树,CART是分类回归树,其中ID3是决策树最基本的构建算法,而C4.5和CART是在ID3的基础上进行优化的算法。
决策树通常有三个步骤:特征选择、决策树的生成、决策树的修剪。
用决策树分类:从根节点开始,对实例的某一特征进行测试,根据测试结果将实例分配到其子节点,此时每个子节点对应着该特征的一个取值,如此递归的对实例进行测试并分配,直到到达叶节点,最后将实例分到叶节点的类中。
下图为决策树示意图,圆点——内部节点,方框——叶节点
- 决策树学习的目标:根据给定的训练数据集构建一个决策树模型,使它能够对实例进行正确的分类。
- 决策树学习的本质:从训练集中归纳出一组分类规则,或者说是由训练数据集估计条件概率模型。
- 决策树学习的损失函数:正则化的极大似然函数
- 决策树学习的测试:最小化损失函数
- 决策树学习的目标:在损失函数的意义下,选择最优决策树的问题。
- 决策树原理和问答猜测结果游戏相似,根据一系列数据,然后给出游戏的答案。
使用决策树做预测需要以下过程:
- 收集数据:可以使用任何方法。比如想构建一个相亲系统,我们可以从媒婆那里,或者通过参访相亲对象获取数据。根据他们考虑的因素和最终的选择结果,就可以得到一些供我们利用的数据了。
- 准备数据:收集完的数据,我们要进行整理,将这些所有收集的信息按照一定规则整理出来,并排版,方便我们进行后续处理。
- 分析数据:可以使用任何方法,决策树构造完成之后,我们可以检查决策树图形是否符合预期。
- 训练算法:这个过程也就是构造决策树,同样也可以说是决策树学习,就是构造一个决策树的数据结构。
- 测试算法:使用经验树计算错误率。当错误率达到了可接收范围,这个决策树就可以投放使用了。
- 使用算法:此步骤可以使用适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义。
2 相关概念
划分数据集的大原则是:将无序数据变得更加有序,但是各种方法都有各自的优缺点,信息论是量化处理信息的分支科学,在划分数据集前后信息发生的变化称为信息增益,获得信息增益最高的特征就是最好的选择,所以必须先学习如何计算信息增益。集合信息的度量方式称为香农熵,或者简称熵。
在划分数据集之前之后信息发生的变化成为信息增益,可以计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择。
2.1 香农熵
熵定义为信息的期望值,如果待分类的事物可能划分在多个类之中,则符号的信息定义为:
其中,是选择该分类的概率。
为了计算熵,我们需要计算所有类别所有可能值所包含的信息期望值,通过下式得到:
其中,n 为分类数目,熵越大,随机变量的不确定性就越大。
2.2 经验熵(empirical entropy)
当熵中的概率由数据估计(特别是最大似然估计)得到时,所对应的熵称为经验熵(empirical entropy)。
似然与极大似然估计:似然与极大似然估计_意念回复的博客-CSDN博客
什么叫由数据估计?比如有10个数据,一共有两个类别,A类和B类。其中有7个数据属于A类,则该A类的概率即为十分之七。其中有3个数据属于B类,则该B类的概率即为十分之三。浅显的解释就是,这概率是我们根据数据数出来的。我们定义贷款申请样本数据表中的数据为训练数据集D,则训练数据集D的经验熵为H(D),|D|表示其样本容量,即样本个数。设有K个类Ck,k = 1,2,3,···,K,|Ck|为属于类Ck的样本个数,这经验熵公式可以写为:
2.3 条件熵
在理解信息增益之前,要明确——条件熵。信息增益表示得知特征X的信息而使得类Y的信息不确定性减少的程度。
条件熵 H(Y∣X) 表示在已知随机变量X的条件下随机变量Y的不确定性:
其中,
当熵和条件熵中的概率由数据估计(特别是极大似然估计)得到时,所对应的分别为经验熵和经验条件熵,此时如果有0概率,令0log0=0 。
3 ID3 和 C4.5 决策树的生成和修剪
从数据集构造决策树算法所需要的子功能模块,包括经验熵的计算和最优特征的选择。其工作原理如下:得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据集被向下传递到树的分支的下一个结点。在这个结点上,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集。
构建决策树的算法有很多,比如C4.5、ID3和CART,这些算法在运行时并不总是在每次划分数据分组时都会消耗特征。由于特征数目并不是每次划分数据分组时都减少,因此这些算法在实际使用时可能引起一定的问题。目前我们并不需要考虑这个问题,只需要在算法开始运行前计算列的数目,查看算法是否使用了所有属性即可。
决策树生成算法递归地产生决策树,直到不能继续下去未为止。这样产生的树往往对训练数据的分类很准确,但对未知的测试数据的分类却没有那么准确,即出现过拟合现象。过拟合的原因在于学习时过多地考虑如何提高对训练数据的正确分类,从而构建出过于复杂的决策树。解决这个问题的办法是考虑决策树的复杂度,对已生成的决策树进行简化(剪枝)。
3.1 ID3 决策树的构建
3.1.1 信息增益
定义: 以某特征划分数据集前后的熵的差值。
信息增益是相对于特征而言的。所以,特征A对训练数据集D的信息增益g(D,A),定义为集合D的经验熵H(D)与特征A给定条件下D的经验条件熵H(D|A)之差,即:
=
-
一般地,熵H(D)与条件熵H(D|A)之差成为互信息(mutual information)。决策树学习中的信息增益等价于训练数据集中类与特征的互信息。
对于样本集合D,类别数为K,数据集D的经验熵表示为 :
其中是样本集合D中属于第k类的样本子集,
表示该子集的元素个数,
表示样本集合的元素个数。
某个特征A对于数据集D的经验条件熵H(D|A)为:
其中,表示D中特征A取第i个值的样本子集,
表示Di中属于第k类的样本子集。
在熵的理解那部分提到了,熵可以表示样本集合的不确定性,熵越大,样本的不确定性就越大。因此可以使用划分前后集合熵的差值来衡量使用当前特征对于样本集合D划分效果的好坏。
划分前样本集合D的熵是一定的 ,entroy(前),
使用某个特征A划分数据集D,计算划分后的数据子集的熵 entroy(后)
信息增益 = entroy(前) - entroy(后)
书中公式:
做法:计算使用所有特征划分数据集D,得到多个特征划分数据集D的信息增益,从这些信息增益中选择最大的,因而当前结点的划分特征便是使信息增益最大的划分所使用的特征。
信息增益的理解:
对于待划分的数据集D,其 entroy(前)是一定的,但是划分之后的熵 entroy(后)是不定的,entroy(后)越小说明使用此特征划分得到的子集的不确定性越小(也就是纯度越高),因此 entroy(前) - entroy(后)差异越大,说明使用当前特征划分数据集D的话,其纯度上升的更快。而我们在构建最优的决策树的时候总希望能更快速到达纯度更高的集合,这一点可以参考优化算法中的梯度下降算法,每一步沿着负梯度方法最小化损失函数的原因就是负梯度方向是函数值减小最快的方向。同理:在决策树构建的过程中我们总是希望集合往最快到达纯度更高的子集合方向发展,因此我们总是选择使得信息增益最大的特征来划分当前数据集D。
缺点:信息增益偏向取值较多的特征
原因:当特征的取值较多时,根据此特征划分更容易得到纯度更高的子集,因此划分之后的熵更低,由于划分前的熵是一定的,因此信息增益更大,因此信息增益比较 偏向取值较多的特征。
信息增益值的大小相对于训练数据集而言的,并没有绝对意义,在分类问题困难时,也就是说在训练数据集经验熵大的时候,信息增益值会偏大,反之信息增益值会偏小,使用信息增益比可以对这个问题进行校正,这是特征选择的另一个标准。
3.1.2 ID3构建决策树
ID3算法的核心是在决策树各个结点上对应信息增益准则选择特征,递归地构建决策树。
ID3相当于用极大似然法进行概率模型的选择,算法步骤:
例如:
特征A3(有自己的房子)的信息增益最大,所以选择A3为根节点的特征,它将训练集D划分为两个子集D1(A3取值为“是”)D2(A3取值为“否”)。由于D1只有同一类的样本点,所以它成为一个叶结点,结点的类标记为“是”。对D2则需要从特征A1(年龄),A2(有工作)和A4(信贷情况)中选择新的特征,计算各个特征的信息增益:
根据计算,选择信息增益最大的A2作为节点的特征,由于其有两个取值可能,所以引出两个子节点:
① 对应“是”(有工作),包含三个样本,属于同一类,所以是一个叶子节点,类标记为“是”
② 对应“否”(无工作),包含六个样本,输入同一类,所以是一个叶子节点,类标记为“否”
这样就生成一个决策树,该树只用了两个特征(有两个内部节点),生成的决策树如下图所示:
3.1.3 ID3算法的不足
- ID3没有考虑连续特征,比如长度,密度都是连续值,无法在ID3运用。这大大限制了ID3的用途。
- ID3采用信息增益大的特征优先建立决策树的节点。很快就被人发现,在相同条件下,取值比较多的特征比取值少的特征信息增益大。
- ID3算法对于缺失值的情况没有做考虑
- 没有考虑过拟合的问题
3.2 C4.5 决策树的构建
3.2.1 信息增益比
与ID3算法相似,但是做了改进,将信息增益比作为选择特征的标准。
信息增益比 = 惩罚参数 * 信息增益
注意:其中的,对于样本集合D,将当前特征A作为随机变量(取值是特征A的各个特征值),求得的经验熵。
(之前是把集合类别作为随机变量,现在把某个特征作为随机变量,按照此特征的特征取值对集合D进行划分,计算熵)
数据集D关于A的取值熵:
信息增益比本质: 是在信息增益的基础之上乘上一个惩罚参数。特征个数较多时,惩罚参数较小;特征个数较少时,惩罚参数较大。
惩罚参数:数据集D以特征A作为随机变量的熵的倒数,即:将特征A取值相同的样本划分到同一个子集中(之前所说数据集的熵是依据类别进行划分的)
缺点:信息增益比偏向取值较少的特征
原因: 当特征取值较少时的值较小,因此其倒数较大,因而信息增益比较大。因而偏向取值较少的特征。
使用信息增益比:基于以上缺点,并不是直接选择信息增益率最大的特征,而是现在候选特征中找出信息增益高于平均水平的特征,然后在这些特征中再选择信息增益率最高的特征。
因此,C4.5寻找最优划分的真实方法是:1)计算所有划分的平均信息增益t;2)从所有信息增益大于t的划分中寻找信息增益率最大的划分,该划分就是最优划分。
3.2.2 C4.5如何处理连续值
C4.5既可以处理离散型属性,也可以处理连续性属性。在选择某节点上的分枝属性时,对于离散型描述属性,C4.5的处理方法与ID3相同。
对离散分布、且取值数目>=3的特征的处理:
C4.5决策树可以支持多叉树的形式,因此对于数目大于等于3的离散特征,可以采用多分叉的形式
对于连续分布的特征,其处理方法是:
先把连续属性转换为离散属性再进行处理。虽然本质上属性的取值是连续的,但对于有限的采样数据它是离散的,如果有N条样本,那么我们有N-1种离散化的方法:<=vj的分到左子树,>vj的分到右子树。计算这N-1种情况下最大的信息增益率。另外,对于连续属性先进行排序(升序),只有在决策属性(即分类发生了变化)发生改变的地方才需要切开,这可以显著减少运算量。经证明,在决定连续特征的分界点时采用增益这个指标(因为若采用增益率,splitted info影响分裂点信息度量准确性,若某分界点恰好将连续特征分成数目相等的两部分时其抑制作用最大),而选择属性的时候才使用增益率这个指标能选择出最佳分类特征
对连续属性的处理如下:
1. 对特征的取值进行升序排序
2. 两个特征取值之间的中点作为可能的分裂点,将数据集分成两部分,计算每个可能的分裂点的信息增益(InforGain)。优化算法就是只计算分类属性发生改变的那些特征取值。
3. 选择修正后信息增益(InforGain)最大的分裂点作为该特征的最佳分裂点
4. 计算最佳分裂点的信息增益率(Gain Ratio)作为特征的Gain Ratio。注意,此处需对最佳分裂点的信息增益进行修正:减去log2(N-1)/|D|(N是连续特征的取值个数,D是训练数据数目,此修正的原因在于:当离散属性和连续属性并存时,C4.5算法倾向于选择连续特征做最佳树分裂点)
3.2.3 C4.5构建决策树
递归构建决策树:
从数据集构造决策树算法所需的子功能模块工作原理如下:得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分,第一次划分之后,数据将被向下传递到树分支的下一个节点,在此节点在此划分数据,因此可以使用递归的原则处理数据集。
递归结束的条件是:
程序完全遍历所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类,如果所有实例具有相同的分类,则得到一个叶子节点或者终止块,任何到达叶子节点的数据必然属于叶子节点的分类。
3.3 python实现
# -*- coding: utf-8 -*-
import operator
from math import log
from sklearn.model_selection import train_test_split
class ID3:
# 计算信息熵
def cal_entropy(self, feature, target):
num = len(feature)
label_count = {}
for current_label in target:
if current_label not in label_count.keys():
label_count[current_label] = 0
label_count[current_label] += 1
entropy = 0
for key in label_count:
prob = float(label_count[key] / num)
entropy = entropy - prob * log(prob, 2)
return entropy
# 划分数据集
def split_dataset(self, feature, target, axis, value):
'''
划分数据集
:param feature、target: 数据集
:param axis: 按第几个属性划分
:param value: 要返回的子集对应的属性值
:return:
'''
sub_feature = []
sub_target = []
for i in range(len(feature)):
except_feature = []
if feature[i][axis] == value:
# 生成新的训练集,测试集不变
except_feature.extend(feature[i][:axis])
except_feature.extend(feature[i][axis + 1:])
sub_feature.append(except_feature)
sub_target.append(target[i])
return sub_feature, sub_target
# 选择最好的数据集划分方式
def choose_best_feature(self, feature, target):
num_feature = len(feature[0])
num_data = len(feature)
base_entropy = self.cal_entropy(feature, target)
best_info_gain = 0.0
best_feature = -1
for i in range(num_feature):
feature_list = [line[i] for line in feature] # 该特征都有哪些取值
feature_list = set(feature_list)
new_entropy = 0
for feature_value in feature_list:
sub_feature, sub_target = self.split_dataset(
feature, target, i, feature_value)
prob = len(sub_feature) / num_data
new_entropy += prob * self.cal_entropy(sub_feature, sub_target)
info_gain = base_entropy - new_entropy # 信息增益
# ID3
if info_gain > best_info_gain: # ID3算法选择信息增益最大的属性
best_info_gain = info_gain
best_feature = i
# C4.5 算法
# gain_ratio = info_gain / base_entropy # 信息增益比
# if gain_ratio > best_info_gain: # C4.5 算法选择信息增益比最大的属性
# best_info_gain = gain_ratio
# best_feature = i
return best_feature
def majority(self, target):
target_count = {}
for tar in target:
if tar not in target_count.keys():
target_count[tar] = 0
target_count[tar] = target_count[tar] + 1
sorted_target_count = sorted(
target_count,
key=lambda x: x[1],
reverse=True) # key使用lambda匿名函数取value进行排序
# sorted_target_count = sorted(target_count,
# key=operator.itemgetter(1), reverse=True) # 使用operator的itemgetter进行排序
return sorted_target_count[0][0]
def creat_tree(self, feature, target, label):
uniq_target = list(set(target))
if len(uniq_target) == 1:
return uniq_target
if len(feature[0]) == 1:
return self.majority(target)
best_feature_num = self.choose_best_feature(
feature, target) # 选择最好的数据集划分方式的特征索引
best_feature = label[best_feature_num]
# print(best_feature)
tree = {best_feature: {}}
# print(tree)
del (label[best_feature_num]) # 已经选择的特征不再参与分类
feature_list = [line[best_feature_num]
for line in feature] # 该特征都有哪些取值
feature_list = set(feature_list) # 节点的分支,去重
for feature_value in feature_list: # 对每个分支,递归构建树
sub_labels = label[:]
sub_feature, sub_target = self.split_dataset(
feature, target, best_feature_num, feature_value)
tree[best_feature][feature_value] = self.creat_tree(
sub_feature, sub_target, sub_labels)
return tree
def predict(self, tree, test_line, label):
'''
进行预测
:param tree: 训练得到的树
:param test_feature: 训练集
:param label: 属性标签
:return:
'''
first_feature = None
for key in tree.keys():
first_feature = key # 根节点
feature_num = label.index(first_feature) # 根节点的属性名称在label中所对应的索引
sec_tree = tree[first_feature]
class_label = None
for key in sec_tree.keys():
if test_line[feature_num] == key:
if type(sec_tree[key]).__name__ == 'dict':
class_label = self.predict(sec_tree[key], test_line, label)
else:
class_label = sec_tree[key]
return class_label
def evalution(predict_result, test_target):
tp = 0
tn = 0
fp = 0
fn = 0
for i in range(len(test_target)):
if predict_result[i] == '是' and test_target[i] == '是':
tp += 1
elif predict_result[i] == '否' and test_target[i] == '否':
tn += 1
elif predict_result[i] == '是' and test_target[i] == '否':
fp += 1
elif predict_result[i] == '否' and test_target[i] == '是':
fn += 1
if tp + tn + fp + fn:
accuracy = (tp + tn) / (tp + tn + fp + fn)
else:
accuracy = 0
if tp + fn:
recall = (tp) / (tp + fn)
else:
recall = 0
if tp + fp:
precision = (tp) / (tp + fp)
else:
precision = 0
return accuracy, recall, precision
if __name__ == "__main__":
# 读取文件中的数据
with open('watermelon', 'r', encoding='UTF-8') as f:
# print(f.read())
data_all = []
for line in f.readlines():
each_line = line.strip().split(',')
data_all.append(each_line)
label = data_all[0][:-1]
dataset = data_all[1:]
feature_data = []
target_data = []
for line in data_all[1:]:
feature_data.append(line[:-1])
target_data.append(line[-1])
train_feature, test_feature, train_target, test_target = train_test_split(
feature_data, target_data, test_size=0.3)
# print(train_feature, test_feature, train_target, test_target, sep='\n')
tree = ID3()
# print(label)
mytree = tree.creat_tree(train_feature, train_target, label)
label = data_all[0][:-1]
print("mytree: ", mytree)
predict_result = []
for test_line in test_feature:
class_label = tree.predict(mytree, test_line, label)
if class_label is not None:
predict_result.append(class_label[0])
else:
predict_result.append(class_label)
print("predict_result: ", predict_result)
print("test_target: ", test_target)
accuracy, recall, precision = evalution(predict_result, test_target)
print(
"accuracy:{:.4f};recall:{:.4f};precision:{:.4f}".format(
accuracy,
recall,
precision))
3.4 ID3和C4.5决策树的剪枝
决策树生成算法递归地产生决策树,直到不能继续下去为止。这样产生的树往往对训练数据的分类很准确,但对未知的测试数据的分类却没有那么准确,即出现过拟合现象。过拟合的原因在于学习时过多地考虑如何提高对训练数据的正确分类,从而构建出过于复杂的决策树。解决这个问题的办法是考虑决策树的复杂度,对已生成的决策树进行简化(剪枝)。
剪枝(pruning):从已经生成的树上裁掉一些子树或叶节点,并将其根节点或父节点作为新的叶子节点,从而简化分类树模型。
实现方式:极小化决策树整体的损失函数或代价函数来实现
决策树学习的损失函数定义为:
其中:
,叶结点t有
个样本点。
将损失函数右端的第1项记作:
这时有:
其中:
|T|表示模型复杂度,为树的叶节点个数。
剪枝就是当α 确定时,选择损失函数最小的模型,即损失函数最小的子树。
- 当 α 值确定时,子树越大,往往与训练数据的拟合越好,但是模型的复杂度越高;
- 子树越小,模型的复杂度就越低,但是往往与训练数据的拟合不好
- 损失函数正好表示了对两者的平衡。
损失函数认为对于每个分类终点(叶子节点)的不确定性程度就是分类的损失因子;而叶子节点的个数是模型的复杂程度,作为惩罚项。损失函数的第一项是样本的训练误差,第二项是模型的复杂度。如果一棵子树的损失函数值越大,说明这棵子树越差,因此我们希望让每一棵子树的损失函数值尽可能得小,损失函数最小化就是用正则化的极大似然估计进行模型选择的过程。
决策树的剪枝过程(泛化过程)就是从叶子节点开始递归,记其父节点将所有子节点回缩后的子树为(分类值取类别比例最大的特征值),未回缩的子树为
,其对应的损失函数值分别是
,如果
说明回缩后使得损失函数减小了,那么应该使这棵子树回缩,递归直到无法回缩为止,这样使用“贪心”的思想进行剪枝可以降低损失函数值,也使决策树得到泛化。
可以看出,决策树的生成只是考虑通过提高信息增益(或信息增益比)对训练数据进行更好的拟合,而决策树剪枝通过优化损失函数还考虑了减小模型复杂度。决策树生成学习局部的模型,而决策树剪枝学习整体的模型。
公式定义的损失函数的极小化等价于正则化的极大似然估计,剪枝过程示意图:
剪枝算法:
4 CART算法
CART算法决策树的生成和剪枝见《统计学习方法》(P83):
决策树的生成就是递归地构建二叉决策树的过程。
- 对回归树用平方误差最小化准则,
- 对分类树用基尼指数最小化准则,
进行特征选择,生成二叉树。
CART,又名分类回归树,是在ID3的基础上进行优化的决策树,为一种二分决策树,是一种树构建算法,这种算法既可以处理离散型的问题,也可以处理连续型的问题。
与ID3算法相反,CART算法正好适用于连续型特征。CART算法使用二元切分法来处理连续型变量。而使用二元切分法则易于对树构建过程进行调整以处理连续型特征。具体的处理方法是:如果特征值大于给定值就走左子树,否则就走右子树。
CART算法有两步:
- 决策树生成:递归地构建二叉决策树的过程,基于训练数据集生成决策树,生成的决策树要尽量大;自上而下从根开始建立节点,在每个节点处要选择一个最好的属性来分裂,使得子节点中的训练集尽量的纯。不同的算法使用不同的指标来定义"最好":
- 决策树剪枝:用验证数据集对已生成的树进行剪枝并选择最优子树,这时损失函数最小作为剪枝的标准。
学习CART记住以下几个关键点:
(1)CART算法既可以用于创建分类树(Classification Tree),也可以用于创建回归树(Regression Tree)、模型树(Model Tree),两者在建树的过程稍有差异;
(2)当CART是分类树时,采用GINI值作为节点分裂的依据;当CART是回归树时,采用样本的最小方差作为节点分裂的依据;
(3)CART是一棵二叉树。
(4)剪枝策略:CART算法的关键点,也是整个Tree-Based算法的关键步骤。
剪枝过程特别重要,所以在最优决策树生成过程中占有重要地位。有研究表明,剪枝过程的重要性要比树生成过程更为重要,对于不同的划分标准生成的最大树(Maximum Tree),在剪枝之后都能够保留最重要的属性划分,差别不大。反而是剪枝方法对于最优树的生成更为关键。
- 创建分类树递归过程中,CART每次都选择当前数据集中具有最小Gini信息增益的特征作为结点划分决策树。ID3算法和C4.5算法虽然在对训练样本集的学习中可以尽可能多地挖掘信息,但其生成的决策树分支、规模较大,CART算法的二分法可以简化决策树的规模,提高生成决策树的效率。对于连续特征,CART也是采取和C4.5同样的方法处理。为了避免过拟合(Overfitting),CART决策树需要剪枝。预测过程当然也就十分简单,根据产生的决策树模型,延伸匹配特征值到最后的叶子节点即得到预测的类别。
- 创建回归树时,观察值取值是连续的、没有分类标签,只有根据观察数据得出的值来创建一个预测的规则。在这种情况下,Classification Tree的最优划分规则就无能为力,CART则使用最小剩余方差(Squared Residuals Minimization)来决定Regression Tree的最优划分,该划分准则是期望划分之后的子树误差方差最小。创建模型树,每个叶子节点则是一个机器学习模型,如线性回归模型。
不考虑剪枝情况下,分类决策树递归创建过程中就是每次选择Gain最小的节点做分叉点,直至子数据集都属于同一类或者所有特征用光了。
(一) 作为分类树
基尼指数(Gini) :
(1)离散属性
对于离散属性,可能会出现属性取值数N>=3的情况,因为CART是二叉树,此时需要考虑将N>=3个取值的离散特征的处理时也只能有两个分支,这就要通过组合人为的创建二取值序列并取Gini最小者作为树分叉决策点。如某特征值具有[‘young’,’middle’,’old’]三个取值,那么二分序列会有如下 3 种可能性:[((‘young’,), (‘middle’, ‘old’)), ((‘middle’,), (‘young’, ‘old’)), ((‘old’,), (‘young’, ‘middle’))]。 采用CART算法,就需要分别计算按照上述List中的二分序列做分叉时的Gini指数,然后选取产生最小的Gain的二分序列做该特征的分叉二值序列参与树构建的递归。因此,CART不适用于离散特征有多个取值的场景。
(2)连续属性
对于连续属性的处理,类似于C4.5,区别在于CART算法中要以Gini最小作为分界点选取标准。是否需要修正?处理过程为:
先把连续属性转换为离散属性再进行处理。虽然本质上属性的取值是连续的,但对于有限的采样数据它是离散的,如果有N条样本,那么我们有N-1种离散化的方法:<=vj的分到左子树,>vj的分到右子树。计算这N-1种情况下最大的信息增益率。另外,对于连续属性先进行排序(升序),只有在决策属性(即分类发生了变化)发生改变的地方才需要切开,这可以显著减少运算量。
(1) 对特征的取值进行升序排序
(2) 两个特征取值之间的中点作为可能的分裂点,将数据集分成两部分,计算每个可能的分裂点的Gini。优化算法就是只计算分类属性发生改变的那些特征取值。
(3)选择Gini最小的分裂点作为该特征的最佳分裂点(注意,若修正则此处需对最佳分裂点的Gini 减去log2(N-1)/|D|(N是连续特征的取值个数,D是训练数据数目)
必须注意的是:根据离散特征分支划分数据集时,子数据集中不再包含该特征(因为每个分支下的子数据集该特征的取值就会是一样的,信息增益或者Gini将不再变化,这也是C4.5等决策树离散型特征不会被重复选择为节点分裂的属性);而根据连续特征分支时,各分支下的子数据集必须依旧包含该特征(当然,左右分支各包含的分别是取值小于、大于等于分裂值的子数据集),因为该连续特征再接下来的树分支过程中可能依旧起着决定性作用。
(二) 作为回归树
当数据拥有众多特征并且特征之间关系十分复杂时,构建全局模型的想法就显得太难了,也略显笨拙。而且,实际生活中很多问题都是非线性的,不可能使用全局线性模型来拟合任何数据。一种可行的方法是将数据集切分成很多份易建模的数据,然后利用线性回归技术来建模。如果首次切分后仍然难以拟合线性模型就继续切分。在这种切分方式下,树结构和回归法就相当有用。
回归树要求观察属性是连续类型,由于节点分裂选择特征属性时通常使用最小绝对偏差(LAD)或者最小二乘偏差(LSD)法,因此通常特征属性也是连续类型。
以最小绝对偏差(LAD)为例
(1) 先令最佳方差为无限大bestVar=inf。
(2) 依次计算根据某特征(FeatureCount次迭代)划分数据后的总方差currentVar(,计算方法为:划分后左右子数据集的总方差之和),如果currentVar<bestVar
,bestVar=currentVar。
(3) 返回最佳分支特征、分支特征值(离散特征则为二分序列、连续特征则为分裂点的值),左右分支子数据集。
(三)作为模型树
用树来对数据建模,除了把叶节点简单地设定为常数值之外,还有一种方法是把叶节点设定为分段线性函数,这里所谓的分段线性(piecewise linear)是指模型由多个线性片段组成,这就是模型树。模型树的可解释性是它优于回归树的特点之一。另外,模型树也具有更髙的预测准确度。
模型树的创建过程大体上与回归树是一样的,区别就在于递归过程中最佳分支特征选取时差值的计算。对于模型树:给定的数据集先用线性的模型来对它进行拟合,然后计算真实的目标值与模型预测值间的差值,将这些差值的平方求和就得到了所需的总差值,最后依然选取总差值最小的特征做分支特征。至于线性回归采用哪种解法,就要参看线性回归模型的求解了。
4.1 CART 回归树生成
4.2 CART 分类树生成
示例:
上图可一目了然的查看具体的输出规则,如根节点有258个观测,其中Middle有88个,当PEG>=0.68时,节点内有143个观测,其中Middle有78个,当PEG>=0.12且PEG<0.34时,节点内有115个观察,其中Low有81个,以此类推还可以得出其他规则。
4.3 CART 剪枝
CART的剪枝和ID3/C4.5的剪枝对比进行理解:二者关键的区别就在于 上面。
可以仔细看李航的《统计学习方法》里两种类型剪枝的步骤里面,ID3/C4.5的剪枝 是作为参数输入的,也就是说这里的
是由人来确定的;CART则不是。
在ID3/C4.5中,是在人给定的 作为前提条件下,从叶结点开始往根结点方向回溯,如果在回溯的过程中,下级的结点相比上级的结点损失函数反而增加了,说明下级没有存在的必要,于是就被剪掉了。
而在CART中多了一个遍历 的步骤,按理说
的取值是从0到正无穷的,是遍历不完的。但是决策树是有限的,因为数据中的特征是有限的,就算再多也是有限个数的。而实际上,每一个子树对应的不是一个
值,而是一个
的区间。其实现在回过头来看,我们其实并不是想要遍历某个参数
,我们想要遍历的是决策树的各个方案而已。
具体的:
(1)第一步:剪枝
上述步骤中,alpha大于g(t)该剪,剪了会使整体损失函数减小;alpha小于g(t)不该剪,剪了会使整体损失函数增大。这个临界点的 叫做误差增益。看看它的分子就会明白了:分子是由剪枝前后模型的拟合程度的差值构成的,也就是说,
的大小和剪枝前后模型的拟合能力的变化决定的,由于剪枝的关系,模型在样本内的拟合能力通常通常是减弱的,所以才叫做"误差"增益值。
所以,对于那个完全长成的树 中的每一个结点
,我们都要算一算它的误差增益是多少,当然拟合能力减少最小的那个剪枝方案
就是我们最需要的。
然后我们继续增加 ,继续计算误差增益,继续找那个最优的子树,最终会找出一个子树序列来:
,最后交叉验证一下就好啦。
(2)第二步,交叉验证得到最优子树
按照:
(3)总结
剪枝过程特别重要,所以在最优决策树生成过程中占有重要地位。有研究表明,剪枝过程的重要性要比树生成过程更为重要,对于不同的划分标准生成的最大树(Maximum Tree),在剪枝之后都能够保留最重要的属性划分,差别不大。反而是剪枝方法对于最优树的生成更为关键。
如何剪枝?(本文第五章)
CART采用CCP(代价复杂度)剪枝方法。代价复杂度选择节点表面误差率增益值最小的非叶子节点,删除该非叶子节点的左右子节点,若有多个非叶子节点的表面误差率增益值相同小,则选择非叶子节点中子节点数最多的非叶子节点进行剪枝。
可描述如下:
令决策树的非叶子节点为。
a)计算所有非叶子节点的表面误差率增益值
b)选择表面误差率增益值最小的非叶子节点
(若多个非叶子节点具有相同小的表面误差率增益值,选择节点数最多的非叶子节点)。
c)对进行剪枝
表面误差率增益值的计算公式:
其中:
表示叶子节点的误差代价,
,
为节点的错误率,
为节点数据量的占比;
表示子树的误差代价,
,
为子节点i的错误率,
表示节点i的数据节点占比;
表示子树节点个数。
4.4 CART剪枝算例
如图所示:这是一个使用CART训练后生成的未剪枝的决策树。共有三个类别,setosa, virginica, versicolor,各有50个样本。
内部结点和叶子结点分别用两种颜色标注,并标注序号。
- 内部结点用橘色
- 叶子结点用紫色
计算:
1. 计算内部结点 I(1) 的g(t)
C(t) = 2 * (1/3) * (1-1/3) = 0.444
C(T) = 1/3 * 0 + 2/3 * 0 = 0
叶子结点的基尼指数都是0,所以这个值很容易求,分子关键是C(t)的大小:
2. 计算 I(3) 的 g(t)
3.计算I(5)
4.计算I(6)
这里就不一一算了,主要是计算方法,算法最终会选择最小的g(t)作为剪枝的结点。
本次计算应该是内部结点5作为剪枝对象。内部结点5以下的几点都被剪掉。
从直观的角度:
内部结点5涵盖了训练集上46个样本,其中有45个是birginica,一个是versicolor。基尼指数已经很低了(0.043),不应该在分裂了,但是为了达到纯度为0,训练集上的准确率100%。有点过拟合的嫌疑,所以选择这个结点进行剪枝是正确的。
同理:内部结点3是应该被剪枝的。
cart决策树剪枝的个人理解_wqtltm的博客-CSDN博客_cart决策树剪枝
5 如何对决策树进行剪枝? 《百面机器学习》
一棵完全生长的决策树会面临一个很严重的问题,即过拟合。假设我们真的需要考虑DNA特征,由于每个人的DNA都不同,完全生长的决策树所对应的每个叶结点中只会包含一个样本,这就导致决策树是过拟合的。用它进行预测时,在测试集上的效果将会很差。因此我们需要对决策树进行剪枝,剪掉一些枝叶,提升模型的泛化能力。
决策树的剪枝通常有两种方法,预剪枝(Pre-Pruning)和后剪枝(Post-Pruning)。那么这两种方法是如何进行的呢?它们又各有什么优缺点?
预剪枝,即在生成决策树的过程中提前停止树的增长。而后剪枝,是在已生成的过拟合决策树上进行剪枝,得到简化版的剪枝决策树。
5.1 预剪枝(Pre-Pruning)
预剪枝的核心思想是在树中结点进行扩展之前,先计算当前的划分是否能带来模型泛化能力的提升,如果不能,则不再继续生长子树。此时可能存在不同类别的样本同时存于结点中,按照多数投票的原则判断该结点所属类别。预剪枝对于何时停止决策树的生长有以下几种方法。
- 当树到达一定深度的时候,停止树的生长。
- 当到达当前结点的样本数量小于某个阈值的时候,停止树的生长。
- 计算每次分裂对测试集的准确度提升,当小于某个阈值的时候,不再继续扩展。
预剪枝具有思想直接、算法简单、效率高等特点,适合解决大规模问题。但如何准确地估计何时停止树的生长(即上述方法中的深度或阈值),针对不同问题会有很大差别,需要一定经验判断。且预剪枝存在一定局限性,有欠拟合的风险,虽然当前的划分会导致测试集准确率降低,但在之后的划分中,准确率可能会有显著上升。
5.2 后剪枝
后剪枝的核心思想是让算法生成一棵完全生长的决策树,然后从最底层向上计算是否剪枝。剪枝过程将子树删除,用一个叶子结点替代,该结点的类别同样按照多数投票的原则进行判断。同样地,后剪枝也可以通过在测试集上的准确率进行判断,如果剪枝过后准确率有所提升,则进行剪枝。相比于预剪枝,后剪枝 方法通常可以得到泛化能力更强的决策树,但时间开销会更大。
常见的后剪枝方法包括错误率降低剪枝(Reduced Error Pruning,REP)、悲观剪枝(Pessimistic Error Pruning,PEP)、代价复杂度剪枝(Cost Complexity Pruning,CCP)、最小误差剪枝(Minimum Error Pruning,MEP)、CVP(Critical Value Pruning)、OPP(Optimal Pruning)等方法,这些剪枝方法各有利弊,关注 不同的优化角度,本文选取著名的CART剪枝方法CCP进行介绍。
代价复杂剪枝主要包含以下两个步骤。
- 从完整决策树
开始,生成一个子树序列
,其中
由
生成,
为树的根结点。
- 在子树序列中,根据真实误差选择最佳的决策树。
步骤(1)从开始,裁剪
中关于训练数据集合误差增加最小的分支以得到
。具体地,当一棵树T在结点 t 处剪枝时,它的误差增加可以用
表示,其中R(t)表示进行剪枝之后的该结点误差,
表示未进行剪枝时子树Tt的误差。考虑到树的复杂性因素,我们用
表示子树Tt的叶子结点个数,则树在结点 t 处剪枝后的误差增加率为
(3.25)
在得到后,我们每步选择α最小的结点进行相应剪枝。
示例:
5.3 预剪枝和后剪枝的优缺点比较
时间成本方面,预剪枝在训练过程中即进行剪枝,后剪枝要在决策树完全生长后自底向上逐一考察。显然,后剪枝训练时间更长。预剪枝更适合解决大规模问题。
剪枝的效果上,预剪枝的常用方法本质上是基于贪心的思想,但贪心法却可能导致欠拟合,后剪枝的欠拟合风险很小,泛化性能更高。
另外,预剪枝的有些方法使用了阈值,如何设置一个合理的阈值也是一项挑战。
6 生成树时停止分裂的条件
(1)最小节点数
当节点的数据量小于一个指定的数量时,不继续分裂。两个原因:一是数据量较少时,再做分裂容易强化噪声数据的作用;二是降低树生长的复杂性。提前结束分裂一定程度上有利于降低过拟合的影响。
(2)熵或者基尼值小于阀值。
由上述可知,熵和基尼值的大小表示数据的复杂程度,当熵或者基尼值过小时,表示数据的纯度比较大,如果熵或者基尼值小于一定程度时,节点停止分裂。
(3)决策树的深度达到指定的条件
节点的深度可以理解为节点与决策树跟节点的距离,如根节点的子节点的深度为1,因为这些节点与跟节点的距离为1,子节点的深度要比父节点的深度大1。决策树的深度是所有叶子节点的最大深度,当深度到达指定的上限大小时,停止分裂。
(4)所有特征已经使用完毕,不能继续进行分裂。
被动式停止分裂的条件,当已经没有可分的属性时,直接将当前节点设置为叶子节点。
7 ID3,C4.5,CART之间的差异
首先,ID3是采用信息增益作为评价标准,会倾向于取值较多的特征。因为,信息增益反映的是给定条件以后不确定性减少的程度,特征取值越多就意味着确定性更高,也就是条件熵越小,信息增益越大。这在实际应用中是一个缺陷。比如,我们引入特征“DNA”,每个人的DNA都不同,如果ID3按照“DNA”特征进行划分一定是最优的(条件熵为0),但这种分类的泛化能力是非常弱的。因此,C4.5实际上是对ID3进行优化,通过引入信息增益比,一定程度上对取值比较多的特征进行惩罚,避免ID3出现过拟合的特性,提升决策树的泛化能力。
其次,从样本类型的角度,ID3只能处理离散型变量,而C4.5和CART都可以处理连续型变量。C4.5处理连续型变量时,通过对数据排序之后找到类别不同的分割线作为切分点,根据切分点把连续属性转换为布尔型,从而将连续型变量转换多个取值区间的离散型变量。而对于CART,由于其构建时每次都会对特征进行二值划分,因此可以很好地适用于连续性变量。
从应用角度,ID3和C4.5只能用于分类任务,而CART(Classification and Regression Tree,分类回归树)从名字就可以看出其不仅可以用于分类,也可以应用于回归任务(回归树使用最小平方误差准则)。
此外,从实现细节、优化过程等角度,这三种决策树还有一些不同。比如:
ID3对样本特征缺失值比较敏感,而C4.5和CART可以对缺失值进行不同方式的处理;
ID3和C4.5可以在每个结点上产生出多叉分支,且每个特征在层级之间不会复用,而CART每个结点只会产生两个分支,因此最后会形成一颗二叉树,且每个特征可以被重复使用;I
D3和C4.5通过剪枝来权衡树的准确性与泛化能力,而CART直接利用全部数据发现所有可能的树结构进行对比。
至此,我们从构造、应用、实现等角度对比了ID3、C4.5、CART这三种经典的决策树模型。这些区别与联系总结起来容易,但在实际应用中还需要读者慢慢体会,针对不同场景灵活变通。
8 常见问题
机器学习面试题之决策树(三)_jaffe_wei的博客-CSDN博客_决策树 面试题
8.1 决策树需要进行归一化处理吗?
概率模型不需要归一化,因为他们不关心变量的值,而是关心变量的分布和变量之间的条件概率。决策树是一种概率模型,数值缩放,不影响分裂点位置。所以一般不对其进行归一化处理。
- 数值缩放不影响分裂点位置,对树模型的结构不造成影响。
- 按照特征值进行排序的,排序的顺序不变,那么所属的分支以及分裂点就不会有不同。
- 树模型是不能进行梯度下降的,因为构建树模型(回归树)寻找最优点时是通过寻找最优分裂点完成的,因此树模型是阶跃的,阶跃点是不可导的,并且求导没意义,也就不需要归一化。
8.2 缺失值处理
决策树(decision tree)(四)——缺失值处理_天泽28的博客-CSDN博客_决策树缺失值的处理
如果测试样本属性也有缺失值那要怎么办?
- 如果有专门处理缺失值的分支,就走这个分支。
- 用这篇论文提到的方法来确定属性a的最有可能取值,然后走相应的分支。
- 从属性a最常用的分支走
- 同时探查所有的分支,并组合他们的结果来得到类别对应的概率,(取概率最大的类别)
- 将最有可能的类别赋给该样本。
8.3 过拟合出现的原因以及我们用什么方法来防止过拟合的产生?
对训练数据预测效果很好,但是测试数据预测效果较差,则称出现了过拟合现象。对于过拟合现象产生的原因,有以下几个方面,
- 第一:在决策树构建的过程中,对决策树的生长没有进行合理的限制(剪枝);
- 第二:在建模过程中使用了较多的输出变量,变量较多也容易产生过拟合;
- 第三:样本中有一些噪声数据,噪声数据对决策树的构建的干扰很多,没有对噪声数据进行有效的剔除。
对于过拟合现象的预防措施,有以下一些方法,
- 第一:选择合理的参数进行剪枝,可以分为预剪枝后剪枝,我们一般用后剪枝的方法来做;
- 第二:K-folds交叉验证,将训练集分为K份,然后进行K次的交叉验证,每次使用K-1份作为训练样本数据集,另外的一份作为测试集合(作者说反了,应该是份作为测试集,其余k-1份作为训练集);
- 第三:减少特征,计算每一个特征和响应变量的相关性,常见的为皮尔逊相关系数,将相关性较小的变量剔除,当然还有一些其他的方法来进行特征筛选,比如基于决策树的特征筛选,通过正则化的方式来进行特征选取等。
8.4 决策树算法和逻辑回归算法之间的区别?
- 对于拥有缺失值的数据,决策树可以应对,而逻辑回归需要挖掘人员预先对缺失数据进行处理;
- 逻辑回归对数据整体结构的分析优于决策树,而决策树对局部结构的分析优于逻辑回归;(决策树由于采用分割的方法,所以能够深入数据内部,但同时失去了对全局的把握。一个分层一旦形成,它和别的层面或节点的关系就被切断了,以后的挖掘只能在局部中进行。同时由于切分,样本数量不断萎缩,所以无法支持对多变量的同时检验。而逻辑回归,始终着眼整个数据的拟合,所以对全局把握较好。但无法兼顾局部数据,或者说缺乏探查局部结构的内在机制。)
- 逻辑回归擅长分析线性关系,而决策树对线性关系的把握较差。线性关系在实践中有很多优点:简洁,易理解,可以在一定程度上防止对数据的过度拟合。决策树对非线性问题的把握较好。(我自己对线性的理解:1,逻辑回归应用的是样本数据线性可分的场景,输出结果是概率,即,输出结果和样本数据之间不存在直接的线性关系;2,线性回归应用的是样本数据和输出结果之间存在线性关系的场景,即,自变量和因变量之间存在线性关系。)
- 逻辑回归对极值比较敏感,容易受极端值的影响,而决策树在这方面表现较好。
- 应用上的区别:决策树的结果和逻辑回归相比略显粗糙。逻辑回归原则上可以提供数据中每个观察点的概率,而决策树只能把挖掘对象分为有限的概率组群。比如决策树确定17个节点,全部数据就只能有17个概率,在应用上受到一定限制。就操作来说,决策树比较容易上手,需要的数据预处理较少,而逻辑回归则要去一定的训练和技巧。
- 执行速度上:当数据量很大的时候,逻辑回归的执行速度非常慢,而决策树的运行速度明显快于逻辑回归。
8.5 决策树的优缺点
优点:
- 决策树可以可视化,易于理解和解释;
- 数据准备工作很少,不需要提前归一化,处理缺失值。。其他很多算法通常都需要数据规范化,需要创建虚拟变量并删除空值等;
- 能够同时处理数值和分类数据,既可以做回归又可以做分类。其他技术通常专门用于分析仅具有一种变量类型的数据集;
- 效率高,决策树只需要一次构建,反复使用,每一次预测的最大计算次数不超过决策树的深度;
- 能够处理多输出问题,即含有多个标签的问题,注意与一个标签中含有多种标签分类的问题区别开;
- 是一个白盒模型,结果很容易能够被解释。如果在模型中可以观察到给定的情况,则可以通过布尔逻辑轻松解释条件。相反,在黑盒模型中(例如,在人工神经网络中),结果可能更难以解释。
- 可以交叉验证的剪枝来选择模型,从而提高泛化能力。
- 对于异常点的容错能力好,健壮性高。
-
使用决策树预测的代价是O(log2m)。m为样本数。
缺点:
- 递归生成树的方法很容易出现过拟合,导致泛化能力不强。可以通过设置节点最少样本数量和限制决策树深度来改进。
- 决策树可能是不稳定的,因为即使非常小的变异,可能会产生一颗完全不同的树;这个可以通过集成学习之类的方法解决。
- 如果某些分类占优势,决策树将会创建一棵有偏差的树。因此,建议在拟合决策树之前平衡数据集。这个可以通过调节样本权重来改善。
- 寻找最优的决策树是一个NP难的问题,我们一般是通过启发式方法,容易陷入局部最优。可以通过集成学习之类的方法来改善。
- 有些比较复杂的关系,决策树很难学习,比如异或。这个就没有办法了,一般这种关系可以换神经网络分类方法来解决。
Regression Tree 回归树:Regression Tree 回归树_没有名字-CSDN博客_回归树
决策树之ID3算法(信息熵和信息增益,ID3算法主要是用来处理离散性的问题,然而对于连续型的问题,ID3算法就无能无力了。)(附matlab程序):简单易学的机器学习算法——决策树之ID3算法_null的专栏-CSDN博客
分类回归树CART(一种树构建算法,这种算法既可以处理离散型的问题,也可以处理连续型的问题。在处理连续型问题时,主要通过使用二元切分来处理连续型变量,即特征值大于某个给定的值就走左子树,或者就走右子树。对于连续型的问题,可以使用方差的概念来表达混乱程度,方差越大,越紊乱。所以我们要找到使得切分之后的方差最小的划分方式。)(Classification and Regression Tree)(附matlab程序):
简单易学的机器学习算法——分类回归树CART_null的专栏-CSDN博客_cart回归树
决策树到Random Forest, GBDT,XGBOOST。
回归树总体流程类似于分类树,分枝时穷举每一个特征的每一个阈值,来寻找最优切分特征j和最优切分点s,衡量的方法是平方误差最小化。分枝直到达到预设的终止条件(如叶子个数上限)就停止。
当然,处理具体问题时,单一的回归树肯定是不够用的。可以利用集成学习中的boosting框架,对回归树进行改良升级,得到的新模型就是提升树(Boosting Decision Tree),在进一步,可以得到梯度提升树(Gradient Boosting Decision Tree,GBDT),再进一步可以升级到XGBoost。
利用GBDT模型构造新特征:
利用GBDT模型构造新特征_Bryan__的专栏-CSDN博客
XGBoost Plotting API以及GBDT组合特征实践:
XGBoost Plotting API以及GBDT组合特征实践_hczheng的专栏-CSDN博客
XGBoost:
xgboost入门与实战(原理篇)_hczheng的专栏-CSDN博客_xgboost
决策树:
cart决策树剪枝的个人理解_wqtltm的博客-CSDN博客_cart决策树剪枝
机器学习实战(三)——决策树_呆呆的猫的博客-CSDN博客_决策树
李航,统计学习方法:CART剪枝算法实例分析李航,统计学习方法:CART剪枝算法实例分析_MingJianGuang的博客-CSDN博客_cart剪枝算法例题