paper2skills Playbook

NeuralNDCG — 可微分排序优化与Learning to Rank

Skill-NeuralNDCG-Learning-to-Rank · 05-推荐系统

causalexperimentforecastingoptimizationrecommendationpricing供应链与补货推荐与搜索定价与利润WF-A 智能补货WF-F 动态定价WF-G Listing内容优化
年化 ROI10万
业务视角
适用角色运营负责人 / 选品负责人 · 产品经理 · 广告优化师
适用平台Amazon · DTC 独立站 · 邮件/SMS 个性化
什么情况下用老客来了只买一件就走,相关产品没被推出去;Bundle 商品连带销售做不起来;站内推荐位点击率低
成功是什么样的老客连带购买率提升 20-35%,客单价提升,品类交叉销售做起来
业务痛点
老客复购率上不去相关产品没有被看到Bundle 凑单没人用新品没有曝光机会

1. 解决的问题

是排序评估指标与训练损失函数之间的不匹配。

2. 核心算法逻辑

Learning to Rank(LTR)的核心问题是排序评估指标与训练损失函数之间的不匹配。模型用交叉熵或MSE训练,但业务用 NDCG 评估——这就像用"练习册分数"预测"考试成绩",两者可能背道而驰。

3. 业务应用场景

背景:用户在 Amazon/Shopify 店铺搜索"baby bottle",系统返回 100+ 商品。当前按销量排序,导致新品/高利润品无法曝光。

LTR建模: 1. 特征工程(Pointwise输入): - 商品特征:价格、评分、评论数、退货率、库存深度 - 用户特征:浏览历史、购买历史、地域、设备 - 交互特征:CTR、加购率、转化率 2. Pairwise 训练数据构建: - 从点击日志提取偏好对:用户点击了商品A但没点击商品B -> A > B - 用 LambdaRank 损失训练初始模型 3. Listwise 精调(NeuralNDCG): - 用 NeuralNDCG 损失替代 LambdaRank - 直接优化 NDCG@10(前10个结果的排序质量) - 温度参数 tau=0.1,Sinkhorn 迭代30次 4. 评

预期效果:搜索转化率提升 10-15%,新品曝光量增加 30%。

4. 输入数据要求

请查看原始代码模板获取输入规格。

5. 输出结果

请查看原始代码模板获取输出规格。

6. 业务价值 / ROI

  • 假设母婴出海平台日均搜索UV 10万,搜索转化率从 2.5% -> 2.9%(+16%)
  • 客单价 ¥300,日均GMV增量 = 10万 x 0.4% x ¥300 = ¥12万/天
  • 年GMV增量:约 ¥4380万
  • REVISION 解决"用户搜什么"(意图识别)
  • NeuralNDCG 解决"结果怎么排"(排序优化)
  • 两者结合形成完整的智能搜索链路:意图理解 → 候选召回 → 精排优化 → 结果展示

7. 代码模板

代码块数量:4 · 路径:未检测到

"""
NeuralNDCG: Learning to Rank with Differentiable Sorting
基于可微分排序松弛的NDCG直接优化

论文: NeuralNDCG: Direct Optimisation of a Ranking Metric via
      Differentiable Relaxation of Sorting
arXiv: 2102.07831
代码: https://github.com/allegro/allRank
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from typing import List, Tuple, Optional


# ============ 1. NeuralSort: 可微分排序松弛 ============

def neural_sort(scores: torch.Tensor, tau: float = 1.0) -> torch.Tensor:
    """
    NeuralSort: 用softmax生成近似排序分配矩阵
    P_hat[b, i, j] = 第j个item排在第i位的概率
    """
    batch_size, list_size = scores.shape

    # 计算 pairwise 绝对差和
    scores_j = scores.unsqueeze(1)  # [B, 1, N]
    scores_k = scores.unsqueeze(2)  # [B, N, 1]
    abs_diff = torch.abs(scores_j - scores_k)  # [B, N, N]
    abs_diff_sum = abs_diff.sum(dim=2, keepdim=True)  # [B, N, 1]

    # row_factor[i] = n + 1 - 2i
    row_indices = torch.arange(1, list_size + 1, device=scores.device, dtype=scores.dtype)
    row_factor = (list_size + 1 - 2 * row_indices)  # [N]

    # term1[b, i, j] = row_factor[i] * scores[b, j]
    term1 = row_factor.view(1, -1, 1) * scores.unsqueeze(1)  # [B, N, N]

    sorted_scores = term1 - abs_diff_sum  # [B, N, N]
    P_hat = F.softmax(sorted_scores / tau, dim=2)
    return P_hat


def sinkhorn_scaling(P: torch.Tensor, num_iter: int = 30,
                     epsilon: float = 1e-6) -> torch.Tensor:
    """Sinkhorn迭代归一化:使矩阵行列和均为1"""
    for _ in range(num_iter):
        P = P / (P.sum(dim=2, keepdim=True) + epsilon)
        P = P / (P.sum(dim=1, keepdim=True) + epsilon)
    return P


# ============ 2. NDCG相关函数 ============

def ndcg_at_k(scores: torch.Tensor, relevances: torch.Tensor,
              k: Optional[int] = None) -> torch.Tensor:
    """计算标准NDCG@k(不可微,仅用于评估)"""
    if k is None:
        k = scores.shape[1]

8. 论文来源

  • 2102.07831