1.來源
本質上 GBDT+LR 是一種具有 stacking 思想的二分類器模型,所以可以用來解決二分類問題。這個方法出自於 Facebook 2014 年的論文 Practical Lessons from Predicting Clicks on Ads at Facebook 。
2.使用場景
GBDT+LR 使用最廣泛的場景是 CTR 點擊率預估,即預測當給用戶推送的廣告會不會被用戶點擊。點擊率預估模型涉及的訓練樣本一般是上億級別,樣本量大,模型常采用速度較快的 LR。但 LR 是線性模型,學習能力有限,此時特征工程尤其重要。現有的特征工程實驗,主要集中在尋找到有區分度的特征、特征組合,折騰一圈未必會帶來效果提升。GBDT 算法的特點正好可以用來發掘有區分度的特征、特征組合,減少特征工程中人力成本。
3.CTR的流程
主要包括兩大部分:離線部分、在線部分,其中離線部分目標主要是訓練出可用模型,而在線部分則考慮模型上線后,性能可能隨時間而出現下降,若出現這種情況,可選擇使用 Online-Learning 來在線更新模型:
3.1離線部分
- 數據收集:主要收集和業務相關的數據,通常會有專門的同事在 app 位置進行埋點,拿到業務數據
- 預處理:對埋點拿到的業務數據進行去臟去重;
- 構造數據集:經過預處理的業務數據,構造數據集,在切分訓練、測試、驗證集時應該合理根據業務邏輯來進行切分;
- 特征工程:對原始數據進行基本的特征處理,包括去除相關性大的特征,離散變量 one-hot,連續特征離散化等等;
- 模型選擇:選擇合理的機器學習模型來完成相應工作,原則是先從簡入深,先找到 baseline,然后逐步優化;
- 超參選擇:利用 gridsearch、randomsearch 或者 hyperopt 來進行超參選擇,選擇在離線數據集中性能最好的超參組合;
- 在線 A/B Test:選擇優化過后的模型和原先模型(如 baseline)進行 A/B Test,若性能有提升則替換原先模型;
3.2 在線部分
- Cache & Logic:設定簡單過濾規則,過濾異常數據;
- 模型更新:當 Cache & Logic 收集到合適大小數據時,對模型進行- pretrain+finetuning,若在測試集上比原始模型性能高,則更新 model server 的模型參數;
- Model Server:接受數據請求,返回預測結果;
4. GBDT + LR 的結構
正如它的名字一樣,GBDT+LR 由兩部分組成,其中 GBDT 用來對訓練集提取特征作為新的訓練輸入數據,LR 作為新訓練輸入數據的分類器。具體來講,有以下幾個步驟:
- GBDT 首先對原始訓練數據做訓練,得到一個二分類器,當然這里也需要利用網格搜索尋找最佳參數組合。
- 與通常做法不同的是,當 GBDT 訓練好做預測的時候,輸出的並不是最終的二分類概率值,而是要把模型中的每棵樹計算得到的預測概率值所屬的葉子結點位置記為 1,這樣,就構造出了新的訓練數據。
舉個例子,下圖是一個 GBDT+LR 模型結構,設 GBDT 有兩個弱分類器,分別以藍色和紅色部分表示,其中藍色弱分類器的葉子結點個數為 3,紅色弱分類器的葉子結點個數為 2,並且藍色弱分類器中對 0-1 的預測結果落到了第二個葉子結點上,紅色弱分類器中對 0-1 的預測結果也落到了第二個葉子結點上。那么我們就記藍色弱分類器的預測結果為[0 1 0],紅色弱分類器的預測結果為[0 1],綜合起來看,GBDT 的輸出為這些弱分類器的組合[0 1 0 0 1] ,或者一個稀疏向量(數組)。
這里的思想與 One-hot 獨熱編碼類似,事實上,在用 GBDT 構造新的訓練數據時,采用的也正是 One-hot 方法。並且由於每一弱分類器有且只有一個葉子節點輸出預測結果,所以在一個具有 n 個弱分類器、共計 m 個葉子結點的 GBDT 中,每一條訓練數據都會被轉換為 1*m 維稀疏向量,且有 n 個元素為 1,其余 m-n 個元素全為 0。
- 新的訓練數據構造完成后,下一步就要與原始的訓練數據中的 label(輸出)數據一並輸入到 Logistic Regression 分類器中進行最終分類器的訓練。思考一下,在對原始數據進行 GBDT 提取為新的數據這一操作之后,數據不僅變得稀疏,而且由於弱分類器個數,葉子結點個數的影響,可能會導致新的訓練數據特征維度過大的問題,因此,在 Logistic Regression 這一層中,可使用正則化來減少過擬合的風險,在 Facebook 的論文中采用的是 L1 正則化。
5. RF + LR ? Xgb + LR?
有心的同學應該會思考一個問題,既然 GBDT 可以做新訓練樣本的構造,那么其它基於樹的模型,例如 Random Forest 以及 Xgboost 等是並不是也可以按類似的方式來構造新的訓練樣本呢?沒錯,所有這些基於樹的模型都可以和 Logistic Regression 分類器組合。至於效果孰優孰劣,我個人覺得效果都還可以,但是之間沒有可比性,因為超參數的不同會對模型評估產生較大的影響。下圖是 RF+LR、GBT+LR、Xgb、LR、Xgb+LR 模型效果對比圖,然而這只能做個參考,因為模型超參數的值的選擇這一前提條件都各不相同。順便來講,RF 也是多棵樹,但從效果上有實踐證明不如 GBDT。且 GBDT 前面的樹,特征分裂主要體現對多數樣本有區分度的特征;后面的樹,主要體現的是經過前 N 顆樹,殘差仍然較大的少數樣本。優先選用在整體上有區分度的特征,再選用針對少數樣本有區分度的特征,思路更加合理,這應該也是用 GBDT 的原因。
5. GBDT + LR 代碼分析
在網上找到了兩個版本的 GBDT+LR 的代碼實現,通過閱讀分析,認為里面有一些細節還是值得好好學習一番的,所以接下來這一小節會針對代碼實現部分做一些總結。目前了解到的 GBDT 的實現方式有兩種:
- Scikit-learn 中的 ensemble.GradientBoostingClassifier
- lgb 里的 params={ 'boosting_type': 'gbdt' }參數
接下里分別對這兩種實現方式進行分析。
5.1 Scikit-learn 的實現:
from sklearn.preprocessing import OneHotEncoder from sklearn.ensemble import GradientBoostingClassifier gbm1 = GradientBoostingClassifier(n_estimators=50, random_state=10, subsample=0.6, max_depth=7, min_samples_split=900) gbm1.fit(X_train, Y_train) train_new_feature = gbm1.apply(X_train) train_new_feature = train_new_feature.reshape(-1, 50) enc = OneHotEncoder() enc.fit(train_new_feature) # # 每一個屬性的最大取值數目 # print('每一個特征的最大取值數目:', enc.n_values_) # print('所有特征的取值數目總和:', enc.n_values_.sum()) train_new_feature2 = np.array(enc.transform(train_new_feature).toarray())
划重點:
5.1.1 model.apply(X_train)的用法
model.apply(X_train)返回訓練數據 X_train 在訓練好的模型里每棵樹中所處的葉子節點的位置(索引)
5.1.2 sklearn.preprocessing 中 OneHotEncoder 的使用
除了 pandas 中的 get_dummies(),sklearn 也提供了一種對 Dataframe 做 One-hot 的方法。OneHotEncoder() 首先 fit() 過待轉換的數據后,再次 transform() 待轉換的數據,就可實現對這些數據的所有特征進行 One-hot 操作。由於 transform() 后的數據格式不能直接使用,所以最后需要使用.toarray() 將其轉換為我們能夠使用的數組結構。
enc.transform(train_new_feature).toarray()
5.1.3 sklearn 中的 GBDT 能夠設置樹的個數,每棵樹最大葉子節點個數等超參數,但不能指定每顆樹的葉子節點數。
5.2 lightgbm 的實現
params = { 'task': 'train', 'boosting_type': 'gbdt', 'objective': 'binary', 'metric': {'binary_logloss'}, 'num_leaves': 64, 'num_trees': 100, 'learning_rate': 0.01, 'feature_fraction': 0.9, 'bagging_fraction': 0.8, 'bagging_freq': 5, 'verbose': 0 } # number of leaves,will be used in feature transformation num_leaf = 64 print('Start training...') # train gbm = lgb.train(params=params, train_set=lgb_train, valid_sets=lgb_train, ) print('Start predicting...') # y_pred 分別落在 100 棵樹上的哪個節點上 y_pred = gbm.predict(x_train, pred_leaf=True) y_pred_prob = gbm.predict(x_train) result = [] threshold = 0.5 for pred in y_pred_prob: result.append(1 if pred > threshold else 0) print('result:', result) print('Writing transformed training data') transformed_training_matrix = np.zeros([len(y_pred), len(y_pred[1]) _ num_leaf], dtype=np.int64) # N _ num_tress _ num_leafs for i in range(0, len(y_pred)): # temp 表示在每棵樹上預測的值所在節點的序號(0,64,128,...,6436 為 100 棵樹的序號,中間的值為對應樹的節點序號) temp = np.arange(len(y_pred[0])) _ num_leaf + np.array(y_pred[i]) # 構造 one-hot 訓練數據集 transformed_training_matrix[i][temp] += 1 y_pred = gbm.predict(x_test, pred_leaf=True) print('Writing transformed testing data') transformed_testing_matrix = np.zeros([len(y_pred), len(y_pred[1]) _ num_leaf], dtype=np.int64) for i in range(0, len(y_pred)): temp = np.arange(len(y_pred[0])) _ num_leaf + np.array(y_pred[i]) # 構造 one-hot 測試數據集 transformed_testing_matrix[i][temp] += 1
划重點:
5.2.1 params 字典里超參數的設置
因為是二分類問題,所以設置 {'boosting_type': 'gbdt','objective': 'binary','metric': {'binary_logloss'}},然后設置樹的個數及每棵樹的葉子結點個數{'num_leaves': 64,'num_trees': 100}
5.2.2 model.predict(x_train, pred_leaf=True)
使用
model.predict(x_train, pred_leaf=True)
返回訓練數據在訓練好的模型里預測結果所在的每棵樹中葉子節點的位置(索引),形式為 7999*100 的二維數組。
5.2.3 構造 Ont-hot 數組作為新的訓練數據
這里並沒有使用 sklearn 中的 OneHotEncoder(),也沒有使用 pandas 中的 get_dummies(),而是手工創建一個 One-hot 數組。(當然也可以像 5.1.2 那樣操作)
- 首先,創建一個二維零數組用於存放 one-hot 的元素;
- 然后,獲取第 2 步得到的二維數組里每個葉子節點在整個 GBDT 模型里的索引號,因為一共有 100 棵樹,每棵樹有 64 個葉子節點,所以索引范圍是 0~6400;(這里有一個技巧,通過把每棵樹的起點索引組成一個列表,再加上由落在每棵樹葉子節點的索引組成的列表,就得到了往二維零數組里插入元素的索引信息)
- 最后,
temp = np.arange(len(y_pred[0])) \* num_leaf + np.array(y_pred[i])
5.2.4 對二維數組填充信息,采用"+=" 的方法
# 構造 one-hot 訓練數據集 transformed_training_matrix[i][temp] += 1
6. GBDT + LR 模型提升
現在,我們思考這樣一個問題,Logistic Regression 是一個線性分類器,也就是說會忽略掉特征與特征之間的關聯信息,那么是否可以采用構建新的交叉特征這一特征組合方式從而提高模型的效果?
其次,我們已經在 4 小節中了解到 GBDT 很有可能構造出的新訓練數據是高維的稀疏矩陣,而 Logistic Regression 使用高維稀疏矩陣進行訓練,會直接導致計算量過大,特征權值更新緩慢的問題。
針對上面可能出現的問題,了解下FM 算法解析及 Python 實現 ,使用 FM 算法代替 LR,這樣就解決了 Logistic Regression 的模型表達效果及高維稀疏矩陣的訓練開銷較大的問題。然而,這樣就意味着可以高枕無憂了嗎?當然不是,因為采用 FM 對本來已經是高維稀疏矩陣做完特征交叉后,新的特征維度會更加多,並且由於元素非 0 即 1,新的特征數據可能也會更加稀疏,那么怎么辦?
所以,我們需要再次回到 GBDT 構造新訓練數據這里。當 GBDT 構造完新的訓練樣本后,我們要做的是對每一個特征做與輸出之間的特征重要度評估並篩選出重要程度較高的部分特征,這樣,GBDT 構造的高維的稀疏矩陣就會減少一部分特征,也就是說得到的稀疏矩陣不再那么高維了。之后,對這些篩選后得到的重要度較高的特征再做 FM 算法構造交叉項,進而引入非線性特征,繼而完成最終分類器的訓練數據的構造及模型的訓練。