关于机器学习,很多地方提到GBDT+LR这类炼金方法,常用于CTR预估的二分类问题。
但该方法具体是如何工作的?又该如何实践呢?本文不涉及原理,仅从肤浅的表面告诉大家如何理解它,使用它。
xgboost模型
GBDT是一种树模型,xgboost是变种(更快更强?),它本质上通过多颗树决定预测结果。
盗用网上图片,给定如下的样本,特征包括Age、is male、Use computer daily,标签是LIKE GC。
经过xgboost模型训练,如果我们指定构建2颗树,那么可能会得到这样的xgboost模型:
用训练好的上述模型做预测,每个样本将落入2颗树中的叶子节点:
模型的输出就是叶子值的sum:
树模型的优势就是对特征预处理要求低,连续特征不需要归一化,分类特征不需要onehot,也能得到不错的效果,xgboost实现甚至能自动处理Nan值。
直接用xgboost模型作分类预测本身就收效不错。
特征组合
我们都知道线性模型原理简单,Logistic Regression(LR)就是代表。
y=w1*x1+w2*x2+…wn*xn
线性模型容易解释结果,计算性能简单粗暴,能够支持更多的特征参与计算。
简单的加权求和计算复杂度有限,无法发现特征之间的组合关系,因此使用LR通常需要进行特征组合。
简单的特征工程可以将特征两两相乘作为二项式特征补充到特征向量中,或者基于对数据的理解进行更加定制化的处理。显然人工发现特征间关系还是成本较高的,能否自动化特征组合呢?
facebook在2014年提出利用训练好的GDBT模型,自动完成特征组合。
当我们将训练样本(比如红衣服小姑娘)再次投入到已经训练好的xgboost模型时,样本会落入2颗树的叶子节点。
小姑娘落在tree1的第2个叶子,tree2的第2个叶子,因此经过onehot编码可以得到上述向量。
在tree1中,小姑娘实际满足了age<15 && is male is N的条件从而才能落到第2个叶子,因此第2个叶子就代表了这两个特征在某种场景下的组合,tree2是同理的。
该样本经过xgboost模型得到的组合向量,可以补充到原始样本中,最终作为LR模型的训练输入,这就是xgboost+LR模型的工作原理了。
实践环节
下面使用sklearn的cancer癌症数据集进行演示,我会训练3种模型做对比:
- xgboost
- lr
- xgboost+lr
引入包
我们用pandas格式加载数据集,这样每个特征都有一个名字,符合生产环境的工作方式。
1 2 3 4 5 6 7 8 9 |
from sklearn.datasets import load_breast_cancer from sklearn.model_selection import KFold, ParameterGrid from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline import xgboost as xgb import pandas as pd import numpy as np |
加载数据集
data属性是一个DataFrame,特征太多就不贴上来了。
1 2 3 |
# 乳腺癌数据集,2分类问题,30个特征 dataset = load_breast_cancer(as_frame=True) dataset.data.head() |
xgboost模型训练(定义)
1 2 3 4 5 |
# xgboost模型 def fit_xgboost_model(params, X_train, y_train): xgb_model = xgb.XGBClassifier(n_estimators=params['n_estimators'], max_depth=params['max_depth'], random_state=1) xgb_model.fit(X_train, y_train) return xgb_model |
传入模型参数,训练样本,返回一个训练好的xgboost模型,很简单吧?
我传入的2个参数非常重要:
- n_estimators:树的棵树。
- max_depth:单颗树的最大深度(二叉树)。
LR模型训练(定义)
1 2 3 4 5 |
# lr模型 def fit_lr_model(params, X_train, y_train): lr_model = LogisticRegression(max_iter=params['max_iter'], n_jobs=-1, random_state=1) lr_model.fit(X_train, y_train) return lr_model |
将训练集投入到模型中训练max_iter次。
xgboost特征组合(定义)
1 2 3 4 5 6 7 8 9 10 |
# xgboost特征生成 def xgboost_extend_features(xgb_model, X): # 命名xgboost每棵树输出的下标 tree_columns = ['tree_{}'.format(i_estimator) for i_estimator in range(xgb_model.n_estimators)] # xgboost模型给出所有树的叶子节点下标 X_leaf = pd.DataFrame(xgb_model.apply(X), columns=tree_columns) # 连接原始特征+组合特征 return pd.concat([X, X_leaf], axis=1, join='inner'), tree_columns |
传入训练好的xgboost模型,原始样本X。
如果xgb模型有n_estimators颗树,那么1个样本就对应n_estimators个叶子下标,暂时不做onehot编码的话,它们就是组合产生的n_estimators个特征。
xgboost模型的apply方法将返回X中每个样本的组合特征向量,我们把它们拼到原始特征后面返回。
xgboost+LR模型训练(定义)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# xgboost+lr模型 def fit_xgboost_lr_model(xgb_model, params, X_train, y_train): # xgboost组合特征 X_train, tree_columns = xgboost_extend_features(xgb_model, X_train) # 根据xbgoost单树的深度,计算叶子节点的最大数目,决定onehot编码0的个数 max_leaf_count = 2 ** (xgb_model.max_depth + 1) # 独热编码tree_{}组合特征,指定每列的0个数 tree_onehot = OneHotEncoder(categories=[list(range(max_leaf_count))] * xgb_model.n_estimators) tree_onehot_with_columns = ColumnTransformer([('onehot', tree_onehot, tree_columns)], remainder='passthrough') xgb_lr_model = Pipeline([ ('tree_onehot', tree_onehot_with_columns), ('lr_model', LogisticRegression(max_iter=params['max_iter'], n_jobs=-1, random_state=1)), ]) xgb_lr_model.fit(X_train, y_train) return xgb_lr_model |
传入已经训练好的xgb模型,超参数,以及训练样本。
假设xgb模型的n_estimators是10颗树,那么特征组合后就多了tree_0~tree_9共10个特征。
为了对这10个特征(10颗树)分别做onehot,我们需要知道每棵树最多可能有多少个叶子,这取决于xgb的树最大深度,我们不关心每棵树的具体长相,直接指定为2^(树深+1)种可能取值即可满足所有情况。
最终的模型是Pipeline,首先对tree_0~tree_9做onehot,然后再输入给LR模型。
参数搜索
1 2 3 4 5 6 7 8 9 |
# 数据集拆分5次,训练5组模型 kfold = KFold(n_splits=5, shuffle=True, random_state=4) # 超参数搜索参数组合 param_grid = ParameterGrid({ 'n_estimators': list(range(10,11)), 'max_depth': list(range(3, 4)), 'max_iter': list(range(5000, 5001)) }) |
我们可以做超参数搜索,这是可选的。
开始训练
对于每一种参数组合,我们进行K折训练与打分,得到K次的最高分作为本参数组合的最终得分。
当所有参数组合都执行完成后,输出所有参数组合的最高分值。
在整个过程中,我同时训练了xgboost、lr、xgboost+lr三种模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# 统计不同参数组合的得分 xgboost_score = [] # xgboost模型 lr_score = [] # lr模型 xgboost_lr_score = [] # xgboost_lr模型 # 对于每种参数组合 for params in param_grid: # 统计K折的结果 kfolds_xgboost_scores = [] kfolds_lr_scores = [] kfolds_xgboost_lr_scores = [] for X_train_index, X_test_index in kfold.split(dataset.data): # 训练集 X_train = dataset.data.iloc[X_train_index].reset_index() y_train = dataset.target[X_train_index] # 测试集 X_test = dataset.data.iloc[X_test_index].reset_index() y_test = dataset.target[X_test_index] # 训练xgboost xgb_model = fit_xgboost_model(params, X_train, y_train) kfolds_xgboost_scores.append(xgb_model.score(X_test,y_test)) # 训练lr lr_model = fit_lr_model(params, X_train, y_train) kfolds_lr_scores.append(lr_model.score(X_test, y_test)) # 训练xgboost+lr xgb_lr_model = fit_xgboost_lr_model(xgb_model, params, X_train, y_train) X_test,_ = xgboost_extend_features(xgb_model, X_test) kfolds_xgboost_lr_scores.append(xgb_lr_model.score(X_test, y_test)) xgboost_score.append(max(kfolds_xgboost_scores)) lr_score.append(max(kfolds_lr_scores)) xgboost_lr_score.append(max(kfolds_xgboost_lr_scores)) |
最终结果:
xgboost_score: 0.9736842105263158
lr_score: 0.9823008849557522
xgboost_lr_score 0.9912280701754386
因为数据集的切分存在随机性,结果存在一定的运气成分,但是大部分时候xgboost+LR的表现都是相对较好的。
总结
xgboost显然提供了一种特征组合能力,这类似于深度学习中的embadding层。
有兴趣可以看看美团的实践历程:https://www.infoq.cn/article/vkZxPzZReHIg9au-o10F
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

1
1