1. 程式人生 > >機器學習演算法GBDT的面試要點總結-上篇

機器學習演算法GBDT的面試要點總結-上篇

1.簡介

    gbdt全稱梯度下降樹,在傳統機器學習演算法裡面是對真實分佈擬合的最好的幾種演算法之一,在前幾年深度學習還沒有大行其道之前,gbdt在各種競賽是大放異彩。原因大概有幾個,一是效果確實挺不錯。二是即可以用於分類也可以用於迴歸。三是可以篩選特徵。這三點實在是太吸引人了,導致在面試的時候大家也非常喜歡問這個演算法。 gbdt的面試考核點,大致有下面幾個:

  • gbdt 的演算法的流程?
  • gbdt 如何選擇特徵 ?
  • gbdt 如何構建特徵 ?
  • gbdt 如何用於分類?
  • gbdt 通過什麼方式減少誤差 ?
  • gbdt的效果相比於傳統的LR,SVM效果為什麼好一些 ?
  • gbdt 如何加速訓練?
  • gbdt的引數有哪些,如何調參 ?
  • gbdt 實戰當中遇到的一些問題 ?
  • gbdt的優缺點 ?

2. 正式介紹

    首先gbdt 是通過採用加法模型(即基函式的線性組合),以及不斷減小訓練過程產生的殘差來達到將資料分類或者回歸的演算法。

  •  gbdt的訓練過程

        我們通過一張圖片,圖片來源來說明gbdt的訓練過程: 

    

                      圖 1:GBDT 的訓練過程

        gbdt通過多輪迭代,每輪迭代產生一個弱分類器,每個分類器在上一輪分類器的殘差基礎上進行訓練。對弱分類器的要求一般是足夠簡單,並且是低方差和高偏差的。因為訓練的過程是通過降低偏差來不斷提高最終分類器的精度,(此處是可以證明的)。

        弱分類器一般會選擇為CART TREE(也就是分類迴歸樹)。由於上述高偏差和簡單的要求 每個分類迴歸樹的深度不會很深。最終的總分類器 是將每輪訓練得到的弱分類器加權求和得到的(也就是加法模型)。

        模型最終可以描述為:$$F_{m}(x) = \sum_{m=1}^{M}T\left ( x;\theta _m \right )$$

        模型一共訓練M輪,每輪產生一個弱分類器 $T\left ( x;\theta _m \right )$。弱分類器的損失函式$$\hat\theta_{m} = \mathop{\arg\min}_{\theta_{m}} \sum_{i=1}^{N}L\left ( y_{i},F_{m-1}(x_{i})+T(x_{i};\theta_{m} ) \right )$$

        $F_{m-1}(x)$ 為當前的模型,gbdt 通過經驗風險極小化來確定下一個弱分類器的引數。具體到損失函式本身的選擇也就是L的選擇,有平方損失函式,0-1損失函式,對數損失函式等等。如果我們選擇平方損失函式,那麼這個差值其實就是我們平常所說的殘差

    • 但是其實我們真正關注的,1.是希望損失函式能夠不斷的減小,2.是希望損失函式能夠儘可能快的減小。所以如何儘可能快的減小呢?
    • 讓損失函式沿著梯度方向的下降。這個就是gbdt 的 gb的核心了。 利用損失函式的負梯度在當前模型的值作為迴歸問題提升樹演算法中的殘差的近似值去擬合一個迴歸樹。gbdt 每輪迭代的時候,都去擬合損失函式在當前模型下的負梯度。
    • 這樣每輪訓練的時候都能夠讓損失函式儘可能快的減小,儘快的收斂達到區域性最優解或者全域性最優解。
  • gbdt如何選擇特徵?

        gbdt選擇特徵的細節其實是想問你CART Tree生成的過程。這裡有一個前提,gbdt的弱分類器預設選擇的是CART TREE。其實也可以選擇其他弱分類器的,選擇的前提是低方差和高偏差。框架服從boosting 框架即可。

        下面我們具體來說CART TREE(是一種二叉樹) 如何生成。CART TREE 生成的過程其實就是一個選擇特徵的過程。假設我們目前總共有 M 個特徵。第一步我們需要從中選擇出一個特徵 j,做為二叉樹的第一個節點。然後對特徵 j 的值選擇一個切分點 m. 一個 樣本的特徵j的值 如果小於m,則分為一類,如果大於m,則分為另外一類。如此便構建了CART 樹的一個節點。其他節點的生成過程和這個是一樣的。現在的問題是在每輪迭代的時候,如何選擇這個特徵 j,以及如何選擇特徵 j 的切分點 m:

    • 原始的gbdt的做法非常的暴力,首先遍歷每個特徵,然後對每個特徵遍歷它所有可能的切分點,找到最優特徵 m 的最優切分點 j。
    • 如何衡量我們找到的特徵 m和切分點 j 是最優的呢? 我們用定義一個函式 FindLossAndSplit 來展示一下求解過程:
 1 def findLossAndSplit(x,y):
 2     # 我們用 x 來表示訓練資料
 3     # 我們用 y 來表示訓練資料的label
 4     # x[i]表示訓練資料的第i個特徵
 5     # x_i 表示第i個訓練樣本
 6 
 7     # minLoss 表示最小的損失
 8     minLoss = Integet.max_value
 9     # feature 表示是訓練的資料第幾緯度的特徵
