簡單易懂的GBDT

轉https://www.cnblogs.com/liuyu124/p/7333080.htmlhtml

梯度提高決策樹(Gradient Boosting Decision Tree,GBDT)算法是近年來被說起比較多的一個算法,這主要得益於其算法的性能,以及該算法在各種數據挖掘以及機器學習比賽中的卓越表現,有不少人對GBDT算法進行了開源代碼的開發,比較火的是陳天奇的XGBoost和微軟的LightGBM。node

1、監督學習

一、監督學習的主要任務

監督學習是機器學習算法中重要的一種,對於監督學習,假設有個訓練樣本:git

 

 

 

其中,稱爲第個樣本的特徵,稱爲第個樣本的標籤,樣本標籤能夠爲離散值,如分類問題;也能夠爲連續值,如迴歸問題。在監督學習中,利用訓練樣本訓練出模型,該模型可以實現從樣本特徵到樣本標籤的映射,即:github

 

 

 

爲了可以對映射進行求解,一般對模型設置損失函數,並求得在損失函數最小的狀況下的映射爲最好的映射:算法

 

 

 

對於一個具體的問題,如線性迴歸問題,其映射函數的形式爲:框架

 

 

 

此時對於最優映射函數的求解,實質是對映射函數中的參數的求解。對於參數的求解方法有不少,如梯度降低法。dom

二、梯度降低法

梯度降低法(Gradient Descent,GD)算法是求解最優化問題最簡單、最直接的方法。梯度降低法是一種迭代的優化算法,對於優化問題:機器學習

 

 

 

其基本步驟爲:函數

  • 隨機選擇一個初始點
  • 重複如下過程: 
    • 決定降低的方向:
    • 選擇步長
    • 更新:
  • 直到知足終止條件

梯度降低法的具體過程以下圖所示:性能

這裏寫圖片描述

由以上的過程,咱們能夠看出,對於最終的最優解,是由初始值通過代的迭代以後獲得的,在這裏,設,則爲:

 

 

 

三、在函數空間的優化

以上是在指定的函數空間中對最優函數進行搜索,那麼,可否直接在函數空間(function space)中查找到最優的函數呢?根據上述的梯度降低法的思路,對於模型的損失函數,爲了可以求解出最優的函數,首先,設置初始值爲:

 

 

 

以函數做爲一個總體,對於每個樣本,都存在對應的函數值。與梯度降低法的更新過程一致,假設通過代,獲得最有的函數爲:

 

 

 

其中,爲:

 

 

 

其中,

由上述的過程能夠獲得函數的更新過程:

 

 

 

與上面相似,函數是由參數決定的,即:

 

 

 

2、Boosting

一、集成方法之Boosting

Boosting方法是集成學習中重要的一種方法,在集成學習方法中最主要的兩種方法爲Bagging和Boosting,在Bagging中,經過對訓練樣本從新採樣的方法獲得不一樣的訓練樣本集,在這些新的訓練樣本集上分別訓練學習器,最終合併每個學習器的結果,做爲最終的學習結果,Bagging方法的具體過程以下圖所示:

這裏寫圖片描述

在Bagging方法中,最重要的算法爲隨機森林Random Forest算法。由以上的圖中能夠看出,在Bagging方法中,個學習器之間彼此是相互獨立的,這樣的特色使得Bagging方法更容易並行。與Bagging方法不一樣,在Boosting算法中,學習器之間是存在前後順序的,同時,每個樣本是有權重的,初始時,每個樣本的權重是相等的。首先,第個學習器對訓練樣本進行學習,當學習完成後,增大錯誤樣本的權重,同時減少正確樣本的權重,再利用第個學習器對其進行學習,依次進行下去,最終獲得個學習器,最終,合併這個學習器的結果,同時,與Bagging中不一樣的是,每個學習器的權重也是不同的。Boosting方法的具體過程以下圖所示:

這裏寫圖片描述

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

二、Gradient Boosting

由上圖所示的Boosting方法中,最終的預測結果爲個學習器結果的合併:

 

 

 

這與上述的在函數空間中的優化相似:

 

 

 

根據如上的函數空間中的優化可知,每次對每個樣本的訓練的值爲:

 

 

 

上創建模型,因爲上述是一個求解梯度的過程,所以也稱爲基於梯度的Boost方法,其具體過程以下所示:

這裏寫圖片描述

3、Gradient Boosting Decision Tree

在上面簡單介紹了Gradient Boost框架,梯度提高決策樹Gradient Boosting Decision Tree是Gradient Boost框架下使用較多的一種模型,在梯度提高決策樹中,其基學習器是分類迴歸樹CART,使用的是CART樹中的迴歸樹。

一、分類迴歸樹CART

分類迴歸樹CART算法是一種基於二叉樹的機器學習算法,其既能處理迴歸問題,又能處理分類爲題,在梯度提高決策樹GBDT算法中,使用到的是CART迴歸樹算法,對於CART樹算法的更多信息,能夠參考簡單易學的機器學習算法——分類迴歸樹CART

對於一個包含了個訓練樣本的迴歸問題,其訓練樣本爲:

 

 

 

其中,維向量,表示的是第個樣本的特徵,爲樣本的標籤,在迴歸問題中,標籤爲一系列連續的值。此時,利用訓練樣本訓練一棵CART迴歸樹:

  • 開始時,CART樹中只包含了根結點,全部樣本都被劃分在根結點上:

這裏寫圖片描述

此時,計算該節點上的樣本的方差(此處要乘以),方差表示的是數據的波動程度。那麼,根節點的方差的倍爲:

 

 

 

其中,爲標籤的均值。此時,從維特徵中選擇第維特徵,從個樣本中選擇一個樣本的值:做爲劃分的標準,當樣本的第維特徵小於等於時,將樣本劃分到左子樹中,不然,劃分到右子樹中,經過以上的操做,劃分到左子樹中的樣本個數爲,劃分到右子樹的樣本的個數爲,其劃分的結果以下圖所示:

這裏寫圖片描述

那麼,什麼樣本的劃分纔是當前的最好劃分呢?此時計算左右子樹的方差之和:

 

 

 

其中,爲左子樹中節點標籤的均值,同理,爲右子樹中節點標籤的均值。選擇其中最小的劃分做爲最終的劃分,依次這樣劃分下去,直到獲得最終的劃分,劃分的結果爲:

這裏寫圖片描述

注意:對於上述最優劃分標準的選擇,以上的計算過程能夠進一步優化。

首先,對於

 

 

 

而對於

 

 

 

 

 

 

經過以上的過程,咱們發現,劃分前,記錄節點的值爲:

 

 

 

當劃分後,兩個節點的值的和爲:

 

 

 

最好的劃分,對應着兩個節點的值的和的最大值。

二、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的版本。

參考文獻

相關文章
相關標籤/搜索