RAG学习之 - 检索质量评估指标详解:从概念到代码实战

检索质量评估指标详解:从概念到代码实战

搞懂 Precision@K、MRR、NDCG,看这一篇就够了


写在前面

如果你在做 RAG、搜索引擎或推荐系统,一定被问过这些问题:

  • "你的检索效果怎么样?"
  • "怎么证明你的系统比别人的好?"
  • "优化之后效果提升了多少?"

这时候,检索质量评估指标就派上用场了。

本文带你彻底搞懂 5 个核心指标:Precision@K、Recall@K、AP、MRR、NDCG,每个指标都包含:

  • 📖 概念解释(说人话)
  • 📐 理论公式
  • 📝 计算实例
  • 💻 代码实现

一、核心概念总览

1.1 什么是检索质量评估?

简单说,就是评判你的检索系统返回的结果好不好

复制代码
用户查询:"Python 怎么读取 JSON 文件?"
    ↓
检索系统返回 10 篇文档
    ↓
怎么评价这 10 篇文档的质量?← 评估指标登场

1.2 五个核心指标

指标 全称 关注点 难度
Precision@K 查准率 前 K 个有多准
Recall@K 召回率 覆盖了多少
AP 平均精度 综合精度 ⭐⭐
MRR 平均倒数排名 第一个正确答案排第几 ⭐⭐
NDCG 归一化折损累计增益 整体排序质量 ⭐⭐⭐

1.3 基础概念:相关性与相关性标注

在计算任何指标之前,我们需要知道哪些文档是相关的

复制代码
检索结果:[文档 A, 文档 B, 文档 C, 文档 D, 文档 E]
相关性标注:[✅,    ❌,    ✅,    ✅,    ❌   ]

相关性标注通常由人工完成,或者基于用户行为(点击、停留时间等)推断。


二、Precision@K(查准率)

2.1 概念解释

Precision@K 就是:前 K 个结果里,有多少是相关的?

生活例子:

你去相亲,媒人给你介绍了 5 个对象(K=5)

其中 3 个你觉得合适,2 个不合适

Precision@5 = 3/5 = 0.6

2.2 理论公式

复制代码
Precision@K = 前 K 个结果中相关的文档数 / K

2.3 计算实例

场景:用户搜索 "Python JSON 教程"

排名 文档 是否相关
1 "Python JSON 完全指南"
2 "Java 入门教程"
3 "Python 解析 JSON 实战"
4 "Python 网络爬虫"
5 "JSON 数据格式详解"
复制代码
Precision@5 = 3 / 5 = 0.6

2.4 代码实现

python 复制代码
def precision_at_k(results: list, k: int) -> float:
    """
    计算 Precision@K
  
    Args:
        results: 检索结果相关性列表,[True, False, True, ...]
                 True 表示相关,False 表示不相关
        k: 前 K 个结果
  
    Returns:
        Precision@K 分数
    """
    if len(results) < k:
        # 如果结果不足 K 个,补齐 False
        results = results + [False] * (k - len(results))
  
    return sum(results[:k]) / k


# 使用示例
results = [True, False, True, False, True, False]
print(f"Precision@5 = {precision_at_k(results, 5):.3f}")  # 0.600
print(f"Precision@3 = {precision_at_k(results, 3):.3f}")  # 0.667

2.5 特点总结

优点 缺点
✅ 简单直观,容易解释 ❌ 不考虑排序质量
✅ 计算快速 ❌ 不考虑 K 之后的结果
✅ 不需要知道所有相关文档 ❌ K 的选择有任意性

三、Recall@K(召回率)

2.1 概念解释

Recall@K 就是:所有相关文档中,前 K 个结果里召回了多少?

生活例子:

仓库里有 100 个合格零件(所有相关文档)

你随机抓了 20 个,里面有 15 个合格

Recall@20 = 15/100 = 0.15

2.2 理论公式

