白話機器學習算法理論+實戰番外篇之Xgboost

1. 寫在前面

如果想從事數據挖掘或者機器學習的工作,掌握常用的機器學習算法是非常有必要的,在這簡單的先捋一捋, 常見的機器學習算法:

  • 監督學習算法:邏輯迴歸,線性迴歸,決策樹,樸素貝葉斯,K近鄰,支持向量機,集成算法Adaboost等
  • 無監督算法:聚類,降維,關聯規則, PageRank等

我前面已經嘗試用最白話的語言完成了一個白話機器學習算法理論+實戰系列,這個系列已經基本包含了上面這些算法的原理和基本使用。 但是,如果僅僅是會用這些算法可是不夠的, 我們也得跟着時代的步伐前進,近幾年,有很多大佬又在上面的某些算法上加以改進,發明了更加厲害的算法,而這些算法纔是當今時代解決問題的主流,所以我們學習的一個方式就是掌握傳統,而又得緊跟時代

所以,後面考慮加上當前流行的一些主流機器學習算法,既當複習,又當提升。由於不想和傳統的機器學習算法混合起來,故稱之爲番外,也是傳統機器學習算法的延伸, 同樣是儘量白話,同樣是豐富實戰,但會夾雜數學的身影,畢竟後面的很多算法如果沒有了數學就彷彿失去了靈魂,無法活靈活現。所以機器學習算法的故事還沒有完,我們還得繼續走着。

學習算法的過程,獲得的不應該只有算法理論,還應該有樂趣和解決實際問題的能力!

今天分享的這個算法堪稱數據科學競賽界的神器, 它似乎是用於贏得數據科學競賽的分類器/預測器必不可少的算法, 那就是Xgboost。聽這個名字,你可能一下就想到了傳統機器學習算法裏面的AdaBoost,哈哈,聯想和對比才能更加理解算法的精華。你還別說,這個算法和那個來自於同一個家族,都是集成學習算法,都屬於boosting流派,但是兩者的boosting採用了不同的策略,而就是這策略的不同,導致xgboost成了目前競賽者眼中的紅人,它是目前最快最好的開源 boosting tree 工具包,比常見的工具包快 10 倍以上, 那麼xgboost到底採用了什麼策略呢? 它又是如何做到高準確率和高速度的呢?Xgboost和AdaBoost到底有什麼不同呢? Xgboost又如何來解決實際問題呢? 這些問題,在這篇文章中都會一一來解剖。

大綱如下:

  • Xgboost? 這個故事還得先從AdaBoost和GBDT說起
  • Xgboost的基本原理(基於例子我們來看看好玩的公式推導)
  • Xgboost的實戰應用(這裏用xgboost做一個分類任務,然後說一下基本使用和高級功能)

Ok, let’s go!

2. Xgboost? 這個故事還得先從AdaBoost和GBDT說起

我覺得,學習一個算法的時候,有時候不能直接單拿出一個算法來說,這樣感覺顯得突兀了些,不知道突然從哪冒出來一樣。所以,講Xgboost之前,我想先帶你回顧一下我們之前的集成學習。

所謂集成學習,就是指構建多個弱分類器對數據集進行預測,然後用某種策略將多個分類器預測的結果集成起來,作爲最終預測結果。 什麼,不懂? 看來得先去聽聽我之前那“三個臭皮匠,頂個諸葛亮”的故事了。在這裏就不再講了(可以理解成集成學習是一種把大傢伙叫到一塊,集思廣益想辦法解決問題的方式吧),在這裏想說的是集成學習的那兩大流派:Boosting和Bagging。

怎麼還有兩個流派呢? 集思廣益不就完事? 哈哈, 集思廣益也有不同的方式嗎? 比如針對同一個問題,把問題劃分成不相干的子問題,然後分派給不同的人各幹各的是一種, 或者同一個問題,劃分成串行的子問題, 先由一個人解決一部分,解決不了的,後面的人再來這又是一種。 把上面這兩種方式用官方的語言描述就是:根據各個弱分類器之間有無依賴關係,分爲Boosting和Bagging。

  • Boosting流派,各分類器之間有依賴關係,必須串行,比如Adaboost、GBDT(Gradient Boosting Decision Tree)、Xgboost
  • Bagging流派,各分類器之間沒有依賴關係,可各自並行,比如隨機森林(Random Forest)

關於Bagging流派的Random Forest(隨機森林)算法,也是比較常用的,簡單的說就是各個弱分類器是獨立的、每個分類器在樣本堆裏隨機選一批樣本,隨機選一批特徵進行獨立訓練,各個分類器之間沒有啥關係, 最後投票表決, 這個在這裏不做詳述,後面遇到的時候再統一總結,今天的主角是Xgboost,所以我們主要是瞭解一下Boosting流派, 這這裏面的最具代表性的算法之一就是AdaBoost, 這個我這裏不做過多的表述,詳細的可以看一下白話機器學習算法理論+實戰之AdaBoost算法, 這裏只回顧一下它的算法原理,這樣好引出後面的GBDT和Xgboost,並且可以進行策略上的對比。

AdaBoost,是英文"Adaptive Boosting"(自適應增強),它的自適應在於:前一個基本分類器分錯的樣本會得到加強,加權後的全體樣本再次被用來訓練下一個基本分類器。同時,在每一輪中加入一個新的弱分類器,直到達到某個預定的足夠小的錯誤率或達到預先指定的最大迭代次數。白話的講,就是它在訓練弱分類器之前,會給每個樣本一個權重,訓練完了一個分類器,就會調整樣本的權重,前一個分類器分錯的樣本權重會加大,這樣後面再訓練分類器的時候,就會更加註重前面分錯的樣本, 然後一步一步的訓練出很多個弱分類器,最後,根據弱分類器的表現給它們加上權重,組合成一個強大的分類器,就足可以應付整個數據集了。 這就是AdaBoost, 它強調自適應,不斷修改樣本權重, 不斷加入弱分類器進行boosting。

那麼,boosting還有沒有別的方式呢? GBDT(Gradient Boost Decision Tree)就是另一種boosting的方式, 上面說到AdaBoost訓練弱分類器關注的是那些被分錯的樣本,AdaBoost每一次訓練都是爲了減少錯誤分類的樣本。 而GBDT訓練弱分類器關注的是殘差,也就是上一個弱分類器的表現與完美答案之間的差距,GBDT每一次訓練分類器,都是爲了減少這個差距,GBDT每一次的計算是都爲了減少上一次的殘差,進而在殘差減少(負梯度)的方向上建立一個新的模型。這是什麼意思呢? 我可以舉個例子,假設我們去銀行借錢,我們想讓一個決策樹系統來預測可以借給我們多少錢, 如果標準答案是1000的話,假設第一棵決策樹預測,可以借給我們950塊錢, 那麼離標準答案的1000還差50, 效果不算好,能不能提高一些呢? 我們就再加一棵決策樹,這課決策樹過來之後,看到前面的那個已經預測到950了,只是差50, 那麼我可以聚焦在這個50上,把這個殘差變得再小一些,所以第二個決策樹預測結果是30, 那麼前兩棵決策樹預測結果結合起來是980, 離標準答案差20, 所以加了一棵樹之後,效果好了。 那麼還能不能提升呢? 我再來一棵樹,發現殘差只有20了,那我把殘差變得再小, 結果第三個決策樹預測20, 那麼這三棵樹就可以正確的預測最終的1000了。

所以GBDT就是這樣的一個學習方式了,GBDT是boosting集成學習,boosting集成學習由多個相關聯的決策樹聯合決策, 什麼是相關聯? 就是我上面的例子:

  1. 有一個樣本[數據->標籤]是:[(feature1,feature2,feature3)-> 1000塊]
  2. 第一棵決策樹用這個樣本訓練的預測爲950
  3. 那麼第二棵決策樹訓練時的輸入,這個樣本就變成了:[(feature1,feature2,feature3)->50]
  4. 第二棵決策樹用這個樣本訓練的預測爲30
  5. 那麼第三棵決策樹訓練時的輸入,這個樣本就變成了:[(feature1,feature2,feature3)->20]
  6. 第三棵決策樹用這個樣本訓練的預測爲20

