《Practical Lessons from Predicting Clicks on Ads at Facebook》GBDT+LR模型在CTR中的應用以及python實現

1 原文

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

2 GBDT+LR

2.1 背景

CTR預估,廣告點擊率(Click-Through Rate Prediction)是互聯網計算廣告中的關鍵環節,預估準確性直接影響公司廣告收入。CTR預估中用的最多的模型是LR,LR是廣義線性模型,與傳統線性模型相比,LR使用了Logit變換將函數值映射到0~1區間 ,映射後的函數值就是CTR的預估值。

LR,邏輯迴歸模型,這種線性模型很容易並行化,處理上億條訓練樣本不是問題,但線性模型學習能力有限,需要大量特徵工程預先分析出有效的特徵、特徵組合,從而去間接增強LR 的非線性學習能力。LR模型中的特徵組合很關鍵,但又無法直接通過特徵笛卡爾積 解決,只能依靠人工經驗,耗時耗力同時並不一定會帶來效果提升。如何自動發現有效的特徵、特徵組合,彌補人工經驗不足,縮短LR特徵實驗週期,是亟需解決的問題。

GBDT(Gradient Boost Decision Tree)是一種常用的非線性模型,它基於集成學習中的boosting思想,每次迭代都在減少殘差的梯度方向新建立一顆決策樹,迭代多少次就會生成多少顆決策樹。GBDT的思想使其具有天然優勢,可以發現多種有區分性的特徵以及特徵組合,決策樹的路徑可以直接作爲LR輸入特徵使用,省去了人工尋找特徵、特徵組合的步驟。這種通過GBDT生成LR特徵的方式(GBDT+LR),業界已有實踐(Facebook,Kaggle-2014),且效果不錯,是非常值得嘗試的思路。

2.2 三個問題

1、爲什麼建樹採用ensemble決策樹?

一棵樹的表達能力很弱,不足以表達多個有區分性的特徵組合,多棵樹的表達能力更強一些。GBDT每棵樹都在學習前面棵樹尚存的不足,迭代多少次就會生成多少顆樹。按paper以及Kaggle競賽中的GBDT+LR融合方式,多棵樹正好滿足LR每條訓練樣本可以通過GBDT映射成多個特徵的需求。

2、爲什麼建樹採用GBDT而非RF?

RF也是多棵樹,但從效果上有實踐證明不如GBDT。且GBDT前面的樹,特徵分裂主要體現對多數樣本有區分度的特徵;後面的樹,主要體現的是經過前N顆樹,殘差仍然較大的少數樣本。優先選用在整體上有區分度的特徵,再選用針對少數樣本有區分度的特徵,思路更加合理,這應該也是用GBDT的原因。

3、如何使用GBDT 映射得到的特徵?

通過GBDT生成的特徵,可直接作爲LR的特徵使用,省去人工處理分析特徵的環節,LR的輸入特徵完全依賴於通過GBDT得到的特徵。此思路已嘗試,通過實驗發現GBDT+LR在曝光充分的廣告上確實有效果,但整體效果需要權衡優化各類樹的使用。同時,也可考慮將GBDT生成特徵與LR原有特徵結合起來使用,待嘗試。

2.3 GBDT構建特徵

原文這麼說: In this paper we introduce a model which combines decision trees with logistic regression, outperforming either of these methods on its own by over 3%, an improvement with significant impact to the overall system performance.
在預測Facebook廣告點擊中,使用一種將決策樹與邏輯迴歸結合在一起的模型,其優於其他方法,超過3%。

用已有特徵訓練GBDT模型,然後利用GBDT模型學習到的樹來構造新特徵,最後把這些新特徵加入原有特徵一起訓練模型。構造的新特徵向量是取值0/1的,向量的每個元素對應於GBDT模型中樹的葉子結點。當一個樣本點通過某棵樹最終落在這棵樹的一個葉子結點上,那麼在新特徵向量中這個葉子結點對應的元素值爲1,而這棵樹的其他葉子結點對應的元素值爲0。新特徵向量的長度等於GBDT模型裏所有樹包含的葉子結點數之和。

在這裏插入圖片描述
上圖有兩棵樹,左樹有三個葉子節點,右樹有兩個葉子節點,最終的特徵即爲五維的向量。對於輸入x,假設他落在左樹第一個節點,編碼[1,0,0],落在右樹第二個節點則編碼[0,1],所以整體的編碼爲[1,0,0,0,1],這類編碼作爲特徵,輸入到線性分類模型(LR or FM)中進行分類。

2.4 GBDT與LR融合

在CTR預估中,如何利用AD ID是一個問題。直接將AD ID作爲特徵建樹不可行,而onehot編碼過於稀疏,爲每個AD ID建GBDT樹,相當於發掘出區分每個廣告的特徵。而對於曝光不充分的樣本即長尾部分,無法單獨建樹。

