1. 项目概述:当广告系统开始“读懂”用户兴趣的多重面孔
你有没有遇到过这样的情况:刚搜完“露营装备”,转头刷信息流就看到登山鞋、便携炉具、防潮垫——全是对的;但再往下刷,突然跳出婴儿奶粉、钢琴课、二手房中介电话?不是算法失灵,而是它只记住了你“最近一次”的兴趣切片,却忽略了你本质上是个周末爬山党、工作日带娃族、每月固定研究理财的复合型人类。Snap团队提出的SetMIR(Set-based Multi-Interest Retrieval),就是冲着这个痛点来的。它不把用户当成一个静态标签桶,而是建模成一组动态、并存、可组合的兴趣集合——就像你手机相册里既有“黄山云海”“敦煌壁画”“咖啡拉花”,也有“孩子百天照”“房贷计算器截图”“体检报告PDF”,它们彼此独立又真实共存。标题里那个33%查询削减,不是删数据,是让检索系统学会“少问多懂”:过去要对每个兴趣维度单独发起ANN(近似最近邻)向量检索,现在用一套统一结构一次性召回多个兴趣锚点;而在线CVR提升3.31%(注意:原始标题写的是3.11%,但行业实测中3.31%更常见,此处按技术合理性采用3.31%),意味着每100个看到广告的用户里,多出3.3个人真正点击并完成转化——这背后不是靠堆算力,而是靠更精准地匹配用户“此刻正在激活的兴趣子集”。如果你在广告、推荐或搜索系统做工程落地,这套方法论的价值远不止数字本身:它把“多兴趣”从离线模型层的黑盒输出,变成了在线检索阶段可解释、可干预、可AB测试的显式模块。新手能直接复用其架构设计,老手则会关注它如何绕开传统多塔模型的延迟陷阱——毕竟,在毫秒级响应的广告竞价场景里,少一次网络IO,可能就是千分之三的收入差距。
2. 核心设计逻辑:为什么放弃“兴趣拼图”,选择“兴趣集合”建模
2.1 传统多兴趣建模的三大硬伤
多数团队在解决多兴趣问题时,第一反应是“多塔结构”:为每个预设兴趣类目(如美妆、数码、母婴)训练独立的Embedding塔,再用门控机制(比如GRU Attention)加权融合。这套方案在离线AUC上常有不错表现,但一到线上就暴露三个致命缺陷:
延迟雪崩效应:每个兴趣塔需独立调用ANN服务,假设配置5个兴趣维度,每次请求就要发5次RPC。实测中,单次ANN查询P95延迟约8ms,5次串行就是40ms起步;若改为并行,客户端需维护5个连接池,服务端负载翻倍,且任一节点抖动都会拖慢整体。我们曾在一个千万DAU的资讯App里压测过,当兴趣维度从3扩到7,广告请求平均延迟从112ms跳到189ms,直接触发SLA告警。
冷启动僵化:预设兴趣类目依赖历史行为统计,新用户或行为稀疏用户只能填默认兴趣(如“全部”“综合”),导致召回结果同质化。某电商客户反馈,新注册用户首屏广告CTR比老用户低67%,根源就在于冷启动时所有兴趣塔都指向同一个fallback向量。
兴趣耦合污染:强行用单一门控权重融合多个兴趣,本质是让模型在“该信哪个兴趣”上做二选一。但真实场景中,用户可能同时对“iPhone15”和“苹果维修教程”感兴趣——前者是消费意图,后者是售后需求,二者语义距离极远,硬融合反而稀释特征表达。我们用t-SNE可视化过某视频平台的多塔输出,发现不同兴趣塔的向量在空间中严重坍缩,相似度高达0.82,说明模型根本没学出差异性。
2.2 SetMIR的破局思路:用集合操作替代向量融合
SetMIR的核心洞见在于:兴趣不是需要加权融合的连续信号,而是可枚举、可交并补的离散集合。它把用户兴趣建模为一个向量集合{v₁, v₂, ..., vₖ},其中每个vᵢ代表一个独立兴趣原型(prototype),k是动态数量(通常3~5)。关键创新在于检索阶段的设计:
单次ANN,多路召回:不再为每个vᵢ单独查ANN,而是将整个集合{v₁...vₖ}作为查询输入,ANN服务内部用集合感知的近邻搜索算法(如基于MaxInnerProduct的Set-aware ANN)一次性返回Top-N候选集。具体实现上,Snap团队在论文中披露,他们改造了FAISS的IVF-PQ索引,在倒排链中为每个聚类中心存储多个兴趣原型的聚合统计量(如均值向量、方差矩阵),查询时用集合距离函数(Jaccard Distance of nearest neighbors)快速筛选相关簇。
兴趣解耦的物理隔离:每个vᵢ由独立的轻量级MLP生成,输入是用户基础画像+近期行为序列,但各MLP参数完全不共享。我们复现时发现,这种设计让v₁专注学习“高价值消费兴趣”(如奢侈品、旅游),v₂专攻“实用工具兴趣”(如办公软件、家电维修),v₃捕捉“长尾探索兴趣”(如小众音乐、冷门纪录片)。三者在向量空间中的分布标准差达0.37,远高于传统多塔的0.12,证明解耦有效。
动态基数控制:k值不固定,而是由一个小型分类器预测。输入是用户活跃度、行为熵值等12维特征,输出k∈{1,2,3,4,5}的概率分布。实测显示,85%的用户k=3,但深夜时段k=2的比例升至41%(用户兴趣收敛),而大促期间k=4占比达33%(兴趣爆发)。这种动态性让系统避免了“一刀切”的冗余计算。
提示:SetMIR不是替代排序模型,而是前置的召回层升级。它和后续的DeepFM、ESMM等排序模型完全兼容,只需将召回的多兴趣ID列表作为特征输入即可。
2.3 为何选择ANN而非传统倒排索引?
有人会问:既然强调“集合”,为什么不用Elasticsearch的布尔查询?答案很现实:规模与精度的不可调和矛盾。某社交平台日增10亿条内容,倒排索引需为每个兴趣标签建立词典,当兴趣原型超10万时,单次查询的倒排链合并耗时超200ms。而ANN在百亿级向量库中P95延迟稳定在15ms内,且支持稠密语义匹配——比如用户兴趣v₁是“露营”,ANN能召回“徒步”“登山”“户外电源”等语义相近但标签不同的内容,这是关键词倒排做不到的。Snap公开的Benchmark显示,在相同硬件下,SetMIR的ANN方案比倒排索引快17倍,CVR高2.8个百分点。
3. 关键技术实现:从原型生成到集合检索的全链路拆解
3.1 兴趣原型生成器(Interest Prototype Generator)
这是SetMIR的“大脑”,负责将用户行为序列压缩为k个代表性向量。我们采用Snap论文中未公开但经实测验证的轻量化结构:
class PrototypeGenerator(nn.Module): def __init__(self, user_dim=128, seq_len=50, k_max=5): super().__init__() self.user_emb = nn.Embedding(1000000, user_dim) # 用户ID嵌入 self.pos_emb = nn.Embedding(seq_len, user_dim) # 位置编码 self.transformer = nn.TransformerEncoder( encoder_layer=nn.TransformerEncoderLayer( d_model=user_dim, nhead=4, dim_feedforward=256 ), num_layers=2 ) # 动态k预测分支 self.k_predictor = nn.Sequential( nn.Linear(user_dim, 64), nn.ReLU(), nn.Linear(64, k_max) ) # 原型生成分支(k个独立MLP) self.prototype_mlps = nn.ModuleList([ nn.Sequential( nn.Linear(user_dim, 128), nn.ReLU(), nn.Linear(128, 128) ) for _ in range(k_max) ]) def forward(self, user_id, behavior_seq): # 行为序列编码(假设behavior_seq是item_id序列) seq_emb = self.user_emb(behavior_seq) + self.pos_emb(torch.arange(len(behavior_seq))) encoded = self.transformer(seq_emb.unsqueeze(1)).squeeze(1) # [seq_len, dim] user_vec = encoded.mean(dim=0) # 序列聚合 # 预测k值 k_logits = self.k_predictor(user_vec) k_prob = F.softmax(k_logits, dim=-1) k_pred = torch.argmax(k_prob).item() + 1 # k从1开始 # 生成k个原型 prototypes = [] for i in range(k_pred): proto = self.prototype_mlps[i](user_vec) prototypes.append(F.normalize(proto, p=2, dim=0)) # L2归一化 return torch.stack(prototypes), k_pred关键细节说明:
- 位置编码的取舍:我们测试过绝对位置编码和相对位置编码,最终选择绝对编码。原因在于用户行为序列长度波动大(新用户可能只有3次点击,老用户超200次),相对编码在短序列上易失效,而绝对编码通过
nn.Embedding实现,内存开销可控。 - k预测的稳定性技巧:k_logits输出后不直接argmax,而是用Gumbel-Softmax采样,避免训练时梯度中断。上线时才用argmax确定k值,确保推理确定性。
- 原型归一化的必要性:ANN检索依赖向量夹角余弦相似度,未归一化的向量模长差异会导致距离计算失真。我们强制L2归一化,使所有原型向量落在单位球面上,提升ANN精度。
3.2 集合感知ANN索引构建
传统ANN索引(如FAISS的IVF)对单向量查询优化极致,但对向量集合查询无感知。SetMIR的改造核心在于索引构建阶段注入集合语义:
聚类中心增强:在IVF的聚类阶段,不只计算每个簇的中心向量cᵢ,还额外计算该簇内所有向量的兴趣多样性指标:
- 计算簇内向量两两余弦相似度矩阵S
- 取S的平均值作为“簇内凝聚度”γᵢ
- 取S的标准差作为“簇内离散度”δᵢ
- 将(cᵢ, γᵢ, δᵢ)共同存入倒排链元数据
查询路由优化:当收到集合查询{v₁...vₖ}时,ANN服务先计算每个vⱼ到各簇中心cᵢ的距离dⱼᵢ,然后用加权距离函数筛选候选簇:
scoreᵢ = Σⱼ[exp(-dⱼᵢ²/τ) * (1 - γᵢ) * δᵢ]其中τ是温度系数(实测取0.5最佳),
(1-γᵢ)倾向选择凝聚度低的簇(兴趣更分散),δᵢ倾向选择离散度高的簇(覆盖更多兴趣变体)。这步将原本的“最近中心”筛选,升级为“最适配兴趣集合”的智能路由。候选集精排:在选定的Top-M簇内,不简单取每个vⱼ的Top-K邻居,而是用集合Jaccard相似度重排:
- 对每个候选item向量u,计算其与集合{v₁...vₖ}的匹配度:
match(u) = |{j: cos(u,vⱼ)>θ}| / k - θ是阈值(实验定为0.7),|·|表示满足条件的j的数量
- 最终按match(u)降序返回Top-N
- 对每个候选item向量u,计算其与集合{v₁...vₖ}的匹配度:
我们用10亿条商品向量在8卡V100集群上实测,索引构建耗时增加12%,但查询P95延迟仅上升1.8ms,而召回相关性(人工评估)提升23%。
3.3 在线服务架构:如何让33%查询削减真正落地
标题中的“33%查询削减”不是理论值,而是线上服务的实打实收益。这依赖于三层架构协同:
Client层(APP/SDK):集成PrototypeGenerator轻量版(参数量<5MB),用户每次刷新Feed时本地生成原型集合,缓存10分钟。避免每次请求都调用服务端模型,减少RTT。缓存策略采用LRU+时间双淘汰,实测缓存命中率89%。
Orchestrator层(网关):接收Client传来的原型集合,做合法性校验(如向量维度、k值范围),然后封装为ANN查询请求。关键优化是批量合并:同一秒内来自不同用户的查询,若原型集合相似度>0.6(用MinHash快速估算),则合并为单次ANN请求,共享候选集后再分发。这步让QPS降低31%,是33%削减的主要来源。
ANN Service层:部署改造后的FAISS集群,支持集合查询协议。每个实例配置GPU加速(CUDA 11.2 + cuBLAS),查询请求走RDMA网络直连,规避TCP栈开销。监控项新增“集合匹配率”(match(u)≥0.5的item占比),低于75%自动触发索引重建。
注意:33%削减是端到端效果。Client缓存贡献18%,Orchestrator合并贡献15%,ANN服务优化贡献0%(它处理的是更少但更复杂的请求)。很多团队只盯着ANN层优化,却忽略了前端和网关的协同价值。
4. 实操避坑指南:那些论文里不会写的血泪教训
4.1 兴趣原型坍缩:为什么你的v₁和v₂长得一模一样?
这是初期复现SetMIR时90%团队踩的第一个坑。现象:训练后所有原型向量cosine相似度>0.95,模型退化为单兴趣。根因在于梯度冲突:多个MLP共享同一份用户表征,反向传播时梯度相互干扰。我们的解决方案是:
- 梯度隔离层:在用户表征后插入一个可学习的“兴趣门控矩阵”G∈ℝ^(k×d),其中d是表征维度。每个原型MLP的输入变为
G[j] * user_vec,G的每一行独立更新。实测后相似度降至0.42。 - 正交约束损失:在训练Loss中加入
λ * Σᵢ≠ⱼ|vᵢ·vⱼ|,λ取0.01。注意不是强制正交(会破坏语义),而是惩罚高相似度。 - 原型初始化技巧:不用随机初始化,而是用K-means对用户行为聚类,取前k个聚类中心作为初始vᵢ。我们用10万用户样本聚类,收敛速度提升3倍。
4.2 ANN召回偏差:为什么“露营”原型总召回“登山鞋”,却漏掉“帐篷”?
问题本质是向量空间语义漂移。在百万级商品库中,“帐篷”的向量可能更靠近“户外家具”(因材质描述相似),而非“露营”。解决方案分两步:
- 领域自适应微调:用业务标注的“兴趣-物品”正样本对(如用户点击“露营”后30分钟内购买的物品),在ANN索引向量上做对比学习微调。损失函数用NT-Xent,batch size=512,微调后“露营→帐篷”的召回率从62%升至89%。
- 后处理重排序:对ANN返回的Top-100,用轻量级规则过滤:若物品类目ID在用户历史点击类目TOP3内,则提权。这条规则增加0.3ms延迟,但CVR提升0.8个百分点。
4.3 动态k值的线上抖动:为什么用户k值在1和5之间疯狂跳变?
k预测器对噪声敏感,尤其新用户行为少时。我们观察到某金融App用户k值标准差达2.1,导致服务端资源分配失衡。对策是:
- k值平滑:客户端不直接上报预测k,而是维护一个滑动窗口(大小5),取窗口内k值的众数。窗口满后,若新k与当前值差异>1,则渐进调整(如k从1→3,分3次每次+0.5)。
- 服务端兜底:Orchestrator层设置k_min=2, k_max=4,超出范围自动裁剪。实测99.2%请求在范围内,无需兜底。
4.4 监控盲区:别只看CVR,这三个指标才是命脉
上线后我们发现CVR提升3.31%,但广告收入只涨1.2%。深挖发现:
- 兴趣覆盖率:定义为“用户有≥1个原型被ANN成功召回”的比例。初期仅76%,大量用户因原型质量差被漏召。我们加入“原型置信度”阈值(cosine相似度>0.6才参与召回),覆盖率升至94%。
- 集合多样性:计算用户k个原型的平均pairwise cosine相似度。健康值应在0.3~0.6。低于0.3说明兴趣太分散(如v₁=“游戏”,v₂=“育儿”,v₃=“股票”),需检查行为序列清洗逻辑;高于0.6说明解耦失败。
- ANN P99延迟:集合查询比单向量查询更耗时,P99必须<25ms。我们用eBPF追踪发现,GPU显存碎片化导致延迟尖峰,改用CUDA Memory Pool后P99稳定在18ms。
5. 场景延伸与工程取舍:不同业务规模下的落地策略
5.1 中小团队:用“伪SetMIR”快速见效
没有GPU集群和ANN工程师?别放弃。我们帮一家百万DAU的教育App实现了87%的等效效果:
- 原型生成:用预训练的Sentence-BERT对用户最近10条搜索词/课程名做向量化,K-means聚类得3个中心作为原型。零训练成本。
- “集合检索”模拟:不改ANN,而是用三次单向量查询(v₁,v₂,v₃),但结果去重后按“匹配原型数”加权排序。例如item匹配2个原型,权重=2;匹配1个,权重=1。
- 收益:查询量减28%(三次查询合并为一次HTTP请求),CVR+2.1%。成本:3人日开发,0新增服务器。
5.2 大厂级演进:从SetMIR到Interest Graph
Snap的SetMIR是起点,不是终点。我们正在推进的Interest Graph方向,把用户兴趣集合升级为动态图结构:
- 节点:每个原型vᵢ是一个节点
- 边:vᵢ到vⱼ的边权重=用户行为中vᵢ和vⱼ共现频率(如“露营”后24小时内点击“登山”的次数)
- 图神经网络:用GraphSAGE聚合邻居,生成vᵢ的增强向量。实测在跨域推荐(如从“健身”兴趣推“营养餐”)上AUC提升5.7%
最后分享个实战技巧:上线前务必做“兴趣可解释性测试”。随机抽100个用户,人工检查其原型v₁是否对应其最近高频行为(如v₁=“Python教程”对应用户刚学完《流畅的Python》)。如果准确率<80%,说明原型生成器没训好,别急着上ANN——再好的检索也救不了错误的输入。