NDCG 详解:以油菜育种品系排序为例

概念

NDCG(Normalized Discounted Cumulative Gain,归一化折损累计增益) 是信息检索与排序学习中用来评价排序质量的核心指标。

它由三个逐层递进的概念组成:

  • CG(Cumulative Gain,累计增益):把排序结果前 k 个位置的相关性得分直接相加。它不关心顺序 —— 排第 1 还是排第 100,贡献完全相同。
  • DCG(Discounted Cumulative Gain,折损累计增益):在 CG 基础上,给越靠后的位置施加越大的折扣(惩罚)。因为决策者通常只关注前几位,排在前面的高相关项理应获得更高权重。
  • NDCG(归一化折损累计增益):把 DCG 除以一个理想排序的 DCG(即 IDCG),把结果压缩到 [0, 1] 区间,方便不同任务之间横向比较。

一句话总结:NDCG 衡量的是真正重要的项,有没有被排在前面,并用 0~1 的分数表达。

作用

NDCG 作为排序评价指标,其普适价值体现在以下几个方面:

  1. 客观比较多个排序模型 / 策略:面对同一批待排序对象,不同算法(如基于规则的排序、基于单特征的排序、基于机器学习的排序)给出的榜单往往不同。NDCG 提供一把统一的量尺,让谁的排序更懂得把重要项往前放有据可循,排除人工主观评判。
  2. 支持分级相关性评价:现实中的相关性常是分级的(如 0~4 的等级、星级、得分),而非简单的「相关 / 不相关」二分。NDCG 能区分「极重要」与「一般重要」的差异,二值指标则做不到。
  3. 聚焦头部质量:很多业务只关心排在最前面的少数结果(搜索的前几条、推荐的前几位、筛选的前 k 个候选)。NDCG@k 直接衡量前 k 个结果里,重要的项是否足够多、是否足够靠前,与真实使用场景高度契合。
  4. 可作模型训练目标:在排序学习(Learning to Rank)中,NDCG 既可作为评估指标,也可被直接作为目标函数,指导模型朝着把高相关项顶到前面的方向学习。

公式

设排序列表前 k 个位置的相关性得分为 rel₁, rel₂, ..., relₖrelᵢ 为第 i 位的相关性等级)。

CG(累计增益)

$CG@k = \sum_{i=1}^{k} rel_i$

DCG(折损累计增益,常用指数形式)

$DCG@k = \sum_{i=1}^{k} \frac{2^{rel_i} - 1}{\log_2(i+1)}$

折扣项 $\log_2(i+1)$ 随位置 i 增大而增大,因此越靠后,单位相关性得分的贡献越小。第 1 位折扣为 $\log_2 2 = 1$(不折损),第 2 位 $\log_2 3 \approx 1.585$,第 3 位 $\log_2 4 = 2$,依此类推。

当相关性得分是连续实数时,则采用对数形式 $DCG@k = \sum \frac{rel_i}{\log_2(i+1)}$。

IDCG(理想 DCG):将相关性得分按从大到小排序后计算的 DCG,即“理论上最好排法”的得分。

NDCG(归一化)

$NDCG@k = \frac{DCG@k}{IDCG@k} \in [0, 1]$

NDCG 越接近 1,说明排序越好;越接近 0,说明排序越差。

示例:油菜育种品系排序

场景设定

某育种单位有 5 个甘蓝型油菜杂交后代品系,准备筛选综合表现最优者进入区域试验。育种专家依据综合育种价值(产量、含油量、抗疫病性、早熟性)给每个品系打了 0~4 的等级分 —— 这是用于事后评估的真实相关性

品系综合育种价值等级 rel说明
中油杂 A4(极优)高产、高油、抗疫病
华油杂 B3(优)高产、中油
中双 C2(中)中产、高油
秦优 D1(一般)中产、晚熟
油研 E0(淘汰)低产、感病

注:rel 是分级的(0~4),正是 NDCG 擅长处理的情形。

两个排序模型

  • 模型甲(综合指数排序)给出的推荐顺序:B → C → A → D → E
  • 模型乙(仅按单产排序,朴素)给出的推荐顺序:E → D → C → B → A(恰好把最差的排最前,用于对照)

理想顺序(按真实 rel 降序):A → B → C → D → E,排序可视化如下图:

由图可知,模型甲给出的排序要较优于模型乙

手动计算(以模型甲为例,k=5)