复制代码
Recall@K = 前 K 个结果中相关的文档数 / 所有相关文档总数

2.3 计算实例

场景 :知识库中关于 "Python JSON" 共有 20 篇 相关文档

检索结果前 10 个中有 6 篇 相关:

复制代码
Recall@10 = 6 / 20 = 0.3

这意味着:虽然你检出了 6 篇相关文档,但还有 14 篇相关的没找出来。

2.4 代码实现

python 复制代码
def recall_at_k(results: list, total_relevant: int) -> float:
    """
    计算 Recall@K
  
    Args:
        results: 检索结果相关性列表,[True, False, True, ...]
        total_relevant: 所有相关文档的总数
  
    Returns:
        Recall@K 分数
    """
    relevant_in_top_k = sum(results)
  
    if total_relevant == 0:
        return 0
  
    return relevant_in_top_k / total_relevant


# 使用示例
results = [True, False, True, True, False, True]  # 前 6 个中有 4 个相关
total_relevant = 20  # 总共有 20 篇相关文档
print(f"Recall@6 = {recall_at_k(results, total_relevant):.3f}")  # 0.200

2.5 特点总结

优点 缺点
✅ 衡量检索覆盖度 ❌ 需要知道所有相关文档(难获取)
✅ 适合评估检索广度 ❌ 不考虑排序质量
✅ 与 Precision 互补 ❌ 可能鼓励"宁可错杀一千"

四、AP(Average Precision,平均精度)

4.1 概念解释

AP 就是:在每个相关文档的位置,计算 Precision,然后求平均。

生活例子:

钓鱼比赛,你钓上了 5 条鱼(相关文档)

第 1 条鱼:前 1 竿钓上来的,Precision@1 = 1/1 = 1.0

第 2 条鱼:前 3 竿钓上来的,Precision@3 = 2/3 = 0.67

第 3 条鱼:前 5 竿钓上来的,Precision@5 = 3/5 = 0.6

...

AP = 这些 Precision 的平均值

4.2 理论公式

复制代码
AP = (Σ Precision@kᵢ) / 相关文档总数

其中 kᵢ 是第 i 个相关文档的位置

4.3 计算实例

检索结果(10 篇)[✅, ❌, ✅, ❌, ❌, ✅, ❌, ❌, ✅, ❌]

相关文档位置:1, 3, 6, 9(共 4 篇相关)

相关文档 位置 累计相关数 Precision@位置
第 1 篇 1 1 1/1 = 1.0
第 2 篇 3 2 2/3 ≈ 0.67
第 3 篇 6 3 3/6 = 0.5
第 4 篇 9 4 4/9 ≈ 0.44
复制代码
AP = (1.0 + 0.67 + 0.5 + 0.44) / 4 = 0.65

4.4 代码实现

python 复制代码
def average_precision(results: list) -> float:
    """
    计算 AP(Average Precision)
  
    Args:
        results: 检索结果相关性列表,[True, False, True, ...]
  
    Returns:
        AP 分数
    """
    precision_sum = 0
    relevant_count = 0
  
    for i, is_relevant in enumerate(results):
        if is_relevant:
            relevant_count += 1
            # 计算当前位置的 Precision
            precision_at_i = relevant_count / (i + 1)
            precision_sum += precision_at_i
  
    if relevant_count == 0:
        return 0
  
    return precision_sum / relevant_count


# 使用示例
results = [True, False, True, False, False, True, False, False, True, False]
ap = average_precision(results)
print(f"AP = {ap:.3f}")  # 0.652

4.5 特点总结

优点 缺点
✅ 综合考虑精度和召回 ❌ 需要知道所有相关文档
✅ 考虑排序质量(靠前的相关文档贡献更大) ❌ 计算相对复杂
✅ 学术界常用指标 ❌ 对单个查询的评估可能不稳定

五、MAP(Mean Average Precision)

5.1 概念解释

MAP 就是:多个查询的 AP 的平均值。

