第5章 决策树
习题5.1
根据表5.1所给的训练数据集,利用信息增益比(C4.5算法)生成决策树。
解答:
表5.1 贷款申请样本数据表
| ID | 年龄 | 有工作 | 有自己的房子 | 信贷情况 | 类别 |
|---|---|---|---|---|---|
| 1 | 青年 | 否 | 否 | 一般 | 否 |
| 2 | 青年 | 否 | 否 | 好 | 否 |
| 3 | 青年 | 是 | 否 | 好 | 是 |
| 4 | 青年 | 是 | 是 | 一般 | 是 |
| 5 | 青年 | 否 | 否 | 一般 | 否 |
| 6 | 中年 | 否 | 否 | 一般 | 否 |
| 7 | 中年 | 否 | 否 | 好 | 否 |
| 8 | 中年 | 是 | 是 | 好 | 是 |
| 9 | 中年 | 否 | 是 | 非常好 | 是 |
| 10 | 中年 | 否 | 是 | 非常好 | 是 |
| 11 | 老年 | 否 | 是 | 非常好 | 是 |
| 12 | 老年 | 否 | 是 | 好 | 是 |
| 13 | 老年 | 是 | 否 | 好 | 是 |
| 14 | 老年 | 是 | 否 | 非常好 | 是 |
| 15 | 老年 | 否 | 否 | 一般 | 否 |
解答思路:
- 列出C4.5的生成算法;
- 使用sklearn的DecisionTreeClassifier类构建决策树,并使用graphviz包展示,默认是Gini,这里可以作为自编程的验证
- 通过自编程实现C4.5算法生成决策树,并进行特征选择
解题步骤:
第1步:列出C4.5的生成算法
根据书中第5.3.2节C4.5的生成算法:
算法5.3 C4.5的生成算法 输入:训练数据集
,特征集 阈值 ;
输出:决策树。
(1)如果中所有实例属于同一类 ,则置 为单结点树,并将 作为该结点的类,返回 ;
(2)如果,则置 为单结点树,并将 中实例数最大的类 作为该结点的类,返回 ;
(3)否则,按式计算 中各特征对 的信息增益比,选择信息增益比最大的特征 ;
(4)如果的信息增益比小于阈值 ,则置 为单结点树,并将 中实例数最大的类 作为该结点的类,返回 ;
(5)否则,对的每一可能值 ,依 将 分割为子集若干非空 ,将 中实例数最大的类作为标记,构建子结点,由结点及其子结点构成树 ,返回 ;
(6)对结点,以 为训练集,以 为特征集,递归地调用步(1)~步(5),得到子树 ,返回
第2步:调用sklearn的DecisionTreeClassifier类构建决策树
from sklearn.tree import DecisionTreeClassifier
from sklearn import preprocessing
import numpy as np
import pandas as pd
from sklearn import tree
import graphviz
features = ["年龄", "有工作", "有自己的房子", "信贷情况"]
X_train = pd.DataFrame([
["青年", "否", "否", "一般"],
["青年", "否", "否", "好"],
["青年", "是", "否", "好"],
["青年", "是", "是", "一般"],
["青年", "否", "否", "一般"],
["中年", "否", "否", "一般"],
["中年", "否", "否", "好"],
["中年", "是", "是", "好"],
["中年", "否", "是", "非常好"],
["中年", "否", "是", "非常好"],
["老年", "否", "是", "非常好"],
["老年", "否", "是", "好"],
["老年", "是", "否", "好"],
["老年", "是", "否", "非常好"],
["老年", "否", "否", "一般"]
])
y_train = pd.DataFrame(["否", "否", "是", "是", "否",
"否", "否", "是", "是", "是",
"是", "是", "是", "是", "否"])
class_names = [str(k) for k in np.unique(y_train)]
# 数据预处理
le_x = preprocessing.LabelEncoder()
le_x.fit(np.unique(X_train))
X_train = X_train.apply(le_x.transform)
# 调用sklearn的DecisionTreeClassifier建立决策树模型
model_tree = DecisionTreeClassifier()
# 训练模型
model_tree.fit(X_train, y_train)
# 导出决策树的可视化文件,文件格式是dot
dot_data = tree.export_graphviz(model_tree, out_file=None,
feature_names=features,
class_names=class_names,
filled=True, rounded=True,
special_characters=True)
# 使用graphviz包,对决策树进行展示
graph = graphviz.Source(dot_data)
# 可使用view方法展示决策树
# 中文乱码:需要对源码_export.py文件(文件路径:sklearn/tree/_export.py)修改,
# 在文件第451行中将helvetica改成SimSun
graph# 打印决策树
tree_text = tree.export_text(model_tree, feature_names=features)
print(tree_text)|--- 有自己的房子 <= 3.00
| |--- 有工作 <= 3.00
| | |--- class: 否
| |--- 有工作 > 3.00
| | |--- class: 是
|--- 有自己的房子 > 3.00
| |--- class: 是
第3步:自编程实现C4.5算法生成决策树
import json
from collections import Counter
import numpy as np
# 节点类
class Node:
def __init__(self, node_type, class_name, feature_name=None,
info_gain_ratio_value=0.0):
# 结点类型(internal或leaf)
self.node_type = node_type
# 特征名
self.feature_name = feature_name
# 类别名
self.class_name = class_name
# 子结点树
self.child_nodes = []
# Gini指数值
self.info_gain_ratio_value = info_gain_ratio_value
def __repr__(self):
return json.dumps(self, indent=3, default=lambda obj: obj.__dict__, ensure_ascii=False)
def add_sub_tree(self, key, sub_tree):
self.child_nodes.append({"condition": key, "sub_tree": sub_tree})class MyDecisionTree:
def __init__(self, epsilon):
self.epsilon = epsilon
self.tree = None
def fit(self, train_set, y, feature_names):
features_indices = list(range(len(feature_names)))
self.tree = self._fit(train_set, y, features_indices, feature_names)
return self
# C4.5算法
def _fit(self, train_data, y, features_indices, feature_labels):
LEAF = 'leaf'
INTERNAL = 'internal'
class_num = len(np.unique(y))
# (1)如果训练数据集所有实例都属于同一类Ck
label_set = set(y)
if len(label_set) == 1:
# 将Ck作为该结点的类
return Node(node_type=LEAF, class_name=label_set.pop())
# (2)如果特征集为空
# 计算每一个类出现的个数
class_len = Counter(y).most_common()
(max_class, max_len) = class_len[0]
if len(features_indices) == 0:
# 将实例数最大的类Ck作为该结点的类
return Node(LEAF, class_name=max_class)
# (3)按式(5.10)计算信息增益,并选择信息增益最大的特征
max_feature = 0
max_gda = 0
D = y.copy()
# 计算特征集A中各特征
for feature in features_indices:
# 选择训练集中的第feature列(即第feature个特征)
A = np.array(train_data[:, feature].flat)
# 计算信息增益
gda = self._calc_ent_grap(A, D)
if self._calc_ent(D) != 0:
# 计算信息增益比
gda /= self._calc_ent(D)
# 选择信息增益最大的特征Ag
if gda > max_gda:
max_gda, max_feature = gda, feature
# (4)如果Ag信息增益小于阈值
if max_gda < self.epsilon:
# 将训练集中实例数最大的类Ck作为该结点的类
return Node(LEAF, class_name=max_class)
max_feature_label = feature_labels[max_feature]
# (6)移除已选特征Ag
sub_feature_indecs = np.setdiff1d(features_indices, max_feature)
sub_feature_labels = np.setdiff1d(feature_labels, max_feature_label)
# (5)构建非空子集
# 构建结点
feature_name = feature_labels[max_feature]
tree = Node(INTERNAL, class_name=None, feature_name=feature_name,
info_gain_ratio_value=max_gda)
max_feature_col = np.array(train_data[:, max_feature].flat)
# 将类按照对应的实例数递减顺序排列
feature_value_list = [x[0] for x in Counter(max_feature_col).most_common()]
# 遍历Ag的每一个可能值ai
for feature_value in feature_value_list:
index = []
for i in range(len(y)):
if train_data[i][max_feature] == feature_value:
index.append(i)
# 递归调用步(1)~步(5),得到子树
sub_train_set = train_data[index]
sub_train_label = y[index]
sub_tree = self._fit(sub_train_set, sub_train_label, sub_feature_indecs, sub_feature_labels)
# 在结点中,添加其子结点构成的树
tree.add_sub_tree(feature_value, sub_tree)
return tree
# 计算数据集x的经验熵H(x)
def _calc_ent(self, x):
x_value_list = set([x[i] for i in range(x.shape[0])])
ent = 0.0
for x_value in x_value_list:
p = float(x[x == x_value].shape[0]) / x.shape[0]
logp = np.log2(p)
ent -= p * logp
return ent
# 计算条件熵H(y/x)
def _calc_condition_ent(self, x, y):
x_value_list = set([x[i] for i in range(x.shape[0])])
ent = 0.0
for x_value in x_value_list:
sub_y = y[x == x_value]
temp_ent = self._calc_ent(sub_y)
ent += (float(sub_y.shape[0]) / y.shape[0]) * temp_ent
return ent
# 计算信息增益
def _calc_ent_grap(self, x, y):
base_ent = self._calc_ent(y)
condition_ent = self._calc_condition_ent(x, y)
ent_grap = base_ent - condition_ent
return ent_grap
def __repr__(self):
return str(self.tree)# 表5.1的训练数据集
feature_names = np.array(["年龄", "有工作", "有自己的房子", "信贷情况"])
X_train = np.array([
["青年", "否", "否", "一般"],
["青年", "否", "否", "好"],
["青年", "是", "否", "好"],
["青年", "是", "是", "一般"],
["青年", "否", "否", "一般"],
["中年", "否", "否", "一般"],
["中年", "否", "否", "好"],
["中年", "是", "是", "好"],
["中年", "否", "是", "非常好"],
["中年", "否", "是", "非常好"],
["老年", "否", "是", "非常好"],
["老年", "否", "是", "好"],
["老年", "是", "否", "好"],
["老年", "是", "否", "非常好"],
["老年", "否", "否", "一般"]
])
y = np.array(["否", "否", "是", "是", "否",
"否", "否", "是", "是", "是",
"是", "是", "是", "是", "否"])
dt_tree = MyDecisionTree(epsilon=0.1)
dt_tree.fit(X_train, y, feature_names)
dt_tree{
"node_type": "internal",
"feature_name": "有自己的房子",
"class_name": null,
"child_nodes": [
{
"condition": "否",
"sub_tree": {
"node_type": "internal",
"feature_name": "年龄",
"class_name": null,
"child_nodes": [
{
"condition": "否",
"sub_tree": {
"node_type": "leaf",
"feature_name": null,
"class_name": "否",
"child_nodes": [],
"info_gain_ratio_value": 0.0
}
},
{
"condition": "是",
"sub_tree": {
"node_type": "leaf",
"feature_name": null,
"class_name": "是",
"child_nodes": [],
"info_gain_ratio_value": 0.0
}
}
],
"info_gain_ratio_value": 1.0
}
},
{
"condition": "是",
"sub_tree": {
"node_type": "leaf",
"feature_name": null,
"class_name": "是",
"child_nodes": [],
"info_gain_ratio_value": 0.0
}
}
],
"info_gain_ratio_value": 0.4325380677663126
}
习题5.2
已知如表5.2所示的训练数据,试用平方误差损失准则生成一个二叉回归树。
表5.2 训练数据表
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 4.50 | 4.75 | 4.91 | 5.34 | 5.80 | 7.05 | 7.90 | 8.23 | 8.70 | 9.00 |
解答:
解答思路:
- 根据书中第5.5.1节平方误差最小的准则,列出最小二乘回归树生成算法(算法5.5);
- 编写代码,实现算法,并用表5.2训练数据进行验证。
解题步骤:
第1步:算法5.5的最小二乘回归树生成算法
根据书中第5.5.1节的算法5.5:
决策树的生成就是递归地构建二叉决策树的过程,对回归树用平方误差最小化准则,对分类树用基尼指数(Gini index)最小化准则,进行特征选择,生成二叉树。
算法5.5(最小二乘回归树生成算法)
输入:训练数据集
输出:回归树
在训练数据集所在的输入空间中,递归地将每个区域划分为两个子区域并决定每个子区域上的输出值,构建二叉决策树;
(1)选择最优切分变量与切分点 ,求解 遍历变量
,对固定的切分变量 扫描切分点 ,选择使得上式达到最小值的对
(2)用选定的对划分区域并决定相应的输出值: (3)继续对两个子区域调用步骤(1),(2),直至满足停止条件
(4)将输入空间划分为个区域 ,生成决策树:
第2步:编写代码,实现算法,并用表5.2训练数据进行验证
import json
import numpy as np
# 节点类
class Node:
def __init__(self, value, feature, left=None, right=None):
self.value = value.tolist()
self.feature = feature.tolist()
self.left = left
self.right = right
def __repr__(self):
return json.dumps(self, indent=3, default=lambda obj: obj.__dict__, ensure_ascii=False)class MyLeastSquareRegTree:
def __init__(self, train_X, y, epsilon):
# 训练集特征值
self.x = train_X
# 类别
self.y = y
# 特征总数
self.feature_count = train_X.shape[1]
# 损失阈值
self.epsilon = epsilon
# 回归树
self.tree = None
def _fit(self, x, y, feature_count):
# (1)选择最优切分点变量j与切分点s,得到选定的对(j,s),并解得c1,c2
(j, s, minval, c1, c2) = self._divide(x, y, feature_count)
# 初始化树
tree = Node(feature=j, value=x[s, j], left=None, right=None)
# 用选定的对(j,s)划分区域,并确定响应的输出值
if minval < self.epsilon or len(y[np.where(x[:, j] <= x[s, j])]) <= 1:
tree.left = c1
else:
# 对左子区域调用步骤(1)、(2)
tree.left = self._fit(x[np.where(x[:, j] <= x[s, j])],
y[np.where(x[:, j] <= x[s, j])],
self.feature_count)
if minval < self.epsilon or len(y[np.where(x[:, j] > x[s, j])]) <= 1:
tree.right = c2
else:
# 对右子区域调用步骤(1)、(2)
tree.right = self._fit(x[np.where(x[:, j] > x[s, j])],
y[np.where(x[:, j] > x[s, j])],
self.feature_count)
return tree
def fit(self):
self.tree = self._fit(self.x, self.y, self.feature_count)
return self
@staticmethod
def _divide(x, y, feature_count):
# 初始化损失误差
cost = np.zeros((feature_count, len(x)))
# 公式5.21
for i in range(feature_count):
for k in range(len(x)):
# k行i列的特征值
value = x[k, i]
y1 = y[np.where(x[:, i] <= value)]
c1 = np.mean(y1)
y2 = y[np.where(x[:, i] > value)]
if len(y2) == 0:
c2 = 0
else:
c2 = np.mean(y2)
y1[:] = y1[:] - c1
y2[:] = y2[:] - c2
cost[i, k] = np.sum(y1 * y1) + np.sum(y2 * y2)
# 选取最优损失误差点
cost_index = np.where(cost == np.min(cost))
# 所选取的特征
j = cost_index[0][0]
# 选取特征的切分点
s = cost_index[1][0]
# 求两个区域的均值c1,c2
c1 = np.mean(y[np.where(x[:, j] <= x[s, j])])
c2 = np.mean(y[np.where(x[:, j] > x[s, j])])
return j, s, cost[cost_index], c1, c2
def __repr__(self):
return str(self.tree)train_X = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]).T
y = np.array([4.50, 4.75, 4.91, 5.34, 5.80, 7.05, 7.90, 8.23, 8.70, 9.00])
model_tree = MyLeastSquareRegTree(train_X, y, epsilon=0.2)
model_tree.fit()
model_tree{
"value": 5,
"feature": 0,
"left": {
"value": 3,
"feature": 0,
"left": 4.72,
"right": 5.57
},
"right": {
"value": 7,
"feature": 0,
"left": {
"value": 6,
"feature": 0,
"left": 7.05,
"right": 7.9
},
"right": {
"value": 8,
"feature": 0,
"left": 8.23,
"right": 8.85
}
}
}
根据上面程序的输出,可得到用平方误差损失准则生成一个二叉回归树:$$f(x)=\begin{cases} 4.72 & x \le 3\ 5.57 & 3 < x \le 5\ 7.05 & 5 < x \le 6\ 7.9 & 6 < x \le 7 \ 8.23 & 7 < x \le 8\ 8.85 & x > 8\ \end{cases}$$
习题5.3
证明 CART 剪枝算法中,当
解答:
解答思路:
- 列出CART剪枝算法;
- 列出树的剪枝算法;
- 采用反证法,假设存在两棵子树使得损失函数最小。
解答步骤:
第1步:算法5.7的CART剪枝算法
根据书中的算法5.7:
算法5.7(CART剪枝算法)
输入:CART算法生成的决策树;
输出:最优决策树。
(1)设。
(2)设。
(3)自下而上地对各内部结点计算 , ,以及 这里,
表示以 为根结点的子树, 是对训练数据的预测误差, 是 的叶结点个数。
(4)对的内部结点 进行剪枝,并对叶结点 以多数表决法决定其类,得到树 。
(5)设。
(6)如果不是由根节点和两个叶结点组成的树,则回到步骤(2),否则 。
(7)采用交叉验证法在子树序列从中选取最优子树 。
第2步:算法5.4的树的剪枝算法
根据书中的算法5.4:
算法5.4(树的剪枝算法)
输入:生成算法产生的整个树,参数 ;
输出:修剪后的子树。
(1)计算每个结点的经验熵。
(2)递归地从树的叶结点向上回缩。
设一组叶结点回缩到其父结点之前与之后的整体树分别为与 ,其对应的损失函数分别是 与 ,如果 则进行剪枝,即将父结点变为新的叶结点。
(3)返回(2),直至不能继续为止,得到损失函数最小的子树。
第3步:采用反证法,假设存在两棵子树使得损失函数最小
从第1、2步可得到以下结论:
- 内部节点是否剪枝只与以该节点为根节点的子树有关;
- 当
时,对以结点 为根节点的子树进行剪枝。
如图5.1所示,整个树
根据子树
根据子树
由上面公式可得:
根据上述公式,可知
因此,如果存在两棵以上的最优子树,其中一棵树总能找到一个来自另一棵子树的剪枝点,使得整体损失进一步下降,所以只能存在唯一的最小子树使得损失函数最小,得证。
习题5.4
证明 CART 剪枝算法中求出的子树序列
解答:
解答思路:
- 对题目重新进行表述;
- 证明当
和 时的最优子树; - 证明
为区间 的最优子树。
解答步骤:
第1步:对题目重新表述
原结论可以表述为:将
第2步:证明当
根据书中第5.5.2节的剪枝描述:
当
大的时候,最优子树 偏小;当 小的时候,最优子树 偏大。极端情况,当 时,整体树是最优的。当 时,根结点组成的单结点树是最优的。
第3步:证明
根据《Classification and Regression Trees》书中第10.2节的定理10.7:
THEOREM 10.7. Every tree T has a unique smallest optimally pruned subtree
. Let T be a nontrivial tree having root and primary branches and . Then $$R_{\alpha}(T(\alpha)) = \min[R_{\alpha}(t_1), R_{\alpha}(T_L(\alpha))+R_{\alpha}(T_R(\alpha))]$$ If , then ; otherwise, .
根据该书的符号描述,其中:
表示,在 条件时,树 的最小最优剪枝子树 表示,在 条件时, 的损失值,即
定理10.7表示,树
根据定理10.7,可以得到该书定理10.9的第一个推论:
THEOREM 10.9.
(i) If, then .
定理10.9(i)表示,当
根据CART剪枝算法和定理10.9,可知(见该书的第368页):
- 当
时, - 当
时,$T_0(\alpha) \preceq T_0(\alpha_1) = T_1 \preceq T_0 $
所以,
依次类推,子树
参考文献
【1】Friedman. Classification and Regression Trees[M]. 1984:364
