1. DecisionTree
决策树是一种用于解决 分类 和 回归 问题的机器学习方法,其结构就是基本数据结构中的 树 结构,对应于 if-then 规则的集合。和
k
k
k-NN 一样,决策树的模型中也没有明确需要求解的参数,而是根据规则从训练数据中建立树。
要从训练数据中建立一棵可用于解决问题的决策树,需要以下
3
3
3 个步骤
- 特征选择
- 信息增益
- 信息增益比
- 基尼指数
- 决策树生成
- ID 3 算法
- C 4.5 算法
- CART 算法
- 决策树修剪
下面会详细的讲述这些步骤。
1.1 模型
决策树的模型,就是一棵树,内部结点表示 特征,叶结点表示 类。决策树的分类过程,就是给出一个样本,从根结点开始,经历一系列选择,最后落入一个叶结点中,叶结点的类别就是该样本的类别。所以说,决策树相当于 if-then 规则的集合,内部结点就是一个个的 if-then。
从另一个视角,也可以将决策树看做给定特征条件下类的条件概率分布,即 if-then 规则将特征空间划分为不相交的区域,每个叶结点对应一个区域。从根结点到叶结点的一条路径,对应于特征,记为
X
X
X,而该叶结点对应类别
Y
Y
Y,那么在已经确定各个特征取值的基础上 (即确定
x
x
x 的基础上),区域属于每个类别
y
i
y_i
yi 有概率
p
(
y
i
∣
x
)
p(y_i|x)
p(yi∣x),所有的
p
(
y
i
∣
x
)
p(y_i|x)
p(yi∣x) 中最大的那个所对应的
y
i
y_i
yi 就是该区域最有可能属于的类别,也就是说模型可表示为条件概率
P
(
Y
∣
X
)
P(Y|X)
P(Y∣X)。
1.2 策略
决策树模型的策略从条件概率的角度看,和 NaiveBayes 的策略是一样的,即后验概率最大化,确定一系列特征的取值,在此条件下,属于哪一类的可能性最大,就分到哪一类。
1.3 算法
从训练数据中学习决策树模型,就是基于训练数据建立一棵决策树,其主要的 3 3 3 个部分如下
- 特征选择
一条数据会有很多条特征,有的特征和该数据的类别关系很大,而有的特征和该数据的类别关系很小,甚至没有关系。特征选择就是从所有的特征中,选出对该数据类别影响比较大的特征,以其为依据建立 if-then 规则 - 决策树生成
特征选择就决定了 if-then 规则,根据这些规则,就可以建立出一棵决策树 - 决策树修剪
决策树不是越精细越好,不是 if-then 规则划分得越细致越好,因为有可能某些特征和分类结果之间没有相关性,用这些特征建立 if-then 规则有可能导致过拟合。也有可能训练数据存在噪声,也就是说训练数据有误,依据这样的数据进行太精细的划分,也可能导致过拟合。所以,需要适当的修剪决策树,也就是将决策树的某些子树剪枝成为叶结点。
2. 决策树算法与实现
2.1 特征选择
特征选择 就是要从所有的特征中,选出和分类结果相关性较强的特征,用于建立 if-then 规则。可以通过 信息增益,信息增益比,基尼指数 等指标定量的衡量各特征对分类的影响大小。
2.1.1 信息增益
2.1.1.1 基本概念
- 熵 entropy
- 随机变量 X X X 的熵是其概率的 负对数期望,即 H ( X ) = E ( − log p i ) = − ∑ i = 1 n p i log p i H(X)=E(-\log p_i)=-\sum\limits_{i=1}^np_i\log p_i H(X)=E(−logpi)=−i=1∑npilogpi,熵只依赖于随机变量的期望,而不依赖于其取值,因此也可记做 H ( p ) H(p) H(p)
- 熵代表了随机变量的不确定程度,熵越大,随机变量的值越不确定
- 例如当随机变量取某个值的概率为 0 0 0 或 1 1 1 时,随机变量已经完全确定,故熵值为 0 0 0,当概率为 0.5 0.5 0.5 时,随机变量取得该值和不取该值的概率一样大,因此最不能确定它,此时熵取到最大值 1 1 1
- 条件熵 conditional entropy
- 条件熵 H ( Y ∣ X ) H(Y|X) H(Y∣X) 是指在随机变量 X X X 的取值确定后, Y Y Y 的不确定程度
- 可以记为 H ( Y ∣ X ) = ∑ i = 1 n p i H ( Y ∣ X = x i ) H(Y|X)=\sum\limits_{i=1}^np_iH(Y|X=x_i) H(Y∣X)=i=1∑npiH(Y∣X=xi)
- 信息增益 information gain
- 信息增益 也称为 互信息,记为 g ( Y , X ) = H ( Y ) − H ( Y ∣ X ) g(Y,X)=H(Y)-H(Y|X) g(Y,X)=H(Y)−H(Y∣X)
- 其体现了,确定随机变量 X X X,能够让随机变量 Y Y Y 的不确定性降低的程度
- 信息增益越大,说明 X X X 越能确定 Y Y Y
2.1.1.2 计算信息增益
给定训练数据集 D D D 和特征 A A A,计算 A A A 对 D D D 的信息增益 g ( D , A ) g(D,A) g(D,A) 的过程如下
- 计算
D
D
D 的熵
H ( D ) = E ( − log p i ) = − ∑ i = 1 n p i log p i = − ∑ i = 1 n ∣ C i ∣ ∣ D ∣ log ∣ C i ∣ ∣ D ∣ \begin{aligned} H(D)&=E(-\log p_i) \\ &=-\sum\limits_{i=1}^np_i\log p_i \\ &=-\sum\limits_{i=1}^n\frac{|C_i|}{|D|}\log\frac{|C_i|}{|D|} \end{aligned} H(D)=E(−logpi)=−i=1∑npilogpi=−i=1∑n∣D∣∣Ci∣log∣D∣∣Ci∣ - 计算条件熵
H
(
D
∣
A
)
H(D|A)
H(D∣A)
H ( D ∣ A ) = ∑ i = 1 m p i H ( D i ) = − ∑ i = 1 m p i ∑ j = 1 n p j log p j = − ∑ i = 1 m ∣ D i ∣ ∣ D ∣ ∑ j = 1 n ∣ D i j ∣ ∣ D i ∣ log ∣ D i j ∣ ∣ D i ∣ \begin{aligned} H(D|A)&=\sum\limits_{i=1}^mp_iH(D_i) \\ &=-\sum\limits_{i=1}^mp_i\sum_{j=1}^np_j\log p_j \\ &=-\sum\limits_{i=1}^m\frac{|D_i|}{|D|}\sum\limits_{j=1}^n\frac{|D_{ij}|}{|D_i|}\log\frac{|D_{ij}|}{|D_i|} \end{aligned} H(D∣A)=i=1∑mpiH(Di)=−i=1∑mpij=1∑npjlogpj=−i=1∑m∣D∣∣Di∣j=1∑n∣Di∣∣Dij∣log∣Di∣∣Dij∣ - 计算信息增益
g
(
D
,
A
)
g(D,A)
g(D,A)
g ( D , A ) = H ( D ) − H ( D ∣ A ) g(D,A)=H(D)-H(D|A) g(D,A)=H(D)−H(D∣A)
注:
- 第 1 1 1 步中的 p i p_i pi 是训练集中数据属于第 i i i 类的概率
- 第 2 2 2 步中的 p i p_i pi 是训练集中数据的特征 A A A 为第 i i i 种情况的概率, p j p_j pj 是训练集中数据特征 A A A 为第 i i i 种情况的数据中,类别为 j j j 的概率
- 其实这里计算熵和条件熵的公式都是根据训练集由 参数估计 得到的,因此被称为 经验熵 和 经验条件熵
- 参数估计的内容可参考 NaiveBayes
2.1.2 信息增益比
信息增益已经是很好的指标,但仍然存在问题,即信息增益倾向于选择取值比较多的特征。因此用信息增益比来修正这一问题,信息增益比定义为信息增益
g
(
D
,
A
)
g(D,A)
g(D,A) 与训练集
D
D
D 关于特征
A
A
A 的值的熵
H
A
(
D
)
H_A(D)
HA(D) 之比,即
g
R
(
D
,
A
)
=
g
(
D
,
A
)
H
A
(
D
)
g_R(D,A)=\frac{g(D,A)}{H_A(D)}
gR(D,A)=HA(D)g(D,A)
其中
H
A
(
D
)
=
−
∑
i
=
1
n
∣
D
i
∣
∣
D
∣
log
∣
D
i
∣
∣
D
∣
H_A(D)=-\sum\limits_{i=1}^n\frac{|D_i|}{|D|}\log \frac{|D_i|}{|D|}
HA(D)=−i=1∑n∣D∣∣Di∣log∣D∣∣Di∣,即代表数据的特征
A
A
A 取值为
i
i
i 的随机变量的熵
2.1.3 基尼指数
2.1.3.1 基本概念
基尼指数 也用来表示不确定性,和熵一样,基尼指数越大,代表不确定性越大。在分类问题中,若有
N
N
N 个类,样本属于第
i
i
i 类的概率为
p
i
p_i
pi,则定义概率分布的基尼指数如下
Gini
(
p
)
=
∑
i
=
1
N
p
i
(
1
−
p
i
)
=
1
−
∑
i
=
1
N
p
i
2
\begin{aligned} \operatorname{Gini}(p)&=\sum\limits_{i=1}^Np_i(1-p_i) \\ &=1-\sum\limits_{i=1}^Np_i^2 \end{aligned}
Gini(p)=i=1∑Npi(1−pi)=1−i=1∑Npi2
特别地,二分类问题的基尼指数 为
Gini
(
p
)
=
2
p
(
1
−
p
)
\operatorname{Gini}(p)=2p(1-p)
Gini(p)=2p(1−p)
对于给定的样本集合
D
D
D,数据属于类别
C
k
C_k
Ck 的概率为
∣
C
k
∣
∣
D
∣
\frac{|C_k|}{|D|}
∣D∣∣Ck∣,即对样本集合
D
D
D 有基尼指数如下
Gini
(
D
)
=
1
−
∑
i
=
1
N
(
∣
C
k
∣
∣
D
∣
)
2
\operatorname{Gini}(D)=1-\sum\limits_{i=1}^N(\frac{|C_k|}{|D|})^2
Gini(D)=1−i=1∑N(∣D∣∣Ck∣)2
对于某个特征
A
A
A 是否取特定值
a
a
a,可以将集合分割成
D
1
D_1
D1 和
D
2
D_2
D2 两部分,由此有 在特征
A
A
A 的条件下,集合
D
D
D 的基尼指数 为
Gini
(
D
,
A
)
=
∣
D
1
∣
∣
D
∣
Gini
(
D
1
)
+
∣
D
2
∣
∣
D
∣
Gini
(
D
2
)
\operatorname{Gini}(D,A)=\frac{|D_1|}{|D|}\operatorname{Gini}(D_1)+\frac{|D_2|}{|D|}\operatorname{Gini}(D_2)
Gini(D,A)=∣D∣∣D1∣Gini(D1)+∣D∣∣D2∣Gini(D2)
Gini
(
D
,
A
)
\operatorname{Gini}(D,A)
Gini(D,A) 代表确定
A
A
A 后集合
D
D
D 的不确定性,因此
Gini
(
D
,
A
)
\operatorname{Gini}(D,A)
Gini(D,A) 越小,
A
A
A 越能确定最终的分类
2.1.3.2 选取最优特征和最优切分点
注意到 基尼指数 和 信息增益 之间有一些区别,即
- 基尼指数需要确定 是哪一个特征,以及 是该特征的哪一个取值 才能计算出来
- 信息增益则是确定 是哪一个特征 就可以计算出来
因此在选取利用基尼指数做特征选择时,需要计算出所有特征的所有取值的基尼指数,从中选出拥有最小基尼指数 的特征及其取值。例如,有 n n n 个特征,每个特征有 m m m 个取值,那么最终需要计算 n × m n\times m n×m 个基尼指数,最终选出有最小基尼指数的特征及其取值 ( n i , m j ) (n_i,m_j) (ni,mj) 来建立 if-then 规则。因此该过程描述如下
- 对每个特征 A A A,对其可能取的每个值 a a a,根据样本点对 A = a A=a A=a 的测试为 是 或 否,将 D D D 分割为 D 1 D_1 D1 和 D 2 D_2 D2 两部分,计算 A = a A=a A=a 的基尼指数
- 从所有可能的特征 A A A 及所有可能的切分点 a a a 中,选取 基尼指数最小 的特征及其对应切分点为最优特征与最优切分点
2.1.4 特征选择的实现
这里编写一个 feature_selection.py 文件,其中包含各种特征选择指标的计算函数,方便后续进行调用,具体的代码如下
def entropy(p):
"""
计算熵
Args:
p(list): 一个列表, 其中为 y 各种取值出现的次数, 例如 [3, 2, 3] 即 0 类出现 3 次, 1 类出现 2 次, 2 类出现 3 次
Returns:
熵
"""
# 1. 计算各种情况出现的概率
s = sum(p)
p = [i / s for i in p]
# 2. 求负对数期望, 得到熵
ans = sum(-i * log(i, 2) for i in p)
# 3. 返回熵
return ans
def entropy_of_split(X, Y, col):
"""
计算条件熵
Args:
X(ndarray): 训练数据的特征矩阵
Y(ndarray): 训练数据的标签向量
col(int): 指取训练数据的第 col 个特征
Returns:
当 X 的第 col 个特征 A 确定时, Y 的条件熵 H(Y|A)
"""
# 1. 统计 X 的第 col 个特征的每种取值各自出现的次数
val_cnt = Counter(x[col] for x in X)
# 2. 计算第 col 个特征取各个值时的 Y 的熵, 乘以权重得累加得条件熵
ans = 0
for val in val_cnt:
weight = val_cnt[val] / len(X) # 计算第 col 个特征取值 val 出现的概率
entr = entropy(Counter(y for x, y in zip(X, Y) if x[col] == val).values()) # 计算当该特征取值 val 时, Y 的熵
ans += weight * entr # 加入答案
# 3. 返回条件熵
return ans
def info_gain(X, Y, col):
"""
计算信息增益
Args:
X(ndarray): 训练数据的特征矩阵
Y(ndarray): 训练数据的标签向量
col(int): 指取训练数据的第 col 个特征
Returns:
X 的第 col 个特征 A 对于 Y 的信息增益 g(Y, A)
"""
# 1. 计算数据集的熵
entropy_of_X = entropy(Counter(Y).values())
# 2. 计算 X 的第 col 个特征 A 确定时的条件熵 H(Y|A)
entropy_of_col = entropy_of_split(X, Y, col)
# 3. 做减法即得信息增益, 将其返回
return entropy_of_X - entropy_of_col
def info_gain_ratio(X, Y, col):
"""
计算信息增益比
Args:
X(ndarray): 训练数据的特征矩阵
Y(ndarray): 训练数据的标签向量
col(int): 指取训练数据的第 col 个特征
Returns:
X 的第 col 个特征 A 对于 Y 的信息增益比 g_R(Y, A)
"""
# 1. 计算 X 的第 col 个特征 A 对于 Y 的信息增益 g(Y, A)
info_gain_of_col = info_gain(X, Y, col)
# 2. 计算 X 的第 col 个特征 A 对数据集的熵
entropy_of_col = entropy(Counter(x[col] for x in X).values())
# 3. 做除法即得信息增益比, 将其返回
return info_gain_of_col / entropy_of_col
def gini(Y):
"""
计算基尼指数
Args:
Y(ndarray): 训练数据的标签向量
Returns:
基尼指数
"""
# 1. 统计各个类别出现次数
cnt = Counter(Y)
# 2. 对每个类别计算 p_i^2, 进行累加
ans = 0.
for y in cnt:
ans += (cnt[y] / len(Y)) ** 2
# 3. 用 1 去减, 即得基尼指数, 将其返回
return 1 - ans
2.2 决策树生成
决策树生成就是根据特征选择所建立的 if-then 规则来从训练集数据中构建起一棵决策树。根据特征选择的策略不同,有以下三种决策树生成的算法,即 ID 3,C 4.5 和 CART。
2.2.1 ID 3 算法
2.2.1.1 算法描述
ID 3 特征选择的策略是 信息增益,算法描述如下:
Input
:
\textbf{Input}:
Input: 训练集
D
D
D,特征集
A
A
A,阈值
ε
\varepsilon
ε
Output
:
\textbf{Output}:
Output: 决策树
T
T
T
- 基准情形:
- 情况 ( 1 ) (1) (1) D D D 中所有数据均属于同一类 C k C_k Ck: 将 T T T 作为单结点树返回,类标记为 C k C_k Ck
- 情况 ( 2 ) (2) (2) A A A 为空集: 将 T T T 作为单结点树返回,类标记为 D D D 中实例数最大的类 C k C_k Ck
- 递归:
- 计算各个特征对 D D D 的 信息增益 g ( D , A i ) g(D,A_i) g(D,Ai)
- 选择最大 信息增益 的特征
A
g
A_g
Ag
- 情况 ( 1 ) (1) (1) A g < ε A_g<\varepsilon Ag<ε: 将 T T T 作为单结点树返回,类标记为 D D D 中实例数最大的类 C k C_k Ck
- 情况 ( 2 ) (2) (2) A g ≥ ε A_g\geq\varepsilon Ag≥ε: 根据 A g A_g Ag 的每个可能取值 a i a_i ai,将 D D D 分割成若干个不相交集合 D i D_i Di 用于构建子结点,每个子结点的类标记为 D i D_i Di 中实例数最大的类
- 对每个 D i D_i Di,以 A − { A g } A - \{A_g\} A−{Ag} 为特征集,递归的生成决策树
2.2.1.2 算法实现
import numpy as np
from collections import Counter
from feature_selection import *
class ID3:
"""ID3 算法"""
class Node:
"""内部类: 定义树结点"""
def __init__(self, col, Y):
"""
Args:
col(int): 指第 col 个特征
Y(ndarray): 标签向量
"""
self.col = col # 该结点所在层次是按第 col 个特征来划分产生子树
self.children = {} # 该结点的儿子集合
self.cnt = Counter(Y) # 统计标签向量中, 各个类别出现的次数
self.label = self.cnt.most_common(1)[0][0] # 每个结点的类别, 为其所包含数据中最多的那个类别
def __init__(self, info_gain_threshold=0.):
"""
Args:
info_gain_threshold: 信息增益阈值
"""
self.info_gain_threshold = info_gain_threshold
def build(self, X, Y, selected):
"""
建立决策树的方法
Args:
X(ndarray): 特征矩阵
Y(ndarray): 标签向量
selected: 已经被挑选过的特征集合
Returns:
返回建立好的树根结点
"""
# 1. 将传入的数据集作为单个结点, 即构成单结点树
cur = self.Node(None, Y)
# 2. 如果还有特征没有被选择且标签向量 Y 中不止一个类别, 则进行特征选择, 否则直接返回单结点树
if len(selected) != self.feat_cnt and len(set(Y)) > 1:
# 2.1 计算还未被挑选的特征的信息增益, 选择信息增益最大的特征
left_feats = list(set(range(self.feat_cnt)) - selected)
info_gain_arr = [info_gain(X, Y, col) for col in left_feats]
col_idx = np.argmax(info_gain_arr)
best_info_gain = info_gain_arr[col_idx]
col = left_feats[col_idx]
# 2.2 若此时最大的信息增益比阈值更大, 那么遍历该特征的各种取值, 取值相同的放到同一个列表, 用它们递归的生成子树
if best_info_gain > self.info_gain_threshold:
cur.col = col
for val in set(x[col] for x in X):
idx = [x[col] == val for x in X]
child_X = [x for i, x in zip(idx, X) if i]
child_Y = [y for i, y in zip(idx, Y) if i]
cur.children[val] = self.build(child_X, child_Y, selected | {col})
# 3. 返回生成的树的根
return cur
def query(self, root, x):
"""
辅助预测方法: 通过给定的决策树, 来预测一条测试数据 x 的类别
Args:
root: 一棵决策(子)树的根结点
x(ndarray): 一条数据, 即一个特征向量
Returns:
对 x 预测的类别
"""
# 1. 如果给的是单结点树或者 x 的第 root.col 个特征没有与该结点儿子的这个特征取值相同的, 则令 x 的标签与该结点的标签相同, 直接返回
if root.col is None or x[root.col] not in root.children:
return root.label
# 2. 否则递归的在与 x 的第 root.col 个特征取值相同的那棵子树中去查询
return self.query(root.children[x[root.col]], x)
def fit(self, X, Y):
"""
训练方法: 即接收训练数据, 调用 ID3 建立决策树
Args:
X(ndarray): 特征矩阵
Y(ndarray): 标签向量
"""
self.feat_cnt = len(X[0]) # 特征的数量
self.root = self.build(X, Y, set())
def _predict(self, x):
"""
预测的辅助方法
Args:
x(ndarray): 一条数据, 即一个特征向量
Returns:
对 x 预测的类别
"""
return self.query(self.root, x)
def predict(self, X):
"""
预测方法
Args:
X(ndarray): 特征矩阵
Returns:
一个向量, 其中包含对每条数据 x 的预测类别
"""
return [self._predict(x) for x in X]
2.2.2 C 4.5 算法
2.2.2.1 算法描述
C 4.5 特征选择的策略是 信息增益比,和 ID 3 相比较,区别仅仅是特征选择时计算的是信息增益比,而非信息增益,其他地方都没有区别,算法描述如下:
Input
:
\textbf{Input}:
Input: 训练集
D
D
D,特征集
A
A
A,阈值
ε
\varepsilon
ε
Output
:
\textbf{Output}:
Output: 决策树
T
T
T
- 基准情形:
- 情况 ( 1 ) (1) (1) D D D 中所有数据均属于同一类 C k C_k Ck: 将 T T T 作为单结点树返回,类标记为 C k C_k Ck
- 情况 ( 2 ) (2) (2) A A A 为空集: 将 T T T 作为单结点树返回,类标记为 D D D 中实例数最大的类 C k C_k Ck
- 递归:
- 计算各个特征对 D D D 的 信息增益比 g ( D , A i ) g(D,A_i) g(D,Ai)
- 选择最大 信息增益比 的特征
A
g
A_g
Ag
- 情况 ( 1 ) (1) (1) A g < ε A_g<\varepsilon Ag<ε: 将 T T T 作为单结点树返回,类标记为 D D D 中实例数最大的类 C k C_k Ck
- 情况 ( 2 ) (2) (2) A g ≥ ε A_g\geq\varepsilon Ag≥ε: 根据 A g A_g Ag 的每个可能取值 a i a_i ai,将 D D D 分割成若干个不相交集合 D i D_i Di 用于构建子结点,每个子结点的类标记为 D i D_i Di 中实例数最大的类
- 对每个 D i D_i Di,以 A − { A g } A - \{A_g\} A−{Ag} 为特征集,递归的生成决策树
2.2.2.2 算法实现
因为它和 ID3 算法的区别仅在于选择特征时采用 信息增益比 而非 信息增益,因此其实现和 ID3 相比也仅有 build 方法处有所不同,故此处只给出 build 方法
def build(self, X, Y, selected):
"""
建立决策树的方法
Args:
X(ndarray): 特征矩阵
Y(ndarray): 标签向量
selected: 已经被挑选过的特征集合
Returns:
返回建立好的树根结点
"""
# 1. 将传入的数据集作为单个结点, 即构成单结点树
cur = self.Node(None, Y)
# 2. 如果还有特征没有被选择且标签向量 Y 中不止一个类别, 则进行特征选择, 否则直接返回单结点树
if len(selected) != self.feat_cnt and len(set(Y)) > 1:
# 2.1 计算还未被挑选的特征的信息增益比, 选择信息增益比最大的特征
left_feats = list(set(range(self.feat_cnt)) - selected)
info_gain_ratio_arr = [info_gain_ratio(X, Y, col) for col in left_feats]
col_idx = np.argmax(info_gain_ratio_arr)
best_info_gain_ratio = info_gain_ratio_arr[col_idx]
col = left_feats[col_idx]
# 2.2 若此时最大的信息增益比比阈值更大, 那么遍历该特征的各种取值, 取值相同的放到同一个列表, 用它们递归的生成子树
if best_info_gain > self.info_gain_threshold:
cur.col = col
for val in set(x[col] for x in X):
idx = [x[col] == val for x in X]
child_X = [x for i, x in zip(idx, X) if i]
child_Y = [y for i, y in zip(idx, Y) if i]
cur.children[val] = self.build(child_X, child_Y, selected | {col})
# 3. 返回生成的树的根
return cur
2.2.3 CART 算法
2.2.3.1 算法描述
CART 生成的决策树显然是一棵二叉决策树,算法的核心就是计算基尼指数,并借助基尼指数筛选出每一次的最优特征和最优切分点。具体地,算法描述如下:
Input
:
\textbf{Input}:
Input: 训练集
D
D
D,停止计算的条件
Output
:
\textbf{Output}:
Output: 决策树
T
T
T
- 基准情形:
- 若满足停止条件,则返回决策树 T T T
- 递归:
- 计算基尼指数,选出最优特征和最优切分点
- 根据选出的最优特征和最优切分点将 D D D 分割为 D 1 D_1 D1 和 D 2 D_2 D2 两部分
- 对两个子结点递归的调用 CART 算法
注: 算法停止的条件可以是以下内容
- 结点中的样本数小于预定阈值: 类标记为结点中实例数最大的类 C k C_k Ck
- 样本集的基尼指数小于预定阈值: 类标记为结点中实例数最大的类 C k C_k Ck
- 没有更多特征: 没有特征了,那最终分到一个结点中的肯定完全相同,类标记就为此类
2.2.3.2 算法实现
2.3 决策树修剪
修剪决策树的过程也被称为 剪枝 Pruning,目的是防止过拟合。具体地,剪枝是从已生成的树上裁剪掉一些子树或叶结点,将其根结点或父节点作为新的叶结点,从而降低决策树模型的复杂程度,避免过拟合。
2.3.1 简单剪枝算法
简单剪枝算法的想法是,模型既要能够较好的拟合训练数据集,又应该拥有较简单的模型复杂度。显然两个要求不可兼得,拟合训练集的效果越好,复杂度就越高,复杂度越低,拟合训练集的效果就越差,因此剪枝的目标就是在这二者中权衡,取到最满意的效果。
2.3.1.1 损失函数
要实现算法,首先需要能够定量的描述 拟合效果 与 模型复杂度 这两个概念
- 对预测数据的训练误差 C ( T ) C(T) C(T): 显然 C ( T ) C(T) C(T) 越小,拟合效果越好
- 叶结点数 ∣ T ∣ |T| ∣T∣: 显然,叶结点数越多,说明模型分类越细致,复杂度越高
因此,我们的目标是使
C
(
T
)
C(T)
C(T) 和
∣
T
∣
|T|
∣T∣ 都比较小,因而得到剪枝的损失函数如下:
C
α
(
T
)
=
C
(
T
)
+
α
∣
T
∣
=
∑
t
=
1
∣
T
∣
N
t
H
t
(
T
)
+
α
∣
T
∣
=
−
∑
t
=
1
∣
T
∣
∑
i
=
1
N
N
t
i
log
N
t
i
N
t
+
α
∣
T
∣
\begin{aligned} C_\alpha(T)&=C(T)+\alpha|T| \\ &=\sum\limits_{t=1}^{|T|}N_tH_t(T)+\alpha|T| \\ &=-\sum\limits_{t=1}^{|T|}\sum\limits_{i=1}^NN_{ti}\log\frac{N_{ti}}{N_t}+\alpha|T| \end{aligned}
Cα(T)=C(T)+α∣T∣=t=1∑∣T∣NtHt(T)+α∣T∣=−t=1∑∣T∣i=1∑NNtilogNtNti+α∣T∣
其中对训练集的预测误差
C
(
T
)
=
∑
t
=
1
∣
T
∣
N
t
H
t
(
T
)
C(T)=\sum\limits_{t=1}^{|T|}N_tH_t(T)
C(T)=t=1∑∣T∣NtHt(T),即将叶结点的经验熵与叶结点中数据个数的乘积累加起来,代表了所有结点的不确定性。显然,不确定性越大,误差越大,不确定性越小,误差越小。
2.3.1.2 剪枝算法
Input
:
\textbf{Input}:
Input: 生成算法产生的树
T
T
T,参数
α
\alpha
α
Output
:
\textbf{Output}:
Output: 修剪后的树
T
α
T_\alpha
Tα,及树
T
α
T_\alpha
Tα 的损失函数值
- 计算将该树在根处剪枝,得到的单结点树的损失函数值 prune_loss
- 基准情形:
- 若该树是单结点,则无须剪枝,直接返回单结点树的损失函数值
- 递归
- 递归的在其各个儿子上进行剪枝操作,累加其各个儿子剪枝后产生子树的损失函数值 cur_loss
- 若 prune_loss <= cur_loss: 进行剪枝,并返回产生单结点树的损失函数值
- 若 prune_loss > cur_loss: 不剪枝,返回 cur_loss
2.3.1.3 剪枝算法实现
此处给出一个简单剪枝算法的实现
import numpy as np
from collections import Counter
from feature_selection import *
from ID3 import ID3
def prune(root, X, Y, alpha=.0):
"""
剪枝算法:
Args:
root: 传入的树根结点
X(ndarray): 训练数据的特征矩阵
Y(ndarray): 训练数据的真实类别向量
alpha: 控制决策树拟合程度与模型复杂度之间影响的参数
Returns:
在该点处进行 prune 操作后产生的树的损失函数值
"""
# 1. 计算将该树在此处剪枝 (即将该树中包含的数据变为一个结点) 时的损失
pruned_entropy = len(X) * entropy(Counter(Y).values())
pruned_loss = pruned_entropy + alpha # 每个结点处加上一个 alpha, 有 |T| 个叶结点, 累加起来就是 alpha|T|
# 2. 若它本身就是一个结点, 则也不必剪枝, 直接返回该单结点树的损失函数值
if not root.children:
return pruned_loss
# 3. 递归的在各个儿子上进行 prune 操作, 累加得到不在 root 处剪枝时的损失函数值
cur_loss = 0.
for col_val in root.children:
child = root.children[col_val]
idx = [x[root.col] == col_val for x in X]
childX = [x for i, x in zip(idx, X) if i]
childY = [y for i, y in zip(idx, Y) if i]
cur_loss += prune(child, childX, childY, alpha)
# 4. 若是在 root 处剪枝后得到的单结点树的损失 < 不在 root 处剪枝的树的损失, 则剪枝 (清空所有儿子), 返回单结点树的损失, 否则返回不在该点处剪枝, 保留下来的树的损失
if pruned_loss < cur_loss:
root.children.clear()
return pruned_loss
else:
return cur_loss
2.3.2 CART 剪枝算法
3. 回归决策树及其实现
前文已经说过,决策树是一种基本的 分类 与 回归 方法,即决策树既可以用于分类,也可以用于回归。而之前所述,均为 分类决策树 的生成算法,而没有涉及到 回归决策树。实际上 2.2.3 2.2.3 2.2.3 中所述的 CART 的全称是 classification and regression tree,即 分类与回归树。因此,在这里叙述 CART 回归树的生成算法。
3.1 决策树生成
3.1.1 输出
对于分类问题,输出是 实例的类别,分类决策树决定输出的策略很简单,对于一个叶子结点,其包含的实例中占比最大的类别,就是该结点的类别。而对于回归问题,输出是一个 实数值,那么应该怎么决定一个叶子结点的输出呢
?
?
?
CART 回归树的策略是,叶子结点所包含实例的
y
i
y_i
yi 的均值
c
^
m
\hat{c}_m
c^m 为该叶子结点的输出值,即如下式子所示
c
^
m
=
ave
(
y
i
∣
x
i
∈
R
m
)
\hat{c}_m=\text{ave}(y_i|x_i\in\mathbf{R_m})
c^m=ave(yi∣xi∈Rm)
其中
R
m
\mathbf{R_m}
Rm 代表第
m
m
m 个叶子结点,将该结点中所有实例的
y
i
y_i
yi 求均值,即得叶子结点输出。
之所以采用这个策略,归根结底是因为 CART 回归树用 平方误差最小化准则 来生成决策树。显然,当已经确定了一个叶子结点
R
m
\mathbf{R_m}
Rm 时,要让平方误差最小化,也就是如下式所表达之意
min
∑
x
i
∈
R
m
(
y
i
−
c
^
m
)
2
\min\limits\sum\limits_{x_i\in\mathbf{R_m}}(y_i-\hat{c}_m)^2
minxi∈Rm∑(yi−c^m)2
显然,当
c
^
m
\hat{c}_m
c^m 取值为叶子结点中所有
x
i
x_i
xi 对应
y
i
y_i
yi 均值时,上式取得最小值。
3.1.2 空间划分准则
这里的空间划分准则,对应的就是如何生成左右儿子的准则。显然,每个变量有一个取值范围,记第 j j j 个变量为 x ( j ) x^{(j)} x(j),其一个取值为 s s s,那么就可以根据这个变量及其取值将数据集一分为二,分别记为 R 1 R_1 R1 和 R 2 R_2 R2,如此就得到了左右儿子。具体地,我们要做的事情有两件
- 选择哪个一个变量 x x x
- 选择该变量的哪一个取值 s s s
解决了这两件事,我们也就得到了空间划分的准则。
CART 回归树的策略是求解下式,得到最优切分变量
j
j
j 和最优切分点
s
s
s
arg min
j
,
s
(
min
c
1
∑
x
i
∈
R
1
(
j
,
s
)
(
y
i
−
c
1
)
2
+
min
c
2
∑
x
i
∈
R
2
(
j
,
s
)
(
y
i
−
c
2
)
2
)
\argmin\limits_{j, s}\left(\min\limits_{c_1}\sum\limits_{x_i\in R_1\left(j, s\right)}\left(y_i-c_1\right)^2+\min\limits_{c_2}\sum\limits_{x_i\in R_2\left(j, s\right)}\left(y_i-c_2\right)^2\right)
j,sargmin⎝⎛c1minxi∈R1(j,s)∑(yi−c1)2+c2minxi∈R2(j,s)∑(yi−c2)2⎠⎞
即,求取使得划分后的两部分的 平方误差 总和最小的变量及取值。
显然,
min
c
1
=
c
^
1
=
ave
(
y
i
∣
x
i
∈
R
1
(
j
,
s
)
)
min
c
2
=
c
^
2
=
ave
(
y
i
∣
x
i
∈
R
2
(
j
,
s
)
)
\begin{aligned} \min\limits c_1=\hat{c}_1=\text{ave}(y_i|x_i\in R_1(j,s)) \\ \min\limits c_2=\hat{c}_2=\text{ave}(y_i|x_i\in R_2(j,s)) \end{aligned}
minc1=c^1=ave(yi∣xi∈R1(j,s))minc2=c^2=ave(yi∣xi∈R2(j,s))
也就是说,确定了
j
j
j 和
s
s
s 之后,就得到了对应的
c
^
1
\hat{c}_1
c^1 和
c
^
2
\hat{c}_2
c^2,从而可以轻易的算出上式的值。
又由于
j
j
j 的取值范围是 离散 的,
s
s
s 的取值范围是 连续 的,因此,取顶一个
j
j
j 后,可以找到对应的最优
s
s
s。我们遍历每个
j
j
j,分布求取对应的最佳
s
s
s。就得到了一系列的
(
j
,
s
)
(j, s)
(j,s) 对,其中使得式子取值最小的
j
j
j 和
s
s
s 就是 最优切分变量 和 最优切分点 了。
显然,我们将数据集按此准则一份为二,得到两个子数据集,再递归地应用此准则于子数据集之上,反复直到满足停止条件,最终就可以得到一棵回归树,称为 最小二乘回归树。