排序之损失函数List-wise loss(系列3)

排序系列篇:

最早的关于list-wise的文章发表在Learning to Rank: From Pairwise Approach to Listwise Approach中,后面陆陆续续出了各种变形,但是也是万变不离其宗,本文梳理重在原理。

论文链接listNet,参考的实现代码:实现代码

1. 为什么要List-wise loss

pairwise优缺点
优点:

  • 一些已经被验证的较好的分类模型可以直接拿来用。
  • 在一些特定场景下,其pairwise features 很容易就可以获得。

缺点:

  • 其学习的目标是最小化文档对的分类错误,而不是最小化文档排序的错误。学习目标和实际目标(MAE,NDCG)有所违背。
  • 训练过程可能是极其耗时的,因为生成的文档对样本数量可能会非常多。

那么本篇论文是如何解决这些问题呢?
在pointwise 中,我们将每一个<query, document> 作为一个训练样本来训练一个分类模型。这种方法没有考虑文档之间的顺序关系;而在pariwise 方法中考虑了同一个query 下的任意两个文档的相关性,但同样有上面已经讲过的缺点;在listwise 中,我们将一个<query,documents> 作为一个样本来训练,其中documents 为与这个query 相关的文件列表
论文中还提出了概率分布的方法来计算listwise 的损失函数。并提出了permutation probability 和top one probability 两种方法。下面会详述这两种方法。

2. 方法介绍

2.1. loss输入格式

假设我们有m 个 querys:
Q = ( q ( 1 ) , q ( 2 ) , q ( 3 ) , . . . , q ( m ) ) Q=(q^{(1)}, q^{(2)}, q^{(3)},...,q^{(m)}) Q=(q(1),q(2),q(3),...,q(m))
每个query 下面有n 个可能与之相关的文档(对于不同的query ,其n 可能不同)
d ( i ) = ( d 1 ( i ) , d 2 ( i ) , . . . , d n ( i ) ) d^{(i)} = (d^{(i)}_1, d^{(i)}_2, ..., d^{(i)}_n) d(i)=(d1(i),d2(i),...,dn(i))
对于每个query 下的所有文档,我们可以根据具体的应用场景得到每个文档与query 的真实相关度得分。
y ( i ) = ( y 1 ( i ) , y 2 ( i ) , . . . . , y n ( i ) ) y^{(i)} = (y^{(i)}_1, y^{(i)}_2, ...., y^{(i)}_n) y(i)=(y1(i),y2(i),....,yn(i))
我们可以从每一个文档对 ( q ( i ) , d j ( i ) ) (q^{(i)}, d^{(i)}_{j}) (q(i),dj(i))得到该文档的打分, q ( i ) q^{(i)} q(i)与文档集合 d ( i ) d^{(i)} d(i)中的每个文档打分,可以得到该query 下的所有文档的特征向量:
x ( i ) = ( x 1 ( i ) , x 2 ( i ) , . . . , x n ( i ) ) x^{(i)} = (x^{(i)}_1, x^{(i)}_2, ..., x^{(i)}_n) x(i)=(x1(i),x2(i),...,xn(i))
并且在已知每个文档真实相关度得分的条件下:
y ( i ) = ( y 1 ( i ) , y 2 ( i ) , . . . , y n ( i ) ) y^{(i)} = (y^{(i)}_1, y^{(i)}_2, ..., y^{(i)}_n) y(i)=(y1(i),y2(i),...,yn(i))
我们可以构建训练样本:
T = { ( x ( i ) , y ( i ) ) } T=\begin{Bmatrix} (x^{(i)}, y^{(i)}) \end{Bmatrix} T={(x(i),y(i))}

要特别注意的是:这里面一个训练样本是 ( x ( i ) , y ( i ) ) (x^{(i)}, y^{(i)}) (x(i),y(i)),而这里的 x ( i ) x^{(i)} x(i)是一个与query 相关的文档列表,这也是区别于pointwise 和pairwise 的一个重要特征。

关于 y ( i ) y^{(i)} y(i)paper里面的相关描述:
在这里插入图片描述

2.2. loss计算