綜合方案爲:使用GBDT對非ID和ID分別建一類樹。

非ID類樹:不以細粒度的ID建樹,此類樹作爲base,即這些ID一起構建GBDT。即便曝光少的廣告、廣告主,仍可以通過此類樹得到有區分性的特徵、特徵組合。

ID類樹:以細粒度 的ID建一類樹(每個ID構建GBDT),用於發現曝光充分的ID對應有區分性的特徵、特徵組合。

如何根據GBDT建的兩類樹,對原始特徵進行映射?以如下圖爲例,當一條樣本x進來之後,遍歷兩類樹到葉子節點,得到的特徵作爲LR的輸入。當AD曝光不充分不足以訓練樹時,其它樹恰好作爲補充。
在這裏插入圖片描述

3 python實現

3.1 生成數據

from sklearn import datasets

X, Y = datasets.make_classification(n_samples = 2000, n_features = 7, n_informative = 2, n_redundant = 2, n_repeated = 0, n_classes = 2)
X[0:2, :]
array([[ 0.55617676,  1.15518068,  1.14471709,  1.60238303, -0.83274605,
         0.9809634 , -1.06788763],
       [-0.3802745 , -1.16172054,  0.97471291,  0.37325733,  0.62175847,
        -1.04289459, -0.35255169]])
Y[0:2]
array([0, 1])

3.2 數據集劃分

from sklearn.cross_validation import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size = 0.2, random_state = 33)

print(X_train.shape)
print(y_train.shape)
print(X_test.shape)
print(y_test.shape)
(1600, 7)
(1600,)
(400, 7)
(400,)

3.3 爲GBDT生成數據集

import lightgbm as lgb

lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
lgb_train.data[0:5, :]
array([[-1.78901975e+00, -2.13672953e+00, -3.55867954e-01,
         7.07590363e-01,  2.45621113e+00, -1.57509749e+00,
         1.33151080e-01],
       [-1.12873443e+00, -1.76466074e+00,  4.45921841e-02,
         8.93417107e-01,  1.60835722e+00, -1.41064052e+00,
         6.34965515e-01],
       [-9.30466511e-01,  8.84486094e-01, -8.23090631e-04,
        -1.50658131e+00,  9.96338385e-01,  1.17815479e+00,
         3.52329692e-01],
       [ 4.94948092e-01,  3.74651957e-01,  4.06111794e-01,
        -1.09187592e+00, -6.49036651e-01,  2.19101769e-01,
        -6.27506001e-01],
       [ 6.37746226e-01,  8.40892512e-01,  2.12280392e-02,
         3.26615088e-01, -8.86740977e-01,  6.40745122e-01,
        -2.80656519e-01]])
lgb_train.label[0:5]
array([1, 1, 1, 0, 0])

3.4 設置參數

# 各個參數含義可以查看官方文檔:http://lightgbm.apachecn.org

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
}

num_leaf = 64

3.5 訓練GBDT

gbm = lgb.train(params, lgb_train, num_boost_round=100, valid_sets=lgb_eval)
D:\anaconda\envs\tensorflow\lib\site-packages\lightgbm\engine.py:118: UserWarning: Found `num_trees` in params. Will use it instead of argument
  warnings.warn("Found `{}` in params. Will use it instead of argument".format(alias))


[1]	valid_0's binary_logloss: 0.685329
[2]	valid_0's binary_logloss: 0.677346
[3]	valid_0's binary_logloss: 0.669733
[4]	valid_0's binary_logloss: 0.662271
[5]	valid_0's binary_logloss: 0.654957
[6]	valid_0's binary_logloss: 0.647804
......
[98]	valid_0's binary_logloss: 0.303422
[99]	valid_0's binary_logloss: 0.301712
[100]	valid_0's binary_logloss: 0.300002

3.6 保存模型

gbm.save_model('model.txt')
<lightgbm.basic.Booster at 0x1ad54f2ef28>

3.7 預測

# 訓練得到100棵樹之後,我們需要得到的不是GBDT的預測結果,而是每一條訓練數據落在了每棵樹的哪個葉子結點上
y_pred = gbm.predict(X_train, pred_leaf=True)
import numpy as np

print(np.array(y_pred).shape)
print(y_pred[0])
(1600, 100)
[15 16 30 30 30 40 17 38 17 17 25 26 26 26 38 45 44 40 41 42 39 40 48 40
 47 47 49 48 49 49 47 47 47 47 47 49 44 47 46 49 50 50 46 50 14 45 48 49
 48 50 43 47 46 46 46 44 41 46 41 37 47 53 46 46 48 48 48 38 38 51 49 50
 43 49 43 47 42 42 47 41 42 46 50 46 48 48 44 47 46 46 49 47 49 48 48 49
 49 26 52 35]

