【Datawhale|天池】心跳信号分类预测 (4) - 模型 之 XGBoost
本文主要分享自己總結的關于 xgboost 的筆記。
基礎知識
基學習器 CART
Regression:L(yi,y^i)=(yi?f(xi))2Classification:Gini(D)=∑k=1Kpk(1?pk)=1?∑k=1Kpk2Regression: L(y_i, \hat y_i) = (y_i - f(x_i))^2 \\ Classification: Gini(D) = \sum_{k=1}^K p_k(1-p_k) = 1 - \sum_{k=1}^K p_k^2 Regression:L(yi?,y^?i?)=(yi??f(xi?))2Classification:Gini(D)=k=1∑K?pk?(1?pk?)=1?k=1∑K?pk2?
回歸單樣本的損失函數,和分類時在數據集D上的基尼指數。f 表示樹,f(x) 則是 x 對應的葉子節點的值。
對于分類問題,在 CART 分割時,我們按照 Gini 指數最小來確定分割點的位置。即:
cutpoint(A)=argminv∈AV∑i=12∣Di∣∣D∣Gini(Di)bestA,best_v=argminA∈features[argminv∈AV∑i=12∣Di∣∣D∣Gini(Di)]cutpoint(A) = arg \; min_{v \in A_V} \; \sum_{i=1}^2 \frac {|D_i|}{|D|} Gini(D_i) \\ bestA, best\_v = arg \; min_{A \in features} [ arg \; min_{v \in A_V} \; \sum_{i=1}^2 \frac {|D_i|}{|D|} Gini(D_i) ] cutpoint(A)=argminv∈AV??i=1∑2?∣D∣∣Di?∣?Gini(Di?)bestA,best_v=argminA∈features?[argminv∈AV??i=1∑2?∣D∣∣Di?∣?Gini(Di?)]
XGBoost 的目標函數:
Obj=∑i=1nL(yi,y^i)+∑k=1KΩ(fk)whereL(yi,y^i)=(yi?f(xi))2Ω(f)=γT+12λ∣∣ω∣∣2=γT+12λ∑j=1T∣∣ωj∣∣2Obj = \sum_{i=1}^nL(y_i, \hat y_i) + \sum_{k=1}^K \Omega (f_k) \\ where \quad L(y_i, \hat y_i) = (y_i - f(x_i))^2 \\ \Omega (f) = \gamma T + \frac12 \lambda ||\omega||^2 = \gamma T + \frac12 \lambda \sum_{j=1}^T ||\omega_j||^2 Obj=i=1∑n?L(yi?,y^?i?)+k=1∑K?Ω(fk?)whereL(yi?,y^?i?)=(yi??f(xi?))2Ω(f)=γT+21?λ∣∣ω∣∣2=γT+21?λj=1∑T?∣∣ωj?∣∣2
假設有 n 個樣本,K 棵樹。目標函數由兩部分構成,第一部分用來衡量預測分數和真實分數的差距,另一部分則是正則化項。正則化項同樣包含兩部分,T 表示葉子結點的個數,w 表示葉子節點的分數。γ\gammaγ 可以控制葉子結點的個數,λ 可以控制葉子節點的分數不會過大,防止過擬合。
自問自答
常用的參數都有哪些?
一開始,當然是要先設置 booster,一版直接默認用 gbtree 就可以了,另外還有兩個常用的全局設置項是 nthread、verbosity。
接下來,幾個常用的能影響到模型擬合能力的參數是:
- num_boost_round(n_estimators)
- max_depth(樹的深度)
- eta(learning_rate)
然后,有兩個正則化、類似于用來控制預剪枝的參數是:
- gamma(分裂時目標函數增益的閾值)
- min_child_weight(分裂時孩子權重之和的閾值)
另外幾個用于控制正則項的參數是:
- lambda(這是葉子節點 L2 正則項的系數)
- alpha(這是葉子節點 L1 正則項的系數)
- subsample(訓練數據"被隨機采樣用來訓練"的比例)
- colsample_bytree(訓練數據的所有特征"被隨機選擇用來訓練一棵樹"的比例)
對于數據不平衡問題,可能還用到的重要參數是:
- scale_pos_weight(一般設置成正例和負例樣本數量的比值)
- max_delta_step
如何通過優化目標函數值來計算葉子節點權重?
Obj=∑i=1nL(yi,y^i)+∑k=1KΩ(fk)whereL(yi,y^i)=(yi?f(xi))2Ω(f)=γT+12λ∣∣ω∣∣2=γT+12λ∑j=1Twj2Obj = \sum_{i=1}^nL(y_i, \hat y_i) + \sum_{k=1}^K \Omega (f_k) \\ where \quad L(y_i, \hat y_i) = (y_i - f(x_i))^2 \\ \Omega (f) = \gamma T + \frac12 \lambda ||\omega||^2 = \gamma T + \frac12 \lambda \sum_{j=1}^T w_j^2 Obj=i=1∑n?L(yi?,y^?i?)+k=1∑K?Ω(fk?)whereL(yi?,y^?i?)=(yi??f(xi?))2Ω(f)=γT+21?λ∣∣ω∣∣2=γT+21?λj=1∑T?wj2?
那么,假設樹的結構已經確定,每個葉子節點對應于一部分樣本,即可以將決策樹表示為一個映射 q:Rd→{1,2,...,T}q : R^d \to \{1, 2, ..., T\}q:Rd→{1,2,...,T} ,每個 d 維的樣本 xix_ixi? 可以映射到決策樹 T 個葉子節點中的一個,要如何計算對應的葉子節點權重和相應的目標函數值呢?
一種簡單粗暴的方法是,對于分類問題,將葉子節點的值設置為該節點樣本集中樣本數量最多類別;對于回歸問題,則設置為標簽值的平均。但是,這就完全忽略目標函數的存在了,我們的目標是要最小化目標函數。
以下對目標函數進行一些化簡:
假設現在學習到了第 t 棵樹,記為 ft(x)f_t(x)ft?(x) ,那么 y^t=y^t?1+ft(x)\hat y^t = \hat y^{t-1} + f_t(x)y^?t=y^?t?1+ft?(x) 。我們的目標是使得 y^t\hat y^ty^?t 盡量等于 yyy ,即 $ \hat y^{t-1} + f_t(x)$ 盡量趨近于 yyy ,也就是讓 ft(x)f_t(x)ft?(x) 趨近于 y?y^t?1y - \hat y^{t-1}y?y^?t?1 ,換言之, y?y^t?1y - \hat y^{t-1}y?y^?t?1 就是當前這棵樹的擬合目標,或者說樣本的標簽值(說起來,這和 GBDT 其實是一樣的,只是這里目標函數不同,導致最后擬合出來的葉子節點的權重會有所不同)。代入目標函數中:
Obj(t)=∑i=1nL(yi,y^it?1+ft(xi))+Ω(ft)Obj^{(t)} = \sum_{i=1}^n L(y_i, \hat y_i^{t-1} + f_t(x_i)) + \Omega(f_t) Obj(t)=i=1∑n?L(yi?,y^?it?1?+ft?(xi?))+Ω(ft?)
我們知道有泰勒公式二階展開:
f(x+Δx)=f(x)+f′(x)?Δx+12f′′(x)?(Δx)2f(x + \Delta x) = f(x) + f'(x)·\Delta x + \frac 12 f''(x)·(\Delta x)^2 f(x+Δx)=f(x)+f′(x)?Δx+21?f′′(x)?(Δx)2
將 yit?1y_i^{t-1}yit?1? 看作上式中的 x,ft(x)f_t(x)ft?(x) 看作上式中的 Δx\Delta xΔx ,L(yi,y^it?1)L(y_i, \hat y_i^{t-1})L(yi?,y^?it?1?) 看作上式的 f(x)f(x)f(x) 。那么目標函數可以改寫成:
Obj(t)≈∑i=1n[L(yi,y^it?1)+gift(xi)+12hift2(xi)]+Ω(ft)Obj^{(t)} \approx \sum_{i=1}^{n} [L(y_i, \hat y_i^{t-1}) + g_i f_t(x_i) + \frac 12 h_i f_t^2 (x_i)] + \Omega (f_t) Obj(t)≈i=1∑n?[L(yi?,y^?it?1?)+gi?ft?(xi?)+21?hi?ft2?(xi?)]+Ω(ft?)
這里的 gi,hig_i, h_igi?,hi? 分別是 L(yi,y^it?1)L(y_i, \hat y_i^{t-1})L(yi?,y^?it?1?) 關于 y^it?1\hat y_i^{t-1}y^?it?1? 的一階導和二階導。若以 平方誤差 作為損失函數,即 L(yi,y^it?1)=(yi?y^it?1)2L(y_i, \hat y_i^{t-1}) = (y_i - \hat y_i^{t-1})^2L(yi?,y^?it?1?)=(yi??y^?it?1?)2 ,那么有 gi=2(y^it?1?yi)g_i = 2(\hat y_i^{t-1} - y_i)gi?=2(y^?it?1??yi?) , hi=2h_i = 2hi?=2 。
由于 L(yi,y^it?1)L(y_i, \hat y_i^{t-1})L(yi?,y^?it?1?) 是常數項不影響我們把目標函數最小化,所以上式可以簡寫成:
Obj(t)≈∑i=1n[gift(xi)+12hift2(xi)]+Ω(ft)Obj^{(t)} \approx \sum_{i=1}^{n} [g_i f_t(x_i) + \frac 12 h_i f_t^2 (x_i)] + \Omega (f_t) Obj(t)≈i=1∑n?[gi?ft?(xi?)+21?hi?ft2?(xi?)]+Ω(ft?)
假設樹的結構已經確定,對目標函數進一步化簡:
設每個葉子節點的權重為 wj,j∈1,2,...,Tw_j, j \in {1, 2, ..., T}wj?,j∈1,2,...,T ,那么可以將決策樹表示成 wq(x)w_{q(x)}wq(x)? ,即每個樣本會映射到第 q(x) 個葉子節點對應的權值。假設 Ij={i∣q(xi)=j}I_j = \{i|q(x_i) = j\}Ij?={i∣q(xi?)=j} 為第 j 個葉子節點的樣本集合,則目標函數可進一步化為:
Obj(t)≈∑i=1n[gift(xi)+12hift2(xi)]+Ω(ft)=∑i=1n[giwq(xi)+12hiwq(xi)2]+γT+12λ∑j=1Twj2=∑j=1T[∑i∈Ijgiwj+12∑i∈Ijhiwj2+12λwj2]+γT=∑j=1T[(∑i∈Ijgi)wj+12(∑i∈Ijhi+λ)wj2]+γTObj^{(t)} \approx \sum_{i=1}^{n} [g_i f_t(x_i) + \frac 12 h_i f_t^2 (x_i)] + \Omega (f_t) \\ = \sum_{i=1}^n [g_i w_{q(x_i)} + \frac 12 h_i w_{q(x_i)}^2] + \gamma T + \frac12 \lambda \sum_{j=1}^T w_j^2 \\ = \sum_{j=1}^T [\sum_{i \in I_j} g_i w_j + \frac 12 \sum_{i \in I_j} h_i w_j^2 + \frac 12 \lambda w_j^2] + \gamma T \\ = \sum_{j=1}^T [(\sum_{i \in I_j} g_i) w_j + \frac 12 (\sum_{i \in I_j} h_i + \lambda) w_j^2] + \gamma T Obj(t)≈i=1∑n?[gi?ft?(xi?)+21?hi?ft2?(xi?)]+Ω(ft?)=i=1∑n?[gi?wq(xi?)?+21?hi?wq(xi?)2?]+γT+21?λj=1∑T?wj2?=j=1∑T?[i∈Ij?∑?gi?wj?+21?i∈Ij?∑?hi?wj2?+21?λwj2?]+γT=j=1∑T?[(i∈Ij?∑?gi?)wj?+21?(i∈Ij?∑?hi?+λ)wj2?]+γT
我們之前樣本的集合,現在都改寫成葉子結點的集合,由于一個葉子結點有多個樣本存在,因此才有了 ∑i∈Ijgi\sum_{i \in I_j} g_i∑i∈Ij??gi? 和 ∑i∈Ijhi\sum_{i \in I_j} h_i∑i∈Ij??hi? 這兩項,把這兩項簡寫為 Gj,HjG_j, H_jGj?,Hj?,于是上式變成:
Obj(t)=∑j=1T[Gjwj+12(Hj+λ)wj2]+γTObj^{(t)} = \sum_{j=1}^T [G_j w_j + \frac 12 (H_j + \lambda) w_j^2] + \gamma T Obj(t)=j=1∑T?[Gj?wj?+21?(Hj?+λ)wj2?]+γT
假設樹的結構已經確定,通過最小化目標函數,求解葉子節點權重:
在當前假設下,上式中只有 www 是變量,這是一個凸函數,為了使其取得最小值,只需要令目標函數一階導為 0,便可求得:
w^j=?GjHj+λ\hat w_j = - \frac {G_j}{H_j + \lambda} w^j?=?Hj?+λGj??
通過求得的葉子節點權重,計算目標函數值:
Obj(t)=?12∑j=1TGj2Hj+λ+γTObj^{(t)} = - \frac 12 \sum_{j=1}^T \frac {G_j^2}{H_j + \lambda} + \gamma T Obj(t)=?21?j=1∑T?Hj?+λGj2??+γT
學習的過程是怎么進行的?
先從總體上進行描述:
生成一棵樹的具體過程是這樣的:
這其實是一種貪心策略,因為我們在分割的每一步都盡量使收益最大,即每一步都得到局部最優解,但無法保證一整棵樹的收益最大。
本質是梯度提升樹,和 GBDT 有什么區別?
我們說 GBDT,一般指的是基于 殘差(y-f(x)) 的梯度提升樹,實際上,如果以誤差的平方作為損失函數,那么殘差其實就是損失函數關于 f(x) 的負梯度,所以擬合殘差就相當于擬合了負梯度(這其實和神經網絡中通過梯度下降來訓練參數類似),這就是為什么稱之為梯度提升樹。
xgboost 的在 GBDT 的基礎上主要改進了兩點,其實從 目標函數 中都可以看出來。
都說 xgboost 效果好,為什么好?
從偏差-方差的角度來理解:
訓練速度快,為什么快?
論文閱讀筆記
2. 提升樹 TREE BOOSTING IN A NUTSHELL
2.1 正則項 Regularized Learning Objective
本節提出了目標函數。其中的正則化項可以防止過擬合。
2.2 泰勒二階展開 Gradient Tree Boosting
對目標函數二階泰勒展開。通過最小化目標函數,可以得到葉子節點的權值、以及目標函數值。
分裂前后目標函數值的增益也很容易計算。
2.3 學習率和特征采樣(Shrinkage and Column Subsampling)
Shrinkage:
類似于隨機梯度下降優化算法中的學習率,Shrinkage 具體做法是對每一輪生成的樹的葉子節點權重乘上一個系數 η\etaη (即希臘字母eta),這就是為什么 XGBoost 包中學習率參數名稱為 eta。
Column Subsampling:
和隨機森林里使用的特征采樣類似,這是一個 bagging 思想的應用,而且也是第一次被使用在 Tree Boosting 里。效果甚至比傳統的 row sub-sampling(樣本采樣)好。而且,特征采樣還能加速訓練。
3. 分裂點尋找算法 SPLIT FINDING ALGORITHMS
3.1 精確貪婪算法(Exact Greedy Algorithm)
每一輪boost,生成一棵樹。由于枚舉所有可能的樹結構是NP-hard問題,不可能完成,所以實際做法是,在每次進行節點分裂的時候,以最大化分裂增益(loss reduction)的方式來進行節點的分裂。這是一種貪婪的策略。
事先對訓練樣本按照特征值排序,可以加速訓練。
每一層,對于某一個特征,基于特征列 Block 結構,并行進行所有葉子節點的分裂點尋找。并行的過程,我現在的理解是這樣的:
對于某個線程,在順序遍歷某排好序的特征列時,對于每一個特征值,我們都能知道它對應的哪個樣本(記為第i個)、哪個葉子節點(記為第 j 個),也因此就能取得該樣本的梯度 gi,hig_i, h_igi?,hi?,并根據 j 計算出 GjL,HjLG_{jL}, H_{jL}GjL?,HjL? 和 GjR,HjRG_{jR}, H_{jR}GjR?,HjR?。遍歷一趟下來,所有葉子節點在該特征上的最大增益就都出來了。
我之前卡在了這里:就是遍歷時,對于每一個特征值,都要計算一遍所有葉子節點的分裂增益,開銷應該很大。現在才搞懂,其實只需要計算該特征所在的葉子節點的分裂增益就可以了。這里說的“對于每一個特征值”,指的是每個分裂點前面緊挨著的那個特征值。
你想,對于某個 leaf node ,你只記錄了該節點中包含了哪些樣本(索引),你怎么取得這個節點中這些樣本的該特征的排序值呢?反正我想了好久就是想不通有啥高效的辦法。在這里卡了好久。
所以,其實相當于沒有遍歷葉子節點這一說,只是,在遍歷特征的所有分裂點的過程中,隱式地完成了對所有葉子節點的遍歷。這也是為什么 xgb 總是滿二叉樹。
參考:XGBoost原理及并行實現 中的 “method 3” 部分。
3.2 近似算法(Approximate Algorithm)
當數據無法一起加載進內存,或者在分布式環境下,精確貪婪算法效率很低。即便正常情況下,有些連續值特征的取值個數可能非常多,這樣計算起來開銷很大,而且還容易過擬合。所以可以使用分桶的方式,把特征的取值分割成確定數量的部分。即,我們對每個特征都 propose 對應的候選分裂點們(由加權分位點算法生成,每次生成一系列候選分裂點稱作一次proposal),可用于每棵樹(global),或者每次分裂(local)。分桶后,需要匯總每個桶內的 gig_igi? 和 hih_ihi? (難道這就是論文中的“construct approximate histograms of gradient statistics”?)。
全局(global):在樹構建之前,確定所有特征的候選分裂點,然后在樹的所有層次 split finding 時都使用相同的proposals(即候選分裂點)。這種方法需要較少的proposal steps,但需要更多的候選分裂點(才能達到和local版本相同的效果),因為在每次分裂后,候選點們并沒有被提純。
局部(local):re-propose after each split. 這樣的好處是,每次分裂后,樣本減少,純度更高,相當于對候選分裂點進行了提純(refine),所以需要的候選分裂點可以較少。潛在地,也更適合于構建深層的樹。
XGBoost支持在單機環境下高效地使用精確貪婪算法,或者在任何環境下使用局部或全局的近似算法。
3.3 加權分位點算法(weighted quantile sketch)
以第 k 個特征為例,對于每一個樣本,由前面的 t-1 棵樹可以得到其二階導 h,以此作為該樣本的權重。n 個樣本的集合記為: Dk={(x1k,h1),(x2k,h2),...,(xnk,hn)}D_k = \{(x_{1k}, h_1), (x_{2k}, h_2), ..., (x_{nk}, h_n)\}Dk?={(x1k?,h1?),(x2k?,h2?),...,(xnk?,hn?)}
對樣本按照特征值升序排列,那么對于一個值 z,可以得到樣本值小于 z 的樣本權重之和 占 所有樣本權重之和 的比例:
rk(z)=1∑(x,h)∈Dkh?∑(x,h)∈Dk,x<zhr_k(z) = \dfrac 1{\sum_{(x,h) \in D_k} h} \cdot \sum_{(x,h)\in D_k, x < z} h rk?(z)=∑(x,h)∈Dk??h1??(x,h)∈Dk?,x<z∑?h
我們的目標是從該特征的這些取值中找到候選分裂點 {sk1,sk2,...,skl}\{s_{k1}, s_{k2}, ..., s_{kl}\}{sk1?,sk2?,...,skl?} ,滿足:
這里 ?\epsilon? 是一個近似系數。直覺上可知,大約有 1/?1 / \epsilon1/? 個候選分裂點。
若使用均方誤差作為損失函數,那么實際上所有樣本的 h 值都為 2。對于所有樣本權重相等的情況,一個已知的分位點算法 quantile sketch 可以解決該問題。
XGBoost支持對樣本設置權重,這樣一來樣本的二階導 h 也應當乘以相應的權重,這就導致每個樣本的權重可以各不相同。本文提出的加權分位點算法可以解決該問題。
3.4 稀疏感知的分裂點尋找(Sparsity-aware Split Finding)
數據稀疏可能由幾個原因造成:
- 數據值缺失
- 統計值經常為 0
- 人為的特征工程(比如 one-hot)
當樣本的某個數據值缺失時,該樣本會被分到默認的分支(獲得最大增益的方向)。
具體的算法思路如下:
對于每個特征:假設將缺失值分到右分支的情況下,遍歷所有特征值不缺失的特征值,尋找最佳分裂點再假設將缺失值分到左分支的情況下,遍歷所有特征值不缺失的特征值,尋找最佳分裂點得到增益最大的分裂點,同時,該最大增益是在哪種假設情況下得到的,哪種假設就作為缺失值的默認分支需要注意的是,該算法將 non-presence 視作缺失值處理,并按照學得的方向進行分支。
我所理解的 non-presence 是類似于 one-hot 之后的 0 值,不知道對不對?
作者在一個由于 one-hot 導致數據極其稀疏的數據集 Allstate-10K 上進行測試,該稀疏感知算法能比原始算法提升 50 倍的速度。
所以,我的理解是,稀疏感知算法既有效處理了缺失值,又加速了訓練。
4. 系統設計 SYSTEM DESIGN
4.1 Column Block for Parallel Learning
訓練樹的時候,最消耗時間的部分是,對數據進行排序。本文提出一種方法,將數據保存在內存單元里,保存的結構被稱為 block。
每一個 block 存儲一列特征,在 block 中,特征值已經排好序,并且記錄了每個特征值對應的樣本索引(這樣可以節省大量的存儲空間)。block 的數據格式是 壓縮的列(CSC格式)。Block 中特征之所以記錄了指向樣本的索引,是為了能根據特征的值來取梯度。
輸入數據的排序只在訓練開始前計算一次,之后每次迭代都可以復用。
4.2 Cache-aware Access
使用Block結構的一個缺點是取梯度的時候,是通過索引來獲取的,而這些梯度的獲取順序是按照特征的大小順序的。這將導致非連續的內存訪問,可能使得CPU cache緩存命中率低,從而影響算法效率。
因此,對于exact greedy算法中, 使用緩存預取(cache-aware prefetching)。具體來說,對每個線程分配一個連續的buffer,讀取梯度信息并存入Buffer中(這樣就實現了非連續到連續的轉化),然后再統計梯度信息。
對于大規模數據,效果十分明顯,大約快了一倍。
在 approximate 算法中,對Block的大小進行了合理的設置。定義Block的大小為Block中最多的樣本數。設置合適的大小是很重要的,設置過大則容易導致命中率低,過小則容易導致并行化效率不高。經過實驗,發現2^16比較好。
4.3 Blocks for Out-of-core Computation
當數據量太大不能全部放入主內存的時候,為了使得out-of-core計算成為可能,將數據劃分為多個Block并存放在磁盤上。計算的時候,使用獨立的線程預先將Block放入主內存,因此可以在計算的同時讀取磁盤。
但是由于磁盤IO速度太慢,通常跟不上計算的速度。因此,需要提升磁盤IO的吞吐量。Xgboost采用了2個策略:
-
Block壓縮(Block Compression):將Block按列壓縮(LZ4壓縮算法?),讀取的時候用另外的線程解壓。對于行索引,只保存第一個索引值,然后只保存該數據與第一個索引值之差(offset),一共用16個bits來保存
offset,因此,一個block一般有2的16次方個樣本。 -
Block拆分(Block Sharding):將數據劃分到不同磁盤上,為每個磁盤分配一個預取(pre-fetcher)線程,并將數據提取到內存緩沖區中。然后,訓練線程交替地從每個緩沖區讀取數據。這有助于在多個磁盤可用時增加磁盤讀取的吞吐量。
總結
以上是生活随笔為你收集整理的【Datawhale|天池】心跳信号分类预测 (4) - 模型 之 XGBoost的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据安全态势感知运营中心的关键防御措施
- 下一篇: 日记侠:微信传说的功能升级了,你用了没有