機器學習系列 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()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章