# Learning To Rank 之 RankNet

先记录一下自己的梯度简化推导过程，我自己纸上推了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\_{\omega} = - \sum\_{i=1}^{N}{t\_i \times log(f\_{\omega}(x\_i)) + (1-t\_i) \times log(1-f\_{\omega}(x\_i))}
$$

其中：

* $$f\_{\omega}(x\_i)$$ 是网络的输出，范围应该在0～1之间，最后一般在Linear层后接入一个Sigmoid激活函数来达到这样的效果。
* $$t\_i$$ 是优化目标，一般来说 $$t\_i\in{0,1}$$，但是实际上允许0～1之间的任意实数。

在RankNet中

$$
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))}
$$

其中：

* $$t\_ij$$的取值为：
  * 如果$$s\_i>s\_j$$，则为1
  * 如果$$s\_i\<s\_j$$，则为0
  * 如果$$s\_i=s\_j$$，则为0.5
  * PyTorch中可以用 `(torch.sign(si-sj)+1.0)*0.5` 计算得到
* $$s\_i$$ 与 $$s\_j$$ 分别是项目i和j的输出分数
* 集合S中记录了所有需要计算的i，j对。

## 如果我们强行令$$s\_i>s\_j$$

如果我们强制$$s\_i>s\_j$$（如果$$s\_i\<s\_j$$的话就交换，且不计算相等的pair）。 那么我们得到一个简单的Loss：

$$
L\_{\omega} = - \sum\_{i,j \in S}{log(sigmoid(s\_i-s\_j))}
$$

### PyTorch的实现

```python
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\_{\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)) } }
$$

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

### PyTorch的实现

```python
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我们也不计算了，直接计算梯度优化。 之所以要计算梯度不是因为少一步能快点，而是我们可以直接针对输出的$$s\_i$$这样的评分输出进行优化。 这样优化可以大大减少网络前馈计算的次数，假设每个用户有N个item，那么我们就可以把前馈从$$N^2$$减少到N次。

$$
\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}}
$$

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

$$
\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}} )
$$

其中$$t\_{ij}$$和$$S\_{ij}$$的关系是：$$t\_{ij} = \frac{(1+S\_{ij})}{2}$$

我个人是使用$$t\_{ij}$$推导了一遍，结果是一样的，形式不一样，不相信的可以自己展开看。

$$
\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}} )
$$

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

不妨把前面的部分叫做$$\lambda\_{ij}$$，这也是论文的核心思想的前戏。 先整理一下：

$$
\lambda\_{ij}= \frac{(1-S\_{ij})}{2} - \frac{1}{1+e^{s\_i-s\_j}}= sigmoid(s\_i-s\_j)-t\_{ij}
$$

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

$$
\lambda\_{i}= \sum\_{j: {i,j}\in I}{\lambda\_{ij}} - \sum\_{j: {j,i}\in I}{\lambda\_{ij}}
$$

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

### 最终版本的PyTorch的实现

```python
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)
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://blog.tsingjyujing.com/ml/recsys/ranknet.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
