转https://www.cnblogs.com/liuyu124/p/7333080.html

梯度提升决策树(Gradient Boosting Decision Tree,GBDT)算法是近年来被提及比较多的一个算法,这主要得益于其算法的性能,以及该算法在各类数据挖掘以及机器学习比赛中的卓越表现,有很多人对GBDT算法进行了开源代码的开发,比较火的是陈天奇的XGBoost和微软的LightGBM。

一、监督学习

1、监督学习的主要任务

监督学习是机器学习算法中重要的一种,对于监督学习,假设有个训练样本:

 

其中,称为第个样本的特征,称为第个样本的标签,样本标签可以为离散值,如分类问题;也可以为连续值,如回归问题。在监督学习中,利用训练样本训练出模型,该模型能够实现从样本特征到样本标签的映射,即:

 

为了能够对映射进行求解,通常对模型设置损失函数,并求得在损失函数最小的情况下的映射为最好的映射:

 

对于一个具体的问题,如线性回归问题,其映射函数的形式为:

 

此时对于最优映射函数的求解,实质是对映射函数中的参数的求解。对于参数的求解方法有很多,如梯度下降法。

2、梯度下降法

梯度下降法(Gradient Descent,GD)算法是求解最优化问题最简单、最直接的方法。梯度下降法是一种迭代的优化算法,对于优化问题:

 

其基本步骤为:

  • 随机选择一个初始点
  • 重复以下过程: 
    • 决定下降的方向:
    • 选择步长
    • 更新:
  • 直到满足终止条件

梯度下降法的具体过程如下图所示:

由以上的过程,我们可以看出,对于最终的最优解,是由初始值经过代的迭代之后得到的,在这里,设,则为:

 

3、在函数空间的优化

以上是在指定的函数空间中对最优函数进行搜索,那么,能否直接在函数空间(function space)中查找到最优的函数呢?根据上述的梯度下降法的思路,对于模型的损失函数,为了能够求解出最优的函数,首先,设置初始值为:

 

以函数作为一个整体,对于每一个样本,都存在对应的函数值。与梯度下降法的更新过程一致,假设经过代,得到最有的函数为:

 

其中,为:

 

其中,。

由上述的过程可以得到函数的更新过程:

 

与上面类似,函数是由参数决定的,即:

 

二、Boosting

1、集成方法之Boosting

Boosting方法是集成学习中重要的一种方法,在集成学习方法中最主要的两种方法为Bagging和Boosting,在Bagging中,通过对训练样本重新采样的方法得到不同的训练样本集,在这些新的训练样本集上分别训练学习器,最终合并每一个学习器的结果,作为最终的学习结果,Bagging方法的具体过程如下图所示:

在Bagging方法中,最重要的算法为随机森林Random Forest算法。由以上的图中可以看出,在Bagging方法中,个学习器之间彼此是相互独立的,这样的特点使得Bagging方法更容易并行。与Bagging方法不同,在Boosting算法中,学习器之间是存在先后顺序的,同时,每一个样本是有权重的,初始时,每一个样本的权重是相等的。首先,第个学习器对训练样本进行学习,当学习完成后,增大错误样本的权重,同时减小正确样本的权重,再利用第个学习器对其进行学习,依次进行下去,最终得到个学习器,最终,合并这个学习器的结果,同时,与Bagging中不同的是,每一个学习器的权重也是不一样的。Boosting方法的具体过程如下图所示:

在Boosting方法中,最重要的方法包括:AdaBoostGBDT

2、Gradient Boosting

由上图所示的Boosting方法中,最终的预测结果为个学习器结果的合并:

 

这与上述的在函数空间中的优化类似:

 

根据如上的函数空间中的优化可知,每次对每一个样本的训练的值为:

 

上建立模型,由于上述是一个求解梯度的过程,因此也称为基于梯度的Boost方法,其具体过程如下所示:

三、Gradient Boosting Decision Tree

在上面简单介绍了Gradient Boost框架,梯度提升决策树Gradient Boosting Decision Tree是Gradient Boost框架下使用较多的一种模型,在梯度提升决策树中,其基学习器是分类回归树CART,使用的是CART树中的回归树。

1、分类回归树CART

分类回归树CART算法是一种基于二叉树的机器学习算法,其既能处理回归问题,又能处理分类为题,在梯度提升决策树GBDT算法中,使用到的是CART回归树算法,对于CART树算法的更多信息,可以参考简单易学的机器学习算法——分类回归树CART

对于一个包含了个训练样本的回归问题,其训练样本为:

 

其中,为维向量,表示的是第个样本的特征,为样本的标签,在回归问题中,标签为一系列连续的值。此时,利用训练样本训练一棵CART回归树:

  • 开始时,CART树中只包含了根结点,所有样本都被划分在根结点上:

此时,计算该节点上的样本的方差(此处要乘以),方差表示的是数据的波动程度。那么,根节点的方差的倍为:

 

其中,为标签的均值。此时,从维特征中选择第维特征,从个样本中选择一个样本的值:作为划分的标准,当样本的第维特征小于等于时,将样本划分到左子树中,否则,划分到右子树中,通过以上的操作,划分到左子树中的样本个数为,划分到右子树的样本的个数为,其划分的结果如下图所示:

那么,什么样本的划分才是当前的最好划分呢?此时计算左右子树的方差之和::

 

其中,为左子树中节点标签的均值,同理,为右子树中节点标签的均值。选择其中最小的划分作为最终的划分,依次这样划分下去,直到得到最终的划分,划分的结果为:

注意:对于上述最优划分标准的选择,以上的计算过程可以进一步优化。

首先,对于:

 

而对于:

 
 

通过以上的过程,我们发现,划分前,记录节点的值为:

 

当划分后,两个节点的值的和为:

 

最好的划分,对应着两个节点的值的和的最大值。

2、GBDT——二分类

在梯度提升决策树GBDT中,通过定义不同的损失函数,可以完成不同的学习任务,二分类是机器学习中一类比较重要的分类算法,在二分类中,其损失函数为:

 

套用上面介绍的GB框架,得到下述的二分类GBDT的算法:

在构建每一棵CART回归树的过程中,对一个样本的预测值应与尽可能一致,对于,其计算过程为:

 
 

在(通常有的地方称为残差,在这里,更准确的讲是梯度下降的方向)上构建CART回归树。最终将每一个训练样本划分到对应的叶子节点中,计算此时该叶子节点的预测值:

 

由Newton-Raphson迭代公式可得:

 

以参考文献3 Idiots’ Approach for Display Advertising Challenge中提供的代码为例:

  • GBDT训练的主要代码为:
void GBDT::fit(Problem const &Tr, Problem const &Va)
{
bias = calc_bias(Tr.Y); //用于初始化的F std::vector<float> F_Tr(Tr.nr_instance, bias), F_Va(Va.nr_instance, bias); Timer timer;
printf("iter time tr_loss va_loss\n");
// 开始训练每一棵CART树
for(uint32_t t = 0; t < trees.size(); ++t)
{
timer.tic(); std::vector<float> const &Y = Tr.Y;
std::vector<float> R(Tr.nr_instance), F1(Tr.nr_instance); // 记录残差和F #pragma omp parallel for schedule(static)
for(uint32_t i = 0; i < Tr.nr_instance; ++i)
R[i] = static_cast<float>(Y[i]/(1+exp(Y[i]*F_Tr[i]))); //计算残差,或者称为梯度下降的方向 // 利用上面的残差值,在此函数中构造一棵树
trees[t].fit(Tr, R, F1); // 分类树的生成 double Tr_loss = 0;
// 用上面训练的结果更新F_Tr,并计算log_loss
#pragma omp parallel for schedule(static) reduction(+: Tr_loss)
for(uint32_t i = 0; i < Tr.nr_instance; ++i)
{
F_Tr[i] += F1[i];
Tr_loss += log(1+exp(-Y[i]*F_Tr[i]));
}
Tr_loss /= static_cast<double>(Tr.nr_instance); // 用上面训练的结果预测测试集,打印log_loss
#pragma omp parallel for schedule(static)
for(uint32_t i = 0; i < Va.nr_instance; ++i)
{
std::vector<float> x = construct_instance(Va, i);
F_Va[i] += trees[t].predict(x.data()).second;
} double Va_loss = 0;
#pragma omp parallel for schedule(static) reduction(+: Va_loss)
for(uint32_t i = 0; i < Va.nr_instance; ++i)
Va_loss += log(1+exp(-Va.Y[i]*F_Va[i]));
Va_loss /= static_cast<double>(Va.nr_instance); printf("%4d %8.1f %10.5f %10.5f\n", t, timer.toc(), Tr_loss, Va_loss);
fflush(stdout);
}
}
  • CART回归树的训练代码为:
void CART::fit(Problem const &prob, std::vector<float> const &R, std::vector<float> &F1){
uint32_t const nr_field = prob.nr_field; // 特征的个数
uint32_t const nr_sparse_field = prob.nr_sparse_field;
uint32_t const nr_instance = prob.nr_instance; // 样本的个数 std::vector<Location> locations(nr_instance); // 样本信息 #pragma omp parallel for schedule(static)
for(uint32_t i = 0; i < nr_instance; ++i)
locations[i].r = R[i]; // 记录每一个样本的残差 for(uint32_t d = 0, offset = 1; d < max_depth; ++d, offset *= 2){// d:深度 uint32_t const nr_leaf = static_cast<uint32_t>(pow(2, d)); // 叶子节点的个数 std::vector<Meta> metas0(nr_leaf); // 叶子节点的信息 for(uint32_t i = 0; i < nr_instance; ++i){ Location &location = locations[i]; //第i个样本的信息 if(location.shrinked)
continue; Meta &meta = metas0[location.tnode_idx-offset]; //找到对应的叶子节点 meta.s += location.r; //残差之和
++meta.n;
} std::vector<Defender> defenders(nr_leaf*nr_field); //记录每一个叶节点的每一维特征
std::vector<Defender> defenders_sparse(nr_leaf*nr_sparse_field);
// 针对每一个叶节点 for(uint32_t f = 0; f < nr_leaf; ++f){ Meta const &meta = metas0[f]; // 叶子节点 double const ese = meta.s*meta.s/static_cast<double>(meta.n); //该叶子节点的ese for(uint32_t j = 0; j < nr_field; ++j)
defenders[f*nr_field+j].ese = ese; for(uint32_t j = 0; j < nr_sparse_field; ++j)
defenders_sparse[f*nr_sparse_field+j].ese = ese;
} std::vector<Defender> defenders_inv = defenders; std::thread thread_f(scan, std::ref(prob), std::ref(locations),
std::ref(metas0), std::ref(defenders), offset, true);
std::thread thread_b(scan, std::ref(prob), std::ref(locations),
std::ref(metas0), std::ref(defenders_inv), offset, false);
scan_sparse(prob, locations, metas0, defenders_sparse, offset, true);
thread_f.join();
thread_b.join(); // 找出最佳的ese,scan里是每个字段的最佳ese,这里是所有字段的最佳ese,赋值给相应的tnode
for(uint32_t f = 0; f < nr_leaf; ++f){
// 对于每一个叶节点都找到最好的划分
Meta const &meta = metas0[f];
double best_ese = meta.s*meta.s/static_cast<double>(meta.n); TreeNode &tnode = tnodes[f+offset];
for(uint32_t j = 0; j < nr_field; ++j){ Defender defender = defenders[f*nr_field+j];//每一个叶节点都对应着所有的特征 if(defender.ese > best_ese)
{
best_ese = defender.ese;
tnode.feature = j;
tnode.threshold = defender.threshold;
} defender = defenders_inv[f*nr_field+j];
if(defender.ese > best_ese)
{
best_ese = defender.ese;
tnode.feature = j;
tnode.threshold = defender.threshold;
}
}
for(uint32_t j = 0; j < nr_sparse_field; ++j)
{
Defender defender = defenders_sparse[f*nr_sparse_field+j];
if(defender.ese > best_ese)
{
best_ese = defender.ese;
tnode.feature = nr_field + j;
tnode.threshold = defender.threshold;
}
}
} // 把每个instance都分配给树里的一个叶节点下
#pragma omp parallel for schedule(static)
for(uint32_t i = 0; i < nr_instance; ++i){ Location &location = locations[i];
if(location.shrinked)
continue; uint32_t &tnode_idx = location.tnode_idx;
TreeNode &tnode = tnodes[tnode_idx];
if(tnode.feature == -1){
location.shrinked = true;
}else if(static_cast<uint32_t>(tnode.feature) < nr_field){ if(prob.Z[tnode.feature][i].v < tnode.threshold)
tnode_idx = 2*tnode_idx;
else
tnode_idx = 2*tnode_idx+1;
}else{
uint32_t const target_feature = static_cast<uint32_t>(tnode.feature-nr_field);
bool is_one = false;
for(uint64_t p = prob.SJP[i]; p < prob.SJP[i+1]; ++p)
{
if(prob.SJ[p] == target_feature)
{
is_one = true;
break;
}
}
if(!is_one)
tnode_idx = 2*tnode_idx;
else
tnode_idx = 2*tnode_idx+1;
}
}
} // 用于计算gamma
std::vector<std::pair<double, double>>
tmp(max_tnodes, std::make_pair(0, 0));
for(uint32_t i = 0; i < nr_instance; ++i)
{
float const r = locations[i].r;
uint32_t const tnode_idx = locations[i].tnode_idx;
tmp[tnode_idx].first += r;
tmp[tnode_idx].second += fabs(r)*(1-fabs(r));
} for(uint32_t tnode_idx = 1; tnode_idx <= max_tnodes; ++tnode_idx)
{
double a, b;
std::tie(a, b) = tmp[tnode_idx];
tnodes[tnode_idx].gamma = (b <= 1e-12)? 0 : static_cast<float>(a/b);
} #pragma omp parallel for schedule(static)
for(uint32_t i = 0; i < nr_instance; ++i)
F1[i] = tnodes[locations[i].tnode_idx].gamma;// 重新更新F1的值
}

在参考文献A simple GBDT in Python中提供了Python实现的GBDT的版本。

参考文献

简单易懂的GBDT的更多相关文章

  1. scikit-learn 梯度提升树(GBDT)调参小结

    在梯度提升树(GBDT)原理小结中,我们对GBDT的原理做了总结,本文我们就从scikit-learn里GBDT的类库使用方法作一个总结,主要会关注调参中的一些要点. 1. scikit-learn ...

  2. 梯度提升树(GBDT)原理小结

    在集成学习之Adaboost算法原理小结中,我们对Boosting家族的Adaboost算法做了总结,本文就对Boosting家族中另一个重要的算法梯度提升树(Gradient Boosting De ...

  3. Adaboost\GBDT\GBRT\组合算法

    Adaboost\GBDT\GBRT\组合算法(龙心尘老师上课笔记) 一.Bagging (并行bootstrap)& Boosting(串行) 随机森林实际上是bagging的思路,而GBD ...

  4. LightGBM中GBDT的实现

    现在LightGBM开源了,这里将之前的一个文档发布出来供大家参考,帮助更快理解LightGBM的实现,整体思路应该是类似的. LightGBM优雅,快速,效果好,希望LightGBM越来越好:) L ...

  5. 决策树和基于决策树的集成方法(DT,RF,GBDT,XGBT)复习总结

    摘要: 1.算法概述 2.算法推导 3.算法特性及优缺点 4.注意事项 5.实现和具体例子 内容: 1.算法概述 1.1 决策树(DT)是一种基本的分类和回归方法.在分类问题中它可以认为是if-the ...

  6. GBDT的基本原理

    这里以二元分类为例子,给出最基本原理的解释 GBDT 是多棵树的输出预测值的累加 GBDT的树都是 回归树 而不是分类树 分类树 分裂的时候选取使得误差下降最多的分裂 计算的技巧 最终分裂收益按照下面 ...

  7. android 史上最简单易懂的跨进程通讯(Messenger)!

    不需要AIDL也不需要复杂的ContentProvider,也不需要SharedPreferences或者共享存储文件! 只需要简单易懂的Messenger,它也称为信使,通过它可以在不同进程中传递m ...

  8. [Machine Learning & Algorithm] 决策树与迭代决策树(GBDT)

    谈完数据结构中的树(详情见参照之前博文<数据结构中各种树>),我们来谈一谈机器学习算法中的各种树形算法,包括ID3.C4.5.CART以及基于集成思想的树模型Random Forest和G ...

  9. GBDT算法原理深入解析

    GBDT算法原理深入解析 标签: 机器学习 集成学习 GBM GBDT XGBoost 梯度提升(Gradient boosting)是一种用于回归.分类和排序任务的机器学习技术,属于Boosting ...

随机推荐

  1. Vue 实例详解与生命周期

    Vue 实例详解与生命周期 Vue 的实例是 Vue 框架的入口,其实也就是前端的 ViewModel,它包含了页面中的业务逻辑处理.数据模型等,当然它也有自己的一系列的生命周期的事件钩子,辅助我们进 ...

  2. HDU 4568 Hunter 最短路+TSP

    题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=4568 Hunter Time Limit: 2000/1000 MS (Java/Others)Me ...

  3. JVM的GC策略

    1 前言 GC(Garbage Collect)是jvm对于内存管理的核心功能,正是因为它才让Java程序员从内存释放的苦海中脱离出来,所以作为一个程序员都有必要去了解一下他的原理. 说一句题外话,我 ...

  4. Enterprise Library 2.0 参考源码索引

    http://www.projky.com/entlib/2.0/Microsoft/Practices/EnterpriseLibrary/Caching/BackgroundScheduler.c ...

  5. Android封装TitleBar基本适用所有常规开发

    Android封装TitleBar基本适用所有常规开发 github地址:https://github.com/SiberiaDante/SiberiaDanteLib/blob/master/sib ...

  6. Sql Server中判断表、列不存在则创建的方法[转]

    一.Sql Server中如何判断表中某列是否存在 首先跟大家分享Sql Server中判断表中某列是否存在的两个方法,方法示例如下: 比如说要判断表A中的字段C是否存在两个方法: 第一种方法  ? ...

  7. By.cssSelector定位元素一个不足发现

     这个如果用cssSelector定位,代码如下,此时输出的数值是0 System.out.println(driver.findElements(By.cssSelector("div[c ...

  8. fidder监控请求响应时间和请求IP(摘抄至网络)

    增加监控请求的详情时间 在CustomRules.js的class Handlers中增加  //添加请求的响应时间 public static BindUIColumn("Time Tak ...

  9. WDS迁移注意事项

    先说背景:公司使用WDS来部署操作系统,目前DHCP和WDS都安装在同一台服务器上,但是此服务器已过保,所以筹划迁移,将WDS和DHCP分别迁移到两台服务器上.迁移计划是保持WDS暂时不动,DHCP先 ...

  10. Longest Substring with At Most Two Distinct

    Given a string, find the length of the longest substring T that contains at most 2 distinct characte ...