本內容將介紹決策樹中的 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()