[白話解析] 通俗解析集成學習之GBDT
0x00 摘要
本文將為大家講解GBDT這個機器學習中非常重要的算法。因為這個算法屬於若干算法或者若干思想的結合,所以很難找到一個現實世界的通俗例子來講解,所以只能少用數學公式來盡量減少理解難度。
0x01 定義 & 簡述
我們首先給出定義和概述,讓大家有個直觀的概念,然后再針對每一個概念和環節做詳述。
1. GBDT(Gradient Boosting Decision Tree)= GB + DT
首先,我們給出一個定義。
-
GB:Gradient Boosting,一種算法框架,用梯度計算擬合損失函數的提升過程。
-
G:Gradient 梯度,確切地說是Gradient Descent(梯度下降),實現層面上是用損失函數的負梯度來擬合本輪損失函數的近似值,進而擬合得到一個弱學習器。
-
B:Boosting 一種集成學習算法,通過迭代訓練一系列弱學習器(每一次訓練都是在前面已有模型的預測基礎上進行),組合成一個強學習器,來提升回歸或分類算法的精確度。"Boosting"的基本思想是通過某種方式使得每一輪基學習器在訓練過程中更加關注上一輪學習錯誤的樣本。提升方法 Boosting 采用的是加法模型和前向分布算法來解決分類和回歸問題。
-
DT:Decision Tree,決策樹,一種常用的用來做回歸或分類的算法,可以理解成樹狀結構的if-else規則的集合。在這里就是上述的弱學習器了。
總結起來,所謂GBDT,就是通過迭代訓練一系列決策樹,其中每棵決策樹擬合的是基於當前已訓練好的決策樹們(當前模型)所得到損失函數的負梯度值,然后用這些決策樹來共同決策,得到最終的結果。
2. 白話簡述
我們需要得到一棵決策樹,這個樹是用殘差擬合出來的。為了提高精度,當使用一棵樹訓練完以后,我們還在它的基礎上再去把它的殘差拿來做二次加工、三次加工......這樣就有了后面的樹。
3. 概括要點
損失函數和負梯度
損失函數:機器學習的訓練目標是讓損失函數最小,損失函數極小化,意味着擬合程度最好,對應的模型參數即為最優參數。
梯度向量:從幾何意義上講,梯度向量就是函數變化增加最快的地方。沿着梯度向量的方向,更加容易找到函數的最大值。反過來說,沿着梯度向量相反的方向,梯度減少最快,也就是更加容易找到函數的最小值。
可以把 GBDT 的求解過程想象成線性模型優化的過程。在線性模型優化的過程中。利用梯度下降我們總是讓參數向負梯度的方向移動,一步步的迭代求解,得到最小化的損失函數。即通過梯度下降可以最小化損失函數。
殘差和負梯度
殘差 在數理統計中是指實際觀察值與估計值(擬合值)之間的差。殘差r=y−f(x)越大,表明前一輪學習器f(x)的結果與真實值y相差較大,那么下一輪學習器通過擬合殘差或負梯度,就能糾正之前的學習器犯錯較大的地方。
GBDT 是使用負梯度進行boost,殘差是一種特例。再准確的說,GBDT的正則化操作中有“學習步長”,即每一步擬合的不是負梯度,而是負梯度的α倍(負梯度與學習率的乘積),這時候擬合目標和殘差就更不相同了。
Boosting和負梯度
"Boosting" 的基本思想是通過某種方式使得每一輪基學習器在訓練過程中更加關注上一輪學習錯誤的樣本。
"Gradient Boosting" 是用梯度計算擬合損失函數的提升過程。用損失函數的負梯度(截止到當前的梯度)來擬合本輪損失函數的近似值,進而擬合得到一個弱學習器,在迭代的每一步構建的弱學習器都是為了彌補已有模型的不足。最后將所有的弱學習器結合起來,得到一個強學習器。這樣通過累加各個學習器使得損失函數減小。
每新建一個樹會做如下操作:
根據之前的fm和損失函數的負梯度來計算殘差 ---> 構建新樹來擬合殘差 (特征划分 / 更新葉子節點預測值) ---> 更新模型 (計算出f{m+1})
兩個層面的隨機梯度下降
參數梯度下降是根據負梯度調整原來的參數,在參數層面調整;而同樣根據負梯度,可以獲得一個新的函數/基學習器(通過獲得新的參數實現,但是參數的意義是通過函數體現),這就是在函數層面調整。
-
在參數空間中優化,每次迭代得到參數的增量,這個增量就是負梯度乘上學習率;
-
在函數空間中優化,每次得到增量函數,這個函數會去擬合負梯度,在GBDT中就是一個個決策樹。要得到最終結果,只需要把初始值或者初始的函數加上每次的增量。處理粒度更新參數w,從而更新函數F(X),使得損失函數L(y,F(X))最小。
可以認為 “GB” 是每一輪基學習器在負梯度上逐步趨近最小損失函數。
基學習器和負梯度
GBDT 中的這個基學習器是一棵分類回歸樹(CART),我們可以使用決策樹直接擬合梯度。假入我們現在有 t 棵樹,我們需要去學習是第 t+1 棵樹,那么如何學習第 t+1 棵樹才是最優的樹呢?這個時候我們參考梯度優化的思想。現在的 t 課樹就是我們現在的狀態,使用這個狀態我們可以計算出現在的損失。如何讓損失更小呢?我們只需要讓 t+1 棵樹去擬合損失的負梯度。正是借用了梯度優化的思想。所以叫梯度提升樹。
GBDT解決分類問題時,擬合的都是類別的概率,是一個值,跟邏輯回歸的思想差不多;二分類問題中,類別的個數與樹的個數肯定是無關的,但多分類問題中,樹的個數就等於k*m,k為類別個數,m為對每個類別訓練的樹的個數;GBDT的多分類問題使用的就是一對多的方法,只要關注訓練該類別所使用的m課樹的擬合值的匯總結果是否大於閾值即可。
加法模型
提升方法 Boosting 采用的是加法模型和前向分布算法來解決分類和回歸問題,實現學習的優化過程。
GBDT是在用forward stage-wise的方式來fit一個additive model。假設我們最終得到一個high performance的模型為F(x),F(x)其實是由多個f(x)累加的。
從additive model 的角度上來看 ,Fm(x) = Fm-1(x) + h(x)=y,則h(x) = y - Fm-1(x)即殘差,所以每次iteration,一個新的cart 樹似乎都是在擬合殘差,但只是一個相近值,也是一個比較朴素的想法。
如果損失函數為square error,其導數即是殘差的導數。 我們都知道,梯度下降是沿着負梯度方向下降的(一階泰勒公式展開推導,即我們能通過一階泰勒展開證明負梯度方向是下降最快的方向)。所以,h(x)去擬合殘差即可。 但是如果用absolute error 、huber error等,那么殘差就不等於負梯度了。此時即用h(x)來擬合負梯度(一般是負梯度與學習率的乘積)。
boosting 的可加性(additive)。可加性指的是 h 的可加,而不是 x 的可加。比如 x 是決策樹,那兩棵決策樹本身怎么加在一起呢? 你頂多把他們並排放在一起。可加的只是樣本根據決策樹模型得到的預測值 h(x,D)罷了。
提升樹
注意GBDT不論是用於回歸還是分類,其基學習器 (即單棵決策樹) 都是回歸樹,即使是分類問題也是將最后的預測值映射為概率,因為回歸樹的預測值累加才是有意義的,而GBDT是把所有樹的結論累加起來做最終結論的。
GBDT在回歸問題中,每輪迭代產生一棵CART(分類和回歸樹),迭代結束時將得到多棵CART回歸樹,然后把所有的樹加總起來就得到了最終的提升樹。
GBDT的核心就在於,每一棵樹學的是之前所有樹結論和的殘差,這個殘差就是一個加預測值后能得真實值的累加量。
回歸樹
每新建一個樹 ,先計算殘差,再根據殘差構建回歸樹 (也就是擬合殘差)。構建樹時候特征划分,更新葉子節點預測值,求出使損失函數最小 (也就是擬合最好) 的預測值:
for m = 1 to M 循環生成決策樹,每新建一個樹 :
(A) 計算更新殘差 res_m = label - f_m
(B) 使用回歸樹來擬合殘差 res_m,葉子結點value就是殘差數值。
(C) 計算更新 “葉子節點預測值“ f_m = f_prev + lr * res_m
每做一次特征划分,計算SE = (殘差res_m - 殘差均值),即每棵樹對應葉子節點的殘差之和。
針對每個葉子節點樣本,計算更新 ”葉子節點預測值“,求出使損失函數最小,也就是擬合葉子節點最好的的輸出值
(D) 更新模型 F_m
假如我們已經進行了五次迭代,那么這個算法流程中,第五顆樹的數據打印如下:
第5棵樹: mse_loss:0.0285
id age weight label f_0 res_1 f_1 res_2 f_2 res_3 \
0 1 5 20 1.1 1.475 -0.375 1.4375 -0.3375 1.40375 -0.30375
1 2 7 30 1.3 1.475 -0.175 1.4575 -0.1575 1.44175 -0.14175
2 3 21 70 1.7 1.475 0.225 1.4975 0.2025 1.51775 0.18225
3 4 30 60 1.8 1.475 0.325 1.5075 0.2925 1.53675 0.26325
f_3 res_4 f_4 res_5 f_5
0 1.373375 -0.273375 1.346037 -0.246037 1.321434
1 1.427575 -0.127575 1.414818 -0.114818 1.403336
2 1.535975 0.164025 1.552377 0.147622 1.567140
3 1.563075 0.236925 1.586768 0.213232 1.608091
這里有兩個特征: age, weight。而label數值是1.1,1.3 ,1.7,1.8。
可以看出res是在梯度下降。f_0都初始化為 1.475。假設學習率是0.1。第五顆回歸樹的葉子結點是res_5,而f_5 = f_4 + 0.1 * res_4。
本例對應算法在后文中會給出詳細代碼。
4. 總結
最后總結下 :
- DT 就是決策樹。
- B 就是連續生成一系列的決策樹,后一個決策樹在前一個決策樹基礎上做優化 。
- G 就是這些決策樹擬合的是殘差(回歸樹的葉子結點value是殘差數值),這個殘差是按照損失函數的負梯度方向步進,一步一步遞進之后,使得最后損失函數最小。
0x02 相關概念
下面會逐一詳述相關概念,以及其在GBDT如何應用。
1. 損失函數
損失函數(loss function):機器學習中,為了評估模型擬合的好壞,通常用損失函數來度量擬合的程度。比如在線性回歸中,損失函數通常為樣本輸出和假設函數的差取平方。
損失函數極小化,意味着擬合程度最好,對應的模型參數即為最優參數。不同的損失函數代表不同優化目標,像MAE,RMSE,指數損失,交叉熵及其他損失。
2. 殘差
殘差在數理統計中是指實際觀察值與估計值(擬合值)之間的差。即殘差(residual)是因變量的觀測值 yi 與根據估計的回歸方程求出的預測 y^i 之差。
3. 梯度
在微積分里面,對多元函數的參數求∂偏導數,把求得的各個參數的偏導數以向量的形式寫出來,就是梯度。
梯度向量從幾何意義上講,就是函數變化增加最快的地方。沿着梯度向量的方向,更加容易找到函數的最大值。反過來說,沿着梯度向量相反的方向,梯度減少最快,也就是更加容易找到函數的最小值。
負梯度也被稱為“響應 (response)” 或 “偽殘差 (pseudo residual)”,從名字可以看出是一個與殘差接近的概念。
直覺上來看,殘差 r = y − f(𝑥) 越大,表明前一輪學習器 f(𝑥) 的結果與真實值 y 相差較大,那么下一輪學習器通過擬合殘差或負梯度,就能糾正之前的學習器犯錯較大的地方。
4. 梯度下降
在機器學習算法中,在最小化損失函數時,可以通過梯度下降法來一步步的迭代求解,得到最小化的損失函數,和模型參數值。反過來,如果我們需要求解損失函數的最大值,這時就需要用梯度上升法來迭代了。
梯度下降法和梯度上升法是可以互相轉化的。比如我們需要求解損失函數f(θ)的最小值,這時我們需要用梯度下降法來迭代求解。但是實際上,我們可以反過來求解損失函數 -f(θ)的最大值,這時梯度上升法就派上用場了。
首先來看看梯度下降的一個直觀的解釋。
比如我們在一座大山上的某處位置,由於我們不知道怎么下山,於是決定走一步算一步,也就是在每走到一個位置的時候,求解當前位置的梯度,沿着梯度的負方向,也就是當前最陡峭的位置向下走一步,然后繼續求解當前位置梯度,向這一步所在位置沿着最陡峭最易下山的位置走一步。這樣一步步的走下去,一直走到覺得我們已經到了山腳。當然這樣走下去,有可能我們不能走到山腳,而是到了某一個局部的山峰低處。
從上面的解釋可以看出,梯度下降不一定能夠找到全局的最優解,有可能是一個局部最優解。當然,如果損失函數是凸函數,梯度下降法得到的解就一定是全局最優解。
5. 在參數空間最優參數估計
對於“在參數空間進行最優參數點估計”這種問題,有一種解法是梯度下降法(Steepest Gradient Descent,SGD),采用分布加和擴展的方案:其實就是給定起點后,貪心尋找最優解。這里的貪心是指每步都是在上一步的基礎上往函數下降最快的方向走。
- 給定一個初始點 x0
- 對 i = 1 , 2, ... , n 分別做如下迭代
- x i = x {i - 1} + bi * gi,其中gi表示 f 在 x {i - 1} 上的負梯度值,bi 是步長 ,是 通過在 gi 方向線性搜索來動態調整 的。
- 一直到 |gi|足夠小,或者 |x i - x {i - 1} |足夠小,即函數收斂
其實就是給定起點后,貪心尋找最優解。這里的貪心是指每步都是在上一步的基礎上往函數下降最快的方向走。
尋得的解可以表示為:
x k = x0 + b1 * g1 + ... + bk * gk
6. 在函數空間最優函數估計
以上是在參數空間進行最優參數點估計,這個思路能不能推廣到函數空間,進行最優函數估計呢?
看看一般函數估計問題。函數估計的目標是得到使得所有訓練樣本在(y,x)的聯合分布上,最小化期望損失函數。
對於給定的損失函數L(y,F),迭代求解基學習器時,損失函數不斷變小越小,Fm也就越靠近F,使得損失函數極速下降的最優方向,就是負梯度。
其實仔細想想,這個和常規的的利用梯度下降最小化損失函數過程一致。數值型解析解不能一步到位求出來,用梯度下降一步一步貪心近似;而我們的模型不能一步到位求出來,就用boosting的方式一步一步地近似出理想模型。參數梯度下降是根據負梯度調整原來的參數,在參數層面調整;而此處根據負梯度,獲得一個新的函數/基學習器(通過獲得新的參數實現,但是參數的意義通過函數體現),在函數層面調整。
7. 為什么前向分步時不直接擬合殘差?
GBDT並不是用負梯度代替殘差!!!GBDT建樹時擬合的是負梯度!
學習負梯度才能保證符合Boosting的基本要求
搞清楚兩個問題
- Boosting的基本要求是:隨着迭代次數的增多,Loss只能遞減(而不能又在某個點突然增大)
- Boosting的本質是通過累加各個學習器使得損失函數減小,而不是使訓練樣本擬合的更好
因此我們的目標是達成Boosting的基本要求而不是學什么殘差(Boosting算法概念里就沒有殘差這個東西),而是這個算法就是根據梯度下降的思想設計出來的。
在Freidman之前,發明了AdaBoost和GBDT,但是卻發現例如GBDT的Loss函數一旦不再是平方差時,如果還是學習殘差就不能滿足Boosting的基本要求。Freidman通過對loss泰勒一階展開發現了真正的奧秘是應該學習負梯度才能保證符合Boosting的基本要求。即利用損失函數的負梯度作為在當前模型的值作為殘差的近似值,這樣就能保證每次迭代過程損失函數不斷減小,所以第m顆基樹擬合負梯度就能解決問題。
GBDT本身就是使用負梯度進行boost,殘差反而是一種特例。更具體的說,損失函數為平方損失函數時梯度值恰好是殘差。不過這是一個特例,其它的損失函數就不會有這樣的性質。使用殘差這個說法解釋GBDT更容易理解,畢竟符合人的認知,也就是更具象化。
負梯度可以擴展到更復雜的損失函數
首先,殘差只針對於平方損失函數,脫離這個前提,殘差與負梯度本身就是完全的兩個概念。
其次,在有些損失函數中(比如Huber loss),殘差與梯度是不等的,殘差比梯度更完備,考慮了噪聲或者outliers的情況。
並且,對於更復雜的損失函數,“殘差”往往是比梯度更加難求的。
再加上對GDBT的正則化操作中有“學習步長”,即每一步擬合的不是負梯度,而是負梯度的α倍,這時候擬合目標和殘差就更不相同了。
提升樹用加法模型與前向分布算法實現學習的優化過程。當損失函數為平方損失和指數損失函數時,每一步優化是很簡單的。但對於一般損失函數而言,往往每一步都不那么容易。對於這問題,Freidman提出了梯度提升算法。這是利用最速下降法的近似方法,其關鍵是利用損失函數的負梯度在當前模型的值。
負梯度來替代殘差是對損失函數的普及化。真正的目的是使損失函數快速減小。由於模型本身是加性模型,將當前迭代(第m次)損失函數在前一代(m-1次)損失處進行泰勒一介展開。
8. 決策樹
決策樹是一種機器學習的方法,一種模型,它的主要思想是將輸入空間划分為不同的子區域,然后給每個子區域確定一個值,它是一種樹形結構,其中每個內部節點表示一個屬性上的判斷,每個分支代表一個判斷結果的輸出,最后每個葉節點代表一種分類結果。如果是分類樹,這個值就是類別,如果是回歸樹,這個樹就是一個實值。
9. 回歸樹擬合
GBDT中的樹擬合的是負梯度,都是CART回歸樹,不是分類樹,因為GBDT的核心在於累加所有樹的結果作為最終結果,而只有回歸樹的結果可以累加,分類樹的結果進行累加是沒有意義的。盡管GBDT調整后也可以用於分類,但這不代表GBDT中用到的決策樹是分類樹。
由於GBDT的學習過程是通過多輪迭代,每次都在上一輪訓練結果的殘差的基礎上進行學習,於是要求基學習器要足夠簡單,具有高偏差、低方差的特點。GBDT的基學習器是CART回歸樹,由於高偏差和簡單的要求,每棵CART回歸樹的深度不會很深。
提升樹的每次迭代,就是用一棵決策樹去擬合上一輪訓練的殘差,每一個棵回歸樹擬合的目標是損失函數的負梯度在當前模型的值。而之前所有樹的預測值的累加值,加上這個殘差就等於真實值。
比如A的真實年齡是18歲,第一棵樹預測的年齡是12歲,那么殘差是6歲,6歲作為第二棵樹學習的目標。如果第二棵樹的預測年齡是5歲,那么殘差等於真實年齡減去這兩棵樹的預測值之和(18-12-5),即為1。於是第三棵樹中A的年齡變成了1歲,繼續去學習,越來越逼近18歲這個目標。如果恰巧在第m棵樹時,殘差為0,那么累加這m棵樹預測的年齡,就和真實的年齡完全相等了。
訓練的過程就是通過降低偏差來不斷提高最終的提升樹進行分類和回歸的精度,使整體趨近於低偏差、低方差。最終的提升樹就是將每輪訓練得到的CART回歸樹加總求和得到(也就是加法模型)。
10. 單棵回歸樹
每次迭代都會建立一個回歸樹去擬合負梯度向量,與建樹相關的點有:
-
損失函數,比如均方差損失函數,決定樹的輸出。
-
切分准則(fitting criterion/splitting criterion),決定樹的結構。比如通常使用的是friedman_mse原則,公式為Greedy Function Approximation: A Gradient Boosting Machine論文中的(35)式
-
葉子節點的值,葉子節點的值為分到該葉子節點的所有樣本對應的輸出yi的平均值。
根據划分函數得到的輸出值主要作用是用來建樹,樹的結構確定后,再極小化損失函數,得到葉子節點的輸出值,這個輸出值才是回歸樹的輸出值。如果損失函數是平方誤差,樹節點的輸出值恰好也是要擬合的yi的均值。
對於回歸樹算法來說最重要的是尋找最佳的划分點,那么回歸樹中的可划分點包含了所有特征的所有可取的值。在分類樹中最佳划分點的判別標准是熵或者基尼系數,都是用純度來衡量的,但是在回歸樹中的樣本標簽是連續數值,所以再使用熵之類的指標不再合適,取而代之的是平方誤差,它能很好的評判擬合程度。
11. 加法模型
加法模型就是基學習器的一種線性組合,也是一種模型集H。它的一般形式如下: h(x) = β1 . f_1(x) + β2 . f_2(x) + β3 . f_3(x) +... + β_m . f_m(x) ,即
f_m(x)叫做基函數,基函數可以有各種各樣的形式,自然也會有自己的參數,我們討論GBDT時,它就是二叉回歸決策樹。β_m是基函數的系數,一般假設大於0。
有了模型,還需定義該模型的經驗損失函數:
現在,我們的問題轉變成了通過極小化經驗損失函數來確定各個系數β_m和各個基函數f_m(x)。
即如何求出f_m和β_m?
12. 前向分步算法
前向分布算法說:“我可以提供一套框架,不管基函數和損失函數是什么形式,只要你的模型是加法模型,就可以按照我的框架的指導,去求解。”
也就是說,前向分步算法提供了一種學習加法模型的普遍性方法,不同形式的基函數、不同形式的損失函數都可以用這種普遍性方法去求出加法模型的最優化參數,它是一種元算法。
它的思路是:加法模型中一共有M個基函數以及與之相應的M個系數,可以從前往后,每次學習一個基函數及其系數。
提升樹的前向分步算法。第m步的模型可以寫成:
fm(x)=fm−1(x)+T(x;β_m)
然后得到損失函數:
𝐿(𝑓𝑚(𝑥),𝑦)=𝐿(𝑓𝑚−1(𝑥)+𝑇(𝑥;β_m),𝑦)
前向分步算法求解這問題的思路:因為學習的是加法模型,如果能夠從前向后,每一步只學習一個基函數及其系數,逐步去逼近上述的目標函數式,就可簡化優化的復雜度,每一步只需優化損失函數。
可見,前向分步算法將同時求解從m=1到M所有參數的優化問題簡化成逐步求解各個參數的優化問題了。
迭代的目的是構建T(x;β_m),使得本輪損失L(fm(x),y)最小。
13. 梯度下降
思想其實並不復雜,但是問題也很明顯,對於不同的任務會有不同的損失函數,損失函數各種各樣,對各種損失函數的殘差進行擬合並不容易,怎么找到一種通用的擬合方法呢?針對這個問題,大牛Freidman提出了用損失函數的負梯度來擬合本輪損失的近似值,進而擬合一個CART回歸樹。
通過損失函數的負梯度來擬合,我們找到了一種通用的擬合損失誤差的辦法,這樣無論是分類問題還是回歸問題,我們通過其損失函數的負梯度的擬合,就可以用GBDT來解決我們的分類回歸問題。區別僅僅在於損失函數不同導致的負梯度不同而已。
於是在GBDT中,就使用損失函數的負梯度作為提升樹算法中殘差的近似值,然后每次迭代時,都去擬合損失函數在當前模型下的負梯度。這就找到了一種通用的擬合方法。
為什么通過擬合負梯度就能糾正上一輪的錯誤了?Gradient Boosting的發明者給出的答案是:函數空間的梯度下降。負梯度永遠是函數下降最快的方向,自然也是gbdt目標函數下降最快的方向,所以用負梯度去擬合首先是沒什么問題的。
14. GBDT的優勢
那么GBDT的優勢有兩個方面:
1 特征組合和發現重要特征
1)特征組合:
原始特征經過GBDT轉變成高維稀疏特征(GBDT的輸出相當於對原始特征進行了特征組合,得到高階特征或者說是非線性映射),然后將這些新特征作為FM(Factorization Machine)或LR(邏輯回歸)的輸入再次進行擬合。
2)發現重要特征:
由於決策樹的生長過程就是不斷地選擇特征、分割特征,因此由大量決策樹組成的GBDT具有先天的優勢,可以很容易得到特征的重要度排序,且解釋性很強。
2 泛化能力強
泛化誤差可以分解為兩部分,偏差(bias)和方差(variance)。
1)為了保證低偏差bias,采用了Boosting,每一步我們都會在上一輪的基礎上更加擬合原數據,可以保證低偏差;
2)為了保證低方差,采用了簡單的模型,如深度很淺的決策樹。
兩者結合,就能基於泛化性能相當弱的學習器構建出泛華能力很強的集成模型。
15. 算法步驟概要
GBDT每一次建立模型,是在之前建立模型損失函數的梯度下降方向。損失函數描述的是模型的不靠譜程度,損失函數越大,說明模型越容易出錯。如果我們的模型能夠讓損失函數持續的下降,說明我們的模型在不停的改進,而最好的方式就是讓損失函數在其梯度的方向下降。
一般的梯度下降是以一個樣本點(xi,yi)作為處理的粒度,w是參數,f(w;x)是目標函數,即減小損失函數L(yi,f(xi;w)),優化過程就是不斷處理參數w(這里用到梯度下降),使得損失函數L最小;GB是以一個函數作為處理粒度,對上一代的到的函數或者模型F(X)求梯度式,即求導,決定下降方向。針對每一個葉子節點里的樣本,我們計算更新葉子節點預測值,求出使損失函數最小,也就是擬合葉子節點最好的的輸出值。
構建分類 GBDT 的步驟是下面兩個:
- 初始化 GBDT
- 循環生成決策樹
我們把分類 GBDT 第二步也可以分成四個子步驟:(A)、(B)、(C)、(D),寫成偽代碼:
for m = 1 to M 循環生成決策樹,每新建一個樹 :
(A) 計算更新殘差 res_m = label - f_m
(B) 使用回歸樹來擬合殘差 res_m
(C) 計算更新葉子節點預測值 f_m = f_prev + lr * res_m
每做一次特征划分,計算SE = (殘差res_m-殘差均值)每棵樹對應葉子節點的殘差之和
(D) 更新模型 F_m
其中 m 表示第 m 棵樹,M 為樹的個數上限,我們先來看
(A):計算殘差
此處為使用 m-1 棵樹的模型,計算每個樣本的殘差 r_{im},這里的偏微分實際上就是求每個樣本的梯度,因為梯度我們已經計算過了,即 -y_i+p_i,那么 r_{im}=y_i-p_i,
(B):使用回歸樹來擬合 r_{im}
(C):對每個葉子節點 j,計算更新葉子節點預測值。意思是,在剛構建的樹 m 中,找到每個節點 j 的輸出,能使該節點的 Loss 最小。
(D):更新模型 F_m(x)
仔細觀察該式,實際上它就是梯度下降——「加上殘差」和「減去梯度」這兩個操作是等價的
最終,循環 M 次后,或總殘差低於預設的閾值時,我們的分類 GBDT 建模便完成了。
0x03 代碼示例
本代碼出自 https://github.com/Freemanzxp/GBDT_Simple_Tutorial。如果大家想依據代碼來學習,這個還是非常推薦的。
1. 訓練
class AbstractBaseGradientBoosting(metaclass=abc.ABCMeta):
def __init__(self):
pass
def fit(self, data):
pass
def predict(self, data):
pass
class BaseGradientBoosting(AbstractBaseGradientBoosting):
def __init__(self, loss, learning_rate, n_trees, max_depth,
min_samples_split=2, is_log=False, is_plot=False):
super().__init__()
self.loss = loss
self.learning_rate = learning_rate
self.n_trees = n_trees
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.features = None
self.trees = {}
self.f_0 = {}
self.is_log = is_log
self.is_plot = is_plot
def fit(self, data):
"""
:param data: pandas.DataFrame, the features data of train training
"""
# 掐頭去尾, 刪除id和label,得到特征名稱
self.features = list(data.columns)[1: -1]
# 初始化 f_0(x)
# 對於平方損失來說,初始化 f_0(x) 就是 y 的均值
self.f_0 = self.loss.initialize_f_0(data)
# 對 m = 1, 2, ..., M
logger.handlers[0].setLevel(logging.INFO if self.is_log else logging.CRITICAL)
for iter in range(1, self.n_trees+1):
# 計算負梯度--對於平方誤差來說就是殘差
self.loss.calculate_residual(data, iter)
target_name = 'res_' + str(iter)
self.trees[iter] = Tree(data, self.max_depth, self.min_samples_split,
self.features, self.loss, target_name, logger)
self.loss.update_f_m(data, self.trees, iter, self.learning_rate, logger)
if self.is_plot:
plot_tree(self.trees[iter], max_depth=self.max_depth, iter=iter)
# print(self.trees)
if self.is_plot:
plot_all_trees(self.n_trees)
2. 損失函數
class LossFunction(metaclass=abc.ABCMeta):
@abc.abstractmethod
def initialize_f_0(self, data):
"""初始化 F_0 """
@abc.abstractmethod
def calculate_residual(self, data, iter):
"""計算負梯度"""
@abc.abstractmethod
def update_f_m(self, data, trees, iter, learning_rate, logger):
"""計算 F_m """
@abc.abstractmethod
def update_leaf_values(self, targets, y):
"""更新葉子節點的預測值"""
@abc.abstractmethod
def get_train_loss(self, y, f, iter, logger):
"""計算訓練損失"""
class SquaresError(LossFunction):
def initialize_f_0(self, data):
data['f_0'] = data['label'].mean()
return data['label'].mean()
def calculate_residual(self, data, iter):
res_name = 'res_' + str(iter)
f_prev_name = 'f_' + str(iter - 1)
data[res_name] = data['label'] - data[f_prev_name]
def update_f_m(self, data, trees, iter, learning_rate, logger):
f_prev_name = 'f_' + str(iter - 1)
f_m_name = 'f_' + str(iter)
data[f_m_name] = data[f_prev_name]
for leaf_node in trees[iter].leaf_nodes:
data.loc[leaf_node.data_index, f_m_name] += learning_rate * leaf_node.predict_value
# 打印每棵樹的 train loss
self.get_train_loss(data['label'], data[f_m_name], iter, logger)
def update_leaf_values(self, targets, y):
return targets.mean()
def get_train_loss(self, y, f, iter, logger):
loss = ((y - f) ** 2).mean()
class BinomialDeviance(LossFunction):
def initialize_f_0(self, data):
pos = data['label'].sum()
neg = data.shape[0] - pos
# 此處log是以e為底,也就是ln
f_0 = math.log(pos / neg)
data['f_0'] = f_0
return f_0
def calculate_residual(self, data, iter):
# calculate negative gradient
res_name = 'res_' + str(iter)
f_prev_name = 'f_' + str(iter - 1)
data[res_name] = data['label'] - 1 / (1 + data[f_prev_name].apply(lambda x: math.exp(-x)))
def update_f_m(self, data, trees, iter, learning_rate, logger):
f_prev_name = 'f_' + str(iter - 1)
f_m_name = 'f_' + str(iter)
data[f_m_name] = data[f_prev_name]
for leaf_node in trees[iter].leaf_nodes:
data.loc[leaf_node.data_index, f_m_name] += learning_rate * leaf_node.predict_value
# 打印每棵樹的 train loss
self.get_train_loss(data['label'], data[f_m_name], iter, logger)
def update_leaf_values(self, targets, y):
numerator = targets.sum()
if numerator == 0:
return 0.0
denominator = ((y - targets) * (1 - y + targets)).sum()
if abs(denominator) < 1e-150:
return 0.0
else:
return numerator / denominator
def get_train_loss(self, y, f, iter, logger):
loss = -2.0 * ((y * f) - f.apply(lambda x: math.exp(1+x))).mean()
logger.info(('第%d棵樹: log-likelihood:%.4f' % (iter, loss)))
class MultinomialDeviance:
def init_classes(self, classes):
self.classes = classes
@abc.abstractmethod
def initialize_f_0(self, data, class_name):
label_name = 'label_' + class_name
f_name = 'f_' + class_name + '_0'
class_counts = data[label_name].sum()
f_0 = class_counts / len(data)
data[f_name] = f_0
return f_0
def calculate_residual(self, data, iter):
# calculate negative gradient
data['sum_exp'] = data.apply(lambda x:
sum([math.exp(x['f_' + i + '_' + str(iter - 1)]) for i in self.classes]),
axis=1)
for class_name in self.classes:
label_name = 'label_' + class_name
res_name = 'res_' + class_name + '_' + str(iter)
f_prev_name = 'f_' + class_name + '_' + str(iter - 1)
data[res_name] = data[label_name] - math.e ** data[f_prev_name] / data['sum_exp']
def update_f_m(self, data, trees, iter, class_name, learning_rate, logger):
f_prev_name = 'f_' + class_name + '_' + str(iter - 1)
f_m_name = 'f_' + class_name + '_' + str(iter)
data[f_m_name] = data[f_prev_name]
for leaf_node in trees[iter][class_name].leaf_nodes:
data.loc[leaf_node.data_index, f_m_name] += learning_rate * leaf_node.predict_value
# 打印每棵樹的 train loss
self.get_train_loss(data['label'], data[f_m_name], iter, logger)
def update_leaf_values(self, targets, y):
numerator = targets.sum()
if numerator == 0:
return 0.0
numerator *= (self.classes.size - 1) / self.classes.size
denominator = ((y - targets) * (1 - y + targets)).sum()
if abs(denominator) < 1e-150:
return 0.0
else:
return numerator / denominator
def get_train_loss(self, y, f, iter, logger):
loss = -2.0 * ((y * f) - f.apply(lambda x: math.exp(1+x))).mean()
0x04 參考
GBDT原理詳解
Boosting(提升方法)之GBDT
梯度提升樹(GBDT)原理小結
GBDT回歸篇
GBDT二分類
GBDT多分類
梯度提升樹(GBDT)原理小結 - 劉建平Pinard - 博客園
GBDT詳解 - 白開水加糖 - 博客園
Regularization on GBDT
梯度下降(Gradient Descent)小結
Boosting(提升方法)之GBDT
AdaBoost原理詳解
梯度提升樹(GBDT)原理小結
AdaBoost & GradientBoost(&GBDT)
從0到1認識GBDT
關於GBDT的幾個不理解的地方?
gbdt的殘差為什么用負梯度代替?
GBDT與梯度的理解
GBDT算法梳理
傳統推薦算法(六)Facebook的GBDT+LR模型(1)劍指GBDT
數據挖掘面試題之梯度提升樹
從0到1認識GBDT
gbdt心得
gbdt的殘差為什么用負梯度代替?
決策樹之 GBDT 算法
GBDT算法原理以及實例理解
GBDT模型