前言
晚上很多內容講解LGBT+LR的推薦系統,但是很多都講解的都過於自然,很多都省略了,本文從小白角度來慢慢分析。包括代碼分析等等。
問題是什麼
CTR全稱是click-through rate,中文名叫點擊率,它是怎麼回事呢?就是給一個樣本,這個樣本的標籤是一個0或者1的值,1表示用戶會點擊,0表示用戶不會點擊,數據集中有很多0、1的數據,這些數據用來訓練,其實可以看作一個預測的二分類任務。
思路
很多博客都有提到這個思路,大致就是把GBDT選擇的葉子節點拿來做one-hot特徵,類似在原來基礎上再做特徵,用於LR的訓練,主體是這個思路。接下來會講解代碼部分,大家可以看下實際的代碼,代碼註釋很全,比其他博客的代碼要更全一些。
數據集:威斯康辛州乳腺癌數據(適用於分類問題)
這個數據集包含了威斯康辛州記錄的569個病人的乳腺癌惡性/良性(1/0)類別型數據(訓練目標),以及與之對應的30個維度的生理指標數據;因此這是個非常標準的二類判別數據集,在這裏使用load_breast_cancer(return_X_y)來導出數據。
代碼
# -*- coding: utf-8 -*-
from scipy.sparse.construct import hstack
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model.logistic import LogisticRegression
from sklearn.metrics.ranking import roc_auc_score
from sklearn.preprocessing.data import OneHotEncoder
import numpy as np
import lightgbm as lgb
import xgboost as xgb
import warnings
warnings.filterwarnings('ignore')
def xgb_lr_train(df_train,df_test):
X_train, X_valid, y_train, y_valid = train_test_split(df_train, df_test , test_size=0.3)
xgboost = xgb.XGBClassifier(nthread=4, learning_rate=0.08, n_estimators=200, max_depth=5, gamma=0, subsample=0.9,
colsample_bytree=0.5)
xgboost.fit(X_train, y_train)
xgb_valid_auc = roc_auc_score(y_valid, xgboost.predict(X_valid))
print("XGBoost valid AUC: %.5f" % xgb_valid_auc)
X_train_leaves = xgboost.apply(X_train)
X_valid_leaves = xgboost.apply(X_valid)
print(" X_train_leaves shape = ",X_train_leaves.shape)
print(" X_valid_leaves shape = ",X_valid_leaves.shape)
all_leaves = np.concatenate((X_train_leaves, X_valid_leaves), axis=0)
all_leaves = all_leaves.astype(np.int32)
print(" all_leaves shape = ",all_leaves.shape)
## 下面這個 one-hot 不準,因爲每次落在的葉子節點都是不確定的,這個方法是對 已知的 數據做one-hot,
# 如果數據集 是拆分的,每次不同的樣本進來會導致,one-hot 大小不一樣,因爲 樣本落在的葉子節點不一樣
xgbenc = OneHotEncoder()
X = xgbenc.fit_transform(all_leaves)
print(" X shape = ",X.shape)
(train_rows, cols) = X_train_leaves.shape
lr = LogisticRegression()
lr.fit(X[:train_rows, :], y_train)
xgb_lr_valid_auc = roc_auc_score(y_valid, lr.predict_proba(X[train_rows:, :])[:, 1])
print("XGBoost-LR valid AUC: %.5f" % xgb_lr_valid_auc)
def lgb_train(df_train,df_test):
X_train, X_test, y_train, y_test = train_test_split(df_train, df_test , test_size=0.3)
# create dataset for lightgbm
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
#
params = {
'task': 'train',
'boosting_type': 'gbdt',
'objective': 'binary',
'metric': {'binary_logloss'},
'num_leaves': 64, # 葉子節點數量
'num_trees': 100, # 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,
lgb_train,
num_boost_round=100,
valid_sets=lgb_train)
print('Start predicting...')
# predict and get data on leaves, training data
# 在訓練得到100棵樹之後,我們需要得到的不是GBDT的預測結果,而是每一條訓練數據落在了每棵樹的哪個葉子結點上,shape是(,100),即訓練數據量*樹的棵樹
# 那麼存儲的是什麼數據呢?返回訓練數據在訓練好的模型裏預測結果所在的每棵樹中葉子節點的位置(索引),形式爲7999*100的二維數組。
y_pred = gbm.predict(X_train, pred_leaf=True)
print("np.array(y_pred).shape = ",np.array(y_pred).shape)
#print(y_pred[:10])
# 將每棵樹的特徵進行one-hot處理,
# 假設第一棵樹落在43號葉子結點上,那我們需要建立一個64維的向量,除43維之外全部都是0。因此用於LR訓練的特徵維數共num_trees * num_leaves。
print('Writing transformed training data')
transformed_training_matrix = np.zeros([len(y_pred), len(y_pred[0]) * num_leaf],
dtype=np.int64) # N * num_tress * num_leafs
print("transformed_training_matrix.shape = ",transformed_training_matrix.shape)
print("len(y_pred[0]) = ",len(y_pred[0]))
# 這個for 循環是對每個樣本的GBDT 預測結果做one-hot
# np.arange(len(y_pred[0])) * num_leaf :是構建 100 200 300 這樣的序列,相隔100,每一個整數100 都是一個樹的表示
# np.array(y_pred[i]) : 把落在的 葉子節點 數值 表示出來,與上面的想加,就表示 每個樹的 葉子編碼
for i in range(0, len(y_pred)):
temp = np.arange(len(y_pred[0])) * num_leaf + np.array(y_pred[i])
#對二維數組填充信息,採用"+=" 的方法,其他都是0,temp命中的 有100位,都是 1
transformed_training_matrix[i][temp] += 1
print('Writing transformed testing data')
y_pred = gbm.predict(X_test, pred_leaf=True)
transformed_testing_matrix = np.zeros([len(y_pred), len(y_pred[0]) * 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])
# temp 的大小是 100 *1
# 對二維數組填充信息,採用"+=" 的方法,其他都是0,就100位是 1
transformed_testing_matrix[i][temp] += 1
print(" X shape = ",transformed_training_matrix.shape)
# logestic model construction
lm = LogisticRegression(penalty='l2',C=0.05)
# fitting the data
lm.fit(transformed_training_matrix,y_train)
y_pred_test = lm.predict_proba(transformed_testing_matrix) # Give the probabilty on each label
print(" y_pred_test.shape = ",y_pred_test.shape)
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))
xgb_lr_valid_auc = roc_auc_score(y_test,y_pred_test[:, 1])
print("lgb_train-LR valid AUC: %.5f" % xgb_lr_valid_auc)
X,y = load_breast_cancer(return_X_y=True)
'''獲取自變量數據的形狀'''
print(X.shape)
'''獲取因變量數據的形狀'''
print(y.shape)
xgb_lr_train(X,y)
lgb_train(X,y)
爲什麼採用LR+GBDT
LR是線性模型,學習能力有限,此時特徵工程尤其重要。現有的特徵工程實驗,主要集中在尋找到有區分度的特徵、特徵組合,折騰一圈未必會帶來效果提升。GBDT算法的特點正好可以用來發掘有區分度的特徵、特徵組合,減少特徵工程中人力成本,且業界現在已有實踐,GBDT+LR、GBDT+FM等都是值得嘗試的思路。不同場景,GBDT融合LR/FM的思路可能會略有不同,可以多種角度嘗試。
總結
我們思考這樣一個問題,
-
Logistic Regression是一個線性分類器,也就是說會忽略掉特徵與特徵之間的關聯信息,那麼是否可以採用構建新的交叉特徵這一特徵組合方式從而提高模型的效果?
-
其次,GBDT很有可能構造出的新訓練數據是高維的稀疏矩陣,而Logistic Regression使用高維稀疏矩陣進行訓練,會直接導致計算量過大,特徵權值更新緩慢的問題。
再次回到GBDT構造新訓練數據這裏。當GBDT構造完新的訓練樣本後,我們要做的是對每一個特徵做與輸出之間的特徵重要度評估並篩選出重要程度較高的部分特徵,這樣,GBDT構造的高維的稀疏矩陣就會減少一部分特徵,也就是說得到的稀疏矩陣不再那麼高維了。之後,對這些篩選後得到的重要度較高的特徵再做FM算法構造交叉項,進而引入非線性特徵,繼而完成最終分類器的訓練數據的構造及模型的訓練。
同時,用阿里蓋坤的話說,GBDT只是對歷史的一個記憶罷了,沒有推廣性,或者說泛化能力。但這並不是說對於大規模的離散特徵,GBDT和LR的方案不再適用,感興趣的話大家可以看一下參考文獻2和3,這裏就不再介紹了。提到了阿里的蓋坤大神,他的團隊在2017年提出了兩個重要的用於CTR預估的模型,MLR和DIN,之後的系列中,我們會講解這兩種模型的理論和實戰!歡迎大家繼續關注