搞定,也就是說,下一棵決策樹輸入樣本會與前面決策樹的訓練和預測相關。用個圖來表示類似這樣:
在這裏插入圖片描述
這就是GBDT的工作原理了, GBDT是旨在不斷減少殘差(迴歸),通過不斷加入新的樹旨在在殘差減少(負梯度)的方向上建立一個新的模型。——即損失函數是旨在最快速度降低殘差。

那麼爲啥要講GBDT呢? 我先賣個關子,不妨先看一下xgboost是怎麼解決問題的。這裏用xgboost原作者陳天奇的講座PPT中的那個圖來看
在這裏插入圖片描述
假設我想預測,這一家子人中每個人想玩遊戲的意願值。我們用xgboost解決這個問題,就是我先訓練出來第一棵決策樹, 預測了一下小男孩想玩遊戲的意願是2, 然後發現離標準答案差一些,又訓練出來了第二棵決策樹, 預測了一下小男孩想玩遊戲的意願是0.9, 那麼兩個相加就是最終的答案2.9。 這個其實就接近了標準答案。 所以xgboost是訓練出來的弱分類結果進行累加就是最終的結論。

恩,你可能要拍案而起了,驚呼,這不是跟上面介紹的GBDT乃異曲同工麼?事實上,如果不考慮工程實現、解決問題上的一些差異,xgboost與gbdt比較大的不同就是目標函數的定義,但這倆在策略上是類似的,都是聚焦殘差(更準確的說, xgboost其實是gbdt算法在工程上的一種實現方式),GBDT旨在通過不斷加入新的樹最快速度降低殘差,而XGBoost則可以人爲定義損失函數(可以是最小平方差、logistic loss function、hinge loss function或者人爲定義的loss function),只需要知道該loss function對參數的一階、二階導數便可以進行boosting,其進一步增大了模型的泛化能力,其貪婪法尋找添加樹的結構以及loss function中的損失函數與正則項等一系列策略也使得XGBoost預測更準確。

所以,這就是我講Xgboost的故事之前,要簡單說一下AdaBoost和GBDT的原因了,這樣腦海裏面是不是對xgboost不那麼陌生了啊,你要知道,這三個同是屬於集成學習的boosting流派,AdaBoost叫做自適應提升,和GBDT,Xgboost提升時採用的策略不同,前者聚焦錯誤樣本,後者聚焦與標準答案的殘差。 而GBDT和Xgboost叫做boosting集成學習,提升時策略類似,都是聚焦殘差,但是降低殘差的方式又各有不同。

好了,鋪墊到此爲止,下面真正進入主角部分 – Xgboost的基本原理。

3. Xgboost的基本原理

Xgboost 的全稱是eXtreme Gradient Boosting,由華盛頓大學的陳天奇博士提出,在Kaggle的希格斯子信號識別競賽中使用,因其出衆的效率與較高的預測準確度而引起了廣泛的關注。

如果boosting算法每一步的弱分類器生成都是依據損失函數的梯度方向,則稱之爲梯度提升(Gradient boosting),XGBoost算法是採用分步前向加性模型,只不過在每次迭代中生成弱學習器後不再需要計算一個係數,XGBoost 是由 k 個基模型組成的一個加法運算式:
y^i=t=1kft(xi)\hat{y}_{i}=\sum_{t=1}^{k} f_{t}\left(x_{i}\right)
其中fkf_k爲第k個基模型, y^i\hat{y}_{i}爲第i個樣本的預測值。
那麼損失函數可由預測值y^i\hat{y}_{i}與真實值yiy_i進行表示:
L=i=1nl(yi,y^i)L=\sum_{i=1}^{n} l\left(y_{i}, \hat{y}_{i}\right)
其中n爲樣本數量。

XGBoost算法通過優化結構化損失函數(加入了正則項的損失函數,可以起到降低過擬合的風險)來實現弱學習器的生成,並且XGBoost算法沒有采用搜索方法,而是直接利用了損失函數的一階導數和二階導數值,並通過預排序、加權分位數等技術來大大提高了算法的性能。

說到這裏,估計你已經看不下去了吧,這說的啥跟啥啊, 聽不懂啦啊!但其實我上面只是用數學的語言來說了一下前面舉得xgboost的那個例子,對於某個樣本,有若干個弱分類器做預測,最後的預測結果就是弱分類器的答案累加(注意此時沒有權重了,如果你還記得AdaBoost模型的話,會發現那個地方每個分類器前面會有個權重αm\alpha_m,最終分類器是F(x)=sign(i=1NαmGm(x))F(x)=\operatorname{sign}\left(\sum_{i=1}^{N} \alpha_{m} G_{m}(x)\right),不騙你喲), 這是上面的第一個公式,第二個公式就是說我怎麼判斷對於整個數據集預測的準不準啊,就得有個損失函數啊, 對比一下與真實值的差距,n個樣本,我都對比一下子。 這個ll表示的某種損失函數,你可以先理解成平方差損失。

如此白話,應該能聽懂了吧, 但還沒真正講xgboost的數學原理呢, 所以後面的數學原理我打算換一種方式,從一個例子展開,剖析數學公式,當然如果你想直接看數學推導的話,我覺得人家這個寫的挺好的終於有人把XGBoost 和 LightGBM 講明白了,項目中最主流的集成算法!,這裏面就全是數學的原理了,如果你感覺直接上數學壓力有點大,那麼可以先跟着我繼續往下,從一個例子中看看xgboost樹到底是如何生成的,然後再回頭看數學原理也不遲 😉

下面就通過算法流程圖舉一個例子來詳解xgboost樹的生成。

先給出一個流程圖, 不懂不要緊,可以看一遍後面的步驟,然後再回來:
在這裏插入圖片描述
爲了讓xgboost數學原理的部分不那麼boring, 我們跟着一個例子走吧:

在這裏插入圖片描述
假設我想預測學生考試分數, 給定若干個學生屬性(比如天賦,每天學習時間,是否談戀愛等),

  • 通過一個決策樹A, 我們可以看到一個天賦屬性的預測結果:天賦高的人+90, 不高的人+60
  • 通過決策樹B, 可以看到每天學習時間高於10小時的+5,低於10小時的-5
  • 通過決策樹C, 可以看到談戀愛的-1, 單身狗的+1


後面依次類推,還可能有更多的決策樹通過學生的某些屬性來推斷分數。

XGboost就是這樣一個不斷生成新的決策樹A,B,C,D…的算法,最終生成的決策樹算法就是樹A+B+C+D+…的和的決策樹。

