知乎 - lightgbm 源碼剖析
知乎 - lightgbm源碼閱讀(一)
知乎 - lightgbm源碼閱讀(二)
lightgbm源碼閱讀+理論分析(處理特征類别,預設值的實作細節)
簡述fastdbt和lightgbm中gbdt的實作
調試準備
從main到gbdt::train
執行路徑
重點看一下trainoneiter
gbdt::boostfromaverage
objectivefunction::getgradients
回顧疊代過程
serialtreelearner::train
num_leaves次數的最佳葉子結點分裂過程
efb
調試準備
把-o3改為-o0,不然指令優化之後有些值調試不出來
代碼裡面有些帶<code>#pragma omp parallel for</code> 的預處理語句可以注釋掉,并行改串行,不然調試不進去
先以二分類為例,資料用的是官方給的例子
從main到gbdt::train
main函數執行路徑
<code>application::train</code>
<code>src/application/application.cpp:200</code>
<code>boosting_->train(config_.snapshot_freq, config_.output_model);</code>
去看boosting
<code>include/lightgbm/boosting.h</code>
boosting是一個抽象類,派生出<code>boosting_type</code>對應的4個子類
有空畫個繼承關系圖
找到gbdt::train函數
<code>src/boosting/gbdt.cpp:246</code>
<code>fun_timer</code>應該是一個raii對象
樸實無華的疊代過程,看<code>trainoneiter</code>
注意上層調用方式是<code>is_finished = trainoneiter(nullptr, nullptr);</code>,<code>gradient</code>和<code>hessian</code>都是空指針
gbdt有兩個成員變量,維護每次疊代時的梯度和黑塞(用了自己寫的記憶體配置設定器)
vector裡面放智能指針比放指針靠譜(對比slsm源碼)
重點看一下trainoneiter
重點看一下初始值怎麼設的
binarylogloss
馬上列印出:
去<code>obtainautomaticinitialscore</code>看看
跳到<code>src/objective/binary_objective.hpp:134</code>
如圖,總共7000全量樣本,正樣本3716,pavg=0.53
i
n
t
s
c
o
r
e
=
log
(
p
a
v
g
1
−
)
initscore=\log(\frac{pavg}{1-pavg})
initscore=log(1−pavgpavg)
算出來的是logits
本小節主要闡述目标函數<code>1 2階導數</code>的求解,如果後期需要實作hinge loss或者focal loss可以用得上
init_scores看完了,我們看<code>gbdt::boosting</code>
→
\rightarrow
→
<code>objectivefunction::getgradients</code>
<code>src/objective/binary_objective.hpp:101</code>
為了友善調試,注釋掉<code>#pragma omp parallel for schedule(static)</code>
注意到binarylogloss初始化的時候*weights_=1.2
發現是配置檔案指定了weight
我們康康gradient和hessian是怎麼算的
初次計算時,score就是<code>init_scores</code>,用label avg算的
忽略樣本權重,可以歸納為這兩個式子(注意,label已經變換為{-1, 1},即
y
∈
{
,
}
y\in\{-1, 1\}
y∈{−1,1}):
+
x
×
^
g=-\frac{y}{1+exp(y\times \hat{y})}
g=−1+exp(y×y^)y
h
∣
h=|g|\times (1-|g|)
h=∣g∣×(1−∣g∣)
好,我們來推一下這兩個公式怎麼來的
知乎 - 邏輯回歸損失函數的兩種形式
根據上述知乎文章,可知在
y∈{−1,1}條件下,負對數似然函數為
l
⋅
nll=\log(1+exp(-y \cdot \hat{y}))
nll=log(1+exp(−y⋅y^))
nll對
\hat{y}
y^求導,可得
∂
σ
\large{ \begin{array}{ll} \frac{\partial{nll}}{\partial{\hat{y}}} &=-\frac{y}{1+exp(y\cdot \hat{y})} \\ \\ &=-\sigma(-y\cdot \hat{y}) \cdot y \end{array} }
∂y^∂nll=−1+exp(y⋅y^)y=−σ(−y⋅y^)⋅y
sigmoid
sigmoid 導數
treelearner派生了很多learner,這裡以serialtreelearner為例進行學習
啟動過程:
gbdt::init
train_data_是const dataset*類型,dataset包含efb之後的結果
在serialtreelearner::init中:
這些變量全部被初始化為長度<code>num_leaves</code>的vector
在histogrampool::dynamicchangesize方法中,會将<code>pool_</code>和<code>data_</code>設為num_leaves,将<code>feature_metas_</code>設為num_features
列印了一下
<code>data_</code>的長度開到<code>num_total_bin * 2</code>
原本在gbdt::init中調用serialtreelearner::init的時候,不應該是7000個資料嗎,怎麼變成5626了呢?我回去看了下,是在gbdt::train中調用了gbdt::bagging導緻的
周遊所有可分裂的葉子<code>for (int split = init_splits; split < config_->num_leaves - 1; ++split) {</code>
serialtreelearner::beforefindbestsplit
<code>histogram_pool_</code> 相關處理
serialtreelearner::findbestsplits
constructhistograms(is_feature_used, use_subtract);
findbestsplitsfromhistograms(is_feature_used, use_subtract);
<code>for (int feature_index = 0; feature_index < num_features_; ++feature_index) {</code>
serialtreelearner::computebestsplitforfeature
featurehistogram::findbestthreshold
find_best_threshold_fun_(仿函數對象)
featurehistogram::findbestthresholdsequentially
根據成員變量<code>best_split_per_leaf_</code>找到最佳葉子索引(int) <code>best_leaf</code>
如果<code>best_leaf_splitinfo.gain <= 0.0</code>,break
調用<code>split(tree_prt, best_leaf, &left_leaf, &right_leaf);</code>進行葉子分裂
real_feature_idx_
serialtreelearner
efb
<code>src/io/dataset.cpp:338</code>
hist_t是double的别名
meta_維護了num_bin,config 等