第四章 GBDT+LR
前言
前面介绍的协同过滤和矩阵分解存在的劣势就是仅仅利用了用户与物品相互行为信息进行推荐,忽视了用户自身特征,物品自身特征以及上下文信息等,导致生成的结果往往会比较片面。而这次介绍的是在2014年由Facebook提出的GBDT+LR模型,该模型利用GBDT自动进行特征筛选和组合,进而生成新的离散特征向量,再把该特征向量当做LR模型的输入,来产生最后的预测结果,该模型能够综合利用用户、物品和上下文等多种不同的特征,生成较为全面的推荐结果,在CTR点击率预估场景下使用较为广泛。
下面首先会介绍GBDT和LR模型各自的原理及优缺点, 然后介绍GBDT+LR模型的工作原理和细节。
4.1 逻辑回归模型
逻辑回归算法(Logistics Regression,LR) 是一种基于回归分析的分类算法。LR算法与线性回归算法非常相似,然而线性回归能够处理的是数值问题,而LR算法则是使用sigmoid函数将线性回归的分析结果转换为概率值。LR算法是最简单和最快速的分类模型之一,在具有线性分离边界的数据集上表现良好,其表达式为
逻辑回归模型非常重要,在推荐领域里面,相比于传统的协同过滤,逻辑回归模型能够综合利用用户、物品、上下文等多种不同的特征生成较为“全面”的推荐结果。
逻辑回归是在线性回归的基础上加了一个 Sigmoid 函数(非线形)映射,使得逻辑回归成为了一个优秀的分类算法, 学习逻辑回归模型,首先应该记住一句话:逻辑回归假设数据服从伯努利分布,通过极大化似然函数的方法,运用梯度下降来求解参数,来达到将数据二分类的目的。
相比于协同过滤和矩阵分解利用用户的物品“相似度”进行推荐, 逻辑回归模型将问题看成了一个分类问题,通过预测正样本的概率对物品进行排序。这里的正样本可以是用户“点击”了某个商品或者“观看”了某个视频,均是推荐系统希望用户产生的“正反馈”行为, 因此逻辑回归模型将推荐问题转化成了一个点击率预估问题。而点击率预测就是一个典型的二分类, 正好适合逻辑回归进行处理,那么逻辑回归是如何做推荐的呢?它的过程如下
- 将用户年龄、性别、物品属性、物品描述、当前时间、当前地点等特征转成数值型向量
- 确定逻辑回归的优化目标,比如把点击率预测转换成二分类问题, 这样就可以得到分类问题常用的损失作为目标,训练模型
- 在预测的时候,将特征向量输入模型产生预测, 得到用户“点击”物品的概率
- 利用点击概率对候选物品排序,得到推荐列表
推断过程可以用下图来表示:
这里的关键就是每个特征的权重参数, 我们一般是使用梯度下降的方式, 首先会先随机初始化参数, 然后将特征向量(也就是我们上面数值化出来的特征)输入到模型, 就会通过计算得到模型的预测概率, 然后通过对目标函数求导得到每个的梯度, 然后进行更新。
目标函数如下
%3D-%5Cfrac%7B1%7D%7Bm%7D%5Cleft(%5Csum%7Bi%3D1%7D%5E%7Bm%7D%5Cleft(y%5E%7Bi%7D%20%5Clog%20f%7Bw%7D%5Cleft(x%5E%7Bi%7D%5Cright)%2B%5Cleft(1-y%5E%7Bi%7D%5Cright)%20%5Clog%20%5Cleft(1-f%7Bw%7D%5Cleft(x%5E%7Bi%7D%5Cright)%5Cright)%5Cright)%5Cright.%0A#card=math&code=J%28w%29%3D-%5Cfrac%7B1%7D%7Bm%7D%5Cleft%28%5Csum%7Bi%3D1%7D%5E%7Bm%7D%5Cleft%28y%5E%7Bi%7D%20%5Clog%20f%7Bw%7D%5Cleft%28x%5E%7Bi%7D%5Cright%29%2B%5Cleft%281-y%5E%7Bi%7D%5Cright%29%20%5Clog%20%5Cleft%281-f%7Bw%7D%5Cleft%28x%5E%7Bi%7D%5Cright%29%5Cright%29%5Cright%29%5Cright.%0A&id=nVWHQ)
对其求导,得
-y%5E%7Bi%7D%5Cright)%20x%7Bj%7D%5E%7Bi%7D%0A#card=math&code=w%7Bj%7D%20%5Cleftarrow%20w%7Bj%7D-%5Cgamma%20%5Cfrac%7B1%7D%7Bm%7D%20%5Csum%7Bi%3D1%7D%5E%7Bm%7D%5Cleft%28f%7Bw%7D%5Cleft%28x%5E%7Bi%7D%5Cright%29-y%5E%7Bi%7D%5Cright%29%20x%7Bj%7D%5E%7Bi%7D%0A&id=yIvUz)
这样通过若干次迭代, 就可以得到最终的了。最后我们分析一下逻辑回归模型的优缺点。
- 优点:
- LR模型形式简单,可解释性好,从特征的权重可以看到不同的特征对最后结果的影响。
- 训练时便于并行化,在预测时只需要对特征进行线性加权,所以性能比较好,往往适合处理海量id类特征,用id类特征有一个很重要的好处,就是防止信息损失(相对于范化的 CTR 特征),对于头部资源会有更细致的描述。
- 资源占用小,尤其是内存。在实际的工程应用中只需要存储权重比较大的特征及特征对应的权重。
- 方便输出结果调整。逻辑回归可以很方便的得到最后的分类结果,因为输出的是每个样本的概率分数,我们可以很容易的对这些概率分数进行cutoff,也就是划分阈值(大于某个阈值的是一类,小于某个阈值的是一类)。
局限性:
- 表达能力不强, 无法进行特征交叉, 特征筛选等一系列“高级“操作(这些工作都得人工来干, 这样就需要一定的经验, 否则会走一些弯路), 因此可能造成信息的损失
- 准确率并不是很高。因为这毕竟是一个线性模型加了个sigmoid, 形式非常的简单(非常类似线性模型),很难去拟合数据的真实分布
- 处理非线性数据较麻烦。逻辑回归在不引入其他方法的情况下,只能处理线性可分的数据, 如果想处理非线性, 首先对连续特征的处理需要先进行离散化(离散化的目的是为了引入非线性),如上文所说,人工分桶的方式会引入多种问题。
- LR 需要进行人工特征组合,这就需要开发者有非常丰富的领域经验,才能不走弯路。这样的模型迁移起来比较困难,换一个领域又需要重新进行大量的特征工程。
所以如何自动发现有效的特征、特征组合,弥补人工经验不足,缩短LR特征实验周期,是亟需解决的问题, 而GBDT模型, 正好可以自动发现特征并进行有效组合。
所以, 我们发现其实GBDT和LR的优缺点可以进行互补。
4.2 GBDT模型
集成学习是一种协同多个“个体学习器”完成任务的学习方法,其原理是使用某种方式将多个学习器进行集成,以此获得比单一学习器更优越的泛化性能。梯度提升决策树(Gradient Boosting Decison Tree、GBDT) 由Friedman于1999年提出,是一种Boost类集成学习算法。其核心思想是通过多轮迭代产生多个弱分类器,在每一次迭代后计算损失函数的负梯度,将其作为残差的近似值。在GBDT分类模型中,一般使用CART回归树作为基学习器,每个分类器的训练都是基于上一轮分类器预测结果的残差,以串行的方式向残差减小的方向进行梯度迭代,最后将每个弱分类器得到的结果进行加权求和得到最终的分类器。
GBDT是在传统机器学习算法里面是对真实分布拟合的最好的几种算法之一,在前几年深度学习还没有大行其道之前,GBDT在各种竞赛是大放异彩。原因大概有几个,一是效果确实挺不错,二是即可以用于分类也可以用于回归,三是可以筛选特征, 所以这个模型依然是一个非常重要的模型。
GBDT通过采用加法模型(即基函数的线性组合),以及不断减小训练过程产生的误差来达到将数据分类或者回归的算法, 其训练过程如下:
GBDT通过多轮迭代,每轮迭代会产生一个弱分类器,每个分类器在上一轮分类器的残差基础上进行训练。GBDT对弱分类器的要求一般是足够简单,并且低方差高偏差。因为训练的过程是通过降低偏差来不断提高最终分类器的精度。由于上述高偏差和简单的要求,每个分类回归树的深度不会很深。最终的总分类器是将每轮训练得到的弱分类器加权求和得到的(也就是加法模型)。
现在分析一下GBDT是如何进行二分类的,首先我们要明确一点就是GBDT每轮的训练是在上一轮的训练的残差基础之上进行训练的, 而这里的残差指的就是当前模型的负梯度值, 这个就要求每轮迭代的时候,弱分类器的输出的结果相减是有意义的, 而GBDT无论用于分类还是回归一直都是使用的CART回归树, 那么既然是回归树, 是如何进行二分类问题的呢?
GBDT 来解决二分类问题和解决回归问题的本质是一样的,都是通过不断构建决策树的方式,使预测结果一步步的接近目标值, 但是二分类问题和回归问题的损失函数是不同的, 回归问题中一般使用的是平方损失, 而二分类问题中, GBDT和逻辑回归一样, 使用的下面这个公式:
%2B%5Cleft(1-y%7Bi%7D%5Cright)%20%5Clog%20%5Cleft(1-p%7Bi%7D%5Cright)%5Cright)%5Cright%5D%0A#card=math&code=L%3D%5Carg%20%5Cmin%20%5Cleft%5B%5Csum%7Bi%7D%5E%7Bn%7D-%5Cleft%28y%7Bi%7D%20%5Clog%20%5Cleft%28p%7Bi%7D%5Cright%29%2B%5Cleft%281-y%7Bi%7D%5Cright%29%20%5Clog%20%5Cleft%281-p_%7Bi%7D%5Cright%29%5Cright%29%5Cright%5D%0A&id=HdgKb)
其中,是第i个样本的观测值, 取值要么是0要么是1, 而是第i个样本的预测值, 取值是0-1之间的概率,由于我们知道GBDT拟合的残差是当前模型的负梯度, 那么我们就需要求出这个模型的导数, 即, 对于某个特定的样本, 求导的话就可以只考虑它本身, 去掉加和号, 那么就变成了, 其中单个样本的损失的计算如下:
-%5Cleft(1-y%7Bi%7D%5Cright)%20%5Clog%20%5Cleft(1-p%7Bi%7D%5Cright)%20%5C%5C%0A%26%3D-y%7Bi%7D%20%5Clog%20%5Cleft(p%7Bi%7D%5Cright)-%5Clog%20%5Cleft(1-p%7Bi%7D%5Cright)%2By%7Bi%7D%20%5Clog%20%5Cleft(1-p%7Bi%7D%5Cright)%20%5C%5C%0A%26%3D-y%7Bi%7D%5Cleft(%5Clog%20%5Cleft(%5Cfrac%7Bp%7Bi%7D%7D%7B1-p%7Bi%7D%7D%5Cright)%5Cright)-%5Clog%20%5Cleft(1-p%7Bi%7D%5Cright)%0A%5Cend%7Baligned%7D%0A#card=math&code=%5Cbegin%7Baligned%7D%0Al%20%26%3D-y%7Bi%7D%20%5Clog%20%5Cleft%28p%7Bi%7D%5Cright%29-%5Cleft%281-y%7Bi%7D%5Cright%29%20%5Clog%20%5Cleft%281-p%7Bi%7D%5Cright%29%20%5C%5C%0A%26%3D-y%7Bi%7D%20%5Clog%20%5Cleft%28p%7Bi%7D%5Cright%29-%5Clog%20%5Cleft%281-p%7Bi%7D%5Cright%29%2By%7Bi%7D%20%5Clog%20%5Cleft%281-p%7Bi%7D%5Cright%29%20%5C%5C%0A%26%3D-y%7Bi%7D%5Cleft%28%5Clog%20%5Cleft%28%5Cfrac%7Bp%7Bi%7D%7D%7B1-p%7Bi%7D%7D%5Cright%29%5Cright%29-%5Clog%20%5Cleft%281-p%7Bi%7D%5Cright%29%0A%5Cend%7Baligned%7D%0A&id=tT3aG)
如果对逻辑回归非常熟悉的话, %5Cright)#card=math&code=%5Cleft%28%5Clog%20%5Cleft%28%5Cfrac%7Bp%7Bi%7D%7D%7B1-p%7Bi%7D%7D%5Cright%29%5Cright%29&id=pK9Av)一定不会陌生吧, 这就是对几率比取了个对数,并且在逻辑回归里面这个式子会等于, 所以才推出了的那个形式。 这里令(这个式子表示几率,即事件发生与不发生的概率的比值),即,则上面这个式子变成了:
-%5Clog%20%5Cleft(1-%5Cfrac%7Be%5E%7B%5Clog%20%5Cleft(%5Ceta%7Bi%7D%5Cright)%7D%7D%7B1%2Be%5E%7B%5Clog%20%5Cleft(%5Ceta%7Bi%7D%5Cright)%7D%7D%5Cright)%20%5C%5C%0A%26%3D-y%7Bi%7D%20%5Clog%20%5Cleft(%5Ceta%7Bi%7D%5Cright)-%5Clog%20%5Cleft(%5Cfrac%7B1%7D%7B1%2Be%5E%7B%5Clog%20%5Cleft(%5Ceta%7Bi%7D%5Cright)%7D%7D%5Cright)%20%5C%5C%0A%26%3D-y%7Bi%7D%20%5Clog%20%5Cleft(%5Ceta%7Bi%7D%5Cright)%2B%5Clog%20%5Cleft(1%2Be%5E%7B%5Clog%20%5Cleft(%5Ceta%7Bi%7D%5Cright)%7D%5Cright)%0A%5Cend%7Baligned%7D%0A#card=math&code=%5Cbegin%7Baligned%7D%0Al%20%26%3D-y%7Bi%7D%20%5Clog%20%5Cleft%28%5Ceta%7Bi%7D%5Cright%29-%5Clog%20%5Cleft%281-%5Cfrac%7Be%5E%7B%5Clog%20%5Cleft%28%5Ceta%7Bi%7D%5Cright%29%7D%7D%7B1%2Be%5E%7B%5Clog%20%5Cleft%28%5Ceta%7Bi%7D%5Cright%29%7D%7D%5Cright%29%20%5C%5C%0A%26%3D-y%7Bi%7D%20%5Clog%20%5Cleft%28%5Ceta%7Bi%7D%5Cright%29-%5Clog%20%5Cleft%28%5Cfrac%7B1%7D%7B1%2Be%5E%7B%5Clog%20%5Cleft%28%5Ceta%7Bi%7D%5Cright%29%7D%7D%5Cright%29%20%5C%5C%0A%26%3D-y%7Bi%7D%20%5Clog%20%5Cleft%28%5Ceta%7Bi%7D%5Cright%29%2B%5Clog%20%5Cleft%281%2Be%5E%7B%5Clog%20%5Cleft%28%5Ceta%7Bi%7D%5Cright%29%7D%5Cright%29%0A%5Cend%7Baligned%7D%0A&id=EgEIf)
此时,我们对#card=math&code=log%28%CE%B7_i%29&id=yZCUd)求导,有
%7D%3D-y%7Bi%7D%2B%5Cfrac%7Be%5E%7B%5Clog%20%5Cleft(%5Ceta%7Bi%7D%5Cright)%7D%7D%7B1%2Be%5E%7B%5Clog%20%5Cleft(%5Ceta%7Bi%7D%5Cright)%7D%7D%3D-y_i%2Bp_i%0A#card=math&code=%5Cfrac%7Bd%20l%7D%7Bd%20%5Clog%20%28%5Ceta_i%29%7D%3D-y%7Bi%7D%2B%5Cfrac%7Be%5E%7B%5Clog%20%5Cleft%28%5Ceta%7Bi%7D%5Cright%29%7D%7D%7B1%2Be%5E%7B%5Clog%20%5Cleft%28%5Ceta%7Bi%7D%5Cright%29%7D%7D%3D-y_i%2Bp_i%0A&id=tpxEU)
这样, 我们就得到了某个训练样本在当前模型的梯度值了, 那么残差就是。GBDT二分类的这个思想,其实和逻辑回归的思想一样,逻辑回归是用一个线性模型去拟合#card=math&code=P%28y%3D1%7Cx%29&id=QdWUn)这个事件的对数几率, GBDT二分类也是如此, 用一系列的梯度提升树去拟合这个对数几率, 其分类模型可以表达为:
%3D%5Cfrac%7B1%7D%7B1%2Be%5E%7B-F%7BM%7D(x)%7D%7D%0A#card=math&code=P%28Y%3D1%20%5Cmid%20x%29%3D%5Cfrac%7B1%7D%7B1%2Be%5E%7B-F%7BM%7D%28x%29%7D%7D%0A&id=jkM5y)
下面我们具体来看GBDT的生成过程, 构建分类GBDT的步骤有两个:
- 初始化GBDT
和回归问题一样,分类 GBDT 的初始状态也只有一个叶子节点,该节点为所有样本的初始预测值,如下: %3D%5Carg%20%5Cmin%20%7B%5Cgamma%7D%20%5Csum%7Bi%3D1%7D%5E%7Bn%7D%20L(y%2C%20%5Cgamma)%0A#card=math&code=F%7B0%7D%28x%29%3D%5Carg%20%5Cmin%20%7B%5Cgamma%7D%20%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%20L%28y%2C%20%5Cgamma%29%0A&id=V3m9U)
其中,F代表GBDT模型,是模型的初始状态,该式的意思是找到一个,是所有样本的Loss最小,在这里及下文中,都表示节点的输出,即叶子节点,且它是一个#card=math&code=log%28%5Cgamma_i%29&id=GSvhO)形式的值(回归值),在初始状态,
下面来看一个例子,假设我们有下面三条样本:
我们希望构建 GBDT 分类树,它能通过「喜欢爆米花」、「年龄」和「颜色偏好」这 3 个特征来预测某一个样本是否喜欢看电影。我们把数据代入上面的公式中求Loss: %2BL(1%2C%20%5Cgamma)%2BL(0%2C%20%5Cgamma)%0A#card=math&code=%5Coperatorname%7BLoss%7D%3DL%281%2C%20%5Cgamma%29%2BL%281%2C%20%5Cgamma%29%2BL%280%2C%20%5Cgamma%29%0A&id=ALLk1)
为了令其最小, 我们求导, 且让导数为0, 则:
于是, 就得到了初始值 %3D0.69%0A#card=math&code=p%3D%5Cfrac%7B2%7D%7B3%7D%3D0.67%2C%20%5Cgamma%3Dlog%28%5Cfrac%7Bp%7D%7B1-p%7D%29%3D0.69%0A&id=aEBzB)
以及模型的初始状态 %20%3D%200.69%0A#card=math&code=F_0%28x%29%20%3D%200.69%0A&id=nnVj6) 循环生成决策树
这里回忆一下回归树的生成步骤, 第一步就是计算负梯度值得到残差, 第二步是用回归树拟合残差, 第三步是计算叶子节点的输出值, 第四步是更新模型。下面一一来看这些步骤- 计算负梯度得到残差 %5Cright)%7D%7B%5Cpartial%20F%5Cleft(x%7Bi%7D%5Cright)%7D%5Cright%5D%7BF(x)%3DF%7Bm-1%7D(x)%7D%0A#card=math&code=r%7Bi%20m%7D%3D-%5Cleft%5B%5Cfrac%7B%5Cpartial%20L%5Cleft%28y%7Bi%7D%2C%20F%5Cleft%28x%7Bi%7D%5Cright%29%5Cright%29%7D%7B%5Cpartial%20F%5Cleft%28x%7Bi%7D%5Cright%29%7D%5Cright%5D%7BF%28x%29%3DF%7Bm-1%7D%28x%29%7D%0A&id=x0nY0)
此处使用m-1棵树的模型, 计算每个样本的残差![](https://g.yuque.com/gr/latex?r%7Bim%7D#card=math&code=r_%7Bim%7D&id=dEXxB), 就是上面的, 于是例子中, 每个样本的残差: - 使用回归树来拟合
这里的表示样本,回归树的建立过程,简单来说就是遍历每个特征, 每个特征下遍历每个取值, 计算分裂后两组数据的平方损失, 找到最小的那个划分节点。 假如我们产生的第2棵决策树如下: - 对于每个叶子节点, 计算最佳残差拟合值 %2B%5Cgamma%5Cright)%0A#card=math&code=%5Cgamma%7Bj%20m%7D%3D%5Carg%20%5Cmin%20%7B%5Cgamma%7D%20%5Csum%7Bx%20%5Cin%20R%7Bi%20j%7D%7D%20L%5Cleft%28y%7Bi%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7Bi%7D%5Cright%29%2B%5Cgamma%5Cright%29%0A&id=xFLqb)
意思是, 在刚构建的树m中, 找到每个节点j的输出![](https://g.yuque.com/gr/latex?%CE%B3%7Bjm%7D#card=math&code=%CE%B3%7Bjm%7D&id=iQ8l2),能使得该节点的loss最小。 那么我们看一下这个γ的求解方式, 这里非常的巧妙。 首先, 我们把损失函数写出来, 对于左边的第一个样本, 有 ![](https://g.yuque.com/gr/latex?L%5Cleft(y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B1%7D%5Cright)%2B%5Cgamma%5Cright)%3D-y%7B1%7D%5Cleft(F%7Bm-1%7D%5Cleft(x%7B1%7D%5Cright)%2B%5Cgamma%5Cright)%2B%5Clog%20%5Cleft(1%2Be%5E%7BF%7Bm-1%7D%5Cleft(x%7B1%7D%5Cright)%2B%5Cgamma%7D%5Cright)%0A#card=math&code=L%5Cleft%28y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B1%7D%5Cright%29%2B%5Cgamma%5Cright%29%3D-y%7B1%7D%5Cleft%28F%7Bm-1%7D%5Cleft%28x%7B1%7D%5Cright%29%2B%5Cgamma%5Cright%29%2B%5Clog%20%5Cleft%281%2Be%5E%7BF%7Bm-1%7D%5Cleft%28x%7B1%7D%5Cright%29%2B%5Cgamma%7D%5Cright%29%0A&id=kwh9S)
这个式子就是上面推导的, 因为我们要用回归树做分类, 所以这里把分类的预测概率转换成了对数几率回归的形式, 即#card=math&code=log%28%CE%B7_i%29&id=YyJ0h), 这个就是模型的回归输出值。而如果求这个损失的最小值, 我们要求导, 解出令损失最小的γ。 但是上面这个式子求导会很麻烦, 所以这里介绍了一个技巧就是使用二阶泰勒公式来近似表示该式, 再求导。 %20%5Capprox%20f(x)%2B%5CDelta%20x%20f%5E%7B%5Cprime%7D(x)%2B%5Cfrac%7B1%7D%7B2%7D%20%5CDelta%20x%5E%7B2%7D%20f%5E%7B%5Cprime%20%5Cprime%7D(x)%2BO(%5CDelta%20x)%0A#card=math&code=f%28x%2B%5CDelta%20x%29%20%5Capprox%20f%28x%29%2B%5CDelta%20x%20f%5E%7B%5Cprime%7D%28x%29%2B%5Cfrac%7B1%7D%7B2%7D%20%5CDelta%20x%5E%7B2%7D%20f%5E%7B%5Cprime%20%5Cprime%7D%28x%29%2BO%28%5CDelta%20x%29%0A&id=AgKMB)
这里就相当于把![](https://g.yuque.com/gr/latex?L(y_1%2CF%7Bm%E2%88%921%7D(x1))#card=math&code=L%28y_1%2CF%7Bm%E2%88%921%7D%28x1%29%29&id=SgT7K)当做常量f(x),γ作为变量Δx, 将f(x)二阶展开: ![](https://g.yuque.com/gr/latex?L%5Cleft(y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B1%7D%5Cright)%2B%5Cgamma%5Cright)%20%5Capprox%20L%5Cleft(y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B1%7D%5Cright)%5Cright)%2BL%5E%7B%5Cprime%7D%5Cleft(y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B1%7D%5Cright)%5Cright)%20%5Cgamma%2B%5Cfrac%7B1%7D%7B2%7D%20L%5E%7B%5Cprime%20%5Cprime%7D%5Cleft(y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B1%7D%5Cright)%5Cright)%20%5Cgamma%5E%7B2%7D%0A#card=math&code=L%5Cleft%28y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B1%7D%5Cright%29%2B%5Cgamma%5Cright%29%20%5Capprox%20L%5Cleft%28y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B1%7D%5Cright%29%5Cright%29%2BL%5E%7B%5Cprime%7D%5Cleft%28y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B1%7D%5Cright%29%5Cright%29%20%5Cgamma%2B%5Cfrac%7B1%7D%7B2%7D%20L%5E%7B%5Cprime%20%5Cprime%7D%5Cleft%28y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B1%7D%5Cright%29%5Cright%29%20%5Cgamma%5E%7B2%7D%0A&id=cAfYG)
这时候再来求导,得到 ![](https://g.yuque.com/gr/latex?%5Cfrac%7Bd%20L%7D%7Bd%20%5Cgamma%7D%3DL%5E%7B%5Cprime%7D%5Cleft(y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B1%7D%5Cright)%5Cright)%2BL%5E%7B%5Cprime%20%5Cprime%7D%5Cleft(y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B1%7D%5Cright)%5Cright)%20%5Cgamma%0A#card=math&code=%5Cfrac%7Bd%20L%7D%7Bd%20%5Cgamma%7D%3DL%5E%7B%5Cprime%7D%5Cleft%28y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B1%7D%5Cright%29%5Cright%29%2BL%5E%7B%5Cprime%20%5Cprime%7D%5Cleft%28y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B1%7D%5Cright%29%5Cright%29%20%5Cgamma%0A&id=Sw2q2)
令上面的式子等于0时,Loss最小,这时就可以得到 γ: ![](https://g.yuque.com/gr/latex?%5Cgamma%7B11%7D%3D%5Cfrac%7B-L%5E%7B%5Cprime%7D%5Cleft(y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B1%7D%5Cright)%5Cright)%7D%7BL%5E%7B%5Cprime%20%5Cprime%7D%5Cleft(y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B1%7D%5Cright)%5Cright)%7D%0A#card=math&code=%5Cgamma%7B11%7D%3D%5Cfrac%7B-L%5E%7B%5Cprime%7D%5Cleft%28y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B1%7D%5Cright%29%5Cright%29%7D%7BL%5E%7B%5Cprime%20%5Cprime%7D%5Cleft%28y%7B1%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B1%7D%5Cright%29%5Cright%29%7D%0A&id=zcTEA)
因为分子就是残差(上述已经求到了), 分母可以通过对残差求导,得到原损失函数的二阶导: ![](https://g.yuque.com/gr/latex?%5Cbegin%7Baligned%7D%0AL%5E%7B%5Cprime%20%5Cprime%7D%5Cleft(y%7B1%7D%2C%20F(x)%5Cright)%20%26%3D%5Cfrac%7Bd%20L%5E%7B%5Cprime%7D%7D%7Bd%20%5Clog%20(%5Ceta1)%7D%20%5C%5C%0A%26%3D%5Cfrac%7Bd%7D%7Bd%20%5Clog%20(%5Ceta_1)%7D%5Cleft%5B-y%7Bi%7D%2B%5Cfrac%7Be%5E%7B%5Clog%20(%5Ceta1)%7D%7D%7B1%2Be%5E%7B%5Clog%20(%5Ceta_1)%7D%7D%5Cright%5D%20%5C%5C%0A%26%3D%5Cfrac%7Bd%7D%7Bd%20%5Clog%20(%5Ceta_1)%7D%5Cleft%5Be%5E%7B%5Clog%20(%5Ceta_1)%7D%5Cleft(1%2Be%5E%7B%5Clog%20(%5Ceta_1)%7D%5Cright)%5E%7B-1%7D%5Cright%5D%20%5C%5C%0A%26%3De%5E%7B%5Clog%20(%5Ceta_1)%7D%5Cleft(1%2Be%5E%7B%5Clog%20(%5Ceta_1)%7D%5Cright)%5E%7B-1%7D-e%5E%7B2%20%5Clog%20(%5Ceta_1)%7D%5Cleft(1%2Be%5E%7B%5Clog%20(%5Ceta_1)%7D%5Cright)%5E%7B-2%7D%20%5C%5C%0A%26%3D%5Cfrac%7Be%5E%7B%5Clog%20(%5Ceta_1)%7D%7D%7B%5Cleft(1%2Be%5E%7B%5Clog%20(%5Ceta_1)%7D%5Cright)%5E%7B2%7D%7D%20%5C%5C%0A%26%3D%5Cfrac%7B%5Ceta_1%7D%7B(1%2B%5Ceta_1)%7D%5Cfrac%7B1%7D%7B(1%2B%5Ceta_1)%7D%20%5C%5C%0A%26%3Dp_1(1-p_1)%0A%5Cend%7Baligned%7D%0A#card=math&code=%5Cbegin%7Baligned%7D%0AL%5E%7B%5Cprime%20%5Cprime%7D%5Cleft%28y%7B1%7D%2C%20F%28x%29%5Cright%29%20%26%3D%5Cfrac%7Bd%20L%5E%7B%5Cprime%7D%7D%7Bd%20%5Clog%20%28%5Ceta1%29%7D%20%5C%5C%0A%26%3D%5Cfrac%7Bd%7D%7Bd%20%5Clog%20%28%5Ceta_1%29%7D%5Cleft%5B-y%7Bi%7D%2B%5Cfrac%7Be%5E%7B%5Clog%20%28%5Ceta1%29%7D%7D%7B1%2Be%5E%7B%5Clog%20%28%5Ceta_1%29%7D%7D%5Cright%5D%20%5C%5C%0A%26%3D%5Cfrac%7Bd%7D%7Bd%20%5Clog%20%28%5Ceta_1%29%7D%5Cleft%5Be%5E%7B%5Clog%20%28%5Ceta_1%29%7D%5Cleft%281%2Be%5E%7B%5Clog%20%28%5Ceta_1%29%7D%5Cright%29%5E%7B-1%7D%5Cright%5D%20%5C%5C%0A%26%3De%5E%7B%5Clog%20%28%5Ceta_1%29%7D%5Cleft%281%2Be%5E%7B%5Clog%20%28%5Ceta_1%29%7D%5Cright%29%5E%7B-1%7D-e%5E%7B2%20%5Clog%20%28%5Ceta_1%29%7D%5Cleft%281%2Be%5E%7B%5Clog%20%28%5Ceta_1%29%7D%5Cright%29%5E%7B-2%7D%20%5C%5C%0A%26%3D%5Cfrac%7Be%5E%7B%5Clog%20%28%5Ceta_1%29%7D%7D%7B%5Cleft%281%2Be%5E%7B%5Clog%20%28%5Ceta_1%29%7D%5Cright%29%5E%7B2%7D%7D%20%5C%5C%0A%26%3D%5Cfrac%7B%5Ceta_1%7D%7B%281%2B%5Ceta_1%29%7D%5Cfrac%7B1%7D%7B%281%2B%5Ceta_1%29%7D%20%5C%5C%0A%26%3Dp_1%281-p_1%29%0A%5Cend%7Baligned%7D%0A&id=UVyBE)
这时候, 就可以算出该节点的输出: ![](https://g.yuque.com/gr/latex?%5Cgamma%7B11%7D%3D%5Cfrac%7Br%7B11%7D%7D%7Bp%7B10%7D%5Cleft(1-p%7B10%7D%5Cright)%7D%3D%5Cfrac%7B0.33%7D%7B0.67%20%5Ctimes%200.33%7D%3D1.49%0A#card=math&code=%5Cgamma%7B11%7D%3D%5Cfrac%7Br%7B11%7D%7D%7Bp%7B10%7D%5Cleft%281-p%7B10%7D%5Cright%29%7D%3D%5Cfrac%7B0.33%7D%7B0.67%20%5Ctimes%200.33%7D%3D1.49%0A&id=yBBBC)
这里的下面![](https://g.yuque.com/gr/latex?%5Cgamma%7Bjm%7D#card=math&code=%5Cgamma%7Bjm%7D&id=e92eX)表示第棵树的第个叶子节点。 接下来是右边节点的输出, 包含样本2和样本3, 同样使用二阶泰勒公式展开: ![](https://g.yuque.com/gr/latex?%5Cbegin%7Barray%7D%7Bl%7D%0AL%5Cleft(y%7B2%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B2%7D%5Cright)%2B%5Cgamma%5Cright)%2BL%5Cleft(y%7B3%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B3%7D%5Cright)%2B%5Cgamma%5Cright)%20%5C%5C%0A%5Capprox%20L%5Cleft(y%7B2%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B2%7D%5Cright)%5Cright)%2BL%5E%7B%5Cprime%7D%5Cleft(y%7B2%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B2%7D%5Cright)%5Cright)%20%5Cgamma%2B%5Cfrac%7B1%7D%7B2%7D%20L%5E%7B%5Cprime%20%5Cprime%7D%5Cleft(y%7B2%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B2%7D%5Cright)%5Cright)%20%5Cgamma%5E%7B2%7D%20%5C%5C%0A%2BL%5Cleft(y%7B3%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B3%7D%5Cright)%5Cright)%2BL%5E%7B%5Cprime%7D%5Cleft(y%7B3%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B3%7D%5Cright)%5Cright)%20%5Cgamma%2B%5Cfrac%7B1%7D%7B2%7D%20L%5E%7B%5Cprime%20%5Cprime%7D%5Cleft(y%7B3%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B3%7D%5Cright)%5Cright)%20%5Cgamma%5E%7B2%7D%0A%5Cend%7Barray%7D%0A#card=math&code=%5Cbegin%7Barray%7D%7Bl%7D%0AL%5Cleft%28y%7B2%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B2%7D%5Cright%29%2B%5Cgamma%5Cright%29%2BL%5Cleft%28y%7B3%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B3%7D%5Cright%29%2B%5Cgamma%5Cright%29%20%5C%5C%0A%5Capprox%20L%5Cleft%28y%7B2%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B2%7D%5Cright%29%5Cright%29%2BL%5E%7B%5Cprime%7D%5Cleft%28y%7B2%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B2%7D%5Cright%29%5Cright%29%20%5Cgamma%2B%5Cfrac%7B1%7D%7B2%7D%20L%5E%7B%5Cprime%20%5Cprime%7D%5Cleft%28y%7B2%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B2%7D%5Cright%29%5Cright%29%20%5Cgamma%5E%7B2%7D%20%5C%5C%0A%2BL%5Cleft%28y%7B3%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B3%7D%5Cright%29%5Cright%29%2BL%5E%7B%5Cprime%7D%5Cleft%28y%7B3%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B3%7D%5Cright%29%5Cright%29%20%5Cgamma%2B%5Cfrac%7B1%7D%7B2%7D%20L%5E%7B%5Cprime%20%5Cprime%7D%5Cleft%28y%7B3%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B3%7D%5Cright%29%5Cright%29%20%5Cgamma%5E%7B2%7D%0A%5Cend%7Barray%7D%0A&id=v3fpO)
求导, 令其结果为0,就会得到第1棵树的第2个叶子节点的输出: ![](https://g.yuque.com/gr/latex?%5Cbegin%7Baligned%7D%0A%5Cgamma%7B21%7D%20%26%3D%5Cfrac%7B-L%5E%7B%5Cprime%7D%5Cleft(y%7B2%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B2%7D%5Cright)%5Cright)-L%5E%7B%5Cprime%7D%5Cleft(y%7B3%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B3%7D%5Cright)%5Cright)%7D%7BL%5E%7B%5Cprime%20%5Cprime%7D%5Cleft(y%7B2%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B2%7D%5Cright)%5Cright)%2BL%5E%7B%5Cprime%20%5Cprime%7D%5Cleft(y%7B3%7D%2C%20F%7Bm-1%7D%5Cleft(x%7B3%7D%5Cright)%5Cright)%7D%20%5C%5C%0A%26%3D%5Cfrac%7Br%7B21%7D%2Br%7B31%7D%7D%7Bp%7B20%7D%5Cleft(1-p%7B20%7D%5Cright)%2Bp%7B30%7D%5Cleft(1-p%7B30%7D%5Cright)%7D%20%5C%5C%0A%26%3D%5Cfrac%7B0.33-0.67%7D%7B0.67%20%5Ctimes%200.33%2B0.67%20%5Ctimes%200.33%7D%20%5C%5C%0A%26%3D-0.77%0A%5Cend%7Baligned%7D%0A#card=math&code=%5Cbegin%7Baligned%7D%0A%5Cgamma%7B21%7D%20%26%3D%5Cfrac%7B-L%5E%7B%5Cprime%7D%5Cleft%28y%7B2%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B2%7D%5Cright%29%5Cright%29-L%5E%7B%5Cprime%7D%5Cleft%28y%7B3%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B3%7D%5Cright%29%5Cright%29%7D%7BL%5E%7B%5Cprime%20%5Cprime%7D%5Cleft%28y%7B2%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B2%7D%5Cright%29%5Cright%29%2BL%5E%7B%5Cprime%20%5Cprime%7D%5Cleft%28y%7B3%7D%2C%20F%7Bm-1%7D%5Cleft%28x%7B3%7D%5Cright%29%5Cright%29%7D%20%5C%5C%0A%26%3D%5Cfrac%7Br%7B21%7D%2Br%7B31%7D%7D%7Bp%7B20%7D%5Cleft%281-p%7B20%7D%5Cright%29%2Bp%7B30%7D%5Cleft%281-p%7B30%7D%5Cright%29%7D%20%5C%5C%0A%26%3D%5Cfrac%7B0.33-0.67%7D%7B0.67%20%5Ctimes%200.33%2B0.67%20%5Ctimes%200.33%7D%20%5C%5C%0A%26%3D-0.77%0A%5Cend%7Baligned%7D%0A&id=ZhzYg)
可以看出, 对于任意叶子节点, 我们可以直接计算其输出值: ![](https://g.yuque.com/gr/latex?%5Cgamma%7Bj%20m%7D%3D%5Cfrac%7B%5Csum%7Bi%3D1%7D%5E%7BR%7Bi%20j%7D%7D%20r%7Bi%20m%7D%7D%7B%5Csum%7Bi%3D1%7D%5E%7BR%7Bi%20j%7D%7D%20p%7Bi%2C%20m-1%7D%5Cleft(1-p%7Bi%2C%20m-1%7D%5Cright)%7D%0A#card=math&code=%5Cgamma%7Bj%20m%7D%3D%5Cfrac%7B%5Csum%7Bi%3D1%7D%5E%7BR%7Bi%20j%7D%7D%20r%7Bi%20m%7D%7D%7B%5Csum%7Bi%3D1%7D%5E%7BR%7Bi%20j%7D%7D%20p%7Bi%2C%20m-1%7D%5Cleft%281-p_%7Bi%2C%20m-1%7D%5Cright%29%7D%0A&id=VyEjp) - 更新模型 %3DF%7Bm-1%7D(x)%2B%5Cnu%20%5Csum%7Bj%3D1%7D%5E%7BJ%7Bm%7D%7D%20%5Cgamma%7Bm%7D%0A#card=math&code=F%7Bm%7D%28x%29%3DF%7Bm-1%7D%28x%29%2B%5Cnu%20%5Csum%7Bj%3D1%7D%5E%7BJ%7Bm%7D%7D%20%5Cgamma_%7Bm%7D%0A&id=ws3xN)
这样, 通过多次循环迭代, 就可以得到一个比较强的学习器#card=math&code=F_m%28x%29&id=kqmil)。
下面分析一下GBDT的优缺点:- 计算负梯度得到残差 %5Cright)%7D%7B%5Cpartial%20F%5Cleft(x%7Bi%7D%5Cright)%7D%5Cright%5D%7BF(x)%3DF%7Bm-1%7D(x)%7D%0A#card=math&code=r%7Bi%20m%7D%3D-%5Cleft%5B%5Cfrac%7B%5Cpartial%20L%5Cleft%28y%7Bi%7D%2C%20F%5Cleft%28x%7Bi%7D%5Cright%29%5Cright%29%7D%7B%5Cpartial%20F%5Cleft%28x%7Bi%7D%5Cright%29%7D%5Cright%5D%7BF%28x%29%3DF%7Bm-1%7D%28x%29%7D%0A&id=x0nY0)
- 优点:我们可以把树的生成过程理解成自动进行多维度的特征组合的过程,从根结点到叶子节点上的整个路径(多个特征值判断),才能最终决定一棵树的预测值, 另外,对于连续型特征的处理,GBDT 可以拆分出一个临界阈值,比如大于 0.027 走左子树,小于等于 0.027(或者 default 值)走右子树,这样很好的规避了人工离散化的问题。这样就非常轻松的解决了逻辑回归那里自动发现特征并进行有效组合的问题, 这也是GBDT的优势所在。
- 局限性: 对于海量的 id 类特征,GBDT 由于树的深度和棵树限制(防止过拟合),不能有效的存储;另外海量特征在也会存在性能瓶颈,当 GBDT 的 one hot 特征大于 10 万维时,就必须做分布式的训练才能保证不爆内存。所以 GBDT 通常配合少量的反馈 CTR 特征来表达,这样虽然具有一定的范化能力,但是同时会有信息损失,对于头部资源不能有效的表达。
5.3 GBDT+LR模型
LR算法属于线性模型,模型简单,计算开销小且易并行化,能够处理海量的数据,但缺点是只在具有良好线性关系的数据集上有效,其学习能力有限,对特征选取要求高,容易造成欠拟合。因此,需要有效的特征工程来生成有区分度的特征,从而产生良好的分类效果。早在2014年He等就提出了通过GBDT模型生产新特征来解决LR的特征工程问题,将其应用于广告点击率的评估。GBDT算法以Boost算法为基础,每次迭代都会生成一棵新树,该特点正好可以用来挖掘有区分度的新特征,避免复杂的人工成本。
GBDT+LR 使用最广泛的场景是CTR(点击通过率)预估,即预测当给用户推送的广告会不会被用户点击。
GBDT-LR融合模型的训练过程如图所示,其具体步骤如下:
- 利用原始训练集训练GBDT模型构造—系列的决策树,组成一个强分类器。
- 利用训练好的GBDT模型对原始数据进行预测时,不以分类概率作为输出,而是以模型中每棵树的预测值所属叶结点的位置为新特征提取特征值,形成新的数据。
- 对新数据进行One-hot编码,也就是将样本输出所属叶结点的位置标记为1,得到每个样本的位置标记向量W。所有样本的输出会组成一个标记每棵决策树输出的叶结点位置的稀疏矩阵。
- 将该W作为新的训练数据供LR模型进行训练。
有了前两节的铺垫,这个模型解释起来就很容易了,它的总体结构如下:
训练时,GBDT建树的过程相当于自动进行的特征组合和离散化,然后从根结点到叶子节点的这条路径就可以看成是不同特征进行的特征组合,用叶子节点可以唯一的表示这条路径,并作为一个离散特征传入 LR 进行二次训练。
比如上图中, 有两棵树,x为一条输入样本,遍历两棵树后,x样本分别落到两颗树的叶子节点上,每个叶子节点对应LR一维特征,那么通过遍历树,就得到了该样本对应的所有LR特征。构造的新特征向量是取值0/1的。 比如左树有三个叶子节点,右树有两个叶子节点,最终的特征即为五维的向量。对于输入x,假设他落在左树第二个节点,编码[0,1,0],落在右树第二个节点则编码[0,1],所以整体的编码为[0,1,0,0,1],这类编码作为特征,输入到线性分类模型(LR or FM)中进行分类。
预测时,会先走 GBDT 的每棵树,得到某个叶子节点对应的一个离散特征(即一组特征组合),然后把该特征以 one-hot 形式传入 LR 进行线性加权预测。
这个方案应该比较简单了, 下面有几个关键的点我们需要了解:
- 通过GBDT进行特征组合之后得到的离散向量是和训练数据的原特征一块作为逻辑回归的输入, 而不仅仅全是这种离散特征 。
2. 建树的时候用ensemble建树的原因就是一棵树的表达能力很弱,不足以表达多个有区分性的特征组合,多棵树的表达能力更强一些。GBDT每棵树都在学习前面棵树尚存的不足,迭代多少次就会生成多少棵树。
3. 随机森林(RF)也是多棵树,但从效果上有实践证明不如GBDT。且GBDT前面的树,特征分裂主要体现对多数样本有区分度的特征;后面的树,主要体现的是经过前N颗树,残差仍然较大的少数样本。优先选用在整体上有区分度的特征,再选用针对少数样本有区分度的特征,思路更加合理,这应该也是用GBDT的原因。
4. 在CRT预估中, GBDT一般会建立两类树(非ID特征建一类, ID类特征建一类), AD,ID类特征在CTR预估中是非常重要的特征,直接将AD,ID作为feature进行建树不可行,故考虑为每个AD,ID建GBDT树。
1)非ID类树:不以细粒度的ID建树,此类树作为base,即便曝光少的广告、广告主,仍可以通过此类树得到有区分性的特征、特征组合
2)ID类树:以细粒度的ID建一类树,用于发现曝光充分的ID对应有区分性的特征、特征组合。
5.4 编程实践
这节我们通过kaggle上的一个ctr预测的比赛来看一下GBDT+LR模型部分的编程实践。该比赛的任务是开发预测广告点击率(CTR)的模型。给定一个用户和他正在访问的页面,预测他点击给定广告的概率是多少?本节中数据集采用的是criteo-Display Advertising Challenge比赛的部分数据集, 里面有train.csv
和test.csv
两个文件,数据集介绍如下:
train.csv
: 训练集由Criteo 7天内的部分流量组成。每一行对应一个由Criteo提供的显示广告。为了减少数据集的大小,正(点击)和负(未点击)的例子都以不同的比例进行了抽样。示例是按时间顺序排列的test.csv
: 测试集的计算方法与训练集相同,只是针对训练期之后一天的事件
数据集的字段说明如下:
- Label: 目标变量, 0表示未点击, 1表示点击
- l1-l13: 13列的数值特征, 大部分是计数特征
- C1-C26: 26列分类特征, 为了达到匿名的目的, 这些特征的值离散成了32位的数据表示
本节我们就基于GBDT+LR模型来完成该任务。
首先对数据进行简单处理,代码如下:
## 数据导入与简单处理
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
import lightgbm as lgb
from sklearn.preprocessing import MinMaxScaler, OneHotEncoder, LabelEncoder
from sklearn.metrics import log_loss
import gc
from scipy import sparse
import warnings
warnings.filterwarnings('ignore')
"""数据读取与预处理"""
# 数据读取
path = './dataset/'
df_train = pd.read_csv(path + 'kaggle_train.csv')
df_test = pd.read_csv(path + 'kaggle_test.csv')
# 简单的数据预处理
# 去掉id列, 把测试集和训练集合并, 填充缺失值
df_train.drop(['Id'], axis=1, inplace=True)
df_test.drop(['Id'], axis=1, inplace=True)
df_test['Label'] = -1
data = pd.concat([df_train, df_test])
data.fillna(-1, inplace=True)
"""下面把特征列分开处理"""
continuous_fea = ['I' + str(i + 1) for i in range(13)]
category_fea = ['C' + str(i + 1) for i in range(26)]
然后开始建模,回顾一下上一节的模型架构,首先是要训练GBDT模型, GBDT的实现一般可以使用xgboost, 或者lightgbm。训练完了GBDT模型之后, 我们需要预测出每个样本落在了哪棵树上的哪个节点上, 然后通过one-hot就会得到一些新的离散特征, 这和原来的特征进行合并组成新的数据集, 然后作为逻辑回归的输入,最后通过逻辑回归模型得到结果。
根据上面的描述,并且我们已经有了处理好的数据x_train
和y_train
,现在来看看代码如何实现:
建立LR模型
def lr_model(data, category_fea, continuous_fea):
# 连续特征归一化
scaler = MinMaxScaler()
for col in continuous_fea:
data[col] = scaler.fit_transform(data[col].values.reshape(-1, 1))
# 离散特征one-hot编码
for col in category_fea:
onehot_feats = pd.get_dummies(data[col], prefix=col)
data.drop([col], axis=1, inplace=True)
data = pd.concat([data, onehot_feats], axis=1)
# 把训练集和测试集分开
train = data[data['Label'] != -1]
target = train.pop('Label')
test = data[data['Label'] == -1]
test.drop(['Label'], axis=1, inplace=True)
# 划分数据集
x_train, x_val, y_train, y_val = train_test_split(train, target, test_size=0.2, random_state=2020)
# 建立模型
lr = LogisticRegression()
lr.fit(x_train, y_train)
tr_logloss = log_loss(y_train, lr.predict_proba(x_train)[:, 1]) # −(ylog(p)+(1−y)log(1−p)) log_loss
val_logloss = log_loss(y_val, lr.predict_proba(x_val)[:, 1])
print('tr_logloss: ', tr_logloss)
print('val_logloss: ', val_logloss)
# 模型预测
y_pred = lr.predict_proba(test)[:, 1] # predict_proba 返回n行k列的矩阵,第i行第j列上的数值是模型预测第i个预测样本为某个标签的概率, 这里的1表示点击的概率
print('predict: ', y_pred[:10]) # 这里看前10个, 预测为点击的概率
建立GBDT模型
可以通过XGBOOST, lightgbm等进行GBDT模型的搭建。通过lgb搭建GBDT的代码如下:def gbdt_model(data, category_fea, continuous_fea):
# 离散特征one-hot编码
for col in category_fea:
onehot_feats = pd.get_dummies(data[col], prefix=col)
data.drop([col], axis=1, inplace=True)
data = pd.concat([data, onehot_feats], axis=1)
# 训练集和测试集分开
train = data[data['Label'] != -1]
target = train.pop('Label')
test = data[data['Label'] == -1]
test.drop(['Label'], axis=1, inplace=True)
# 划分数据集
x_train, x_val, y_train, y_val = train_test_split(train, target, test_size=0.2, random_state=2020)
# 建模
gbm = lgb.LGBMClassifier(boosting_type='gbdt', # 这里用gbdt
objective='binary',
subsample=0.8,
min_child_weight=0.5,
colsample_bytree=0.7,
num_leaves=100,
max_depth=12,
learning_rate=0.01,
n_estimators=10000
)
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,
)
tr_logloss = log_loss(y_train, gbm.predict_proba(x_train)[:, 1]) # −(ylog(p)+(1−y)log(1−p)) log_loss
val_logloss = log_loss(y_val, gbm.predict_proba(x_val)[:, 1])
print('tr_logloss: ', tr_logloss)
print('val_logloss: ', val_logloss)
# 模型预测
y_pred = gbm.predict_proba(test)[:, 1] # predict_proba 返回n行k列的矩阵,第i行第j列上的数值是模型预测第i个预测样本为某个标签的概率, 这里的1表示点击的概率
print('predict: ', y_pred[:10]) # 这里看前10个, 预测为点击的概率
GBDT+LR建模
这一步就是把上面两个模型进行组合, GBDT负责对各个特征进行交叉和组合, 把原始特征向量转换为新的离散型特征向量, 然后再使用逻辑回归模型。
每个样本落在每个树的位置 , 下面两个是矩阵 (样本个数, 树的棵树) , 每一个数字代表某个样本落在了某个数的哪个叶子节点
gbdt_feats_train = model.predict(train, pred_leaf = True) gbdt_feats_test = model.predict(test, pred_leaf = True)
把上面的矩阵转成新的样本-特征的形式, 与原有的数据集合并
gbdtfeats_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)
构造新数据集
train = pd.concat([train, df_train_gbdt_feats], axis = 1) test = pd.concat([test, df_test_gbdt_feats], axis = 1) train_len = train.shape[0] data = pd.concat([train, test])
<br />完整的代码如下
```python
def gbdt_lr_model(data, category_feature, continuous_feature): # 0.43616
# 离散特征one-hot编码
for col in category_feature:
onehot_feats = pd.get_dummies(data[col], prefix=col)
data.drop([col], axis=1, inplace=True)
data = pd.concat([data, onehot_feats], axis=1)
train = data[data['Label'] != -1]
target = train.pop('Label')
test = data[data['Label'] == -1]
test.drop(['Label'], axis=1, inplace=True)
# 划分数据集
x_train, x_val, y_train, y_val = train_test_split(train, target, test_size=0.2, random_state=2020)
gbm = lgb.LGBMClassifier(objective='binary',
subsample=0.8,
min_child_weight=0.5,
colsample_bytree=0.7,
num_leaves=100,
max_depth=12,
learning_rate=0.01,
n_estimators=1000,
)
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_
gbdt_feats_train = model.predict(train, pred_leaf=True)
gbdt_feats_test = model.predict(test, pred_leaf=True)
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)
train = pd.concat([train, df_train_gbdt_feats], axis=1)
test = pd.concat([test, df_test_gbdt_feats], axis=1)
train_len = train.shape[0]
data = pd.concat([train, test])
del train
del test
gc.collect()
# 连续特征归一化
scaler = MinMaxScaler()
for col in continuous_feature:
data[col] = scaler.fit_transform(data[col].values.reshape(-1, 1))
for col in gbdt_feats_name:
onehot_feats = pd.get_dummies(data[col], prefix=col)
data.drop([col], axis=1, inplace=True)
data = pd.concat([data, onehot_feats], axis=1)
train = data[: train_len]
test = data[train_len:]
del data
gc.collect()
x_train, x_val, y_train, y_val = train_test_split(train, target, test_size=0.3, random_state=2018)
lr = LogisticRegression()
lr.fit(x_train, y_train)
tr_logloss = log_loss(y_train, lr.predict_proba(x_train)[:, 1])
print('tr-logloss: ', tr_logloss)
val_logloss = log_loss(y_val, lr.predict_proba(x_val)[:, 1])
print('val-logloss: ', val_logloss)
y_pred = lr.predict_proba(test)[:, 1]
print(y_pred[:10])
模型训练和预测GBDT模型
gbdt_model(data.copy(), category_fea, continuous_fea)
训练和预测GBDT+LR模型
gbdt_lr_model(data.copy(), category_fea, continuous_fea)
```
结果如下图所示
5.5 思考
- 为什么使用集成的决策树? 为什么使用GBDT构建决策树而不是随机森林?
使用 - 面对高维稀疏类特征的时候(比如ID类特征), 逻辑回归一般要比GBDT这种非线性模型好, 为什么?