我們針對這個問題看看詳細的建樹過程吧:

  1. 首先,我們有三個學生, 屬性和標籤如下
    在這裏插入圖片描述
    我們初始化三個樣本的考試成績預測值爲0。

  2. 定義目標函數:
    模型的預測精度由偏差和方差共同決定,損失函數代表了模型的偏差,想要方差小則需要更簡單的模型, 所以目標函數最終由損失函數L與抑制模型複雜度的正則項Ω組成, 所以目標函數如下:
    Obj=i=1nl(y^i,yi)+t=1kΩ(ft)O b j=\sum_{i=1}^{n} l\left(\hat{y}_{i}, y_{i}\right)+\sum_{t=1}^{k} \Omega\left(f_{t}\right)
    這個公式應該不需要過多的解釋了吧, 其中Ω(ft)Ω(f_t)是正則化項
    Ω(ft)=γTt+12λj=1Twj2\Omega\left(f_{t}\right)=\gamma{T_{t}}+\frac{1}{2} \lambda \sum_{j=1}^{T} w_{j}^{2}

    前面的TtT_t爲葉子節點數, wjw_j表示j葉子上的節點權重, γλ\gamma,\lambda是預先給定的超參數。引入了正則化之後,算法會選擇簡單而性能優良的模型, 正則化項只是用來在每次迭代中抑制弱分類器fi(x)f_i(x)過擬合,不參與最終模型的集成。(這個正則化項可以先不用管它, 有個印象即可,後面樹那個地方會統一解釋)

    我們下面看看這個目標函數objobj還能不能化簡呢?

    我們知道, boosting模型是前向加法, 以第t步模型爲例, 模型對第i個樣本xix_i的預測爲:y^it=y^it1+ft(xi)\hat{y}_{i}^{t}=\hat{y}_{i}^{t-1}+f_{t}\left(x_{i}\right)
    其中,y^it1\hat{y}_{i}^{t-1}是第t-1步的模型給出的預測值, 是已知常數,ft(xi)f_t(x_i)是我們這次需要加入的新模型,所以把這個y^it\hat{y}_{i}^{t}代入上面,就可以進一步化簡:
    Obj(t)=i=1nl(yi,y^it)+i=1tΩ(fi)=i=1nl(yi,y^it1+ft(xi))+i=1tΩ(fi)\begin{aligned} O b j^{(t)} &=\sum_{i=1}^{n} l\left(y_{i}, \hat{y}_{i}^{t}\right)+\sum_{i=1}^{t} \Omega\left(f_{i}\right) \\ &=\sum_{i=1}^{n} l\left(y_{i}, \hat{y}_{i}^{t-1}+f_{t}\left(x_{i}\right)\right)+\sum_{i=1}^{t} \Omega\left(f_{i}\right) \end{aligned}
    這個就是xgboost的目標函數了,最優化這個目標函數,其實就是相當於求解當前的ft(xi)f_t(x_i)Xgboost系統的每次迭代都會構建一顆新的決策樹,決策樹通過與真實值之間殘差來構建。什麼,沒有看到殘差的身影? 別急,後面會看到這個殘差長什麼樣子。

    我們回到我們的例子,假設已經根據天賦這個屬性建立了一棵決策樹A(關於如何建樹在這裏不做解釋,可以看看白話機器學習算法理論+實戰之決策樹),只不過這裏的樹分裂計算收益的方式換了一種,後面會具體說到。

    在這裏插入圖片描述
    我們有了第一棵樹, 通過這個樹的預測結果:
    在這裏插入圖片描述
    那麼我們建立第二棵樹的時候,我們是考慮的殘差,也就是樣本其實變成了下面這樣:
    在這裏插入圖片描述
    通過最小化殘差學習到一個通過學習時間屬性構建的決策樹得到了90+5,60+5,90-5的預測值,再繼續通過(100-95=5)(70-65)(86-85)的殘差構建下一個決策樹,以此類推,當迭代次數達到上限或是殘差不再減小是停止,就得到一個擁有多個(迭代次數)決策樹的強分類器。這個就是xgboost工作的宏觀過程了。光宏觀部分確實挺好理解,但具體細節呢? 比如我每一次建樹是怎麼建的呢? 既然說計算收益的方式不同, 那麼我考慮分裂的時候是怎麼計算收益的呢? 目前你心中肯定會有這些疑問, 莫慌莫慌, 下面我把建樹的細節給你娓娓道來,不過道來的過程中得崎嶇一點,需要用到數學的語言。

    那麼究竟是如何得到一棵新的樹的呢? 下面就是Xgboost的精髓了。前方高能,一大波數學公式來襲, 請戴好安全帽!!!

  3. 目標函數的Taylor化簡:
    這是Xgboost的精髓了。我們首先看一個關於目標函數的簡單等式變換, 把上面的目標函數拿過來:
    Obj(t)=i=1nl(yi,y^it)+i=1tΩ(fi)=i=1nl(yi,y^it1+ft(xi))+i=1tΩ(fi)\begin{aligned} O b j^{(t)} &=\sum_{i=1}^{n} l\left(y_{i}, \hat{y}_{i}^{t}\right)+\sum_{i=1}^{t} \Omega\left(f_{i}\right) \\ &=\sum_{i=1}^{n} l\left(y_{i}, \hat{y}_{i}^{t-1}+f_{t}\left(x_{i}\right)\right)+\sum_{i=1}^{t} \Omega\left(f_{i}\right) \end{aligned}
    我們看看這裏的l(yi,y^it1+ft(xi))l\left(y_{i}, \hat{y}_{i}^{t-1}+f_{t}\left(x_{i}\right)\right),這個部分,如果結合偉大的Taylor的話,會發生什麼情況,你還記得泰勒嗎?

    在這裏插入圖片描述

    根據Taylor公式, 我們把函數f(x+Δ(x))f(x+\Delta(x))在點xx處二階展開,可得到:
    f(x+Δx)f(x)+f(x)Δx+12f(x)Δx2f(x+\Delta x) \approx f(x)+f^{\prime}(x) \Delta x+\frac{1}{2} f^{\prime \prime}(x) \Delta x^{2}
    那麼我們把l(yi,y^it1+ft(xi))l\left(y_{i}, \hat{y}_{i}^{t-1}+f_{t}\left(x_{i}\right)\right)中的y^it1\hat{y}_{i}^{t-1}視爲xx, ft(xi)f_t(x_i)視爲Δ(x)\Delta(x), 那麼目標函數就可以寫成:
    Obj(t)i=1n[l(yi,y^it1)+gift(xi)+12hift2(xi)]+i=1tΩ(fi)O b j^{(t)}\approx\sum_{i=1}^{n}\left[l\left(y_{i}, \hat{y}_{i}^{t-1}\right)+g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)\right]+\sum_{i=1}^{t} \Omega\left(f_{i}\right)
    其中gig_i是損失函數ll的一階導數, hih_i是損失函數ll的二階導, 注意這裏的求導是對y^it1\hat{y}_{i}^{t-1}求導。
    gi=L(yi,y^i(t1))y^i(t1),hi=2L(yi,y^i(t1))y^i(t1)g_{i}=\frac{\partial L\left(y_{i}, \hat{y}_{i}^{(t-1)}\right)}{\partial \hat{y}_{i}^{(t-1)}}, h_{i}=\frac{\partial^{2} L\left(y_{i}, \hat{y}_{i}^{(t-1)}\right)}{\partial \hat{y}_{i}^{(t-1)}}

    這裏我們以平方損失函數爲例:
    i=1n(yi(y^it1+ft(xi)))2\sum_{i=1}^{n}\left(y_{i}-\left(\hat{y}_{i}^{t-1}+f_{t}\left(x_{i}\right)\right)\right)^{2}
    則對於每一個樣本:
    gi=(y^t1yi)2y^t1=2(y^t1yi)hi=2(y^t1yi)2y^t1=2\begin{aligned} &g_{i}=\frac{\partial\left(\hat{y}^{t-1}-y_{i}\right)^{2}}{\partial \hat{y}^{t-1}}=2\left(\hat{y}^{t-1}-y_{i}\right)\\ &h_{i}=\frac{\partial^{2}\left(\hat{y}^{t-1}-y_{i}\right)^{2}}{\hat{y}^{t-1}}=2 \end{aligned}

    由於在第t步時y^it1\hat{y}_i^{t-1}是一個已知的值, 所以l(yi,y^it1)l\left(y_{i}, \hat{y}_{i}^{t-1}\right)是一個常數,其對函數的優化不會產生影響, 因此目標函數可以進一步寫成:
    Obj(t)i=1n[gift(xi)+12hift2(xi)]+i=1tΩ(fi)\operatorname{Obj}^{(t)} \approx \sum_{i=1}^{n}\left[g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)\right]+\sum_{i=1}^{t} \Omega\left(f_{i}\right)
    所以我們只需要求出每一步損失函數的一階導和二階導的值(由於前一步的y^it1\hat{y}_i^{t-1}是已知的,所以這兩個值就是常數),然後最優化目標函數,就可以得到每一步的 f(x) ,最後根據加法模型得到一個整體模型。

    但是還有個問題,就是我們如果是建立決策樹的話,根據上面的可是無法建立出一棵樹來。因爲這裏的ft(xi)f_t(x_i)是什麼鬼? 咱不知道啊!所以啊,還得進行一步映射, 將樣本x映射到一個相對應的葉子節點纔可以,看看是怎麼做的?

  4. 基於決策樹的目標函數的終極化簡
    上面的目標函數先放下來:
    Obj(t)i=1n[gift(xi)+12hift2(xi)]+i=1tΩ(fi)\operatorname{Obj}^{(t)} \approx \sum_{i=1}^{n}\left[g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)\right]+\sum_{i=1}^{t} \Omega\left(f_{i}\right)
    我們這裏先解決一下這個ft(xi)f_t(x_i)的問題,這個究竟怎麼在決策樹裏面表示呢? 那解決這個問題之前,我們看看這個ft(xi)f_t(x_i)表示的含義是什麼, ftf_t就是我有一個決策樹模型, xix_i是每一個訓練樣本,那麼這個整體ft(xi)f_t(x_i)就是我某一個樣本xix_i經過決策樹模型ftf_t得到的一個預測值,對吧? 那麼,我如果是在決策樹上,可以這麼想, 我的決策樹就是這裏的ftf_t, 然後對於每一個樣本xix_i我要在決策樹上遍歷獲得預測值,其實就是在遍歷決策樹的葉子節點,因爲每個樣本最終通過決策樹都到了葉子上去, 不信?看下圖(樣本都在葉子上, 只不過這裏要注意一個葉子上不一定只有一個樣本):
    在這裏插入圖片描述
    所以,通過決策樹遍歷樣本,其實就是在遍歷葉子節點。這樣我們就可以把問題就行轉換,把決策樹模型定義成ft(x)=wq(x)f_t(x)=w_{q(x)}, 其中q(x)q(x)代表了該樣本在哪個葉子節點上, ww表示該葉子節點上的權重(上面分數預測裏面+90, +60就是葉子節點的權重)。所以wq(x)w_{q(x)}就代表了每個樣本的取值(預測值)。 那麼這個樣本的遍歷,就可以這樣化簡:
    i=1n[gift(xi)+12hift2(xi)]=i=1n[giwq(xi)+12hiwq(xi)2]=j=1T[(iIjgi)wj+12(iIjhi)wj2]\sum_{i=1}^{n}\left[g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)\right] = \sum_{i=1}^{n}\left[g_{i} w_{q\left(x_{i}\right)}+\frac{1}{2} h_{i} w_{q\left(x_{i}\right)}^{2}\right] = \sum_{j=1}^{T}\left[\left(\sum_{i \in I_{j}} g_{i}\right) w_{j}+\frac{1}{2}\left(\sum_{i \in I_{j}} h_{i}\right) w_{j}^{2}\right]
    這個再解釋一遍就是:遍歷所有的樣本後求每個樣本的損失函數,但樣本最終會落在葉子節點上,所以我們也可以遍歷葉子節點,然後獲取葉子節點上的樣本集合(注意第二個等式和第三個等式求和符號的上下標, T代表葉子總個數)。 由於一個葉子節點有多個樣本存在,所以後面有了iIjgi\sum_{i \in I_{j}} g_{i}iIjhi\sum_{i \in I_{j}} h_{i}這兩項,這裏的IjI_{j}它代表一個集合,集合中每個值代表一個訓練樣本的序號,整個集合就是某棵樹第j個葉子節點上的訓練樣本 , wjw_j爲第j個葉子節點的取值。只要明白了這一步,後面的公式就很容易理解了。

    我們再解決一下後面那部分,在決策樹中,決策樹的複雜度可由葉子數 T 組成,葉子節點越少模型越簡單,此外葉子節點也不應該含有過高的權重 w (類比 LR 的每個變量的權重),所以目標函數的正則項可以定義爲:
    Ω(ft)=γT+12λj=1Twj2\Omega\left(f_{t}\right)=\gamma T+\frac{1}{2} \lambda \sum_{j=1}^{T} w_{j}^{2}
    即決策樹模型的複雜度由生成的所有決策樹的葉子節點數量(γ\gamma權衡),和所有節點權重(λ\lambda權衡)所組成的向量的 範式共同決定。
    在這裏插入圖片描述
    這張圖給出了基於決策樹的 XGBoost 的正則項的求解方式。

    這樣,目標函數的前後兩部分都進行了解決,那麼目標函數就可以化成最後這個樣子,看看能懂嗎?
    Obj(t)i=1n[gift(xi)+12hift2(xi)]+Ω(ft)=i=1n[giwq(xi)+12hiwq(xi)2]+γT+12λj=1Twj2=j=1T[(iIjgi)wj+12(iIjhi+λ)wj2]+γT\begin{aligned} O b j^{(t)} & \approx \sum_{i=1}^{n}\left[g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)\right]+\Omega\left(f_{t}\right) \\ &=\sum_{i=1}^{n}\left[g_{i} w_{q\left(x_{i}\right)}+\frac{1}{2} h_{i} w_{q\left(x_{i}\right)}^{2}\right]+\gamma T+\frac{1}{2} \lambda \sum_{j=1}^{T} w_{j}^{2} \\ &=\sum_{j=1}^{T}\left[\left(\sum_{i \in I_{j}} g_{i}\right) w_{j}+\frac{1}{2}\left(\sum_{i \in I_{j}} h_{i}+\lambda\right) w_{j}^{2}\right]+\gamma T \end{aligned}
    這裏的Ij={iq(xi)=j}I_{j}=\left\{i | q\left(x_{i}\right)=j\right\}爲第j個葉子節點的樣本集合。爲了簡化表達式,我們再定義:Gj=iIjgiHj=iIjhiG_{j}=\sum_{i \in I_{j}} g_{i} \quad H_{j}=\sum_{i \in I_{j}} h_{i}, 那麼決策樹版本xgboost的目標函數:
    Obj(t)=j=1T[Gjwj+12(Hj+λ)wj2]+γTO b j^{(t)}=\sum_{j=1}^{T}\left[G_{j} w_{j}+\frac{1}{2}\left(H_{j}+\lambda\right) w_{j}^{2}\right]+\gamma T
    這裏要注意GjG_jHjH_j是前t-1步得到的結果, 其值已知,只有最後一棵樹的葉子節點wjw_j的值不確定, 那麼將目標函數對wjw_j求一階導, 並令其等於0, J(ft)wj=Gj+(Hj.+λ)wj=0\frac{\partial J\left(f_{t}\right)}{\partial w_{j}}=G_{j}+\left(H_{j .}+\lambda\right) w_{j}=0, 則可以求得葉子節點j對應的權值:
    wj=GjHj+λw_{j}^{*}=-\frac{G_{j}}{H_{j}+\lambda}
    那麼這個目標函數又可以進行化簡:
    obj=12j=1TGj2Hj+λ+γTob j=-\frac{1}{2} \sum_{j=1}^{T} \frac{G_{j}^{2}}{H_{j}+\lambda}+\gamma T
    這個就是基於決策樹的xgboost模型的目標函數最終版本了,這裏的G和H的求法,就需要明確的給出損失函數來, 然後求一階導和二階導,然後代入樣本值即得出。

    這個objobj代表了當我們指定一個樹的結構的時候,我們在目標上最多能夠減少多少,我們之前不是說建立一個樹就是讓殘差儘可能的小嗎? 到底小多少呢? 這個objobj就是衡量這個的, 可以叫做結構分數。 就類似於基尼係數那樣對樹結構打分的一個函數。那麼這個分數怎麼算呢? 看下面的例子:

    在這裏插入圖片描述
    還是上面的那個預測玩遊戲的意願,我們假設建了右邊的那棵樹,那麼每個樣本都對應到了葉子節點上去,每一個樣本都會對應一個g和h, 那麼我們遍歷葉子節點,就會得到G和H,然後累加就可以得到這棵樹的結構分數obj(這裏有個小細節就是假設有N個訓練樣本, 那麼就會有N次計算各自的gihigi和hi, 但是由於每個樣本的gihigi和hi沒有啥關係,所以可以並行計算,這樣就可以加速訓練了,而且,gihigi和hi是不依賴於損失函數的形式的,只要這個損失函數二次可微就可以了, emmm…powerful)。

    有了這個,我們就知道這棵樹建的好不好了。

    上面是可以判斷出來一棵樹究竟好不好,那麼建立樹的時候應該怎麼建立呢?一棵樹的結構近乎無限多,總不能一個一個去測算它們的好壞程度,然後再取最好的吧(這是個NP問題)。所以,我們仍然需要採取一點策略,這就是逐步學習出最佳的樹結構。這與我們將K棵樹的模型分解成一棵一棵樹來學習是一個道理,只不過從一棵一棵樹變成了一層一層節點而已。 這叫什麼? emmm, 貪心(找到每一步最優的分裂結果)!xgboost採用二叉樹, 開始的時候, 全部樣本在一個葉子節點上, 然後葉子節點不斷通過二分裂,逐漸生成一棵樹。

    那麼在葉子節點分裂成樹的過程中最關鍵的一個問題就是應該在哪個特徵的哪個點上進行分裂,也就是尋找最優切分點的過程。

  5. 最優切分點劃分算法及優化策略
    在決策樹的生長過程中,一個非常關鍵的問題是如何找到節點的最優切分點, 我們學過了決策樹的建樹過程,那麼我們知道ID3也好,C4.5或者是CART,它們尋找最優切分點的時候都有一個計算收益的東西,分別是信息增益,信息增益比和基尼係數。 而xgboost這裏的切分, 其實也有一個類似於這三個的東西來計算每個特徵點上分裂之後的收益。

    假設我們在某一節點完成特徵分裂,則分列前的目標函數可以寫爲:
    Obj1=12[(GL+GR)2HL+HR+λ]+γO b j_{1}=-\frac{1}{2}\left[\frac{\left(G_{L}+G_{R}\right)^{2}}{H_{L}+H_{R}+\lambda}\right]+\gamma
    分裂後的目標函數:
    Obj2=12[GL2HL+λ+GR2HR+λ]+2γO b j_{2}=-\frac{1}{2}\left[\frac{G_{L}^{2}}{H_{L}+\lambda}+\frac{G_{R}^{2}}{H_{R}+\lambda}\right]+2 \gamma
    則對於目標函數來說,分裂後的收益爲(Obj1-Obj2):
    Gain=12[GL2HL+λ+GR2HR+λ(GL+GR)2HL+HR+λ]γ\text {Gain}=\frac{1}{2}\left[\frac{G_{L}^{2}}{H_{L}+\lambda}+\frac{G_{R}^{2}}{H_{R}+\lambda}-\frac{\left(G_{L}+G_{R}\right)^{2}}{H_{L}+H_{R}+\lambda}\right]-\gamma
    注意該特徵收益也可作爲特徵重要性輸出的重要依據。

    那麼我們就可以來梳理一下最優切分點的劃分算法了:

    • 從深度爲 0 的樹開始,對每個葉節點枚舉所有的可用特徵;
    • 針對每個特徵,把屬於該節點的訓練樣本根據該特徵值進行升序排列,通過線性掃描的方式來決定該特徵的最佳分裂點,並記錄該特徵的分裂收益;(這個過程每個特徵的收益計算是可以並行計算的,xgboost之所以快,其中一個原因就是因爲它支持並行計算,而這裏的並行正是指的特徵之間的並行計算,千萬不要理解成各個模型之間的並行)
    • 選擇收益最大的特徵作爲分裂特徵,用該特徵的最佳分裂點作爲分裂位置,在該節點上分裂出左右兩個新的葉節點,併爲每個新節點關聯對應的樣本集(這裏稍微提一下,xgboost是可以處理空值的,也就是假如某個樣本在這個最優分裂點上值爲空的時候, 那麼xgboost先把它放到左子樹上計算一下收益,再放到右子樹上計算收益,哪個大就把它放到哪棵樹上。)
    • 回到第 1 步,遞歸執行到滿足特定條件爲止


    上面就是最優切分點劃分算法的過程, 看完之後,是不是依然懵逼, 這到底是怎麼做的啊, 下面就看一個尋找最優切分點的栗子吧:

    還是上面玩遊戲的那個例子,假設我有這一家子人樣本,每個人有性別,年齡,興趣等幾個特徵,我想用xgboost建立一棵樹預測玩遊戲的意願值。首先,五個人都聚集在根節點上,現在就考慮根節點分叉,我們就遍歷每個特徵,對於當前的特徵,我們要去尋找最優切分點以及帶來的最大收益,比如當前特徵是年齡,我們需要知道兩點:

    • 按照年齡分是否有效,也就是是否減少了obj的值
    • 如果真的可以分,特徵收益比較大, 那麼我們從哪個年齡點分開呢?


    對於這兩個問題,我們可以這樣做,首先我們先把年齡進行一個排序, 如下圖:
    在這裏插入圖片描述
    按照這個圖從左至右掃描,我們就可以找出所有的切分點a, 對於每一個切分點a,計算出分割的梯度和GLG_LGRG_R 。然後用上面的公式計算出每個分割方案的分數。然後哪個最大,就是年齡特徵的最優切分點,而最大值就是年齡這個特徵的最大信息收益。

    遍歷完所有特徵後,我們就可以確定應該在哪個特徵的哪個點進行切分。對切分出來的兩個節點,遞歸地調用這個過程,我們就能獲得一個相對較好的樹結構, 有了樹結構就比較容易找最優的葉子節點,這樣就能對上面的樣本進行預測了。當然,特徵與特徵之間的收益計算是互不影響的,所以這個遍歷特徵的過程其實可以並行運行。

    在這個過程中你是否注意到了一個問題, 就是xgboost的切分操作和普通的決策樹切分過程是不一樣的。普通的決策樹在切分的時候並不考慮樹的複雜度,所以纔有了後續的剪枝操作。而xgboost在切分的時候就已經考慮了樹的複雜度(obj裏面看到那個γ\gamma了嗎)。所以,它不需要進行單獨的剪枝操作。

    這就是xgboost貪心建樹的一個思路了,即遍歷所有特徵以及所有分割點,每次選最好的那個。 GBDT也是採用的這種方式, 這算法的確不錯,但是有個問題你發現了沒? 就是計算代價太大了,尤其是數據量很大,分割點很多的時候,計算起來非常複雜並且也無法讀入內存進行計算。 所以作者想到了一種近似分割的方式(可以理解爲分割點分桶的思路),選出一些候選的分裂點,然後再遍歷這些較少的分裂點來找到最佳分裂點。那麼怎麼進行分桶選候選分裂點才比較合理呢?我們一般的思路可能是根據特徵值的大小直接進行等寬或者等頻分桶, 像下面這樣(這個地方理解起來有點難,得畫畫了,圖可能不太好看,能說明問題就行,哈哈):

    在這裏插入圖片描述

    上面就是等頻和等寬分桶的思路了(這個不用較真,我這裏只是爲了和作者的想法產生更清晰的對比才這樣舉得例子),這樣選擇出的候選點是不是比就少了好多了?但是這樣劃分其實是有問題的,因爲這樣劃分沒有啥依據啊, 比如我上面畫的等頻分桶,我是5個訓練樣本放一個桶,但是你說你還想10個一組來,沒有個標準啥的啊。 即上面那兩種常規的劃分方式缺乏可解釋性,所以重點來了, 作者這裏採用了一種對loss的影響權重的等值percentiles(百分比分位數)劃分算法(Weight Quantile Sketch), 我上面的這些鋪墊也正是爲了引出這個方式,下面就來看看作者是怎麼做的,這個地方其實不太好理解,所以慢一些

    作者進行候選點選取的時候,考慮的是想讓loss在左右子樹上分佈的均勻一些,而不是樣本數量的均勻,因爲每個樣本對降低loss的貢獻可能不一樣,按樣本均分會導致分開之後左子樹和右子樹loss分佈不均勻,取到的分位點會有偏差。這是啥意思呢? 再來一個圖(這個圖得看明白了):
    在這裏插入圖片描述
    這其實就是作者提出的那種找候選節點的方式(分桶的思路),明白了這個圖之後,下面就是解釋一下上面這個圖的細節:第一個就是hih_i是啥?它爲啥就能代表樣本對降低loss的貢獻程度? 第二個問題就是這個bin是怎麼分的,爲啥是0.6一個箱?

    下面從第一個問題開始,揭開hih_i的神祕面紗, 其實hih_i上面已經說過了,損失函數在樣本ii處的二階導數啊!還記得開始的損失函數嗎?
    Obj(t)i=1n[gift(xi)+12hift2(xi)]+i=1tΩ(fi)\operatorname{Obj}^{(t)} \approx \sum_{i=1}^{n}\left[g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)\right]+\sum_{i=1}^{t} \Omega\left(f_{i}\right)
    就是這個hih_i, 那麼你可能要問了, 爲啥它就能代表第ii個樣本的權值啊?這裏再拓展一下吧,我們在引出xgboost的時候說過,GBDT這個系列都是聚焦在殘差上面,但是我們單看這個目標函數的話並沒有看到什麼殘差的東西對不對? 其實這裏這個損失函數還可以進一步化簡的(和上面的化簡不一樣,上面的化簡是把遍歷樣本轉到了遍歷葉子上得到基於決策樹的目標函數,這裏是從目標函數本身出發進行化簡):
    L(t)i=1n[l(yi,y^i(t1))+gift(xi)+12hift2(xi)]+Ω(ft)=i=1n[12hi2gift(xi)hi+12hift2(xi)]+Ω(ft)=i=1n12hi[2gihift(xi)+ft2(xi)]+Ω(ft)=i=1n12hi[(2gihift(xi)+ft2(xi)+(gihi)2)(gihi)2]+Ω(ft)=i=1n12hi[(ft(xi)+gihi)2]+Ω(ft)+Constant=i=1n12hi(ft(xi)(gihi))2+Ω(ft)+Constant\begin{aligned} \mathcal{L}^{(t)} & \simeq \sum_{i=1}^{n}\left[l\left(y_{i}, \hat{y}_{i}^{(t-1)}\right)+g_{i} f_{t}\left(\mathbf{x}_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(\mathbf{x}_{i}\right)\right]+\Omega\left(f_{t}\right) \\ &=\sum_{i=1}^{n}\left[\frac{1}{2} h_{i} \cdot \frac{2 \cdot g_{i} f_{t}\left(\mathbf{x}_{i}\right)}{h_{i}}+\frac{1}{2} h_{i} \cdot f_{t}^{2}\left(\mathbf{x}_{i}\right)\right]+\Omega\left(f_{t}\right) \\ &=\sum_{i=1}^{n} \frac{1}{2} h_{i}\left[2 \cdot \frac{g_{i}}{h_{i}} \cdot f_{t}\left(\mathbf{x}_{i}\right)+f_{t}^{2}\left(\mathbf{x}_{i}\right)\right]+\Omega\left(f_{t}\right) \\ &=\sum_{i=1}^{n} \frac{1}{2} h_{i}\left[\left(2 \cdot \frac{g_{i}}{h_{i}} \cdot f_{t}\left(\mathbf{x}_{i}\right)+f_{t}^{2}\left(\mathbf{x}_{i}\right)+\left(\frac{g_{i}}{h_{i}}\right)^{2}\right)-\left(\frac{g_{i}}{h_{i}}\right)^{2}\right]+\Omega\left(f_{t}\right) \\ &=\sum_{i=1}^{n} \frac{1}{2} h_{i}\left[\left(f_{t}\left(\mathbf{x}_{i}\right)+\frac{g_{i}}{h_{i}}\right)^{2}\right]+\Omega\left(f_{t}\right)+Constant \\ &=\sum_{i=1}^{n} \frac{1}{2} h_{i}\left(f_{t}\left(\mathbf{x}_{i}\right)-\left(-\frac{g_{i} }{ h_{i}}\right)\right)^{2}+\Omega\left(f_{t}\right)+Constant \end{aligned}
    這樣化簡夠簡潔明瞭了吧, 你看到殘差的身影了嗎? 後面的每一個分類器都是在擬合每個樣本的一個殘差gihi-\frac{g_i}{h_i}, 其實把上面化簡的平方損失函數拿過來就一目瞭然了。而前面的hih_i可以看做計算殘差時某個樣本的重要性,即每個樣本對降低loss的貢獻程度。第一個問題說的聽清楚了吧 😉

    PS:這裏加點題外話,Xgboost引入了二階導之後,相當於在模型降低殘差的時候給各個樣本根據貢獻度不同加入了一個權重,這樣就能更好的加速擬合和收斂,GBDT只用到了一階導數,這樣只知道梯度大的樣本降低殘差效果好,梯度小的樣本降低殘差不好(這個原因我會放到Lightgbm的GOSS那裏說到),但是好與不好的這個程度,在GBDT中無法展現。而xgboost這裏就通過二階導可以展示出來,這樣模型訓練的時候就有數了。

    下面再解釋第二個問題,這個分箱是怎麼分的? 比如我們定義一個數據集Dk={(x1k,h1),(x2k,h2),...,(xnk,hn)}D_k={\{(x_{1k}, h_1),(x_{2k}, h_2),...,(x_{nk}, h_n)\}}代表每個訓練樣本的第kk個特徵的取值和二階梯度值,那麼我們可以有一個排名函數rk(z)r_k(z)
    rk(z)=1(x,h)Dkh(x,h)Dk,x<zhr_{k}(z)=\frac{1}{\sum_{(x, h) \in \mathcal{D}_{k}} h} \sum_{(x, h) \in \mathcal{D}_{k}, x<z} h
    這裏的zz代表特徵值小於zz的那些樣本。這個排名函數表示特徵值小於z的樣本的貢獻度比例。假設上面圖中,z是第一個候選點,那麼rk(z)=13r_k(z)=\frac{1}{3}, 這個東西的目的就是去找相對準確的候選點{sk1,sk2,...skl}\{s_{k1}, s_{k2}, ...s_{kl}\}, 這裏的sk1=minixik,skl=maxixiks_{k 1}=\min _{i} \mathbf{x}_{i k}, s_{k l}=\max _{i} \mathbf{x}_{i k}, 而相鄰兩個桶之間樣本貢獻度的差距應滿足下面這個函數:
    rk(sk,j)rk(sk,j+1)<ϵ\left|r_{k}\left(s_{k, j}\right)-r_{k}\left(s_{k, j+1}\right)\right|<\epsilon
    這個ϵ\epsilon控制每個桶中樣本貢獻度比例的大小,其實就是貢獻度的分位點。我們自己設定。比如在上面圖中我們設置了ϵ=1/3\epsilon=1/3, 這意味着每個桶樣本貢獻度的比例是1/3(貢獻度的1/3分位點), 而所有的樣本貢獻度總和是1.8, 那麼每個箱貢獻度是0.6(1.8ϵ1.8*\epsilon),分爲3(1ϵ\frac{1}{\epsilon})個箱, 上面這些公式看起來挺複雜,可以計算起來很簡單,就是計算一下總的貢獻度, 然後指定ϵ\epsilon, 兩者相乘得到每個桶的貢獻度進行分桶即可。這樣我們就可以確定合理的候選切分點,然後進行分箱了。

    到這終於把這一塊描述完了, 有點多,稍微理一理邏輯,前面那一部分是圍繞着如何建立一棵樹進行的,即採用貪心的方式從根節點開始一層層的建立樹結構(每一層爭取最優),然後就是建樹過程中一個關鍵的問題:如何尋找最優切分點,給出了最優切分點算法,基於這個算法就可以建立樹了。後面這一部分是一個優化的過程,提出了一種Weight Quantile Sketch的算法,這個算法可以將原來的分割點進行分桶,然後找到合適的候選分裂點,這樣可以減少遍歷時嘗試的分裂點的數量,是xgboost相比於GBDT做出的切分點優化策略,現在知道爲啥xgboost要快了吧,因爲xgboost尋找切分點的時候不用遍歷所有的,而是隻看候選點就可以了。而且在特徵上,xgboost是可以並行處理的。這樣xgboost的建樹過程及優化策略基本上就是這些了,當然這裏面還有很多的細節,由於篇幅的原因就不寫在這裏了。

  6. 利用新的決策樹預測樣本值,並累加到原來的值上
    若干個決策樹是通過加法訓練的, 所謂加法訓練,本質上是一個元算法,適用於所有的加法模型,它是一種啓發式算法。運用加法訓練,我們的目標不再是直接優化整個目標函數,而是分步驟優化目標函數,首先優化第一棵樹,完了之後再優化第二棵樹,直至優化完K棵樹。整個過程如下圖所示:

    在這裏插入圖片描述

    上圖中會發現每一次迭代得到的新模型前面有個η\eta(這個是讓樹的葉子節點權重乘以這個係數), 這個叫做收縮率,這個東西加入的目的是削弱每棵樹的作用,讓後面有更大的學習空間,有助於防止過擬合。也就是,我不完全信任每一個殘差樹,每棵樹只學到了模型的一部分,希望通過更多棵樹的累加來來彌補,這樣讓這個讓學習過程更平滑,而不會出現陡變。 這個和正則化防止過擬合的原理不一樣,這裏是削弱模型的作用,而前面正則化是控制模型本身的複雜度。

好了, 到這裏爲止,xgboost的數學原理部分就描述完了,希望我描述清楚了吧。簡單的回顧一下上面的過程吧: xgboost是好多弱分類器的集成,訓練弱分類器的策略就是儘量的減小殘差,使得答案越來越接近正確答案。 xgboost的精髓部分是目標函數的Taylor化簡,這樣就引入了損失函數的一階和二階導數。然後又把樣本的遍歷轉成了對葉子節點的遍歷,得到了最終的目標函數。這個函數就是衡量一棵樹好壞的標準。在建樹過程中,xgboost採用了貪心策略,並且對尋找分割點也進行了優化。基於這個,纔有了後面的最優點切分建立一棵樹的過程。 xgboost訓練的時候,是通過加法進行訓練,也就是每一次只訓練一棵樹出來, 最後的預測結果是所有樹的加和表示。

關於xgboost,依然還有很多的細節沒有說到,具體的去看論文吧。下面,我們就進行xgboost的實戰部分, 這裏我們簡單的做一個分類任務, 主要是看看xgboost主要怎麼用, 尤其是在一個數據競賽中(這次重點總結了一些用法)。

3. Xgboost實戰二分類

安裝::默認可以通過pip安裝,若是安裝不上可以通過https://www.lfd.uci.edu/~gohlke/pythonlibs/網站下載相關安裝包,將安裝包拷貝到Anacoda3的安裝目錄的Scrripts目錄下, 然後pip install 安裝包安裝

3.1 xgboost的基本使用

我們使用xgboost做一個分類任務,可以直接使用xgboost。

# 0 1:1 9:1 19:1 21:1 24:1 34:1 36:1 39:1 42:1 53:1 56:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 106:1 117:1 122:1
# 1 3:1 9:1 19:1 21:1 30:1 34:1 36:1 40:1 41:1 53:1 58:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 106:1 118:1 124:1
# 0 1:1 9:1 20:1 21:1 24:1 34:1 36:1 39:1 41:1 53:1 56:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 106:1 117:1 122:1
# 0 3:1 9:1 19:1 21:1 24:1 34:1 36:1 39:1 51:1 53:1 56:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 106:1 116:1 122:1
# 0 4:1 7:1 11:1 22:1 29:1 34:1 36:1 40:1 41:1 53:1 58:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 105:1 119:1 124:1
# 0 3:1 10:1 20:1 21:1 23:1 34:1 37:1 40:1 42:1 54:1 55:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 106:1 118:1 126:1
# 1 3:1 9:1 11:1 21:1 30:1 34:1 36:1 40:1 51:1 53:1 58:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 106:1 117:1 124:1

"""上面是libsvm的數據存儲格式, 也是一種常用的格式,存儲的稀疏數據。 
第一列是label. a:b a表示index, b表示在該index下的數值, 這就類似於one-hot"""

import numpy as np
import scipy.sparse    # 稀疏矩陣的處理
import pickle
import xgboost as xgb

# libsvm format data 的讀入方式, 直接用xgb的DMatrix
dtrain = xgb.DMatrix('./xgbdata/agaricus.txt.train')
dtest = xgb.DMatrix('./xgbdata/agaricus.txt.test')

下面我們進行參數設置:
關於xgboost的參數,詳細的可以看上面的參數說明,這裏拿分類器來說,解釋一些參數:

xgb1 = XGBClassifier( learning_rate =0.1, n_estimators=1000, max_depth=5, min_child_weight=1, gamma=0, subsample=0.8, colsample_bytree=0.8, objective= ‘binary:logistic’, nthread=4, scale_pos_weight=1, seed=27)

  • ‘booster’:‘gbtree’, 這個指定基分類器
  • ‘objective’: ‘multi:softmax’, 多分類的問題, 這個是優化目標,必須得有,因爲xgboost裏面有求一階導數和二階導數,其實就是這個。
  • ‘num_class’:10, 類別數,與 multisoftmax 並用
  • ‘gamma’:損失下降多少才進行分裂, 控制葉子節點的個數
  • ‘max_depth’:12, 構建樹的深度,越大越容易過擬合
  • ‘lambda’:2, 控制模型複雜度的權重值的L2正則化項參數,參數越大,模型越不容易過擬合。
  • ‘subsample’:0.7, 隨機採樣訓練樣本
  • ‘colsample_bytree’:0.7, 生成樹時進行的列採樣
  • ‘min_child_weight’:3, 孩子節點中最小的樣本權重和。如果一個葉子節點的樣本權重和小於min_child_weight則拆分過程結束
  • ‘silent’:0 ,設置成1則沒有運行信息輸出,最好是設置爲0.
  • ‘eta’: 0.007, 如同學習率
  • ‘seed’:1000,
  • ‘nthread’:7, cpu 線程數

當然不需要全記住,常用的幾個記住即可。可以結合着上面的數學原理,看看哪個參數到底對於xgboost有什麼作用,這樣利於調參。 設置好參數,訓練測試就行了,使用起來和sklearn的模型非常像

"""paramet setting"""
param = {
    'max_depth': 2,
    'eta': 1, 
    'silent': 1,
    'objective': 'binary:logistic'
}
watch_list = [(dtest, 'eval'), (dtrain, 'train')]  # 這個是觀測的時候在什麼上面的結果  觀測集
num_round = 5
model = xgb.train(params=param, dtrain=dtrain, num_boost_round=num_round, evals=watch_list)

然後就是預測:

"""預測"""
pred = model.predict(dtest)    # 這裏面表示的是正樣本的概率是多少

from sklearn.metrics import accuracy_score
predict_label = [round(values) for values in pred]
accuracy_score(labels, predict_label)   # 0.993

模型的保存瞭解一下:

"""兩種方式: 第一種, pickle的序列化和反序列化"""
pickle.dump(model, open('./model/xgb1.pkl', 'wb'))
model1 = pickle.load(open('./model/xgb1.pkl', 'rb'))
model1.predict(dtest)

"""第二種模型的存儲與導入方式 - sklearn的joblib"""
from sklearn.externals import joblib
joblib.dump(model, './model/xgb.pkl')
model2 = joblib.load('./model/xgb.pkl')
model2.predict(dtest)

3.2 交叉驗證 xgb.cv

# 這是模型本身的參數
param = {'max_depth':2, 'eta':1, 'silent':1, 'objective':'binary:logistic'}
num_round = 5   # 這個是和訓練相關的參數

xgb.cv(param, dtrain, num_round, nfold=5, metrics={'error'}, seed=3)

3.3 調整樣本權重

這個是針對樣本不平衡的情況,可以在訓練時設置樣本的權重, 訓練的時候設置fpreproc這個參數, 相當於在訓練之前先對樣本預處理。

# 這個函數是說在訓練之前,先做一個預處理,計算一下正負樣本的個數,然後加一個權重,解決樣本不平衡的問題
def preproc(dtrain, dtest, param): 
    labels = dtrain.get_label()
    ratio = float(np.sum(labels==0)) / np.sum(labels==1)
    param['scale_pos_ratio'] = ratio
    return (dtrain, dtest, param)

# 下面我們在做交叉驗證, 指明fpreproc這個參數就可以調整樣本權重
xgb.cv(param, dtrain, num_round, nfold=5, metrics={'auc'}, seed=3, fpreproc=preproc)

3.4 自定義目標函數(損失函數)

如果在一個比賽中,人家給了自己的評判標準,那麼這時候就需要用人家的這個評判標準,這時候需要修改xgboost的損失函數, 但是這時候請注意一定要提供一階和二階導數

# 自定義目標函數(log似然損失),這個是邏輯迴歸的似然損失。 交叉驗證
# 注意: 需要提供一階和二階導數

def logregobj(pred, dtrain):
    labels = dtrain.get_label()
    pred = 1.0 / (1+np.exp(-pred))    # sigmoid函數
    grad = pred - labels
    hess = pred * (1-pred)
    return grad, hess     # 返回一階導數和二階導數

def evalerror(pred, dtrain):
    labels = dtrain.get_label()
    return 'error', float(sum(labels!=(pred>0.0)))/len(labels)

訓練的時候,把損失函數指定就可以了:

param = {'max_depth':2, 'eta':1, 'silent':1}

# 自定義目標函數訓練
model = xgb.train(param, dtrain, num_round, watch_list, logregobj, evalerror)

# 交叉驗證
xgb.cv(param, dtrain, num_round, nfold=5, seed=3, obj=logregobj, feval=evalerror)

3.6 用前n棵樹做預測 ntree_limit

太多的樹可能發生過擬合,這時候我們可以指定前n棵樹做預測, 預測的時候設置ntree_limit這個參數

# 前1棵
pred1 = model.predict(dtest, ntree_limit=1)
evalerror(pred2, dtest)

3.7 畫出特徵重要度 plot_importance

from xgboost import plot_importance
plot_importance(model, max_num_features=10)

3.8 同樣,也可以用sklearn的GridSearchCV調參

from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import StratifiedKFold

model = XGBClassifier()
learning_rate = [0.0001, 0.001, 0.1, 0.2, 0.3]
param_grid = dict(learning_rate=learning_rate)
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=7)
grid_search = GridSearchCV(model, param_grid, scoring="neg_log_loss", n_jobs=-1, cv=kfold)
grid_result = grid_search.fit(x_train, y_train)

