LearningToRank(LTR)排序算法LGBMRanker的原理和使用

1. 写在前面

在最近新闻推荐的比赛中, 接触到了一个排序模型LGBMRanker, 该模型与普通的分类模型LGBMClassifier不太一样, 普通的分类模型在进行推荐的时候, 往往是先预测某个商品或者文章用户会不会点击, 也就是它的目标是预测用户点击某篇文章或者某个商品的概率, 然后根据这个概率值进行排序, 排完之后,把最靠前的几个返回回来给用户进行推荐。 而LGBRanker模型, 它不关心用户点击某篇文章的概率, 而是根据用户点击或者不点击的这个行为, 直接去预测最后商品或者文章的一个相对顺序, 返回一个排好序的列表回来。 然后我们把最靠前的几个返回给用户进行推荐。 这样就会发现, 分类模型和排序模型最终的目标是不一样的, 这样也就导致了他们的训练数据集和损失函数等也会有所不同。 但仍然是监督学习的问题, 数据依然也是Feature+Label的形式, 只不过利用的时候发生了一些区别。 关于普通的分类模型LGBMClassifier, 这里不做过多的描述。

这里主要是介绍一下LGBMRanker的原理和使用。 但是介绍这个之前呢? 我们需要先了解一下LambdaRank的原理。 关于这个, 我大部分的内容都是参考的下面链接里面的第一篇文章, 因为感觉人家讲的很清晰了, 通过LambdaRank的介绍, 估计就知道LGBMRanker在干一个什么样的事情了,然后再介绍一下LGBMRanker的使用。 下面开始。

在互联网应用场景中,排序是非常核心的模块。一个最直接的应用,就是日常生活常用到的搜索引擎。用户通过搜索框提交query, 搜索引擎就会返回以一些与query相关的文档,并根据相关大小排序后展示给用户。 这一应用场景中, 最相关的一些文档能够通过排序后优先展示, 将直接影响用户的体验, 所以如何更好的进行排序展示结果是一个非常重要的问题。 当然,排序算法也应用于在线广告、协同过滤、多媒体检索等领域。

传统排序方法,基于人工方式做策略组合,在数据量较小时能够起到作用。随着互联网数据量的增加,这种方法变得越来越困难。因而,更自然的解决方案,是开发基于机器学习的搜索引擎排序算法。这种算法通常称之为Learning to Rank(LTR)。

此次的新闻推荐中, 我们是根据用户的历史点击来预测最后的一次点击。 而我们最后给出结果的时候, 是给出了最有可能的5篇文章, 这5篇文章我们是按照模型的预测分值score进行排序。 这个任务呢, 就像上面提到的, 第一个方式就是可以用一个分类模型,训练的时候就根据特征+label的形式训练, 然后直接预测点击概率,把这个当做score进行排序, 这种属于PointWise的方法,因为它的输入和输出呢? 都是基于的单个样本。 而另一个方式呢, 就是排序这里的思路, 我不是基于单个的样本进行训练和预测, 而是针对一个query(用户)对应的所有点击行为, 我预测的是这些点击行为与对应的query的一个相关度排序列表。 训练的时候这里的训练集就变成了一个query对应的所有点击行为的特征(这算是一个样本了), 而label变成了这些点击行为与query的一个相关度排序列表。 通过这个去计算损失,然后优化模型。 这是一种Listwise Approach。当然中间还有种PointWise的方法,这个看名字也知道,这个是预测某个query对应的点击行为里面两两之间的一个相对顺序, 关于这三种方法, 我下面会给出参考文章。 这里我们主要是看看这个ListWise 方法到底是怎样做的。 LGBMRanker就是这样的一种方法。

LTR算法中,LambdaMART[1]是最常被使用的一种Listwise算法,在各大搜索引擎中均有应用。从其名称上,可以知道其由Lambda和MART两部分组合成,其中MART(Multiple Additive Regression Tree)表示的是底层训练模型,而Lambda表示的是MART求解过程使用的梯度。