AP 是单个查询的指标,MAP 是多个查询的综合指标。

5.2 理论公式

复制代码
MAP = (AP₁ + AP₂ + ... + APₙ) / n

5.3 计算实例

查询 AP
查询 1 0.65
查询 2 0.80
查询 3 0.45
查询 4 0.70
复制代码
MAP = (0.65 + 0.80 + 0.45 + 0.70) / 4 = 0.65

5.4 代码实现

python 复制代码
def mean_average_precision(all_results: list) -> float:
    """
    计算 MAP(Mean Average Precision)
  
    Args:
        all_results: 多个查询的结果列表
                    [[True, False, True...], [False, True...], ...]
  
    Returns:
        MAP 分数
    """
    ap_scores = [average_precision(results) for results in all_results]
  
    if not ap_scores:
        return 0
  
    return sum(ap_scores) / len(ap_scores)


# 使用示例
all_results = [
    [True, False, True, False, False, True],   # 查询 1
    [True, True, False, True, False],          # 查询 2
    [False, True, True, False],                # 查询 3
]
map_score = mean_average_precision(all_results)
print(f"MAP = {map_score:.3f}")

六、MRR(Mean Reciprocal Rank,平均倒数排名)

6.1 概念解释

MRR 就是:第一个正确答案排第几名?

生活例子:

考试单选题,选项有 A、B、C、D

正确答案是 B,排在第 2 个

RR = 1/2 = 0.5

如果是多选题,MRR 只看第一个正确选项排第几。

6.2 理论公式

复制代码
对于单个查询:
Reciprocal Rank (RR) = 1 / 第一个正确答案的排名
                      (如果没找到正确答案,RR = 0)

对于多个查询:
MRR = (RR₁ + RR₂ + ... + RRₙ) / n

6.3 计算实例

5 个测试查询:

查询 第一个正确答案排名 RR = 1/排名
查询 1 第 1 名 1/1 = 1.0
查询 2 第 3 名 1/3 ≈ 0.333
查询 3 第 2 名 1/2 = 0.5
查询 4 前 10 名都没有 0
查询 5 第 1 名 1/1 = 1.0
复制代码
MRR = (1.0 + 0.333 + 0.5 + 0 + 1.0) / 5 = 0.566

6.4 代码实现

python 复制代码
def reciprocal_rank(results: list) -> float:
    """
    计算单个查询的 RR(Reciprocal Rank)
    """
    for i, is_relevant in enumerate(results):
        if is_relevant:
            return 1 / (i + 1)
    return 0  # 没找到正确答案


def mean_reciprocal_rank(all_results: list) -> float:
    """
    计算 MRR(Mean Reciprocal Rank)
  
    Args:
        all_results: 多个查询的结果列表
                    [[True, False, True...], [False, True...], ...]
  
    Returns:
        MRR 分数
    """
    rr_scores = [reciprocal_rank(results) for results in all_results]
  
    if not rr_scores:
        return 0
  
    return sum(rr_scores) / len(rr_scores)


# 使用示例
all_results = [
    [True, False, False, False],      # 第 1 名正确,RR=1.0
    [False, False, True, False],      # 第 3 名正确,RR=0.333
    [False, True, True, False],       # 第 2 名正确,RR=0.5
    [False, False, False, False],     # 没正确,RR=0
    [True, False, False, False],      # 第 1 名正确,RR=1.0
]
mrr_score = mean_reciprocal_rank(all_results)
print(f"MRR = {mrr_score:.3f}")  # 0.566

6.5 特点总结

优点 缺点
✅ 计算简单,易于理解 ❌ 只关注第一个正确答案
✅ 适合问答系统(只需一个正确答案) ❌ 忽略多个正确答案的情况
✅ 不需要知道所有相关文档 ❌ 不适合搜索场景(用户会看多条结果)

七、NDCG(归一化折损累计增益)