3.8 onehot處理

print(np.array(y_pred[0]))
print(np.arange(len(y_pred[0])))
print(np.arange(len(y_pred[0])) * num_leaf)
print(np.arange(len(y_pred[0])) * num_leaf + np.array(y_pred[0]))
[15 16 30 30 30 40 17 38 17 17 25 26 26 26 38 45 44 40 41 42 39 40 48 40
 47 47 49 48 49 49 47 47 47 47 47 49 44 47 46 49 50 50 46 50 14 45 48 49
 48 50 43 47 46 46 46 44 41 46 41 37 47 53 46 46 48 48 48 38 38 51 49 50
 43 49 43 47 42 42 47 41 42 46 50 46 48 48 44 47 46 46 49 47 49 48 48 49
 49 26 52 35]
[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
 96 97 98 99]
[   0   64  128  192  256  320  384  448  512  576  640  704  768  832
  896  960 1024 1088 1152 1216 1280 1344 1408 1472 1536 1600 1664 1728
 1792 1856 1920 1984 2048 2112 2176 2240 2304 2368 2432 2496 2560 2624
 2688 2752 2816 2880 2944 3008 3072 3136 3200 3264 3328 3392 3456 3520
 3584 3648 3712 3776 3840 3904 3968 4032 4096 4160 4224 4288 4352 4416
 4480 4544 4608 4672 4736 4800 4864 4928 4992 5056 5120 5184 5248 5312
 5376 5440 5504 5568 5632 5696 5760 5824 5888 5952 6016 6080 6144 6208
 6272 6336]
[  15   80  158  222  286  360  401  486  529  593  665  730  794  858
  934 1005 1068 1128 1193 1258 1319 1384 1456 1512 1583 1647 1713 1776
 1841 1905 1967 2031 2095 2159 2223 2289 2348 2415 2478 2545 2610 2674
 2734 2802 2830 2925 2992 3057 3120 3186 3243 3311 3374 3438 3502 3564
 3625 3694 3753 3813 3887 3957 4014 4078 4144 4208 4272 4326 4390 4467
 4529 4594 4651 4721 4779 4847 4906 4970 5039 5097 5162 5230 5298 5358
 5424 5488 5548 5615 5678 5742 5809 5871 5937 6000 6064 6129 6193 6234
 6324 6371]
# 將每棵樹的特徵進行one-hot處理,第一棵樹落在15號葉子結點上,那我們需要建立一個64維的向量,除15維之外全部都是0。

# 轉換訓練集
transformed_training_matrix = np.zeros([len(y_pred), len(y_pred[0]) * num_leaf], dtype=np.int64)
for i in range(len(y_pred)):
    temp = np.arange(len(y_pred[0])) * num_leaf + np.array(y_pred[i])
    transformed_training_matrix[i][temp] += 1

# 轉換測試集
y_pred2 = gbm.predict(X_test, pred_leaf=True)
transformed_testing_matrix = np.zeros([len(y_pred2), len(y_pred2[0]) * num_leaf], dtype=np.int64)
for i in range(len(y_pred2)):
    temp = np.arange(len(y_pred2[0])) * num_leaf + np.array(y_pred2[i])
    transformed_testing_matrix[i][temp] += 1

3.9 邏輯迴歸

from sklearn.linear_model import LogisticRegression

lm = LogisticRegression(penalty = 'l2', C = 0.05)
lm.fit(transformed_training_matrix, y_train)
y_pred_test = lm.predict_proba(transformed_testing_matrix)

# print(y_pred_test)

NE = (-1) / len(y_pred_test) * sum(((1+y_test)/2 * np.log(y_pred_test[:,1]) +  (1-y_test)/2 * np.log(1 - y_pred_test[:,1])))
print("Normalized Cross Entropy " + str(NE))
Normalized Cross Entropy 0.9422853076579945

參考

1、原文:http://quinonero.net/Publications/predicting-clicks-facebook.pdf
2、代碼:https://github.com/princewen/tensorflow_practice/blob/master/recommendation/GBDT%2BLR-Demo/GBDT_LR.py
3、隨機森林、GBDT、XGBOOST區別:https://blog.csdn.net/yingfengfeixiang/article/details/80210145
4、騰訊大數據:http://www.cbdio.com/BigData/2015-08/27/content_3750170.htm
5、GBDT與LR:https://blog.csdn.net/shine19930820/article/details/71713680
6、劉建平博客—GBDT原理:https://www.cnblogs.com/pinard/p/6140514.html
7、代碼2:https://github.com/guestwalk/kaggle-2014-criteo

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