那么有训练样本了,如何计算loss 呢?
假设我们已经有了排序函数 f ff,我们可以计算特征向量 x ( i ) x^{(i)} x(i)的得分情况:
z ( i ) = ( f ( x 1 ( i ) ) , f ( x 2 ( i ) ) , . . . , f ( x n ( i ) ) ) z^{(i)} = (f(x_1^{(i)}), f(x_2^{(i)}), ..., f(x_n^{(i)})) z(i)=(f(x1(i)),f(x2(i)),...,f(xn(i)))
显然我们学习的目标就是,最小化真实得分和预测得分的误差:
∑ i = 1 m L ( y ( i ) , z ( i ) ) \sum_{i=1}^{m} L(y^{(i)}, z^{(i)}) i=1mL(y(i),z(i))
L 为 listwise 的损失函数。

2.2.1. 概率模型

假设对于某一个query 而言,与之可能相关的文档有 { 1 , 2 , 3 , . . . , n } \{1, 2, 3, ..., n\} {1,2,3,...,n},假设某一种排序的结果为 π \pi π
π = < π ( 1 ) , π ( 2 ) , . . , π ( n ) > \pi=<\pi(1), \pi(2), .., \pi(n)> π=<π(1),π(2),..,π(n)>

对于n 个文档,有n! 种排列情况。这所有的排序情况记为 Ω n \Omega_n Ωn。假设已有排序函数,那么对于每个文档,我们都可以计算出相关性得分 s = ( s 1 , s 2 , . . . , s n ) s = (s_1, s_2, ..., s_n) s=(s1,s2,...,sn)。 显然对于每一种排序情况,都是有可能发生的,但是每一种排列都有其最大似然值。

我们可以这样定义某一种排列 π \pi π的概率(最大似然值):
P s ( π ) = ∏ j = 1 n ϕ ( s π ( j ) ) ∑ k = j n ϕ ( s π ( k ) ) P_s(\pi) = \prod_{j=1}^{n} \frac{\phi (s_{\pi(j)})}{\sum_{k=j}^{n}\phi(s_{\pi(k)})} Ps(π)=j=1nk=jnϕ(sπ(k))ϕ(sπ(j))
其中 ϕ \phi ϕ表示对分数的归一化处理。

例如有三个文档 π = < 1 , 2 , 3 > \pi = <1,2,3> π=<1,2,3> ,其排序函数计算每个文档得分为 s = ( s 1 , s 2 , s 3 ) s=(s_1, s_2, s_3) s=(s1,s2,s3),则该种排序概率为:
P s ( π ) = ϕ ( s 1 ) ϕ ( s 1 ) + ϕ ( s 2 ) + ϕ ( s 3 ) ⋅ ϕ ( s 2 ) ϕ ( s 2 ) + ϕ ( s 3 ) ⋅ ϕ ( s 3 ) ϕ ( s 3 ) P_s(\pi) =\frac{\phi(s_1)}{\phi(s_1)+\phi(s_2)+\phi(s_3)} \cdot \frac{\phi(s_2)}{\phi(s_2)+\phi(s_3)} \cdot \frac{\phi(s_3)}{\phi(s_3)} Ps(π)=ϕ(s1)+ϕ(s2)+ϕ(s3)ϕ(s1)ϕ(s2)+ϕ(s3)ϕ(s2)ϕ(s3)ϕ(s3)
对于另外一种排序,例如 π ′ = < 3 , 2 , 1 > {\pi}' = <3,2,1> π=<3,2,1> ,则这种排列概率为:

P s ( π ) = ϕ ( s 3 ) ϕ ( s 3 ) + ϕ ( s 2 ) + ϕ ( s 1 ) ⋅ ϕ ( s 2 ) ϕ ( s 1 ) + ϕ ( s 2 ) ⋅ ϕ ( s 1 ) ϕ ( s 1 ) P_s(\pi) =\frac{\phi(s_3)}{\phi(s_3)+\phi(s_2)+\phi(s_1)} \cdot \frac{\phi(s_2)}{\phi(s_1)+\phi(s_2)} \cdot \frac{\phi(s_1)}{\phi(s_1)} Ps(π)=ϕ(s3)+ϕ(s2)+ϕ(s1)ϕ(s3)ϕ(s1)+ϕ(s2)ϕ(s2)ϕ(s1)ϕ(s1)

很明显,< 3,2,1 >这个排序的打分最低,< 1,2,3 >这个排序的打分最高。

2.2.2. Top K Probability

上面那种计算排列概率的方式,其计算复杂度达到 n ! n! n!,太耗时间,由此论文中提出了一种更有效率的方法 top one。我们在这里推广到top k来分析总结。

上面计算某一种排序方式概率:
P s ( π ) = ∏ j = 1 n ϕ ( s π ( j ) ) ∑ k = j n ϕ ( s π ( k ) ) P_s(\pi) = \prod_{j=1}^{n} \frac{\phi (s_{\pi(j)})}{\sum_{k=j}^{n}\phi(s_{\pi(k)})} Ps(π)=j=1nk=jnϕ(sπ(k))ϕ(sπ(j))
排在第一位的有 n n n 种情况,排在第二位的有 n − 1 n−1 n1 种情况,后面依次类推。相当与利用 top n n n来计算。
那么 top K ( K < n ) K(K<n) K(K<n)计算:
P s ( π ) = ∏ j = 1 K ϕ ( s π ( j ) ) ∑ k = j n ϕ ( s π ( k ) ) P_s(\pi) = \prod_{j=1}^{K} \frac{\phi (s_{\pi(j)})}{\sum_{k=j}^{n}\phi(s_{\pi(k)})} Ps(π)=j=1Kk=jnϕ(sπ(k))ϕ(sπ(j))
同理,这里的计算复杂度为 n ∗ ( n − 1 ) ∗ ( n − 2 ) ∗ . . . ∗ ( n − k + 1 ) n∗(n−1)∗(n−2)∗...∗(n−k+1) n(n1)(n2)...(nk+1),即为 N ! / ( N − k ) ! N!/(N-k)! N!/(Nk)!种不同排列,大大减少了计算复杂度。
如果 K = 1 K=1 K=1,就蜕变成论文中top 1 的情况,此时有 n 种不同排列情况:
P s ( π ) = ϕ ( s π ( j ) ) ∑ k = j n ϕ ( s π ( k ) ) P_s(\pi) = \frac{\phi (s_{\pi(j)})}{\sum_{k=j}^{n}\phi(s_{\pi(k)})} Ps(π)=k=jnϕ(sπ(k))ϕ(sπ(j))

对于 N ! / ( N − k ) ! N!/(N-k)! N!/(Nk)!种不同的排列情况,就有 N!/(N−k)! 个排列预测概率,就形成了一种概率分布,再由真实的相关性得分计算相应的排列概率,得到真实的排列概率分布。由此可以利用 cross−entropy 来计算两种分布的距离作为损失函数:
L ( y ( i ) , z ( i ) ) = − ∑ j = 1 n { P y ( i ) ( j ) ∗ l o g ( P z ( i ) ( j ) ) } L(y^{(i)}, z^{(i)}) = - \sum_{j=1}^{n} \{P_{y^{(i)}}(j) *log(P_{z^{(i)}}(j))\} L(y(i),z(i))=j=1n{Py(i)(j)log(Pz(i)(j))}

例如一个查询下有三个文档 < A , B , C > <A,B,C> <A,B,C>

上图中 g 为有真实打分计算出的各种排列的概率分布, f、h 为另外两种排列概率分布,我们就是需要比较那种排列概率分布与真实的排列概率分布更为接近,就用该分布的预测相关性得分作为最终得分。

2.2.3. ListNet

这里给出ListNet最终的形式
在论文中,Listnet只是将上面的 t o p K topK topK 中的 ϕ \phi ϕ函数变成 exp 函数:
P s ( π ) = exp ⁡ ( s π ( j ) ) ∑ k = j n exp ⁡ ( s π ( k ) ) P_s(\pi) = \frac{\exp (s_{\pi(j)})}{\sum_{k=j}^{n}\exp(s_{\pi(k)})} Ps(π)=k=jnexp(sπ(k))exp(sπ(j))

这样不就是计算预测出的得分的softmax 了吗?实际上的确如此,在实现代码中就是这样做的,当时我直接看代码还一脸懵逼,这不就是对文档预测出来的得分做了个softmax 操作吗?跟top−one 有什么关系,仔细看论文才知道怎么回事。

top−1 时,只有n 种排列情况,这大大减少了计算量。如果top K ( K > 1 ) K (K>1) K(K>1),则需要计算的排列情况就会变多。