7.1 概念解释

NDCG 是检索评估的"黄金指标",它同时考虑两件事:

  1. 相关性有等级(0-3 分),不是非黑即白
  2. 排名越靠后,价值越低(折损)

生活例子:

老师批改试卷,不是简单的"对/错",而是:

  • 完全正确:3 分
  • 基本正确:2 分
  • 沾点边:1 分
  • 完全错误:0 分

而且,排在前面的答案更重要!

7.2 为什么需要 NDCG?

MRR 和 Precision 的局限性:

复制代码
场景:用户搜 "Python 教程"(会看前 5-10 条结果)

结果 A: [✅, ❌, ❌, ❌, ❌]  第一个正确在第 1 名
结果 B: [✅, ✅, ✅, ❌, ❌]  第一个正确在第 1 名

用 MRR 评估:两者一样(都只关心第一个)
用 Precision@5 评估:两者一样(3/5 vs 1/5... 不对,这里 Precision 不同)

但用户明显更喜欢结果 B!

NDCG 能区分这种情况,因为它考虑所有结果的相关性。

7.3 核心概念

7.3.1 相关性等级
等级 分数 说明
完全无关 0 完全不相关
有点相关 1 沾点边
相关 2 比较相关
非常相关 3 完美匹配
7.3.2 折损因子

排名越靠后,价值越低:

排名 折损因子 1/log₂(排名 +1) 含义
1 1/log₂(2) = 1.0 100% 价值
2 1/log₂(3) ≈ 0.63 63% 价值
3 1/log₂(4) = 0.5 50% 价值
4 1/log₂(5) ≈ 0.43 43% 价值
5 1/log₂(6) ≈ 0.39 39% 价值

7.4 理论公式

复制代码
CG@K = Σ relᵢ  (累计增益)

DCG@K = Σ (relᵢ / log₂(i + 1))  (折损累计增益)

IDCG@K = 最优排序下的 DCG  (理想折损累计增益)

NDCG@K = DCG@K / IDCG@K  (归一化)

7.5 完整计算实例

检索结果及人工标注:

排名 文档 相关性分数
1 "机器学习完全指南" 3(非常相关)
2 "深度学习教程" 2(相关)
3 "Python 编程基础" 1(有点相关)
4 "机器学习实战项目" 3(非常相关)
5 "Java 入门教程" 0(无关)
步骤 1:计算 DCG@5
复制代码
DCG@5 = 3/1.0 + 2/0.63 + 1/0.5 + 3/0.43 + 0/0.39
      = 3.0 + 3.17 + 2.0 + 6.98 + 0
      = 15.15
步骤 2:计算 IDCG@5

最优排序应该是相关性从高到低:[3, 3, 2, 1, 0]

复制代码
IDCG@5 = 3/1.0 + 3/0.63 + 2/0.5 + 1/0.43 + 0/0.39
       = 3.0 + 4.76 + 4.0 + 2.33 + 0
       = 14.09
步骤 3:计算 NDCG@5
复制代码
NDCG@5 = DCG@5 / IDCG@5
       = 15.15 / 14.09
       ≈ 1.08(>1 说明计算有误,通常 NDCG ≤ 1)

注:实际应用中 NDCG 应在 0-1 之间。上面例子用于演示计算过程,实际数据应保证 IDCG ≥ DCG。

7.6 代码实现

python 复制代码
import numpy as np


def dcg(relevances: list) -> float:
    """
    计算 DCG(Discounted Cumulative Gain)
  
    公式:DCG = Σ (2^rel - 1) / log₂(i + 2)
    """
    dcg_score = 0
    for i, rel in enumerate(relevances):
        dcg_score += (2 ** rel - 1) / np.log2(i + 2)
    return dcg_score