print("best: %f using %s" %(grid_result.best_score_, grid_result.best_params_))

means = grid_result.cv_results_['mean_test_score']
params = grid_result.cv_results_['params']

for mean, param in zip(means, params):
    print("%f  with: %r" % (mean, param))

好了,實戰部分就整理這麼多吧, 重點在於怎麼使用,xgboost使用起來和sklearn的模型也是非常像, 也是.fit(), .predict()方法,只不過xgboost的參數很多,這個調起來會比較複雜, 但是懂了原理之後,至少每個參數是幹啥的就瞭解了,關於調參的技術, 得從經驗中多學習,多嘗試,多總結才能慢慢修煉出來。

4. 總結

哇,終於完了,到這裏,終於把xgboost的一些知識說清楚了, 每一次不知不覺就會這麼多字, 可能是因爲這個算法太重要了吧,所以多寫了點, 趕緊回顧一下: 首先, 我們從集成算法開始講起,回顧了一下AdaBoost,GBDT, 然後引出了xgboost, 我們知道同屬boosting流派,但集成策略又有不同, 即使集成策略類似,那麼得到最後結果的方式又不同。 但對比之中,我們能更加體會它們的原理。 其次,我們從數學原理的角度剖析了一下xgboost, 看到了它的目標函數,看到了如何生成一棵樹,看到了如何Taylor化簡,知道了爲什麼需要損失函數的一二階導數,也明白了爲啥這個算法這麼快。 最後,我們通過實戰一個二分類問題,見識到了xgboost的代碼實現,基本使用和一些高級策略。