为什么LambdaMART可以很好的应用于排序场景?这主要受益于Lambda梯度的使用。但Lambda最初是在LambdaRank模型中被提出,而LambdaRank模型又是在RankNet模型的基础上改进而来。

下面我们将从MART、Lambda来深入了解LambdaMART算法。

2. MART算法

说起MART(Multiple Additive Regression Tree),大家可能比较陌生。但是提起GBDT(Gradient Boosting Decision Tree),很多人都熟悉。没错,MART本质上就是梯度提升决策树GBDT。所以,我们很容易知道MART的一些特征:

  • 基于多个决策树来预测结果;
  • 决策树之间通过加法模型叠加结果;
  • 每棵决策树都是针对之前决策树的不足进行改进。

本质上,MART是一种Boosting思想下的算法框架。它通过不断迭代弱模型,改进新模型的能力,最终得到的是所有模型的叠加,这一结果起到足以预测真实值的强模型。

那么在LambdaMART中,MART的损失函数是如何定义的?这里就必须了解下Lambda的演化史了。

3. Lambda

Lambda最早诞生于LambdaRank算法,而LambdaRank算法改进自RankNet算法。为了对排序算法有更深刻的理解,我们先来了解下RankNet算法。

3.1 RankNet

我们知道,在排序中,常用的评价指标NDCG,MAP,ERR都是不可导的,即无法求梯度,这就导致了无法运用梯度下降算法求解排序问题。RankNet以一种巧妙方法,将无法用梯度下降求解的排序问题,优化为对概率的交叉熵损失函数,继而可用梯度下降算法求解。 本次新闻推荐中, MAP的计算方式已经见识过, 这里要了解一下NDCG, 后面也给出了参考链接。

下面来看看RankNet是如何优化排序问题的。RankNet的训练目标是得到一个模型 s s s,输入是文档 x x x,输出是该文档的得分:
s = f ( x ; W ) s=f(x ; W) s=f(x;W)
其中 W W W表示模型 s s s的参数集。

有了模型 s s s后, 对于文档 x i x_i xi x j x_j xj, 我们就可以分布得到其得分 s i s_i si s j s_j sj:
s i = f ( x i ; W ) , s j = f ( x j ; W ) s_{i}=f\left(x_{i} ; W\right), s_{j}=f\left(x_{j} ; W\right) si=f(xi;W),sj=f(xj;W)
RankNet巧妙的地方在于,它通过偏序关系将文档之间的得分与文档顺序关联起来,进而得出概率。具体地, 记 P i j P_{ij} Pij表示文档 x i x_i xi排在 x j x_j xj前面的概率, 则
P i j = P ( x i > x j ) = exp ⁡ ( σ ⋅ ( s i − s j ) ) 1 + exp ⁡ ( σ ⋅ ( s i − s j ) ) = 1 1 + exp ⁡ ( − σ ⋅ ( s i − s j ) ) P_{i j}=P\left(x_{i}>x_{j}\right)=\frac{\exp \left(\sigma \cdot\left(s_{i}-s_{j}\right)\right)}{1+\exp \left(\sigma \cdot\left(s_{i}-s_{j}\right)\right)}=\frac{1}{1+\exp \left(-\sigma \cdot\left(s_{i}-s_{j}\right)\right)} Pij=P(xi>xj)=1+exp(σ(sisj))exp(σ(sisj))=1+exp(σ(sisj))1

可以看到,RankNet使用了sigmoid函数来转化排序概率,本质上就是逻辑回归!这里 σ \sigma σ影响的是sigmoid函数的形状,对最终结果影响不大,为简化说明,后文将使用 σ \sigma σ=1。