def ndcg(relevances: list) -> float:
    """
    计算 NDCG(Normalized DCG)
  
    Args:
        relevances: 相关性分数列表 [3, 2, 3, 1, 0]
  
    Returns:
        NDCG 分数(0-1 之间)
    """
    # 计算实际 DCG
    dcg_score = dcg(relevances)
  
    # 计算理想 DCG(最优排序:相关性从高到低)
    ideal_relevances = sorted(relevances, reverse=True)
    idcg_score = dcg(ideal_relevances)
  
    if idcg_score == 0:
        return 0
  
    return dcg_score / idcg_score


# 使用示例
relevances = [3, 2, 1, 3, 0]  # 5 个结果的相关性分数
ndcg_score = ndcg(relevances)
print(f"NDCG@5 = {ndcg_score:.3f}")

# 对比两种排序
good_ranking = [3, 3, 2, 1, 0]  # 好的排序:高分在前
bad_ranking = [0, 1, 2, 3, 3]   # 差的排序:高分在后

print(f"好的排序 NDCG = {ndcg(good_ranking):.3f}")  # ≈ 1.0
print(f"差的排序 NDCG = {ndcg(bad_ranking):.3f}")  # < 1.0

7.7 特点总结

优点 缺点
✅ 考虑多个正确答案 ❌ 计算复杂
✅ 考虑相关性等级(不是非黑即白) ❌ 需要人工标注相关性
✅ 考虑排序质量(靠前的更重要) ❌ 对标注一致性敏感
✅ 结果在 0-1 之间,易于比较

八、五大指标对比总结

8.1 对比表

指标 关注点 是否考虑排序 需要全部标注 计算复杂度 适用场景
Precision@K 前 K 个有多准 O(K) 快速评估
Recall@K 覆盖率 O(K) 评估召回能力
AP/MAP 综合精度 O(n) 学术评估
MRR 第一个正确答案 O(n) 问答系统
NDCG 整体排序质量 O(n log n) 搜索/推荐

8.2 选择建议

复制代码
你的场景是什么?
│
├─ 只需一个正确答案(问答系统)
│   └─→ MRR
│
├─ 用户会看多条结果(搜索引擎)
│   └─→ NDCG
│
├─ 快速评估效果
│   └─→ Precision@K
│
├─ 需要知道召回覆盖率
│   └─→ Recall@K
│
└─ 学术论文/综合评估
    └─→ MAP + NDCG

8.3 记忆口诀

Precision 看前 K,Recall 看覆盖
AP 综合算平均,MRR 只看第一名
NDCG 最全面,相关性 + 排序都考虑


九、完整实战代码

python 复制代码
import numpy as np
from typing import List


class RetrievalEvaluator:
    """检索质量评估器"""
  
    def precision_at_k(self, results: List[bool], k: int) -> float:
        """计算 Precision@K"""
        if len(results) < k:
            results = results + [False] * (k - len(results))
        return sum(results[:k]) / k
  
    def recall_at_k(self, results: List[bool], total_relevant: int) -> float:
        """计算 Recall@K"""
        if total_relevant == 0:
            return 0
        return sum(results) / total_relevant
  
    def average_precision(self, results: List[bool]) -> float:
        """计算 AP"""
        precision_sum = 0
        relevant_count = 0
      
        for i, is_relevant in enumerate(results):
            if is_relevant:
                relevant_count += 1
                precision_at_i = relevant_count / (i + 1)
                precision_sum += precision_at_i
      
        return precision_sum / relevant_count if relevant_count > 0 else 0
  
    def mean_average_precision(self, all_results: List[List[bool]]) -> float:
        """计算 MAP"""
        ap_scores = [self.average_precision(r) for r in all_results]
        return np.mean(ap_scores) if ap_scores else 0
  
    def reciprocal_rank(self, results: List[bool]) -> float:
        """计算单个查询的 RR"""
        for i, is_relevant in enumerate(results):
            if is_relevant:
                return 1 / (i + 1)
        return 0
  
    def mean_reciprocal_rank(self, all_results: List[List[bool]]) -> float:
        """计算 MRR"""
        rr_scores = [self.reciprocal_rank(r) for r in all_results]
        return np.mean(rr_scores) if rr_scores else 0
  
    def dcg(self, relevances: List[float]) -> float:
        """计算 DCG"""
        return sum((2 ** rel - 1) / np.log2(i + 2) 
                   for i, rel in enumerate(relevances))
  
    def ndcg(self, relevances: List[float]) -> float:
        """计算 NDCG"""
        dcg_score = self.dcg(relevances)
        ideal_relevances = sorted(relevances, reverse=True)
        idcg_score = self.dcg(ideal_relevances)
        return dcg_score / idcg_score if idcg_score > 0 else 0


