前言
决策树(design tree)是一种基本的分类与回归的方法。在学习过程中,利用训练数据,根据损失函数最小化原则建立决策树模型。预测时,对于新的数据用决策树模型进行分类。决策树学习通常包括三个步骤:特征选择、决策树的生成和决策树的修剪。
一、决策树是什么?
决策树表示基于特征对实例进行分类的树形结构,从给定的训练数据集中,递归选择最优划分特征,依据此特征对训练数据集进行划分,直到结点符合停止条件。决策树可以看作是一系列 if-then
规则的集合。
二、信息增益
- 熵是表示随机变量不确定性的度量,设
X
X
X是一个有限个值的离散随机变量,其概率分布为:
P ( X = x i ) = p i , i = 1 , 2 , . . . , n P(X = x_i) = p_i, i=1,2,...,n P(X=xi)=pi,i=1,2,...,n
则随机变量 X X X的熵定义为:
H ( p ) = − ∑ i = 1 n p i log p i H(p)=-\sum_{i=1}^{n}p_i\log{p_i} H(p)=−i=1∑npilogpi - 条件熵
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_{i=1}^{n}p_iH(Y|X=x_i) H(Y∣X)=i=1∑npiH(Y∣X=xi)
当熵和条件熵中的概率由数据估计得到时(如极大似然估计),所对应的熵和条件熵分别称为经验熵和经验条件熵
- 信息增益(表示得知特征
X
X
X的信息而使类
Y
Y
Y信息的不确定性减少的程度)
特征 A A A对于训练数据集D的信息增益 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)
信息增益大的具有更强的分类能力
- 信息增益比:由于以信息增益作为划分数据集的特征,存在偏向于取值较多的特征,所以用信息增益比作为划分标准
特征A对训练数据D的信息增益比g_R(D,A)定义为其信息增益g(D,A)与训练数据集D关于特征A的值的熵之比:
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 2 ∣ D i ∣ ∣ D ∣ H_A(D) = -\sum_{i=1}^{n}\frac{|D_i|}{|D|}\log_2\frac{|D_i|}{|D|} HA(D)=−i=1∑n∣D∣∣Di∣log2∣D∣∣Di∣
n是特征值A的取值个数
三、决策树算法
1. ID3算法
输入:训练数据集
D
D
D,特征集
A
A
A
输出: 决策树
T
T
T
- 若 D D D中所有的实例都属于同一类 C k C_k Ck,则 T T T为单节点树,并将 C k C_k Ck该节点类的标记,返回 T T T;
- 若 A A A为空集,则 T T T为单节点树,并将 D D D中实例数最大的类 C k C_k Ck作为该节点的类标记,返回T;
- 否则,计算 A A A中各个特征对于 D D D的信息增益,选择信息增益最大的特征 A g A_g Ag;
- 对于 A g A_g Ag中的每一个可能取值 a i a_i ai,依据该值将 D D D划分为若干非空子集 D i D_i Di构建子节点;
- 对于每一个子节点以 A − A g A-A_g A−Ag为特征集,递归调用1-4;
2. C4.5的生成算法
输入:训练数据集
D
D
D,特征集
A
A
A
输出: 决策树
T
T
T
- 若 D D D中所有的实例都属于同一类 C k C_k Ck,则 T T T为单节点树,并将 C k C_k Ck该节点类的标记,返回 T T T;
- 若 A A A为空集,则 T T T为单节点树,并将 D D D中实例数最大的类 C k C_k Ck作为该节点的类标记,返回T;
- 否则,计算 A A A中各个特征对于 D D D的信息增益比,选择信息增益比最大的特征 A g A_g Ag;
- 对于 A g A_g Ag中的每一个可能取值 a i a_i ai,依据该值将 D D D划分为若干非空子集 D i D_i Di构建子节点;
- 对于每一个子节点以 A − A g A-A_g A−Ag为特征集,递归调用1-4;
3. CART算法
- 基尼指数:表示一个集合的不确定性
在分类问题中,假设有K个类,样本点属于第k类的概率为p_K,则概率分布的基尼指数定义为
G i n i ( p ) = ∑ k = 1 K p k ( 1 − p k ) = 1 − ∑ k = 1 K p k 2 Gini(p) = \sum_{k=1}^{K}p_k(1-p_k)=1- \sum_{k=1}^{K}p_k^2 Gini(p)=k=1∑Kpk(1−pk)=1−k=1∑Kpk2
在特征A的条件下,集合D的基尼指数为
G i n i ( D ∣ A ) = ∑ i = 1 n ∣ D i ∣ ∣ D ∣ G i n i ( D i ) Gini(D|A) = \sum_{i=1}^{n}\frac{|D_i|}{|D|}Gini(D_i) Gini(D∣A)=i=1∑n∣D∣∣Di∣Gini(Di)
n为A的不同取值个数
- 最小二乘回归树生成算法(过程如C4.5,只是划分节点时所用的指标不同)
- 将输入空间划分为 M M M个区域R1,R2,…,RM,当输入空间划分确定后,用平方误差来表示回归树对于训练数据的的预测误差,各个区域的输出值分别为c1,c2,…,cM,当取ci时各区域输出的平方误差之和最小。
- 对于每个区域的预测标签采用平均值代替
- 分类树的生成
- 计算现有特征对于数据集 D D D的基尼指数,对于每一个特征 A A A,对其可能取的每个值 a a a,根据样本点 A = a A=a A=a的测试,将D分割成俩部分 D 1 D_1 D1和 D 2 D_2 D2
- 对于所有的切分点,选择基尼指数最小的特征及切分点作为最优特征和最优切分点
四、决策树的剪枝
- 预剪枝:在每一次实际对结点进行进一步划分之前,先采用某一种指标来判断划分是否能提高增益,如验证集的数据的准确性、信息增益是否大于最低标准、样本个数是否小于最低标准等,如果是,就把结点标记为叶结点并退出进一步划分,否则就继续递归生成结点。
- 后剪枝:后剪枝则是先从训练集生成一颗完整的决策树,然后自底向上地对非叶结点进行考察,若将该结点对应的子树替换为叶结点能带来泛化性能提升(如验证集的准确率),则将该子树替换为叶结点。
具体地,对于ID3和C4.5,可以使用如下损失函数来判断是否剪枝:
C α ( T ) = ∑ t = 1 ∣ T ∣ N t H t ( T ) + α ∣ T ∣ C_\alpha(T)=\sum_{t=1}^{|T|}N_tH_t(T)+\alpha|T| Cα(T)=t=1∑∣T∣NtHt(T)+α∣T∣
H t ( T ) = − ∑ k N t k N t log N t k N t H_t(T)=-\sum_{k}\frac{N_{tk}}{N_t}\log \frac{N_{tk}}{N_t} Ht(T)=−k∑NtNtklogNtNtk
其中T为叶结点数量, α \alpha α控制经验风险与结构风险所占比例,越小树越复杂,过拟合风险越大。
若将非叶子结点替换为叶子结点后损失函数降低,则将该子树替换为叶结点。
对于CART树,剪枝算法由两步组成:首先从生成算法产生的决策树T_0底端开始不断剪枝,直到根节点,形成一个子树序列 T 0 , . . . , T n {T_0,...,T_n} T0,...,Tn;然后通过交叉验证法在独立的验证数据集上对子树序列进行测试,从中选取最优子树。
定义子树的损失函数为:
C α ( T ) = C ( T ) + α ∣ T ∣ C_\alpha(T)=C(T)+\alpha|T| Cα(T)=C(T)+α∣T∣
其中,T为任意子树,C(T)为对训练数据的预测误差(基尼指数、平方误差),|T|为子树的叶节点个数, α > 0 \alpha>0 α>0为参数, C α ( T ) C_\alpha(T) Cα(T)为参数是 α \alpha α时T的整体损失, α \alpha α权衡训练数据的拟合程度与模型的复杂度。
固定的 α \alpha α,一定存在使损失函数 C α ( T ) C_\alpha(T) Cα(T)最小的唯一最小子树。当 α \alpha α较大时,子树偏小;当 α \alpha α较小时,子树偏大。考虑将 α \alpha α从小增大,得到 0 = α 0 < α 1 < . . . < α n < + ∞ 0=\alpha_0<\alpha_1<...<\alpha_n<+\infty 0=α0<α1<...<αn<+∞,每个区间 [ α i , α i + 1 ) [\alpha_i,\alpha_{i+1}) [αi,αi+1)都对应一棵剪枝得到的子树,子树序列为 { T 0 , T 1 , . . . , T n } \{T_0,T_1,...,T_n\} {T0,T1,...,Tn},序列中的子树是嵌套的。
对任意子树 T t T_t Tt,其剪枝前的损失为:
C α ( T t ) = C ( T t ) + α ∣ T t ∣ C_\alpha(T_t)=C(T_t)+\alpha|T_t| Cα(Tt)=C(Tt)+α∣Tt∣
以t为单结点的子树的损失为:
C α ( t ) = C ( t ) + α C_\alpha(t)=C(t)+\alpha Cα(t)=C(t)+α
当 α \alpha α为0或者充分小的时候, C α ( T t ) < C α ( t ) C_\alpha(T_t)<C_\alpha(t) Cα(Tt)<Cα(t)。当 α \alpha α从0开始增大时, C α ( T t ) C_\alpha(T_t) Cα(Tt)的增幅要比 C α ( t ) C_\alpha(t) Cα(t)大,因此存在一个 α \alpha α使得二者相等,易得二者相等时:
α = C ( t ) − C ( T t ) ∣ T t ∣ − 1 \alpha=\frac{C(t)-C(T_t)}{|T_t|-1} α=∣Tt∣−1C(t)−C(Tt)
此时二者有相同的损失函数值,而t的结点少,所以选择对 T t T_t Tt剪枝。
为此,对整体树 T 0 T_0 T0中每一个内部结点t计算:
g ( t ) = C ( t ) − C ( T t ) ∣ T t ∣ − 1 g(t)=\frac{C(t)-C(T_t)}{|T_t|-1} g(t)=∣Tt∣−1C(t)−C(Tt)
它表示该内部结点被剪枝的阈值。为了得到一个从小到大的 α \alpha α序列,每次选择剪去 g ( t ) g(t) g(t)最小的内部结点。容易证明剪去该结点后包含该结点的剩余内部结点对应的阈值会增大,因此不必担心得到的 α \alpha α序列不是单调递增的。
最后利用独立的验证数据集,测试子树序列 T 0 , . . . , T n {T_0,...,T_n} T0,...,Tn中各个子树的平方误差或基尼指数。平方误差或者基尼指数最小的的决策树被认为是最优的决策树。
五、三种算法的代码实现
代码如下:
"""
决策树
ID3 C4.5 CART
"""
import numpy as np
from sklearn.datasets import load_digits # 手写数据集
from sklearn.datasets import fetch_california_housing
import random
class Node(object):
'''
树节点
'''
def __init__(self, is_leaf, label=None, split_attr=None):
super().__init__()
self.is_leaf = is_leaf # 是否是叶子节点
self.label = label # 叶节点所属于的类别
self.split_attr = split_attr # 划分所依据的属性
self.childs = {} # 保存孩子节点
class DescionTree(object):
def __init__(self, _type, predict_type, thres_value, max_depth=float('inf'), split_count=10) -> None:
'''
:param _type: 算法类型 ['ID3', 'C4.5', 'CART']
:param predict_type: 预测类型 ['classification','regression']
:param thres_value: 阈值
:param max_depth: 树的最大深度
:param split_count: 对于连续特征值的划分区间个数
'''
super().__init__()
assert _type in ['ID3', 'C4.5', 'CART']
assert predict_type in ['classification', 'regression']
self._type = _type
self.predict_type = predict_type
self.thres_value = thres_value
self.max_depth = max_depth
self.split_count = split_count
if self._type != 'CART' and self.predict_type == 'regressiom':
raise NotImplemented()
self.tree = None
self.predict_values = None
def build_tree(self, train_xs, train_ys, attrs_idxs, attr_types, example_idxs, depth):
'''
构建决策树
:param train_xs: 样本特征
:param train_ys: 样本标签
:param attrs_idxs: 根节点到当前节点的还未使用的划分属性的索引
:param attr_types: 属性的类型 0 ——> 离散, 1——>连续
:param example_idxs: 样本的索引
:param depth: 当前树的深度
:return:
'''
if self.predict_values is None:
self.predict_values = np.zeros((train_xs.shape[0],))
m, n = train_xs.shape
tree = Node(False)
# 判断是否符合终止条件
# 样本数为0、所有样本属于同一类别、每个特征都只有一种取值、树深度超过最大深度
if len(train_xs) == 0 or len(np.unique(train_ys)) == 1 or len(attrs_idxs) == 0 or \
depth >= self.max_depth:
tree.is_leaf = True
tree.label = self._majority_vote(train_ys)
if example_idxs is not None:
for example_idx in example_idxs:
self.predict_values[example_idx] = tree.label
return tree
if self._type in ['ID3', 'C4.5']:
original_entropy = self._calculate_entropy(train_ys) # 原始数据的经验熵
entropy_gains = [] # 保存信息增益
entropy_gain_ratios = [] # 保存信息增益比
# CART 对于分类问题,保存分割后的最低基尼指数、分割特征、特征取值
# CART 对于回归问题,保存分割后的最低平方误差、分割特征、特征阈值
cart_split_cache = [float('inf'), None, None]
for index in range(len(attrs_idxs)):
attr_idx = attrs_idxs[index]
if attr_types[attr_idx] == 0: # 当特征值为离散的时
uniques, counts = np.unique(train_xs[:, attr_idx], return_counts=True)
if self._type in ['ID3', 'C4.5']:
conditional_emp_entropy = 0
for i in range(len(uniques)):
value, count = uniques[i], counts[i]
conditional_emp_entropy += (count / m) * \
self._calculate_entropy(train_ys[train_xs[:, attr_idx] == value]) # 计算条件经验熵
entropy_gain = original_entropy - conditional_emp_entropy # 计算信息增益
entropy_gains.append(entropy_gain)
if self._type == 'C4.5':
h = max(self._calculate_entropy(train_xs[:, attr_idx]), 1e-4)
entropy_gain_ratio = entropy_gain / h
entropy_gain_ratios.append(entropy_gain_ratio)
elif self._type == 'CART':
for i in range(len(uniques)):
value, count = uniques[i], counts[i]
if self.predict_type == 'classification':
probs = count / m
# 计算分割后的基尼指数
metric = probs * self._calculate_gini(train_ys[train_xs[:, attr_idx] == value]) \
+ (1 - probs) * self._calculate_gini(train_ys[train_xs[:, attr_idx] != value])
else:
# 计算分割后的平方误差
metric = self._calculate_square_error(train_ys[train_xs[:, attr_idx] == value]) + \
self._calculate_square_error(train_ys[train_xs[:, attr_idx] != value])
if metric < cart_split_cache[0]:
cart_split_cache[0] = metric
cart_split_cache[1] = attr_idx
cart_split_cache[2] = value
else: # 当特征值为连续时
if self._type in ['ID3', 'C4.5']:
raise NotImplemented()
else:
_min, _max = np.min(train_xs[:, attr_idx]), np.max(train_xs[:, attr_idx])
step = (_max - _min) / self.split_count
for i in range(1, self.split_count):
threshold = _min + step * i
left_count = (train_xs[:, attr_idx] <= threshold).sum()
if self.predict_type == 'classification':
probs = left_count / m
# 计算分割后的基尼指数
metric = probs * self._calculate_gini(train_ys[train_xs[:, attr_idx] <= threshold]) \
+ (1 - probs) * self._calculate_gini(train_ys[train_xs[:, attr_idx] > threshold])
else:
# 计算分割后的平方误差
metric = self._calculate_square_error(train_ys[train_xs[:, attr_idx] <= threshold]) + \
self._calculate_square_error(train_ys[train_xs[:, attr_idx] > threshold])
if metric < cart_split_cache[0]:
cart_split_cache[0] = metric
cart_split_cache[1] = attr_idx
cart_split_cache[2] = threshold
if self._type in ['ID3', 'C4.5']:
if self._type == 'ID3':
best_feat = attrs_idxs[np.argmax(entropy_gain)]
else:
entropy_gain_mean = np.mean(entropy_gain)
best_gain_ratio = -1
best_feat = -1
for index, (gain, gain_ratio) in enumerate(zip(entropy_gains, entropy_gain_ratios)):
if gain >= entropy_gain_mean and gain_ratio >= best_gain_ratio:
best_gain_ratio = gain_ratio
best_feat = attrs_idxs[index]
tree.split_attr = best_feat
uniques = np.unique(train_xs[:, best_feat])
for value in uniques:
indexs = train_xs[:, best_feat] == value
tree.childs[value] = self.build_tree(train_xs[indexs, :], train_ys[indexs], attrs_idxs[attrs_idxs != best_feat],
attr_types, example_idxs[indexs], depth+1)
else:
best_feat = cart_split_cache[1:]
tree.split_attr = best_feat
if attr_types[best_feat[0]] == 0: # 特征为离散
indexs = train_xs[:, best_feat[0]] == best_feat[1]
tree.childs['left'] = self.build_tree(train_xs[indexs, :], train_ys[indexs], attrs_idxs[attrs_idxs != best_feat[0]],
attr_types, example_idxs[indexs], depth+1)
tree.childs['right'] = self.build_tree(train_xs[~indexs, :], train_ys[~indexs], attrs_idxs[attrs_idxs != best_feat[0]],
attr_types, example_idxs[~indexs], depth+1)
else: # 特征为连续
indexs = train_xs[:, best_feat[0]] <= best_feat[1]
tree.childs['left'] = self.build_tree(train_xs[indexs, :], train_ys[indexs], attrs_idxs[attrs_idxs != best_feat[0]],
attr_types, example_idxs[indexs], depth + 1)
tree.childs['right'] = self.build_tree(train_xs[~indexs, :], train_ys[~indexs], attrs_idxs[attrs_idxs != best_feat[0]],
attr_types, example_idxs[~indexs], depth + 1)
return tree
def predict(self, tree, data):
'''
对于一个实例进行预测
'''
if tree.is_leaf:
return tree.label
else:
if self._type in ['ID3', 'C4.5']:
if data[tree.split_attr] in tree.childs.keys():
sub_tree = tree.childs[data[tree.split_attr]]
else:
random_child = random.choice(list(tree.childs.keys()))
sub_tree = tree.childs[random_child]
else:
best_feat, value = tree.split_attr
if value in [0, 1]: # 特征为离散
sub_tree = tree.childs['left'] if data[best_feat] == value else tree.childs['right']
else: # 特征为连续的
sub_tree = tree.childs['left'] if data[best_feat] <= value else tree.childs['right']
return self.predict(sub_tree, data)
def test(self, test_xs, test_ys):
'''
测试
'''
predict_ys = []
for i in range(test_xs.shape[0]):
predict_y = self.predict(self.tree, test_xs[i])
predict_ys.append(predict_y)
predict_ys = np.array(predict_ys)
if self.predict_type == 'regression':
square_error = ((predict_ys - test_ys) ** 2).mean()
print('MSE:%f' % square_error)
else:
accuracy = (predict_ys == test_ys).mean()
print('Accuracy:%f' % accuracy)
def _majority_vote(self, targets):
'''
多数表决法
对于分类采用多数投票
对于回归采用均值
:param targets: 样本的标签
:return:
'''
if len(targets) == 0:
return
else:
if self.predict_type == 'classification':
uniques, counts = np.unique(targets, return_counts=True)
return uniques[np.argmax(counts)]
else:
return np.mean(targets)
def _calculate_entropy(self, train_ys):
'''
计算经验熵
'''
_, counts = np.unique(train_ys, return_counts=True)
probs = counts / counts.sum()
emp_entropy = -(probs * np.log2(probs)).sum()
return emp_entropy
def _calculate_gini(self, train_ys):
'''
计算基尼指数
'''
_, counts = np.unique(train_ys, return_counts=True)
probs = counts / counts.sum()
gini_indicator = (probs * (1 - probs)).sum()
return gini_indicator
def _calculate_square_error(self, train_ys):
'''
计算平方误差
'''
targets = train_ys.mean() # 用平均值代替相应的输出值
square_error = ((train_ys - targets) ** 2).sum()
return square_error
if __name__ == '__main__':
_type = 'CART'
predict_type = 'regression'
if predict_type == 'classification':
digits = load_digits()
features = digits.data
targets = digits.target
targets = (targets > 4).astype(int)
if _type != 'CART': # CART既可以用于分类任务又可以用于回归任务
features = (features > 7).astype(int)
else:
# 加载California Housing数据集
digits = fetch_california_housing()
features = digits.data
targets = digits.target
np.random.seed(2024)
shuffle_indices = np.random.permutation(features.shape[0])
features = features[shuffle_indices]
target = targets[shuffle_indices]
train_count = int(len(features) * 0.8)
train_xs, test_xs = features[:train_count], features[train_count:]
train_ys, test_ys = target[:train_count], target[train_count:]
descion_tree = DescionTree(_type, predict_type, 1e-4)
attrs_idxs = np.arange(0, train_xs.shape[1])
attr_types = np.zeros(train_xs.shape[1]) if predict_type == 'classification' else np.ones(train_xs.shape[1])
example_idxs = np.arange(0, train_xs.shape[0])
depth = 0
# build_tree(train_xs, train_ys, attrs_idxs, attr_types, example_idxs, depth)
descion_tree.tree = descion_tree.build_tree(train_xs, train_ys, attrs_idxs, attr_types, example_idxs, depth)
descion_tree.test(test_xs, test_ys)