GBDT的原理和應用

週二、週三參加了QCon上海2017|全球軟件開發大會,聽了幾場機器學習相關的 Session,多次提及 GBDT(Gradient Boost Decision Tree),並且在模型演化歷史中,都有很重要或者最重要的地位。

如《Pinterest如何利用機器學習實現兩億月活躍用戶》提到的模型發展歷史,GBDT帶來過巨大的效果提升。

迭代決策樹(GBDT)

《唯品金融機器學習實踐》中也提到因爲GBDT+LR良好的表達能力和可解釋性成爲他們的最重要模型之一。筆者的工作實踐中,餓了麼的Rank系統中,GBDT+FTRL,也是AUC最高的模型(不是之一,超過深度學習嘗試)。

因爲最近負責使用 GBDT模型做競價排名廣告的點擊率預估(CTR),所以就從頭學習了一下原理並涉獵了一些相關的應用場景,寫一篇文章總結一下,希望可以表述清楚,讀者會有所收穫。

概述

DT-Decision Tree決策樹,GB是Gradient Boosting,是一種學習策略,GBDT的含義就是用Gradient Boosting的策略訓練出來的DT模型。模型的結果是一組迴歸分類樹組合(CART Tree Ensemble): T_1...T_K 。其中 T_j 學習的是之前 j-1 棵樹預測結果的殘差,這種思想就像準備考試前的複習,先做一遍習題冊,然後把做錯的題目挑出來,在做一次,然後把做錯的題目挑出來在做一次,經過反覆多輪訓練,取得最好的成績。

而模型最後的輸出,是一個樣本在各個樹中輸出的結果的和:

\bar{y} = \sum_{k=1}^{K}{f_k(x)}, f_k \subset\Gamma,f_k表示樣本到樹輸出的映射

假設我們要預測一個人是否會喜歡電腦遊戲,特徵包括年齡,性別是否爲男,是否每天使用電腦。標記(label)爲是否喜歡電腦遊戲,假設訓練出如下模型

該模型又兩棵樹組成, T_1 使用 age < 15 和 is male 作爲內節點,葉子節點是輸出的分數。 T_2 使用是否每日使用電腦作爲根節點。假設測試樣本如下:

樣本在兩棵樹中所在的葉節點如下:

最後對某樣本累加它所在的葉子節點的輸出值,例如:

GBDT + LR

單獨的使用GBDT模型,容易出現過擬合,在實際應用中往往使用 GBDT+LR的方式做模型訓練,算法更多細節可以參考 [Practical Lessons from Predicting Clicks on Ads at Facebook]。本文只介紹結論性的做法。

首先根據樣本訓練出GBDT樹,對於每個葉子節點,回溯到根節點都可以得到一組組合特徵,所以用葉子節點的標號可以代表一個新的組合特徵。結合上面的圖,用一個樣本爲例,直觀的表達如下:

其中 0號 組合特徵的含義是:ageLessThan15AndIsMale,該樣本取值 0
其中 1號 組合特徵的含義是:ageLessThan15AndIsNotMale,該樣本取值 1
其中 2號 組合特徵的含義是:ageLargerOrEqualThan15,該樣本取值 0
其中 3號 組合特徵的含義是:useComputerDaily,該樣本取值 0
其中 3號 組合特徵的含義是:notUseComputerDaily,該樣本取值 1

這部分特徵是GBDT生成的組合特徵,再結合LR固有的稀疏特徵,就組成了 GBDT + LR 模型。生成樣本向量階段,樣本首先過GBDT模型,生成組合特徵部分的輸入向量,再結合固有的稀疏特徵向量,組成新的特徵向量,示例如下:

在該例子中,第一行綠顏色是通過 GBDT 模型生成的特徵向量,每個值都代表一個葉子節點的輸出(樣本在某棵樹只在一個葉子節點有輸出),第二行表示 LR 模型的稀疏特徵向量,第三行表示把兩部分特徵向量拼接在一起,組成一個最終的特徵向量,並使用該向量訓練LR模型。

實踐

XGBoost是GBDT最廣爲人知的一個實現。通過使用一定程度的近似,使得求解變得更高效。同時支持分佈式和 GPU 優化,有着廣泛的使用。在實踐中,算法工程師使用 Spark 或者Python 的 XGBoost 庫訓練模型,並保存成文件,線上根據不同的語言採用相應的依賴包,將模型導入,執行決策。Java 中使用 xgboost4j 導入模型,完成特徵變換後,調用 predict 方法,就可以得到當前樣本的預測值。