假设排序函数 f 的参数为w ,则 top-one 的排列概率分布为:
P z ( i ) ( f w ) ( x j ( i ) ) = exp ⁡ ( f w ( x j ( i ) ) ) ∑ k = j n exp ⁡ ( f w ( x k ( i ) ) ) P_{z^{(i)}(f_w)}(x_j^{(i)}) = \frac{\exp (f_w(x_j^{(i)}))}{\sum_{k=j}^{n}\exp(f_w(x_k^{(i)}))} Pz(i)(fw)(xj(i))=k=jnexp(fw(xk(i)))exp(fw(xj(i)))

这里还是需要注意:是将某一个查询下的所有可能与之相关的文档列表,作为一个样本来训练。

最终的损失函数:
L ( y ( i ) , z ( i ) ( f w ) ) = − ∑ j = 1 n { P y ( i ) ( x j ( i ) ) ∗ l o g ( P z ( i ) ( f w ) ( x j ( i ) ) ) } L(y^{(i)}, z^{(i)}(f_w)) = - \sum_{j=1}^{n} \{P_{y^{(i)}}(x_j^{(i)}) *log(P_{z^{(i)}(f_w)}(x_j^{(i)}))\} L(y(i),z(i)(fw))=j=1n{Py(i)(xj(i))log(Pz(i)(fw)(xj(i)))}

小编总结:
简单来说, Listwise的loss其实从本质上可以归纳如下:
step1: 对所有包含正样本和负样本的集合进行softmax; step2: 在用交叉熵对所有样本求和计算loss
但是从原理上来说,其实这只是一个为了计算速度的折中。另外交叉熵中的groundtruth,也就是上面的 y j ( i ) y^{(i)}_j yj(i)打分,这样groundtruth打分越高的,如果预测误差大导致的loss越大,从而对实际为高分但是预测为低分的关注度更高,从而提高top的打分。

实现细节

在看源码的时候发现了一个细节,paper里面说的是交叉熵,但是在代码实现中我发现用的是交叉熵,也就是红色里面是KL散度,而绿色才是交叉熵。
在这里插入图片描述

总结

在pairwise 中,只考虑了两个文档对的相对先后顺序,却没有考虑文档出现在搜索列表中的位置,排在搜索站果前列的文档更为重要,如果前列文档出现判断错误,代价明显高于排在后面的文档。针对这个问题的改进思路是引入代价敏感因素,即每个文档对根据其在列表中的顺序具有不同的权重,越是排在前列的权重越大,即在搜索列表前列,如果排错顺序的话其付出的代价更高(评价指标NDCG); 而listwise 讲一个查询下的所有文档作为一个样本,因为要组合出不同的排列,得到其排列概率分布,来最小化与真实概率分布的误差,这里面就考虑了文档之间的各种顺序关系。很好的避免了这种情况。
从概率模型的角度定义损失函数。
在实做时,其实将一个query 下的的所有可能与之相关的n个doc作为一个训练样本(这时可以理解batch_size=n) ,一定要注意:在计算top_1 probability 时,是在一个query 内的所有文档做softmax ,而不是在当前正在训练的所有的样本内做。这是区别pointwise、pairwise 的重要不同之处。

参考链接:
论文分享— >Learning to Rank: From Pairwise Approach to Listwise Approach

