客户终身价值(Customer Lifetime Value,LTV)决定了获客广告到底应该为一个新用户出价多少。Swiggy 的实践重点不只是训练一个更大的模型,而是把下单前可获得的 350 多个特征、Food 与 Instamart 两类业务,以及订单数这一辅助任务组合起来,构建内部预测 LTV(pLTV)模型,并将信号用于 Google Target ROAS 出价优化。
预测目标不止一个
单独预测未来收入,模型需要从大量稀疏行为中推断用户是否会继续下单、会下多少单,以及订单可能带来的价值。Swiggy 采用多任务 MLP,同时学习 pLTV 和订单数等目标。
订单数是一个很自然的辅助任务:它与长期价值直接相关,又比最终收入更容易从用户的早期行为中学习。摘要显示,引入订单数辅助任务后,模型参数量减少了 63%,同时预测表现得到改善。这说明多任务学习的价值不只是“多输出一个结果”,还可以通过共享表示减少重复参数,让主任务获得更稳定的行为信号。
可以把模型抽象成下面的结构:
350+ 个下单前特征
|
共享 MLP 表示
/ \
pLTV 回归头 订单数预测头
Food 和 Instamart 可以共享部分用户行为表示,同时保留业务相关的输出头或特征。实际拆分方式需要根据样本量、标签定义和业务差异验证,不能仅凭业务名称决定模型结构。
为什么强调“下单前”特征
LTV 模型用于获客时,预测只能使用广告曝光或转化决策之前已经知道的信息。把转化之后的订单金额、未来行为或人工生成的事后字段混入特征,会造成标签泄漏:离线指标可能变好,但线上出价会失真。
一组可实践的特征大致可以包括:
- 用户历史访问、搜索、加购和转化行为的时间窗口统计。
- 渠道、设备、地区、时间段等上下文信息。
- Food 与 Instamart 的业务偏好及跨业务互动信号。
- 最近一次活动距今时间、历史订单间隔、行为频率等时序特征。
- 缺失标记、截断后的计数和经过校准的连续变量。
关键不在于简单堆到 350 个字段,而在于每个字段在预测时点是否可用、统计窗口是否固定、线上与离线计算是否一致。高维特征也会放大数据漂移和缺失率问题,因此应记录每个特征的生成时间、覆盖率和版本。
一个可运行的多任务 MLP 示例
下面的示例使用 PyTorch 生成一组模拟数据,演示共享 MLP、pLTV 回归头、订单数辅助头和联合损失。它不是 Swiggy 的生产实现,真实系统需要替换为经过时间切分的数据,并处理类别特征、样本权重、标签延迟和模型校准。
运行前安装依赖:
python -m pip install torch
将下面内容保存为 multitask_ltv.py 后运行:
import torch
from torch import nn
# 固定随机种子,便于复现实验
torch.manual_seed(7)
n_samples, n_features = 4096, 350
x = torch.randn(n_samples, n_features)
# 模拟两个与用户行为相关的监督信号
latent = 0.8 * x[:, 0] - 0.4 * x[:, 1] + 0.2 * x[:, 2]
orders = torch.poisson(torch.exp((latent + 1.0).clamp(-2.0, 2.0)))
ltv = (orders * (12.0 + 3.0 * torch.sigmoid(x[:, 3]))
+ 0.5 * torch.randn(n_samples)).clamp_min(0.0)
class MultiTaskLTV(nn.Module):
def __init__(self, input_dim):
super().__init__()
self.shared = nn.Sequential(
nn.Linear(input_dim, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
)
self.ltv_head = nn.Linear(64, 1)
self.orders_head = nn.Linear(64, 1)
def forward(self, features):
representation = self.shared(features)
predicted_ltv = self.ltv_head(representation).squeeze(-1)
predicted_orders = self.orders_head(representation).squeeze(-1)
return predicted_ltv, predicted_orders
model = MultiTaskLTV(n_features)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
ltv_loss_fn = nn.SmoothL1Loss()
orders_loss_fn = nn.PoissonNLLLoss(log_input=False)
for step in range(500):
predicted_ltv, predicted_orders = model(x)
predicted_orders = predicted_orders.clamp_min(1e-5)
# 主任务预测 LTV,辅助任务预测订单数
loss_ltv = ltv_loss_fn(predicted_ltv, ltv)
loss_orders = orders_loss_fn(predicted_orders, orders)
loss = loss_ltv + 0.3 * loss_orders
optimizer.zero_grad()
loss.backward()
optimizer.step()
if step % 100 == 0:
print(
f"step={step:03d} total={loss.item():.4f} "
f"ltv={loss_ltv.item():.4f} orders={loss_orders.item():.4f}"
)
with torch.no_grad():
sample_ltv, sample_orders = model(x[:3])
print("predicted_ltv:", sample_ltv.tolist())
print("predicted_orders:", sample_orders.tolist())
注意:代码中的 torch.manual_seed(7) 前面多了一个空格时会触发缩进错误。实际保存时应写成:
torch.manual_seed(7)
在生产训练中,可以把 0.3 作为可调的辅助任务权重,并用时间外验证集评估不同权重。订单数标签还可以使用 log1p 变换后的回归目标,具体取决于订单分布和线上决策需要。
pLTV 如何进入获客决策
预测信号的终点不是离线报表,而是广告出价。Swiggy 将 pLTV 与 Google Target ROAS bidding 结合,用于按预期长期价值优化客户获取。
可以这样理解这个闭环:
- 在用户转化前计算特征并预测 pLTV。
- 将预测价值作为转化价值信号传给广告投放系统。
- Target ROAS 根据价值与广告成本的关系调整竞价。
- 用后续真实订单和收入回流,重新评估预测偏差与投放增量。
实践中不能把 pLTV 直接当作真实收入而跳过校准。建议至少检查预测分桶后的实际 LTV、不同渠道的误差、Food 与 Instamart 的分业务误差,以及新老用户和地区之间的稳定性。若模型只学到“高活跃用户更有价值”,却没有带来增量获客,广告系统可能只是为原本就会转化的人提高出价。
落地清单
- 明确预测窗口,例如未来 30 天、90 天或更长周期,并固定标签生成规则。
- 只使用广告决策时点可获得的特征,建立防止标签泄漏的特征审计。
- 以共享 MLP 加多个任务头作为基线,再比较单任务模型和更复杂结构。
- 记录主任务与辅助任务的损失、分桶校准结果和跨业务表现。
- 评估参数量、推理延迟、特征新鲜度和线上缺失率,而不只看离线误差。
- 通过增量实验判断 pLTV 出价是否真正改善获客效率。
Swiggy 的案例给出的核心启发是:LTV 预测需要同时考虑特征可用性、行为辅助信号和广告系统接口。多任务学习可以让模型以更少参数学习更有用的用户表示,但最终价值仍要由时间外评估、校准和真实投放结果共同验证。