在实际应用中, 文档 x i x_i xi排在 x j x_j xj之前的真实概率我们是知道的, 这里记 P ˉ i j \bar{P}_{i j} Pˉij。 我们约定, 如果 x i x_i xi排在 x j x_j xj之前, 则 P ˉ i j \bar{P}_{i j} Pˉij为1, 否则为0。 那么在新闻推荐里面怎么看在不在之前呢? 这个之前或者之后,其实就是说的与query相似度大小。 这个在新闻推荐里面我们是通过label进行判断的, 输入一个用户, 我们能得到这个用户召回的候选文章过来, 然后我们根据label值, 就能得到这些文章与用户之间的一个相似度, 用户点击的文章,显然相似度要大,排在前面。

接着, 根据模型概率 P i j P_{ij} Pij和真实概率 P ˉ i j \bar{P}_{i j} Pˉij, 使用交叉熵可衡量出模型在排序文档 x i x_i xi x j x_j xj产生的损失函数:
L i j = − P ˉ i j log ⁡ P i j − ( 1 − P ˉ i j ) log ⁡ ( 1 − P i j ) = 1 2 ( 1 − S i j ) ⋅ ( s i − s j ) + log ⁡ { 1 + exp ⁡ ( − ( s i − s j ) ) } \begin{aligned} L_{i j} &=-\bar{P}_{i j} \log P_{i j}-\left(1-\bar{P}_{i j}\right) \log \left(1-P_{i j}\right) \\ &=\frac{1}{2}\left(1-S_{i j}\right) \cdot\left(s_{i}-s_{j}\right)+\log \left\{1+\exp \left(-\left(s_{i}-s_{j}\right)\right)\right\} \end{aligned} Lij=PˉijlogPij(1Pˉij)log(1Pij)=21(1Sij)(sisj)+log{ 1+exp((sisj))}

