本文代码托管在Machine-Learning-Homework-Exercise-ZhouZhihua
本文参考https://zhuanlan.zhihu.com/p/44666694
4.1
试证明对于不含冲突数据(即特征向量完全相同但标记不同)的训练集,必存在与训练集一致(即训练误差为 0) 的决策树。
答:
从原书p74的图4.2的决策树学习的基本算法可以看出,生成一个叶节点有三种情况:
- 节点下样本 D D D 全属于同一类别 C C C ,则将当前节点作为 C C C 类叶节点。
- 属性集 A = ∅ A=\varnothing A=∅ ,或者样本在当前属性集上取值相同。即特征用完了,或者样本在 A A A 上取值都相同。这时取 D D D 中最多的类作为此节点的类别标记。
- 在某一节点上的属性值 a ∗ v a_*^v a∗v ,样本为空,即没有样本在属性 a ∗ a_* a∗ 上取值为 a ∗ v a_*^v a∗v 。同样取 D D D 中最多的类作为此节点的类别标记。
在这道题中,目标是找出和训练集一致的决策树,所以不必考虑第3点,从1、2情况来看,决策树中树枝停止“生长”生成叶节点只会在样本属于同一类或者所有特征值都用完的时候,那么可能导致叶节点标记与实际训练集不同时只会发生在特征值都用完的情况(同一节点中的样本,其路径上的特征值都是完全相同的),而由于训练集中没有冲突数据,那每个节点上训练误差都为0。
4.2
试析使用"最小训练误差"作为决策树划分选择准则的缺陷。
答:
这道题暂时没想出答案。在网上找了其他的答案,都是认为会造成过拟合,没给出具体证明。而我的理解决策树本身就是容易过拟合的,就算使用信息增益或者基尼指数等,依旧容易过拟合,至于使用“最小训练误差”会不会“更容易”过拟合暂时没理解明白。
4.3
试编程实现基于信息熵进行划分选择的决策树算法,并为表 4.3 中数据生成一棵决策树。
构建决策树的流程如下
其中,树节点Node
中定义了如下属性
self.class_ = None # 节点所属类别
self.attribute = None # 节点划分属性
self.purity = None # 节点纯度(信息增益、增益率等)
self.split_point = None # 划分点
self.attribute_index = None # 划分属性索引
self.child = {} # 子节点字典,key为属性取值,value为节点 Node
信息熵的计算使用向量化方法:
def info_entropy(self, y):
'''
计算信息熵
输入:
y:标签向量
输出:
entropy:信息熵
'''
p = y.value_counts() / len(y) # 计算各类样本所占比例
entropy = np.sum(-p * np.log2(p))
return entropy
生成的决策树如下:
4.4
试编程实现基于基尼指数进行划分选择的决策树算法,为表 4.2 中数据生成预剪枝、后剪枝决策树并与未剪枝决策树进行比较.
未剪枝决策树:
预剪枝决策树:不分支,因为第一个节点分支前后准确率都是
4
7
\frac{4}{7}
74
后剪枝决策树:代码中注意递归的运用
4.5
试编程实现基于对率回归进行划分选择的决策树算法,并为表 4.3 中数据生成一棵决策树.
这里是先将
X
X
X中的离散属性转化为 one-hot
编码,利用逻辑回归拟合后的值进行分类。
逻辑回归决策树:
4.7
栈转队列的算法
图 4.2 是一个递归算法,若面临巨量数据,则决策树的层数会很深,使用递归方法易导致"栈"溢出。试使用"队列"数据结构,以参数MaxDepth 控制树的最大深度,写出与图 4.2 等价、但不使用递归的决策树生成算法.
答:
主要思路:每一次循环遍历下一层节点(除去叶节点),为每一个节点生成子节点,将非叶节点入队;用参数 L
保存每一层有多少个节点。下一次循环执行同样的步骤。直至所有的节点都叶节点,此时队列为空。具体如下:
输入:训练集D = {(x1, y1), (x2, y2)...(xm, ym)};
属性集A = {a1, a2 ... ad};
最大深度MaxDepth = maxDepth
过程:函数TreeDenerate(D, A, maxDepth)
1: 生成三个队列,NodeQueue、DataQueue、AQueue分别保存节点、数据、和剩余属性集;
2: 生成节点node;
3: if A为空 OR D上样本都为同一类别:
4: 将node标记为叶节点,其标记类别为D中样本最多的类;
5: return node;
6: end if
7: 将node入队NodeQueue; 将D入队 DataQueue; 将A入队AQueue;
8: 初始化深度depth=0;
9: 初始化L = 1; # L用于记录每一层有多少非叶节点。
10: while NodeQueue 非空:
11: L* = 0
12: for _ in range(L): # 遍历当前L个非叶节点
13: NodeQueue 出队node; DataQueue出队D; AQueue 出队A;
14: 从A中选择一个最优划分属性a*;
15: for a* 的每一个值 a*v do:
16: 新建一个node*,并将node*连接为node的一个分支;
17: 令 Dv表示为D中在a*上取值为a*v的样本子集;
18: if Dv为空:
19: 将node*标记为叶节点,其标记类别为D中样本最多的类;
20: continue;
21: end if
22: if A\{a*}为空 OR Dv上样本都为同一类别 OR depth == maxDepth:
23: 将node*标记为叶节点,其标记类别为Dv中样本最多的类;
24: continue;
25: end if
26: 将node*入队NodeQueue; 将Dv入队 DataQueue; 将A\{a*} 入队AQueue;
27: L* += 1; # 用于计算在第 depth+1 层有多少个非叶节点
28: L = L*;
29: depth += 1;
输入以node为根节点的一颗决策树
4.8
试将决策树生成的深度优先搜索过程修改为广度优先搜索,以参数MaxNode控制树的最大结点数,将题 4.7 中基于队列的决策树算法进行改写。对比题 4.7 中的算法,试析哪种方式更易于控制决策树所需存储不超出内存。
答:
4.7写的算法就是广度优先搜索的。这道题将MaxNode改为MaxDepth,只需要改几个地方。有一点需要注意的地方,就是在给一个节点生成子节点时(19-32行),可能造成节点数大于最大值的情况,比如某属性下有3种取值,那么至少要生成3个叶节点,这个时候节点总数可能会超过最大值,这时最终节点数可能会是MaxNode+2。
至于两种算法对比。个人理解当数据特征值,各属性的取值较多时,形成的决策树会趋于较宽类型的树,这时使用广度优先搜索更容易控制内存。若属性取值较少时,深度优先搜索更容易控制内存。
对4.7中修改如下:
输入:训练集D = {(x1, y1), (x2, y2)...(xm, ym)};
属性集A = {a1, a2... ad};
最大深度MaxNode = maxNode
过程:函数TreeDenerate(D, A, maxNode)
1: 生成三个队列,NodeQueue、DataQueue、AQueue分别保存节点、数据、和剩余属性集;
2: 生成节点node;
3: if A为空 OR D上样本都为同一类别:
4: 将node标记为叶节点,其标记类别为D中样本最多的类;
5: return node;
6: end if
7: 将node入队NodeQueue; 将D入队 DataQueue; 将A入队AQueue;
8: 初始化深度numNode=1;
9: 初始化L = 1; # L用于记录每一层有多少非叶节点。
10: while NodeQueue 非空:
11: L* = 0
12: for _ in range(L): # 遍历当前L个非叶节点
13: NodeQueue 出队Node; DataQueue出队D; AQueue 出队A;
14: if numNode >= maxNode:
15: 将node标记为叶节点,其标记类别为D中样本最多的类;
16: continue;
17: end if;
18: 从A中选择一个最优划分属性a*;
19: for a* 的每一个值 a*v do:
20: numNode+=1
21: 生成一个node*,并将node*连接为node的一个分支;
22: 令 Dv表示为D中在a*上取值为a*v的样本子集;
23: if Dv为空:
24: 将node*标记为叶节点,其标记类别为D中样本最多的类;
25: continue;
26: end if
27: if A\{a*}为空 OR Dv上样本都为同一类别:
28: 将node*标记为叶节点,其标记类别为Dv中样本最多的类;
29: continue;
30: end if
31: 将node*入队NodeQueue; 将Dv入队 DataQueue; 将A\{a*} 入队AQueue;
32: L* += 1; # 用于计算在第depth+1 层有多少个非叶节点
33: end if;
34: L = L*;
输入以node为根节点的一颗决策树