Gradient Boosting Decision Tree
前提说明:本文是在我学习集成学习时的浅显总结,由于个人水平暂时有限,故基本没有推导与公式过程,可能部分内容还存在错误的理解,请谅解。
GBDT 的全称是 Gradient Boosting Decision Tree(梯度提升树)
1、前置知识
1.1、 向前分步走算法
给定数据集 T = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , ⋯ , ( x N , y N ) } T=\left\{\left(x_{1}, y_{1}\right),\left(x_{2}, y_{2}\right), \cdots,\left(x_{N}, y_{N}\right)\right\} T={(x1,y1),(x2,y2),⋯,(xN,yN)}。损失函数 L ( y , f ( x ) ) L(y, f(x)) L(y,f(x)),基函数集合 { b ( x ; γ ) } \{b(x ; \gamma)\} {b(x;γ)},需要输出加法模型位 f ( x ) f(x) f(x)
-
初始化: f 0 ( x ) = 0 f_{0}(x)=0 f0(x)=0
-
对对m = 1,2,…,M:
– (a) 极小化损失函数:
( β m , γ m ) = arg min β , γ ∑ i = 1 N L ( y i , f m − 1 ( x i ) + β b ( x i ; γ ) ) \left(\beta_{m}, \gamma_{m}\right)=\arg \min _{\beta, \gamma} \sum_{i=1}^{N} L\left(y_{i}, f_{m-1}\left(x_{i}\right)+\beta b\left(x_{i} ; \gamma\right)\right) (βm,γm)=argβ,γmini=1∑NL(yi,fm−1(xi)+βb(xi;γ))
得到参数 β m \beta_{m} βm与 γ m \gamma_{m} γm– (b) 更新:
f m ( x ) = f m − 1 ( x ) + β m b ( x ; γ m ) f_{m}(x)=f_{m-1}(x)+\beta_{m} b\left(x ; \gamma_{m}\right) fm(x)=fm−1(x)+βmb(x;γm) -
得到加法模型:
f ( x ) = f M ( x ) = ∑ m = 1 M β m b ( x ; γ m ) f(x)=f_{M}(x)=\sum_{m=1}^{M} \beta_{m} b\left(x ; \gamma_{m}\right) f(x)=fM(x)=m=1∑Mβmb(x;γm)
前向分步算法将同时求解从m=1到M的所有参数 β m , γ m \beta_{m}, \gamma_{m} βm,γm的问题。
看到这里,回顾AdaBoost,即可以认为AdaBoost算法是模型为加法模型、损失函数为指数函数、学习算法为向前分步走算法时的二分类学习方法。
1.2、 Boosting Tree(提升树)
提升树的通俗理解:假如有个人30岁,我们首先用20岁去拟合,发现损失有10岁,这时我们用6岁去拟合剩下的损失,发现差距还有4岁,第三轮我们用3岁拟合剩下的差距,差距就只有一岁了。如果我们的迭代轮数还没有完,可以继续迭代下面,每一轮迭代,拟合的岁数误差都会减小。最后将每次拟合的岁数加起来便是模型输出的结果。
提升树算法:
(1)初始化
f
0
(
x
)
=
0
f_0(x) = 0
f0(x)=0
(2)对 m = 1,2,…,M:
- 计算每个样本的残差: r m i = y i − f m − 1 ( x i ) , i = 1 , 2 , ⋯ , N r_{m i}=y_{i}-f_{m-1}\left(x_{i}\right), \quad i=1,2, \cdots, N rmi=yi−fm−1(xi),i=1,2,⋯,N
- 拟合残差 r m i r_{mi} rmi学习一棵回归树,得到 T ( x ; Θ m ) T\left(x ; \Theta_{m}\right) T(x;Θm)
- 更新 : f m ( x ) = f m − 1 ( x ) + T ( x ; Θ m ) f_{m}(x)=f_{m-1}(x)+T\left(x ; \Theta_{m}\right) fm(x)=fm−1(x)+T(x;Θm)
(4)得到最终的回归问题的提升树: f M ( x ) = ∑ m = 1 M T ( x ; Θ m ) f_{M}(x)=\sum_{m=1}^{M} T\left(x ; \Theta_{m}\right) fM(x)=m=1∑MT(x;Θm)
2、 梯度提升树算法(Gradient Boosting Decision Tree)
由Friedman提出,其关键是:利用损失函数的负梯度作为提升树算法中的残差的近似值。
Gradient Boost其实是一个框架,里面可以套入很多不同的算法。每一次的计算都是为了减少上一次的残差,为了消除残差,我们可以在残差减少的梯度方向建立一个新的模型,所以说,每一个新模型的建立都为了使得之前的模型残差向梯度方向上减少。它用来优化loss function有很多种。
常见损失函数:
两个版本的GBDT
残差版本把GBDT说成一个残差迭代树,认为每一棵回归树都在学习前N-1棵树的残差,前面所说的主要在描述这一版本。
Gradient版本把GBDT说成一个梯度迭代树,使用梯度下降法求解,认为每一棵回归树在学习前N-1棵树的梯度下降值。
tips:
对初始分类器(函数)的选择就可以直接用0,通过平方差LOSS函数求得的残差当然就是样本本身了,也可以选择样本的均值;一棵树的分裂过程只需要找到找到每个结点的分裂的特征id与特征值,而寻找的方法可以是平均最小均方差,也可以是使得(左子树样本目标值和的平方均值+右子树样本目标值和的平方均值-父结点所有样本目标值和的平方均值)最大的那个分裂点与分裂特征值等等方法;从而将样本分到左右子树中,继续上面过程;注意样本的估计值Fk(x)是前面所有树的估值之和,因此,计算残差时,用样本的目标值减去Fk(x)就可以得到残差了。
3. 代码示例
GradientBoostingRegressor
from sklearn.metrics import mean_squared_error
from sklearn.datasets import make_friedman1
from sklearn.ensemble import GradientBoostingRegressor
X, y = make_friedman1(n_samples=1200, random_state=0, noise=1.0)
X_train, X_test = X[:200], X[200:]
y_train, y_test = y[:200], y[200:]
est = GradientBoostingRegressor(n_estimators=100, learning_rate=0.1,
max_depth=1, random_state=0, loss='ls').fit(X_train, y_train)
mean_squared_error(y_test, est.predict(X_test))
from sklearn.datasets import make_regression
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import train_test_split
X, y = make_regression(random_state=0)
X_train, X_test, y_train, y_test = train_test_split(
X, y, random_state=0)
reg = GradientBoostingRegressor(random_state=0)
reg.fit(X_train, y_train)
reg.score(X_test, y_test)
GradientBoostingRegressor的源码
class BaseGradientBoosting(AbstractBaseGradientBoosting):#定义基类
def __init__(self, loss, learning_rate, n_trees, max_depth,
min_samples_split=2, is_log=False, is_plot=False):
super().__init__()
self.loss = loss
self.learning_rate = learning_rate
self.n_trees = n_trees
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.features = None
self.trees = {}
self.f_0 = {}
self.is_log = is_log
self.is_plot = is_plot
def fit(self, data):
"""
:param data: pandas.DataFrame, the features data of train training
"""
# 掐头去尾, 删除id和label,得到特征名称
self.features = list(data.columns)[1: -1]
# 初始化 f_0(x)
# 对于平方损失来说,初始化 f_0(x) 就是 y 的均值
self.f_0 = self.loss.initialize_f_0(data)
# 对 m = 1, 2, ..., M
logger.handlers[0].setLevel(logging.INFO if self.is_log else logging.CRITICAL)
for iter in range(1, self.n_trees+1):
if len(logger.handlers) > 1:
logger.removeHandler(logger.handlers[-1])
fh = logging.FileHandler('results/NO.{}_tree.log'.format(iter), mode='w', encoding='utf-8')
fh.setLevel(logging.DEBUG)
logger.addHandler(fh)
# 计算负梯度--对于平方误差来说就是残差
logger.info(('-----------------------------构建第%d颗树-----------------------------' % iter))
self.loss.calculate_residual(data, iter)
target_name = 'res_' + str(iter)
self.trees[iter] = Tree(data, self.max_depth, self.min_samples_split,
self.features, self.loss, target_name, logger)
self.loss.update_f_m(data, self.trees, iter, self.learning_rate, logger)
if self.is_plot:
plot_tree(self.trees[iter], max_depth=self.max_depth, iter=iter)
# print(self.trees)
if self.is_plot:
plot_all_trees(self.n_trees)
class GradientBoostingRegressor(BaseGradientBoosting):
def __init__(self, learning_rate, n_trees, max_depth,
min_samples_split=2, is_log=False, is_plot=False):
super().__init__(SquaresError(), learning_rate, n_trees, max_depth,
min_samples_split, is_log, is_plot)
def predict(self, data):
data['f_0'] = self.f_0
for iter in range(1, self.n_trees+1):
f_prev_name = 'f_' + str(iter - 1)
f_m_name = 'f_' + str(iter)
data[f_m_name] = data[f_prev_name] + \
self.learning_rate * \
data.apply(lambda x: self.trees[iter].root_node.get_predict_value(x), axis=1)
data['predict_value'] = data[f_m_name]
参考:
[1]. https://github.com/datawhalechina/team-learning-data-mining/tree/master/EnsembleLearning
[2].集成学习(基础与算法) 周志华
[3].https://blog.csdn.net/niuniuyuh/article/details/76922210
[4].https://blog.csdn.net/zpalyq110/article/details/79527653