笔者最近再学习腾讯广告算法大赛,发现一些选择会用GBDT来进行“降维”与特征工程,有提分点,于是乎也来看看。之前的一篇跟LightGBM相关的文章:python - 机器学习lightgbm相关实践
这里可以直接跑通的github:wangru8080/gbdt-lr
1 GBDT LR原理
参考:GBDT LR算法解析及Python实现
1.1 CTR常见流程
GBDT LR 使用最广泛的场景是CTR点击率预估,即预测当给用户推送的广告会不会被用户点击。
点击率预估模型涉及的训练样本一般是上亿级别,样本量大,模型常采用速度较快的LR。但LR是线性模型,学习能力有限,此时特征工程尤其重要。现有的特征工程实验,主要集中在寻找到有区分度的特征、特征组合,折腾一圈未必会带来效果提升。GBDT算法的特点正好可以用来发掘有区分度的特征、特征组合,减少特征工程中人力成本。
从知乎https://zhuanlan.zhihu.com/p/29053940上看到了一个关于CTR的流程,如下图所示:
如上图,主要包括两大部分:离线部分、在线部分,其中离线部分目标主要是训练出可用模型,而在线部分则考虑模型上线后,性能可能随时间而出现下降,弱出现这种情况,可选择使用Online-Learning来在线更新模型.
1.2 RF/GBDT哪个更适合
RF也是多棵树,但从效果上有实践证明不如GBDT。且GBDT前面的树,特征分裂主要体现对多数样本有区分度的特征;后面的树,主要体现的是经过前N颗树,残差仍然较大的少数样本。优先选用在整体上有区分度的特征,再选用针对少数样本有区分度的特征,思路更加合理,这应该也是用GBDT的原因。
1.3 GBDT LR比 FM进步在哪?
(读论文)推荐系统之ctr预估-LR与GBDT LR模型解析
特征交叉而提出的FM和FFM虽然能够较好地解决数据稀疏性的问题,但他们仍停留在二阶交叉的情况。
Facebook提出了一种利用GBDT(Gradient Boosting Decision Tree)自动进行特征筛选和组合,进而生成新的离散特征向量,再把该特征向量当作LR模型输入,预估CTR的模型结构。
由于决策树的结构特点,事实上,决策树的深度就决定了特征交叉的维度。如果决策树的深度为4,通过三次节点分裂,最终的叶节点实际上是进行了3阶特征组合后的结果,如此强的特征组合能力显然是FM系的模型不具备的。但由于GBDT容易产生过拟合,以及GBDT这种特征转换方式实际上丢失了大量特征的数值信息,因此我们不能简单说GBDT由于特征交叉的能力更强,效果就比FM或FFM好(事实上FFM是2015年提出的)。在模型的选择和调试上,永远都是多种因素综合作用的结果。
GBDT LR比FM重要的意义在于,它大大推进了特征工程模型化这一重要趋势,某种意义上来说,之后深度学习的各类网络结构,以及embedding技术的应用,都是这一趋势的延续。
1.3 树模型对稀疏离散特征,处理较差
参考:
- 腾讯大数据:CTR预估中GBDT与LR融合方案
- 推荐系统遇上深度学习(十)–GBDT LR融合方案实战
GBDT只是对历史的一个记忆罢了,没有推广性,或者说泛化能力。
但这并不是说对于大规模的离散特征,GBDT和LR的方案不再适用,感兴趣的话大家可以看一下参考文献2和3,这里就不再介绍了。
2 LightGBM LR融合案例
一段核心代码,整体流程为:
代码语言:javascript复制源数据 -> 标准化 -> 训练LGM模型 -> 预测训练集 验证集的每个样本落在每棵树的哪个节点上 -> LGB的节点特征合并成为新的训练集/验证集
def gbdt_ffm_predict(data, category_feature, continuous_feature):
# 离散特征one-hot编码
print('开始one-hot...')
for col in category_feature:
onehot_feats = pd.get_dummies(data[col], prefix = col)
data = pd.concat([data, onehot_feats], axis = 1)
print('one-hot结束')
feats = [col for col in data if col not in category_feature] # onehot_feats continuous_feature
tmp = data[feats]
train = tmp[tmp['Label'] != -1]
target = train.pop('Label')
test = tmp[tmp['Label'] == -1]
test.drop(['Label'], axis = 1, inplace = True)
# 划分数据集
print('划分数据集...')
x_train, x_val, y_train, y_val = train_test_split(train, target, test_size = 0.2, random_state = 2018)
print('开始训练gbdt..')
gbm = lgb.LGBMRegressor(objective='binary',
subsample= 0.8,
min_child_weight= 0.5,
colsample_bytree= 0.7,
num_leaves=100,
max_depth = 12,
learning_rate=0.05,
n_estimators=10,
)
gbm.fit(x_train, y_train,
eval_set = [(x_train, y_train), (x_val, y_val)],
eval_names = ['train', 'val'],
eval_metric = 'binary_logloss',
# early_stopping_rounds = 100,
)
model = gbm.booster_
print('训练得到叶子数')
gbdt_feats_train = model.predict(train, pred_leaf = True) # 获得训练集的各颗树的节点数(10棵树,每棵树100个叶子节点)
train.shape,gbdt_feats_train.shape # ((1599, 13104), (1599, 10)) 从13104维度 降维到10维
gbdt_feats_test = model.predict(test, pred_leaf = True) # 获得验证集的各颗树的节点数(10棵树,每棵树100个叶子节点)
gbdt_feats_name = ['gbdt_leaf_' str(i) for i in range(gbdt_feats_train.shape[1])]
df_train_gbdt_feats = pd.DataFrame(gbdt_feats_train, columns = gbdt_feats_name)
df_test_gbdt_feats = pd.DataFrame(gbdt_feats_test, columns = gbdt_feats_name)
print('构造新的数据集...')
tmp = data[category_feature continuous_feature ['Label']]
train = tmp[tmp['Label'] != -1]
test = tmp[tmp['Label'] == -1]
train = pd.concat([train, df_train_gbdt_feats], axis = 1)
test = pd.concat([test, df_test_gbdt_feats], axis = 1)
data = pd.concat([train, test])
del train
del test
gc.collect()
# 连续特征归一化
print('开始归一化...')
scaler = MinMaxScaler()
for col in continuous_feature:
data[col] = scaler.fit_transform(data[col].values.reshape(-1, 1))
print('归一化结束')
data.to_csv('data/data.csv', index = False)
return category_feature gbdt_feats_name
先来看一下LGBMClassifier
,参考:lightgbm.LGBMClassifier
如下:
classlightgbm.LGBMClassifier(boosting_type='gbdt', num_leaves=31, max_depth=- 1, learning_rate=0.1, n_estimators=100, subsample_for_bin=200000, objective=None, class_weight=None, min_split_gain=0.0, min_child_weight=0.001, min_child_samples=20, subsample=1.0, subsample_freq=0, colsample_bytree=1.0, reg_alpha=0.0, reg_lambda=0.0, random_state=None, n_jobs=- 1, silent=True, importance_type='split', **kwargs)
其中:
- n_estimators - 树的棵树,相当于主成分,多少个主成分一样
- num_leaves,叶子节点
- max_depth (int, optional (default=-1)) – Maximum tree depth for base learners,树的深度
model.predict(train, pred_leaf = True)
这里通过pred_leaf
(pred_leaf (bool, optional (default=False)) – Whether to predict leaf index.)就可以预测每个样本在叶子节点的位置:
train.shape,gbdt_feats_train.shape
# ((1599, 13104), (1599, 10))
gbdt_feats_train
>>> array([[ 2, 9, 3, ..., 2, 14, 4],
[ 1, 8, 1, ..., 20, 16, 1],
[18, 35, 39, ..., 38, 24, 29],
...,
[44, 18, 13, ..., 8, 17, 23],
[23, 20, 17, ..., 4, 0, 30],
[20, 24, 0, ..., 28, 22, 36]])
从13104维度 降维到10维(树的棵树),然后每个样本标记的,在10棵树的叶子位置(每个样本(1599)在10颗树的叶子(100片叶子)节点的编号)