XGBoost解析系列--源码主流程

  • 前言
  • 入口过程
  • Train过程
    • 1 Train主框架
    • 2 UpdateOneIter流程
      • 21 LazyInitDMatrix过程
      • 22 PredictRaw过程
      • 23 obj_-GetGradient过程
      • 24 gbm_-DoBoost过程
        • 241 分裂过程
        • 242 剪枝过程
    • 3 EvalOneIter流程


0.前言

  本文介绍XGBoost的源代码流程,先梳理源码主干流程,方便读者理解,结合函数名进行说明具体逻辑与功能。如果读者对底层实现感兴趣,后续会有细节具体专题,默认读者已读过《 XGBoost解析系列-准备》、《XGBoost解析系列-原理》。未来还会有分布式实现内容,这部分才是hard work。本文暂时先介绍单机版本实现,这样才更容易理解复杂的分布式版本。

1.入口过程

  重要说明:本文笔者基于xgboost git库的log commit:d9d5293cdbbbf67dc8ff9d4a3f171d0990fdd1ee (2017.10.26 17:31:10提交的commit),以demo文件夹binary_classification案例为例作为说明例子,对应的配置文件为:mushroom.conf,默认读者阅读过《 XGBoost解析系列-准备》,并将相应的配置修改正确,否则下面运行会出错。进入xgboost主目录,运行命令:

./xgboost demo/binary_classification/mushroom.conf

  该语句会运行整个xgboost框架,程序入口函数为cli_main.ccmain函数,调用xgboost::CLIRunTask(argc, argv); 主过程很简单,依次执行:

  1. 判断参数合法性,不合法直接退出;
  2. 调用rabit::Init初始化整个框架的分布特性,rabit是分布式通信库。
  3. 将配置文件通过kv形式读入vector >cfg变量,实现基于common::ConfigIterator继承于ConfigStreamReader,上层父类ConfigReaderBase,实现核心接口Next()函数,调用GetNextToken解析每行的token,分别解析出参数namevalue
  4. 使用sscanf读入命令行参数到cfg变量。
  5. 将cfg变量初始化CLIParam对象,继承于参数类模板dmlc::Parameter,后续会有专题介绍模板宏定义参数。
  6. 根据task类型执行核心流程:训练train、dump模型DumpModel、预测Predict。其中主目录下demo/binary_classification/runexp.sh脚本有完整例子,默认为训练train。
  7. 程序退出时调用rabit::Finalize 释放资源

2.Train过程

