先记录一下自己的梯度简化推导过程,我自己纸上推了3遍,每次纸丢了就要重新推。 特此记录,省的全尼玛忘了。
标准的 RankNet Loss 推导
对于Ranknet,其实是将一个排序问题(比如Top N推荐)演变成一个分类问题。 假设我们已经有一个训练好的评分器,输入User ID和Item ID能给出一个评分,那么,这个评分应该满足越相关(或者用户越喜欢)数值越大。 那么在训练这个评分器的时候,我们假定有i和j两个item,且i更加相关,那么对于分类来说满足:
f(u,i)>f(u,j) 换个写法:
P(f(u,i)>f(u,j))=1 对于一般的BCE Loss训练的分类模型,我们有:
对于一般的BCE Loss,我们有:
Lω=−i=1∑Nti×log(fω(xi))+(1−ti)×log(1−fω(xi)) 其中:
fω(xi) 是网络的输出,范围应该在0~1之间,最后一般在Linear层后接入一个Sigmoid激活函数来达到这样的效果。
ti 是优化目标,一般来说 ti∈{0,1},但是实际上允许0~1之间的任意实数。
在RankNet中
Lω=−i,j∈S∑tij×log(sigmoid(si−sj))+(1−tij)×log(1−sigmoid(si−sj)) 其中:
tij的取值为:
如果si>sj,则为1
如果si<sj,则为0
如果si=sj,则为0.5
PyTorch中可以用 (torch.sign(si-sj)+1.0)*0.5 计算得到
si 与 sj 分别是项目i和j的输出分数
如果我们强行令
si>sj
如果我们强制si>sj(如果si<sj的话就交换,且不计算相等的pair)。 那么我们得到一个简单的Loss:
Lω=−i,j∈S∑log(sigmoid(si−sj)) PyTorch的实现
如果我们采用矩阵计算加速
考虑一下,假设某个用户(或者query)有N个item,如果我们计算除了某个用户的所有的item的分数,那么Loss的计算就如
Lω=−i=1∑Nj=1∑Ntij×log(sigmoid(si−sj))+(1−tij)×log(1−sigmoid(si−sj)) 注意:原则上来说,只要计算不含对角线的下三角矩阵就可以了,也就是j从i+1开始计算。损失函数应该是对称的。 但是这里为了在numpy或者pytorch等框架下矩阵比循环快,且可读性好出发,所以这里j从1开始计算。
PyTorch的实现
如果我们直接计算梯度
这一次我们优化到直接计算梯度,对,就连Loss我们也不计算了,直接计算梯度优化。 之所以要计算梯度不是因为少一步能快点,而是我们可以直接针对输出的si这样的评分输出进行优化。 这样优化可以大大减少网络前馈计算的次数,假设每个用户有N个item,那么我们就可以把前馈从N2减少到N次。
∂ω∂Lω=∂si∂Lω∂ω∂si+∂sj∂Lω∂ω∂sj 这里有两个结果列出,第一个是论文里的结果(sigma先取1好了):
∂ω∂Lω=(2(1−Sij)−1+esi−sj1)(∂ω∂si−∂ω∂sj) 其中tij和Sij的关系是:tij=2(1+Sij)
我个人是使用tij推导了一遍,结果是一样的,形式不一样,不相信的可以自己展开看。
∂ω∂Lω=(sigmoid(si−sj)−tij)(∂ω∂si−∂ω∂sj) 这样我们就看到,针对 ω 的Loss的导数其实有两部分,一部分针对项目i,一部分针对项目j的。
不妨把前面的部分叫做λij,这也是论文的核心思想的前戏。 先整理一下:
λij=2(1−Sij)−1+esi−sj1=sigmoid(si−sj)−tij 我们可以看到,对网络整体的导数可以被分解为对每一个项目的导数。
λi=j:{i,j}∈I∑λij−j:{j,i}∈I∑λij 这里的意思是,把所有的pairab里面a=i或者b=i的挑选出来,如果i在前面,那就取正,否则就取负。 全部加起来以后,就是对这一个分数项的梯度了。
最终版本的PyTorch的实现