上面公式里面的 S i j S_{ij} Sij, 表示的是文档 x i x_i xi x j x_j xj的真实序关系:
S i j = { 1 , x i  比  x j  更相关  0 , x i  比  x j  相关性一样  − 1 , x j  比  x i  更相关  S_{i j}=\left\{\begin{aligned} 1, & x_{i} \text { 比 } x_{j} \text { 更相关 } \\ 0, & x_{i} \text { 比 } x_{j} \text { 相关性一样 } \\ -1, & x_{j} \text { 比 } x_{i} \text { 更相关 } \end{aligned}\right. Sij=1,0,1,xi  xj 更相关 xi  xj 相关性一样 xj  xi 更相关 
最后, RankNet的损失函数定义如下, 目标是使得所有文档对的排序概率估计的损失最小:
L = ∑ ( i , j ) ∈ D L i j L=\sum_{(i, j) \in D} L_{i j} L=(i,j)DLij
这里, D D D表示的是一次查询中所有文档对集合。

RankNet的训练目标是求解模型 s s s的参数 W W W, 此时可通过梯度下降法求解:
w k → w k − η ∂ L ∂ w k w_{k} \rightarrow w_{k}-\eta \frac{\partial L}{\partial w_{k}} wkwkηwkL

3.2 LambdaRank

RankNet避开了直接对排序中的评价指标进行优化,以概率模型抽象解决了排序中的相关顺序问题。但是由于没有涉及到评价指标的优化,实际应用中存在一些问题。
在这里插入图片描述
如图所示, 每个线条表示一个文档,蓝色表示相关文档,而灰色表示不相关文档,排在前面的文档将优先展示给用户。由于RankNet只关心两两文档之间的顺序,忽视了文档的具体顺序信息,那么当面对左图的情形时,假设此时损失值为13,RankNet通过把排在首位的相关文档下调3个位置,排在倒数第二的相关文档上调5个位置,从而将损失值降为11。但是,对于用户来说,通常更关注top k个结果的顺序,在优化过程中下调top结果中的相关文档并不能令用户满意。

图右图左边黑色的箭头表示RankNet下一轮的调序方向和强度,但用户真正需要的是右边红色箭头代表的方向和强度,即更关注靠前位置的相关文档的排序位置的提升。

所以,排序中的评价指标NDCG更能反应用户对相关性的潜在需求。这里问题就来了:能不能通过NDCG指标来定义梯度呢?

LambdaRank正是基于这个思想演化而来,其中Lambda指的就是红色箭头,代表下一次迭代优化的方向和强度,也就是梯度。

我们来看看LambdaRank是如何通过NDCG指标定义梯度的。首先,对于RankNet的梯度,我们有如下推导:
∂ L ∂ w k = ∑ ( i , j ) ∈ P ∂ L i j ∂ w k = ∑ ( i , j ) ∈ P ∂ L i j ∂ s i ∂ s i ∂ w k + ∂ L i j ∂ s j ∂ s j ∂ w k \frac{\partial L}{\partial w_{k}}=\sum_{(i, j) \in P} \frac{\partial L_{i j}}{\partial w_{k}}=\sum_{(i, j) \in P} \frac{\partial L_{i j}}{\partial s_{i}} \frac{\partial s_{i}}{\partial w_{k}}+\frac{\partial L_{i j}}{\partial s_{j}} \frac{\partial s_{j}}{\partial w_{k}} wkL=(i,j)PwkLij=(i,j)PsiLijwksi+sjLijwksj
进一步的,可以观察到对模型 s s s的求导有下面的对称性:
∂ L i j ∂ s i = ∂ { 1 2 ( 1 − S i j ) ( s i − s j ) + log ⁡ { 1 + exp ⁡ ( − ( s i − s j ) ) } } ∂ s i = 1 2 ( 1 − S i j ) − 1 1 + exp ⁡ ( s i − s j ) = [ 1 2 ( 1 − S i j ) − 1 1 + exp ⁡ ( s i − s j ) ] = − ∂ L i j ∂ s j \begin{aligned} \frac{\partial L_{i j}}{\partial s_{i}} &=\frac{\partial\left\{\frac{1}{2}\left(1-S_{i j}\right)\left(s_{i}-s_{j}\right)+\log \left\{1+\exp \left(-\left(s_{i}-s_{j}\right)\right)\right\}\right\}}{\partial s_{i}} \\ &=\frac{1}{2}\left(1-S_{i j}\right)-\frac{1}{1+\exp \left(s_{i}-s_{j}\right)} \\ &=\left[\frac{1}{2}\left(1-S_{i j}\right)-\frac{1}{1+\exp \left(s_{i}-s_{j}\right)}\right] \\ &=-\frac{\partial L_{i j}}{\partial s_{j}} \end{aligned} siLij=si{ 21(1Sij)(sisj)+log{ 1+exp((sisj))}}=21(1Sij)1+exp(sisj)1=[21(1Sij)1+exp(sisj)1]=sjLij
因此,LambdaRank有如下关于文档 x i x_i xi x j x_j xj的Lambda定义:

λ i j =  def  ∂ L i j ∂ s i = − ∂ L i j ∂ s j \lambda_{i j} \stackrel{\text { def }}{=} \frac{\partial L_{i j}}{\partial s_{i}}=-\frac{\partial L_{i j}}{\partial s_{j}} λij= def siLij=sjLij
我们首先考虑有序对 ( i , j ) (i,j) (i,j),因而有 S i j = 1 S_{ij}=1 Sij=1,于是上述公式可以进一步简化:
λ i j =  def  − 1 1 + exp ⁡ ( s i − s j ) \lambda_{i j} \stackrel{\text { def }}{=}-\frac{1}{1+\exp \left(s_{i}-s_{j}\right)} λij= def 1+exp(sisj)1
至此可以得到每个文档 x i x_i xi的Lambda值为:
λ i = ∑ ( i , j ) ∈ I λ i j − ∑ ( j , i ) ∈ I λ i j \lambda_{i}=\sum_{(i, j) \in I} \lambda_{i j}-\sum_{(j, i) \in I} \lambda_{i j} λi=(i,j)Iλij(j,i)Iλij
公式中的 I I I是文档对 ( x i , x j ) (x_i,x_j) (xi,xj)的集合, 其中文档 x i x_i xi排在 x j x_j xj前面, 即更相关。

为了加强排序中顺序前后的重要性,LambdaRank进一步在Lambda中引入评价指标Z(如NDCG),把交换两个文档的位置引起的评价指标的变化 ∣ Δ Z i j ∣ \left|\Delta Z_{i j}\right| ΔZij作为其中一个因子:
λ i j = − 1 1 + exp ⁡ ( s i − s j ) ⋅ ∣ Δ Z i j ∣ \lambda_{i j}=-\frac{1}{1+\exp \left(s_{i}-s_{j}\right)} \cdot\left|\Delta Z_{i j}\right| λij=1+exp(sisj)1ΔZij
可以这么理解Lambda,Lambda量化了一个待排序的文档在下一次迭代时应该调整的方向和强度

可以看出,LambdaRank不是通过显示定义损失函数再求梯度的方式对排序问题进行求解,而是分析排序问题需要的梯度的物理意义,直接定义梯度,可以反向推导出LambdaRank的损失函数为:
L i j = log ⁡ { 1 + exp ⁡ ( s i − s j ) } ⋅ ∣ Δ Z i j ∣ L_{i j}=\log \left\{1+\exp \left(s_{i}-s_{j}\right)\right\} \cdot\left|\Delta Z_{i j}\right| Lij=log{ 1+exp(sisj)}ΔZij
注意, 这个东西是从梯度反向推出了损失函数, 而不是从损失函数,然后求梯度。

3.3 LambdMART

理解了Lambda和MART的定义后,对LambdaMART理解起来就不难了。LambdaMART模型结果由许多棵决策树通过Boosting思想组成,每棵树的拟合目标是损失函数的梯度,这里的梯度采用Lambda方法计算。
在这里插入图片描述
算法的参数有:决策树的数量 M M M、叶子节点数 L L L和学习率 η \eta η

  1. 初始时, 没有决策树模型, 因而每个文档的模型得分为0.
  2. 针对每棵树的训练, 算法会遍历训练数据中的label不同文档时, 求出label不同文档对位置互换导致的指标变化 ∣ Δ Z i j ∣ \left|\Delta Z_{i j}\right| ΔZij以及 λ i j \lambda_{ij} λij, 进而得到每个文档的Lambda值 λ i \lambda_i λi
  3. 计算每个 λ i \lambda_i λi的导数 w i w_i wi, 用于后面的Newton step求解叶子节点的数值。
  4. 以所有文档的 λ \lambda λ作为label训练一棵决策树,注意这里 λ \lambda λ是决策树的拟合目标。建立决策树的关键是如何决定分裂节点。LambdaMART采用最朴素的最小二乘法,也就是最小化平方误差和来分裂节点:即对于某个选定的feature,选定一个值val,所有<=val的样本分到左子节点,>val的分到右子节点。然后分别对左右两个节点计算Lambda的平方误差和,并加在一起作为这次分裂的代价,进而选出代价最小的(feature,val)作为当前分裂点,最后生成一颗叶子节点数为L的决策树。
  5. 对上述生成的决策树,采用Newton step计算每个叶子节点的数值,即对落入该叶子节点的文档集,计算该叶子节点的输出值。
  6. 更新模型,将当前学习到的决策树加入到已有的模型中,用学习率v做regularization。

这就是LambdaMART模型了, 这是一种ListWise类型的LTR算法, 基于Lambda思想和MART算法,将搜索引擎结果排序问题转换为了回归决策树问题。优点如下:

  • 直接求解排序问题,而不是用分类或者回归的方法;
  • 可以将NDCG之类的不可求导的评价指标转换为可导的损失函数,具有明确的物理意义;
  • 可以在已有模型的基础上进行Continue Training;
  • 每次迭代选取gain最大的特征进行梯度下降,因此可以学到不同特征的组合情况,并体现出特征的重要程度(特征选择);
  • 对正例和负例的数量比例不敏感。

LGBMRanker模型和这个LambdaMART的原理很像, 下面我们主要是看看这个东西具体怎么用。

4. LGBMRanker模型的使用

下面这个来自于意哥的整理。
在这里插入图片描述
上面的LGBMRanker默认的boosting_type是gbdt,结合上面的内容,这里的gbdt就相当于score function, 通过gbdt得到score值之后进行分组,构建pair的特征和标签。

具体的训练参数如下:

在这里插入图片描述
LGBMRanker模型相对于分类和回归的模型,有几个参数是专门为排序任务定制的。

  1. group: group值的是每个query,在我们这里就是user对应的item列表的长度
  2. eval_group: 与group类似,只不过这个是用在验证集合中的用户item列表长度
  3. eval_metric: 这里的eval_metric指的就是上面提到的用于优化排序模型的评估指标,默认是使用ndcg
  4. eval_at:这个指的是排序指标中的参数,例如NDCG@5, NDCG@10

之所以分组, 是为了在优化的时候对于每个用户只优化其相关的item, 这里需要我们给定数据集中总共有哪些用户,以及每个用户需要优化多少个item。所以我们传入进去的group参数其实是一个列表,表示的意思就是前面所说的。

以推荐为例,每条sample是uid, iid, label,label=1则代表用户uid点击了物品iid,反之未点击。那么每个group就是同一个uid对应的samples,也就是以uid来划分group。这样的话,优化目标就是优化同一个group内的list序,也就是同一个用户,其点击的item越靠前越好,未点击的越靠后越好。

以搜索为例,比如KDD CUP 2020 MultiModal Recall赛道,给定一个query以及对应的30个candidate product images,30个中有若干个匹配的正样本,其余为负样本,这样就可以将同一个query以及对应的candidate images看作是1个group,来优化group内的序。具体代码实现时,以推荐为例,其中一种使用方式如下,先对数据按照uid排序,这样同一个group的数据是挨在一起的。然后直接去统计每个group内的samples的数量,传入这个数量数组即可。eval_group设置同理。另外,推断预测predict的时候,不需要传入group参数。

def lgb_main(train_final_df, val_final_df=None):
    print('ranker begin....')
    train_final_df.sort_values(by=['user_id'], inplace=True)
    g_train = train_final_df.groupby(['user_id'], as_index=False).count()["label"].values

    if mode == 'offline':
        val_final_df = val_final_df.sort_values(by=['user_id'])
        g_val = val_final_df.groupby(['user_id'], as_index=False).count()["label"].values

    lgb_ranker = lgb.LGBMRanker(
        boosting_type='gbdt', num_leaves=31, reg_alpha=0.0, reg_lambda=1,
        max_depth=-1, n_estimators=300, subsample=0.7, colsample_bytree=0.7, subsample_freq=1,
        learning_rate=0.01, min_child_weight=50, random_state=2018,
        n_jobs=-1) 

    if mode == 'offline':
        lgb_ranker.fit(train_final_df[lgb_cols], train_final_df['label'], group=g_train,
                       eval_set=[(val_final_df[lgb_cols], val_final_df['label'])], eval_group=[g_val],
                       eval_at=[50], eval_metric=['auc',],
                       early_stopping_rounds=50, )
    else:
        lgb_ranker.fit(train_final_df[lgb_cols], train_final_df['label'], group=g_train)

    print('train done...')
    return lgb_ranker

上面这个来自下面的第三篇链接。

参考

猜你喜欢

转载自blog.csdn.net/wuzhongqiang/article/details/110521519