2.1 Train主框架

  xgboost最核心部分当属Train过程,训练进入核心函数CLITrain(const CLIParam& param),根据上层实例化param进行训练控制。主要执行以下:

  1. rabit::IsDistributed()判断是否为分布式模式,若是,则打印相关log信息。
  2. DMatrix::Load()根据数据文件路径,支持本地路径与分布式的hadoop文件路径,生成统一URI路径格式;解析底层数据格式,支持libsvm、libffm。解析生成数据源对象,再加载到内存数据CSR格式对象,即DMatrix对象,专题详细介绍查看此处。加载训练数据与预测数据到dtraindeval(支持多份eval数据),并合并到cache_matscache_mats用于构建全局特征统计信息,合并是因为deval中的数据可能超过dtrain的边界,或者dtrain不存在。接着将dtrain加入deval,因为训练集也需要评估。
  3. 使用cache_mats实例化learner,基于C++工厂设计模式,使用Learner::Create()静态方法实例化出具体实现类LearnerImplLearner 继承于rabit::Serializable,分布式下具备相互通信功能。
  4. 检查当前版本号rabit::LoadCheckPoint(); 若为0,为初始状态,判断param.model_in是否存在dump模型路径,存在则使用dump模型来load()初始化learner对象,通过Configure()初始化模型。否则,通过Configure()初始化learner,再调用InitModel()初始化内部模型。两者都需要调用Configure(),根据配置信息初始化成员,过程如下:

    1)构建map cfg_,设置objective、booster、updater、predictor等配置项
    2)调用InitAllowUnknown()构建训练参数对象LearnerTrainParam
    3)不存在模型会调用InitAllowUnknown()构建模型参数对象LearnerModelParam mparam
    4) 前者调用,后者不调用:前者load模型直接实例化boost框架组件实例与目标函数组件实例,通过指针gbm_、obj_调用初始化,而后者在InitModel()才生成实例,因此会跳过。

  InitModel()采用懒惰方式初始化,LazyInitModel()过程如下:

    1) 遍历cache_mats数据得到本地特征数最大值,调用rabit::Allreduce会上报本地特征数最大值,通过Max算子计算全局最大值,利用Allreduce同步所有主机。
    2) 同样基于C++工厂设计模式,使用类静态函数Create()实例化损失函数框架组件与boost框架组件实例,后续会有专题说明。本文mushroom.conf配置中目标函数与boost框架为:
      a) objective=binary:logisticObjFunction基类使用静态函数Create()实例化损失函数框架组件对象RegLossObj()LogisticClassification提供:i)PredTransform生成base_score作为boost初始值;ii)一阶梯度计算FirstOrderGradient与二阶梯度计算SecondOrderGradientObjFunction还实现抽象方法GetGradient()调用具体类的梯度计算方法。
      b)booster=gbtreeGradientBooster基类使用静态函数Create()实例化boost框架组件对象GBTree(),继承于抽象类GradientBooster。i)调用Configure()方法来初始化内部成员:构建GBTreeModel核心成员model_,该成员封装回归树集合vector> trees; ii)清空updaters列表,后续会构建;iii)初始化预测器predictor:基于工厂设计模式,Predictor基类调用静态函数Create(),根据配置参数”cpu_predictor”生成对应CPUPredictor对象。

  5. 根据参数num_round执行迭代:UpdateOneIter执行单次迭代更新,EvalOneIter每次迭代后对预测数据集进行预测。迭代结束前会判断是否保存模型,最后检查rabit 同步version号,rabit::CheckPoint同步所有主机完成状态,开始下一轮的迭代。

  下面把UpdateOneIterEvalOneIter单独进行详解。

2.2 UpdateOneIter流程

  UpdateOneIter流程主要有以下几个步骤:

  1. LazyInitDMatrix(train);
  2. PredictRaw(train, &preds_);
  3. obj_->GetGradient(preds_, train->info(), iter, &gpair_);
  4. gbm_->DoBoost(train, &gpair_, obj_.get());

2.2.1 LazyInitDMatrix过程

  LazyInitDMatrix采用lazy方式构建ColIter列迭代器,加载数据进内存为CSR格式存储,但是xgboost分裂点查找是基于特征内的实例数据,因此需要将CSR格式存储转化为CSC格式存储。HaveColAccess()函数判断DMatrix对象是否存在ColIter成员,不存在则构建:
  1. 根据树构建模式tree_method,取值范围:'auto', 'approx', 'exact', 'hist', 'gpu_exact', 'gpu_hist'等,默认设置为'auto'。使用'auto'自适应到具体的算法,对于数据量小于 222 使用'exact'精确方法,否则会重置'approx'近视方法。计算批次量大小max_row_perbatch=min(用户设置max_row_perbatch, safe_max_row),进行批次处理,其中safe_max_row= 216
  2. 调用InitColAccess()初始化构建ColIter对象,考虑数据采样率prob_buffer_row,使用伯努利采样。分2种情况讨论:
    1)如果记录数num_row小于max_row_perbatch,则调用MakeOneBatch()col_iter_.cpages_绑定SparsePage对象,使用common::ParallelGroupBuilder builder多线程并行加速CSR格式转化为CSC格式,先说下ParallelGroupBuilderSparsePage定义如下:

template<typename ValueType, typename SizeType = size_t>
struct ParallelGroupBuilder {
  std::vector &rptr;  // CSC结构的offset引用,即下标为特征id,value为特征数据在data中的偏移
  std::vector &data; // 真正数据存储的引用,真正数据块存储于SparsePage对象中
  std::vector<std::vector > &thread_rptr;     // 线程统计fid数
  std::vector<std::vector > tmp_thread_rptr;  // 临时对象
}

