转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. JDK自带的监控工具方法

    一.概述       SUN 的JDK中的几个工具,非常好用.秉承着有免费,不用商用的原则.以下简单介绍一下这几种工具.(注:本文章下的所有工具都存在JDK5.0以上版本的工具集里(jdk的bin目录 ...

  2. 测试效率 timeit cProfile

    timeit使用 def f1(lIn): l1 = sorted(lIn) # O(nlogn) C语言的 l2 = [i for i in l1 if i<0.5] # O(n) retur ...

  3. Class-dump 安装和使用记录(导出应用的头文件)

    class-dump算是逆向工程中一个入门级的工具,可以很方便的导出程序头文件,可以轻松的了解程序结构方便逆向.安装包下载地址:http://stevenygard.com/projects/clas ...

  4. oraclejdbc

    https://segmentfault.com/q/1010000004952621/a-1020000004955600

  5. 最详细的springmvc-mybatis教程

    链接:http://blog.csdn.net/qq598535550/article/details/51703190

  6. java实验三 敏捷开发与XP实践

    一.实验内容 (一)敏捷开发与XP 软件开发流程的目的是为了提高软件开发.运营.维护的效率,并提高软件的质量.用户满意度.可靠性和软件的可维护性. 光有各种流程的思想是不够的,我们还要有一系列的工具来 ...

  7. C#窗体——四则运算

    用户需求:程序能接收用户输入的整数答案,并判断对错程序结束时,统计出答对.答错的题目数量.补充说明:0——10的整数是随机生成的用户可以选择四则运算中的一种用户可以结束程序的运行,并显示统计结果.在此 ...

  8. bata7

    目录 组员情况 组员1:胡绪佩 组员2:胡青元 组员3:庄卉 组员4:家灿 组员5:恺琳 组员6:翟丹丹 组员7:何家伟 组员8:政演 组员9:黄鸿杰 组员10:何宇恒 组员11:刘一好 展示组内最新 ...

  9. BATA冲刺准备

    目录 第一部分 调研,评测 福大助手的bug IOS端 Android端 福大助手结构体系的思维导图 为什么开发人员没有发现这个bug 假设团队开发这款app,应注意哪些方面(架构.部署运维.微服务等 ...

  10. Internet History, Technology and Security (Week5.1)

    Week5 The Transport layer is built on the Internetwork layer and is what makes our network connectio ...