推荐算法学习系列 3:JRC loss讲解与实现

2,530 阅读7分钟

Joint Optimization of Ranking and Calibration with Contextualized Hybrid Model

这是阿里妈妈KDD'2023年发表的一篇文章,在这篇文章中提出了一种基于上下文感知的排序和校准联合优化的精排模型,让精排模型同时拥有pointwise的ctr校准能力和listwise的排序能力。

这篇工作还是很有借鉴意义的:广告推荐场景可以通过listwise提升排序能力,还不丢失模型输出是CTR预估值的优点;搜索场景可以让listwise模型输出也能用于CTR预估,支持搜索多业务混排等复杂场景。

Background

工业界的排序模型通常建模为CTR预估任务。CTR预估关注两个方面的能力:

  • 校准能力(calibration ability),是指模型的输出值应当尽可能逼近真实点击率,这在广告等对ctr预估精准度要求较高的场景来说很重要。尽管有各种各样的排序优化算法,如:pointwise、pairwise和listwise,目前CTR预估中优化方法仍然以pointwise loss为主,这主要因为pointwise loss学习到的预测值可以看做是点击率预估值。
  • 排序能力(ranking ability),是指模型能够具备更好地排序能力,将用户感兴趣地尽可能排的更靠前,属于learning to rank关注的范畴。因此,像pairwise 或 listwise loss等优化方法,比pointwise会具备更好的排序能力,能够更好地学习到相同context下用户的偏好,缺点是输出的预测值反映了相对序,不能作为CTR预估值。

一般来说,关注校准的场景大部分都采用pointwise loss,能够较好的近似真实CTR,但是在排序能力上相对较弱。此外,采用交叉熵的pointwise方法遵循独立同分布(i.i.d.)假设,认为用户点击某个item与否和其他item无关,实际情况是用户通常会先看到整个列表的结果再决定是否点击,与i.i.d的假设有悖,导致排序指标上不是最优的。典型场景是广告。

相反,重点关注排序能力的场景更多采用的是listwise loss,直接优化整个list排序指标,如AUC/NDCG/MAP等,具备较好的排序能力,但是也丧失了预测值真实含义,无法直接用于CTR预估。典型场景是搜索场景,不需要关注预测值的含义,更关注排序效果。

然而真实场景是复杂的,是既要又要,既想要排序能力、也想要校准能力, 比如:

  • 广告场景毋庸置疑也想要排序能力的提升,排序能力越强,意味着排的更好,则用户体验更好,CTR和CVR等指标都更好;
  • 大厂推荐搜索通常是多场景多业务的,在多业务混排场景,同样也关注校准能力,保证不同业务对应的物料打分是可比的。

一种直观的思路是可以通过融合pointwise和listwise损失来达到兼顾这两种能力的目的,会定义两类概率值,a.点击的概率值,b.排在top的概率值。不仅使得问题变复杂,还使得logits的概念不明确,打破了logits为CTR预估值的含义,且可能导致非最优解。

Motivation

为了解决这一的问题,作者提出了联合优化排序能力校准能力的方法: Jointly optimize the Ranking and Calibration abilities (同时优化校准和排序的损失函数,简写为JRC),通过将一维的logits拓展成二维的logits,在此基础上联合优化排序目标和校准目标(注意这里的校准和业务通常 CTR 校准不是一个概念, 目标和通常意义下的二分类排序损失 + LTR 损失联合建模更接近)。

  • 在排序能力上:通过listwise-like loss的方式来优化不同label样本的区分能力,提升排序能力。
  • 在校准能力上:可以证明模型输出logits上的减法操作可用于CTR预估,能够较好的近似真实CTR。

JRC损失函数公式如下:

lcalib=x,ylogp^(yx)=x,ylogexp(fθ(x))1+exp(fθ(x))=x,ylog11+exp(fθ(x))l_{\mathrm{calib}}=-\sum_{x,y}\log\left.\hat{p}\left(y|x\right)=-\sum_{x,y}\log\frac{\exp(f_\theta(x))}{1+\exp(f_\theta(x))}=-\sum_{x,y}\log\frac1{1+\exp(-f_\theta(x))}\right.

Joint Optimization of Calibration

image.png

  • 作者认为 CTR 模型不能兼顾训练准度和校准的原因是我们有两个想要的目标, 但在默认的设定下只有一个 logit; 假如模型的输出有两个 logit, (也就是对 logit 增加一个自由度), 就能把校准和排序的目标分解开, 怎么拆开呢?
  • 怎么把一个 logit 拆成 2 个? 把原本模型的输出从 1 个 logit 强行拆分成两个 logit , 一个是点击状态 logit, 一个是不点击状态 logit; 我们约定模型参数为 θθ , 用 fθ(x)[0]f_θ(x)[0] 表示非点击状态下的 logit, 且用 fθ(x)[1]f_θ(x)[1] 表示点击状态下的 logit
p^(yx)=11+exp((fθ(x)[1]fθ(x)[0])两个logit的差作为预估点击概率,取代原本1个自由度下的fθ(x))\hat{p}\left(y|x\right)=\frac{1}{1+\exp(-\underbrace{(f_\theta(x)[1]-f_\theta(x)[0])}_{\text{两个logit的差作为预估点击概率,取代原本1个自由度下的}f_\theta(x)})}

