决策树
决策树
回顾线性模型
在线性模型中,对于一个拥有i个属性的数据集我们通过构造$y = WX(W = (w,b)^*,X = (x_1;x_2;...;x_i)$根据均方方差最小化或者对数几率最大化来进行线性回归或者对数几率回归来确定$W$,得到模型从而模拟得到输出y与数据集$X$的线性关系,其中对数几率回归由于输出的是概率因此被用作分类任务的一部分。而线性判别分析通过类间散度矩阵$S_b$和类内散度矩阵$S_w$拟合一个目标函数使得$S_b$和$S_w$的广义瑞利商最大,最后求得$W$的闭式解,通过将输出的值离散化为类别从而达到分类的目的。具体来说:
在线性模型中,对于一个拥有 $i$个属性的数据集,我们通过构造 $$y = WX$$(其中 $$W = (w_1, w_2, \ldots, w_i, b)^T$$,$$X = (x_1, x_2, \ldots, x_i, 1)^T$$)(x1,x2,…,xi 是输入数据的特征),根据最小化均方误差来进行线性回归,确定 $$W$$ 的取值,从而拟合模型,得到输出 $$y$$ 与数据集 $$X$$ 之间的线性关系。
对数几率回归通过构造 $$P(y=1|X) = \sigma(WX)$$(其中 $$\sigma(z) = \frac{1}{1 + e^{-z}}$$ 是Sigmoid函数),输出的是概率,因此被用作分类任务的一部分。模型通过最大化对数似然函数来确定 $$W$$ 的取值,通常使用迭代优化算法而非闭式解。
线性判别分析通过类间散度矩阵 $$S_b$$ 和类内散度矩阵 $$S_w$$ 拟合一个目标函数,使得 $$S_b$$ 和 $$S_w$$ 的广义瑞利商最大,最后求得 $$W$$ 的闭式解($S_w ^{-1}S_b$_的N-1个最大广义特征值所对应的特征向量组成的矩阵),。通过将输出的值离散化为类别从而达到分类的目的。
决策树的引入
在分类问题中,决策树比线性模型生动得多。决策树是一种划分策略,它通过一系列二分判断划分数据空间,并生成树状结构,每个节点表示一次决策后的数据集,边表示决策后数据集的分裂,叶子节点为最终输出的类别标签。决策树的引入是自然的,符合人们认识事物的规律,当判断一个瓜是好瓜还是坏瓜,我们根据西瓜的颜色瓜蒂等属性进行一系列二分,最后形成一套仅通过输入属性以及属性值就能判断瓜好坏的“模型”。(属性与特征在这里同义)
以下是一个简单的西瓜数据集示例,包含一些离散属性和属性值:
| 色泽 | 根蒂 | 敲声 | 纹理 | 脐部 | 触感 | 好瓜 |
|---|---|---|---|---|---|---|
| 青绿 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
| 乌黑 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 是 |
| 乌黑 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
| 青绿 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 是 |
| 浅白 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
| 青绿 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 软粘 | 是 |
| 乌黑 | 稍蜷 | 浊响 | 稍糊 | 稍凹 | 软粘 | 是 |
| 乌黑 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 硬滑 | 是 |
| 乌黑 | 稍蜷 | 沉闷 | 稍糊 | 稍凹 | 硬滑 | 否 |
| 青绿 | 硬挺 | 清脆 | 清晰 | 平坦 | 软粘 | 否 |
| 浅白 | 硬挺 | 清脆 | 模糊 | 平坦 | 硬滑 | 否 |
| 浅白 | 蜷缩 | 浊响 | 模糊 | 平坦 | 软粘 | 否 |
| 青绿 | 蜷缩 | 浊响 | 稍糊 | 凹陷 | 硬滑 | 否 |
| 浅白 | 蜷缩 | 浊响 | 清晰 | 稍凹 | 硬滑 | 否 |
| 乌黑 | 稍蜷 | 沉闷 | 稍糊 | 稍凹 | 软粘 | 否 |
这个表格展示了每个西瓜样本的属性及其对应的好瓜(是)或坏瓜(否)标签。
决策树构建
决策树的构建可以这样描述:
- 选取最优特征;
- 分割数据集;
- 在以下三种情况退出分割:
- 当所有数据属于同一类别;
- 数据集的属性为空或者属性值全部相同;
- 数据集无法继续分割(样本数据量小于我们设定的某个阈值)
选取策略
上面我们提到了决策树的具体算法,大部分都很好实现,只有"选取最优特征"存在疑惑--什么是最优特征?
为了判断什么是最优特征,我们需要引入一些量化指标进行评估。
信息增益
| 色泽 | 根蒂 | 敲声 | 纹理 | 脐部 | 触感 | 好瓜 |
|---|---|---|---|---|---|---|
| 青绿 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
| 乌黑 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 是 |
| 乌黑 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
| 青绿 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 是 |
| 浅白 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
| 青绿 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 软粘 | 是 |
| 乌黑 | 稍蜷 | 浊响 | 稍糊 | 稍凹 | 软粘 | 是 |
| 乌黑 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 硬滑 | 是 |
| 乌黑 | 稍蜷 | 沉闷 | 稍糊 | 稍凹 | 硬滑 | 否 |
| 青绿 | 硬挺 | 清脆 | 清晰 | 平坦 | 软粘 | 否 |
| 浅白 | 硬挺 | 清脆 | 模糊 | 平坦 | 硬滑 | 否 |
| 浅白 | 蜷缩 | 浊响 | 模糊 | 平坦 | 软粘 | 否 |
| 青绿 | 蜷缩 | 浊响 | 稍糊 | 凹陷 | 硬滑 | 否 |
| 浅白 | 蜷缩 | 浊响 | 清晰 | 稍凹 | 硬滑 | 否 |
| 乌黑 | 稍蜷 | 沉闷 | 稍糊 | 稍凹 | 软粘 | 否 |
信息熵(Entropy)是用来度量样本集合纯度的指标,假设定当前样本集合$D$中第$k$类样本所占的比例为 $P_k$ (k = 1, 2,. . . , $|Y|$),在这里我们可以将其表示为:
$$
Entr(D)=-\sum_{k=1}^{|Y|}p_klog_2p_k\ .
$$
其中Ent(D)的值越小,则D越纯,这也很符合直觉,因为熵越大表明混乱程度越高,携带信息越多,而分类目的就是减少混乱程度。
因此我们可以利用熵来表示当前数据集合的纯度,假设离散属性$a$有$V$个可能的取值{${a1,a2,a3,a4...}$},当根据属性$a$进行划分会产生$V$个分支节点,第$v$个节点包含$D$中所有属性$a$为$av$的数据集,记作$Dv$,我们为所有划分后的子集分配权重,并与进行划分前的信息熵作差就得到了我们第一个参考指标,信息增益(Gain):
$$
Gain(D,a) = Ent(D) -\sum_{k=1}{|Y|}\frac{|Dv|}{|D|}Entr(D^v) \ .
$$
这里给出规律:
- Gain越大表明使用$a$进行属性划分获得的纯度增益越大
从而我们有了第一个评估指标。
增益率
| 编号 | 色泽 | 根蒂 | 敲声 | 纹理 | 脐部 | 触感 | 好瓜 |
|---|---|---|---|---|---|---|---|
| 1 | 青绿 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
| 2 | 乌黑 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 是 |
| 3 | 乌黑 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
| 4 | 青绿 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 是 |
| 5 | 浅白 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
| 6 | 青绿 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 软粘 | 是 |
| 7 | 乌黑 | 稍蜷 | 浊响 | 稍糊 | 稍凹 | 软粘 | 是 |
| 8 | 乌黑 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 硬滑 | 是 |
| 9 | 乌黑 | 稍蜷 | 沉闷 | 稍糊 | 稍凹 | 硬滑 | 否 |
| 10 | 青绿 | 硬挺 | 清脆 | 清晰 | 平坦 | 软粘 | 否 |
| 11 | 浅白 | 硬挺 | 清脆 | 模糊 | 平坦 | 硬滑 | 否 |
| 12 | 浅白 | 蜷缩 | 浊响 | 模糊 | 平坦 | 软粘 | 否 |
| 13 | 青绿 | 蜷缩 | 浊响 | 稍糊 | 凹陷 | 硬滑 | 否 |
| 14 | 浅白 | 蜷缩 | 浊响 | 清晰 | 稍凹 | 硬滑 | 否 |
| 15 | 乌黑 | 稍蜷 | 沉闷 | 稍糊 | 稍凹 | 软粘 | 否 |
思考这样一个问题,假设编号也是一个属性,我们将编号作为划分依据,这样的结果如何呢?
从结果上不难想到,这样划分得到的数据集只包含一个数据样例,因此这样划分的纯度最高,但根据经验判断,编号本身对西瓜好坏程度是没有关联的,因此选取编号作为划分依据是错误的做法,为了规避信息增益对可取数目较多的属性有所偏好,于是引入增益率(Gain Ratio):
$$
Gain_ratio(D,a) = \frac{Gain(D,a)}{IV(a)},\
IV(a) = -\sum_{v=1}{V}\frac{|Dv|}{|D|}log_2\frac{|D^v|}{|D|}
$$
$IV(a)$称为属性$a$的固有值(instrinic value),可以看到当属性$a$可能的取值越多,$IV(a)$越大,有效规避信息增益对可取数目较多的属性有所偏好。
增益率在C4.5决策树作为择优标准,增益率越大,划分效果越好。
基尼指数
假设定当前样本集合$D$中第$k$类样本所占的比例为 $p_k$ (k = 1, 2,. . . , $|Y|$),定义基尼值:
$$
Gini(D)=1-\sum_{k=1}{|Y|}p_k2\ .
$$
前面我们通过定义数据的纯度引入信息熵,通过信息熵构造的信息增益和增益率来作为判断纯度增加的依据,在这里,我们不使用信息熵,反而通过描述一个数据集之间随机抽取两个样本,通过计算两个样本的标签类别不一致的概率来作为判断纯度的依据,因此基尼值的引入也很自然
所以,假设离散属性$a$有$V$个可能的取值{${a1,a2,a3,a4...}$},当根据属性$a$进行划分会产生$V$个分支节点,第$v$个节点包含$D$中所有属性$a$为$av$的数据集,记作$Dv$,我们为所有划分后的子集分配权重,就得到了基尼指数:
$$
Gini_index(D,a) = \sum_{v=1}V\frac{|Dv|}{|D|}Gini(D^v)\ .
$$
$ Gini(D)$ 越小,则数据集$D$的纯度越高.
决策树的构建
将上述数据集合抽象为特征矩阵$X$和标签向量$y$,特征矩阵将使用数字将特征值编号,列代表特征,标签向量为特征矩阵每一行对应一个分类标签,类似如下:
X = np.array([
[0, 0, 0, 0, 0, 0], # 青绿, 蜷缩, 浊响, 清晰, 凹陷, 硬滑
[1, 0, 0, 0, 0, 0], # 乌黑, 蜷缩, 浊响, 清晰, 凹陷, 硬滑
[2, 0, 0, 0, 0, 0], # 浅白, 蜷缩, 浊响, 清晰, 凹陷, 硬滑
[0, 1, 1, 0, 1, 1], # 青绿, 稍蜷, 沉闷, 清晰, 稍凹, 软粘
[1, 1, 1, 1, 1, 1], # 乌黑, 稍蜷, 沉闷, 稍糊, 稍凹, 软粘
[2, 1, 1, 1, 1, 0], # 浅白, 稍蜷, 沉闷, 稍糊, 稍凹, 硬滑
[0, 2, 2, 2, 2, 1], # 青绿, 硬挺, 清脆, 模糊, 平坦, 软粘
[1, 2, 2, 1, 2, 0], # 乌黑, 硬挺, 清脆, 稍糊, 平坦, 硬滑
[2, 2, 2, 2, 2, 1] # 浅白, 硬挺, 清脆, 模糊, 平坦, 软粘
])
# 标签向量 y
y = np.array([1, 1, 1, 0, 0, 0, 0, 0, 0]) # 1: 好瓜, 0: 坏瓜
接下来直接给出代码
class Node:
def __init__(self, feature_index = None, feature_val=None, left=None, right=None,value=None):
self.feature_index = feature_index
self.feature_val = feature_val
self.left = left
self.right = right
self.value = value
class DecisionTree(object):
def __init__(self, criterion = 'gini', max_deepth = None, min_sample_split = None, root = None):
self.root = root
self.criterion = criterion
self.max_deepth = max_deepth
self.min_sample_split = 2
def _caculate_gini(self, X, y, feature_index, feature_val):
'''
计算基尼指数,
基尼值:计算所有标签的1-p_k^2之和,
基尼指数:在每个属性(feature_index)下根据属性值(feature_val)分配权重计算基尼值之和
'''
left = X[:, feature_index] <= feature_val
right = X[:, feature_index] > feature_val
y_left, y_right = y[left], y[right]
# 计算基尼值
def gini(y_subset):
classes, counts = np.unique(y_subset, return_counts = True)
p_k = counts / len(y_subset)
gini_val = 1 - sum(p_k** 2)
return gini_val
left_gini = gini(y_left)
right_gini = gini(y_right)
#计算加权值
total_gini = (len(y_left)/len(y))*left_gini+(len(y_right)/len(y))*right_gini
return total_gini
def _split_node(self, X, y, criterion = 'gini'):
'''
将X进行划分,根据gini指数等,返回最佳划分方案,
为一个包含gini指数,最佳划分属性编号和最佳划分属性值的的三元组
'''
best_criterion = float('inf') if criterion == 'gini' else -float('inf')
best_feature_index = None
best_feature_val = None
_,n_features = X.shape
for feature_index in range(n_features):
feature_vals = np.unique(X[:,feature_index])
# 如果判断其是gini指数
for feature_val in feature_vals:
if criterion == 'gini':
gini = self._caculate_gini(X, y, feature_index, feature_val)
# 更新最优划分
if gini < best_criterion:
best_criterion = gini
best_feature_index = feature_index
best_feature_val = feature_val
# 如果判断器是gain增益率
elif criterion == 'gain':
gain = self._caculate_gain(X, y, feature_index, feature_val)
# 更新最优划分
if gain > best_criterion:
best_criterion = gain
best_feature_index = feature_index
best_feature_val = feature_val
return best_criterion, best_feature_index,best_feature_val
def _most_common_label(self, y):
return np.bincount(y).argmax()
def _build_tree(self, X, y, depth = 0, pred = 0):
'''
构建树,传入特征向量X和标签向量y
'''
# 首先设置停止划分的条件,叶子节点保存输出类别,类别为当前标签集合中最常见的标签
n_samples, n_features = X.shape# 行数正好是数据总数,列数为属性的总数目
n_labels = len(np.unique(y))
if n_labels == 1 or depth >= self.max_deepth or n_samples < self.min_sample_split:
leaf_val = self._most_common_label(y)
return Node(value=leaf_val)
# 开始划分
_, feature_index, feature_val = self._split_node(X, y, 'gini')
# 设置划分的开始编号
left_idx = X[:,feature_index] <= feature_val
right_idx = X[:,feature_index] > feature_val
left_pred = np.sum(y[left_idx] == 0) / len(y[left_idx])
right_pred = np.sum(y[right_idx] == 0) / len(y[right_idx])
# 预剪枝,递归处理左右子集
if pred >= left_pred and pred >= right_pred:
leaf_val = self._most_common_label(y)
return Node(value=leaf_val)
else:
left = self._build_tree(X[left_idx,:],y[left_idx], depth+1, left_pred)
right = self._build_tree(X[right_idx,:], y[right_idx],depth+1, right_pred)
if left is None and right is None:
leaf_val = self._most_common_label(y)
return Node(value=leaf_val)
# 返回树结构
return Node(feature_index=feature_index,feature_val=feature_val,left=left, right=right)
def _traverse_tree(self, x, node):
'''
遍历树以匹配输入数据x的输出标签类别
'''
if node.value is not None:
return node.value
if x[node.feature_index] <= node.feature_val:
return self._traverse_tree(x, node.left)
return self._traverse_tree(x, node.right)
def fit(self, X, y):
self.root = self._build_tree(X,y) #将树保存再在oot中
def predict(self, X):
return [self._traverse_tree(x, self.root) for x in X]
剪枝
预剪枝
预剪枝发生在划分子集时,如果划分子集之和,精度(这里指子集中正类的占比)没有提升,那么这次划分就是需要舍去的
left_pred = np.sum(y[left_idx] == 0) / len(y[left_idx])
right_pred = np.sum(y[right_idx] == 0) / len(y[right_idx])
# 预剪枝,递归处理左右子集
if pred >= left_pred and pred >= right_pred:
leaf_val = self._most_common_label(y)
return Node(value=leaf_val)
else:
left = self._build_tree(X[left_idx,:],y[left_idx], depth+1, left_pred)
right = self._build_tree(X[right_idx,:], y[right_idx],depth+1, right_pred)
if left is None and right is None:
leaf_val = self._most_common_label(y)
return Node(value=leaf_val)
后剪枝
后剪枝先从训练集生成一棵完整决策树,计算决策树的精度,然后自底向上地将节点替换为叶子节点,如果精度上升,则保留叶子节点,如果下降,则撤回操作,继续对其他节点进行操作。
# 后剪枝
def _prune_tree(self, node, X_val, y_val):
if node is None:
return None
# 递归地剪枝左子树和右子树
node.left = self._prune_tree(node.left, X_val, y_val)
node.right = self._prune_tree(node.right, X_val, y_val)
# 如果当前节点的左右子树都为空,返回当前节点
if node.left is None and node.right is None:
return node
# 获取当前节点的值
new_leaf_val = node.value
# 如果左右子树都存在,则尝试剪枝
if node.left is not None and node.right is not None:
# 保存原始树
original_tree = self
# 创建一个新的叶子节点,其值为当前节点的值
new_node = Node(value=new_leaf_val)
self.root = new_node
# 计算剪枝前后模型在验证集上的准确度
if self._evaluate_tree(X_val, y_val) > self._evaluate_tree(original_tree, X_val, y_val):
# 如果剪枝后的树在验证集上的表现更好,返回新的叶子节点
return new_node
else:
# 否则,恢复原始树并返回原始节点
self.root = node
return node
在写训练函数fit时作出适当改动:
def fit(self, X, y, X_val = None, y_val = None, prune = False):
self.root = self._build_tree(X,y) #将树保存再在root中
if prune and X_val is not None and y_val is not None:
self._prune_tree(self.root, X_val, y_val)

浙公网安备 33010602011771号