class SparsePage {
 public:
  bst_uint min_index;
  std::vector offset;           // 真正的CSC结构的offset数据
  std::vector data; // 真正的CSC结构的data数据
}

  ParallelGroupBuilder利用CSR存储数据生成CSC格式存储数据,核心是设计上述数据结构,利用OMP设计多线程操作,线程之间操作互相独立不互斥,极大提升效率。提供核心的3个函数:AddBudget()、InitStorage()、Push()AddBudget()使用多线程并行遍历CSR存储数据,统计featureId对应的记录数,这样就准确知道特征数据开始的偏移;InitStorage()将统计信息汇总,更新后期线程能push操作的范围,这样后期多线程push操作不互斥,同时设置CSC结构的offset数据;push()被多线程调用,将SparseBatch::Entry对象根据当前线程获取到的offset位置插入到data中。
  最后MakeOneBatch执行sort排序,对每个featureId对应的数据进行排序,完成论文中对每个特征,记录按照特征值进行预排序过程。ParallelGroupBuilder实现是个很棒的想法,操作offset节约内存,设计的数据结构使得多线程操作独立而不互斥,无缝连接OMP多线程算法效率真的很高,很值得工作中学习使用。具体操作如图:


XGBoost解析系列--源码主流程_第1张图片

    2)否则记录数过得,会调用MakeManyBatch()构造多个SparsePage对象,不断收集训练数据,一旦记录数大于max_row_perbatch,触发执行MakeColPage操作,该过程与MakeOneBatch类似,不再累述,差别在于MakeColPage是针对部分数据,而不是全量。

  InitColAccess执行完生成col_iter_.cpages_,统计每列特征实例数,生成col_size_变量。col_iter_.cpages_数据没有合并成一块,因为CSC存储每列实例数小于 216 ,可以使用16bit类型存储,节约内存,这是论文提到的Block Compression优化。但是需要借助额外的结构col_size_进行全量数据访问,满足col_size_[j]+=pcol->offset[j+1]-pcol->offset[j],现在训练数据已准备好,也完成了每个特征下,样本记录按照特征值排序,能提供数据的CSC按照列遍历的能力。

  3. SingleColBlock()判断为多个列块, 且没有任何updater,即使设置精确分裂算法,程序也会重置成近似算法grow_histmaker+prune,并且初始化boost框架组件。

2.2.2 PredictRaw过程

  PredictRaw对预测数据进行预测,后续根据预测值才能计算一阶梯度与二阶梯度,间接调用gbm_->PredictBatch,利用CSR行遍历访问能力,批次预测实例数据。gbm_框架配置预测器对象predictor,要么是CPUPredictor或者GPUPredictor,默认配置为CPUPredictor,这里以CPUPredictor进行说明。
  1. 首先predictor调用PredictFromCache,查看上次训练完是否对eval数据集进行评估,train数据集初始化时早已合并到eval数据集,如果找到就直接使用评估后的预测值,返回true。第一次调用时,由于还未训练,直接返回false,进行下面步骤。
  2. 调用InitOutPredictions函数,将预测值设置为base_margin。如果用户在训练数据文件中对实例设置base_margin,则使用设置值,否则使用参数设置model.base_margin
  3. 调用PredLoopInternal函数,以base_margin为初始值,使用GBTreeModel对象进行预测。实际上该对象封装了训练到当前迭代的所有回归树集合,额外判断是否为多分类任务,即num_output_group!=1。使用不同参数,调用预测核心方法PredLoopSpecalize()。该函数中通过K=8设置预测的local batch,利用OMP多线程对记录并行预测。每个线程内按照local batch执行。为什么使用local batch,具备缓存预取功能。其他地方也有使用这种技术。

  PredictRaw过程直接调用PredictBatchPredictBatch是个通用接口。训练过程只有第一次执行调用PredictBatch中的PredLoopInternal,而且第一次调用GBTreeModel对象内的回归树为空,不执行回归树的预测,第一次也就只有base_margin值。纯粹的预测任务中,调用PredictBatch,才会执行回归树的预测过程。