有了上述新的2个自由度的建模方式之后, 相应的 pointwise 的校准 loss 如下所示, 这个式子其实就是等价于两个 logits 采用了 softmax 的计算:

lcalib=x,ylogp^(yx)=x,ylogexp(fθ(x)[y])exp(fθ(x)[1])+exp(fθ(x)[0])=x,y[ylogp^(y=1x)(1y)log(1p^(y=1x))]\begin{align*} l_{\mathrm{calib}} &= -\sum_{x,y}\log\hat{p}(y|x) \\ &= -\sum_{x,y}\log\frac{\exp(f_\theta(x)[y])}{\exp(f_\theta(x)[1])+\exp(f_\theta(x)[0])} \\ &= -\sum_{x,y}\left[ y\cdot\log\hat{p}(y=1|x)-(1-y)\log(1-\hat{p}(y=1|x)) \right] \end{align*}

这个公式到底是怎么推导出来的呢?

这里要还原一下 softmax 的本质,softmax 这个函数其实就是 sigmoid 在多分类下的推广;softmax 和 sigmoid 在二分类任务上有区别吗?完全没有。

这里猜测作者在思考解耦 ranking 和 calibration 的时候, 还是不想放弃属于 Listwise 的建模的 naive 思想:经典Listwise 的建模方式本质上是建模的是元素排在 Top1 的概率, 模型计算 loss 执行的是 softmax 的计算过程, 然后再这样的 softmax 和 二分类的 sigmoid 的关系中找到一种可联系二分类的的方法

output(x1)=11+ex1=sigmoid(x1)=ex1ex1+ex2=11+e(x1x2)写成z1=softmax(x1x2)output(z1)=11+ez1output(x1)\begin{aligned} &output(x_1) =\frac1{1+e^{-x_1}}=\mathrm{sigmoid}(x_1) \\ &=\frac{e^{x_1}}{e^{x_1}+e^{x_2}}=\frac1{1+e^{-\underbrace{(x_1-x_2)}_{\text{写成}z_1}}}=\mathrm{softmax}(x_1-x_2) \\ &output(z_1) =\frac1{1+e^{-z_1}}\Leftrightarrow output(x_1) \end{aligned}

Joint Optimization of Ranking

listwise 的 ranking 损失应该怎么表达呢? 选择一个 session,目标是对点击 logit 和 未点击 logit 都增加一个 listwise 的 loss 提升排序建模,其中让正样本的点击 logit 大于 session 内其他样本的点击 logit,让负样本的非点击 logit 大于同 session 内样本的非点击的 logit。

具体来说,主要是两部分:

  • 对于正样本, 对比的是点击logits 和其他样本的点击logits, 也就是fθ(x)[1]f_\theta(x)[1]
  • 对于负样本, 对比的是非点击logits 和其他样本的非点击logits, 也就是fθ(x)[0]f_\theta(x)[0]
lrank=logexp(fθ(x)[y])xsessionxexp(fθ(x)[y]),y{0,1}l_{rank}=-\log\frac{\exp(f_\theta(x)[y])}{\sum_{x'\in\text{session}_x}\exp(f_\theta(x')[y])},y\in\{0,1\}

其实我们对比下 softmax 函数, 本质上是没有区别的

softmax(x)=exp(xi)j=1nexp(xj)\mathrm{softmax}(x)=\frac{\exp(x_i)}{\sum_{j=1}^n\exp(x_j)}

综合两个 loss , 得到最终的 JRC loss 如下

lfinal=αlcalib+(1α)lrankl_\mathrm{final}=\alpha l_\mathrm{calib}+(1-\alpha)l_\mathrm{rank}

最终的JRC目标是上述两个目标的加权融合:α\alpha是超参数,用于权衡两种目标的重要度。相比于单个预测输出值,两个输出预测值额外引入了1个自由度,在这两个预测值上定义排序目标和校准目标,可以较好地缓解目标之间的冲突性。

Code of JRC loss

# B: batch size, label: [B, 2], context_index: [B, 1]
# Feed forward computation to get the 2-dimensional logits 
# and compute LogLoss -log p(y|x, z)
logits = feed_forward(inputs)
ce_loss = mean(CrossEntropyLoss(logits, label))

# Mask: shape [B, B], mask[i,j]=1 indicates the i-th sample 
# and j-th sample are in the same context
mask = equal(context_index, transpose(context_index))

# 铺开 logits 和 label [B,2] => [B, B, 2]
logits = tile(expand_dims(logits, 1), [1, B, 1])
y = tile(expand_dims(label, 1), [1, B, 1])

# Set logits that are not in the same context to -inf
y = y * expand_dims(mask, 2)
logits = logits + (1-expand_dims(mask, 2))*-1e9 
y_neg, y_pos = y[:,:,0], y[:,:,1]
l_neg, l_pos = logits[:,:,0], logits[:,:,1]

# Compute listwise generative loss -log p(x|y, z)
loss_pos = -sum(y_pos * log(softmax(l_pos, axis=0)), axis=0) 
loss_neg = -sum(y_neg * log(softmax(l_neg, axis=0)), axis=0) 
ge_loss = mean((loss_pos + loss_neg) / sum(mask, axis=0))

# 合并JRC的 loss
loss = alpha * ce_loss + (1 - alpha) * ge_loss

参考文献: