决策树是一种基本的分类与回归方法。决策树模型具有分类速度快,模型容易可视化等优点,但是同时是也有容易发生过拟合,虽然有剪枝,但也是差强人意。
如果你CART树(对分类回归树)的知识不熟悉,请看这篇文章:
CART分类回归树分析与python实现
提升方法(boosting)在分类问题中,它通过改变训练样本的权重(增加分错样本的权重,减小分对样本的的权重),学习多个分类器,并将这些分类器线性组合,提高分类器性能。boosting数学表示为:
f ( x ) = w 0 + ∑ m = 1 M w m ϕ m ( x ) f(x)=w_{0}+\sum_{m=1}^{M}{w_{m}\phi_{m}(x)} f(x)=w0+m=1∑Mwmϕm(x)
其中:
w m w_{m} wm:第m个弱分类分类器权重
ϕ m ( x ) \phi_{m}(x) ϕm(x):第m个弱分类器
可以看出最终就是基函数的线性组合。
于是决策树与boosting结合产生许多算法,主要有提升树、GBDT等。本文主要是GBDT学习笔记。
以决策树为基函数的提升方法称为提升树,其决策树可以是分类树或回归树。提升树模型可以表示为决策树的加法模型。
f M ( x ) = ∑ m = 1 M T ( x ; θ m ) f_{M}(x)=\sum_{m=1}^{M}{T(x;\theta_{m})} fM(x)=m=1∑MT(x;θm)
其中:
T ( x ; θ m ) T(x;\theta_{m}) T(x;θm):表示第m棵决策树
θ m \theta_{m} θm:表示树的参数
M M M:树的个数
我们不妨假设同事的年龄分别为5岁、6岁、7岁,那么同事的平均年龄就是6岁。所以我们用6岁这个常量来预测同事的年龄,即[6, 6, 6]。每个同事年龄的残差 = 年龄 - 预测值 = [5, 6, 7] - [6, 6, 6],所以残差为[-1, 0, 1]
为了让模型更加准确,其中一个思路是让残差变小。如何减少残差呢?我们不妨对残差建立一颗回归树,然后预测出准确的残差。假设这棵树预测的残差是[-0.9, 0, 0.9],将上一轮的预测值和这一轮的预测值求和,每个同事的年龄 = [6, 6, 6] + [-0.9, 0, 0.9] = [5.1, 6, 6.9],显然与真实值[5, 6, 7]更加接近了, 年龄的残差此时变为[-0.1, 0, 0.1],预测的准确性得到了提升。
重新整理一下思路,假设我们的预测一共迭代3轮 年龄:[5, 6, 7]
第1轮预测:[6, 6, 6]
第2轮预测:[6, 6, 6] + [-0.9, 0, 0.9 ]= [5.1, 6, 6.9]
第2轮残差:[5, 6, 7] - [5.1, 6, 6.9] = [-0.1, 0, 0.1]
第3轮预测:[6, 6, 6] + [-0.9, 0, 0.9] + [-0.08, 0, 0.07 ]= [5.02, 6, 6.97]
第3轮残差:[5, 6, 7] - [5.02, 6, 6.97] = [-0.08, 0, 0.03]
看上去残差越来越小,而这种预测方式就是GBDT算法。
因此,我们需要通过用第m-1轮残差的均值来得到函数 f m ( x ) f_{m}(x) fm(x),进而优化函数 F m ( x ) F_{m}(x) Fm(x)。而回归树的原理就是通过最佳划分区域的均值来进行预测。所以 f m ( x ) f_{m}(x) fm(x)可以选用回归树作为基础模型,将初始值,m-1颗回归树的预测值相加便可以预测y。
首先要实现构建CART树的代码,在实现GBDT代码时,要调用构建CART树的函数
数据集:在参考的GitHub链接里
import copy
import numpy as np
class Node:
"""
树节点类
"""
def __init__(self, feature=None, split_val=None, results=None, left=None, right=None):
"""
:param feature: {int} 样本特征索引
:param split_val: {float} 样本特征集划分值
:param results: {float} 叶节点的值
:param left: {Node} 左子树
:param right: {Node} 右子树
"""
self.feature = feature
self.split_val = split_val
self.results = results
self.left = left
self.right = right
def combine(X, Y):
f"""
:param X: {list} 样本特征
:param Y: {list} 样本标签
:return: data {list} 样本特征与标签集合
"""
data = copy.deepcopy(X)
for i in range(len(X)):
data[i].append(Y[i])
return data
def leaf(dataSet):
"""
:param dataSet: {list}样本数据集
:return: 标签平均值
"""
data = np.array(dataSet)
return np.mean(data[:, -1])
def err_cnt(dataSet):
"""
:param dataSet: {ndarray} 样本数据集
:return: 标签平方误差
"""
return np.var(dataSet[:, -1]) * np.shape(dataSet)[0]
def split_tree(data, feature, split_val):
"""
将data按split_val划分为两个集合
:param data: {ndarray} 样本集合
:param feature: {int} 划分特征索引
:param split_val: {float} 划分值
:return: 左右两个数据集
"""
set_L, set_R = [], []
tmp_LX, tmp_LY, tmp_RX, tmp_RY = [], [], [], []
for i in data:
if data[feature] < split_val:
tmp_LX.append(list(data[0:-1]))
tmp_LY.append(list(data[-1]))
else:
tmp_RX.append(list(data[0:-1]))
tmp_RY.append(list(data[-1]))
set_L.append(tmp_LX)
set_L.append(tmp_LY)
set_R.append(tmp_RX)
set_R.append(tmp_RY)
return set_L, set_R
class CART_regression(object):
"""
CART回归树算法类
"""
def __init__(self,X, Y, min_sample, min_err):
"""
:param X: 样本特征集
:param Y: 样本标签集
:param min_sample: 叶子节点最小样本数量
:param min_err: 构造树结束的最小误差
"""
self.X = X
self.Y = Y
self.min_sample = min_sample
self.min_err = min_err
def fit(self):
"""
构造CART回归树
:return: Root {Node} 根节点
"""
data = combine(self.X, self.Y)
data= np.array(data)
# 初始化
bestErr = err_cnt(data)
# 最佳切分值
bestCreteria = None
# 切分集合
bestSets = None
# 如果data样本数少于最小样本数或者样本误差小于最小误差,生成叶子节点
if len(data) <= self.min_sample or bestErr < self.min_err:
return Node(results=leaf(data))
val_feat = []
for feat in range(len(data[0]) - 1):
val_feat = np.unique(data[:, feat])
for val in val_feat:
set_L, set_R = split_tree(data, feat, val)
comb_L = combine(set_L[0], set_L[1])
comb_R = combine(set_R[0], set_R[1])
err_now = err_cnt(comb_L) + err_cnt(comb_R)
if len(comb_L) < 2 or len(comb_R) < 2:
continue
if err_now < bestErr:
bestErr = err_now
bestCreteria = (feat, val)
bestSets = (set_L, set_R)
if bestErr > self.min_err:
left = CART_regression(bestSets[0][0], bestSets[0][1], self.min_sample, self.min_err)
right = CART_regression(bestSets[1][0], bestSets[1][1], self.min_sample, self.min_err)
return Node(feature=bestCreteria[0], split_val=bestCreteria[1], left=left, right=right)
else:
return Node(results=leaf(data))
def predicts(sample, tree):
"""
预测样本的值
:param sample: {list} 测试样本
:param tree: {Node} 训练好的模型
:return: {float} 预测值
"""
# 如果是叶子节点
if tree.results is not None:
return tree.results
# 非叶子节点
else:
val_sample = sample[tree.feature]
branch = None
if val_sample < tree.split_val:
branch = tree.left
else:
branch = tree.right
return Node(sample, branch)
def cal_err(Y_test, predicts):
"""
:param Y_test: {list} 测试样本标签
:param predicts: {list} 测试样本预测值
:return: error {float}均方误差
"""
y_test = np.array(Y_test)
pre_y = np.array(predicts)
error = np.square(y_test - pre_y).sum() / len(Y_test)
return error
import CART_regression_tree
import numpy as np
def load_data(datafile):
"""
加载数据
:param datafile: 保存训练数据的文件
:return: X:{list} 训练样本特征 Y:{list} 训练样本标签
"""
X, Y = [], []
f = open(datafile)
for line in f.readlines():
sample = []
lines = line.strip().strip('\t')
Y.append(lines[-1])
for i in range(len(lines) - 1):
sample.append(float(lines[i]))
X.append(sample)
return X, Y
class GBDT_RT(object):
"""
GBDT回归算法类
"""
def __init__(self):
self.trees = None # 存储生成的多棵回归树
self.learn_rate = None # 学习率
self.init_val = None # 初始值
def get_init_val(self, y):
"""
获得初始值
:param y: {list}样本标签
:return: {float}样本标签平均值
"""
return sum(y) / len(y)
def get_residuals(self, y, y_hat):
"""
计算残差值
:param y: {list}样本标签
:param y_hat: {list}样本预测值
:return: y_residuals {list}残差
"""
y_residuals = []
for i in range(len(y)):
y_residuals.append(y[i] - y_hat[i])
return y_residuals
def fit(self, X, Y, n_estimates, learn_rate, min_sample, min_err):
"""
训练GBDT模型
:param X: {list}训练样本特征
:param Y: {list}训练样本标签
:param n_estimates: {int}设置GBDT中CART树的数量
:param learn_rate: {float}学习率
:param min_sample: {int}训练CART树时叶节点最小样本数量
:param min_err: {float}训练CART树时生成叶节点的最小误差
:return: trees: {list} GBDT中CART树的列表
"""
self.trees = []
# 获取初始值
self.init_val = self.get_init_val(Y)
n = len(Y)
# 以样本标签的均值作为初始预测值
y_hat = [self.init_val] * n
# 生成初始残差
y_residuals = self.get_residuals(Y, y_hat)
self.learn_rate = learn_rate
for k in range(n_estimates):
# 拟合残差生成CART树
tree = CART_regression_tree.CART_regression(X, y_residuals, min_sample, min_err)
for i in range(len(X)):
res_hat = CART_regression_tree.predict(X[i], tree)
y_hat[i] += self.learn_rate * res_hat
y_residuals = self.get_residuals(Y, y_hat)
self.trees.append(tree)
def GBDT_predict(self, X_test):
"""
预测测试集样本
:param X_test: {list}测试样本特征
:return: predicts {list} 预测值
"""
predicts = []
for i in range(len(X_test)):
pre_y = self.init_val
for tree in self.trees:
pre_y += CART_regression_tree.predict(X_test[i], tree)
predicts.append(pre_y)
return predicts
def GBDT_cal_err(self, Y_test, predicts):
"""
测试样本预测的均方误差
:param Y_test: {list}测试样本标签
:param predicts: {list}测试样本预测值
:return: error: {list}均方误差
"""
y_test = np.array(Y_test)
pre_y = np.array(predicts)
error = np.square(y_test - pre_y).sum() / len(Y_test)
return error
1.GBDT回归的原理及Python实现
2.GBDT原理-Gradient Boosting Decision Tree
3.基于GBDT的数据回归及python实现
3.https://github.com/shiluqiang/GBDT_regression