决策树
第一章 简介
1.1 决策树的发展史
最初的决策树算法是心理学家兼计算机科学家E.B.Hunt1962年在研究人类的概念学习过程中提出的概念学习算法(Concept Learning System),该算法确立了决策树“分而治之”的学习策略。澳大利亚计算机科学家罗斯
⋅
\cdot
⋅昆兰(J.Ross Quinlan)在Hunt的指导下于1968年在美国华盛顿大学获得计算机博士学位,然后到悉尼大学任教。1978年他在学术假时到斯坦福大学访问,选修了图灵的助手D.Michie开设的一门研究生课程。课上有个大作业,要求用程序来学习出完备正确的规则,以判断国际象棋残局中一方是否会在两步棋后被将死。昆兰写了一个类似于CLS的程序来完成作业,其中最重要的改进是引入了信息增益。后来他把这个工作整理出来在1979年发表,这就是ID3(Interactive Dichotomizer 3,ID3)算法。
1986年Machine Learning杂志创刊,昆兰应邀在创刊上重新发表了ID3算法,掀起了决策树研究的热潮。短短几年间众多决策树算法问世,ID4、ID5等名字迅速被其他研究者提出的算法占用,昆兰只好将自己的ID3后继算法命名为C4.0,在此基础上进一步提出了著名的C4.5(Classifier 4.5)。有趣的是,昆兰自称C4.5仅是对C4.0做的一些小改进,因此将它命名为“第4.5代分类器”,而后续的商用版本为C5.0。
加州大学伯克利分校的教授Leo Breiman1984年提出了分类与回归树 (Classification and Regression Trees,CART),该方法既可以用于分类,也可以用于回归。同时,他又提出了Bagging(Bootstrap Aggregating)方法和随机森林 (Random Forest,RF)算法。
1.2 预备知识
1.2.1 信息
1928年Hartly给出过信息的定义: 信息就是消除不确定性,这个定义后来被学术界广泛引用。例如,天气预报报道”明天可能下雨,可能不下雨“,这样的报道肯定无法让你满意,因为它没有告诉你任何信息。如果天气预报报道”明天会下雨“,那么这就是一个令人满意的答复,因为它告诉了我们有用的信息。具体分析:”明天是否会下雨“只有两种状态:下雨或者不下雨。当天气预报报道“明天可能下雨,明天可能不下雨”的时候,没有降低或消除不确定性,所以没有给予我们任何信息。当天气预报报道“明天会下雨”时,就从两种状态变成一种状态,降低了不确定性,所以它就是信息。
1.2.2 信息量
给出信息定义后,自然会考虑信息度量。实际上,信息的量化(信息的计算方式)最早也是Hartly提出的,他将状态数的对数值定义为信息量。例如,假设信息源有N种等概率的状态数,那么信息量计算如下:
H
(
x
)
=
l
o
g
2
(
N
)
(2-1)
H(x) = log2(N) \tag{2-1}
H(x)=log2(N)(2-1)
其中,
H
(
x
)
H(x)
H(x)是事件
x
x
x 的Hartley信息量,
N
N
N是事件
x
x
x 的可能状态数。
假设有两种场景:
场景一:投掷一枚均有的硬币,有人告诉我投掷出的结果是“正面朝上”。
场景二:投掷一枚非均匀的硬币,有人告诉我“正面朝上”。
根据Hartley公式计算这两种场景的信息量如下:
场景一:
H
1
=
log
2
2
=
1
H_1=\log_22=1
H1=log22=1
场景二:
H
2
=
log
2
2
=
1
H_2 = \log_22=1
H2=log22=1
从上述示例中我们可以看出,不管正面朝上的可能性有多大,Hartley的信息量始终保持为1。由于场景一的消除不确定性与场景二消除的不确定性在直觉层面上存在差异,这意味着Hartley的信息量认为所事件的结果都是等概率出现的。然而,现实中事件的结果并非等概率出现。
为了让信息量的计算方式更加科学,1948年Shannon通过对信息量进行深入研究后,指出信息量应该是概率的函数。假设Shannon将Hartly信息量计算公式中的N替换为概率,那么计算公式如下:
H
(
x
)
=
log
2
P
(
X
=
x
)
H(x) = \log_2P(X=x)
H(x)=log2P(X=x)
其中,
X
X
X 表示某个事件,
x
x
x 表示该事件发生的结果,
P
P
P 表示这个事件发生的概率。接下来,我们以场景二为例,来理解该公式。
1)假设正面朝上的概率为0.9,那么根据上述信息量计算公式,可得
H
(
X
=
正面
)
=
log
2
0.9
=
−
0.152
H(X=正面)=\log_20.9=-0.152
H(X=正面)=log20.9=−0.152
2)假设正面朝上的概率为0.99,那么根据上述信息量计算公式,可得
H
(
X
=
正面
)
=
log
2
0.99
=
−
0.0145
H(X=正面)=\log_20.99=-0.0145
H(X=正面)=log20.99=−0.0145
通过案例可以发现,该公式存在两方面缺陷:
1)不确定性越大,信息量越小,与信息定义不符;
2)该公式度量的是某个事件发生时所携带的信息量,缺乏预测性,导致无法解决实际问题。
1.2.3 信息熵
1948年,Shannon受到热力学中熵的概念启发,定义了信息熵的概念。信息熵是用来度量信息的不确定性。Shannon的信息熵计算公式如下:
H
(
X
)
=
−
∑
i
=
1
n
P
{
X
=
x
i
}
log
2
P
{
X
=
x
i
}
(2-2)
H(X)=-\sum_{i=1}^{n}P\{X=x_i\}\log_2P\{X=x_i\} \tag{2-2}
H(X)=−i=1∑nP{X=xi}log2P{X=xi}(2-2)
通过公式可以发现,信息熵越大,事件结果的不确定性越大。反之,事件结果的确定性越大。此外,信息熵也可以看作是所有可能结果的期望值。这是因为信息熵的计算涉及每种可能结果的概率和信息量,并对这些值进行加权求和的结果。我们依然用场景二来理解信息熵。
1)假设正面朝上的概率为0.9,那么根据信息熵的计算公式,可得
H
=
−
0.9
×
log
2
0.9
−
0.1
×
log
2
0.1
=
0.469
H=-0.9\times\log_20.9-0.1\times\log_20.1=0.469
H=−0.9×log20.9−0.1×log20.1=0.469
2)假设正面朝上的概率为0.99,那么根据信息熵计算公式,可得
H
=
−
0.99
×
log
2
0.99
−
0.01
×
log
2
0.01
=
0.081
H=-0.99\times\log_20.99-0.01\times\log_20.01=0.081
H=−0.99×log20.99−0.01×log20.01=0.081
通过对比发现,信息熵克服了Hartly信息量度量的不足。
第二章 决策树
决策树由结点(node)和有向边(directed edge)组成。结点有两种类型:内部结点(internal node)和叶子结点(leaf node)。内部结点表示一个特征或属性,叶子结点表示一个类别。
2.1 定义数据结构
决策树是由结点构成的层次结构,每个结点表示一个决策条件或特征,并且通过连接节点来形成决策树的结构。定义节点数据结构,我们可以将节点的属性(如特征、阈值、类别等)存储在节点对象中,以便在构建和操作决策树时可以方便地访问和处理这些属性。结点数据结构具体如下所示。
class Node:
"""
构建Node类,来表示决策树中的节点,其中叶子节点用node_value记录节点的值,内部节点用feature_index记录界定啊对应的特征索引,
children记录子节点。
"""
def __init__(self):
self.feature_index = None # 分类特征编号
self.leaf_node_value = None # 叶子节点值
self.children = {} # 子节点
2.2 特征选择
选择划分特征时,应选择最有助于分类的特征。按照该特征将当前数据集进行切分,使得各个数据子集的样本尽可能属于同一类别,也就是尽量提高数据子集的纯度(purity),降低不确定性。
在统计学习中,通常用熵来度量随机变量的不确定性。在使用一个特征划分数据集后,用信息增益或信息增益比来量化分类不确定性降低降低程度。
2.2.1 条件信息熵
给定训练集
D
=
{
(
x
1
,
y
1
)
)
,
(
x
2
,
y
2
)
,
…
,
(
x
n
,
y
n
)
}
D=\{(x_1, y_1)), (x_2, y_2), \dots, (x_n, y_n)\}
D={(x1,y1)),(x2,y2),…,(xn,yn)} ,其中特征集或属性集为
A
=
{
a
1
,
a
2
,
…
,
a
d
}
A=\{a_1, a_2, \dots, a_d\}
A={a1,a2,…,ad},
y
i
∈
{
C
1
,
C
2
,
…
,
C
k
}
y_i \in \{C_1, C_2, \dots, C_k\}
yi∈{C1,C2,…,Ck},特征
a
i
a_i
ai 将
D
D
D 进行分割为
m
m
m 个数据子集
{
D
1
,
D
2
,
…
,
D
m
}
\{D_1, D_2, \dots, D_m\}
{D1,D2,…,Dm},则以特征
a
i
a_i
ai 为条件
D
D
D 的熵为:
H
(
D
∣
A
=
a
i
)
=
∑
i
=
1
m
∣
D
i
∣
∣
D
∣
H
(
D
i
)
=
−
∑
i
=
1
m
∣
D
i
∣
∣
D
∣
(
∑
j
=
1
k
∣
C
i
j
∣
∣
D
i
∣
log
2
(
∣
C
i
j
∣
∣
D
i
∣
)
)
\begin{align*} H(D|A=a_i) &= \sum_{i=1}^{m}\frac{|D_i|}{|D|}H(D_i) \\ &=-\sum_{i=1}^{m}\frac{|D_i|}{|D|}(\sum_{j=1}^{k}\frac{|C_{ij}|}{|D_i|}\log_2(\frac{|C_{ij}|}{|D_i|})) \end{align*}
H(D∣A=ai)=i=1∑m∣D∣∣Di∣H(Di)=−i=1∑m∣D∣∣Di∣(j=1∑k∣Di∣∣Cij∣log2(∣Di∣∣Cij∣))
其中,|*|表示样本容量。
2.2.2 信息增益
在使用一个特征划分数据集后,使用信息增益来量化不确定性降低程度,即熵降低程度。具体公式如下:
G
(
D
,
A
=
a
i
)
=
H
(
D
)
−
H
(
D
∣
A
=
a
i
)
G(D, A=a_i) = H(D) - H(D|A=a_i)
G(D,A=ai)=H(D)−H(D∣A=ai)
2.2.3 信息增益比
在选择当前最优划分特征时,如果一个特征的取值较多,那么它的信息增益较大,从而使该特征在选择最优划分特征时被优先考虑,而忽略了取值较少特征的潜在重要性。为了解决上述问题,C4.5算法引入信息增益比来选择最优划分特征,具体公式如下:
G
r
a
t
i
o
(
D
,
A
=
a
i
)
=
G
(
D
,
A
=
a
i
)
H
A
=
a
i
G_{ratio}(D, A=a_i) = \frac{G(D, A=a_i)}{H_{A=a_i}}
Gratio(D,A=ai)=HA=aiG(D,A=ai)
其中,
H
A
=
a
i
H_{A=a_i}
HA=ai为:
H
A
=
a
i
=
−
∑
i
=
1
m
∣
D
i
∣
∣
D
∣
log
2
(
∣
D
i
∣
∣
D
∣
)
H_{A=a_i} = -\sum_{i=1}^{m}\frac{|D_i|}{|D|}\log_2(\frac{|D_i|}{|D|})
HA=ai=−i=1∑m∣D∣∣Di∣log2(∣D∣∣Di∣)
class Entropy:
@staticmethod
def entropy(y):
"""信息熵"""
n_classes = np.bincount(y) # 统计类别频数
p = n_classes[np.nonzero(n_classes)] / len(y) # 统计类别频率
return np.sum(-p * np.log2(p))
def condition_entropy(self, feature, y):
"""条件熵"""
feature_classes = np.unique(feature) # 特征
condition_entropy = 0.0 # 条件熵
for c in feature_classes:
y_sub = y[feature == c]
condition_entropy += len(y_sub) * self.entropy(y_sub) / len(y)
return condition_entropy
def information_gain(self, feature, y):
"""信息增益"""
return self.entropy(y) - self.condition_entropy(feature, y)
2.3 构建决策树
训练决策树的关键性问题是如何建立决策树。一个很直观想法是从根节点开始,使用递归方法构建决策树。为了构建决策树,我们需要解决一下几个问题:
(1) 每个内部节点应该选择那个特征作为判定?这个判定将数据集一分为二,然后用这两个子集构造左右子树。
(2) 选定特征后,以什么规则进入左子树?对于数值型变量,需要寻找一个阈值进行判断,小于该阈值的进入左子树,否则进入右子树。类别型变量需要确定一个子集划分,将特征取值集合划分两个不相交的子集,特征属于第一个子集进入左子树,否则进入右子树。
(3) 何时停止分裂,把节点设置为叶子节点?对于分类问题,当样本点属于同一类别时,停止分裂,但这样可能会导致树节点过多,深度过大,产生过拟合。另一种方案是当节点的样本数小于阈值时停止分裂。
Algorithm
1
决策树构造伪代码[1]
输入:训练数据集
D
=
{
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
,
…
,
(
x
n
,
y
n
)
}
;
属性集
A
=
{
a
1
,
a
2
,
…
,
a
d
}
.
输出:以node为根结点的一棵决策树
.
过程:函数buildTree
(
D
,
A
)
1.
生成节点node
;
2.
if
D
中样本全属于同一类别
C
then
3.
将node标记为C类结点
;
return
4.
end
if
5.
if
A
=
∅
OR
D
中样本在
A
上取值相同
then
6.
将 node 标记为叶子结点,其类别标记为
D
中样本数最多的类
;
return
7.
end
if
8.
从
A
中选择最优划分属性
a
∗
;
9.
for
a
∗
的每一个值
a
v
∗
do
10.
为node生成一个分支; 令
D
v
表示
D
中在
a
∗
上取值为
a
v
∗
的样本子集
;
11.
if
D
v
为空
then
12.
将分支结点标记为叶子结点, 其类别标记为
D
中样本最多的类;
return
13.
else
14.
以 buildTree
(
D
v
,
A
−
a
∗
)
为分支结点
15.
end
if
16.
end
for
\begin{array}{ll} \hline \textbf{Algorithm 1 决策树构造伪代码[1]} \\ \hline \text{输入:训练数据集 }D=\{(x_1,y_1),(x_2,y_2),\dots,(x_n,y_n)\}; \\ \quad\quad\quad{属性集 }A=\{a_1,a_2,\dots,a_d\}. \\ \text{输出:以node为根结点的一棵决策树}. \\ \\ \text{过程:函数buildTree}(D,A)\\ 1. \enspace \text{生成节点node};\\ 2. \enspace \textbf{if\:\:}D\text{中样本全属于同一类别}C \textbf{\:\:then}\\ 3. \enspace\enspace\enspace\text{将node标记为C类结点}; \textbf{\:return}\\ 4. \enspace \textbf{end if}\\ 5. \enspace \textbf{if\:\:} A=\empty \textbf{ OR } D中样本在A上取值相同 \textbf{\:\:then} \\ 6. \enspace\enspace\enspace将\text{ node }\text{标记为叶子结点,其类别标记为}D\text{中样本数最多的类}; \textbf{\:return}\\ 7. \enspace \textbf{end if}\\ 8. \enspace \text{从 }A\text{ 中选择最优划分属性}a^*;\\ 9. \enspace \textbf{for\:\:}a^*\text{的每一个值}a^{*}_{v} \textbf{\:\:do}\\ 10. \enspace\enspace\enspace\text{为node生成一个分支; 令 }D_v\text{表示}D\text{中在}a^{*}\text{上取值为}a^{*}_{v}的样本子集;\\ 11. \enspace\enspace\enspace\textbf{if\:\:}D_v\text{ 为空} \textbf{\:\:then}\\ 12. \enspace\enspace\enspace\enspace\enspace\enspace\text{将分支结点标记为叶子结点, 其类别标记为 }D\text{ 中样本最多的类; } \textbf{\:return}\\ 13. \enspace\enspace\enspace\textbf{else\:\:}\\ 14. \enspace\enspace\enspace\enspace\enspace\enspace\text{以 buildTree}(D_v,A - a^*)\text{ 为分支结点}\\ 15. \enspace\enspace\enspace\textbf{end if} \enspace\\ 16. \enspace \textbf{end for}\\ \hline \end{array}
Algorithm 1 决策树构造伪代码[1]输入:训练数据集 D={(x1,y1),(x2,y2),…,(xn,yn)};属性集A={a1,a2,…,ad}.输出:以node为根结点的一棵决策树.过程:函数buildTree(D,A)1.生成节点node;2.ifD中样本全属于同一类别Cthen3.将node标记为C类结点;return4.end if5.ifA=∅ OR D中样本在A上取值相同then6.将 node 标记为叶子结点,其类别标记为D中样本数最多的类;return7.end if8.从 A 中选择最优划分属性a∗;9.fora∗的每一个值av∗do10.为node生成一个分支; 令 Dv表示D中在a∗上取值为av∗的样本子集;11.ifDv 为空then12.将分支结点标记为叶子结点, 其类别标记为 D 中样本最多的类; return13.else14.以 buildTree(Dv,A−a∗) 为分支结点15.end if16.end for
2.4 可解释性
决策树相对于其他分类器而言有一个显著的优势,即可解释性。以一个生动的比喻为例,当有人询问为什么认为这个男人是包拯时,决策树能够提供清晰的解释:“因为他的额头有月亮”。这种是一种简单、直观的解释,准确揭示了决策的依据。
相比之下,神经网络在处理类似问题时,通常只是返回类别标签,而很少提供关于为什么选择该类别的深入见解。K近邻分类器虽然提供了一种较为表面的论证,例如“某样例应该被标记为正类是因为该样例与 x 最接近”。但是这种解释与决策树所提供的基于属性的详细解释相差甚远。
第三章 应用与实践
3.1 测试数据集
测试集下载地址:
https://archive.ics.uci.edu/static/public/58/lenses.zip
表3-1 隐形眼镜数据集
Variable Name | Role | Type | Missing Values | Discribe |
---|---|---|---|---|
id | ID | Integer | no | / |
age | Feature | Categorical | no | 患者年龄 |
spectacle_prescription | Feature | Categorical | no | 近视/远视 |
astigmatic | Feature | Binary | no | 散光 |
class | Target | Categorical | no | 隐形眼镜类型 |
3.2 ID3代码
import numpy as np
class Node:
"""
构建Node类,来表示决策树中的节点,其中叶子节点用node_value记录节点的值,内部节点用feature_index记录界定啊对应的特征索引,
children记录子节点。
"""
def __init__(self):
self.feature_index = None # 分类特征编号
self.leaf_node_value = None # 叶子节点值
self.children = {} # 子节点
def __str__(self):
if self.children:
s = "内部节点<%s>:\n" % self.feature_index
for fv, node in self.children.items():
ss = "[%s]-> %s" % (fv, node)
s += "\t" + ss.replace("\n", "\n\t") + "\n"
else:
s = "叶子节点(%s)" % self.leaf_node_value
return s
class Entropy:
@staticmethod
def entropy(y):
"""信息熵"""
n_classes = np.bincount(y) # 统计类别频数
p = n_classes[np.nonzero(n_classes)] / len(y) # 统计类别频率
return np.sum(-p * np.log2(p))
def condition_entropy(self, feature, y):
"""条件熵"""
feature_classes = np.unique(feature) # 特征
condition_entropy = 0.0 # 条件熵
for c in feature_classes:
y_sub = y[feature == c]
condition_entropy += len(y_sub) * self.entropy(y_sub) / len(y)
return condition_entropy
def information_gain(self, feature, y):
"""信息增益"""
return self.entropy(y) - self.condition_entropy(feature, y)
class ID3(Entropy):
def __init__(self):
self._tree = None
def _best_splitting(self, X, y, feature_indices): # 这个函数有问题
"""最佳划分"""
if feature_indices:
# 切片并计算特征信息增益
info_gains = np.apply_along_axis(self.information_gain, 0, X[:, feature_indices], y)
max_info_gain_index = np.argmax(info_gains) # 最大信息增益索引
return feature_indices[max_info_gain_index]
return None
def _build_tree(self, X: np.ndarray, y: np.ndarray, feature_indices: list):
"""构造树"""
node = Node() # 创建节点
# 判断样本是否属于同一类别
if len(np.unique(y)) == 1:
node.leaf_node_value = y[0]
return node
# 判断特征集合是否为空或样本在特征上的取值都相同
if len(feature_indices) == 0 or len(np.unique(X, axis=0)) == 1:
n_classes = np.bincount(y) # 统计数组的频数
node.leaf_node_value = np.argmax(n_classes) # 叶子节点值等于数据集类别最多的标记
return node
node.feature_index = self._best_splitting(X, y, feature_indices) # 选择最佳特征 # todo: 这个有问题
feature_values = X[:, node.feature_index] # 特征值
feature_classes = np.unique(feature_values) # 特征值
feature_indices.remove(node.feature_index) # 删除已选择的索引
for c in feature_classes:
mask = c == feature_values
X_sub, y_sub = X[mask], y[mask] # 划分子集
if len(X_sub) < 1:
n_classes = np.bincount(y) # 统计数组的频数
node.leaf_node_value = np.argmax(n_classes) # 叶子节点值等于数据集类别最多的标记
return node
else:
node.children[c] = self._build_tree(X_sub, y_sub, feature_indices.copy()) # 创建子树
return node
def train(self, X, y):
"""训练模型"""
_feature_list = list(range(X.shape[1]))
self._tree = self._build_tree(X, y, _feature_list)
pass
def _predict_one(self, x):
node = self._tree
while node.children:
child = node.children.get(x[node.feature_index])
if not child:
break
node = child
return node.leaf_node_value
def predict(self, X):
"""预测"""
return np.apply_along_axis(self._predict_one, axis=1, arr=X)
def __str__(self):
if hasattr(self, "_tree"):
return str(self._tree)
return ""
第四章 算法评估
ID3算法是比较早的机器学习算法,在1979年Quinlan提出了该算法的思想。它以信息熵为度量标准,划分出决策树特征节点,每次优先选取信息量最多的特征,即使信息熵变为最小的特征,以构造一棵信息熵下降最快的决策树。同时,也暴露以下该算法的缺点:
- ID3算法的节点划分度量标准采用的是信息增益,信息增益偏向选择特征值个数较多的特征。而取值个数较多的特征并不一定是最有特征,所以需要改进选择特征的节点划分度量标准。
- ID3算法递归过程中需要依次计算每个特征值的,对于大型数据会生成比较复杂的决策树:层次和分支都很多,而其中某些分支的特征值概率很小,如果不加忽略就会造成过拟合问题,即决策树对样本数据的分类精度较高,但在测试集上,分类的结果受决策树分支的影响很大。
参考文献
[1] 周志华. 机器学习[M]. 北京: 清华大学出版社, 2016:74-78.
[2] 刘硕. Python机器学习算法原理、实现与案例[M]. 北京: 清华大学出版社, 2019:53-57.
[3] 郑捷. 机器学习算法原理与编程实践[M]. 北京: 电子工业出版社, 2015:100-110.
附件一
参考文献[2]修改代码如下。
import numpy as np
class DecisionTree:
class Node:
def __init__(self):
self.value = None
# 内部节点属性
self.feature_index = None
self.children = {}
def __str__(self):
if self.children:
s = "内部节点<%s>:\n" % self.feature_index
for fv, node in self.children.items():
ss = "[%s]-> %s" % (fv, node)
s += "\t" + ss.replace("\n", "\n\t") + "\n"
else:
s = "叶子节点(%s)" % self.value
return s
def __init__(self, gain_threshold=1e-2):
"""信息增益阈值"""
self.gain_threshold = gain_threshold
@staticmethod
def _entropy(y):
c = np.bincount(y)
p = c[np.nonzero(c)] / y.size
return -np.sum(p * np.log2(p))
def _conditional_entropy(self, feature, y):
feature_values = np.unique(feature)
h = 0.0
for v in feature_values:
y_sub = y[feature == v]
p = y_sub.size / y.size
h += p * self._entropy(y_sub)
return h
def _information_gain(self, feature, y):
return self._entropy(y) - self._conditional_entropy(feature, y)
def _select_feature(self, X, y, features_list):
if features_list:
gains = np.apply_along_axis(self._information_gain, 0, X[:, features_list], y)
index = np.argmax(gains)
if gains[index] > self.gain_threshold:
return index
return None
def _build_tree(self, X, y, features_list):
# 创建节点
node = DecisionTree.Node()
# 统计数据集中样本类标记的个数
labels_count = np.bincount(y)
# 任何情况下节点值总等于数据集中样本最多的类标记
node.value = np.argmax(np.bincount(y))
# 判断类标记是否全部一致
if np.count_nonzero(labels_count) != 1:
# 选择信息增益最大的特征
index = self._select_feature(X, y, features_list)
# 能选择到合适的特征时,创建内部节点,否则创建叶节点
if index is not None:
# 将已选择特征从特征集合中删除
node.feature_index = features_list.pop(index)
# 根据已选择特征的取值划分数据集,并使用数据子集创建子树
features_values = np.unique(X[:, node.feature_index])
for v in features_values:
# 筛选出数据子集
idx = X[:, node.feature_index] == v
X_sub, y_sub = X[idx], y[idx]
# 创建子树
node.children[v] = self._build_tree(X_sub, y_sub, features_list.copy())
return node
def train(self, X_train, y_train):
_, n = X_train.shape
self.tree_ = self._build_tree(X_train, y_train, list(range(n)))
pass
def _predict_one(self, x):
node = self.tree_
while node.children:
child = node.children.get(x[node.feature_index])
if not child:
break
node = child
return node.value
def predict(self, X):
return np.apply_along_axis(self._predict_one, axis=1, arr=X)
def __str__(self):
if hasattr(self, "tree_"):
return str(self.tree_)
return ""