簡單易學的機器學習演算法——梯度提升決策樹GBDT
梯度提升決策樹(Gradient Boosting Decision Tree,GBDT)演算法是近年來被提及比較多的一個演算法,這主要得益於其演算法的效能,以及該演算法在各類資料探勘以及機器學習比賽中的卓越表現,有很多人對GBDT演算法進行了開原始碼的開發,比較火的是陳天奇的XGBoost和微軟的LightGBM。
一、監督學習
1、監督學習的主要任務
2、梯度下降法
梯度下降法的具體過程如下圖所示:
3、在函式空間的優化
二、Boosting
1、整合方法之Boosting
Boosting方法是整合學習中重要的一種方法,在整合學習方法中最主要的兩種方法為Bagging和Boosting,在Bagging中,通過對訓練樣本重新取樣的方法得到不同的訓練樣本集,在這些新的訓練樣本集上分別訓練學習器,最終合併每一個學習器的結果,作為最終的學習結果,Bagging方法的具體過程如下圖所示:
在Bagging方法中,最重要的演算法為隨機森林Random Forest演算法。由以上的圖中可以看出,在Bagging方法中,bb個學習器之間彼此是相互獨立的,這樣的特點使得Bagging方法更容易並行。與Bagging方法不同,在Boosting演算法中,學習器之間是存在先後順序的,同時,每一個樣本是有權重的,初始時,每一個樣本的權重是相等的。首先,第11個學習器對訓練樣本進行學習,當學習完成後,增大錯誤樣本的權重,同時減小正確樣本的權重,再利用第22個學習器對其進行學習,依次進行下去,最終得到bb個學習器,最終,合併這bb個學習器的結果,同時,與Bagging中不同的是,每一個學習器的權重也是不一樣的。Boosting方法的具體過程如下圖所示:
在Boosting方法中,最重要的方法包括:AdaBoost和GBDT。
2、Gradient Boosting
上建立模型,由於上述是一個求解梯度的過程,因此也稱為基於梯度的Boost方法,其具體過程如下所示:
三、Gradient Boosting Decision Tree
在上面簡單介紹了Gradient Boost框架,梯度提升決策樹Gradient Boosting Decision Tree是Gradient Boost框架下使用較多的一種模型,在梯度提升決策樹中,其基學習器是分類迴歸樹CART,使用的是CART樹中的迴歸樹。
1、分類迴歸樹CART
分類迴歸樹CART演算法是一種基於二叉樹的機器學習演算法,其既能處理迴歸問題,又能處理分類為題,在梯度提升決策樹GBDT演算法中,使用到的是CART迴歸樹演算法,對於CART樹演算法的更多資訊,可以參考簡單易學的機器學習演算法——分類迴歸樹CART。
注意:對於上述最優劃分標準的選擇,以上的計算過程可以進一步優化。
2、GBDT——二分類
以參考文獻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_lossn");
// 開始訓練每一棵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.5fn", 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的版本。