10     feature = 0
11     # split 表示切分點的個數
12     split = 0
13 
14     # M 表示 樣本x的特徵個數
15     for j in range(0,M):
16         # 該維特徵下,特徵值的每個切分點,這裡具體的切分方式可以自己定義
17         for c in range(0,x[j]):
18             L = 0
19             # 第一類
20             R1 = {x|x[j] <= c}
21             # 第二類
22             R2 = {x|x[j] > c}
23             # 屬於第一類樣本的y值的平均值
24             y1 = ave{y|x 屬於 R1}
25             # 屬於第二類樣本的y值的平均值
26             y2 = ave{y| x 屬於 R2}
27             # 遍歷所有的樣本,找到 loss funtion 的值
28             for x_1 in all x
29                 if x_1 屬於 R1: 
30                     L += (y_1 - y1)^2 
31                 else:
32                     L += (y_1 - y2)^2
33             if L < minLoss:
34                minLoss = L
35                feature  = i
36                split = c
37     return minLoss,feature ,split
    • 如果對這段程式碼不是很瞭解的,可以先去看看李航第五章中對CART TREE 演算法的敘述。在這裡,我們先遍歷訓練樣本的所有的特徵,對於特徵 j,我們遍歷特徵 j 所有特徵值的切分點 c。找到可以讓下面這個式子最小的特徵 j 以及切分點c.
  • gbdt 如何構建特徵 ?

        其實說gbdt 能夠構建特徵並非很準確,gbdt 本身是不能產生特徵的,但是我們可以利用gbdt去產生特徵的組合。在CTR預估中,工業界一般會採用邏輯迴歸去進行處理,在我的上一篇博文當中已經說過,邏輯迴歸本身是適合處理線性可分的資料,如果我們想讓邏輯迴歸處理非線性的資料,其中一種方式便是組合不同特徵,增強邏輯迴歸對非線性分佈的擬合能力。

        長久以來,我們都是通過人工的先驗知識或者實驗來獲得有效的組合特徵,但是很多時候,使用人工經驗知識來組合特徵過於耗費人力,造成了機器學習當中一個很奇特的現象:有多少人工就有多少智慧。關鍵是這樣通過人工去組合特徵並不一定能夠提升模型的效果。所以我們的從業者或者學界一直都有一個趨勢便是通過演算法自動,高效的尋找到有效的特徵組合。Facebook 在2014年 發表的一篇論文便是這種嘗試下的產物,利用gbdt去產生有效的特徵組合,以便用於邏輯迴歸的訓練,提升模型最終的效果。

             圖 2:用GBDT 構造特徵

        如圖 2所示,我們 使用 GBDT 生成了兩棵樹,兩顆樹一共有五個葉子節點。我們將樣本 X 輸入到兩顆樹當中去,樣本X 落在了第一棵樹的第二個葉子節點,第二顆樹的第一個葉子節點,於是我們便可以依次構建一個五緯的特徵向量,每一個緯度代表了一個葉子節點,樣本落在這個葉子節點上面的話那麼值為1,沒有落在該葉子節點的話,那麼值為 0.

        於是對於該樣本,我們可以得到一個向量[0,1,0,1,0] 作為該樣本的組合特徵,和原來的特徵一起輸入到邏輯迴歸當中進行訓練。實驗證明這樣會得到比較顯著的效果提升。

  • GBDT 如何用於分類 ?

    首先明確一點,gbdt 無論用於分類還是迴歸一直都是使用的CART 迴歸樹不會因為我們所選擇的任務是分類任務就選用分類樹,這裡面的核心是因為gbdt 每輪的訓練是在上一輪的訓練的殘差基礎之上進行訓練的。這裡的殘差就是當前模型的負梯度值 。這個要求每輪迭代的時候,弱分類器的輸出的結果相減是有意義的。殘差相減是有意義的。

        如果選用的弱分類器是分類樹,類別相減是沒有意義的。上一輪輸出的是樣本 x 屬於 A類,本一輪訓練輸出的是樣本 x 屬於 B類。 A 和 B 很多時候甚至都沒有比較的意義,A 類- B類是沒有意義的。

        我們具體到分類這個任務上面來,我們假設樣本 X 總共有 K類。來了一個樣本 x,我們需要使用gbdt來判斷 x 屬於樣本的哪一類。

    • 圖三 gbdt 多分類演算法流程

        第一步 我們在訓練的時候,是針對樣本 X 每個可能的類都訓練一個分類迴歸樹。舉例說明,目前樣本有三類,也就是 K = 3。樣本 x 屬於 第二類。那麼針對該樣本 x 的分類結果,其實我們可以用一個 三維向量 [0,1,0] 來表示。0表示樣本不屬於該類,1表示樣本屬於該類。由於樣本已經屬於第二類了,所以第二類對應的向量維度為1,其他位置為0。

        針對樣本有 三類的情況,我們實質上是在每輪的訓練的時候是同時訓練三顆樹。第一顆樹針對樣本x的第一類,輸入為$(x,0)$。第二顆樹輸入針對 樣本x 的第二類,輸入為$(x,1)$。第三顆樹針對樣本x 的第三類,輸入為$(x,0)$

        在這裡每顆樹的訓練過程其實就是就是我們之前已經提到過的CATR TREE 的生成過程。在此處我們參照之前的生成樹的程式 即可以就解出三顆樹,以及三顆樹對x 類別的預測值$f_{1}(x),f_{2}(x),f_{3}(x)$。那麼在此類訓練中,我們仿照多分類的邏輯迴歸 ,使用softmax 來產生概率,則屬於類別 1 的概率$$p_{1}=exp(f_{1}{(x)})/\sum_{k= 1}^{3}exp(f_{k}{(x)})$$

        並且我們我們可以針對類別1 求出 殘差$y_{11}(x) = 0-p_{1}(x)$;類別2 求出殘差$y_{22}(x)= 1-p_2(x)$;類別3 求出殘差$y_{33}(x)= 0-p_{3}(x)$.

        然後開始第二輪訓練 針對第一類 輸入為(x,$y_{11}(x)$), 針對第二類輸入為(x,$y_{22}(x))$, 針對 第三類輸入為 (x,$y_{33}(x)$).繼續訓練出三顆樹。一直迭代M輪。每輪構建 3顆樹。

        所以當K =3。我們其實應該有三個式子 $$F_{1M}{(x)}=\sum_{m=1}^{M}{\hat{C_{1m}}I(x\epsilon R_{1m})}$$ $$F_{2M}{(x)}=\sum_{m=1}^{M}{\hat{C_{2m}}I(x\epsilon R_{2m})}$$ $$F_{3M}{(x)}=\sum_{m=1}^{M}{\hat{C_{3m}}I(x\epsilon R_{3m})}$$

        當訓練完畢以後,新來一個樣本 x1 ,我們需要預測該樣本的類別的時候,便可以有這三個式子產生三個值,$f_{1}(x),f_{2}(x),f_{3}(x)$。樣本屬於 某個類別c的概率為 $$p_{c}=exp(f_{c}{(x)})/\sum_{k= 1}^{3}exp(f_{k}{(x)})$$

  • GBDT 多分類舉例說明

        上面的理論闡述可能仍舊過於難懂,我們下面將拿Iris 資料集中的六個資料作為例子,來展示gbdt 多分類的過程。

    • 樣本編號 花萼長度(cm) 花萼寬度(cm) 花瓣長度(cm) 花瓣寬度 花的種類
      1 5.1 3.5 1.4 0.2 山鳶尾
      2 4.9 3.0 1.4 0.2 山鳶尾
      3 7.0 3.2 4.7 1.4 雜色鳶尾
      4 6.4 3.2 4.5 1.5 雜色鳶尾
      5 6.3 3.3 6.0 2.5 維吉尼亞鳶尾
      6 5.8 2.7 5.1 1.9 維吉尼亞鳶尾
      圖四 Iris 資料集

        這是一個有6個樣本的三分類問題。我們需要根據這個花的花萼長度,花萼寬度,花瓣長度,花瓣寬度來判斷這個花屬於山鳶尾,雜色鳶尾,還是維吉尼亞鳶尾。具體應用到gbdt多分類演算法上面。我們用一個三維向量來標誌樣本的label。[1,0,0] 表示樣本屬於山鳶尾,[0,1,0] 表示樣本屬於雜色鳶尾,[0,0,1] 表示屬於維吉尼亞鳶尾。

        gbdt 的多分類是針對每個類都獨立訓練一個 CART Tree。所以這裡,我們將針對山鳶尾類別訓練一個 CART Tree 1。雜色鳶尾訓練一個 CART Tree 2 。維吉尼亞鳶尾訓練一個CART Tree 3,這三個樹相互獨立。

        我們以樣本 1 為例。針對 CART Tree1 的訓練樣本是$[5.1, 3.5 , 1.4, 0.2]$,label 是 1,最終輸入到模型當中的為$[5.1, 3.5 , 1.4, 0.2, 1]$。針對 CART Tree2 的訓練樣本也是$[5.1, 3.5 , 1.4, 0.2]$,但是label 為 0,最終輸入模型的為$[5.1, 3.5 , 1.4, 0.2, 0]$. 針對 CART Tree 3的訓練樣本也是$[5.1, 3.5 , 1.4, 0.2] $,label 也為0,最終輸入模型當中的為$[5.1, 3.5 , 1.4, 0.2, 0]$. 

        下面我們來看 CART Tree1 是如何生成的,其他樹 CART Tree2 , CART Tree 3的生成方式是一樣的。CART Tree的生成過程是從這四個特徵中找一個特徵做為CART Tree1 的節點。比如花萼長度做為節點。6個樣本當中花萼長度 大於5.1 cm的就是 A類,小於等於 5.1 cm 的是B類。生成的過程其實非常簡單,問題 1.是哪個特徵最合適? 2.是這個特徵的什麼特徵值作為切分點? 即使我們已經確定了花萼長度做為節點。花萼長度本身也有很多值。在這裡我們的方式是遍歷所有的可能性,找到一個最好的特徵和它對應的最優特徵值可以讓當前式子的值最小。