Yolov3是一种流行的目标检测模型,它的损失函数设计非常特殊。与传统的目标检测模型不同,yolov3损失函数不是基于交叉熵或类似的损失函数,而是将目标检测问题定义为一种回归问题,通过对坐标和大小进行回归来预测目标框。 在yolov3损失函数中,主要包含三部分损失函数:置信度损失、分类损失和坐标损失。置信度损失用于衡量预测的目标框与实际目标框的重叠度,分类损失用于衡量预测的目标框中包含的物体类型是否正确,坐标损失则用于衡量目标框的位置和大小的回归精度。 具体的代码实现如下: def yolo_loss(args, anchors, num_classes, rescore_confidence=False, print_loss=False): """ YOLOv3 loss function. :param args: YOLOv3 output tensor list. :param anchors: Anchor box list. :param num_classes: Number of classes. :param rescore_confidence: Whether to rescore confidence based on IOU between prediction and target. :param print_loss: Whether to print loss values for debugging purposes. :return: Total loss tensor. """ # Retrieve model input shape. input_shape = tf.cast(tf.shape(args[0])[1:3] * 32, tf.float32) # Tuple of scalars representing the grid shape (width, height). grid_shape = [tf.cast(tf.shape(args[l])[1:3], tf.float32) for l in range(3)] # Compute scale factors for box width and height. scales = [input_shape / grid_shape[l] for l in range(3)] # Anchor box tensor. anchors_tensor = tf.reshape(tf.constant(anchors, dtype=tf.float32), [1, 1, 1, 3, 2]) # Element-wise compute inverse of anchor box dimensions. anchor_dims = anchors_tensor[..., ::-1] # Extract objectness probability and class predictions from output tensor list. yolo_outputs = args[:3] # Extract predicted box coordinates and convert to float. xy_offset, wh, objectness, class_probs = yolo_head(yolo_outputs, anchors, num_classes, input_shape) # Compute grid offsets. grid_offset = [tf.range(tf.cast(grid_shape[l], tf.float32), dtype=tf.float32) for l in range(2)] grid_offset = tf.meshgrid(grid_offset[1], grid_offset[0]) grid_offset = tf.expand_dims(tf.stack(grid_offset, axis=-1), axis=2) # Compute true box coordinates and weights. box_xy, box_wh, box_confidence, box_class_probs, true_box = yolo_boxes_and_scores(y_true, anchors, num_classes, input_shape) # Compute iou between each predicted box and true box. iou = yolo_box_iou(xy_offset, wh, true_box[..., 0:4], anchor_dims) # Parse batch size from input tensor. batch_size = tf.cast(tf.shape(yolo_outputs[0])[0], tf.float32) # Compute objectness, class and regression losses. object_mask = tf.reduce_max(iou, axis=-1, keepdims=True) * y_true[..., 4:5] object_mask = tf.cast((iou >= object_mask) & (y_true[..., 4:5] > 0), tf.float32) object_mask_neg = tf.cast((iou < object_mask) & (iou >= 0.5), tf.float32) object_mask_pos = tf.cast((iou >= object_mask) & (y_true[..., 4:5] > 0), tf.float32) pred_box_xy = xy_offset * object_mask_pos pred_box_wh = wh * tf.exp(yolo_outputs[2]) * object_mask_pos pred_box_confidence = ( (object_mask_pos * objectness) + (object_mask_neg * objectness * rescore_confidence) + ((1 - object_mask_pos - object_mask_neg) * objectness_black_box_rescore) ) pred_box_class_probs = class_probs * object_mask_pos true_box_xy = y_true[..., 0:2] / scales[0] - grid_offset true_box_wh = y_true[..., 2:4] / scales[0] xy_loss_scale = 2.0 - y_true[..., 2:3] * y_true[..., 3:4] / input_shape / input_shape wh_loss_scale = 2.0 - y_true[..., 2:3] * y_true[..., 3:4] / input_shape / input_shape confidence_loss_scale = (1 - y_true[..., 4:5]) + (y_true[..., 4:5] * 4.) * (1 - yolo_outputs[2]) + 1e-8 class_loss_scale = y_true[..., 4:5] * 1. xy_loss = tf.reduce_sum(tf.square(true_box_xy - pred_box_xy) * xy_loss_scale, axis=-1) wh_loss = tf.reduce_sum(tf.square(tf.sqrt(true_box_wh) - tf.sqrt(pred_box_wh)) * wh_loss_scale, axis=-1) confidence_loss = tf.reduce_sum(tf.square(true_box[..., 4:5] - pred_box_confidence) * confidence_loss_scale, axis=-1) class_loss = tf.reduce_sum(tf.square(true_box[..., 5:] - pred_box_class_probs) * class_loss_scale, axis=-1) # Normalization factos. num_positives = tf.reduce_sum(object_mask_pos, axis=[1, 2, 3]) # Compute total YOLOv3 loss. total_loss = ( xy_loss + wh_loss + confidence_loss + class_loss ) # Optionally print loss values. if print_loss: total_loss = tf.Print( total_loss, [tf.reduce_mean(xy_loss / num_positives), tf.reduce_mean(wh_loss / num_positives), tf.reduce_mean(confidence_loss / num_positives), tf.reduce_mean(class_loss / num_positives)], message=&#39;loss: &#39; ) return total_loss
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI蜗牛之家

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值