推送通知在任何产品设计中都承载着双刃剑。做得好,这些通知将用户与他们关心的内容重新连接。做得不好,它们变成噪音,导致用户完全静音或卸载应用。取得正确平衡需要一个精确且可扩展的系统,理解什么对每个用户重要以及何时打扰他们是合理的。
Reddit 的通知推荐系统在大规模下处理这个问题。
它每天评估数百万新帖子,并决定哪些应该作为个性化通知发送给数千万用户。每个决定背后是一个结合因果建模、实时检索、深度学习和产品驱动重新排序的管道。
在本文中,我们了解这个管道如何工作。它遍历管道的关键组件(预算、检索、排序和重新排序),并突出每个阶段的权衡。
系统的一些关键特征如下:
- 它使用接近实时的管道操作
- 由异步工作器和队列驱动
- 与 Reddit 的其他 ML 和排序系统共享核心组件
- 旨在实现低延迟和高新鲜度的推荐
系统已经显著演变,但核心目标保持不变:交付及时、相关的通知,推动参与而不过度压倒用户。
通知管道
通知管道每天处理数百万帖子,决定哪些作为推送通知发送。
它结构化为一系列专注的阶段,每个负责缩小和细化候选集。
- 预算:设置每天向每个用户发送多少通知的每日限制,平衡参与和疲劳。
- 检索:使用快速、轻量级方法拉取可能感兴趣帖子的短名单。
- 排序:使用在用户交互(如点击、赞成和评论)上训练的深度学习模型对这些候选评分。
- 重新排序:根据业务目标调整最终顺序——提升某些内容类型或执行多样性。
管道在基于队列的异步基础设施上运行,以确保及时交付,即使在大规模下。
预算
系统每天做出的第一个决定是用户应该接收多少通知。
通知疲劳不仅仅是用户体验的麻烦,而是永久性的覆盖损失。一旦用户禁用通知,就很少有路径回来。系统将此视为高成本失败。目标是最大化参与而不使事情变得恼人。
不是每个用户都得到相同的对待。预算器估计每个额外通知如何影响用户行为。推送太用力,用户禁用通知或流失。保留太多,系统错过重新参与他们的机会。两者之间的平衡使用因果推断和自适应评分建模。
系统使用因果建模方法来权衡结果:
- 正面结果包括增加活动,如点击通知、浏览应用和与内容交互。
- 负面结果包括流失迹象(使用长间隔)或完全禁用推送通知。
通常,基本相关性(例如,“这个用户收到 5 个通知并保持活跃”)不揭示全貌。相反,系统使用过去用户行为来估计不同通知量如何影响结果,如保持活跃与流失。这种方法,称为因果建模,帮助避免过度拟合噪声参与数据。它不只是看发生了什么,而是尝试估计在不同条件下会发生什么。
为此,团队通过有意在不同用户组之间变化通知量来构建无偏数据集。这些变化用于估计治疗效果以及不同预算如何影响长期参与模式。
在每天开始时,多模型集合估计用户的几个候选预算。每个模型在不同条件下模拟结果:一些更保守,一些更激进。
系统然后选择优化最终参与分数的预算。该分数反映预期收益(点击、会话)和预期风险(禁用、流失)。如果模型表明额外通知会产生有意义的价值,预算会增加到那一点。如果不是,系统保持底线。结果是动态的、每用户的推送策略,反映实际行为数据。
检索
在通知可以排序或发送之前,系统需要一个值得考虑的候选帖子短名单。这是检索阶段的工作。
这个阶段扫描 Reddit 的每日内容 firehose,并将其缩小到可能感兴趣特定用户的几百个帖子。
这个步骤必须快速高效。用重型 ML 模型对每个新帖子排序在理论上是理想的,但实际上,计算上不可能。Reddit 每天看到数百万帖子,通知生成的延迟预算紧张。
为保持在这些约束内,系统依赖规则和基于模型的检索方法的混合。这些技术设计为轻量级,提供高召回率而不进行深度计算。目标是撒大网并保持有希望的候选而不过载管道。
用户兴趣的最简单信号是 subreddit 订阅。如果有人订阅”r/AskHistorians”和”r/MechanicalKeyboards”,他们很可能想听到来自这些社区的新帖子。
这个方法如何工作:
- 列出用户的订阅 subreddit
- 应用 subreddit 级别的过滤器,如移除不属于通知的 NSFW 社区
- 基于参与或交互新鲜度选择前 X 个 subreddit
- 对于每个 subreddit,使用混合分数(混合赞成、反对和帖子年龄)从过去几天选择前 Y 个帖子
- 过滤掉用户已经看过的帖子
- 使用轮询选择从多个 subreddit 拉取,确保一个活跃社区不主导列表
这个基于规则的方法快速且透明。但它也有限。订阅不捕获每个用户不断演变的兴趣,并非 subreddit 中的所有帖子都同样相关。
为超越简单启发式,系统使用双塔模型:大规模推荐系统中的标准技术。一个塔学习表示用户,而另一个学习表示帖子。两者输出嵌入(固定长度向量),捕获用户和内容特征。
模型使用历史 PN 点击数据训练。如果用户过去点击过帖子,模型学习在嵌入空间中将该用户和帖子放得更近。
在运行时,过程看起来像这样:
- 帖子嵌入预计算和索引。这在通知生成期间节省时间,因为帖子编码昂贵。
- 用户嵌入使用其最近行为和元数据实时计算。
- 系统在用户向量和预计算帖子向量之间执行最近邻搜索,找到最接近的匹配。这些是模型认为用户最可能点击的帖子。
在这个步骤之后,候选列表经过最后一轮过滤以移除陈旧或已经查看的内容。结果是高度个性化的帖子集,既新鲜又计算便宜地获得。
排序
一旦检索阶段返回一组可能感兴趣的帖子,排序模型介入决定哪些值得作为推送通知发送。
这是管道中最计算重的部分,预测用户与每个候选交互的可能性。
排序系统使用深度神经网络(DNN),接收数百个信号并输出用户参与预测可能性。但参与意味着不同的事情:点击通知、赞成帖子或留下评论。
为处理这个,模型使用称为多任务学习(MTL)的结构。在 MTL 中,模型不只为单个结果优化。它学习同时预测多个行为:
- P(click):用户点击通知的概率
- P(upvote):用户赞成帖子的概率
- P(comment):用户留下评论的概率
这些预测使用加权公式输入最终分数:
最终分数 = Wclick * P(click) + Wupvote * P(upvote) + ... + Wdownvote * P(downvote)权重(Wclick、Wupvote 等)让团队调整模型的优先级。例如,一些用户可能更关心讨论重的帖子(评论),而其他人更可能与高质量内容(赞成)交互。调整这些权重允许系统基于对参与最重要的内容引导输出。
神经网络结构为两部分:
- 共享层在开始处理所有输入特征(用户档案、帖子元数据、一天中的时间等)为公共表示。
- 任务特定头在末尾专门化预测每个目标(点击、评论、赞成等)。
这个架构允许模型在跨行为泛化良好,同时仍然优化每个交互类型的细微差别。
大规模 ML 系统中最微妙的挑战之一是训练 - 服务偏斜。这是训练期间数据看起来如何与模型在生产中看到什么之间的不匹配。
Reddit 通过使用预测日志处理这个:系统记录所有在模型服务通知时传入模型的特征,以及实际结果(点击、忽略等)。
这个方法带来几个好处:
- 准确的训练反馈,基于真实服务条件
- 更快迭代新功能。无需等待数周收集数据
- 改进可观察性到模型如何在生产中行为
重新排序
即使排序模型已经对每个帖子(候选)评分,工作还没有完全完成。最高排名结果可能在统计上相关,但这不总是意味着它是发送的正确选择。
这就是重新排序介入的地方。它是基于产品策略、UX 目标和业务逻辑调整排名列表的最终层。
机器学习模型优化历史模式。然而,产品目标通常比模型重新训练演变更快。例如,模型可能始终从一个非常活跃的 subreddit 表面高参与帖子,因为那是用户上周点击的。但每天发送类似通知创造疲劳或使系统感觉一维。
这是重新排序帮助的地方。将重新排序视为系统在原始模型输出之上应用编辑判断的方式。它不完全覆盖模型,但它以反映 Reddit 想要用户体验看起来和感觉如何的方式推动最终结果。
这个阶段使用的一些策略如下:
- 提升订阅内容超过通用帖子。即使非订阅帖子评分稍高,系统通常 favor 植根于用户选择社区的个性化内容。
- 执行多样性。防止来自同一 subreddit 或类似主题的多个帖子拥挤体验。这避免重复并保持推荐感觉新鲜。
- 个性化内容类型强调。如果用户有参与讨论线程的历史,系统可以提升有活跃评论部分的帖子——即使那些帖子最初不是最高排名。
团队也在实验动态权重调整,其中提升和优先级实时适应。这些调整从 UX 研究和行为建模汲取。例如:
- 如果用户很少赞成但经常评论,提升喜欢重的帖子和对话启动器可能是好主意。
- 如果用户正在探索其通常订阅之外的新主题,稍微提升非订阅但语义相关的帖子。
这个动态重新排序方法允许系统响应意图,而不仅仅是历史,并改进相关性。
本文为学习目的的个人翻译,译文仅供参考。
原文链接:How Reddit Delivers Notifications to Tens of Millions of Users。
版权归原作者或原刊登方所有。本文为非官方译本;如有不妥,请联系删除。