文章轉載自https://zhuanlan.zhihu.com/p/81016622
1. GBDT簡介
Boosting、Bagging和Stacking是集成學習(Ensemble Learning)的三種主要方法。Boosting是一族可將弱學習器提升為強學習器的算法,不同於Bagging、Stacking方法,Boosting訓練過程為串聯方式,弱學習器的訓練是有順序的,每個弱學習器都會在前一個學習器的基礎上進行學習,最終綜合所有學習器的預測值產生最終的預測結果。
梯度提升(Gradient boosting)算法是一種用於回歸、分類和排序任務的機器學習技術,屬於Boosting算法族的一部分。之前我們介紹過Gradient Boosting算法在迭代的每一步構建一個能夠沿着梯度最陡的方向降低損失的學習器來彌補已有模型的不足。經典的AdaBoost算法只能處理采用指數損失函數的二分類學習任務,而梯度提升方法通過設置不同的可微損失函數可以處理各類學習任務(多分類、回歸、Ranking等),應用范圍大大擴展。梯度提升算法利用損失函數的負梯度作為殘差擬合的方式,如果其中的基函數采用決策樹的話,就得到了梯度提升決策樹 (Gradient Boosting Decision Tree, GBDT)。
基於梯度提升算法的學習器叫做GBM(Gradient Boosting Machine)。理論上,GBM可以選擇各種不同的學習算法作為基學習器。現實中,用得最多的基學習器是決策樹。
決策樹有以下優點:
- 決策樹可以認為是if-then規則的集合,易於理解,可解釋性強,預測速度快。
- 決策樹算法相比於其他的算法需要更少的特征工程,比如可以不用做特征標准化。
- 決策樹可以很好的處理字段缺失的數據。
- 決策樹能夠自動組合多個特征,也有特征選擇的作用。
- 對異常點魯棒
- 可擴展性強,容易並行。
決策樹有以下缺點:
- 缺乏平滑性(回歸預測時輸出值只能輸出有限的若干種數值)。
- 不適合處理高維稀疏數據。
- 單獨使用決策樹算法時容易過擬合。
我們可以通過抑制決策樹的復雜性,降低單棵決策樹的擬合能力,再通過梯度提升的方法集成多個決策樹,最終能夠很好的解決過擬合的問題。由此可見,梯度提升方法和決策樹學習算法可以互相取長補短,是一對完美的搭檔。
2. GBDT回歸算法
2.1 GBDT回歸算法推導
當我們采用的基學習器是決策樹時,那么梯度提升算法就具體到了梯度提升決策樹。GBDT算法又叫MART(Multiple Additive Regression),是一種迭代的決策樹算法。GBDT算法可以看成是 棵樹組成的加法模型,其對應的公式如下:
其中, 為輸入樣本;
為模型參數;
為分類回歸樹;
為每棵樹的權重。GBDT算法的實現過程如下:
給定訓練數據集: 其中,
,
為輸入空間,
,
為輸出空間,損失函數為
,我們的目標是得到最終的回歸樹
。
1)初始化第一個弱學習器 :
2)對於建立M棵分類回歸樹 :
a)對 ,計算第
棵樹對應的響應值(損失函數的負梯度,即偽殘差):
b)對於 ,利用CART回歸樹擬合數據
,得到第
棵回歸樹,其對應的葉子節點區域為
,其中
,且
為第
棵回歸樹葉子節點的個數。
c)對於 個葉子節點區域
,計算出最佳擬合值:
d)更新強學習器 :
3)得到強學習器 的表達式:
2.2 GBDT回歸算法實例
(1)數據集介紹
訓練集如下表所示,一組數據的特征有年齡和體重,身高為標簽值,共有4組數據。
測試數據如下表所示,只有一組數據,年齡為25、體重為65,我們用在訓練集訓練好的GBDT模型預測該組數據的身高值為多少。
(2)模型訓練階段
參數設置:
- 學習率:learning_rate = 0.1
- 迭代次數:n_trees = 5
- 樹的深度:max_depth = 3
1)初始化弱學習器:
損失函數為平方損失,因為平方損失函數是一個凸函數,直接求導,導數等於零,得到 。
令導數等於0:
所以初始化時, 取值為所有訓練樣本標簽值的均值。
,此時得到的初始化學習器為
。
2)對於建立M棵分類回歸樹 :
由於我們設置了迭代次數:n_trees=5,且設置了M=5。
首先計算負梯度,根據上文損失函數為平方損失時,負梯度就是殘差,也就是 與上一輪得到的學習器
的差值:
現將殘差的計算結果列表如下:
此時將殘差作為樣本的真實值來訓練弱學習器 ,即下表數據:
接着,尋找回歸樹的最佳划分節點,遍歷每個特征的每個可能取值。從年齡特征值為5開始,到體重特征為70結束,分別計算分裂后兩組數據的平方損失(Square Error), 為左節點的平方損失,
為右節點的平方損失,找到使平方損失和
最小的那個划分節點,即為最佳划分節點。
例如:以年齡7為划分節點,將小於7的樣本划分為到左節點,大於等於7的樣本划分為右節點。左節點包括 ,右節點包括樣本
,
,
,
,所有可能的划分情況如下表所示:
以上划分點的總平方損失最小為0.025有兩個划分點:年齡21和體重60,所以隨機選一個作為划分點,這里我們選年齡21。現在我們的第一棵樹長這個樣子:
我們設置的參數中樹的深度max_depth=3,現在樹的深度只有2,需要再進行一次划分,這次划分要對左右兩個節點分別進行划分:
對於左節點,只含有0,1兩個樣本,根據下表結果我們選擇年齡7為划分點(也可以選體重30)。
對於右節點,只含有2,3兩個樣本,根據下表結果我們選擇年齡30為划分點(也可以選體重70)。
現在我們的第一棵回歸樹長下面這個樣子:
此時我們的樹深度滿足了設置,還需要做一件事情,給這每個葉子節點分別賦一個參數 ,來擬合殘差。
這里其實和上面初始化弱學習器是一樣的,對平方損失函數求導,令導數等於零,化簡之后得到每個葉子節點的參數 ,其實就是標簽值的均值。這個地方的標簽值不是原始的
,而是本輪要擬合的標殘差
。
根據上述划分結果,為了方便表示,規定從左到右為第1,2,3,4個葉子結點,其計算值過程如下:
此時的樹長這下面這個樣子:
此時可更新強學習器,需要用到參數學習率:learning_rate=0.1,用 表示。
為什么要用學習率呢?這是Shrinkage的思想,如果每次都全部加上擬合值 ,即學習率為1,很容易一步學到位導致GBDT過擬合。
重復此步驟,直到 結束,最后生成5棵樹。
下面將展示每棵樹最終的結構,這些圖都是我GitHub上的代碼生成的,感興趣的同學可以去運行一下代碼。https://github.com/Microstrong0305/WeChat-zhihu-csdnblog-code/tree/master/Ensemble%20Learning/GBDT_Regression
第一棵樹:
第二棵樹:
第三棵樹:
第四棵樹:
第五棵樹:
3)得到最后的強學習器:
(3)模型預測階段
- 在
中,測試樣本的年齡為25,大於划分節點21歲,又小於30歲,所以被預測為0.2250。
- 在
中,測試樣本的年齡為25,大於划分節點21歲,又小於30歲,所以被預測為0.2025。
- 在
中,測試樣本的年齡為25,大於划分節點21歲,又小於30歲,所以被預測為0.1823。
- 在
中,測試樣本的年齡為25,大於划分節點21歲,又小於30歲,所以被預測為0.1640。
- 在
中,測試樣本的年齡為25,大於划分節點21歲,又小於30歲,所以被預測為0.1476。
最終預測結果為:
3. 手撕GBDT回歸算法
本篇文章所有數據集和代碼均在我的GitHub中,地址:https://github.com/Microstrong0305/WeChat-zhihu-csdnblog-code/tree/master/Ensemble%20Learning
3.1 用Python3實現GBDT回歸算法
需要的Python庫:
pandas、PIL、pydotplus、matplotlib
其中pydotplus庫會自動調用Graphviz,所以需要去Graphviz官網下載graphviz-2.38.msi安裝,再將安裝目錄下的bin添加到系統環境變量,最后重啟計算機。
由於用Python3實現GBDT回歸算法代碼量比較多,我這里就不列出詳細代碼了,感興趣的同學可以去我的GitHub中看一下,地址:https://github.com/Microstrong0305/WeChat-zhihu-csdnblog-code/tree/master/Ensemble%20Learning/GBDT_Regression
3.2 用sklearn實現GBDT回歸算法
import numpy as np from sklearn.ensemble import GradientBoostingRegressor gbdt = GradientBoostingRegressor(loss='ls', learning_rate=0.1, n_estimators=5, subsample=1 , min_samples_split=2, min_samples_leaf=1, max_depth=3 , init=None, random_state=None, max_features=None , alpha=0.9, verbose=0, max_leaf_nodes=None , warm_start=False ) train_feat = np.array([[1, 5, 20], [2, 7, 30], [3, 21, 70], [4, 30, 60], ]) train_id = np.array([[1.1], [1.3], [1.7], [1.8]]).ravel() test_feat = np.array([[5, 25, 65]]) test_id = np.array([[1.6]]) print(train_feat.shape, train_id.shape, test_feat.shape, test_id.shape) gbdt.fit(train_feat, train_id) pred = gbdt.predict(test_feat) total_err = 0 for i in range(pred.shape[0]): print(pred[i], test_id[i]) err = (pred[i] - test_id[i]) / test_id[i] total_err += err * err print(total_err / pred.shape[0])
用sklearn中的GBDT庫實現GBDT回歸算法的難點在於如何更好調制下列參數:
用sklearn實現GBDT回歸算法的GitHub地址:https://github.com/Microstrong0305/WeChat-zhihu-csdnblog-code/tree/master/Ensemble%20Learning/GBDT_Regression_sklearn
4. GBDT回歸任務常見的損失函數
對於GBDT回歸模型,sklearn中實現了四種損失函數,有均方差'ls', 絕對損失'lad', Huber損失'huber'和分位數損失'quantile'。默認是均方差'ls'。一般來說,如果數據的噪音點不多,用默認的均方差'ls'比較好。如果是噪音點較多,則推薦用抗噪音的損失函數'huber'。而如果我們需要對訓練集進行分段預測的時候,則采用'quantile'。下面我們具體來了解一下這四種損失函數。
(1)均方差,這個是最常見的回歸損失函數了,公式如下:
對應的負梯度誤差為:
(2)絕對損失,這個損失函數也很常見,公式如下:
對應的負梯度誤差為:
(3)Huber損失,它是均方差和絕對損失的折衷產物,對於遠離中心的異常點,采用絕對損失,而中心附近的點采用均方差。這個界限一般用分位數點度量。損失函數如下:
對應的負梯度誤差為:
(4)分位數損失,它對應的是分位數回歸的損失函數,表達式為:
其中, 為分位數,需要我們在回歸前指定。對應的負梯度誤差為:
對於Huber損失和分位數損失,主要用於健壯回歸,也就是減少異常點對損失函數的影響。
5. GBDT的正則化
為了防止過擬合,GBDT主要有五種正則化的方式。
(1)“Shrinkage”:這是一種正則化(regularization)方法,為了防止過擬合,在每次對殘差估計進行迭代時,不直接加上當前步所擬合的殘差,而是乘以一個系數 。系數
也被稱為學習率(learning rate),因為它可以對梯度提升的步長進行調整,也就是它可以影響我們設置的回歸樹個數。對於前面的弱學習器的迭代:
如果我們加上了正則化項,則有:
的取值范圍為
。對於同樣的訓練集學習效果,較小的
意味着我們需要更多的弱學習器的迭代次數。通常我們用學習率和迭代最大次數一起來決定算法的擬合效果。即參數learning_rate會強烈影響到參數n_estimators(即弱學習器個數)。learning_rate的值越小,就需要越多的弱學習器數來維持一個恆定的訓練誤差(training error)常量。經驗上,推薦小一點的learning_rate會對測試誤差(test error)更好。在實際調參中推薦將learning_rate設置為一個小的常數(e.g. learning_rate <= 0.1),並通過early stopping機制來選n_estimators。
(2)“Subsample”:第二種正則化的方式是通過子采樣比例(subsample),取值為 (0,1]。注意這里的子采樣和隨機森林不一樣,隨機森林使用的是放回抽樣,而這里是不放回抽樣。如果取值為1,則全部樣本都使用,等於沒有使用子采樣。如果取值小於1,則只有一部分樣本會去做GBDT的決策樹擬合。選擇小於1的比例可以減少方差,即防止過擬合,但會增加樣本擬合的偏差,因此取值不能太低。推薦在 [0.5, 0.8]之間。
使用了子采樣的GBDT有時也稱作隨機梯度提升樹 (Stochastic Gradient Boosting Tree, SGBT)。由於使用了子采樣,程序可以通過采樣分發到不同的任務去做Boosting的迭代過程,最后形成新樹,從而減少弱學習器難以並行學習的弱點。
(3)對於弱學習器即CART回歸樹進行正則化剪枝。這一部分在學習決策樹原理時應該掌握的,這里就不重復了。
(4)“Early Stopping”:Early Stopping是機器學習迭代式訓練模型中很常見的防止過擬合技巧,具體的做法是選擇一部分樣本作為驗證集,在迭代擬合訓練集的過程中,如果模型在驗證集里錯誤率不再下降,就停止訓練,也就是說控制迭代的輪數(樹的個數)。在sklearn的GBDT中可以設置參數n_iter_no_change實現early stopping。
(5)“Dropout”:Dropout是deep learning里很常用的正則化技巧,很自然的我們會想能不能把Dropout用到GBDT模型上呢?AISTATS2015有篇文章《DART: Dropouts meet Multiple Additive Regression Trees》進行了一些嘗試。文中提到GBDT里會出現over-specialization的問題:前面迭代的樹對預測值的貢獻比較大,后面的樹會集中預測一小部分樣本的偏差。Shrinkage可以減輕over-specialization的問題,但不是很好。作者想通過Dropout來平衡所有樹對預測的貢獻。
具體的做法是:每次新加一棵樹,這棵樹要擬合的並不是之前全部樹ensemble后的殘差,而是隨機抽取的一些樹ensemble;同時新加的樹結果要規范化一下。對這一部分感興趣的同學可以閱讀一下原論文。
6. 關於GBDT若干問題的思考
(1)GBDT與AdaBoost的區別與聯系?
AdaBoost和GBDT都是重復選擇一個表現一般的模型並且每次基於先前模型的表現進行調整。不同的是,AdaBoost是通過調整錯分數據點的權重來改進模型,GBDT是通過計算負梯度來改進模型。因此,相比AdaBoost, GBDT可以使用更多種類的目標函數,而當目標函數是均方誤差時,計算損失函數的負梯度值在當前模型的值即為殘差。
(2)GBDT與隨機森林(Random Forest,RF)的區別與聯系?
相同點:都是由多棵樹組成,最終的結果都是由多棵樹一起決定。
不同點:1)集成的方式:隨機森林屬於Bagging思想,而GBDT是Boosting思想。2)偏差-方差權衡:RF不斷的降低模型的方差,而GBDT不斷的降低模型的偏差。3)訓練樣本方式:RF每次迭代的樣本是從全部訓練集中有放回抽樣形成的,而GBDT每次使用全部樣本。4)並行性:RF的樹可以並行生成,而GBDT只能順序生成(需要等上一棵樹完全生成)。5)最終結果:RF最終是多棵樹進行多數表決(回歸問題是取平均),而GBDT是加權融合。6)數據敏感性:RF對異常值不敏感,而GBDT對異常值比較敏感。7)泛化能力:RF不易過擬合,而GBDT容易過擬合。
(3)我們知道殘差=真實值-預測值,明明可以很方便的計算出來,為什么GBDT的殘差要用用負梯度來代替?為什么要引入麻煩的梯度?有什么用呢?
回答第一小問:在GBDT中,無論損失函數是什么形式,每個決策樹擬合的都是負梯度。准確的說,不是用負梯度代替殘差,而是當損失函數是均方損失時,負梯度剛好是殘差,殘差只是特例。
回答二三小問:GBDT的求解過程就是梯度下降在函數空間中的優化過程。在函數空間中優化,每次得到增量函數,這個函數就是GBDT中一個個決策樹,負梯度會擬合這個函數。要得到最
終的GBDT模型,只需要把初始值或者初始的函數加上每次的增量即可。我這里高度概括的回答了這個問題,詳細推理過程可以參考:梯度提升(Gradient Boosting)算法,地址:https://mp.weixin.qq.com/s/Ods1PHhYyjkRA8bS16OfCg
(4)為什么說GBDT在損失函數上做了一階泰勒級數展開?在和XGBoost比較時,一個不同點是GBDT算法中對損失函數做了一階泰勒級數展開,也就是計算梯度,而XGBoost對損失函數做了二階泰勒級數展開,GBDT算法中對損失函數做一階泰勒級數展開體現在每棵樹創建之初樣本的y值計算上。在第t-1棵樹創建完成后,整個模型的損失為
,那么接下來再構建第t棵樹時,應該盡量讓模型損失減少,如何做到盡量?答案是當損失函數按照梯度下降方向減少時能最大程度減小損失,那么可以求損失函數的梯度,也就是一階導數,也即一階泰勒級數展開,然后設定一個步長
,也就是第5節的第(1)部分提到的內容,負梯度乘以步長
得到值a,當前t-1棵樹造成的模型損失加上a值后,可以使模型的損失最快的減小,對於每個樣本來說,當其y值的計算損失最小時,那么整個模型的損失也就最小,因此第t課樹的樣本的y值擬合值就應該是第t-1棵樹上樣本輸出的y值減去a值,然后構建第 t 棵樹,使得該樹各個葉子節點的輸出值接近建樹時樣本的y值,這就是GBDT中建每棵樹原則,讓輸出葉子節點的值盡量接近樣本的y值
(5)每個葉子節點上最終輸出值c如何確定?在GBDT算法中,每棵樹的葉子節點的輸出值是互不關聯的,可以單獨計算,當每個葉子節點的樣本的損失降到最小時,那么整棵樹的損失就最小。葉子節點的損失需要借助損失函數來計算,在GBDT算法中,當采用均方差損失時,葉子節點上的損失為
c值是變量,這是一個一元二次表達式,求極小值的方式是使一階導數為0,通過這種方式可以計算出c值,這和XGBoost算法中計算葉子節點的輸出值的方式一致。
計算葉子節點輸出值和樹的分裂過程是獨立的。
7. 總結
在本文中,我們首先引出回歸樹與梯度提升算法結合的優勢;然后詳細推導了GBDT回歸算法的原理,並用實際案例解釋GBDT回歸算法;其次不僅用Python3實現GBDT回歸算法,還用sklearn實現GBDT回歸算法;最后,介紹了GBDT回歸任務常見的損失函數、GBDT的正則化 和我對GBDT回歸算法的若干問題的思考。GBDT中的樹是回歸樹(不是分類樹),GBDT可以用來做回歸預測,這也是我們本文講的GBDT回歸算法,但是GBDT調整后也可以用於分類任務。讓我們期待一下GBDT分類算法,在分類任務中的表現吧!