2.2.3 obj_->GetGradient过程

  obj_是目标函数框架组件对象,本文以RegLossObj()为例,该框架是回归损失框架RegLossObj,包含损失函数为LogisticRegression,框架执行过程如下:

for (omp_ulong i = 0; i < ndata; ++i) {
    bst_float p = Loss::PredTransform(preds[i]);
    bst_float w = info.GetWeight(i);
    if (info.labels[i] == 1.0f) w *= param_.scale_pos_weight;
    if (!Loss::CheckLabel(info.labels[i])) label_correct = false;
    out_gpair->at(i) = bst_gpair(Loss::FirstOrderGradient(p, info.labels[i]) * w,
                                 Loss::SecondOrderGradient(p, info.labels[i]) * w);
}

  可见,框架会通过PredTransform将预测值转化为概率值p,支持代价敏感性学习,利用样本权重w(用户设置样本权重)和scale_pos_weight(相当于正例加倍采样逻辑),最终生成样本权重。计算一阶梯度与二阶梯度并赋值给变量out_gpair,这个变量在后面很重要,请读者注意。框架不仅执行梯度计算,还会计算EvalMetric,打印出metric监控。

2.2.4 gbm_->DoBoost过程

  DoBoost流程:i)支持多分类,根据参数num_output_group确定分类输出数。如果是多分类,会利用OMP并行对每类调用BoostNewTrees()构建相应的xgboost模型,预测时会将每个模型的得分输出,利用softmax来计算各个模型的概率,选择最大概率值对应的分类。ii)支持boosted random forest,根据参数num_parallel_tree来构建每次迭代树的个数来组成森林,参数默认1,构建单颗树森林。下面介绍BoostNewTrees()过程:
  1. 构造森林前调用InitUpdater();初始化更新器列表,本文配置最终生成的是"grow_colmaker + prune",根据工厂方法生成精确节点分裂算法更新器对象ColMaker与剪枝器对象TreePruner,均继承于TreeUpdater类,基类提供统一update更新接口。
  2. 遍历num_parallel_tree次,通过InitModel初始化生成回归树RegTree对象,最终合并成森林vectornew_treesRegTree定义以及相关类定义的字段如下:

class RegTree: public TreeModel {
    std::vector node_mean_values;
}


struct RTreeNodeStat {
  bst_float loss_chg;       // 当前节点分裂造成的loss变化
  bst_float sum_hess;       // 当前节点下数据的二阶梯度hessian总和
  bst_float base_weight;    // 当前节点作为叶子节点时的weight值
  int leaf_child_cnt;       // 当前节点下面的叶子节点总数
};


class Node {
   union Info{
      bst_float leaf_value; // 叶子节点值,叶子节点使用
      TSplitCond split_cond;// 分裂条件,非叶子节点使用
    };
    int parent_;            // parent_&((1U<<31)-1)为父节点index,最高位0、1为左、右节点的标记,
    int cleft_, cright_;    // 左、右子节点index,cleft_=-1时为叶子节点,cright_默认-1,但可以存其他信息,设置为0表示待分裂节点。
    unsigned sindex_;       // sindex_&((1U<<31)-1U)为分裂特征的index,最高位0、1为miss值的左、右分裂方向
    Info info_;             // 额外信息
};

template<typename TSplitCond, typename TNodeStat>
class TreeModel {
  std::vector nodes;  // 树节点vector
  std::vector<int>  deleted_nodes;
  std::vector stats;         // 节点对应的统计值
  std::vector leaf_vector;   // 叶子节点vector,存储额外信息
  TreeParam param;          // 树参数设置
}

  3. Updaters对遍历森林,对每颗树进行更新:节点特征分裂建树与树剪枝

2.2.4.1 分裂过程

  DoBoost中核心步骤是3,先说分裂。使用ColMaker对象,具体流程在update接口实现上。为了支持boosted森林,对学习率learning_rate均分,因为随机森林是将结果累加作为预测输出。对应森林中的每棵树构建算法更新器Builder对象builder,Builder类定义在ColMaker内部,操作流程如下:

this->InitData(gpair, *p_fmat, *p_tree);
this->InitNewNode(qexpand_, gpair, *p_fmat, *p_tree);
for (int depth = 0; depth < param.max_depth; ++depth) {
    this->FindSplit(depth, qexpand_, gpair, p_fmat, p_tree);
    this->ResetPosition(qexpand_, p_fmat, *p_tree);
    this->UpdateQueueExpand(*p_tree, &qexpand_);
    this->InitNewNode(qexpand_, gpair, *p_fmat, *p_tree);
    // if nothing left to be expand, break
    if (qexpand_.size() == 0) break;
}

  为了梳理上述的流程,后续会对上述过程进行详解,下面先给出核心类定义与算法执行器Builder的定义,为了排版做了相关修改精简,其中函数定义就不附上参数:

struct SplitEntry {
  bst_float loss_chg;       // 节点分裂的增益loss变化值
  unsigned sindex;          // 分裂特征的index
  bst_float split_value;    // 特征的分裂值
};

template<typename TStats, typename TConstraint>
class ColMaker: public TreeUpdater {  
  TrainParam param;         // 训练参数

  struct ThreadEntry {
    TStats stats;           // TStats类型统计值,比如GradStats
    TStats stats_extra;     // 额外TStats类型统计值
    bst_float last_fvalue;  // 最后扫描到的特征值
    bst_float first_fvalue; // 首次扫描到的特征值
    SplitEntry best;        // 最优的分裂方案(分裂特征index,分裂值,增益loss变化)
  };

  struct NodeEntry {
    TStats stats;           // 节点的统计值
    bst_float root_gain;    // 节点没有分裂时的增益
    bst_float weight;       // 当前计算的最优weight
    SplitEntry best;        // 最优的分裂方案(分裂特征index,分裂值,增益loss变化)
  };


  // 算法执行器
  struct Builder {
    const TrainParam& param;    // 训练参数
    const int nthread;          // 训练期间的OMP线程数
    std::vector feat_index;   // shuffle后feature id对应的inddex
    std::vector<int> position;          // 每个样本当前节点位置
    std::vector< std::vector > stemp;  // 并行计算,线程间互不影响。每个线程计算的节点分裂信息
    std::vector snode;       // 节点统计信息
    std::vector<int> qexpand_;          // 逐层分裂时,对应待分裂节点集合
    std::vector constraints_;          // 约束条件集合

    virtual void Update();              // 核心接口
    inline void InitData(); 
    inline void InitNewNode();          // 初始化qexpand_待分裂节点的统计信息
    inline void UpdateQueueExpand();    // 当每层qexpand_分裂介绍后调用,作为下一层分裂的开始
    inline void ParallelFindSplit();    // 并行当前fid对应的分裂值,不支持嵌套包含OMP并行函数
    inline void UpdateEnumeration();    // 更新枚举最优方案
    inline void EnumerateSplitCacheOpt();   // 统计于UpdateEnumeration,使用local batch实现缓存预期优化
    inline void EnumerateSplit();       // 对于特定特征,枚举出最优分裂值
    virtual void UpdateSolution();      // 更新solution候选集
    inline void FindSplit();            // 逐层分裂,找到expand节点的分裂方案
    inline void ResetPosition();        // 每次分裂后更新样本所在的节点信息
    virtual void SyncBestSolution();    // 每个节点同步下最优方案
    virtual void SetNonDefaultPosition();   // 设置特征值非空实例position
    inline int DecodePosition();        // 解码position
    inline void SetEncodePosition();    // 编码position

  };

  流程详解如下:

  1. InitData初始化开始的数据和状态信息,xgboost支持多任务训练,根据参数param.num_roots设置回归树root节点数param.num_roots注意:builder只更新每棵树,多个root节点是被认为一颗树,只不过有多个root节点而已,为了便于理解,复杂的多任务多分类boosted森林图示如下:


XGBoost解析系列--源码主流程_第2张图片

   InitData流程如下:
    1)初始化每个样本实例开始所在root节点位置 position,如果是单任务,则root节点直接为0;如果是多任务,对应root信息为taskid。
    2)删除二阶梯度小于0的样本实例,直接 position取反,最高位为1,则实例在将来分裂统计会被跳过。
    3)基于随机森林的行采样特性,利用伯努利来采样比例 param.subsample作为训练初始数据,删除实例也是 position取反。
    4)利用 colsample_bytree参数,不同于随机森林列采样,后面会有真正的随机列采样过程,利用伯努利采样生成回归树训练初始特征候选集,构建生成 feat_index,index为序号,value为特征id。
    5)为线程计算初始化临时空间 stemp,每个空间预设256个 ThreadEntry,预设256个统计节点空间 snode,预设 256qexpand_元素,同时设置第一层分裂节点为0到 param.num_roots-1stemp、snode都是基于生成所有节点, qexpand_为当前待分裂节点,树深在7层以内不需要额外 vector内存分配,超过7层会先引起 stemp、snode重新分配。

  2. InitNewNode初始化分裂前初始值,后续分裂结束会被调用继续下轮的分裂查找。
    1)初始化qexpand_待分裂节点对应indexstemp、snode的梯度统计信息与constraints_的预设信息。
    2)利用OMP多线程按行分片,并行统计训练数据所在节点position[ridx]对应的统计信息,position<0表示删除节点或者不进行继续分裂的节点,存在后者是因为xgboost是按照level逐层进行分裂查找,每层的数据是全量数据,按照position来分配到expand_分裂节点id上,对于早期某层节点无法继续分裂情况,会对该节点的所有的实例设置position<0,因此需要对这部分实例进行过滤处理。
    3)合并多线程梯度统计信息到snode;设置约束条件,Colmaker基于NoConstraint,实际上不会执行任何约束设置操作;按照论文公式计算每个qexpand_节点增益与最优值weight值。

  3. 根据参数param.max_depth逐层分裂生成节点和查找分裂最优方案。首先调用FindSplit
    1)每次开始查找前,为了支持随机森林的列采样特性,利用伯努利实现采样feat_index,生成逐层分裂前的特征子集feat_set
    2)使用ColIterator遍历器基于feat_set选择出每次分裂的特征子集,进行按列遍历,每次调用生成列batch数据,调用UpdateSolution找出batch内的最优特征与特征分裂值。该过程非常复杂但有非常重要;首先调整并行模式,对于batch.size * 2 < nthread,直接执行ParallelFindSplit,否则,根据parallel_option并行模式,选择执行EnumerateSplit还是ParallelFindSplit,无论哪种,对于特征存在缺失值情况,会有2次遍历:前向遍历+后向遍历查找,具体算法流程对应论文中的xgboost稀疏算法的前、后遍历流程。分裂枚举有2种并行模式:
      a) EnumerateSplit基于特征粒度的OMP多线程并行,特征上并行,但依次串行处理qexpand节点,在线程分配到的特定特征子集内,找出对应的最优特征和特征分裂值。EnumerateSplit内部首先判断是否使用cacheline aware优化技术。默认是使用,直接调用EnumerateSplitCacheOpt,结束返回,否则执行后续逻辑,与EnumerateSplitCacheOpt是一致的。后者主要使用local cache buffer做缓存预取优化效率,正常逻辑下CSR遍历出特征排序后的实例index是乱序的,访问positiongpair不容易构建缓存,因此每次执行特征枚举之前,构建一次cache buffer,预取batch=32positiongpair到连续内存vector,后续枚举计算容易读取缓存, 便于理解,笔者给出EnumerateSplit过程图示。


XGBoost解析系列--源码主流程_第3张图片

      b) ParallelFindSplit实现单个特征内数据并行+expand粒度并行,i)预先将单个特征内实例数据进行分片,每个线程在各种划分到的数据分片内,遍历数据,根据 position计算对应expand的一阶导数和二阶导数统计值,记录边缘值 first_fvalue、last_fvalue。ii)expand粒度并行,计算 (上一片last_fvalue + 当前first_fvalue)/2作为分裂值、计算分裂值对应的增益作为初始值,以及对应分片数据上其实统计累计值。iii)利用计算好的分片起始统计值,数据分片并行,每个线程在当前数据分片下,更新expand节点的分裂最优值方案,最后会使用SyncBestSolution汇总计算出最终的最优值方案, 便于理解,笔者给出 ParallelFindSplit过程图示。

XGBoost解析系列--源码主流程_第4张图片

    3)同步 qexpand节点内每个节点的最优特征与特征分裂值,最优方案是由OMP多线程并行统计,存储在 stemp变量中。按照 qexpand节点 nid案例遍历下所有线程对应的下 stemp[tid][nid],调用 NodeEntry方法 best即可。
    4)对于 qexpand每个节点,最优增益变化大于阈值 rt_eps,执行树的分裂,分裂成左、右叶子节点,并设置叶子节点 cright_字段为0,表示该叶子节点是待分裂节点,重置父节点的 cleft_、cright_,这样父节点本来属于叶子节点变成非叶子节点。否则不分裂,设置叶子节点的 weight值和 cright_=-1,此时该叶子节点不能够再被分裂,该叶子节点上的所有实例后期会设置 position<0

  4. ResetPosition更新实例的position,即将实例划分到待分裂节点qexpand的候选集上,主要流程如下:
    1)首先对特征值非空实例进行节点划分,通过遍历qexpand非叶子节点,生成分裂特征集合fsplits,利用ColIterator获取fsplits的分片数据,进行batch遍历。每个实例通过DecodePosition()获取对应父节点,根据父节点分裂条件来进入不同子节点,即通过SetEncodePosition()设置实例position()对应的左、右节点上。

    2)对特征值为空的数据进行节点划分,由于实例还挂在qexpand节点上,对应节点仍然属于非叶子节点时,会根据默认分裂方向来划分。实际上对于InitData初始删除的实例也会执行该步骤,只不过这部分实例不会被InitNewNode统计,个人觉得没必要对已删除实例进行额外的划分工作

    3)之前已分配到叶子节点,而叶子节点原本属于待更新状态变成不更新状态是,会对处于该叶子节点上的实例position取反,在将来分裂会被跳过。

  5. UpdateQueueExpand比较简单,对于qexpand上非叶子节点扩展出左、右节点到newnodes,最后qexpand = newnodes;,个人觉得swap效率更高,没用中间多余的拷贝、释放。所以这个过程类似广度遍历,即xgboost是按照逐层训练的。

  6. 继续调用InitNewNode,跟上述2)一致,初始化信息与状态,主要是为了下层训练的开始。

  7. 对于达到训练深度但是还没结束时,将多余的expand节点都设置为不更新叶子节点状态,并设置最优weight值。并将所有节点在builder对象中上统计数据同步到RegTree对象上。

2.2.4.2 剪枝过程

  剪枝过程相对来说要简单点,剪枝器只有TreePruner类,继承于TreeUpdater,由于每次迭代是森林,同样地,计算每颗树的学习率:lr = lr / trees.size();,每棵树进行DoPrune()操作:对所有叶子节点调用TryPruneLeaf(),找到父节点,增益满足loss_chg < min_split_loss,实现剪枝:将左、右子节点删除,设置父节点为叶子节点,同时基于当前节点回溯调用TryPruneLeaf继续剪枝。如果增益不满足则TryPruneLeaf()直接返回。
  剪枝器由于是在最后阶段调用,还包含同步器TreeSyncher对象,也继承于TreeUpdater。如果是单机模式直接返回,否则属于分布式模式,会将所有的回归森林每个RegTree对象进行save序列化生成string,通过rabit::Broadcast广播到分布式中其他节点上,其他节点通过load反序列化生成得到森林,至此,完成实现分布式的迭代训练。

2.3 EvalOneIter流程

  EvalOneIter流程包含打印metric与预测,首选通过目标函数组件得到相应metric配置,基于工厂模式,通过基类Metric调用静态create()实例化出metric对象。通过PredictRaw进行预测,最后通过metric计算打印输出。PredictRaw内部就不仔细将了,本文前面内容已经提到过。至此,一次迭代结束。

你可能感兴趣的:(xgboost,c++)