反事实推荐 - 双重校准估计器(DCE)
Skill-Counterfactual-Recommendation-DCE · 05-推荐系统
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