我們以第一個特徵的第一個特徵值為例。R1 為所有樣本中花萼長度小於 5.1 cm 的樣本集合,R2 為所有樣本當中花萼長度大於等於 5.1cm 的樣本集合。所以 $R1 = \left \{  2\right \}$,$R2 = \left \{ 1,3,4,5,6 \right \}$.

       

圖 5 節點分裂示意圖

        y1 為 R1 所有樣本的label 的均值 $1/1 = 1$。y2 為 R2 所有樣本的label 的均值 $(1+0+0+0+0) /5 = 0.2$。

        下面便開始針對所有的樣本計算這個式子的值。樣本1 屬於 R2 計算的值為$( 1 - 0.2)^2$, 樣本2 屬於R1 計算的值為$( 1 -1 )^2$, 樣本 3,4,5,6同理都是 屬於 R2的 所以值是$(0-0.2)^2$. 把這六個值加起來,便是 山鳶尾型別在特徵1 的第一個特徵值的損失值。這裡算出來(1-0.2)^2+ (1-1)^2 + (0-0.2)^2+(0-0.2)^2+(0-0.2)^2 +(0-0.2)^2= 0.84

        接著我們計算第一個特徵的第二個特徵值,計算方式同上,R1 為所有樣本中 花萼長度小於 4.9 cm 的樣本集合,R2 為所有樣本當中 花萼長度大於等於 4.9 cm 的樣本集合.所以 $R1 = \left \{  \right \}$,$R1 = \left \{ 1,2,3,4,5,6 \right \}$. y1 為 R1 所有樣本的label 的均值 = 0。y2 為 R2 所有樣本的label 的均值 $(1+1+0+0+0+0) /6 = 0.3333$。

