本内容将介绍决策树中的 CART 算法以及 Python 代码实现。其可用于分类和回归,具体实现包含分类树、回归树和模型树。
CART(classification and regression Trees,分类回归树),由 Breiman 等人在 1984 年提出,是应用广泛的决策树学习方法。CART 同样包括特征选择、决策树的生成和决策树的剪枝三个步骤,既可以用于分类也可以用于回归。
在阅读本内容前,需要了解决策树的基本概念。如果您还不了解,可以参阅 机器学习系列:决策树 01 (这里介绍了决策树的基本概念,以及 ID3、C4.5 和 CART 算法生成分类树)进行了解。本内容将介绍 CART 算法实现回归树和模型树,最后将列出 ID3、C4.5 和 CART 三种算法的对比。
一、CART 算法
在 机器学习系列:决策树 01 中已经介绍了决策树的相关知识,本内容将不再叙述。主要从与分类树之间的区别和代码实现来介绍CART 算法实现回归树和模型树。
使用 CART 算法构建回归树和模型树的过程与构建分类树的过程基本类似,不过其叶节点和特征选择准则不同。叶节点分别为:
- 分类树:叶节点是单个值,为离散型数据(即分类类别);
- 回归树:叶节点也是单个值,不过为连续型数据;
- 模型树:叶节点包含一个线性模型。
特征选择准则分别为:
- 分类树:使用基尼指数(Gini index)最小化准则;
- 回归树和模型树:使用平方误差最小化准则。
下面我们开始了解如何生成回归树和模型树。首先给出生成回归树和模型树的公用代码(Python 3.x):
import numpy as np
def load_data_set(file_name):
"""
从文件中获取数据集
:param file_name: 文件名
:return: 返回从文件中获取的数据集
"""
data_set = []
file = open(file_name)
for line in file.readlines():
# 文件中的数据以 tab 键进行分割
cur_line = line.strip().split('\t')
float_line = list(map(float, cur_line))
data_set.append(float_line)
# 返回的数据存放在一个矩阵中
return np.mat(data_set)
class DecisionTreeCART:
def _bin_split_data_set(self, data_set, feature, value):
"""
根据给定的特征及其值将数据集划分为两个子集。
:param data_set: 待划分的子集
:param feature: 划分的特征
:param value: 划分的特征值
:return: 划分后的两个子集
"""
# 将对应特征的值大于 value 的数据放入 mat_0 中,
# 将不大于 value 的数据放入 mat_1 中
mat_0 = data_set[np.nonzero(data_set[:, feature] > value)[0], :]
mat_1 = data_set[np.nonzero(data_set[:, feature] <= value)[0], :]
return mat_0, mat_1
def _create_tree_cell(self, data_set, leaf_type, err_type, ops):
"""
生成一棵决策树。它是一个递归函数。
:param data_set: 用于生成树的数据集
:param leaf_type: 对创建叶节点函数的引用
:param err_type: 对计算误差函数的引用
:param ops: 用户定义的参数构成的元组
:return: 返回生成的决策树
"""
# 通过函数 _choose_best_split() 选择最佳划分
feature, value = self._choose_best_split(data_set, leaf_type, err_type, ops)
# 如果返回的叶节点,则直接返回叶节点的值
if feature is None:
return value
# 因为决策树使用字典保存,所以这里初始化一个字典用来保存树的信息
ret_tree = {}
# 保存该节点进行划分的特征以及划分值
ret_tree['spInd'] = feature
ret_tree['spVal'] = value
# 对数据集进行划分
left_set, right_set = self._bin_split_data_set(data_set, feature, value)
# 对划分后的子集递归调用 _create_tree_cell() 生成树,并对左右子树进行保存
ret_tree['left'] \
= self._create_tree_cell(left_set, leaf_type, err_type, ops)
ret_tree['right'] \
= self._create_tree_cell(right_set, leaf_type, err_type, ops)
return ret_tree
第一个函数 load_data_set()
的主要作用为从文件中获取数据集。需要注意的是:在这个例子中目标变量和自变量方法同一行,并且目标变量放在最后位置;每行里面的数据内容以 tab 键进行分割。
第二个函数 _bin_split_data_set()
的主要作用为根据给定的特征及其值将数据集划分为两个子集。
第三个函数 _create_tree_cell()
是一个递归函数,主要作用为生成一棵决策树。
在函数 _create_tree_cell()
中使用了函数 _choose_best_split()
,该函数的主要作用是找到数据的最优二元划分方式。下面我们就来看看 _choose_best_split()
的具体实现代码。
二、回归树
我们在 _choose_best_split()
函数中加入具体实现代码前,需要先知道如何选择最优划分特征。在生成回归树时,使用平方误差最小化准则选择最优划分特征。
平方误差计算方法:首先计算所有数据的均值,然后计算每条数据的值到均值的差值。因为差值存在正负的情况,相加时会进行抵消,所以一般使用绝对值或平方值来代替差值。
按照上面的误差计算方法,遍历所有的特征以及其可能的取值来划分数据集,从而找到最优划分特征。
下面我们给出具体代码(Python 3.x):
import numpy as np
class DecisionTreeCART:
def _regression_leaf(self, data_set):
"""
首先获取数据集 data_set 中的最后一列数据,然后计算均值,即 y 的均值。
:param data_set: 数据集,最后一列为目标变量
:return: 返回目标变量的均值
"""
return np.mean(data_set[:, -1])
def _regression_err(self, data_set):
"""
首先获取数据集 data_set 中的最后一列数据,然后调用 var() 计算方差,
最后用方差乘以数据集中的样本个数,得到总方差(即平方误差之和)
:param data_set: 数据集,最后一列为目标变量
:return: 返回目标变量的平方误差之和
"""
return np.var(data_set[:, -1]) * np.shape(data_set)[0]
def _choose_best_split(self, data_set, leaf_type, err_type, ops):
"""
找到数据集的最佳二元切分方式。如果无法找到,返回 None 和 叶节点的值。
:param data_set: 待划分的数据集,需要是矩阵,最后一列为目标标量(即 y 值)
:param leaf_type: 对创建叶节点函数的引用
:param err_type: 对计算误差函数的引用
:param ops: 用户定义的参数构成的元组
:return: 如果找到最佳划分方式,返回最佳划分特征以及划分值。
如果没有找到,返回 None 和 叶节点的值。
"""
# 如果数据集中所有样本的目标变量值都相同,则创建叶节点
if len(set(data_set[:, -1].T.tolist()[0])) == 1:
return None, leaf_type(data_set)
# 误差下降阈值。即划分数据集后,如果误差下降小于该值,将停止划分。
err_threshold = ops[0]
# 可切分的最小样本数量。即如果数据集中的样本数量小于该值,将停止划分。
num_threshold = ops[1]
# 获取矩阵 data_set 的大小
m, n = np.shape(data_set)
# 计算数据集的误差
total_err = err_type(data_set)
min_err = np.inf
best_index = 0
best_value = 0
# 遍历数据集中的每一个特征
for feature_index in range(n-1):
# 遍历某一特征的每一个取值,set() 函数用于去除重复的值
for split_value in set(data_set[:, feature_index].T.tolist()[0]):
# 对数据集进行二分为两个子集
mat_0, mat_1 \
= self._bin_split_data_set(data_set, feature_index, split_value)
# 如果任一子集中样本数小于 tol_n,则跳过这种划分
if (np.shape(mat_0)[0] < num_threshold) \
or (np.shape(mat_1)[0] < num_threshold):
continue
# 计算划分数据集后的误差
new_err = err_type(mat_0) + err_type(mat_1)
# 更新最小划分误差的信息
if new_err < min_err:
best_index = feature_index
best_value = split_value
min_err = new_err
# 如果划分数据集后,误差下降小于阈值,即划分后效果提升不大。直接创建叶节点
if (total_err - min_err) < err_threshold:
return None, leaf_type(data_set)
# 确定划分后子集的样本数量大于阈值
mat_0, mat_1 = self._bin_split_data_set(data_set, best_index, best_value)
if (np.shape(mat_0)[0] < num_threshold) \
or (np.shape(mat_1)[0] < num_threshold):
return None, leaf_type(data_set)
# 返回最优划分特征以及划分值
return best_index, best_value
第一个函数 _regression_leaf()
的主要作用为生成叶节点。当 _choose_best_split()
函数确定不再对数据进行切分时,将调用该 _regression_leaf()
函数生成叶节点的模型。在回归树中,该模型就是目标变量的均值。
第二个函数 _regression_err()
的主要作用为计算平方误差。前面已经给出了平方误差计算方法。在实际代码中,使用均方差乘以数据集中样本数量的计算方法,实际效果一样。
第三个函数 _choose_best_split()
的主要作用为找到数据集的最佳二元切分方式。如果找不到,该函数返回 None
和叶节点的模型;如果找到了一个好的划分方式,将返回最优划分特征和特征值。
从函数 _choose_best_split()
可知停止划分的条件为:
- 如果数据集中所有样本的目标变量值都相同;
- 如果划分数据集后,效果提升不大(即误差减少小于阈值
err_threshold
); - 如果划分数据集后,子集的样本数量小于阈值
num_threshold
。
在生成回归树时,函数 _choose_best_split()
的参数 leaf_type
和 err_type
分别为 _regression_leaf()
和 _regression_err()
。在生成模型树是,这两个参数为不同的函数引用。
三、剪枝
CART 回归树同样需要进行剪枝。
在函数 _choose_best_split()
中通过 err_threshold
和 num_threshold
提前停止划分,实际上就是在进行预剪枝操作。但是在实际任务中,到底应该如何选择 err_threshold
和 num_threshold
的值,我们需要通过不断的测试来获取一个较好的值。即使通过不断的测试,我们可能仍然无法得到一个较好的值,所以我们需要使用后剪枝技术。
使用后剪枝方法需要将数据集分成测试集和训练集。构建决策树后,从上到下找到叶节点,用测试集来判断将这些叶节点合并是否能降低测试误差,如果能就进行合并。具体代码如下(Python 3.x):
import numpy as np
class DecisionTreeCART:
def _is_tree(self, obj):
"""
测试输入变量(obj)是否为一棵树
"""
return isinstance(obj, dict)
def _get_mean(self, tree):
"""
是一个递归函数。从上到下遍历树直到叶节点为此,求两个叶节点的平均值。
将树 tree 剪枝为一个叶节点。
:return: 返回树的左右子节点的平均值
"""
# 如果左右子节点为树,则递归调用 _get_mean()
# 直到左右子节点都为叶节点,则求它们的平均值
if self._is_tree(tree['left']):
tree['left'] = self._get_mean(tree['left'])
if self._is_tree(tree['right']):
tree['right'] = self._get_mean(tree['right'])
return (tree['left'] + tree['right']) / 2
def prune(self, tree, test_data):
"""
根据测试数据对树进行剪枝
:param tree: 待剪枝的树
:param test_data: 剪枝使用的测试数据
:return: 返回剪枝后的树或叶节点
"""
# 如果测试集为空,则将此树剪枝为一个叶节点
if np.shape(test_data)[0] == 0:
print('merging 01')
return self._get_mean(tree)
# 如果左右子节点为树,则对测试数据进行划分
if (self._is_tree(tree['left'])) or (self._is_tree(tree['right'])):
left_set, right_set \
= self._bin_split_data_set(test_data, tree['spInd'], tree['spVal'])
# 如果左右子节点为树,则递归调用 prune() 进行剪枝操作
if self._is_tree(tree['left']):
tree['left'] = self.prune(tree['left'], left_set)
if self._is_tree(tree['right']):
tree['right'] = self.prune(tree['right'], right_set)
# 对左右子节点进行剪枝操作后,如果都不是树(即都是叶节点),则尝试合并
if (not self._is_tree(tree['left'])) and (not self._is_tree(tree['right'])):
# 对测试数据进行划分
left_set, right_set \
= self._bin_split_data_set(test_data, tree['spInd'], tree['spVal'])
# 计算左右子树没有合并前的误差
err_no_merge = sum(np.power(left_set[:, -1] - tree['left'], 2)) \
+ sum(np.power(right_set[:, -1] - tree['right'], 2))
# 计算左右子树合并后的误差
tree_mean = (tree['left'] + tree['right']) / 2
err_merge = sum(np.power(test_data[:, -1] - tree_mean, 2))
# 如果合并后的误差小于合并前的误差,则进行合并
if err_merge < err_no_merge:
print('merging 02')
return tree_mean
else:
return tree
else:
return tree
第一个函数 _is_tree()
的主要作用为测试输入变量是否为一棵树。
第二个函数 _get_mean()
是一个递归函数,其主要作用为上到下遍历树直到叶节点为此,求两个叶节点的平均值,最终将树剪枝为一个叶节点。
第三个函数 prune()
是一个递归函数,其主要作用为对树进行剪枝。对测试数据集进行递归划分和调用 prune()
进行剪枝。
从函数 prune()
可知发生合并(即进行剪枝)的条件为:
- 测试数据集为空;
- 当合并后的误差小于合并前的误差。
需要注意的是,只有当某个节点的左右子节点都为叶节点时才能进行合并。
四、模型树
用树来对数据建模,除了把叶节点简单地设定为常数值之外,还有一种方法是把叶节点设定为分段线性函数,这里所谓的分段线性(piecewise linear)是指模型由多个线性片段组成。决策树相比于其他机器学习算法的优势之一在于结果更容易理解。
我们只需要稍微修改一下前面回归树的生成代码,就可以用来生成模型树。前面已经提到过,在生成模型树时,_choose_best_split()
的参数 leaf_type
和 err_type
与回归树不同。这是因为它们之间的叶节点模型和平方误差计算方法不同。具体代码如下(Python 3.x):
import numpy as np
class DecisionTreeCART:
def _linear_solve(self, data_set):
"""
将数据集格式化为目标变量 Y 和自变量 X,
对 X 和 Y 构建一个线性回归模型。
:param data_set: 数据集
:return: 构建的线性回归模型的权值和目标变量 Y 和自变量 X
"""
m, n = np.shape(data_set)
X = np.mat(np.ones((m, n)))
# 将数据集中前 n-1 列放入到自变量 X 中
X[:, 1:n] = data_set[:, 0:n-1]
# 将数据集中最后一列放入到目标变量 Y 中
Y = data_set[:, -1]
# 通过最小二乘法求得线性回归模型的权值
xTx = X.T * X
if np.linalg.det(xTx) == 0:
raise NameError('This matrix is singular, cannot do inverse,\n\
try increasing the second value of ops')
ws = xTx.I * (X.T * Y)
return ws, X, Y
def _model_leaf(self, data_set):
"""
对数据集构建一个线性回归模型
:param data_set: 数据集
:return: 返回构建的线性回归模型的权值
"""
ws, X, Y = self._linear_solve(data_set)
return ws
def _model_err(self, data_set):
"""
首先对数据集构建一个线性回归模型,然后计算总平方误差
:param data_set: 数据集
:return: 返回目标变量的总平方误差
"""
# 首先拟合一个线性回归模型
ws, X, Y = self._linear_solve(data_set)
# 根据拟合的线性回归模型,计算出预测值
y_hat = X * ws
# 返回预测值与实际值之间的总平方误差
return sum(np.power(Y-y_hat, 2))
在生成模型树时,_choose_best_split()
的参数 leaf_type
和 err_type
分别为 _model_leaf()
和 _model_err()
。
第一个函数 _linear_solve()
被其他两个函数调用,其主要作用为将数据集格式化为目标变量 Y 和自变量 X,并生成一个线性回归模型。需要注意的是:如果矩阵的逆不存在,会出现程序异常;所以在实际任务中,可能需要对这个函数进行修改。
第二个函数 _model_leaf()
的主要作用为生成叶节点模型(为线性回归模型)。当数据集不再需要划分时,会调用它。
第三个函数 _model_err()
的主要作用为计算平方误差。
总结:在每个叶节点上,回归树使用各自的均值做预测;模型树需要在每个叶节点上都构建一个线性模型。模型树的可解释性是它优于回归树的特点之一。另外,模型树也具有更高的预测准确度。
五、预测
在生成决策树后,我们需要对未见数据进行预测。预测流程:从上到下遍历决策树,直至到达叶节点;然后根据叶节点的模型输出预测结果。具体代码如下:
def _reg_tree_eval(self, model, in_data):
"""
用于回归树预测,直接返回叶节点中保存的均值
"""
return float(model)
def _model_tree_eval(self, model, in_data):
"""
用于模型树预测,根据叶节点中的线性回归模型返回预测值
:param model: 叶节点中线性回归模型的一组权值
:param in_data: 输入数据,即一组特征值
:return: 返回预测结果
"""
# 在输入数据的第一列添加 1,因为需要计算偏置
n = np.shape(in_data)[1]
X = np.mat(np.ones((1, n+1)))
X[:, 1:n+1] = in_data
return float(X * model)
def tree_fore_cast(self, tree, in_data, model_eval=_reg_tree_eval):
"""
是一个递归函数。对一组数据的预测。
从上而下遍历树,直至叶节点;然后根据叶节点中保存的模型进行预测。
"""
if not self._is_tree(tree):
return model_eval(tree, in_data)
if in_data[tree['spInd']] > tree['spVal']:
return self.tree_fore_cast(tree['left'], in_data, model_eval)
else:
return self.tree_fore_cast(tree['right'], in_data, model_eval)
def create_fore_cast(self, tree, test_data):
"""
对多组数据进行预测。
"""
m = len(test_data)
y_hat = np.mat(np.zeros((m, 1)))
# 调用 tree_fore_cast() 对数据逐个进行预测
for i in range(m):
y_hat[i, 0] = \
self.tree_fore_cast(tree, np.mat(test_data[i]), self.model_eval)
return y_hat
函数 _reg_tree_eval()
和 _model_tree_eval()
的主要作用为根据模型进行预测,分别用于回归树预测和模型树预测。
函数 tree_fore_cast()
和 create_fore_cast()
分别用于对一组和多组数据进行预测。
六、总结
下面列出决策树的三种算法(ID3、C4.5 和 CART)之间的区别:
描述 | ID3 | C4.5 | CART |
---|---|---|---|
作用 | 分类 | 分类 | 分类和回归 |
可处理属性类型 | 仅可处理离散属性 | 可处理离散属性和连续属性 | 可处理离散属性和连续属性 |
特征选择准则 | 信息增益 | 信息增益率 | 分类:基尼系数;回归:平方误差 |
划分方式 | 多分 | 二分 | |
输出数据类型 | 离散型数据 | 离散型数据 | 分类:离散型数据;回归:连续型数据 |
参考:
[1] 周志华《机器学习》
[2] 李航《统计学习方法》
[3] 《机器学习实战》
附录:
下面给出完整代码(Python 3.x):
import numpy as np
class DecisionTreeCART:
def __init__(self, type):
if type == 'classification':
# 此代码不支持分类树
pass
elif type == 'regression':
self.leaf_type = self._regression_leaf
self.err_type = self._regression_err
self.model_eval = self._reg_tree_eval
elif type == 'model':
self.leaf_type = self._model_leaf
self.err_type = self._model_err
self.model_eval = self._model_tree_eval
def create_tree(self, data_set, ops=(1, 4)):
return self._create_tree_cell(data_set, self.leaf_type, self.err_type, ops)
def _create_tree_cell(self, data_set, leaf_type, err_type, ops):
"""
生成一棵决策树。它是一个递归函数。
:param data_set: 用于生成树的数据集
:param leaf_type: 对创建叶节点函数的引用
:param err_type: 对计算误差函数的引用
:param ops: 用户定义的参数构成的元组
:return: 返回生成的决策树
"""
# 通过函数 _choose_best_split() 选择最佳划分
feature, value = \
self._choose_best_split(data_set, leaf_type, err_type, ops)
# 如果返回的叶节点,则直接返回叶节点的值
if feature is None:
return value
# 因为决策树使用字典保存,所以这里初始化一个字典用来保存树的信息
ret_tree = {}
# 保存该节点进行划分的特征以及划分值
ret_tree['spInd'] = feature
ret_tree['spVal'] = value
# 对数据集进行划分
left_set, right_set = self._bin_split_data_set(data_set, feature, value)
# 对划分后的子集递归调用 _create_tree_cell() 生成树,并对左右子树进行保存
ret_tree['left'] \
= self._create_tree_cell(left_set, leaf_type, err_type, ops)
ret_tree['right'] \
= self._create_tree_cell(right_set, leaf_type, err_type, ops)
return ret_tree
def _bin_split_data_set(self, data_set, feature, value):
"""
根据给定的特征及其值将数据集划分为两个子集。
:param data_set: 待划分的子集
:param feature: 划分的特征
:param value: 划分的特征值
:return: 划分后的两个子集
"""
# 将对应特征的值大于 value 的数据放入 mat_0 中,
# 将不大于 value 的数据放入 mat_1 中
mat_0 = data_set[np.nonzero(data_set[:, feature] > value)[0], :]
mat_1 = data_set[np.nonzero(data_set[:, feature] <= value)[0], :]
return mat_0, mat_1
def _choose_best_split(self, data_set, leaf_type, err_type, ops):
"""
找到数据集的最佳二元切分方式。如果无法找到,返回 None 和 叶节点的值。
:param data_set: 待划分的数据集,需要是矩阵,最后一列为目标标量(即 y 值)
:param leaf_type: 对创建叶节点函数的引用
:param err_type: 对计算误差函数的引用
:param ops: 用户定义的参数构成的元组
:return: 如果找到最佳划分方式,返回最佳划分特征以及划分值。
如果没有找到,返回 None 和 叶节点的值。
"""
# 如果数据集中所有样本的目标变量值都相同,则创建叶节点
if len(set(data_set[:, -1].T.tolist()[0])) == 1:
return None, leaf_type(data_set)
# 误差下降阈值。即划分数据集后,如果误差下降小于该值,将停止划分。
err_threshold = ops[0]
# 可切分的最小样本数量。即如果数据集中的样本数量小于该值,将停止划分。
num_threshold = ops[1]
# 获取矩阵 data_set 的大小
m, n = np.shape(data_set)
# 计算数据集的误差
total_err = err_type(data_set)
min_err = np.inf
best_index = 0
best_value = 0
# 遍历数据集中的每一个特征
for feature_index in range(n-1):
# 遍历某一特征的每一个取值,set() 函数用于去除重复的值
for split_value in set(data_set[:, feature_index].T.tolist()[0]):
# 对数据集进行二分为两个子集
mat_0, mat_1 = self._bin_split_data_set(data_set, feature_index, split_value)
# 如果任一子集中样本数小于 tol_n,则跳过这种划分
if (np.shape(mat_0)[0] < num_threshold) \
or (np.shape(mat_1)[0] < num_threshold):
continue
# 计算划分数据集后的误差
new_err = err_type(mat_0) + err_type(mat_1)
# 更新最小划分误差的信息
if new_err < min_err:
best_index = feature_index
best_value = split_value
min_err = new_err
# 如果划分数据集后,误差下降小于阈值,即划分后效果提升不大。直接创建叶节点
if (total_err - min_err) < err_threshold:
return None, leaf_type(data_set)
# 确定划分后子集的样本数量大于阈值
mat_0, mat_1 = self._bin_split_data_set(data_set, best_index, best_value)
if (np.shape(mat_0)[0] < num_threshold) \
or (np.shape(mat_1)[0] < num_threshold):
return None, leaf_type(data_set)
# 返回最优划分特征以及划分值
return best_index, best_value
# 用于回归树
def _regression_leaf(self, data_set):
"""
首先获取数据集 data_set 中的最后一列数据,然后计算均值,即 y 的均值。
:param data_set: 数据集,最后一列为目标变量
:return: 返回目标变量的均值
"""
return np.mean(data_set[:, -1])
def _regression_err(self, data_set):
"""
首先获取数据集 data_set 中的最后一列数据,然后调用 var() 计算方差,
最后用方差乘以数据集中的样本个数,得到总方差(即平方误差之和)
:param data_set: 数据集,最后一列为目标变量
:return: 返回目标变量的平方误差之和
"""
return np.var(data_set[:, -1]) * np.shape(data_set)[0]
# 用于模型树
def _linear_solve(self, data_set):
"""
将数据集格式化为目标变量 Y 和自变量 X,
对 X 和 Y 构建一个线性回归模型。
:param data_set: 数据集
:return: 构建的线性回归模型的权值和目标变量 Y 和自变量 X
"""
m, n = np.shape(data_set)
X = np.mat(np.ones((m, n)))
# 将数据集中前 n-1 列放入到自变量 X 中
X[:, 1:n] = data_set[:, 0:n-1]
# 将数据集中最后一列放入到目标变量 Y 中
Y = data_set[:, -1]
# 通过最小二乘法求得线性回归模型的权值
xTx = X.T * X
if np.linalg.det(xTx) == 0:
raise NameError('This matrix is singular, cannot do inverse,\n\
try increasing the second value of ops')
ws = xTx.I * (X.T * Y)
return ws, X, Y
def _model_leaf(self, data_set):
"""
对数据集构建一个线性回归模型
:param data_set: 数据集
:return: 返回构建的线性回归模型的权值
"""
ws, X, Y = self._linear_solve(data_set)
return ws
def _model_err(self, data_set):
"""
首先对数据集构建一个线性回归模型,然后计算总平方误差
:param data_set: 数据集
:return: 返回目标变量的总平方误差
"""
# 首先拟合一个线性回归模型
ws, X, Y = self._linear_solve(data_set)
# 根据拟合的线性回归模型,计算出预测值
y_hat = X * ws
# 返回预测值与实际值之间的总平方误差
return sum(np.power(Y-y_hat, 2))
# 用于剪枝
def _is_tree(self, obj):
"""
测试输入变量(obj)是否为一棵树
"""
return isinstance(obj, dict)
def _get_mean(self, tree):
"""
是一个递归函数。从上到下遍历树直到叶节点为此,求两个叶节点的平均值。
将树 tree 剪枝为一个叶节点。
:return: 返回树的左右子节点的平均值
"""
# 如果左右子节点为树,则递归调用 _get_mean()
# 直到左右子节点都为叶节点,则求它们的平均值
if self._is_tree(tree['left']):
tree['left'] = self._get_mean(tree['left'])
if self._is_tree(tree['right']):
tree['right'] = self._get_mean(tree['right'])
return (tree['left'] + tree['right']) / 2
def prune(self, tree, test_data):
"""
根据测试数据对树进行剪枝
:param tree: 待剪枝的树
:param test_data: 剪枝使用的测试数据
:return: 返回剪枝后的树或叶节点
"""
# 如果测试集为空,则将此树剪枝为一个叶节点
if np.shape(test_data)[0] == 0:
print('merging 01')
return self._get_mean(tree)
# 如果左右子节点为树,则对测试数据进行划分
if (self._is_tree(tree['left'])) or (self._is_tree(tree['right'])):
left_set, right_set \
= self._bin_split_data_set(test_data, tree['spInd'], tree['spVal'])
# 如果左右子节点为树,则递归调用 prune() 进行剪枝操作
if self._is_tree(tree['left']):
tree['left'] = self.prune(tree['left'], left_set)
if self._is_tree(tree['right']):
tree['right'] = self.prune(tree['right'], right_set)
# 对左右子节点进行剪枝操作后,如果都不是树(即都是叶节点),则尝试合并
if (not self._is_tree(tree['left'])) \
and (not self._is_tree(tree['right'])):
# 对测试数据进行划分
left_set, right_set \
= self._bin_split_data_set(test_data, tree['spInd'], tree['spVal'])
# 计算左右子树没有合并前的误差
err_no_merge = sum(np.power(left_set[:, -1] - tree['left'], 2)) \
+ sum(np.power(right_set[:, -1] - tree['right'], 2))
# 计算左右子树合并后的误差
tree_mean = (tree['left'] + tree['right']) / 2
err_merge = sum(np.power(test_data[:, -1] - tree_mean, 2))
# 如果合并后的误差小于合并前的误差,则进行合并
if err_merge < err_no_merge:
print('merging 02')
return tree_mean
else:
return tree
else:
return tree
# 用于预测
def _reg_tree_eval(self, model, in_data):
"""
用于回归树预测,直接返回叶节点中保存的均值
"""
return float(model)
def _model_tree_eval(self, model, in_data):
"""
用于模型树预测,根据叶节点中的线性回归模型返回预测值
:param model: 叶节点中线性回归模型的一组权值
:param in_data: 输入数据,即一组特征值
:return: 返回预测结果
"""
# 在输入数据的第一列添加 1,因为需要计算偏置
n = np.shape(in_data)[1]
X = np.mat(np.ones((1, n+1)))
X[:, 1:n+1] = in_data
return float(X * model)
def tree_fore_cast(self, tree, in_data, model_eval=_reg_tree_eval):
"""
是一个递归函数。对一组数据的预测。
从上而下遍历树,直至叶节点;然后根据叶节点中保存的模型进行预测。
"""
if not self._is_tree(tree):
return model_eval(tree, in_data)
if in_data[tree['spInd']] > tree['spVal']:
return self.tree_fore_cast(tree['left'], in_data, model_eval)
else:
return self.tree_fore_cast(tree['right'], in_data, model_eval)
def create_fore_cast(self, tree, test_data):
"""
对多组数据进行预测。
"""
m = len(test_data)
y_hat = np.mat(np.zeros((m, 1)))
# 调用 tree_fore_cast() 对数据逐个进行预测
for i in range(m):
y_hat[i, 0] = \
self.tree_fore_cast(tree, np.mat(test_data[i]), self.model_eval)
return y_hat
def load_data_set(file_name):
"""
从文件中获取数据集
:param file_name: 文件名
:return: 返回从文件中获取的数据集
"""
data_set = []
file = open(file_name)
for line in file.readlines():
# 文件中的数据以 tab 键进行分割
cur_line = line.strip().split('\t')
float_line = list(map(float, cur_line))
data_set.append(float_line)
# 返回的数据存放在一个矩阵中
return np.mat(data_set)
def test_regression_tree():
print('\nTest regression tree:')
decision_tree_regression = DecisionTreeCART('regression')
data_set_01 = load_data_set('ex00.txt')
regression_tree_01 = decision_tree_regression.create_tree(data_set_01)
print(regression_tree_01)
data_set_02 = load_data_set('ex0.txt')
regression_tree_02 = decision_tree_regression.create_tree(data_set_02)
print(regression_tree_02)
def test_regression_tree_prune():
print('\nTest regression tree prune:')
decision_tree_regression = DecisionTreeCART('regression')
train_data = load_data_set('ex2.txt')
regression_tree = decision_tree_regression.create_tree(train_data, ops=(0, 1))
print(regression_tree)
test_data = load_data_set('ex2test.txt')
prune_regression_tree = decision_tree_regression.prune(regression_tree, test_data)
print(prune_regression_tree)
def test_model_tree():
print('\nTest model tree:')
decision_tree_model = DecisionTreeCART('model')
data_set01 = load_data_set('exp2.txt')
model_tree = decision_tree_model.create_tree(data_set01)
print(model_tree)
def compare_model():
print('\nCompare model:')
train_data = load_data_set('bikeSpeedVsIq_train.txt')
test_data = load_data_set('bikeSpeedVsIq_test.txt')
# 回归树
decision_tree_regression = DecisionTreeCART('regression')
regression_tree = decision_tree_regression.create_tree(train_data, ops=(1, 20))
y_hat = decision_tree_regression.create_fore_cast(regression_tree, test_data[:, 0])
print(np.corrcoef(y_hat, test_data[:, 1], rowvar=0)[0, 1])
# 模型树
decision_tree_model = DecisionTreeCART('model')
model_tree = decision_tree_model.create_tree(train_data, ops=(1, 20))
y_hat = decision_tree_model.create_fore_cast(model_tree, test_data[:, 0])
print(np.corrcoef(y_hat, test_data[:, 1], rowvar=0)[0, 1])
if __name__ == "__main__":
# 测试回归树
test_regression_tree()
# 测试回归树剪枝
test_regression_tree_prune()
# 测试模型树
test_model_tree()
# 比较各个模型
compare_model()