推荐系统 · 时间衰减公式怎么调参
日期: 2026-07-09 | 作者: sunxin(+ Cursor AI 结对讲解) 关联路线图:
推荐系统建设路线图.md §五 Phase 3+§六 Phase 4预计触发: ADR-019(画像聚合的时间衰减,P3 ✅ 已沉淀)/ ADR-020(热榜打分的时间衰减,P4 待沉淀)
TL;DR
- 推荐系统里到处都用"时间衰减"—— 热榜排序、用户画像、新旧内容平衡、消息 TTL —— 本质是告诉系统"越老的信息越不重要"。
- 业界最常用的公式只有 3 种:Hacker News 幂次衰减
1/(hours+2)^1.5、Reddit 对数衰减、指数衰减e^(-λt)。三者是"不同直觉"的数学化,选哪个要看你想让内容多久"失去竞争力"。 - 本项目选幂次衰减
^1.5:24h 前热度 ≈ 30%、48h 前 ≈ 15%、72h 前 ≈ 8% —— 与"帖子内容的现实半衰期约 1-2 天"匹配。 - 调参核心是"半衰期"直觉:不要盯着公式看,盯着"多久热度掉一半"这个人话指标 —— 反推公式参数。
- 不同场景要不同参数:热榜(快衰减)、画像(慢衰减)、消息触达(分档衰减)—— 一套参数打天下是典型错误。
目录
1. 场景:为什么"时间衰减"是推荐系统的心脏
1.1 一个反面例子
假设你做"热榜",用最朴素的公式:
hot_score = like_count + favorite_count × 2 + comment_count × 3跑一周后你会发现:热榜永远是同几条帖子。原因显而易见 —— 一条 3 天前爆火的帖子累积了 5000 赞,新发的帖子怎么也追不上。
结果:新内容没曝光机会 → 用户觉得"没啥新东西" → 活跃度下降 → 内容生态死循环。
1.2 时间衰减的核心作用
时间衰减做的事:让老内容的分数随时间"自然掉出榜单",给新内容让出位置。
理想效果:
- 新发布的帖子:起步分数低,但随时间累积互动能爬榜
- 爬到榜后:靠新增互动追赶时间衰减的下降
- 一旦互动增速放缓:分数快速下降,被更新的帖子挤下去
- 3 天/7 天后:即使互动数还在缓慢增加,也已经几乎不再上榜
这就是"热度"的正确物理含义:不是"总互动数",而是"近期互动速率"。时间衰减是它的数学表达。
1.3 场景延伸:时间衰减在推荐系统的 4 大用途
| 用途 | 说明 | 衰减速度需求 |
|---|---|---|
| 热榜排序 | 让新内容有机会上榜(本笔记主场景) | 快(半衰期 12-24h) |
| 用户画像 | 用户最近的兴趣比 3 个月前的更重要 | 慢(半衰期 7-14 天) |
| 新老内容平衡 | 推荐流里控制"新内容比例",避免只推老爆款 | 中(半衰期 24-48h) |
| 消息 TTL | 30 天前的通知自动过期 | 阶跃函数(0/1 硬截断) |
关键洞察:同一个项目在不同场景应该用不同的衰减参数,不能一套打天下。
2. 三种主流公式对比
2.1 Hacker News 幂次衰减
score = (P - 1) / (T + 2) ^ α
P: 点赞数 + 1(防止 0 分)
T: 帖子年龄(小时)
α: 衰减指数,Hacker News 用 1.8
+2: 让新帖子(T=0 时)不至于分数无穷大特点:
- 前期衰减快(0-6h 内下降明显)
- 中期趋于平缓(12-48h)
- 尾部慢(一周后仍有微弱余量)
曲线示意(α=1.5, +2, 权重 P=100):
T=0h 分数=100/(2^1.5) ≈ 35.4 (100%)
T=1h 分数=100/(3^1.5) ≈ 19.2 (54%)
T=6h 分数=100/(8^1.5) ≈ 4.4 (12%)
T=12h 分数=100/(14^1.5) ≈ 1.9 (5.4%)
T=24h 分数=100/(26^1.5) ≈ 0.75 (2.1%)
T=48h 分数=100/(50^1.5) ≈ 0.28 (0.8%)为什么加 +2:避免新帖 T=0 时分母 = 0,分数飙到无穷大;+2 相当于"起跑时压 2 小时权重",让新帖不至于一发就霸榜。
2.2 Reddit 对数衰减
score = log10(max(|P - N|, 1)) × sign(P - N) + seconds_since / 45000
P: 顶数
N: 踩数
sign: 顶多则 +1,踩多则 -1
seconds_since: 帖子发布后秒数
45000: ≈ 12.5 小时,用作时间权重特点:
- 顶踩用对数抑制"刷票"(10 万顶 vs 1 万顶只差 log10(10) = 1 分)
- 时间是线性加分而非衰减 —— 每 12.5 小时加 1 分
- 结果:新帖天然有优势(时间分持续累加),老帖必须靠更多顶超越
为什么 Reddit 采用:适合"讨论区"型内容 —— 观点被讨论几天是常态,一个好帖子 3 天后还有价值,用时间线性加分而不是衰减更符合场景。
为什么本项目不选:小红书/朋友圈型内容的"新鲜度"重要性远高于讨论型内容,Reddit 公式会让老爆款帖霸榜。
2.3 指数衰减(牛顿冷却)
score = P × e^(-λ × T)
P: 原始热度(互动加权和)
λ: 衰减率(大 → 掉得快;小 → 掉得慢)
T: 帖子年龄(小时)特点:
- 每单位时间衰减固定比例(严格半衰期)
- 数学最"纯粹"(对应牛顿冷却定律:温度差消散)
- 参数 λ 的直觉:半衰期 = ln(2) / λ ≈ 0.693 / λ
例:λ = 0.05 → 半衰期 ≈ 14 小时
为什么本项目不选:指数衰减尾部消失太快,一周前的老帖分数几乎为 0,无法保留"经典好帖"的长尾曝光机会;幂次衰减在这方面更宽容。
2.4 三者曲线对比
假设新帖 P = 100,画 168 小时(7 天)内的分数曲线:
分数(相对 T=0)
100% ┤●
│ ●
80% ┤ ●● 幂次 α=1.5
│ ●●●
60% ┤ ●●●●
│ ●●●●
40% ┤ ●●●●●●
│ ●●●●●●●●
20% ┤ ●●●●●●●●●●●●●●●
│ ●●●●●●●●●●●●●●
0% ┼──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──▶
0 12 24 36 48 60 72 84 96 108 120 132 144 156 168 小时
指数 λ=0.05:
100% ┤●
│ ●
80% ┤ ●●
│ ●●
60% ┤ ●●
│ ●●●
40% ┤ ●●●
│ ●●●●
20% ┤ ●●●●●
│ ●●●●●●●●
0% ┼──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──▶
(72h 后几乎归零)
Reddit(线性时间加分,无衰减):
⬆持续增长(假设互动数不变,只算时间维度)结论:
- 幂次衰减:曲线软着陆,前期快落后期慢,符合"新鲜感"直觉
- 指数衰减:曲线硬着陆,短时间热点后完全冷却,适合"极速新闻"
- Reddit 线性:反向鼓励新内容,适合讨论型
社交内容平台几乎都选幂次衰减(HN / Reddit-except-Reddit / 微博 / Instagram / 抖音等),因为"新鲜感 + 长尾并存"是社交内容的典型形态。
3. 幂次衰减 1/(t+c)^α 的调参方法
3.1 三个参数各干嘛
score = interaction_weight_sum / (T + c) ^ α| 参数 | 作用 | 常见取值 | 直觉 |
|---|---|---|---|
interaction_weight_sum | 互动加权和(分子) | 因项目而异(见 §4) | 越大越"热" |
c | 起步保护常数 | 1-4 | 越大越保护"新帖不至于霸榜" |
α | 衰减指数(分母幂) | 0.8-2.0 | 核心调参 —— 越大衰减越快 |
3.2 用"半衰期"反推 α
半衰期:一个分数掉到原始 50% 所需的时间。
对幂次衰减 1/(t+c)^α:
掉到 50% ⇔ 1/(t_half + c)^α = 0.5 × 1/c^α
⇔ (t_half + c)^α = 2 × c^α
⇔ t_half = c × (2^(1/α) - 1)c=2 时的半衰期表:
| α | 半衰期(小时) | 24h 剩余 | 72h 剩余 |
|---|---|---|---|
| 0.8 | 3.7 h | 18% | 8.6% |
| 1.0 | 2.0 h | 7.7% | 2.7% |
| 1.5 ← 本项目 | 1.2 h | 2.1% | 0.4% |
| 1.8 (HN) | 1.0 h | 1.0% | 0.15% |
| 2.0 | 0.83 h | 0.66% | 0.075% |
注意:这里"半衰期"是纯衰减因子的半衰期。实际热榜里帖子分数下降更慢,因为分子(互动数)在持续增加。
换个更直观的问法:"我希望一条 24h 前的帖子,除非新增互动比新帖多多少倍,才能保持在同榜位置?"
- α=1.0:24h 前的帖子需要新帖 13 倍的互动才能追上(1/0.077 ≈ 13)
- α=1.5:48 倍
- α=1.8:100 倍
- α=2.0:150 倍
本项目选 α=1.5 的理由:
- 一条 24h 前的帖子如果能吸引 50 倍于新帖的互动(比如爆款),仍值得让它保留 —— α=1.5 满足
- 一条 24h 前的帖子如果没有 50 倍热度,就该给新帖让位 —— α=1.5 会让它掉出榜
- 相比 HN 的 α=1.8(要求 100 倍),α=1.5 对老爆款更宽容,符合"生活分享类"内容的调性(微博/小红书都用 1.4-1.6 之间)
3.3 参数选择流程(面向不同产品阶段)
确定你想要的半衰期(用人话:多久后老内容基本失去竞争力)
- 突发新闻/直播:1-3 小时
- 热榜微博/微信朋友圈:12-24 小时(本项目)
- 长内容/深度好文:1-3 天
- 短视频(抖音/快手):3-6 小时
反推 α(用 §3.2 表格 or 公式)
确定 c(起步保护):
- c=0:新帖直接爆发,可能一发就霸榜
- c=2:新帖起步压 2 小时权重,前 2h 与已有 2h 帖打平
- c=4:更保守,避免"新帖靠僵尸粉一波流"
确定互动权重(下面 §4 项目落地部分详解)
上线一周后 A/B:对比几个 α 值下的"用户平均停留时间"、"人均点赞数"、"新用户次日留存",选最优
3.4 常见错误
- ❌ α 太大(如 3.0 甚至 5.0):几乎没有"热榜"效果,全站几乎只推刚发的帖子
- ❌ α 太小(如 0.3):老爆款永远不掉,热榜死气沉沉
- ❌ c=0:新帖分数无穷大(一定要 c ≥ 1)
- ❌ 忘记规范化:不同类目的互动数量级差异大(美食帖平均 100 赞、政治帖 3 赞),必须按类目归一化再打分
4. 项目里的具体落地
4.1 P4 热榜打分公式(对应路线图 §六 §6.2.2 ③)
hot_score = (
like_count × 2 +
favorite_count × 3 +
comment_count × 2 +
view_count × 0.5
) / (hours_since_publish + 2) ^ 1.5关键设计:
- c = 2:新帖起步压 2 小时权重
- α = 1.5:24h 前的帖子需要 ~50 倍互动才能保持同榜位置
- 互动权重:
fav > comment ≈ like > view- fav 是"愿意保存",最强意图 → 3
- comment、like 是"表态",中等意图 → 2
- view 是"看过",弱意图 → 0.5
- 不加 share(分享带社交性但主要看用户圈子而非内容质量)
Redis ZSet 存储:
Key: hot_posts:global
Type: ZSet
Score: hot_score (float)
Member: postId
刷新方式:HotPostRankingRefreshTask 每 5 min 全量重算 + AFTER_COMMIT 事件驱动增量为什么 c=2 而不是 HN 的 c=2:跟 HN 一样,因为经验值证明这个值最平衡(c=1 太激进、c=4 太保守)。
为什么 α=1.5 而不是 HN 的 1.8:本项目定位"生活分享 + 关注流",不是"新闻聚合",用户对"48 小时前的好内容"仍有兴趣,衰减稍缓一点更合适。
4.2 P3 用户画像的时间衰减(对应路线图 §五 §5.2.2)
score(user, category) = SUM over action in last_30d {
if action.target_category == category:
weight(action.action_type) / (days_since(action.action_time) + 1) ^ 0.5
}关键设计(跟热榜完全不同的参数):
- T 用天,不用小时(画像半衰期以天为单位)
- α = 0.5(比热榜的 1.5 缓和得多,因为兴趣变化速度远慢于热度衰减)
- c = 1(防 T=0 时分母 0)
半衰期表(α=0.5, c=1):
| 天数 T | 权重 | 保留率 |
|---|---|---|
| 0 | 1.0 | 100% |
| 7 | 0.35 | 35% |
| 14 | 0.26 | 26% |
| 30 | 0.18 | 18% |
为什么画像用 α=0.5 而热榜用 α=1.5:兴趣是长期属性,一个月前的兴趣仍有 18% 参考价值;热度是即时属性,一个月前的热点基本无关。同一个数学工具(幂次衰减),不同场景配不同参数。
4.3 消息 TTL(用阶跃函数而非衰减)
expiresAt = createdTime + 30 天
isExpired() = LocalDateTime.now().isAfter(expiresAt)为什么消息不用衰减:消息是"要么显示、要么不显示"的二元态,没有"半可见"这种中间态。硬截断即可。
4.4 参数集中管理建议
Phase 4 完成时建议把所有时间衰减参数收敛到 RecommendConfig 配置类:
@ConfigurationProperties(prefix = "recommend.decay")
public class DecayConfig {
// 热榜
private double hotAlpha = 1.5;
private double hotBase = 2.0;
private Map<String, Double> hotInteractionWeights = Map.of(
"like", 2.0, "favorite", 3.0, "comment", 2.0, "view", 0.5
);
// 画像
private double profileAlpha = 0.5;
private double profileBase = 1.0;
private Map<String, Double> profileInteractionWeights = Map.of(
"view", 1.0, "comment", 2.0, "like", 3.0, "share", 4.0, "favorite", 5.0
);
}好处:
- A/B 调参不改代码(配置中心热更新)
- 未来上机器学习模型时,这些配置项就是模型初始化的基线特征权重
5. 教学收获
5.1 "调参"不是玄学 —— 有明确的推导路径
新手容易觉得"这些参数怎么定的?看起来很玄学"。实际推导流程:
业务目标("我希望老帖多久失去竞争力")
↓
数学化("半衰期 = X 小时")
↓
反推参数(用 §3.2 公式或表格)
↓
上线 A/B(用实际指标验证)
↓
调整(如果发现新帖曝光太多/太少)关键:先想业务目标,再挑数学工具,最后调参数。反过来(先看到别人用了什么参数就抄)容易走偏。
5.2 "同一个工具在不同场景要不同参数" 是通用启示
热榜 α=1.5 vs 画像 α=0.5:同一个数学工具,参数差 3 倍,因为业务本质不同。
延伸案例:
- 缓存 TTL:热数据 30 秒 vs 冷数据 30 分钟(同样是 TTL,差 60 倍)
- 重试间隔:网络抖动 100ms 指数退避 vs 支付回调 5 min 线性重试(同样是重试,策略完全不同)
- 超时时间:内网 RPC 100ms vs 外网 API 5s(同样是超时,差 50 倍)
教训:看到"同一个概念"不要以为"同一个参数"。参数是场景的函数,不是概念的函数。
5.3 数学公式是业务直觉的"翻译"
看到 1/(t+2)^1.5 这个公式,正确的思考不是"1.5 是什么魔法数",而是翻译回业务:
1/...^1.5= 越老越弱(t+2)= 保护新帖不至于起点无穷大1.5= 衰减速度中等(12h 掉一半)
训练自己的能力:看到任何公式,能立刻用一句人话讲清楚它在业务上的含义。做不到就是没真正理解。
6. 附录
6.1 术语表
| 术语 | 说明 |
|---|---|
| 时间衰减 | 让老信息权重随时间自动下降的机制 |
| 半衰期 | 分数掉到 50% 所需的时间 |
| 幂次衰减 | 分母是"时间的幂次"(本项目采用) |
| 指数衰减 | 分母是"e 的时间次方" |
| 起步保护常数 | 幂次公式里的 +c,防止新帖分数爆炸 |
6.2 延伸阅读
- Hacker News Ranking Algorithm 源码:GitHub - HackerNews Firebase API 或搜"Y Combinator news.arc"
- Reddit Ranking Algorithm 分析:搜"Reddit hot ranking algorithm derivation"
- Nathan Marz 讲座《The Idealized Streaming System》—— 深入讲流处理里的时间语义
- 《数学之美》(吴军) 第 8 章 "布尔代数和搜索引擎的索引" —— 相似的"数学工具翻译业务" 的思维训练
6.3 关联决策
- ADR-020(P4 待沉淀):本项目 P4 热榜采用幂次衰减
1/(t+2)^1.5+ 5 类互动加权 - ADR-019(P3 ✅ 已沉淀):本项目 P3 画像采用幂次衰减
1/(d+1)^0.5+ 30 天窗口