清雨影的Blog
  • README
  • 机器学习
    • 一般话题
      • 再谈正则化项
      • 论文阅读:“快把卷积神经网络中的平移不变性带回来”
      • 半监督/无监督学习收集
      • 收藏夹
    • 推荐系统
      • Understanding LightGCN in a visualized way
      • Learning To Rank 之 RankNet
      • 随想: BPR Loss 与 Hinger Loss
      • 关于AA测试和AB测试的一些思考
      • 无采样的矩阵分解
      • 收藏夹
    • 强化学习
      • Re:从零开始的Multi-armed Bandit
  • 高级物理引擎实战指南笔记
    • 弹簧质点系统
    • 光滑粒子法
    • 专题:线性方程组求解
  • 有限单元法
    • 1. 引论
    • 2. 基于直接刚度法的杆系有限元方法
    • 3. 针对复杂几何形状变形体的力学描述(1)
  • Web开发相关技术
    • JWT简介
  • 技术杂文
    • React-Script转Vite时引用路径的问题
    • Let's encrypt -- 让我们一起愉快的使用HTTPS
    • 干掉吸血雷,重塑和谐P2P环境
    • 开源CAN总线信号可编程台架
    • Linux下利用mdadm设置软件 RAID
    • 互不联网时代的自给自足
    • 为什么我劝你不要使用云计算?
    • 科学的公司内网连接技术选型
    • 构建家用NAS过程中的碎碎念
    • 简易的Linux迁移指北
    • 记录一次rsync命令引起的异常
    • 为FFMPEG添加Intel QSV支持
    • 备忘录
    • 福冈外免切替(中国驾照换日本驾照)攻略
    • 记一个离谱的MySQL语句的性能问题
    • 拯救变砖的OpenWRT路由器
    • 使用FRP进行内网穿透
  • 政治不正确
    • 吃屎系列:资本家如何喂员工吃屎
      • 华为251事件记忆
    • 吃屎系列:资本家如何喂用户吃屎
      • 互不联网公司是如何强奸用户的(持续更新)
    • 吃屎系列:大学如何喂学生吃屎
    • 推荐系统如何让我们变得极端
    • 互联网政治圈观察日志
    • 中国网络防火长城简史
    • 《线性代数》(同济版)——教科书中的耻辱柱
    • 杂谈
      • 访谈:为什么毛泽东时代工人的积极性很高?
      • 90年代到21世纪初的商业环境
    • 为什么不应该用国产手机
    • “救救孩子”
  • 随园食单
    • ボロネーゼ
    • 甜酒酿的制作
    • 香草与香料
    • 皮塔饼
    • 韭菜鸡蛋饼
    • 牛肉蔬菜汤
由 GitBook 提供支持
在本页
  • 标准的 RankNet Loss 推导
  • 如果我们强行令
  • PyTorch的实现
  • 如果我们采用矩阵计算加速
  • PyTorch的实现
  • 如果我们直接计算梯度
  • 最终版本的PyTorch的实现

这有帮助吗?

  1. 机器学习
  2. 推荐系统

Learning To Rank 之 RankNet

先记录一下自己的梯度简化推导过程,我自己纸上推了3遍,每次纸丢了就要重新推。 特此记录,省的全尼玛忘了。

标准的 RankNet Loss 推导

对于Ranknet,其实是将一个排序问题(比如Top N推荐)演变成一个分类问题。 假设我们已经有一个训练好的评分器,输入User ID和Item ID能给出一个评分,那么,这个评分应该满足越相关(或者用户越喜欢)数值越大。 那么在训练这个评分器的时候,我们假定有i和j两个item,且i更加相关,那么对于分类来说满足:

f(u,i)>f(u,j)f(u,i)>f(u,j)f(u,i)>f(u,j)

换个写法:

P(f(u,i)>f(u,j))=1P(f(u,i)>f(u,j))=1P(f(u,i)>f(u,j))=1

对于一般的BCE Loss训练的分类模型,我们有:

对于一般的BCE Loss,我们有:

Lω=−∑i=1Nti×log(fω(xi))+(1−ti)×log(1−fω(xi))L_{\omega} = - \sum_{i=1}^{N}{t_i \times log(f_{\omega}(x_i)) + (1-t_i) \times log(1-f_{\omega}(x_i))}Lω​=−i=1∑N​ti​×log(fω​(xi​))+(1−ti​)×log(1−fω​(xi​))

其中:

  • fω(xi)f_{\omega}(x_i)fω​(xi​) 是网络的输出,范围应该在0~1之间,最后一般在Linear层后接入一个Sigmoid激活函数来达到这样的效果。

  • tit_iti​ 是优化目标,一般来说 ti∈{0,1}t_i\in\{0,1\}ti​∈{0,1},但是实际上允许0~1之间的任意实数。

在RankNet中

Lω=−∑i,j∈Stij×log(sigmoid(si−sj))+(1−tij)×log(1−sigmoid(si−sj))L_{\omega} = - \sum_{i,j \in S}{t_{ij} \times log(sigmoid(s_i-s_j)) + (1-t_{ij}) \times log(1-sigmoid(s_i-s_j))}Lω​=−i,j∈S∑​tij​×log(sigmoid(si​−sj​))+(1−tij​)×log(1−sigmoid(si​−sj​))

其中:

  • tijt_ijti​j的取值为:

    • 如果si>sjs_i>s_jsi​>sj​,则为1

    • 如果si<sjs_i<s_jsi​<sj​,则为0

    • 如果si=sjs_i=s_jsi​=sj​,则为0.5

    • PyTorch中可以用 (torch.sign(si-sj)+1.0)*0.5 计算得到

  • sis_isi​ 与 sjs_jsj​ 分别是项目i和j的输出分数

  • 集合S中记录了所有需要计算的i,j对。

如果我们强行令si>sjs_i>s_jsi​>sj​

如果我们强制si>sjs_i>s_jsi​>sj​(如果si<sjs_i<s_jsi​<sj​的话就交换,且不计算相等的pair)。 那么我们得到一个简单的Loss:

Lω=−∑i,j∈Slog(sigmoid(si−sj))L_{\omega} = - \sum_{i,j \in S}{log(sigmoid(s_i-s_j))}Lω​=−i,j∈S∑​log(sigmoid(si​−sj​))

PyTorch的实现

import torch.nn
import torch.nn.functional as F

def ranknet_bce_loss(diff_output: torch.FloatTensor, weight: torch.FloatTensor = None):
    """
    Calculate the loss of rncf with weight, and reduce by mean()
    We assumed that all the output is positive (1)
    :param diff_output: The value of net(x1)-net(x2)
    :param weight: The weight for each sample
    :return: Loss of ranknet
    """
    y_loss = -F.logsigmoid(diff_output)
    if weight is not None:
        return torch.mul(y_loss, weight).mean()
    else:
        return y_loss.mean()

如果我们采用矩阵计算加速

考虑一下,假设某个用户(或者query)有N个item,如果我们计算除了某个用户的所有的item的分数,那么Loss的计算就如

Lω=−∑i=1N∑j=1Ntij×log(sigmoid(si−sj))+(1−tij)×log(1−sigmoid(si−sj))L_{\omega} = - \sum_{i=1}^{N}{ \sum_{j=1}^{N}{ t_{ij} \times log(sigmoid(s_i-s_j)) + (1-t_{ij}) \times log(1-sigmoid(s_i-s_j)) } }Lω​=−i=1∑N​j=1∑N​tij​×log(sigmoid(si​−sj​))+(1−tij​)×log(1−sigmoid(si​−sj​))

注意:原则上来说,只要计算不含对角线的下三角矩阵就可以了,也就是j从i+1开始计算。损失函数应该是对称的。 但是这里为了在numpy或者pytorch等框架下矩阵比循环快,且可读性好出发,所以这里j从1开始计算。

PyTorch的实现

import torch.nn
import torch.nn.functional as F

def ranknet_loss(
        score_predict: torch.Tensor,
        score_real: torch.Tensor,
):
    """
    Calculate the loss of ranknet without weight
    :param score_predict: 1xN tensor with model output score
    :param score_real: 1xN tensor with real score
    :return: Loss of ranknet
    """
    score_diff = torch.sigmoid(score_predict - score_predict.t())
    tij = (1.0 + torch.sign(score_real - score_real.t())) / 2.0
    loss_mat = tij * torch.log(score_diff) + (1-tij)*torch.log(1-score_diff)
    return loss_mat.sum()

如果我们直接计算梯度

这一次我们优化到直接计算梯度,对,就连Loss我们也不计算了,直接计算梯度优化。 之所以要计算梯度不是因为少一步能快点,而是我们可以直接针对输出的sis_isi​这样的评分输出进行优化。 这样优化可以大大减少网络前馈计算的次数,假设每个用户有N个item,那么我们就可以把前馈从N2N^2N2减少到N次。

