参考:

论文:《Practical Lessons from Predicting Clicks on Ads at Facebook》

1、背景

本文主要介绍Facebook提出的CTR预估模型LR(Logistic Regression)+GBDT。当时深度学习还没有应用到计算广告领域,Facebook提出利用GBDT的叶节点编号作为非线性特征的表示,或者说是组合特征的一种方式。

LR+GBDT相比於单纯的LR或者GBDT带来了较大的性能提升,论文中给出数据为3%,这在CTR预估领域确实非常不错。除此之外,Facebook还在在线学习、Data freshness、学习速率、树模型参数、特征重要度等方面进行了探索。

相比于搜索广告领域,根据用户query来给出候选广告,然后利用Rank模型对候选广告进行排序。这些广告要么显式要么隐式的和用户query相关联。但是在Facebook这样的社交场合中,广告并没有和用户query相关联,但是用户看到的广告一定程度上反映了用户的人口统计特性和兴趣特性。基于这个原因,在Facebook上展示的广告相比于搜索广告中的要多一些。

在实际的生产环境中,Facebook做了多个分类器,并把他们级联起来。但是论文中分析的是最后的那一个prediction模型。它直接给出最后的CTR概率。

在介绍这个模型之前,我们先来介绍两个问题:

1)为什么要使用集成的决策树模型,而不是单棵的决策树模型:一棵树的表达能力很弱,不足以表达多个有区分性的特征组合,多棵树的表达能力更强一些。可以更好的发现有效的特征和特征组合

2)为什么建树采用GBDT而非RF:RF也是多棵树,但从效果上有实践证明不如GBDT。且GBDT前面的树,特征分裂主要体现对多数样本有区分度的特征;后面的树,主要体现的是经过前N颗树,残差仍然较大的少数样本。优先选用在整体上有区分度的特征,再选用针对少数样本有区分度的特征,思路更加合理,这应该也是用GBDT的原因。

了解了为什么要用GBDT,我们就来看看到底二者是怎么融合的吧!

2、评估函数

论文目的是分析机器学习模型的影响因素,所以没有使用实际利益相关的评测函数。而是主要从以下两方面进行:

  • Normalized Cross-Entropy 或者叫做 Normalized Entropy, 缩写NE
  • Calibration 校准

2.1、Normalized Cross-Entropy(NE)

NE的公式如下:

  • NE等于预测的log loss除以background CTR的熵
  • NE越小模型性能越好
  • 除以了background CTR的熵,使得NE对background CTR不敏感
  • p代表平均经验CTR

2.2、 Calibration

  • Calibration校准是期待或预测的点击数除以实际的点击数。它是一个比例。
  • Calibration越接近1,模型性能越好

AUC也是一个非常不错的评价指标,但是它有个问题。比如当我们的模型预测的CTR概率都偏高了2倍,我们可以通过Calibration校准,使用一个全局的0.5的系数来修正。修正之后NE也会提高,而AUC却保持不变。

在实际工作中,我们希望得到的是尽可能准确的预测每个广告被点击的概率,而不是仅仅得到相对的概率排序。所以AUC不如上面的NE、Calibration合适。

3、GBDT和LR的融合方案

GBDT和LR的融合方案,FaceBook的paper中有个例子:

图中共有两棵树,x为一条输入样本,遍历两棵树后,x样本分别落到两颗树的叶子节点上,每个叶子节点对应LR一维特征,那么通过遍历树,就得到了该样本对应的所有LR特征。构造的新特征向量是取值0/1的。举例来说:上图有两棵树,左树有三个叶子节点,右树有两个叶子节点,最终的特征即为五维的向量。对于输入x,假设他落在左树第一个节点,编码[1,0,0],落在右树第二个节点则编码[0,1],所以整体的编码为[1,0,0,0,1],这类编码作为特征,输入到LR中进行分类。

4、代码

训练GBDT模型

本文使用lightgbm包来训练我们的GBDT模型,训练共100棵树,每棵树有64个叶子结点。

import lightgbm as lgb

import pandas as pd
import numpy as np

from sklearn.metrics import mean_squared_error
from sklearn.linear_model import LogisticRegression

print(Load data...)
df_train = pd.read_csv(data/train.csv)
df_test = pd.read_csv(data/test.csv)

NUMERIC_COLS = [
"ps_reg_01", "ps_reg_02", "ps_reg_03",
"ps_car_12", "ps_car_13", "ps_car_14", "ps_car_15",
]

print(df_test.head(10))

y_train = df_train[target] # training label
y_test = df_test[target] # testing label
X_train = df_train[NUMERIC_COLS] # training dataset
X_test = df_test[NUMERIC_COLS] # testing dataset

# 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,
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(Save model...)
# save model to file
gbm.save_model(model.txt)

print(Start predicting...)
# predict and get data on leaves, training data

# 特征转换

# 在训练得到100棵树之后,我们需要得到的不是GBDT的预测结果,而是每一条训练数据落
#在了每棵树的哪个叶子结点上,因此需要使用下面的语句:

y_pred = gbm.predict(X_train, pred_leaf=True)# 8001*100 查看每个样本落在每棵树的第几个叶子上

print(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 8001*(100*64) 对每棵树的特征进行onehot处理,所以在y_pred的基础上每一列扩展成64列(每棵树有64个叶子)
for i in range(0, len(y_pred)):
temp = np.arange(len(y_pred[0])) * num_leaf + np.array(y_pred[i])#计算onehot在100*64列当中的位置 arange(100)*64 + y_pred[i]
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[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])
transformed_testing_matrix[i][temp] += 1

#然后我们可以用转换后的训练集特征和label训练我们的LR模型,并对测试集进行测试:
lm = LogisticRegression(penalty=l2,C=0.05) # logestic model construction L2惩罚 C为正则化系数λ的倒数,通常默认为1
lm.fit(transformed_training_matrix,y_train) # fitting the data
#我们这里得到的不是简单的类别,而是每个类别的概率。
y_pred_test = lm.predict_proba(transformed_testing_matrix) # Give the probabilty on each label

print(y_pred_test)

效果评价

在Facebook的paper中,模型使用NE(Normalized Cross-Entropy),进行评价,计算公式如下:

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))

5、总结

现在的GBDT和LR的融合方案真的适合现在的大多数业务数据么?现在的业务数据是什么?是大量离散特征导致的高维度离散数据。而树模型对这样的离散特征,是不能很好处理的,要说为什么,因为这容易导致过拟合。下面的一段话来自知乎:

用盖坤的话说,GBDT只是对历史的一个记忆罢了,没有推广性,或者说泛化能力。 但这并不是说对于大规模的离散特征,GBDT和LR的方案不再适用,感兴趣的话大家可以看一下参考文献2和3,这里就不再介绍了。 刚才提到了阿里的盖坤大神,他的团队在2017年提出了两个重要的用于CTR预估的模型,MLR和DIN,之后的系列中,我们会讲解这两种模型的理论和实战!欢迎大家继续关注!参考:推荐系统遇上深度学习(十)--GBDT+LR融合方案实战?

www.jianshu.com
图标

推荐阅读:
相关文章