离线评估指标(一):Precision@K、Recall@K 的定义、计算方式与适用场景
聊召回评估,我习惯先从两个最基础的指标入手——Precision@K 和 Recall@K。
说白了,这两个指标就是回答两个问题:
- 你推荐给用户的 K 个东西里,有多少是用户真正喜欢的?
- 用户所有喜欢的东西里,你成功捞出来了多少?
嗯,听起来很简单对吧?但实际落地的时候,坑比想象中多。我刚开始做召回时,就吃过「只看 Precision 不看 Recall」的亏,结果线上指标涨了,用户满意度反而掉了……
咱们今天就把这两个指标掰开揉碎,讲清楚。
1. 先搞懂几个基本概念
在讲公式之前,有几个术语得先对齐。不然你后面看代码会懵。
| 术语 | 含义 | 我习惯怎么记 |
|---|---|---|
| Ground Truth | 用户真实喜欢的物品集合 | 「用户心里那杆秤」 |
| 推荐列表 | 模型返回的 Top-K 个物品 | 「你递给用户的菜单」 |
| 命中 | 推荐列表 ∩ Ground Truth | 「猜中了」 |
| K | 截断位置,比如 Top-10、Top-20 | 「你给用户看几个?」 |
2. Precision@K:你给的,准不准?
定义
Precision@K 衡量的是:在模型返回的 Top-K 个物品中,有多少是用户真正感兴趣的。
公式长这样:
Precision@K = (推荐列表 ∩ Ground Truth) 中的物品数 / K
举个例子:
- 用户真实喜欢的物品:{A, B, C, D, E}
- 模型返回 Top-5:{A, B, F, G, H}
- 命中:{A, B} → 2 个
- Precision@5 = 2 / 5 = 0.4
说白了,你给了用户 5 个,只有 2 个是他想要的。剩下 3 个是噪音。
适用场景
我个人觉得,Precision@K 最适合的场景是:用户容忍度低、翻看成本高的地方。
- 搜索推荐:用户搜「机械键盘」,你前几个必须是机械键盘。如果混进去一个鼠标垫,用户可能直接走了。
- Push 推送:你给用户推 3 条消息,如果 3 条都不感兴趣,用户可能直接关通知。
- 广告召回:广告位就那么多,每个位置都要精准。
3. Recall@K:你捞得,全不全?
定义
Recall@K 衡量的是:用户所有感兴趣的物品中,模型成功捞出来了多少。
公式:
Recall@K = (推荐列表 ∩ Ground Truth) 中的物品数 / Ground Truth 总数
还是刚才的例子:
- 用户真实喜欢的物品:{A, B, C, D, E} → 5 个
- 模型返回 Top-5:{A, B, F, G, H}
- 命中:{A, B} → 2 个
- Recall@5 = 2 / 5 = 0.4
你看,同样的命中数,Precision 和 Recall 的分母不一样。Precision 看的是「你给的里面有多少对的」,Recall 看的是「对的里面你捞了多少」。
适用场景
Recall@K 更适合用户愿意多翻、内容池大的场景:
- 视频推荐:用户刷抖音,刷 100 条才找到 3 条喜欢的,但他愿意刷。这时候你更关心「他喜欢的有没有被捞出来」。
- 商品库召回:用户想买一件「蓝色、纯棉、圆领」的 T 恤,你召回阶段漏掉了,后面排序再怎么调也救不回来。
- 长尾内容分发:新作者的内容没人看,但内容本身不错。Recall 能帮你衡量「这些冷门好内容有没有被捞到」。
4. 一张图看懂 Precision 和 Recall 的关系
我画了一张图,帮你直观理解这两个指标到底在衡量什么。
你看这张图就明白了:
- Precision 关心的是「红色圈里有多少绿色」——你给的列表里,命中占比高不高。
- Recall 关心的是「蓝色圈里有多少绿色」——用户喜欢的,你捞出来多少。
5. 代码实现:Python 版
理论讲完了,咱们直接上代码。这是我项目里常用的实现方式:
def precision_at_k(recommended, ground_truth, k):
"""
计算 Precision@K
Args:
recommended: list, 模型返回的推荐列表(已排序)
ground_truth: set, 用户真实喜欢的物品集合
k: int, 截断位置
Returns:
float, Precision@K 值
"""
if k <= 0:
return 0.0
# 截断推荐列表到 Top-K
top_k = recommended[:k]
# 计算命中数
hits = len([item for item in top_k if item in ground_truth])
return hits / k
def recall_at_k(recommended, ground_truth, k):
"""
计算 Recall@K
Args:
recommended: list, 模型返回的推荐列表(已排序)
ground_truth: set, 用户真实喜欢的物品集合
k: int, 截断位置
Returns:
float, Recall@K 值
"""
if not ground_truth:
return 0.0
top_k = recommended[:k]
hits = len([item for item in top_k if item in ground_truth])
return hits / len(ground_truth)
# 使用示例
recommended = ['A', 'B', 'F', 'G', 'H', 'C', 'D', 'E']
ground_truth = {'A', 'B', 'C', 'D', 'E'}
print(f"Precision@5 = {precision_at_k(recommended, ground_truth, 5):.3f}")
print(f"Recall@5 = {recall_at_k(recommended, ground_truth, 5):.3f}")
print(f"Precision@10 = {precision_at_k(recommended, ground_truth, 10):.3f}")
print(f"Recall@10 = {recall_at_k(recommended, ground_truth, 10):.3f}")
输出结果:
Precision@5 = 0.400
Recall@5 = 0.400
Precision@10 = 0.500
Recall@10 = 1.000
6. 实际项目中的坑与经验
最后,分享几个我踩过的坑:
-
Ground Truth 怎么定义?
我曾经直接用「用户点击」作为 Ground Truth,结果 Precision 虚高。为什么?因为用户点不点,还取决于排序和展示位置。后来我改用「用户点击 + 停留时长 > 30s」作为正反馈,指标才靠谱。
-
K 的取值影响巨大。
同样的模型,P@5=0.8,P@20=0.3。你说模型好不好?其实两个都对,只是视角不同。我建议你根据业务场景定 K,然后固定下来做对比实验。
-
不要只看均值。
我见过一个项目,整体 Precision@10=0.6,但拆开看,头部用户 P@10=0.9,尾部用户 P@10=0.1。均值掩盖了问题。所以,我习惯按用户活跃度分桶看指标。
-
Precision 和 Recall 是跷跷板。
你调高 Precision,Recall 大概率会降。反过来也一样。没有「最好」的模型,只有「最适合业务」的模型。我一般会跟产品经理对齐一个「可接受的最低 Recall」,然后在这个约束下最大化 Precision。