用GBDT构建组合特征
一、理论
Facebook在2014年发表“Practical Lessons from Predicting Clicks on Ads at Facebook”,论文中提出经典的GBDT+LR的模型结构,开启特征工程模型化、自动化的新阶段。文章提出采用GBDT自动进行特征筛选和组合,进而生成新的特征向量,再把该特征向量作为LR模型的输入,预测CTR,模型结构如下图所示。其中采用GBDT构造新特征的方法可以使特征工程自动化,十分方便,这里介绍如何使用GBDT构建新的组合特征。
1.1 GBDT为何可以做特征组合
首先,我们知道GBDT是由许多回归树组成的森林,后一棵树采用前一颗树的预测结果与真实结果的残差来作为拟合目标,每棵树的生成过程都是一颗标准的回归树的生成过程;
因此每个节点的分裂都是在做特征选择,多层节点的结构自然构成有效的特征组合;
而且做特征选择的过程,是利用信息熵对样本作划分,选择最优特征和最优划分的过程,这样组合起来的特征可以有效划分样本,得到很好的特征组合,组合特征也具有一定的解释性。
1.2 如何构造组合特征
首先,我们先训练好的GBDT模型,然后用训练好的模型对数据做预测,便可得到组合特征。具体来说,一个样本在输入GBDT的一颗子树后,会根据每个节点的划分规则最终落入某一叶子节点,那么这个叶子节点置为1,其他节点置为0,所有叶子节点组成的向量形成了该棵树的特征向量,把GBDT所有子树的特征向量连接起来,即形成了后续LR模型输入的特征向量。
举例来说,比如GBDT由三颗子树构成,每个子树有4个叶子节点,一个训练样本进来后,先后落到了“子树1”的第3个叶节点中,那么特征向量就是[0,0,1,0],“子树2”的第1个叶节点,特征向量为[1,0,0,0],“子树3”的第4个叶节点,特征向量为[0,0,0,1],最后concatenate所有特征向量,形成的最终的特征向量为[0,0,1,0,1,0,0,0,0,0,0,1],我们再把该向量作为LR的输入,预测CTR。
在实际采用GBDT构造组合特征时,我们可以选择sklearn或者lightGBM中的GBDT模型来实现,由于GBDT模型最终输出的是每个样本在每棵树中落入叶子节点的索引,实际使用时,先得到所有样本在子树中叶子节点索引的数组,然后对每棵树的叶子节点做one-hot,即可得到所有样本的组合特征。
比如:1000个样本,采用GBDT训练得到10棵树的模型,最终生成1000×10×1的三维数组,其中:若索引为(1, 2, 1)的位置保存的值3,表示第一个样本在第二颗树中划分到第3个叶子节点。将三维数组重构为1000×10的二维数组,其中每一行表示一个样本,每一列表示一颗子树(每一列的值是树的叶子节点的索引位置),对每一颗子树做one-hot,即可得到所有样本的组合特征。
二、实现
下面采用sklearn和lightgbm两种方法构造组合特征:
sklearn:
import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import log_loss
from sklearn.preprocessing import OneHotEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
# baseline:LR
def lr_predict(X, y):
# 划分数据集
x_tr, x_val, y_tr, y_val = train_test_split(X, y, test_size=0.3, random_state=2019)
# LR
lr = LogisticRegression(solver='lbfgs')
lr.fit(x_tr, y_tr)
# 评估:log_loss
tr_logloss = log_loss(y_tr, lr.predict_proba(x_tr)[:, 1])
print("lr_tr: ", tr_logloss)
val_logloss = log_loss(y_val, lr.predict_proba(x_val)[:, 1])
print("lr_val", val_logloss)
# GBDT + LR
def gbdt_lr(X, y):
# 训练GBDT
gbc = GradientBoostingClassifier(n_estimators=50, random_state=2019, subsample=0.8, max_depth=6)
gbc.fit(X, y)
# 构造组合特征
gbc_leaf = gbc.apply(X)
gbc_feats = gbc_leaf.reshape(-1, 50) # 50棵树
enc = OneHotEncoder(categories='auto')
enc.fit(gbc_feats)
gbc_new_feature = np.array(enc.transform(gbc_feats).toarray())
# 原始特征&组合特征
X = np.concatenate((X, gbc_new_feature),axis=1)
# 划分数据集
x_train, x_val, y_train, y_val = train_test_split(X, y, test_size = 0.3, random_state = 2019)
# LR
lr = LogisticRegression(solver='lbfgs')
lr.fit(x_train, y_train)
# 评估:log_loss
tr_logloss = log_loss(y_train, lr.predict_proba(x_train)[:, 1])
print("gbdt_lr_tr: ", tr_logloss)
val_logloss = log_loss(y_val, lr.predict_proba(x_val)[:, 1])
print("gbdt_lr_val", val_logloss)
if __name__ == '__main__':
iris = datasets.load_iris()
X = iris.data[:100]
y = iris.target[:100]
lr_predict(X, y)
gbdt_lr(X, y)
输出:
lr_tr: 0.02796944108291242
lr_val 0.033278794811420216
gbdt_lr_tr: 0.003021625219689864
gbdt_lr_val 0.003096137856021479
明显可以看出,GBDT构造组合特征后,采用LR训练的模型log_loss更小。
其中:
-
model.apply(X_train)的用法:
model.apply(X)返回训练数据X在训练好的模型里每棵树中所处的叶子节点的位置(索引);
sklearn-apply -
sklearn.preprocessing 中OneHotEncoder的使用:
除了pandas中的 get_dummies(),sklearn也提供了一种对Dataframe做One-hot的方法;
OneHotEncoder() 首先fit() 过待转换的数据后,再次transform() 待转换的数据,就可实现对这些数据的所有特征进行One-hot 操作;
由于transform() 后的数据格式不能直接使用,所以最后需要使用.toarray() 将其转换为我们能够使用的数组结构。
enc.transform(gbc_feats).toarray()
lightgbm:
def lgb_predict(X, y):
x_train, x_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=2019)
print('开始训练gbdt..')
gbm = lgb.LGBMRegressor(objective='binary', subsample=0.8, max_depth=6, n_estimators=50)
gbm.fit(x_train, y_train,
eval_set=[(x_train, y_train), (x_val, y_val)],
eval_names=['train', 'val'],
eval_metric='binary_logloss',
)
model = gbm.booster_
print('训练得到叶子数')
gbm_feats = model.predict(X, pred_leaf=True)
gbc_feats = gbm_feats.reshape(-1, 50) # 50棵树
enc = OneHotEncoder(categories='auto')
enc.fit(gbc_feats)
gbc_new_feature = np.array(enc.transform(gbc_feats).toarray())
# 原始特征&组合特征
X = np.concatenate((X, gbc_new_feature), axis=1)
# 划分数据集
x_train, x_val, y_train, y_val = train_test_split(X, y, test_size=0.3, random_state=2019)
# LR
lr = LogisticRegression(solver='lbfgs')
lr.fit(x_train, y_train)
# 评估:log_loss
tr_logloss = log_loss(y_train, lr.predict_proba(x_train)[:, 1])
print("gbdt_lr_tr: ", tr_logloss)
val_logloss = log_loss(y_val, lr.predict_proba(x_val)[:, 1])
print("gbdt_lr_val", val_logloss)