排序算法(Learn to rank)的一些看法
回来自我隔离期,出不了小区加上倒春寒阴天;疯与快疯之间,重读了微软研究院Learn to Rank几篇经典论文,参考的看了CSDN上不少博主的观点。总觉得对于文章,有些思路上的点没有点透;尝试从排序更根本思路去讲解排序类算法为何如此、以及如此演进。思路:排序从冒泡法说起——打分、参考比较、决策冒泡排序时候每个容器中默认是一个数,所以没有从特征到打分这个步骤冒泡排序时候两个数据大小比...
1.序言
回来自我隔离期,出不了小区加上倒春寒阴天;疯与快疯之间,重读了微软研究院Learn to Rank几篇经典论文,参考的看了CSDN上不少博主的观点。总觉得对于文章,有些思路上的点没有点透;尝试从排序更根本思路去讲解排序类算法为何如此、以及如此演进。
思路:
排序从冒泡法说起——打分、参考比较、决策
冒泡排序时候每个容器中默认是一个数,所以没有从特征到打分这个步骤
冒泡排序时候两个数据大小比对,其实是用我们认为认定的一套数字随比随大的标准;这个参考物就是我们规定好的那些数的顺序,只是我们日用忽视了这个参考比较过程
冒泡排序当符合我们认为的数的大小顺序就认为是对的(1)不需要调整两数顺序,不符合则认为是不对的(-1)调整两数顺序
Ranknet根据query对Document排序步骤和原本思路和冒泡排序如出一撤:
根据文本、用户特征、query特征利用一个函数对Document打分:F(特征、用户、query)
利用打好序列标签的训练样本做参考,比较打完分的文本顺序
决策,Ranknet通过分类的思路解决,如果打完分的两个Documeng排序和大号标签训练样本顺序是一致则为1,否则为0
这就是Learn to Rank算法的最朴素思路、也是后续lambdaRank、lambdaMART、深度学习模型TF-Ranking的建模框架。
多说一句其实大部分现在看起来高深莫测的技术,其实最朴素的思路都来自于我们了无生趣的生活中;好的想法都是简单朴实但是都是框架级别的存在,后续的变化不过是框架里填入什么东西后者小改动什么东西。
问题:
1、Ranknet只考虑到了两两比较文档对之间序列关系(给的是全局排序,它只学了俩俩关系,视野格局不够)
2、lamdaRank引入NDCG或ERR全局序列参数,但NDCG、ERR是非凸在进行梯度求解时候计算复杂
3、lambdaMART通过MART多项式来你和含NDCG或ERR的lambda梯度解决梯度非凸问题,但对于图像、文本、稀疏特征打分效果不够好
改进:
1、Ranknet梯度和deltaNDCG相乘,引入NDCG全局排序因子
2、用凸函数来拟合梯度,比如用MART多相似来拟合
3、引入对文本、图像、稀疏特征处理能力强的深度学习,替代MART或者浅层线性模型
所有的解决对策,都来源于对问题的认识。看到问题才会有对策,难得在于往往牵一发动全身,如何在简单和有效中tade off。
到此其实文章就可以结束了,因为对于learn to rank具体技术细节的文章有太多,具体可见下文参考。但出于文章完整性考虑,我简单介绍Ranknet、lambdaRank、lambdaMART。
-
-
2. RANKNET
-
2.1 算法基础定义
RankNet解决如下搜索排序问题:给定query集合,每个query都对应着一个文档集合,如何对每个query返回排序后的文档集合。可以想象这样的场景:某位高考生在得知自己的成绩后,准备报考志愿。听说最近西湖大学办得不错,所以就想到网上搜搜关于西湖大学的资料。他打开一个搜索引擎,输入“西湖大学”四个字,然后点击“搜索”,页面从上到下显示了10条搜索结果,他认为排在上面的肯定比下面的相关,所以就开始从上往下一个个地浏览。所以RankNet的目标就是对所有query,都能将其返回的文档按照相关性进行排序。
RankNet网络将输入query的特征向量𝑥∈ℝ𝑛x∈Rn映射为一个实数𝑓(𝑥)∈ℝf(x)∈R。RankNet采用pairwise的方法进行模型训练。具体地,给定特定query下的两个文档𝑈𝑖Ui和𝑈𝑗Uj,其特征向量分别为𝑥𝑖xi和𝑥𝑗xj,经过RankNet进行前向计算得到对应的分数为𝑠𝑖=𝑓(𝑥𝑖)si=f(xi)和𝑠𝑗=𝑓(𝑥𝑗)sj=f(xj)。用𝑈𝑖⊳𝑈𝑗Ui⊳Uj表示𝑈𝑖Ui比𝑈𝑗Uj排序更靠前(如对某个query来说,𝑈𝑖Ui被标记为“good”,𝑈𝑗Uj被标记为“bad”)。继而可以用下面的公式来表示𝑈𝑖Ui应该比𝑈𝑗Uj排序更靠前的概率:
𝑃𝑖𝑗≡𝑃(𝑈𝑖⊳𝑈𝑗)≡11+𝑒−𝜎(𝑠𝑖−𝑠𝑗)
这个概率实际上就是深度学习中经常使用的sigmoid函数,参数𝜎σ决定sigmoid函数的形状。对于特定的query,定义𝑆𝑖𝑗∈{0,±1}Sij∈{0,±1}为文档𝑖i和文档𝑗j被标记的标签之间的关联,即
𝑆𝑖𝑗=⎧⎩⎨⎪⎪10−1文档𝑖比文档𝑗更相关文档𝑖和文档𝑗相关性一致文档𝑗比文档𝑖更相关Sij={1文档i比文档j更相关0文档i和文档j相关性一致−1文档j比文档i更相关
定义𝑃⎯⎯⎯⎯𝑖𝑗=12(1+𝑆𝑖𝑗)P¯ij=12(1+Sij)表示𝑈𝑖Ui应该比𝑈𝑗Uj排序更靠前的已知概率,则可以用交叉熵定义优化目标的损失函数:
𝐶=−𝑃⎯⎯⎯⎯𝑖𝑗𝑙𝑜𝑔𝑃𝑖𝑗−(1−𝑃⎯⎯⎯⎯𝑖𝑗)𝑙𝑜𝑔(1−𝑃𝑖𝑗)C=−P¯ijlogPij−(1−P¯ij)log(1−Pij)
如果不太熟悉什么是交叉熵,可以参考宗成庆老师的《统计自然语言处理》2.2节“信息论基本概念”,里面将熵、联合熵、互信息、相对熵、交叉熵和困惑度等概念都讲得相当清楚。
结合以上多个公式,可以改写损失函数𝐶C为:
𝐶=12(1−𝑆𝑖𝑗)𝜎(𝑠𝑖−𝑠𝑗)+𝑙𝑜𝑔(1+𝑒−𝜎(𝑠𝑖−𝑠𝑗))C=12(1−Sij)σ(si−sj)+log(1+e−σ(si−sj))
对于𝑆𝑖𝑗=1Sij=1,
𝐶=𝑙𝑜𝑔(1+𝑒−𝜎(𝑠𝑖−𝑠𝑗))C=log(1+e−σ(si−sj))
然而对于𝑆𝑖𝑗=−1Sij=−1,
𝐶=𝑙𝑜𝑔(1+𝑒−𝜎(𝑠𝑗−𝑠𝑖))C=log(1+e−σ(sj−si))
可以看出损失函数𝐶C具有对称性,也即交换𝑖i和𝑗j的位置,损失函数的值不变。
分析损失函数𝐶C的趋势发现,如果对文档𝑈𝑖Ui和𝑈𝑗Uj的打分可以正确地拟合标记的标签,则𝐶C趋向于0,否则𝐶C趋向于线性函数。具体地,假如𝑆𝑖𝑗=1Sij=1,也即𝑈𝑖Ui应该比𝑈𝑗Uj排序高,如果𝑠𝑖>𝑠𝑗si>sj,则拟合的分数可以正确排序文档𝑖i和文档𝑗j,
lim𝑠𝑖−𝑠𝑗→∞𝐶=lim𝑠𝑖−𝑠𝑗→∞𝑙𝑜𝑔(1+𝑒−𝜎(𝑠𝑖−𝑠𝑗))=𝑙𝑜𝑔1=0limsi−sj→∞C=limsi−sj→∞log(1+e−σ(si−sj))=log1=0
如果𝑠𝑖<𝑠𝑗si<sj,则拟合的分数不能正确排序文档𝑖i和文档𝑗j,
lim𝑠𝑖−𝑠𝑗→∞𝐶=lim𝑠𝑖−𝑠𝑗→∞𝑙𝑜𝑔(1+𝑒−𝜎(𝑠𝑖−𝑠𝑗))=𝑙𝑜𝑔(𝑒−𝜎(𝑠𝑖−𝑠𝑗))=−𝜎(𝑠𝑖−𝑠𝑗)limsi−sj→∞C=limsi−sj→∞log(1+e−σ(si−sj))=log(e−σ(si−sj))=−σ(si−sj)
利用神经网络对模型进行训练,目前最有效的方法就是反向传播算法。反向传播算法中最核心部分就是损失函数对模型参数的求导,然后可以使用下面的公式对模型参数进行迭代更新:
𝑤𝑘←𝑤𝑘−𝜂∂𝐶∂𝑤𝑘=𝑤𝑘−𝜂(∂𝐶∂𝑠𝑖∂𝑠𝑖∂𝑤𝑘+∂𝐶∂𝑠𝑗∂𝑠𝑗∂𝑤𝑘)wk←wk−η∂C∂wk=wk−η(∂C∂si∂si∂wk+∂C∂sj∂sj∂wk)
损失函数𝐶C对𝑠𝑖si和𝑠𝑗sj的偏导数为:
∂𝐶∂𝑠𝑖=𝜎(12(1−𝑆𝑖𝑗)−11+𝑒𝜎(𝑠𝑖−𝑠𝑗))=−∂𝐶∂𝑠𝑗∂C∂si=σ(12(1−Sij)−11+eσ(si−sj))=−∂C∂sj
𝑠𝑖si和𝑠𝑗sj对𝑤𝑘wk的偏导数可根据神经网络求偏导数的方式求得。求得了损失函数𝐶C对神经网络模型参数𝑤𝑘wk的偏导数之后,就可以使用梯度下降算法对其更新。这里的学习率𝜂η也是一个正数,因为𝜂η需要满足下面的不等式:
𝛿𝐶=∑𝑘∂𝐶∂𝑤𝑘𝛿𝑤𝑘=∑𝑘∂𝐶∂𝑤𝑘(−𝜂∂𝐶∂𝑤𝑘)=−𝜂∑𝑘(∂𝐶∂𝑤𝑘)2<0
-
2.2 RankNet分解形式:加速RankNet训练过程
2.1节中定义的RankNet,对于每一个文档对(𝑈𝑖(Ui,𝑈𝑗)Uj)都将计算损失函数对神经网络的参数𝑤𝑘wk的偏导数,然后更新模型参数𝑤𝑘wk。这样做的缺点在于,对模型参数更新慢,耗时长。所以本节讲解如何通过分解组合的方式加快这一训练过程。
对于给定的文档对𝑈𝑖Ui和𝑈𝑗Uj,损失函数𝐶C对参数𝑤𝑘wk的偏导数为:
∂𝐶∂𝑤𝑘=∂𝐶∂𝑠𝑖∂𝑠𝑖∂𝑤𝑘+∂𝐶∂𝑠𝑗∂𝑠𝑗∂𝑤𝑘=𝜎(12(1−𝑆𝑖𝑗)−11+𝑒𝜎(𝑠𝑖−𝑠𝑗))(∂𝑠𝑖∂𝑤𝑘−∂𝑠𝑗∂𝑤𝑘)=𝜆𝑖𝑗(∂𝑠𝑖∂𝑤𝑘−∂𝑠𝑗∂𝑤𝑘)∂C∂wk=∂C∂si∂si∂wk+∂C∂sj∂sj∂wk=σ(12(1−Sij)−11+eσ(si−sj))(∂si∂wk−∂sj∂wk)=λij(∂si∂wk−∂sj∂wk)
其中:
𝜆𝑖𝑗=∂𝐶(𝑠𝑖−𝑠𝑗)∂𝑠𝑖=𝜎(12(1−𝑆𝑖𝑗)−11+𝑒𝜎(𝑠𝑖−𝑠𝑗))λij=∂C(si−sj)∂si=σ(12(1−Sij)−11+eσ(si−sj))
定义𝐼I为索引对{𝑖,𝑗}{i,j}的集合,在不损失信息量的情况下,可以将集合𝐼I中的索引对都转换成满足𝑈𝑖⊳𝑈𝑗Ui⊳Uj的形式。另外集合𝐼I中的索引对还应该满足最多只出现一次的条件。在此基础上,累加权重参数𝑤𝑘wk的更新量:
𝛿𝑤𝑘=−𝜂∑(𝑖,𝑗)∈𝐼(𝜆𝑖𝑗∂𝑠𝑖∂𝑤𝑘−𝜆𝑖𝑗∂𝑠𝑗∂𝑤𝑘)=−𝜂∑𝑖𝜆𝑖∂𝑠𝑖∂𝑤𝑘δwk=−η∑(i,j)∈I(λij∂si∂wk−λij∂sj∂wk)=−η∑iλi∂si∂wk
其中:
𝜆𝑖=∑𝑗:{𝑖,𝑗}∈𝐼𝜆𝑖𝑗−∑𝑗:{𝑗,𝑖}∈𝐼𝜆𝑖𝑗λi=∑j:{i,j}∈Iλij−∑j:{j,i}∈Iλij
𝜆𝑖λi可以看成是作用在排序文档上的力,其正负代表了方向,长度代表了力的大小。最初的实现是对每个文档对,都计算一遍梯度并且更新神经网络的参数值,而这里则是将同一个query下的所有文档对进行叠加,然后更新一次网络的权重参数。这种分解组合形式实际上就是一种小批量学习方法,不仅可以加快迭代速度,还可以为后面使用非连续的梯度模型打下基础。
-
3.LAMBDARANK
-
3.1 为什么需要LambdaRank
先看一张论文原文中的图,如下所示。这是一组用二元等级相关性进行排序的链接地址,其中浅灰色代表链接与query不相关,深蓝色代表链接与query相关。 对于左边来说,总的pairwise误差为13,而右边总的pairwise误差为11。但是大多数情况下我们更期望能得到左边的结果。这说明最基本的pairwise误差计算方式并不能很好地模拟用户对搜索引擎的期望。右边黑色箭头代表RankNet计算出的梯度大小,红色箭头是期望的梯度大小。NDCG和ERR在计算误差时,排名越靠前权重越大,可以很好地解决RankNet计算误差时的缺点。但是NDCG和ERR均是不可导的函数,如何加入到RankNet的梯度计算中去?
3.2 LambdaRank定义
RankNet中的𝜆𝑖𝑗λij可以看成是𝑈𝑖Ui和𝑈𝑗Uj中间的作用力,如果𝑈𝑖⊳𝑈𝑗Ui⊳Uj,则𝑈𝑗Uj会给予𝑈𝑖Ui向上的大小为|𝜆𝑖𝑗||λij|的推动力,而对应地𝑈𝑖Ui会给予𝑈𝑗Uj向下的大小为|𝜆𝑖𝑗||λij|的推动力。如何将NDCG等类似更关注排名靠前的搜索结果的评价指标加入到排序结果之间的推动力中去呢?实验表明,直接用|Δ𝑁𝐷𝐶𝐺||ΔNDCG|乘以原来的𝜆𝑖𝑗λij就可以得到很好的效果,也即:
𝜆𝑖𝑗=∂𝐶(𝑠𝑖−𝑠𝑗)∂𝑠𝑖=−𝜎1+𝑒𝜎(𝑠𝑖−𝑠𝑗)|Δ𝑁𝐷𝐶𝐺|λij=∂C(si−sj)∂si=−σ1+eσ(si−sj)|ΔNDCG|
其中|Δ𝑁𝐷𝐶𝐺||ΔNDCG|是交换排序结果𝑈𝑖Ui和𝑈𝑗Uj得到的NDCG差值。NDCG倾向于将排名高并且相关性高的文档更快地向上推动,而排名地而且相关性较低的文档较慢地向上推动。
另外还可以将|Δ𝑁𝐷𝐶𝐺||ΔNDCG|替换成其他的评价指标。
4. LambdaMART
4.1 逻辑回归+MART进行二分类
了解了MART之后,下面举一个MART实际应用的例子:使用MART和逻辑回归进行二分类。用于分类的样本𝑥𝑖∈ℝ𝑛xi∈Rn,标签𝑦𝑖∈{±1}yi∈{±1},拟合函数𝐹(𝑥)F(x)。为了简化表示,我们表示条件概率如下:
𝑃+≡𝑃(𝑦=1|𝑥)P+≡P(y=1|x)
𝑃−≡𝑃(𝑦=−1|𝑥)P−≡P(y=−1|x)
用交叉熵表示损失函数:
𝐿(𝑦,𝐹)=−𝑦𝑙𝑜𝑔(𝑃+)−(1−𝑦)𝑙𝑜𝑔(𝑃−)L(y,F)=−ylog(P+)−(1−y)log(P−)
逻辑回归使用对数机率(属于正例概率/属于负例概率)进行建模,
𝐹𝑛(𝑥)=12𝑙𝑜𝑔(𝑃+𝑃−)Fn(x)=12log(P+P−)
𝑃+=11+𝑒−2𝜎𝐹𝑛(𝑥)P+=11+e−2σFn(x)
𝑃−=1−𝑃+=11+𝑒2𝜎𝐹𝑛(𝑥)P−=1−P+=11+e2σFn(x)
将𝑃+P+和𝑃−P−带入𝐿(𝑦,𝐹)L(y,F)中,得到:
𝐿(𝑦,𝐹𝑛)=𝑙𝑜𝑔(1+𝑒−2𝑦𝜎𝐹𝑛)L(y,Fn)=log(1+e−2yσFn)
𝑅𝑗𝑚Rjm表示落入第𝑚m棵树的第𝑗j个叶子节点中的样例集合,可以通过下式对该叶子节点的值进行优化:
𝛾𝑗𝑚=𝑎𝑟𝑔min𝛾∑𝑥𝑖∈𝑅𝑗𝑚log(1+𝑒−2𝜎𝑦𝑖(𝐹𝑚−1(𝑥𝑖)+𝛾))γjm=argminγ∑xi∈Rjmlog(1+e−2σyi(Fm−1(xi)+γ))
上式可以使用Newton-Raphson方法按照下面的公式进行迭代求解:
𝛾𝑛+1=𝛾𝑛−𝑔′(𝛾𝑛)𝑔″(𝛾𝑛)γn+1=γn−g′(γn)g″(γn)
4.2 LambdaMART基本定义
LambdaMART基于MART,优化𝜆λ梯度。根据上面的定义,对于任意𝑈𝑖Ui和𝑈𝑗Uj,有:
𝜆𝑖𝑗=∂𝐶(𝑠𝑖−𝑠𝑗)∂𝑠𝑖=−𝜎|Δ𝑍𝑖𝑗|1+𝑒𝜎(𝑠𝑖−𝑠𝑗)λij=∂C(si−sj)∂si=−σ|ΔZij|1+eσ(si−sj)
|Δ𝑍𝑖𝑗||ΔZij|表示交换𝑈𝑖Ui和𝑈𝑗Uj的位置产生的评价指标差值,𝑍Z可以是𝑁𝐷𝐶𝐺NDCG或者𝐸𝑅𝑅ERR等。对于特定𝑈𝑖Ui,累加其他所有排序项的影响,得到:
𝜆𝑖=∑𝑗:{𝑖,𝑗}∈𝐼𝜆𝑖𝑗−∑𝑗:{𝑗,𝑖}∈𝐼𝜆𝑖𝑗λi=∑j:{i,j}∈Iλij−∑j:{j,i}∈Iλij
为了简化表示:
∑{𝑖,𝑗}⇌𝐼𝜆𝑖𝑗=∑𝑗:{𝑖,𝑗}∈𝐼𝜆𝑖𝑗−∑𝑗:{𝑗,𝑖}∈𝐼𝜆𝑖𝑗∑{i,j}⇌Iλij=∑j:{i,j}∈Iλij−∑j:{j,i}∈Iλij
于是我们可以更新损失函数:
∂𝐶∂𝑠𝑖=∑𝑗:{𝑖,𝑗}∈𝐼−𝜎|Δ𝑍𝑖𝑗|1+𝑒𝜎(𝑠𝑖−𝑠𝑗)=∑𝑗:{𝑖,𝑗}∈𝐼−𝜎|Δ𝑍𝑖𝑗|𝜌𝑖𝑗∂C∂si=∑j:{i,j}∈I−σ|ΔZij|1+eσ(si−sj)=∑j:{i,j}∈I−σ|ΔZij|ρij
其中,我们定义:
𝜌𝑖𝑗=11+𝑒𝜎(𝑠𝑖−𝑠𝑗)=−𝜆𝑖𝑗𝜎|Δ𝑍𝑖𝑗|ρij=11+eσ(si−sj)=−λijσ|ΔZij|
然后可以得到:
∂2𝐶∂𝑠2𝑖=∑{𝑖,𝑗}⇌𝐼𝜎2|Δ𝑍𝑖𝑗|𝜌𝑖𝑗(1−𝜌𝑖𝑗)∂2C∂si2=∑{i,j}⇌Iσ2|ΔZij|ρij(1−ρij)
所以我们可以用下面的公式计算第𝑚m棵树的第𝑘k个叶子节点上的值:
𝛾𝑘𝑚=∑𝑥𝑖∈𝑅𝑘𝑚∂𝐶∂𝑠𝑖∑𝑥𝑖∈𝑅𝑘𝑚∂2𝐶∂𝑠2𝑖=−∑𝑥𝑖∈𝑅𝑘𝑚∑{𝑖,𝑗}⇌𝐼|Δ𝑍𝑖𝑗|𝜌𝑖𝑗∑𝑥𝑖∈𝑅𝑘𝑚∑{𝑖,𝑗}⇌𝐼|Δ𝑍𝑖𝑗|𝜎𝜌𝑖𝑗(1−𝜌𝑖𝑗)
5.RankLib实战
LambdaMART.java中的LambdaMART.learn()是学习流程的管控函数,学习过程主要有下面四步构成:
1. 计算deltaNDCG以及lambda;
2. 以lambda作为label训练一棵regression tree;
3. 在tree的每个叶子节点通过预测的regression lambda值还原出gamma,即最终输出得分;
4. 用3的模型预测所有训练集合上的得分(+learningRate*gamma),然后用这个得分对每个query的结果排序,计算新的每个query的base ndcg,以此为基础回到第1步,组成森林。
重复这个步骤,直到满足下列两个收敛条件之一:
1. 树的个数达到训练参数设置;
2. Random Forest在validation集合上没有变好。
下面用一组实际的数据来说明整个计算过程,假设我们有10个query的训练数据,每个query下有10个doc,每个q-d对有10个feature,如下:
0 qid:1830 1:0.002736 2:0.000000 3:0.000000 4:0.000000 5:0.002736 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000
0 qid:1830 1:0.025992 2:0.125000 3:0.000000 4:0.000000 5:0.027360 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000
0 qid:1830 1:0.001368 2:0.000000 3:0.000000 4:0.000000 5:0.001368 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000
1 qid:1830 1:0.188782 2:0.375000 3:0.333333 4:1.000000 5:0.195622 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000
1 qid:1830 1:0.077975 2:0.500000 3:0.666667 4:0.000000 5:0.086183 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000
0 qid:1830 1:0.075239 2:0.125000 3:0.333333 4:0.000000 5:0.077975 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000
1 qid:1830 1:0.079343 2:0.250000 3:0.666667 4:0.000000 5:0.084815 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000
1 qid:1830 1:0.147743 2:0.000000 3:0.000000 4:0.000000 5:0.147743 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000
0 qid:1830 1:0.058824 2:0.000000 3:0.000000 4:0.000000 5:0.058824 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000
0 qid:1830 1:0.071135 2:0.125000 3:0.333333 4:0.000000 5:0.073871 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000
1 qid:1840 1:0.007364 2:0.200000 3:1.000000 4:0.500000 5:0.013158 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000
1 qid:1840 1:0.097202 2:0.000000 3:0.000000 4:0.000000 5:0.096491 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000
2 qid:1840 1:0.169367 2:0.000000 3:0.500000 4:0.000000 5:0.169591 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000
......
为了简便,省略了余下的数据。上面的数据格式是按照Ranklib readme中要求的格式组织(类似于svmlight),除了行号之外,第一列是q-d对的实际label(人标注数据),第二列是qid,后面10列都是feature。
这份数据每组qid中的doc初始顺序可以是随机的,也可以是从实际的系统中获得的当前顺序。总之这个是计算ndcg的初始状态。对于qid=1830,它的10个doc的初始顺序的label序列是:0, 0, 0, 1, 1, 0, 1, 1, 0, 0(虽然这份序列中只有label值为0和1的,实际中也会有2,3等,由自己的标注标准决定)。我们知道dcg的计算公式是:
𝑑𝑐𝑔(𝑖)=2𝑙𝑎𝑏𝑒𝑙(𝑖)−1𝑙𝑜𝑔2(𝑖+1)(1)(1)dcg(i)=2label(i)−1log2(i+1)
i表示当前doc在这个qid下的位置(从1开始,避免分母为0),label(i)是doc(i)的标注值。而一个query的dcg则是其下所有doc的加和:
𝑑𝑐𝑔(𝑞𝑢𝑒𝑟𝑦)=∑𝑖2𝑙𝑎𝑏𝑒𝑙(𝑖)−1𝑙𝑜𝑔2(𝑖+1)(2)(2)dcg(query)=∑i2label(i)−1log2(i+1)
根据上式可以计算初始状态下每个qid的dcg: 𝑑𝑐𝑔(𝑞𝑖𝑑=1830)=20−1𝑙𝑜𝑔2(1+1)+20−1𝑙𝑜𝑔2(2+1)+...+20−1𝑙𝑜𝑔2(10+1)dcg(qid=1830)=20−1log2(1+1)+20−1log2(2+1)+...+20−1log2(10+1)=0+0+0+0.431+0.387+0+0.333+0.315+0+0=1.466=0+0+0+0.431+0.387+0+0.333+0.315+0+0=1.466
要计算ndcg,还需要计算理想集的dcg,将初始状态按照label排序,qid=1830得到的序列是1,1,1,1,0,0,0,0,0,0,计算dcg: 𝑖𝑑𝑒𝑎𝑙_𝑑𝑐𝑔(𝑞𝑖𝑑=1830)=21−1𝑙𝑜𝑔2(1+1)+21−1𝑙𝑜𝑔2(2+1)+...+20−1𝑙𝑜𝑔2(10+1)ideal_dcg(qid=1830)=21−1log2(1+1)+21−1log2(2+1)+...+20−1log2(10+1)=1+0.631+0.5+0.431+0+0+0+0+0+0=2.562=1+0.631+0.5+0.431+0+0+0+0+0+0=2.562
两者相除得到初始状态下qid=1830的ndcg: 𝑛𝑑𝑐𝑔(𝑞𝑖𝑑=1830)=𝑑𝑐𝑔(𝑞𝑖𝑑=1830)𝑖𝑑𝑒𝑎𝑙_𝑛𝑑𝑐𝑔(𝑞𝑖𝑑=1830)=1.4662.562=0.572ndcg(qid=1830)=dcg(qid=1830)ideal_ndcg(qid=1830)=1.4662.562=0.572
下面要计算每一个doc的deltaNDCG,公式如下:
𝑑𝑒𝑙𝑡𝑎𝑁𝐷𝐶𝐺(𝑖,𝑗)=|𝑛𝑑𝑐𝑔(𝑜𝑟𝑖𝑔𝑖𝑛𝑎𝑙 𝑠𝑒𝑞𝑢𝑒𝑛𝑐𝑒)−𝑛𝑑𝑐𝑔(𝑠𝑤𝑎𝑝(𝑖,𝑗) 𝑠𝑒𝑞𝑢𝑒𝑛𝑐𝑒)|(3)(3)deltaNDCG(i,j)=|ndcg(original sequence)−ndcg(swap(i,j) sequence)|
deltaNDCG(i,j)是将位置i和位置j的位置互换后产生的ndcg变化(其他位置均不变),显然有相同label的deltaNDCG(i,j)=0。
在qid=1830的初始序列0, 0, 0, 1, 1, 0, 1, 1, 0, 0,由于前3的label都一样,所以deltaNDCG(1,2)=deltaNDCG(1,3)=0,不为0的是deltaNDCG(1,4), deltaNDCG(1,5), deltaNDCG(1,7), deltaNDCG(1,8)。
将1,4位置互换,序列变为1, 0, 0, 0, 1, 0, 1, 1, 0, 0,计算得到dcg=2.036,整个deltaNDCG(1,4)的计算过程如下: 𝑑𝑐𝑔(𝑞𝑖𝑑=1830,𝑠𝑤𝑎𝑝(1,4))=21−1𝑙𝑜𝑔2(1+1)+20−1𝑙𝑜𝑔2(2+1)+...+20−1𝑙𝑜𝑔2(10+1)dcg(qid=1830,swap(1,4))=21−1log2(1+1)+20−1log2(2+1)+...+20−1log2(10+1) =1+0+0+0+0.387+0+0.333+0.315+0+0=2.036=1+0+0+0+0.387+0+0.333+0.315+0+0=2.036
𝑛𝑑𝑐𝑔(𝑠𝑤𝑎𝑝(1,4))=𝑑𝑐𝑔(𝑠𝑤𝑎𝑝(1,4))𝑖𝑑𝑒𝑎𝑙_𝑑𝑐𝑔=2.0362.562=0.795ndcg(swap(1,4))=dcg(swap(1,4))ideal_dcg=2.0362.562=0.795
𝑑𝑒𝑙𝑡𝑎𝑁𝐷𝐶𝐺(1,4)=𝑑𝑒𝑡𝑎𝑙𝑁𝐷𝐶𝐺(4,1)=|𝑛𝑑𝑐𝑔(𝑜𝑟𝑖𝑔𝑖𝑛𝑎𝑙 𝑠𝑒𝑞𝑢𝑒𝑛𝑐𝑒)−𝑛𝑑𝑐𝑔(𝑠𝑤𝑎𝑝(1,4))|=|0.572−0.795|=0.222deltaNDCG(1,4)=detalNDCG(4,1)=|ndcg(original sequence)−ndcg(swap(1,4))|=|0.572−0.795|=0.222
同样过程可以计算出deltaNDCG(1,5)=0.239, deltaNDCG(1,7)=0.260, deltaNDCG(1,8)=0.267等。
进一步,要计算lambda(i),根据paper,还需要ρ值,ρ可以理解为doci比docj差的概率,其计算公式为:
𝜌𝑖𝑗=11+𝑒𝜎(𝑠𝑖−𝑠𝑗)(4)(4)ρij=11+eσ(si−sj)
Ranklib中直接取σ=1(σ的值决定rho的S曲线陡峭程度),如下图,蓝,红,绿三种颜色分别对应σ=1,2,4时ρ函数的曲线情形(横坐标是si-sj):
初始时,模型为空,所有模型预测得分都是0,所以si=sj=0,ρij≡1/2,lambda(i,j)的计算公式为:
𝜆𝑖𝑗=𝜌𝑖𝑗∗|𝑑𝑒𝑙𝑡𝑎𝑁𝐷𝐶𝐺(𝑖,𝑗)|(5)(5)λij=ρij∗|deltaNDCG(i,j)|
上式为Ranklib中实际使用的公式,而在paper中,还需要再乘以-σ,在σ=1时,就是符号正好相反,这两种方式是等价的,符号并不影响模型训练结果(其实大可以把代码中lambda的值前面加一个负号,只是注意在每轮计算train, valid和最后计算test的ndcg的时候,模型预测的得分modelScores要按升序排列——越负的doc越好,而不是源代码中按降序。最后训练出的模型是一样的,这说明这两种方式完全对称,所以符号的问题可以省略。甚至不乘以-σ,更符合人的习惯——分数越大越好,降序排列结果。):
𝜆𝑖=∑𝑗(𝑙𝑎𝑏𝑒𝑙(𝑖)>𝑙𝑎𝑏𝑒𝑙(𝑗))𝜆𝑖𝑗−∑𝑗(𝑙𝑎𝑏𝑒𝑙(𝑖)<𝑙𝑎𝑏𝑒𝑙(𝑗))𝜆𝑖𝑗(6)(6)λi=∑j(label(i)>label(j))λij−∑j(label(i)<label(j))λij
计算lambda(1),由于label(1)=0,qid=1830中的其他doc的label都大于或者等于0,所以lamda(1)的计算中所有的lambda(1,j)都为负项。将之前计算的各deltaNDCG(1,j)代入,且初始状态下ρij≡1/2,所以: 𝜆1=−0.5∗(𝑑𝑒𝑙𝑡𝑎𝑁𝐷𝐶𝐺(1,3)+𝑑𝑒𝑙𝑡𝑎𝑁𝐷𝐶𝐺(1,4)+𝑑𝑒𝑙𝑡𝑎𝑁𝐷𝐶𝐺(1,6)+𝑑𝑒𝑙𝑡𝑎𝑁𝐷𝐶𝐺(1,7))λ1=−0.5∗(deltaNDCG(1,3)+deltaNDCG(1,4)+deltaNDCG(1,6)+deltaNDCG(1,7)) =−0.5∗(0.222+0.239+0.260+0.267)=−0.495
可以计算出初始状态下qid=1830各个doc的lambda值,如下:
qId=1830 0.000 0.000 0.000 -0.111 -0.120 0.000 -0.130 -0.134 0.000 0.000 lambda(1): -0.495
qId=1830 0.000 0.000 0.000 -0.039 -0.048 0.000 -0.058 -0.062 0.000 0.000 lambda(2): -0.206
qId=1830 0.000 0.000 0.000 -0.014 -0.022 0.000 -0.033 -0.036 0.000 0.000 lambda(3): -0.104
qId=1830 0.111 0.039 0.014 0.000 0.000 0.015 0.000 0.000 0.025 0.028 lambda(4): 0.231
qId=1830 0.120 0.048 0.022 0.000 0.000 0.006 0.000 0.000 0.017 0.019 lambda(5): 0.231
qId=1830 0.000 0.000 0.000 -0.015 -0.006 0.000 -0.004 -0.008 0.000 0.000 lambda(6): -0.033
qId=1830 0.130 0.058 0.033 0.000 0.000 0.004 0.000 0.000 0.006 0.009 lambda(7): 0.240
qId=1830 0.134 0.062 0.036 0.000 0.000 0.008 0.000 0.000 0.003 0.005 lambda(8): 0.247
qId=1830 0.000 0.000 0.000 -0.025 -0.017 0.000 -0.006 -0.003 0.000 0.000 lambda(9): -0.051
qId=1830 0.000 0.000 0.000 -0.028 -0.019 0.000 -0.009 -0.005 0.000 0.000 lambda(10): -0.061
上表中每一列都是考虑了符号的lamda(i,j),即如果label(i)<label(j),则为负值,反之为正值,每行结尾的lamda(i)是前面的加和,即为最终的lambda(i)。
可以看到,lambda(i)在系统中表达了doc(i)上升或者下降的强度,label越高,位置越后,lambda(i)为正值,越大,表示趋向上升的方向,力度也越大;label越小,位置越靠前,lambda(i)为负值,越小,表示趋向下降的方向,力度也大(lambda(i)的绝对值表达了力度。)
然后Regression Tree开始以每个doc的lamda值为目标,训练模型。
Regression Tree的训练很简单,最主要的就是决定如何分裂节点。lambdaMART采用最朴素的最小二乘法,也就是最小化平方误差和来分裂节点:即对于某个选定的feature,选定一个值val,所有<=val的样本分到左子节点,>val的分到右子节点。然后分别对左右两个节点计算平方误差和,并加在一起作为这次分裂的代价。遍历所有feature以及所有可能的分裂点val(每个feature按值排序,每个不同的值都是可能的分裂点),在这些分裂中找到代价最小的。
举个栗子,假设样本只有上一节中计算出 𝜆λ 的那10个:
qId=1830 0.000 0.000 0.000 -0.111 -0.120 0.000 -0.130 -0.134 0.000 0.000 lambda(1): -0.495
qId=1830 0.000 0.000 0.000 -0.039 -0.048 0.000 -0.058 -0.062 0.000 0.000 lambda(2): -0.206
qId=1830 0.000 0.000 0.000 -0.014 -0.022 0.000 -0.033 -0.036 0.000 0.000 lambda(3): -0.104
qId=1830 0.111 0.039 0.014 0.000 0.000 0.015 0.000 0.000 0.025 0.028 lambda(4): 0.231
qId=1830 0.120 0.048 0.022 0.000 0.000 0.006 0.000 0.000 0.017 0.019 lambda(5): 0.231
qId=1830 0.000 0.000 0.000 -0.015 -0.006 0.000 -0.004 -0.008 0.000 0.000 lambda(6): -0.033
qId=1830 0.130 0.058 0.033 0.000 0.000 0.004 0.000 0.000 0.006 0.009 lambda(7): 0.240
qId=1830 0.134 0.062 0.036 0.000 0.000 0.008 0.000 0.000 0.003 0.005 lambda(8): 0.247
qId=1830 0.000 0.000 0.000 -0.025 -0.017 0.000 -0.006 -0.003 0.000 0.000 lambda(9): -0.051
qId=1830 0.000 0.000 0.000 -0.028 -0.019 0.000 -0.009 -0.005 0.000 0.000 lambda(10): -0.061
上表中除了第一列是qId,最后一列是lambda外,其余都是feature,比如我们选择feature(1)的0.059做分裂点,则左子节点<=0.059的doc有: 1, 2, 3, 9;而>0.059的被安排到右子节点,doc有4, 5, 6, 7, 8, 10。由此左右两个子节点的lambda均值分别为:
𝜆𝐿¯=𝜆1+𝜆2+𝜆3+𝜆94=−0.495−0.206−0.104−0.0514=−0.214λL¯=λ1+λ2+λ3+λ94=−0.495−0.206−0.104−0.0514=−0.214 𝜆𝑅¯=𝜆4+𝜆5+𝜆6+𝜆7+𝜆8+𝜆106=0.231+0.231−0.033+0.240+0.247−0.0616=0.143λR¯=λ4+λ5+λ6+λ7+λ8+λ106=0.231+0.231−0.033+0.240+0.247−0.0616=0.143
继续计算左右子节点的平方误差和:
𝑠𝐿=∑𝑖∈𝐿(𝜆𝑖−𝜆𝐿¯)2=(−0.495+0.214)2+(−0.206+0.214)2+(−0.104+0.214)2+(−0.051+0.214)2=0.118sL=∑i∈L(λi−λL¯)2=(−0.495+0.214)2+(−0.206+0.214)2+(−0.104+0.214)2+(−0.051+0.214)2=0.118
𝑠𝑅=∑𝑖∈𝑅(𝜆𝑖−𝜆𝑅¯)2=(0.231−0.143)2+(0.231−0.143)2+(−0.033−0.143)2+(0.240−0.143)2+(0.247−0.143)2+(0.016−0.143)2=0.083sR=∑i∈R(λi−λR¯)2=(0.231−0.143)2+(0.231−0.143)2+(−0.033−0.143)2+(0.240−0.143)2+(0.247−0.143)2+(0.016−0.143)2=0.083
因此将feature(1)的0.059的均方差(分裂代价)是:
𝐶𝑜𝑠𝑡0.059@𝑓𝑒𝑎𝑡𝑢𝑟𝑒(1)=𝑠𝐿+𝑠𝑅=0.118+0.083=0.201Cost0.059@feature(1)=sL+sR=0.118+0.083=0.201
我们可以像上面那样遍历所有feature的不同值,尝试分裂,计算Cost,最终选择所有可能分裂中最小Cost的那一个作为分裂点。然后将 𝑠𝐿sL 和 𝑠𝑅sR 分别作为左右子节点的属性存储起来,并把分裂的样本也分别存储到左右子节点中,然后维护一个队列,始终按平方误差和 s 降序插入新分裂出的节点,每次从该队列头部拿出一个节点(并基于这个节点上的样本)进行分裂(即最大均方差优先分裂),直到树的分裂次数达到参数设定(训练时传入的leaf值,叶子节点的个数与分裂次数等价)。这样我们就训练出了一棵Regression Tree。
上面讲述了一棵树的标准分裂过程,需要多提一点的是,树的分裂还有一个参数设定:叶子节点上的最少样本数,比如我们设定为3,则在feature(1)处,0.001和0.003两个值都不能作为分裂点,因为用它们做分裂点,左子树的样本数分别是1和2,均<3。叶子节点的最少样本数越小,模型则拟合得越好,当然也容易过拟合(over-fitting);反之如果设置得越大,模型则可能欠拟合(under-fitting),实践中可以使用cross validation的办法来寻找最佳的参数设定。
6. 参考
https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/MSR-TR-2010-82.pdf
https://www.microsoft.com/en-us/research/wp-content/uploads/2005/08/icml_ranking.pdf
https://liam.page/uploads/slides/lambdamart.pdf
https://www.cnblogs.com/genyuan/p/9788294.html
https://ai.googleblog.com/2018/12/tf-ranking-scalable-tensorflow-library.html
https://github.com/shiba24/learning2rank
https://www.cnblogs.com/wowarsenal/p/3900359.html
更多推荐
所有评论(0)