前言
晚上很多内容讲解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,之后的系列中,我们会讲解这两种模型的理论和实战!欢迎大家继续关注
参考博客
GBDT+LR算法解析及Python实现
腾讯大数据:CTR预估中GBDT与LR融合方案
CTR预估经典模型总结
盘点前深度学习时代阿里、谷歌、Facebook 的 CTR 预估模型
谷歌、阿里等 10 大深度学习 CTR 模型最全演化图谱
推荐系统工程师必看:Embedding 技术在深度学习 CTR 模型中的应用