需要注意的是,xgboost4j 需要鏈接到本地庫,需要自己編譯並打包。首先在本地編譯 xgboost4j,生成平臺相關的本地庫文件,例如 linux 下生的 libxgboost4j.so。然後把這個文件連同xgboost4j 的源代碼一起,發佈成一個新的工程,供線上依賴。

hopeztm7500/xgboost4jL 這個項目是我在實踐中自己編譯 xgboost4j 生成了一個平臺相關xgboost4j 包。不過這個項目僅供參考,因爲平臺相關,所以不一定能被其他人使用。

XGBoost算法原理

這部分內容比較便理論,對於沒有興趣的讀者可以忽略掉。

還記得前文提到的 GBDT 可以用如下公式表示:

\bar{y} = \sum_{k=1}^{K}{f_k(x)}, f_k \subset\Gamma

優化目標如下:

Obj = \sum_{i = 1}^{n}{l (y_i, \bar{y_i})} + \sum_{k = 1}^{K}{\Omega(f_k)}

n 樣本數量, y_i表示樣本真實 Label, \bar{y} 是模型輸出,所以前半部分代表模型的損失函數。
K 表示樹的個數, f_k 表示第 k 棵樹,形式化的來說它是一個樣本到葉子節點值的映射( x\rightarrow R ), \Omega 是模型複雜度函數。模型越複雜,越容易出現過擬合,所以採用這樣的目標函數,爲了使得最終的模型既有很好的準確度也有不錯的泛化能力。

那麼如何找到一組樹,使得 Obj 最小呢。

追加法訓練(Additive Training、Boosting)

核心的思想是,已經訓練好的樹 T_1 \sim T_{t-1} 不再調整。根據目標函數最小原則,新增樹 T_t,表示如下:

\bar{y}_{}^{0} = 0 算法初始化

\bar{y}_{}^{(1)} = f_1(x) = \bar{y}_{}^{(0)} + f_1(x) 訓練第 1 棵樹 f_1(x)

\bar{y}_{}^{(2)} = f_1(x) + f_2(x) = \bar{y}_{}^{(1)} + f_2(x) 訓練第二棵樹 f_2(x) ,第 1 棵不再調整

\bar{y}_{}^{(t)} = \sum_{k=1}^{t}{f_k(x)} = \bar{y}_{}^{(t-1)} + f_t(x) 訓練第 t 棵樹 f_t(x) ,前面 t-1 棵不再調整

假設此時對第 t 棵樹訓練,則目標函數表示爲

Obj^{(t)} = \sum_{i = 1}^{n}{l (y_i, \bar{y_i}^{(t)})} + \sum_{i = 1}^{t}{\Omega(f_i)}

=\sum_{i = 1}^{n}{l (y_i, \bar{y_i}^{(t-1)} + f_t(x_i))} + \Omega(f_t) + constant

constant 代表 \sum_{i=1}^{t-1}{\Omega(f_i)} ,因爲前面 t-1 棵樹結構不再變化,所以他們的複雜度爲常數。
回顧高等數學中的泰勒展開,它使用一個函數的高階導數,用多項式的形式逼近原始函數。當展開到二階導數的時候公式如下:

f(x+\triangle x) \simeq f(x) + f'(x)\triangle x + ({\frac{1}{2}})f''(x)\triangle x^2

利用泰勒展開公式和上文推導的 Obj^{(t)} , Obj^{(t)} 式中,\bar{y_i}^{(t-1)} 對應泰勒公式中的 x ,而 f_t(x) 是一棵待增加的新樹,對應泰勒公式中的\triangle x , l (y_i, \bar{y_i}^{(t-1)} ) 對應泰勒公式中的 f(x) , l (y_i, \bar{y_i}^{(t-1)} + f_t(x)) 對應泰勒公式中的 f(x+\triangle x)。則對 l (y_i, \bar{y_i}^{(t-1)} + f_t(x)) 做二階泰勒展開後,得:
Obj^{(t)} =\sum_{i = 1}^{n}{[ l (y_i, \bar{y_i}^{(t-1)} ) + g_if_t(x_i) + (\frac12)h_if_t^2(x_i)]} + \Omega(f_t) + constant 
其中
g_i = \partial_{\bar{y_i}^{(t-1)}} l(y_i, \bar{y_i}^{(t-1)})

h_i = \partial_{\bar{y_i}^{(t-1)}}^{(2)} l(y_i, \bar{y_i}^{(t-1)})

