返回资讯中心
外部精选
软件工程
#开发者实时流

TimeScore 算法:让点赞随时间衰减,按当前热度排序

来源标题:TimeScore Weighted Scoring Algorithm

TimeScore 提出用时间衰减来修正按点赞数排序的缺陷:每个赞随时间按半衰期折半计分,条目得分为衰减后点赞之和,因此榜单反映的是近期热度。演示中时钟以 3600 倍速运行、半衰期为 8 小时,初始点赞最多的 Red 因点赞已过两天而被近期获赞的 Blue 和 Green 超越。每个条目的得分只是一个数字,随时间推移无需更新。

Hacker News 最新mckoss2026/11/4 · 5 天前30 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

每个展示“热门”内容列表(故事、链接、帖子、商品、标签)的网站都必须定义什么叫热门。最简单的答案就是计数:点赞、投票、点击或分享最多的条目排在顶部。但计数只会增长。上个月积累了一千个赞的条目仍然领先于过去一小时被五十人点赞的条目,而按计数排名的列表会被昨天的新闻填满。读者通常想要的是当下热门的内容。

TimeScore 通过让每个赞随时间衰减来解决这个问题。一个赞在发生时值一分,经过一个半衰期后值半分,经过两个半衰期后值四分之一分,以此类推。一个条目的分数是其所有已衰减赞值的总和,因此列表顶部就是最近一直获得关注的内容。而且每个条目的分数都保存为单个数字,随着时间的流逝永远不需要更新。

这里有三个帖子,以及一个以真实时间 3600 倍运行的时钟(一秒等于一小时)。赞以八小时的半衰期衰减。时钟启动时,Red 的赞最多,但它们已经是两天前的了。Blue 的赞是在过去半天里获得的,而 Green 刚刚在过去两小时里获得了三个赞。

点击帖子下方的“赞”,看它的曲线跳升。停止点击后,它每八小时下降一半。开始时,Red 的赞最多,但它的排名却垫底。

上面的帖子是虚构的。下面是 TimeScore 在真实活动上的表现:Data Science Stack Exchange 上从该网站 2014 年 5 月创立到 2024 年 3 月、每周一次、带有八种工具和主题之一标签的每个问题。每周带有某个标签的问题计为该标签的一个事件,价值等于问题数量。

左侧的标签按迄今为止的总问题数排名,也就是普通计数会给出的排名方式。右侧则按 TimeScore 排名。它每秒运行十周;拖动滑块可以自己随时间移动。总数只会增长,所以一旦 Keras 在 2019 年取得领先,它就再也不会让出位置,而早年的领先者 R 在 2024 年仍然排在第三。TimeScore 则讲述了一个快速发展的领域的故事:R 和数据挖掘起初领先,Keras 和 TensorFlow 在 2019 年接管,到 2024 年 PyTorch 和 transformers 位居榜首。选择一年的半衰期可以看到同样的故事以更慢的速度展开,或者选择一个月来看它对每个繁忙周的反应。

假设一个条目在时间 ttt 获得事件(这里的赞),每个事件的值为 sts_tst​(赞为 1)。半衰期为 hhh 时,它在时间 TTT 的 TimeScore 是这些值的总和,每个值自发生以来每经过一个半衰期就减半:

问题在于,即使什么都没发生,这个值每时每刻都在变化。要维护一个按它排序的列表,你必须在时钟推进时重新计算每个条目的分数,或者至少每次显示列表时重新计算。

但每个条目的分数都以相同的速率衰减。在事件之间,所有分数以相同的因子缩小,所以它们的顺序永远不会改变。只有新事件才能使一个条目在列表中上升。

因此 TimeScore 保留第二个完全不衰减的分数。它不是缩小旧事件,而是放大新事件:每个事件的值乘以 2t/h2^{t/h}2t/h,其中 ttt 从某个固定起始时间(t=0t = 0t=0)算起。晚一个半衰期的事件计为两倍,晚两个半衰期计为四倍,以此类推。时间不变的 TimeScore 就是这些值的总和:

下面是它与 TimeScore 的关系。从 TimeScore 的定义出发,每一步都重写它,直到 TSI\mathrm{TSI}TSI 出现:

所以 TSIT=TST⋅2T/h\mathrm{TSI}_T = \mathrm{TS}_T \cdot 2^{T/h}TSIT​=TST​⋅2T/h:TimeScore 乘以一个在任何时刻对每个条目都相同的因子。这意味着它按相同的顺序排列条目。而且由于它的定义中没有任何东西依赖于当前时间 TTT,它只在添加事件时变化。在时间 ttt 添加一个值为 sss 的事件,只是向总和中多加一项:

有一个问题。TSI\mathrm{TSI}TSI 每个半衰期翻倍。半衰期为八小时时,它在不到一年内就会超过 double 能容纳的最大数(210242^{1024}21024)。所以我改为存储它的对数,它每个半衰期只增长 ln⁡2≈0.69\ln 2 \approx 0.69ln2≈0.69:

没有事件的条目 TSI=0\mathrm{TSI} = 0TSI=0,所以它从 L=−∞L = -\inftyL=−∞ 开始。(任何对数底都可以:改变底数只是把每个条目的 LLL 乘以同一个常数,不会改变它们的顺序。自然对数使代码最简单。)

更新 LLL 意味着对上面的更新取对数。但按字面计算是行不通的:它需要 TSI=eL\mathrm{TSI} = e^{L}TSI=eL,正是那个会溢出的数。需要的是一种把两个以对数形式存储的数相加、而永远不必计算这些数本身的方法:

假设两个数以其对数 xxx 和 yyy 保存,那么这两个数本身是 exe^{x}ex 和 eye^{y}ey。它们之和的对数是 ln⁡ ⁣(ex+ey)\ln!\left(e^{x} + e^{y}\right)ln(ex+ey)。这样计算时,exe^{x}ex 和 eye^{y}ey 可能远远超出 double 的范围(太大或太小),但答案本身是一个适中的数。为了安全地得到它,设 xxx 为两者中较大的那个(如果不是就交换),并从和中提取 exe^{x}ex:

由于 xxx 较大,y−xy - xy−x 永远不会为正,所以 e y−xe^{,y - x}ey−x 在 0 和 1 之间。不会溢出,答案是较大的对数加上一个介于 0 和 ln⁡2\ln 2ln2 之间的修正:

在不假设哪个更大的情况下,同样的公式写作

这就是数值计算中广泛使用的“log-sum-exp”技巧。有了它,更新需要四个步骤。前三步把和中的两项都写成 eee 的幂,最后一步使用侧栏的公式:

这里 aaa 是新事件单独具有的对数分数。所以添加一个事件就是取条目的对数分数和事件的对数分数中的较大者,再加上一个介于 0 和 ln⁡2\ln 2ln2 之间的修正。对一个已经点赞过的条目再点一个赞

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →