回顾与本文目标
还记得上一篇博文中,我们回顾了 python
的训练接口,本文我们深挖 XGBoost
的 C++
源代码。从训练迭代内核 XGBoosterUpdateOneIter()
函数开始讨论。争取对训练过程再有更多感性认识。以下引用的代码,为了行文简洁起见,把一些明显用于记录时间、注释行、检查操作等删除。
进行一步迭代 XGBoosterUpdateOneIter()
我们在 c_api.cc
中找到了 XGBoosterUpdateOneIter()
的源代码。不难发现,它也是一件外衣。穿着这件外衣的是 learner()->UpdateOneIter()
。函数前的 XGB_DLL
意味着它是可以调用的函数接口,也与 python
端的接口一致。
XGB_DLL int XGBoosterUpdateOneIter(BoosterHandle handle, int iter, DMatrixHandle dtrain)
{
API_BEGIN();
auto* bst = static_cast<Booster*>(handle);
auto *dtr = static_cast<std::shared_ptr<DMatrix>*>(dtrain);
bst->LazyInit();
bst->learner()->UpdateOneIter(iter, dtr->get());
API_END();
}
这里的 leaner()
函数就定义在 c_api.cc
文件中,如下:
inline Learner* learner()
{
return learner_.get();
}
它是 Booster
类的一个方法。Booster
类中有一个Learner
类的成员变量 learner_
,定义在 learner.h
中,具体实现在 learner.cc
中。函数 UpdateOneIter()
也在这个文件中,如下:
void UpdateOneIter(int iter, DMatrix* train) override
{
if (tparam_.seed_per_iteration || rabit::IsDistributed()) {
common::GlobalRandom().seed(tparam_.seed * kRandSeedMagic + iter);
}
this->PerformTreeMethodHeuristic(train);
this->PredictRaw(train, &preds_);
obj_->GetGradient(preds_, train->Info(), iter, &gpair_);
gbm_->DoBoost(train, &gpair_, obj_.get());
}
这里的 monitor_
是用于记录训练过程的日志信息(其实现在 src/common/timer.h
中,也就是说,这主要是用来计时的)。如果除去检查部分和并行代码随机种子部分,剩下的代码只有:
this->PerformTreeMethodHeuristic(train);
这部分是配置不同的 树方法 (TreeMethod
)。this->PredictRaw(train, &preds_);
它是gbm_->PredictBatch()
的外衣。obj_->GetGradient(preds_, train->Info(), iter, &gpair_);
(TODO)gbm_->DoBoost(train, &gpair_, obj_get());
(TODO)
到这里,我们可以看出,越来越接近实在干活的代码了。从代码的格式,可以猜测,带 &
符号的变量应该是输出变量。注意,这里支持两种模型:线性模型和树模型。我们首先来看一下线性模型的具体实现。
learner.cc
中的 PerformTreeMethodHeuristic()
- 树方法有:
kAuto
自动的、kHist
柱状图的、kApprox
近似的、kExact
精确的、kGPUHist
柱状图和kGPUExact
GPU精确的。如果gbm
不是gbtree
,那么训练目前还没有 GPU 分布式的算法。 - 函数会将树方法传递到
cfg_
,进而传入gbm_
。
learner.cc
中的 PredictRaw()
调用方式如下
inline void PredictRaw(DMatrix* data, HostDeviceVector<bst_float>* out_preds,
unsigned ntree_limit = 0) const
{
gbm_->PredictBatch(data, out_preds, ntree_limit);
}
而在接口方面,这个方法定义于 include\xgboost\gbm.h
。
virtual void PredictBatch(DMatrix* dmat, HostDeviceVector<bst_float>* out_preds,
unsigned ntree_limit = 0) = 0;
可以看出,这是一个虚方法,有两个对应的实现:src/gbm/gblinear.cc
和 src/gbm/gbtree.cc
。这两个方法使用了不同的路径。(下面我们详细讨论这两个函数)
gblinear.cc
中的 PredictBatch()
源代码如下:
void PredictBatch(DMatrix *p_fmat, HostDeviceVector<bst_float> *out_preds,
unsigned ntree_limit) override
{
auto it = cache_.find(p_fmat);
if (it != cache_.end() && it->second.predictions.size() != 0) {
std::vector<bst_float> &y = it->second.predictions;
out_preds->Resize(y.size());
std::copy(y.begin(), y.end(), out_preds->HostVector().begin());
} else {
this->PredictBatchInternal(p_fmat, &out_preds->HostVector());
}
}
其执行逻辑基本上是:
- 如果已经有了预测结果,则把预测结果复制到指定未知。
- 否则调用
PredictBatchInternal()
函数。所以,这个函数基本上也就是件外衣。
注:
- 这里的
p_fmat
就是原来的data
- 这里的
out_preds
就是原来的preds_
所以,我们还需要看一下 PredictBatchInternal()
的实现,它在 gblinear.cc
文件中:
void PredictBatchInternal(DMatrix *p_fmat, std::vector<bst_float> *out_preds) {
model_.LazyInitModel();
std::vector<bst_float> &preds = *out_preds;
const auto& base_margin = p_fmat->Info().base_margin_.ConstHostVector();
const int ngroup = model_.param.num_output_group;
preds.resize(p_fmat->Info().num_row_ * ngroup);
for (const auto &batch : p_fmat->GetRowBatches()) {
const auto nsize = static_cast<omp_ulong>(batch.Size());
#pragma omp parallel for schedule(static)
for (omp_ulong i = 0; i < nsize; ++i) {
const size_t ridx = batch.base_rowid + i;
for (int gid = 0; gid < ngroup; ++gid) {
bst_float margin = (base_margin.size