机器学习系列 07:决策树 02 - CART 算法

  本内容将介绍决策树中的 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_typeerr_type 分别为 _regression_leaf()_regression_err()。在生成模型树是,这两个参数为不同的函数引用。

三、剪枝

  CART 回归树同样需要进行剪枝。

  在函数 _choose_best_split() 中通过 err_thresholdnum_threshold 提前停止划分,实际上就是在进行预剪枝操作。但是在实际任务中,到底应该如何选择 err_thresholdnum_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_typeerr_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_typeerr_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()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章