# ==================== 使用示例 ====================

if __name__ == "__main__":
    evaluator = RetrievalEvaluator()
  
    print("=" * 60)
    print("检索质量评估示例")
    print("=" * 60)
  
    # 单个查询的检索结果
    results = [True, False, True, True, False, True, False, False]
  
    print(f"\n检索结果:{results}")
    print(f"Precision@5 = {evaluator.precision_at_k(results, 5):.3f}")
    print(f"Recall (总相关=10) = {evaluator.recall_at_k(results, 10):.3f}")
    print(f"AP = {evaluator.average_precision(results):.3f}")
  
    # 多查询评估
    all_results = [
        [True, False, True, False],
        [True, True, False, False],
        [False, True, True, False],
    ]
  
    print(f"\nMAP = {evaluator.mean_average_precision(all_results):.3f}")
    print(f"MRR = {evaluator.mean_reciprocal_rank(all_results):.3f}")
  
    # NDCG 评估
    relevances = [3, 2, 3, 1, 0]
    print(f"\nNDCG@5 = {evaluator.ndcg(relevances):.3f}")
  
    print("=" * 60)

十、结语

检索质量评估指标是衡量和优化检索系统的基础工具

  • Precision@KRecall@K 简单直观,适合快速评估
  • AP/MAP 综合考虑精度和召回,学术界常用
  • MRR 专注于第一个正确答案,适合问答系统
  • NDCG 最全面,是工业界的黄金指标

实际项目中,我推荐:

  1. 快速迭代阶段:用 Precision@K 监控
  2. 正式评估阶段:用 NDCG + MRR 综合评估
  3. 学术论文:用 MAP + NDCG@K 多指标报告

记住:没有完美的指标,只有适合的指标。选择哪个指标,取决于你的业务场景和用户需求。


本文代码已测试,可直接运行。欢迎收藏备用!

文档整理日期:2026-04-15

相关推荐
逻辑驱动的ken2 小时前
Java高频面试考点04
java·开发语言·算法·哈希算法·散列表
InfinteJustice2 小时前
如何加固SQL通信安全_启用SSL加密确保数据传输安全
jvm·数据库·python
nap-joker2 小时前
利用多模态神经影像数据进行阿尔茨海默病分类和检测的可解释性尖峰神经网络框架
人工智能·深度学习·神经网络
紧固视界2 小时前
设备防松如何选用垫圈和挡圈?常见方案与应用解析_6月上海紧固件展
大数据·人工智能·紧固件·上海紧固件展·紧固件展·上海紧固件专业展
搬砖者(视觉算法工程师)2 小时前
关于英伟达的世界模型Cosmos的介绍
人工智能
TTGGGFF2 小时前
SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习
学习·英语学习·生词本
密瓜智能2 小时前
从 Device Plugin 到 DRA:GPU 调度范式升级与 HAMi-DRA 实践
人工智能·kubernetes·开源·密瓜智能
John.Lewis2 小时前
C++加餐课-继承和多态:扩展学习
开发语言·c++·笔记
2301_817672262 小时前
C#怎么实现RSA非对称加密 C#如何用RSA算法进行公钥加密私钥解密和数字签名【安全】
jvm·数据库·python