∂Lω∂ω=∂Lω∂si∂si∂ω+∂Lω∂sj∂sj∂ω\frac{\partial{L_{\omega}}}{\partial{\omega}}= \frac{\partial{L_{\omega}}}{\partial{s_i}}\frac{\partial{s_i}}{\partial{\omega}} + \frac{\partial{L_{\omega}}}{\partial{s_j}}\frac{\partial{s_j}}{\partial{\omega}}∂ω∂Lω​​=∂si​∂Lω​​∂ω∂si​​+∂sj​∂Lω​​∂ω∂sj​​

这里有两个结果列出,第一个是论文里的结果(sigma先取1好了):

∂Lω∂ω=((1−Sij)2−11+esi−sj)(∂si∂ω−∂sj∂ω)\frac{\partial{L_{\omega}}}{\partial{\omega}} = (\frac{(1-S_{ij})}{2} - \frac{1}{1+e^{s_i-s_j}})( \frac{\partial{s_i}}{\partial{\omega}} - \frac{\partial{s_j}}{\partial{\omega}} )∂ω∂Lω​​=(2(1−Sij​)​−1+esi​−sj​1​)(∂ω∂si​​−∂ω∂sj​​)

其中tijt_{ij}tij​和SijS_{ij}Sij​的关系是:tij=(1+Sij)2t_{ij} = \frac{(1+S_{ij})}{2}tij​=2(1+Sij​)​

我个人是使用tijt_{ij}tij​推导了一遍,结果是一样的,形式不一样,不相信的可以自己展开看。

∂Lω∂ω=(sigmoid(si−sj)−tij)(∂si∂ω−∂sj∂ω)\frac{\partial{L_{\omega}}}{\partial{\omega}} = (sigmoid(s_i-s_j)-t_{ij})( \frac{\partial{s_i}}{\partial{\omega}} - \frac{\partial{s_j}}{\partial{\omega}} )∂ω∂Lω​​=(sigmoid(si​−sj​)−tij​)(∂ω∂si​​−∂ω∂sj​​)

这样我们就看到,针对 ω\omegaω 的Loss的导数其实有两部分,一部分针对项目i,一部分针对项目j的。

不妨把前面的部分叫做λij\lambda_{ij}λij​,这也是论文的核心思想的前戏。 先整理一下:

λij=(1−Sij)2−11+esi−sj=sigmoid(si−sj)−tij\lambda_{ij}= \frac{(1-S_{ij})}{2} - \frac{1}{1+e^{s_i-s_j}}= sigmoid(s_i-s_j)-t_{ij}λij​=2(1−Sij​)​−1+esi​−sj​1​=sigmoid(si​−sj​)−tij​

我们可以看到,对网络整体的导数可以被分解为对每一个项目的导数。

λi=∑j:{i,j}∈Iλij−∑j:{j,i}∈Iλij\lambda_{i}= \sum_{j: \{i,j\}\in I}{\lambda_{ij}} - \sum_{j: \{j,i\}\in I}{\lambda_{ij}}λi​=j:{i,j}∈I∑​λij​−j:{j,i}∈I∑​λij​

这里的意思是,把所有的pairabpair_{ab}pairab​里面a=ia=ia=i或者b=ib=ib=i的挑选出来,如果i在前面,那就取正,否则就取负。 全部加起来以后,就是对这一个分数项的梯度了。

最终版本的PyTorch的实现

import torch.nn
import torch.nn.functional as F

def ranknet_grad(
        score_predict: torch.Tensor,
        score_real: torch.Tensor,
) -> torch.Tensor:
    """
    Get loss from one user's score output
    :param score_predict: 1xN tensor with model output score
    :param score_real: 1xN tensor with real score
    :return: Gradient of ranknet
    """
    sigma = 1.0
    score_predict_diff_mat = score_predict - score_predict.t()
    score_real_diff_mat = score_real - score_real.t()
    tij = (1.0 + torch.sign(score_real_diff_mat)) / 2.0
    lambda_ij = torch.sigmoid(sigma * score_predict_diff_mat) - tij
    return lambda_ij.sum(dim=1, keepdim=True) - lambda_ij.t().sum(dim=1, keepdim=True)
上一页Understanding LightGCN in a visualized way下一页随想: BPR Loss 与 Hinger Loss

最后更新于2年前

这有帮助吗?