先准备:位置分母 $\log_2(i+1)$ 为 1, 1.585, 2, 2.322, 2.585;指数项 $(2^{rel}-1)$ 为 rel=4→15,3→7,2→3,1→1,0→0。

模型甲 DCG@5

位置 i品系rel2^rel−1log₂(i+1)贡献
1B371.0007.000
2C231.5851.893
3A4152.0007.500
4D112.3220.431
5E002.5850.000
DCG16.823

理想 IDCG@5

位置 i品系rel2^rel−1log₂(i+1)贡献
1A4151.00015.000
2B371.5854.416
3C232.0001.500
4D112.3220.431
5E002.5850.000
IDCG21.347

$NDCG@5_{甲} = \frac{16.823}{21.347} \approx 0.788$

同理,模型乙 DCG@5 ≈ 10.952,故 $NDCG@5_{乙} \approx 0.513$;理想排序 NDCG = 1.000。

解读:模型甲把极优品系 A 排到了第 3 位(被 B、C 挡在前面),所以没拿满分,但整体仍不错(0.79);模型乙几乎把顺序完全颠倒,得分骤降到 0.51。NDCG 清晰量化了「谁更懂得把好东西往前放」。

代码实现

下面用 Python 实现 NDCG,并复现上面的油菜育种示例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import math

def dcg(rels, k=None):
    """计算 DCG@k(指数形式)。rels 为相关性得分列表(已按模型排序)。"""
    if k is None:
        k = len(rels)
    gain = 0.0
    for i, rel in enumerate(rels[:k], start=1):
        gain += (2 ** rel - 1) / math.log2(i + 1)
    return gain

def ndcg(rels_model, rels_ideal=None, k=None):
    """
    计算 NDCG@k。
    rels_model : 模型给出的排序相关性(顺序即排序结果)
    rels_ideal : 理想排序相关性;缺省时由 rels_model 降序得到
    """
    if rels_ideal is None:
        rels_ideal = sorted(rels_model, reverse=True)
    idcg = dcg(rels_ideal, k)
    if idcg == 0:
        return 0.0
    return dcg(rels_model, k) / idcg

# ---------- 油菜育种示例数据 ----------
# 真实综合育种价值等级:A=4, B=3, C=2, D=1, E=0
true_rel = {"A": 4, "B": 3, "C": 2, "D": 1, "E": 0}

order_甲    = ["B", "C", "A", "D", "E"]   # 模型甲:综合指数排序
order_乙    = ["E", "D", "C", "B", "A"]   # 模型乙:仅按单产排序(朴素对照)
order_ideal = sorted(true_rel, key=lambda x: true_rel[x], reverse=True)

rels_甲    = [true_rel[x] for x in order_甲]
rels_乙    = [true_rel[x] for x in order_乙]
rels_ideal = [true_rel[x] for x in order_ideal]

print("理想顺序   :", order_ideal, "-> IDCG@5   =", round(dcg(rels_ideal), 4))
print("模型甲顺序 :", order_甲,    "-> NDCG@5   =", round(ndcg(rels_甲), 4))
print("模型乙顺序 :", order_乙,    "-> NDCG@5   =", round(ndcg(rels_乙), 4))
print("模型甲 NDCG@3 =", round(ndcg(rels_甲, k=3), 4))
print("理想 NDCG@5   =", round(ndcg(rels_ideal), 4))

运行输出:

1
2
3
4
5
理想顺序   : ['A', 'B', 'C', 'D', 'E'] -> IDCG@5   = 21.3472
模型甲顺序 : ['B', 'C', 'A', 'D', 'E'] -> NDCG@5   = 0.7881
模型乙顺序 : ['E', 'D', 'C', 'B', 'A'] -> NDCG@5   = 0.5129
模型甲 NDCG@3 = 0.7837
理想 NDCG@5   = 1.0

代码要点

  • dcg() 用指数形式 $\sum (2^{rel}-1)/\log_2(i+1)$,位置从 1 开始计数。
  • ndcg() 默认把输入相关性降序当作理想排序(IDCG);也可显式传入真实理想列表。
  • NDCG 对位置敏感:把 rel=4 的 A 从第 1 位挪到第 3 位,分数立刻从 1.0 掉到 0.79,直观体现了「好东西要往前放」。

总结

  • NDCG 是什么:一种把排序结果映射成 0~1 分数的指标,核心是位置越靠后折扣越大 + 归一化到理想排序
  • 关键特性:支持分级相关性;关注头部顺序;跨任务可比。