圖 6 第一個特徵的第二個特偵值的節點分裂情況        

        我們需要針對所有的樣本,樣本1 屬於 R2, 計算的值為$( 1 - 0.333 )^2$, 樣本2 屬於R2 ,計算的值為$( 1 -0.333 )^2$, 樣本 3,4,5,6同理都是 屬於 R2的, 所以值是$(0-0.333)^2$. 把這六個值加起來山鳶尾型別在特徵1 的第二個特徵值的損失值。這裡算出來 (1-0.333)^2+ (1-0.333)^2 + (0-0.333)^2+(0-0.333)^2+(0-0.333)^2 +(0-0.333)^2 = 2.244189. 這裡的損失值大於 特徵一的第一個特徵值的損失值,所以我們不取這個特徵的特徵值。

圖 7 所有情況說明  

        這樣我們可以遍歷所有特徵的所有特徵值,找到讓這個式子最小的特徵以及其對應的特徵值,一共有24種情況,4個特徵*每個特徵有6個特徵值。在這裡我們算出來讓這個式子最小的特徵花萼長度,特徵值為5.1 cm。這個時候損失函式最小為 0.8。

        於是我們的預測函式此時也可以得到: $$f(x) = \sum_{x\epsilon R_{1}} y_{1}*I(x\epsilon R_{1})+\sum_{x\epsilon R_{2}} y_{2}*I(x\epsilon R_{2})$$

        此處 R1 = {2},R2 = {1,3,4,5,6},y1 = 1,y2 = 0.2。訓練完以後的最終式子為 $$f_{1}(x) = \sum_{x\epsilon R_{1}} 1*I(x\epsilon R_{1})+\sum_{x\epsilon R_{2}} 0.2*I(x\epsilon R_{2})$$

        藉由這個式子,我們得到對樣本屬於類別1 的預測值 $f_{1}(x) = 1 + 0.2 * 5  = 2$。同理我們可以得到對樣本屬於類別2,3的預測值$f_{2}(x)$,$f_{3}(x)$.樣本屬於類別1的概率 即為 $$p_{1}=exp(f_{1}{(x)})/\sum_{k= 1}^{3}exp(f_{k}{(x)})$$

        下面我們用程式碼來實現整個找特徵的過程,大家可以自己再對照程式碼看看

 1 # 定義訓練資料
 2 train_data = [[5.1,3.5,1.4,0.2],[4.9,3.0,1.4,0.2],[7.0,3.2,4.7,1.4],[6.4,3.2,4.5,1.5],[6.3,3.3,6.0,2.5],[5.8,2.7,5.1,1.9]]
 3 
 4 # 定義label
 5 label_data = [[1,0,0],[1,0,0],[0,1,0],[0,1,0],[0,0,1],[0,0,1]]
 6 # index 表示的第幾類
 7 def findBestLossAndSplit(train_data,label_data,index):
 8         sample_numbers = len(label_data)
 9         feature_numbers = len(train_data[0])
10         current_label = []
11 
12         # define the minLoss
13         minLoss = 10000000
14 
15         # feature represents the dimensions of the feature
16         feature = 0
17 
18         # split represents the detail split value
19         split = 0
20 
21         # get current label
22         for label_index in range(0,len(label_data)):
23             current_label.append(label_data[label_index][index])
24 
25         # trans all features
26         for feature_index in range(0,feature_numbers):
27             ## current feature value
28             current_value = []
29 
30             for sample_index in range(0,sample_numbers):
31                 current_value.append(train_data[sample_index][feature_index])
32             L = 0
33             ## different split value
34             print current_value
35             for index in range(0,len(current_value)):
36                 R1 = []
37                 R2 = []
38                 y1 = 0
39                 y2 = 0
40 
41                 for index_1 in range(0,len(current_value)):
42                     if current_value[index_1] < current_value[index]:
43                         R1.append(index_1)
44                     else:
45                         R2.append(index_1)
46 
47                 ## calculate the samples for first class
48                 sum_y = 0
49                 for index_R1 in R1:
50                     sum_y += current_label[index_R1]
51                 if len(R1) != 0:
52                     y1 = float(sum_y) / float(len(R1))
53                 else:
54                     y1 = 0
55 
56                 ## calculate the samples for second class
57                 sum_y = 0
58                 for index_R2 in R2:
59                     sum_y += current_label[index_R2]
60                 if len(R2) != 0:
61                     y2 = float(sum_y) / float(len(R2))
62                 else:
63                     y2 = 0
64 
65                 ## trans all samples to find minium loss and best split
66                 for index_2 in range(0,len(current_value)):
67                     if index_2 in R1:
68                         L += float((current_label[index_2]-y1))*float((current_label[index_2]-y1))
69                     else:
70                         L += float((current_label[index_2]-y2))*float((current_label[index_2]-y2))
71 
72                 if L < minLoss:
73                     feature = feature_index
74                     split = current_value[index]
75                     minLoss = L
76                     print "minLoss"
77                     print minLoss
78                     print "split"
79                     print split
80                     print "feature"
81                     print feature
82         return minLoss,split,feature
83 
84 findBestLossAndSplit(train_data,label_data,0)
  • 3 總結

        目前,我們總結了 gbdt 的演算法的流程,gbdt如何選擇特徵,如何產生特徵的組合,以及gbdt 如何用於分類,這個目前可以認為是gbdt 最經常問到的四個部分。至於剩餘的問題,因為篇幅的問題,我們準備再開一個篇幅來進行總結。