paper2skills Playbook

反事实推荐 - 双重校准估计器(DCE)

Skill-Counterfactual-Recommendation-DCE · 05-推荐系统

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

1. 解决的问题

是 MNAR(Missing Not At Random)选择偏差:用户只对系统曝光过的商品产生反馈,而曝光本身受热度/历史 CTR 影响,导致推荐模型陷入"自我强化"循环。

2. 核心算法逻辑

电商推荐系统的核心痛点是 MNAR(Missing Not At Random)选择偏差:用户只对系统曝光过的商品产生反馈,而曝光本身受热度/历史 CTR 影响,导致推荐模型陷入"自我强化"循环。DCE 用双重校准同时校准倾向分(propensity)与插补误差(imputation),即插即用与所有 DR 变体兼容。

3. 业务应用场景

- 业务问题:Shopee 印尼站某德国奶粉因历史曝光高 CTR 数据被高估,推荐模型持续压制澳洲/新西兰品牌的同质量 SKU。新妈妈点击不到优质冷门品牌,小品牌冷启动失败率 80%+。 - 数据要求:用户行为日志(曝光/点击/购买) + 商品特征(品牌/价格/认证) - 预期产出:DCE-DR 模型给出的去偏 CTR 预测,新品牌召回率提升 - 业务价值:小品牌冷启动 ROI 提升 30-50%,平台 SKU 多样性扩大,小品牌入驻意愿↑;按印尼站月 GMV 5000 万元计,长尾品牌 GMV 增量约 200-400 万元/月

- 业务问题:母婴电商的核心特征是月龄驱动的时间窗口需求(0-6月奶粉1段, 6-12月辅食),用户在"错误时期"未购买 ≠ 无需求,但模型把"未购"当作"不喜欢"。 - 数据要求:用户行为日志 + 宝宝月龄 + 商品适用月龄属性 - 预期产出:对每个用户做"如果在正确月龄推送,购买概率是多少"的反事实预测,前置 1-2 月推送 - 业务价值:前置触达转化率提升 25-40%,以美亚母婴专区 100 万月活计,GMV 增量约 80-150 万元/月

4. 输入数据要求

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

5. 输出结果

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

6. 业务价值 / ROI

  • 长尾品牌 GMV 增量:200-400 万元/月(以印尼站 5000 万 GMV 计)
  • 模型部署成本:GPU 训练 ~2 万元/月 + 工程 1 人月
  • ROI ≈ 100-200 倍/月
  • 前置触达 GMV 增量:80-150 万元/月(美亚母婴专区 100 万月活)
  • 年化收益:1000-1800 万元
  • 易处:有官方 PyTorch 开源代码可直接复用

7. 代码模板

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

"""
DCE (Doubly Calibrated Estimator) 最小骨架
论文 arXiv:2403.00817, WWW 2024 (oral)
官方完整实现: https://github.com/WonbinKweon/DCE_WWW2024
"""
from __future__ import annotations
import torch
import torch.nn as nn
import torch.nn.functional as F


class CalibratedPropensityModel(nn.Module):
    def __init__(self, n_users: int, n_items: int, emb_dim: int = 32, n_experts: int = 5):
        super().__init__()
        self.user_emb = nn.Embedding(n_users, emb_dim)
        self.item_emb = nn.Embedding(n_items, emb_dim)
        self.router = nn.Sequential(nn.Linear(emb_dim, n_experts), nn.Softmax(dim=1))
        self.a = nn.Parameter(torch.ones(n_experts))
        self.b = nn.Parameter(-torch.ones(n_experts))

    def forward(self, users: torch.Tensor, items: torch.Tensor, T: float = 1e-3) -> torch.Tensor:
        u = self.user_emb(users)
        v = self.item_emb(items)
        logit = (u * v).sum(-1)

        pi = self.router(u)
        g = -torch.log(-torch.log(torch.rand_like(pi) + 1e-10) + 1e-10)
        pi = F.softmax((pi.log() + g) / T, dim=1)

        logit_exp = logit.unsqueeze(1).expand(-1, self.a.size(0))
        p_cal = torch.sigmoid(logit_exp * self.a + self.b)
        return (p_cal * pi).sum(1).clamp(1e-4, 1 - 1e-4)


def dce_dr_loss(
    pred: torch.Tensor,
    label: torch.Tensor,
    prop: torch.Tensor,
    imp_pred: torch.Tensor,
    gamma: float = 0.05,
) -> torch.Tensor:
    inv_p = 1.0 / prop.detach().clamp(gamma, 1.0)
    ips_term = F.binary_cross_entropy(pred, label, weight=inv_p, reduction="mean")
    imp_term = F.binary_cross_entropy(pred, imp_pred.detach(), reduction="mean")
    return ips_term - imp_term


def main() -> None:
    n_users, n_items = 5000, 2000
    model = CalibratedPropensityModel(n_users, n_items, emb_dim=32, n_experts=5)

    users = torch.randint(0, n_users, (128,))
    items = torch.randint(0, n_items, (128,))
    labels = torch.randint(0, 2, (128,)).float()

    prop = model(users, items)
    print(f"校准倾向分均值: {prop.mean():.4f}, 标准差: {prop.std():.4f}")

    pred = torch.sigmoid(torch.randn(128))
    imp = torch.sigmoid(torch.randn(128))

8. 论文来源

  • 2403.00817