即對應泰勒公式中的一階導數 f'(x) 和二階導數 f''(x) 。因爲我們求解的目標是使得 Obj^{(t)} 最小的 f_t(x) 。當前面 t-1 棵樹都已經確定時, \sum_{i = 1}^{n}{[ l (y_i, \bar{y_i}^{(t-1)} )}] 是一個常量,可以省略, constant 常量也可以省略,簡化得到新的目標函數:

\bar{Obj}^{(t)} =\sum_{i = 1}^{n}{[g_if_t(x_i) + ({\frac12})h_if_t^2(x_i)]} + \Omega(f_t)

複雜度函數 \Omega(f_t) 的引入

假設待訓練的第 t 棵樹有 T 個葉子節點,葉子節點的輸出用向量表述爲[w_1,w_2,w_3...,w_T]。那麼 f_t(x) 可以表示爲如下形式:

f_t(x) = w_{q(x)}, w\in R^T, q: R^d\rightarrow{\{1,2,3...T\}}

可以理解爲 q 是樹結構,把樣本 x 帶到某個葉子節點 j ,而這個節點的輸出值是 w_j 。例如:

前文提到爲了提高泛化能力,引入複雜度函數,定義複雜度函數的方式也可以有很多選擇,XGBoost中定義如下:

\Omega(f_t) = \gamma T+({\frac{1}{2}})\lambda\sum_{j=1}^{T}{w_j^2}

如前述 T 表示葉節點個數, w_j 是葉子節點 j 的輸出。例如:


則目標函數可以推導爲如下:

\bar{Obj}^{(t)} =\sum_{i = 1}^{n}{[g_if_t(x_i) + ({{\frac{1}{2}}})h_if_t^2(x_i)]} + \Omega(f_t) =\sum_{i = 1}^{n}{[g_iw_{q(x_i)} + (1/2)h_iw^2_{q(x_i)}]} + \gamma T + \lambda(1/2)\sum_{j=1}^Tw_j

前半部分, \sum_{i = 1}^{n}{[g_iw_{q(x_i)} + (1/2)h_iw^2_{q(x_i)}]} , \sum_{i=1}^{n} 選取是最直觀的樣本累加視角,我們可以把這個視角換一下。既然每個樣本都會落到一個葉子節點,最外層的累加唯獨可以爲葉節點。定義 I_j = \{i|q(x_i) = j\} ,含義是被映射到第 j 個葉子節點的樣本,該葉子節點的輸出爲 w_j ,那麼

\sum_{i = 1}^{n}{[g_iw_{q(x_i)} + (1/2)h_iw^2_{q(x_i)}]} =\sum_{j=1}^T[\sum_{i\in I_j}g_iw_j + (1/2)\sum_{i\in I_j}h_iw_j^2 ]

可以推導出

\bar{Obj^{(t)}} =\sum_{j=1}^T[\sum_{i\in I_j}g_iw_j + (1/2)\sum_{i\in I_j}h_iw_j^2 ] + \gamma T + \lambda(1/2)\sum_{j=1}^Tw_j =\sum_{j=1}^T[(\sum_{i\in I_j}g_i)w_j + (1/2)(\sum_{i\in I_j}h_i +\lambda)w_j^2 ] + \gamma T

定義 G_j = \sum_{i\in I_j}g_i , H_j = \sum_{i\in I_j}h_i ,則上式

\bar{Obj^{(t)}} = \sum_{j=1}^{T}{[G_j w_j + (1/2)(H_j+\lambda)w_j^2]} +\gamma T

假設新樹的結構不變,上式是一個由 T 個相互獨立的二次函數的累加,所以目標函數最小,等價於求對每個 j 最小。

回顧對於 y = ax^2 + bx +c 當 a > 0 ,當 x = -(b/2a) 對稱軸位置的時候,函數取得最小值,此時最小值爲 (4ac - b^2) /4a

對 G_j w_j + (1/2)(H_j+\lambda)w_j^2 分別取極小值時, w_j 取值如下

w_j^* = - (G_i/(H_j + \lambda))

\bar{Obj^{(t)}}_{min} = -(1/2)\sum_{j=1}^{T}{G_j^2}/{{(H_j+\lambda)}} +\gamma T

舉個例子,假設要求解第一棵 T_1 樹的結構如下,各個樣本按照該樹的結構,會被映射到相應的位置,得到每個樣本 x_i 對應的 g_i, h_i ,就可以求得到對應 \bar{Obj^{(1)}}_{min}

