GBDT 特徵提取(2)

1. GBDT + LR 是什麼

本質上GBDT+LR是一種具有stacking思想的二分類器模型,所以可以用來解決二分類問題。這個方法出自於Facebook 2014年的論文 Practical Lessons from Predicting Clicks on Ads at Facebook 。

2. GBDT + LR 用在哪

GBDT+LR 使用最廣泛的場景是CTR點擊率預估,即預測當給用戶推送的廣告會不會被用戶點擊。

點擊率預估模型涉及的訓練樣本一般是上億級別,樣本量大,模型常採用速度較快的LR。但LR是線性模型,學習能力有限,此時特徵工程尤其重要。現有的特徵工程實驗,主要集中在尋找到有區分度的特徵、特徵組合,折騰一圈未必會帶來效果提升。GBDT算法的特點正好可以用來發掘有區分度的特徵、特徵組合,減少特徵工程中人力成本。

從知乎https://zhuanlan.zhihu.com/p/29053940上看到了一個關於CTR的流程,如下圖所示:

如上圖,主要包括兩大部分:離線部分、在線部分,其中離線部分目標主要是訓練出可用模型,而在線部分則考慮模型上線後,性能可能隨時間而出現下降,弱出現這種情況,可選擇使用Online-Learning來在線更新模型:

2.1 離線部分

  1. 數據收集:主要收集和業務相關的數據,通常會有專門的同事在app位置進行埋點,拿到業務數據
  2. 預處理:對埋點拿到的業務數據進行去髒去重;
  3. 構造數據集:經過預處理的業務數據,構造數據集,在切分訓練、測試、驗證集時應該合理根據業務邏輯來進行切分;
  4. 特徵工程:對原始數據進行基本的特徵處理,包括去除相關性大的特徵,離散變量one-hot,連續特徵離散化等等;
  5. 模型選擇:選擇合理的機器學習模型來完成相應工作,原則是先從簡入深,先找到baseline,然後逐步優化;
  6. 超參選擇:利用gridsearch、randomsearch或者hyperopt來進行超參選擇,選擇在離線數據集中性能最好的超參組合;
  7. 在線A/B Test:選擇優化過後的模型和原先模型(如baseline)進行A/B Test,若性能有提升則替換原先模型;

2.2 在線部分

  1. Cache & Logic:設定簡單過濾規則,過濾異常數據;
  2. 模型更新:當Cache & Logic 收集到合適大小數據時,對模型進行pretrain+finetuning,若在測試集上比原始模型性能高,則更新model server的模型參數;
  3. Model Server:接受數據請求,返回預測結果;

3. GBDT + LR 的結構

正如它的名字一樣,GBDT+LR 由兩部分組成,其中GBDT用來對訓練集提取特徵作爲新的訓練輸入數據,LR作爲新訓練輸入數據的分類器。

具體來講,有以下幾個步驟:

3.1 GBDT首先對原始訓練數據做訓練,得到一個二分類器,當然這裏也需要利用網格搜索尋找最佳參數組合。

3.2 與通常做法不同的是,當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。

3.3 新的訓練數據構造完成後,下一步就要與原始的訓練數據中的label(輸出)數據一併輸入到Logistic Regression分類器中進行最終分類器的訓練。思考一下,在對原始數據進行GBDT提取爲新的數據這一操作之後,數據不僅變得稀疏,而且由於弱分類器個數,葉子結點個數的影響,可能會導致新的訓練數據特徵維度過大的問題,因此,在Logistic Regression這一層中,可使用正則化來減少過擬合的風險,在Facebook的論文中採用的是L1正則化。

4. 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 那樣操作)

  1. 首先,創建一個二維零數組用於存放one-hot的元素;
  2. 然後,獲取第2步得到的二維數組裏每個葉子節點在整個GBDT模型裏的索引號,因爲一共有100棵樹,每棵樹有64個葉子節點,所以索引範圍是0~6400;(這裏有一個技巧,通過把每棵樹的起點索引組成一個列表,再加上由落在每棵樹葉子節點的索引組成的列表,就得到了往二維零數組裏插入元素的索引信息)
  3. 最後,

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是一個線性分類器,也就是說會忽略掉特徵與特徵之間的關聯信息,那麼是否可以採用構建新的交叉特徵這一特徵組合方式從而提高模型的效果?

其次,我們已經在2.3小節中瞭解到GBDT很有可能構造出的新訓練數據是高維的稀疏矩陣,而Logistic Regression使用高維稀疏矩陣進行訓練,會直接導致計算量過大,特徵權值更新緩慢的問題。

針對上面可能出現的問題,可以翻看我之前的文章:FM算法解析及Python實現 ,使用FM算法代替LR,這樣就解決了Logistic Regression的模型表達效果及高維稀疏矩陣的訓練開銷較大的問題。然而,這樣就意味着可以高枕無憂了嗎?當然不是,因爲採用FM對本來已經是高維稀疏矩陣做完特徵交叉後,新的特徵維度會更加多,並且由於元素非0即1,新的特徵數據可能也會更加稀疏,那麼怎麼辦?

所以,我們需要再次回到GBDT構造新訓練數據這裏。當GBDT構造完新的訓練樣本後,我們要做的是對每一個特徵做與輸出之間的特徵重要度評估並篩選出重要程度較高的部分特徵,這樣,GBDT構造的高維的稀疏矩陣就會減少一部分特徵,也就是說得到的稀疏矩陣不再那麼高維了。之後,對這些篩選後得到的重要度較高的特徵再做FM算法構造交叉項,進而引入非線性特徵,繼而完成最終分類器的訓練數據的構造及模型的訓練。

7. 參考資料

https://www.cnblogs.com/wkang/p/9657032.html

[1] CTR預估中GBDT與LR融合方案

[2] 常見計算廣告點擊率預估算法總結

[3] GBDT+LR算法進行特徵擴增

[4] 推薦系統遇上深度學習(十)--GBDT+LR融合方案實戰

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