下面看看xgboost相比於GBDT有哪些優點(面試的時候可能會涉及):

  • 精度更高:GBDT只用到一階泰勒, 而xgboost對損失函數進行了二階泰勒展開, 一方面爲了增加精度, 另一方面也爲了能夠自定義損失函數,二階泰勒展開可以近似大量損失函數
  • 靈活性更強:GBDT以CART作爲基分類器,而Xgboost不僅支持CART,還支持線性分類器,另外,Xgboost支持自定義損失函數,只要損失函數有一二階導數。
  • 正則化:xgboost在目標函數中加入了正則,用於控制模型的複雜度。有助於降低模型方差,防止過擬合。正則項裏包含了樹的葉子節點個數,葉子節點權重的L2範式。
  • Shrinkage(縮減):相當於學習速率。這個主要是爲了削弱每棵樹的影響,讓後面有更大的學習空間,學習過程更加的平緩
  • 列抽樣:這個就是在建樹的時候,不用遍歷所有的特徵了,可以進行抽樣,一方面簡化了計算,另一方面也有助於降低過擬合
  • 缺失值處理:這個是xgboost的稀疏感知算法,加快了節點分裂的速度
  • 並行化操作:塊結構可以很好的支持並行計算

上面的這些優點,我在描述的時候基本上都涉及到了,正是因爲xgboost有了這些優點,才讓它變得非常火,堪稱神器了現在,但是xgboost真的perfect了嗎? 正所謂金無足赤,人無完人, xgboost也同樣如此,比如雖然利用了預排序和近似算法可以降低尋找最優分裂點的計算量,但在節點分裂過程中仍需要遍歷整個數據集。預排序過程的空間複雜度過高,不僅需要存儲特徵值,還需要存儲特徵對應樣本梯度統計值的索引,相當於消耗了兩倍的內存。所以在內存和計算方面還是有很大的優化空間的。那麼xgboost還可以在哪些角度進行優化呢? 後面通過lightgbm的故事再說給你聽 😉

xgboost的故事就先講到這裏了,希望對你有所幫助,當然還有很多的細節沒有提到,本文只是拋磚引玉,具體的建議去看看原文,畢竟這個算法還是超級重要的,面試的時候也會摳得很細, 不看原文的話有些精華get不到。

參考

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