g_i , h_i 前文定義爲損失函數 l (y_i, \bar{y_i}^{(t-1)} ) 對 \bar{y_i}^{(t-1)} 的一階導數和二階導數。如果把損失函數定義爲平方損失(Square Loss),則可以得到如下等式:

g_i = \partial_{\bar{y_i}^{(t-1)}} (y_i - \bar{y_i}^{(t-1)})^2 = 2(\bar{y_i}^{(t-1)}-y_i)

h_i = \partial^{(2)}_{\bar{y_i}^{(t-1)}} (y_i - \bar{y_i}^{(t-1)})^2 = 2

所以根對於待訓練的新樹而言, g_i, h_i 可以根據訓練好的樹預先計算好,枚舉所有可能的新樹的結構,選擇目標函數最小的那棵樹,同時使用 w_j^* = - (G_i/(H_j + \lambda)) 求得該樹每個葉子節點的輸出值,不斷求解下去,就可以訓練出一組最優樹。但是,枚舉所有的樹是不現實的,因爲樣本特徵值可能是連續的,樹的結構可能是無窮的。

貪心法求解樹

在XGBoost使用貪心法求解樹結構,算法描述如下:

  • 初始化樹深度 0(即只有一個葉子節點,所有樣本都落在該節點)
  • 對於每個葉子節點,嘗試分裂該節點,在分裂後得到的增益定義如下:
    Gain = \frac12[{\frac{G_L^2}{H_L+\lambda}}+{\frac{G_R^2}{H_R+\lambda}} - {\frac{{(G_L+G_R)}^2}{H_L+H_R + \lambda}}] - \lambda
    該公式定義了,分裂後左右子樹的新增得分減去不分裂時候節點得分,再減去因爲新增一個節點,增加的複雜度。

那麼,如何定義最佳分裂。

  • 對於每個葉子節點,枚舉所有的特徵
  • 對每個特徵,把映射到該葉節點的樣本按照該特徵值排序
  • 使用線性掃描來決定最佳分裂點 O(ndK\log{n})
  • 在所有枚舉的特徵中,選擇最優的分裂

示例如下,假如當前枚舉的特徵是年齡,樣本排序和分裂點如下所示:

在該分裂點計算分裂後的增益:

Gain = \frac12[{\frac{G_L^2}{H_L+\lambda}}+{\frac{G_R^2}{H_R+\lambda}} - {\frac{{(G_L+G_R)}^2}{H_L+H_R + \lambda}}] - \lambda

如果用 K 表示新增樹的深度,那麼該算法複雜度是 O(ndK\log{n}) , d 代表特徵數, n\log{n} 是對樣本排序複雜度。如果緩存對樣本在各個特徵下對排序結果,算法還可以進一步在時間上優化。

總結算法流程如下:

  • 迭代生成樹 T_i
  • 迭代初始,對每個樣本計算 g_i, h_i ,該計算只依賴於訓練好的樹 T_1..T_{i-1}
  • 使用 \bar{Obj^{(t)}}_{min} = -(1/2)\sum_{j=1}^{T}{{{G_j^2}}/{{(H_j+\lambda)}}} +\gamma T 爲目標函數,採用貪心法求得樹 T_i
  • 新增樹後,模型表示爲 \bar{y}_{}^{(t)} = \bar{y}_{}^{(t-1)} + f_t(x) ,實踐中使用
    \bar{y}_{}^{(t)} = \bar{y}_{}^{(t-1)} + \epsilon f_t(x) 替代上面的迭代公式, \epsilon 被稱爲步長(steps-size)或收縮率(shrinkage),通常設置爲 0.1 附近的值。設置它的目的在於,不期望模型每一次迭代學習到所有殘差,防止過擬合。

從XGBoost原理部分可以看出,XGBoost的實現中,採用了二階泰勒展開公式展開來近似損失函數,同時在求解樹的過程中,使用了貪心算法,並使用枚舉特徵,樣本按照特徵排序後,採用線性掃描找到最優分裂點。這種實現方法,是對GDBT思想的逼近,但是在工程上確非常有效。

總結

本文首先從應用出發,概括了GBDT的應用場景,介紹瞭如何使用GBDT和GBDT+LR。然後對訓練和上線的工程化給予簡單闡述。然後用最長的篇幅,介紹了 XGBoost原理。希望讀者有所收穫。

傳送門:https://zhuanlan.zhihu.com/p/30339807

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章