决策树是一种用于解决 分类 和 回归 问题的机器学习方法,其结构就是基本数据结构中的 树 结构,对应于 if-then 规则的集合。和 k k k-NN 一样,决策树的模型中也没有明确需要求解的参数,而是根据规则从训练数据中建立树。
要从训练数据中建立一棵可用于解决问题的决策树,需要以下 3 3 3 个步骤
下面会详细的讲述这些步骤。
决策树的模型,就是一棵树,内部结点表示 特征,叶结点表示 类。决策树的分类过程,就是给出一个样本,从根结点开始,经历一系列选择,最后落入一个叶结点中,叶结点的类别就是该样本的类别。所以说,决策树相当于 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)。
决策树模型的策略从条件概率的角度看,和 NaiveBayes 的策略是一样的,即后验概率最大化,确定一系列特征的取值,在此条件下,属于哪一类的可能性最大,就分到哪一类。
从训练数据中学习决策树模型,就是基于训练数据建立一棵决策树,其主要的 3 3 3 个部分如下
特征选择 就是要从所有的特征中,选出和分类结果相关性较强的特征,用于建立 if-then 规则。可以通过 信息增益,信息增益比,基尼指数 等指标定量的衡量各特征对分类的影响大小。
给定训练数据集 D D D 和特征 A A A,计算 A A A 对 D D D 的信息增益 g ( D , A ) g(D,A) g(D,A) 的过程如下
注:
信息增益已经是很好的指标,但仍然存在问题,即信息增益倾向于选择取值比较多的特征。因此用信息增益比来修正这一问题,信息增益比定义为信息增益 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 的随机变量的熵
基尼指数 也用来表示不确定性,和熵一样,基尼指数越大,代表不确定性越大。在分类问题中,若有 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 越能确定最终的分类
注意到 基尼指数 和 信息增益 之间有一些区别,即
因此在选取利用基尼指数做特征选择时,需要计算出所有特征的所有取值的基尼指数,从中选出拥有最小基尼指数 的特征及其取值。例如,有 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 规则。因此该过程描述如下
这里编写一个 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
决策树生成就是根据特征选择所建立的 if-then 规则来从训练集数据中构建起一棵决策树。根据特征选择的策略不同,有以下三种决策树生成的算法,即 ID 3,C 4.5 和 CART。
ID 3 特征选择的策略是 信息增益,算法描述如下:
Input : \textbf{Input}: Input: 训练集 D D D,特征集 A A A,阈值 ε \varepsilon ε
Output : \textbf{Output}: Output: 决策树 T T T
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]
C 4.5 特征选择的策略是 信息增益比,和 ID 3 相比较,区别仅仅是特征选择时计算的是信息增益比,而非信息增益,其他地方都没有区别,算法描述如下:
Input : \textbf{Input}: Input: 训练集 D D D,特征集 A A A,阈值 ε \varepsilon ε
Output : \textbf{Output}: Output: 决策树 T T T
因为它和 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
CART 生成的决策树显然是一棵二叉决策树,算法的核心就是计算基尼指数,并借助基尼指数筛选出每一次的最优特征和最优切分点。具体地,算法描述如下:
Input : \textbf{Input}: Input: 训练集 D D D,停止计算的条件
Output : \textbf{Output}: Output: 决策树 T T T
注: 算法停止的条件可以是以下内容
修剪决策树的过程也被称为 剪枝 Pruning,目的是防止过拟合。具体地,剪枝是从已生成的树上裁剪掉一些子树或叶结点,将其根结点或父节点作为新的叶结点,从而降低决策树模型的复杂程度,避免过拟合。
简单剪枝算法的想法是,模型既要能够较好的拟合训练数据集,又应该拥有较简单的模型复杂度。显然两个要求不可兼得,拟合训练集的效果越好,复杂度就越高,复杂度越低,拟合训练集的效果就越差,因此剪枝的目标就是在这二者中权衡,取到最满意的效果。
要实现算法,首先需要能够定量的描述 拟合效果 与 模型复杂度 这两个概念
因此,我们的目标是使 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),即将叶结点的经验熵与叶结点中数据个数的乘积累加起来,代表了所有结点的不确定性。显然,不确定性越大,误差越大,不确定性越小,误差越小。
Input : \textbf{Input}: Input: 生成算法产生的树 T T T,参数 α \alpha α
Output : \textbf{Output}: Output: 修剪后的树 T α T_\alpha Tα,及树 T α T_\alpha Tα 的损失函数值
此处给出一个简单剪枝算法的实现
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.2.3 2.2.3 2.2.3 中所述的 CART 的全称是 classification and regression tree,即 分类与回归树。因此,在这里叙述 CART 回归树的生成算法。
对于分类问题,输出是 实例的类别,分类决策树决定输出的策略很简单,对于一个叶子结点,其包含的实例中占比最大的类别,就是该结点的类别。而对于回归问题,输出是一个 实数值,那么应该怎么决定一个叶子结点的输出呢 ? ? ?
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 均值时,上式取得最小值。
这里的空间划分准则,对应的就是如何生成左右儿子的准则。显然,每个变量有一个取值范围,记第 j j j 个变量为 x ( j ) x^{(j)} x(j),其一个取值为 s s s,那么就可以根据这个变量及其取值将数据集一分为二,分别记为 R 1 R_1 R1 和 R 2 R_2 R2,如此就得到了左右儿子。具体地,我们要做的事情有两件
解决了这两件事,我们也就得到了空间划分的准则。
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 就是 最优切分变量 和 最优切分点 了。
显然,我们将数据集按此准则一份为二,得到两个子数据集,再递归地应用此准则于子数据集之上,反复直到满足停止条件,最终就可以得到一棵回归树,称为 最小二乘回归树。