决策树是一种基于规则的方法,它用一组嵌套的规则进行预测。在树的每个决策节点处,根据判断结果进入一个分支,反复执行这种操作直到到达叶子节点,得到预测结果。这些规则是通过训练得到的,而不是人工制定的。
6.1树形决策过程
首先看一个简单的例子。银行要确定是否给客户发放贷款,需要考察客户的收入与房产情况,然后做出决策。如果把这个决策看作是分类问题,收入和房产就是两个特征分量,类别的标签是可以贷款和不能贷款。银行可以按照下图的过程进行决策。
上图这个过程就是一棵决策树,它是一种判别模型。决策树从树的根节点开始,在每一个节点处做出判断,直到到达一个叶子节点,得到决策结果。
决策树的节点分为两种类型:
(1)决策节点。在这些节点处需要进行判断以决定进入哪个分支。
(2)叶子节点。表示最终的决策结果。对于分类问题,叶子节点中存储的是类别标签。
6.2训练算法
下面介绍如何用训练样本建立决策树。
6.2.1递归分裂过程
训练算法是一个递归的过程,首先创建根节点,然后递归地建立左子树和右子树。假如训练样本集为D,训练算法的整体流程如下:
(1)用样本集D建立跟节点,找到一个判定条件,为根节点设置判定规则,将样本集分裂成D1和D2两部分。
(2)用样本集D1递归建立左子树。
(3)用样本集D2递归建立右子树。
(4)如果不能再进行分裂,则把节点标记为叶子节点,同时为它赋值。
在确定递归流程之后,接下来要解决的核心问题是怎样对训练样本集进行分裂。
6.2.2寻找最佳分裂
训练时需要找到一个分裂规则把训练样本集分裂成两个子集,因此,需要确定分裂的评价标准,根据评价标准寻找最佳分裂。对于分类问题,要保证分裂之后的左右子树的样本尽可能纯,即他们的样本尽可能属于不相交的类。因此需要定义不纯度指标,包括熵不纯度、Gini不纯度、误分类不纯度。
首先需要计算每个类出现的概率:
其中, 是第i类样本数,N为总样本数。根据这个概率值定义各种不纯度指标。
样本集D的熵不纯度定义为:
当样本只属于某一类时熵最小,当样本均匀地分布于所有类中时熵最大,因此,如果能找到一个分裂让熵最小,这就是我们想要的最佳分裂。
样本集D的Gini不纯度定义为:
当样本只属于某一类时Gini不纯度的值最小,此时最小值为0;当样本均与地分布于每一类时Gini不纯度的值最大。
样本集D的误分类不纯度定义为:
和上面两个指标一样,当样本只属于某一类时误分类不纯度的值最小,当样本均匀地分布于每一类时误分类不纯度的值最大。
接下来,我们根据不纯度指标构造出分裂的不纯度。分裂规则训练样本分裂成左、右两个子集,分裂的目标是分裂后的两个子集都尽可能纯。因此,计算左、右子集的不纯度之和作为分裂结果的不纯度,求和需要加上权重,以反映左右两边训练样本数的差异。由此得到分裂的不纯度为:
其中,是左子集的不纯度,是右子集的不纯度,N是总样本数,是左子集的样本数,是右子集的样本数。这里的不纯度可以根据情况选择上面三种不纯度的任意一种。
假设特征分量是数值型的,我们为每个特征分量设置一系列阈值,寻找最佳分裂时计算用每个阈值对样本集进行分裂后的不纯度,不纯度值最小对应的分裂就是最佳分裂。每次都选择当前条件下最好的分裂作为决策节点的分裂。
对于回归问题,衡量分裂的标准是回归误差(即样本方差),每次分裂时选用使方差最小化的那个分裂。假设节点的训练样本集有个样本,为特征向量,为标签值,样本集D的回归误差定义为:
其中,是样本集D所有样本标签的均值。
6.2.3叶子节点的设定
如果不能继续分裂,则将该节点设置为叶子节点。对于分类问题(分类树),将叶子节点的值设置成本节点的训练样本集中出现概率最大的那个类;对于回归问题(回归树),叶子节点的值设置为本节点训练样本标签的均值。
6.2.4剪枝算法
如果决策树的结构过于复杂,可能会导致过拟合问题。此时需要对树进行剪枝,消掉某些节点让它变得更简单。剪枝的关键问题是确定剪掉哪些树节点。决策树的剪枝算法可以分为两类:预剪枝和后剪枝。预剪枝在树的训练过程中通过停止分裂对树的规模进行限制;后剪枝先训练得到一棵完整的树,然后通过某种规则消掉部分节点。
预剪枝可以通过限定树的高度、节点的训练样本数、分裂纯度提升的最小值来实现。后剪枝有很多种方案,包括:降低错误剪枝(Reduced-Error Pruning,REP)、悲观错误剪枝(Pesimistic-Error Pruning,PEP)、代价-复杂度剪枝(Cost-Complexity pruning,CCP)等。大家可以自行查找相关资料学习各种剪枝方案的原理。
下面我们详细介绍CCP方案。代价是指剪枝后导致的错误率的变化值,复杂度是指决策树的规模。训练出一棵决策树之后,剪枝算法首先计算该决策树每个非叶子节点的值,它是代价与复杂度的比值,定义为:
其中,E(n)是节点n的错误率,是以节点n为根的子树的错误率,是该子树所有叶子节点的错误率之和,是子树的叶子节点数量,即复杂度。值表示将整个子树剪掉之后用一个叶子节点代替,相对于原来的子树错误率的增加值。该值越小,剪枝之后树的预测效果与剪枝之前越接近。
对于分类问题,错误率也就是误分类指标,定义为:
其中,N是节点的总样本数,是第i类样本数。
对于回归问题,错误率为节点样本集的均方误差:
剪枝算法的实现方案为计算出所有非叶子节点的值之后,剪掉值最小的节点得到剪枝后的树,然后重复这种操作。
6.3实验程序
6.3.1手动代码实现+预剪枝
import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 定义节点类
class Node:
def __init__(self, feature_index=None, threshold=None, left=None, right=None, value=None):
self.feature_index = feature_index
self.threshold = threshold
self.left = left
self.right = right
self.value = value
# 定义决策树类
class DecisionTree:
def __init__(self, max_depth=None, min_samples_split=2):
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.root = None
# 计算基尼系数
def gini(self, y):
types, counts = np.unique(y, return_counts=True)
probabilities = counts / len(y)
return 1 - np.sum(probabilities * probabilities)
# 计算基尼不纯度
def gini_impurity(self, y_left, y_right):
n = len(y_left) + len(y_right)
gini_left = self.gini(y_left)
gini_right = self.gini(y_right)
return (len(y_left) / n) * gini_left + (len(y_right) / n) * gini_right
# 选择最佳分裂特征和阈值
def find_best_split(self, X, y):
m, n = X.shape
best_gini = float('inf')
best_feature_index = None
best_threshold = None
for feature_index in range(n):
thresholds = np.unique(X[:, feature_index])
for threshold in thresholds:
y_left = y[X[:, feature_index] <= threshold]
y_right = y[X[:, feature_index] > threshold]
gini = self.gini_impurity(y_left, y_right)
if gini < best_gini:
best_gini = gini
best_feature_index = feature_index
best_threshold = threshold
return best_feature_index, best_threshold
# 构建决策树
def build_tree(self, X, y, depth=0):
unique, counts = np.unique(y, return_counts=True)
most_common = unique[np.argmax(counts)]
# 检查终止条件
if (self.max_depth is not None and depth >= self.max_depth) or len(y) < self.min_samples_split:
return Node(value=most_common)
# 寻找最佳分裂特征和阈值
best_feature_index, best_threshold = self.find_best_split(X, y)
# 分裂数据集
left_indices = X[:, best_feature_index] <= best_threshold
right_indices = X[:, best_feature_index] > best_threshold
left = self.build_tree(X[left_indices], y[left_indices], depth + 1)
right = self.build_tree(X[right_indices], y[right_indices], depth + 1)
return Node(feature_index=best_feature_index, threshold=best_threshold, left=left, right=right)
# 拟合模型
def fit(self, X, y):
self.root = self.build_tree(X, y)
# 预测
def predict(self, X):
return np.array([self.predict_tree(x, self.root) for x in X])
# 预测单个样本
def predict_tree(self, x, node):
if node.value is not None:
return node.value
if x[node.feature_index] <= node.threshold:
return self.predict_tree(x, node.left)
else:
return self.predict_tree(x, node.right)
# 测试代码
if __name__ == "__main__":
# 加载数据
iris = load_iris()
X, y = iris.data, iris.target
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=41)
# 初始化并拟合模型
dt = DecisionTree(max_depth=5, min_samples_split=10)
dt.fit(X_train, y_train)
# 预测
y_pred = dt.predict(X_test)
# 计算准确率
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy:", accuracy)
6.3.2手动代码实现+后剪枝
前面的代码与预剪枝的几乎一样,只是后面多个一个后剪枝的函数。
import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 定义节点类
class Node:
def __init__(self, feature_index=None, threshold=None, left=None, right=None, value=None):
self.feature_index = feature_index
self.threshold = threshold
self.left = left
self.right = right
self.value = value
# 定义决策树类
class DecisionTree:
def __init__(self, ccp_alpha=0.0):
self.ccp_alpha = ccp_alpha
self.root = None
# 计算基尼系数
def gini(self, y):
types, counts = np.unique(y, return_counts=True)
probabilities = counts / len(y)
return 1 - np.sum(probabilities ** 2)
# 计算基尼不纯度
def gini_impurity(self, y_left, y_right):
n = len(y_left) + len(y_right)
gini_left = self.gini(y_left)
gini_right = self.gini(y_right)
return (len(y_left) / n) * gini_left + (len(y_right) / n) * gini_right
# 选择最佳分裂特征和阈值
def find_best_split(self, X, y):
m, n = X.shape
best_gini = float('inf')
best_feature_index = None
best_threshold = None
for feature_index in range(n):
thresholds = np.unique(X[:, feature_index])
for threshold in thresholds:
y_left = y[X[:, feature_index] <= threshold]
y_right = y[X[:, feature_index] > threshold]
gini = self.gini_impurity(y_left, y_right)
if gini < best_gini:
best_gini = gini
best_feature_index = feature_index
best_threshold = threshold
return best_feature_index, best_threshold
# 构建决策树
def build_tree(self, X, y):
unique, counts = np.unique(y, return_counts=True)
most_common = unique[np.argmax(counts)]
# 检查终止条件
if len(np.unique(y)) == 1:
return Node(value=most_common)
# 寻找最佳分裂特征和阈值
best_feature_index, best_threshold = self.find_best_split(X, y)
# 分裂数据集
left_indices = X[:, best_feature_index] <= best_threshold
right_indices = X[:, best_feature_index] > best_threshold
left = self.build_tree(X[left_indices], y[left_indices])
right = self.build_tree(X[right_indices], y[right_indices])
return Node(feature_index=best_feature_index, threshold=best_threshold, left=left, right=right, value=most_common)
# 后剪枝
def prune_tree(self, node, X, y):
if node.left is None and node.right is None:
return
error_rate = self.error_rate(y)
if node.left:
left_indices = X[:, node.feature_index] <= node.threshold
left_error, leaf = self.tree_error(node.left, X[left_indices], y[left_indices], 0)
if (error_rate - left_error)/leaf < self.ccp_alpha:
node.left = Node(value=node.value)
if node.right:
right_indices = X[:, node.feature_index] > node.threshold
right_error, leaf = self.tree_error(node.right, X[right_indices], y[right_indices], 0)
if (error_rate - right_error)/leaf < self.ccp_alpha:
node.right = Node(value=node.value)
if node.left:
self.prune_tree(node.left, X, y)
if node.right:
self.prune_tree(node.right, X, y)
# 计算节点错误率
def error_rate(self, y):
unique, counts = np.unique(y, return_counts=True)
most_common = counts[np.argmax(counts)]
return 1 - most_common / len(y)
# 计算子树的错误率
def tree_error(self, node, X, y, leaf):
error_rate = 0
if node.left is None and node.right is None:
error_rate += self.error_rate(y)
leaf += 1
else:
if node.left:
left_indices = X[:, node.feature_index] <= node.threshold
error, leaf = self.tree_error(node.left, X[left_indices], y[left_indices], leaf)
error_rate += error
if node.right:
right_indices = X[:, node.feature_index] > node.threshold
error, leaf = self.tree_error(node.right, X[right_indices], y[right_indices], leaf)
error_rate += error
return error_rate, leaf
# 拟合模型
def fit(self, X, y):
self.root = self.build_tree(X, y)
self.look(self.root)
self.prune_tree(self.root, X, y)
print("**************")
self.look(self.root)
def look(self, node):
if node.left is not None and node.right is not None:
print("feature_index:{},threshold:{}".format(node.feature_index, node.threshold))
if node.left is not None:
self.look(node.left)
if node.right is not None:
self.look(node.right)
# 预测
def predict(self, X):
return np.array([self.predict_tree(x, self.root) for x in X])
# 预测单个样本
def predict_tree(self, x, node):
if node.left is None and node.right is None:
return node.value
if x[node.feature_index] <= node.threshold:
return self.predict_tree(x, node.left)
else:
return self.predict_tree(x, node.right)
# 测试代码
if __name__ == "__main__":
# 加载数据
iris = load_iris()
X, y = iris.data, iris.target
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=41)
# 初始化并拟合模型
dt = DecisionTree(ccp_alpha=0.01)
dt.fit(X_train, y_train)
# 预测
y_pred = dt.predict(X_test)
# 计算准确率
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy:", accuracy)
6.3.3使用sklearn库的代码实现
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn import tree
from sklearn.metrics import accuracy_score
from matplotlib import pyplot as plt
iris = load_iris()
X = iris.data
y = iris.target
# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=41)
# 决策节点的样本数不能少于10
dc_tree = tree.DecisionTreeClassifier(criterion='entropy', max_depth=5, min_samples_leaf=10)
dc_tree.fit(X_train, y_train)
y_predict = dc_tree.predict(X_test)
accuracy = accuracy_score(y_test, y_predict)
print(accuracy)
fig = plt.figure()
tree.plot_tree(dc_tree, filled=True,
feature_names=['sepal length', 'sepal width', 'petal length', 'petal width'],
class_names=['Setosa', 'Versicolour', 'Virginica'])
plt.show()
数据集和代码下载地址:
链接:https://pan.baidu.com/s/1u_z-vg5aLc3GVoZoF3GdsA
提取码:u0b4
参考文献
雷明. 机器学习——原理、算法与应用.清华大学出版社.