由於下下周要在組里介紹一個算法,最近開始提前准備,當初非常自信地寫下自己最喜歡的GBDT,但隨着逐步深入,發現其實自己對這個算法的細節並不是非常了解,了解的只是一些面試題的答案而已……(既然沒有深入了解,又怎么配說最喜歡呢?)
此外,由於野路子的鄙人數學功底不行,對公式的理解非常捉急,故而在本次探究和摸索的過程當中,參考了不少GBDT相關的博客。然而我發現有些博客對細節(尤其是分類)語焉不詳,有些則是寫着寫着混到Xgboost去了,總之似乎並沒有能找到一篇足夠“通俗易懂”的。於是我便想把一個完整的,通俗的例子記錄下來,幫助后來人理解GBDT。以下包括二分類和回歸的實例各一個,逐步推導(某些公式或者結論我實在沒實力推導,就略過一下)。
1、先說一些基礎的東西
GBDT模型訓練的步驟:
- 初始化根節點 F0(x),如果是分類模型,計算其對應的概率p0;
- 計算“偽殘差”,回歸模型即為 y - F(x),而分類模型為 y - p,這個偽殘差即為我們接下來要擬合或者說逼近的目標;
- 遍歷各個特征和其分裂閾值,找出最優的特征和分裂閾值;
- 按照該閾值分裂該特征后,分別計算左右葉節點的對應值 f(x);
- 通過學習率 lr 和 Fm(x) = Fm-1(x) + lr × fm(x) 計算下一個 F(x),如果是分類模型,計算其對應的概率p;
- 重復第(2) ~ (5)步。
以上內容當然不夠詳實,我們通過實例就明白了。
2、二分類實例
我們就用網上的一個實例吧:

單一特征x,y為目標值,非常簡單的二分類。假設我們的樹深度均為1,損失函數為log loss。
(1)我們首先按照第1步,計算 F0(x),分類的 F0(x) 比較特殊,為 ln(pos/neg),即logit。在這里也就是 ln(4/6) = -0.4055 (4個1,6個0),所有x對應的 F0(x) 全都一樣。
由於這是分類問題,我們將 F0(x) 轉化為概率 p0,這一步通過一個簡單的Logistic函數實現,即 1/(1+e-F(x)) ,此處我們得到一堆0.4(因為 F0(x) 都一樣)。
(2)接下來進入第2步,計算偽殘差(姑且叫這個名字,因為像殘差但不是真正的殘差),這個也很簡單,用 y 減去我們剛剛算出來的一堆0.4就行,我們得到:

重復一遍,這個偽殘差即為我們接下來要擬合或者說逼近的目標。
(3)然后是第3步,尋找分裂點,由於這里我們只有一個特征 x,所以我們只需要搜索 x 的所有分裂點(閾值)即可。我們需要搜索一個能讓分裂准則(criterion)達到最小的分裂點。
這里需要說明的是,GBDT的criterion不是gini!!!千萬不要跟CART搞混了。通常我們采用friedman_mse,網上許多例子對於這個mse的計算都着墨頗少,我琢磨了很久可算是琢磨出來了(智商捉急)。
首先我們針對特征 x 枚舉每個分裂點(0.5,1.5,2.5,...,10.5),每個分裂點你可以得到左側子樹和右側子樹,比如分裂點為8.5,左側子樹為 x ≤ 8.5 (即 x = 1, 2, 3, 4, 5, 6, 7, 8),右側子樹為 x>8.5(即 x = 9, 10)。
然后我們計算各個分裂點下,左側子樹和右側子樹各自偽殘差的均值。比如分裂點為8.5時,左側偽殘差的均值為 (-0.4 - 0.4 -0.4 + 0.6 + 0.6 - 0.4 - 0.4 - 0.4) / 8 = -0.15,右側均值為 (0.6 + 0.6) / 2 = 0.6。
接着我們用左右側的每個偽殘差減去其對應的均值,得到誤差error,再計算其對應的平方誤差square_error,這個值描述了我們離我們要逼近的目標(偽殘差)還差多少:

我們將所有 x 對應的square_error加和起來,得到 ∑square_error = 1.5。我們對 x 的每個分裂點(0.5,1.5,2.5,...,10.5)都這么計算一遍,最后得出 ∑square_error 的最小值為1.5,此時的分裂點為 x = 8.5。
(4)分裂完成后,就需要計算左右子樹的值。具體計算方法與損失函數的選取有關,推導詳見Friedman的論文,此處不做展開(數學白痴),僅說結論:
二分類問題常用的損失函數log loss對應的子樹值計算方法為:
![]()
假設我們計算的是左側子樹,首先看一下分子,分子很簡單,即左側偽殘差的和,即 (-0.4 - 0.4 -0.4 + 0.6 + 0.6 - 0.4 - 0.4 - 0.4) = -1.2。
我們再看分母,分母是 (y - 偽殘差) × (1 - y + 偽殘差) 的和,比如 x = 1時,其為 [0 - (-0.4)] × [1 - 0 + (-0.4)] = 0.24,以此類推,我們可以算出左側所有情況下的分母,其總和為1.92。
因此左側子樹的值也就是 -1.2 / 1.92 = -0.625,我們可以用同樣的方法算出右側子樹的值,為2.5。這兩個就是第1棵樹的 f(x)。
至此,第1顆樹的結構完全確定下來了,即為:

(5)現在我們需要更新 F(x) 了。根據GBDT的加法原則,我們只需要將上一棵樹的 F(x) 加上學習率乘以本棵樹的 f(x)。即 Fm(x) = Fm-1(x) + lr × fm(x),此處也就是 F1(x) = F0(x) + lr × f1(x)。
此處 F0(x) 即我們之前算出的 ln(4/6) = -0.4055 ,f1(x) 即我們剛才計算的左右子樹的值 -0.625 和 2.5。每一次更新的步長可以通過line search得到,但比較麻煩,通常取而代之都是采用一個固定的學習率(sklearn中也是這樣做的)。
例如 x = 1時,該節點分在左側,所以f1(1) = -0.625,因此 F1(1) = -0.4055 + 0.1 × (-0.625) = -0.468;類似的, x = 9時,該節點分在右側,所以f1(1) = 2.5,因此 F1(1) = -0.4055 + 0.1 × 2.5 = -0.1555。據此,我們可以算出每個x對應的F1(x),如下表:

當然,為了得到概率,我們還得Logistic一下,通過 1/(1+e-F1(x)) ,我們得到更新后的概率 p1:

(6)假如我們要再加2棵樹,我們可以循環利用(2)~(5)的方法,我們計算新的偽殘差 res_F1,以此算出第2棵樹的最佳分裂點(仍然是 x = 8.5),計算左右子樹的值(左:-0.5705,右:2.168),乘以學習率0.1后拼接到 F1(x) 上,從而得到 F2(x);以此類推,第3棵樹的最佳分裂點為 x = 3.5,左右子樹的值為,左:-1.5915,右:0.6663,類似的方法可以得到F3(x),最終轉化成概率。



我們可以用來sklearn中的GradientBoostingClassifier來核對一下結果,應當是完全一致的(除了精度差異)。

3、回歸實例
GBDT的回歸比分類更為簡單,我們省去了計算概率這一步,而且節點值的計算也相對容易一些。 同樣,我們用網上的實例:

同樣簡單起見,樹深度均為1,損失函數為MSE。
(1)第1步初始化,計算 F0(x),回歸的 F0(x) 非常簡單,取平均就行,也就是 y 的平均值7.307。
(2)第2步,計算偽殘差,也很簡單,y - F0(x),如下表:

(3)第3步,尋找分裂點,由於這里我們只有一個特征 x,所以我們只需要搜索 x 的所有分裂點(閾值)即可。非常幸運的是,回歸問題的分裂准則通常依然采用的是friedman_mse,所以這個過程和我們在分類中的一模一樣。
我們同樣枚舉分裂點,分別計算左右側偽殘差的均值,計算偽殘差與各自均值的平方誤差,尋找使 ∑square_error 最小的分裂閾值。
此處,我們通過枚舉計算可以得到,當 x = 6.5 時,∑square_error 最小,為1.9300。

(4)得到分裂點之后,我們需要計算左右子樹的值。之前說過,具體計算方法與損失函數的選取有關,通常回歸問題的損失函數我們會選擇MSE。MSE對應的計算方法非常簡單——取平均……
我們按照 x = 6.5 分裂左右子樹后,左側為 x = 1, 2, 3, 4, 5, 6,其偽殘差的均值為 (-1.747 - 1.607 - 1.397 - 0.907 - 0.507 -0.257) / 6 = -1.0703;類似的,右側為 x = 7, 8, 9, 10,其偽殘差的均值為 (1.593 + 1.393 + 1.693 + 1.743) / 4 = 1.6055。此二者即左右子樹的值。
至此,我們也就得到了第1棵樹的結構:

(5)類似的,我們來更新 F(x) 。根據GBDT的加法原則,公式是一模一樣的,即 Fm(x) = Fm-1(x) + lr × fm(x),此處也就是 F1(x) = F0(x) + lr × f1(x)。同樣,我們假設學習率設置為0.1,我們通過跟分類一樣的辦法計算得到 F1(x):

如前所述,回歸不需要轉化成概率,F1(x) 所見即所得。
(6)同樣地,假如我們要再加2棵樹,我們可以循環利用(2)~(5)的方法,算偽殘差,找分裂點,算左右子樹的值,更新F(x) 。本例中3棵樹的最佳分裂點都在 x = 6.5。



我們同樣可以用來sklearn中的GradientBoostingRegressor來核對一下結果,應當是完全一致的(除了精度差異)。

4、更進一步
至此,我終於可以大言不慚地說我大致搞懂了GBDT了。當然由於我舉的例子都非常的簡單,在於實際對接的過程中我們可能還會有一些問題,比如:
(1)例子里的樹深度都是1,如果深度更深該怎么辦?
深度更深時其實基本步驟還是一樣的,但在第3步,尋找最佳分裂點時,我們可能要多做幾步。首先我們按照同樣的方法先找到最佳分裂點分裂1次(depth= 1),然后在分裂完的基礎上對左右子樹再次進行分裂,尋找最佳分裂點的准則和方法依然沿用。
比如剛才的分類問題,我們第1棵樹分裂完一次之后,左側為 x = 1, 2, 3, 4, 5, 6, 7, 8,右側為 x = 9, 10。假如我們的樹深度設置為2,那么我們需要再進行一次分裂。由於右側已經純凈(y都為1),所以無須分裂,我們對左側再次枚舉每個分裂點,得到下一級的左右子樹(depth = 2),對子樹計算偽殘差與其均值的平方誤差,找到 ∑square_error 的分裂點。所有操作都是如出一轍的重復而已。
類似的,計算各個子樹的值也是套用同樣的方法,只不過要多算即可子樹而已。最后乘上學習率,再加到上一級函數 F(x) 上即可。
(2)例子里只有1個特征,如果我有幾個特征怎么辦?
方法沒有任何變化,但在第3步,尋找最佳分裂點時,我們需要枚舉每個特征的每個分裂點來進行計算,最后選取最優的分裂特征上的最佳分裂點,僅此而已。
希望本期的內容也足夠通俗易懂。回想前幾天推不出分類時晚上做夢都在想,今天終於可以渾身舒暢了!
配套Notebook:
https://github.com/SilenceGTX/algorithms/blob/master/GBDT.ipynb
