在外卖和即时零售业务中,获客成本并不应该只和“首单金额”比较。一个新用户未来会下多少单、是否会使用 Instamart、能持续贡献多少收入,才决定一次广告转化是否真正值得。Swiggy 为 Food 和 Instamart 构建了内部预测客户终身价值(predicted Customer Lifetime Value,pLTV)模型,使用 350 多个下单前特征,并通过多任务 MLP 同时预测多个相关目标。
这个信号随后与 Google Target ROAS 出价结合,用于优化客户获取。更值得注意的是,订单数量被设置为辅助任务后,模型参数减少了 63%,预测效果反而得到提升。
pLTV 的输入不只是用户画像
传统的用户价值模型容易把特征集中在年龄、地域、设备或历史消费等静态信息上。但在广告转化场景中,模型必须在下单前完成判断,因此特征设计更接近“用户当前是否可能形成长期价值”。
可以纳入的特征类型包括:
- 用户历史订单次数、最近一次订单距今天数和订单间隔。
- Food 与 Instamart 的品类偏好、使用频率和交叉购买行为。
- 价格、优惠券、配送费、折扣敏感度等交易特征。
- 城市、服务可用性、配送时段和供给情况。
- 广告渠道、活动、设备和转化上下文。
- 下单前可观察到的会话行为,例如浏览、搜索和加购信号。
“350 多个特征”本身并不意味着特征越多越好。关键约束是时间点:线上预测时只能使用广告曝光或下单前已经可获得的信息,不能把首单之后的行为泄漏进输入。否则离线指标会很好,投放时却会失效。
为什么把订单数设为辅助任务
pLTV 通常是一个回归目标,但客户终身价值往往由多个行为共同决定。订单数量是其中一个更直接、更稳定的中间信号:订单越多,通常意味着更高的消费机会和更长的关系周期。
多任务 MLP 可以共享底层表示,同时输出不同任务的预测结果:
输入:350+ 个下单前特征
│
共享的 MLP 表示层
┌───┴────┐
│ │
pLTV 回归头 订单数回归头
训练时可以将两个损失组合起来:
总损失 = pLTV 损失 + λ × 订单数损失
订单数任务提供了额外的监督信号,帮助共享层学习“高价值用户”的行为结构,而不是只拟合一个可能噪声较大的金额标签。Swiggy 的实践显示,这种辅助任务不仅改善了预测表现,还让模型参数量减少了 63%。这通常意味着模型可以在较小的表示层上获得更有效的共享特征,也可能降低线上推理和维护成本。
不过,多任务学习并不是自动有效。辅助任务与主任务相关性不足时,可能产生负迁移;两个标签的数值范围差异很大时,未经处理的损失也会让某一个任务主导训练。因此需要监控各任务损失、离线指标和线上投放结果,而不能只看总 loss。
一个可运行的多任务 MLP 示例
下面的示例使用 PyTorch 生成 350 维合成特征,同时训练 pLTV 回归头和订单数回归头。它不是 Swiggy 的生产实现,数据、标签和网络宽度都只是演示假设;实际项目需要替换为经过时间切分和脱敏处理的业务数据。
运行前安装依赖:
python -m pip install torch
python multitask_pltv.py
将以下内容保存为 multitask_pltv.py:
import torch
from torch import nn
torch.manual_seed(7)
# 演示假设:每个样本有 350 个下单前特征。
num_samples = 4096
num_features = 350
x = torch.randn(num_samples, num_features)
# 合成两个相关标签:订单数和 pLTV。
latent = 0.8 * x[:, 0] - 0.4 * x[:, 1] + 0.2 * x[:, 2]
orders = torch.poisson(torch.exp((latent * 0.25).clamp(-1.0, 1.0)))
pltv = 30.0 + 12.0 * latent + 4.0 * orders + torch.randn(num_samples) * 3.0
class MultiTaskMLP(nn.Module):
def __init__(self, input_dim):
super().__init__()
self.shared = nn.Sequential(
nn.Linear(input_dim, 64),
nn.ReLU(),
nn.Linear(64, 32),
nn.ReLU(),
)
self.pltv_head = nn.Linear(32, 1)
self.orders_head = nn.Linear(32, 1)
def forward(self, features):
representation = self.shared(features)
return self.pltv_head(representation).squeeze(-1), self.orders_head(representation).squeeze(-1)
model = MultiTaskMLP(num_features)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
pltv_loss_fn = nn.SmoothL1Loss()
orders_loss_fn = nn.MSELoss()
auxiliary_weight = 0.2
for epoch in range(20):
predicted_pltv, predicted_orders = model(x)
main_loss = pltv_loss_fn(predicted_pltv, pltv)
auxiliary_loss = orders_loss_fn(predicted_orders, orders)
loss = main_loss + auxiliary_weight * auxiliary_loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (epoch + 1) % 5 == 0:
print(
f"epoch={epoch + 1:02d} "
f"total={loss.item():.3f} "
f"pltv={main_loss.item():.3f} "
f"orders={auxiliary_loss.item():.3f}"
)
parameter_count = sum(parameter.numel() for parameter in model.parameters())
print(f"trainable_parameters={parameter_count}")
生产环境中可以继续改造几个部分:对 pLTV 做对数变换以降低长尾影响;对订单数使用 Poisson 或负二项分布损失;通过验证集调节 auxiliary_weight;并按时间划分训练集、验证集和测试集,避免未来行为泄漏到过去样本。
从预测价值到广告出价
pLTV 模型的价值不在于单独生成一张用户分数表,而在于进入投放决策。将预测价值提供给 Google Target ROAS 后,广告系统可以把“获得一个新客户”的目标从短期收入扩展到预期长期回报。
一个简化的业务流程可以写成:
广告点击或转化
│
提取下单前特征
│
预测 pLTV
│
回传价值信号
│
Target ROAS 调整竞价
│
观察长期订单与收入
这里有两个容易被忽略的边界。第一,模型输出必须经过校准,否则广告平台接收到的价值会系统性偏高或偏低。第二,不能只用离线 RMSE 判断投放价值,还要观察获客成本、长期订单数、收入增量和不同渠道之间的预算变化。
还需要处理预测时延、特征缺失、冷启动用户和 Food 与 Instamart 之间的业务差异。两个业务共享部分底层用户信号是合理的,但完全共用标签定义和价值窗口可能会掩盖真实差异。
落地检查清单
- 明确 pLTV 的预测窗口、收入口径和退款处理规则。
- 固定预测时点,只使用下单前可获得的特征。
- 对 350 多个特征建立血缘、缺失率和线上可用性监控。
- 比较单任务模型与多任务模型,确认辅助任务确实改善主任务。
- 检查辅助任务权重,防止订单数损失压过 pLTV 目标。
- 同时评估回归误差、排序能力、校准度和广告增量效果。
- 在 Google Target ROAS 中逐步放量,设置预算与异常回滚机制。
Swiggy 的案例说明,客户终身价值预测不是单纯堆叠特征或扩大网络规模。更可靠的路径是把可观测的用户行为、相关的辅助目标和实际投放反馈连接起来,再用严格的时间边界保证预测在真实广告决策中成立。