K_DM Book
用語集
対象者 標準
対象者別の入口 →

対象者の設定はこのブラウザの中にだけ保存されます。

言語 JA

NDCG(Normalized Discounted Cumulative Gain)

中級

03.5.1

NDCG(Normalized Discounted Cumulative Gain)

最終更新 2020-01-29 読了時間 5 分
まとめ
NDCG は、ランキングの各順位 \(i\) にあるアイテムの関連度 \(rel_i\) を利得 \(2^{rel_i} - 1\) に変え、順位が下がるほど \(\log_2(i+1)\) で割り引いて足し合わせた DCG を、関連度の高い順に並べ替えた理想ランキングの DCG(IDCG)で割って 0〜1 に正規化した指標です。1 位の利得はそのまま、10 位の利得は約 3.46 分の 1 で数えるので、「関連度の高いものを上に置けたか」が値に直結します。関連度を 0/1 の二値ではなく 0〜3 の段階で扱えるのが MAP や Recall@k との違いで、scikit-learn の ndcg_score(y_true, y_score, k=10) で計算できます。

直感:上位の正解ほど「割引なし」で数える #

検索結果の 1 位に関連度 3 のページ、2 位に関連度 2、4 位に関連度 1 が並んでいるとします。ユーザーは上から見るので、下の順位ほど割り引いて数えたい。NDCG はこの「割引」を \(1/\log_2(i+1)\) で行い、1 位は 100%、2 位は 63%、4 位は 43% の重みで利得を足します。最後に理想順の合計で割るので、どのクエリでも満点は 1.0 です。

定義 #

ランク \(i\) の関連度スコアを \(rel_i\) とすると、DCG(Discounted Cumulative Gain)は次の和です。

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

分子 \(2^{rel_i} - 1\) が利得で、関連度 3 なら 7、関連度 1 なら 1、関連度 0 なら 0 です。分母 \(\log_2(i+1)\) が順位による割引で、1 位では 1、10 位では約 3.46 になります。理想的な並び(関連度の降順)で同じ計算をした値が IDCG で、それで割って正規化すると NDCG になります。

$$ \mathrm{NDCG@k} = \frac{\mathrm{DCG@k}}{\mathrm{IDCG@k}} $$

計算例:ステップバイステップ #

具体的な数値を使い、DCG → IDCG → NDCG の算出過程を一歩ずつ追います。

問題設定 #

検索エンジンが返した 5 件 の結果に対し、人手で以下の関連度(relevance grade)を付けたとします。

順位 $i$アイテム関連度 $rel_i$
1A3
2B2
3C0
4D1
5E0

関連度は 0(無関係)〜3(非常に関連)の 4 段階です。


ステップ 1:DCG@5 を求める #

DCG の定義式を再掲します。

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

各順位の 利得(gain) $2^{rel_i} - 1$ と 対数割引(discount) $\log_2(i+1)$ を計算し、それぞれの寄与を求めます。

順位 $i$$rel_i$利得 $2^{rel_i}-1$割引 $\log_2(i+1)$寄与 $\dfrac{2^{rel_i}-1}{\log_2(i+1)}$
13$2^3 - 1 = 7$$\log_2 2 = 1.000$$7 / 1.000 = 7.000$
22$2^2 - 1 = 3$$\log_2 3 \approx 1.585$$3 / 1.585 \approx 1.893$
30$2^0 - 1 = 0$$\log_2 4 = 2.000$$0 / 2.000 = 0.000$
41$2^1 - 1 = 1$$\log_2 5 \approx 2.322$$1 / 2.322 \approx 0.431$
50$2^0 - 1 = 0$$\log_2 6 \approx 2.585$$0 / 2.585 = 0.000$

したがって、

$$ \mathrm{DCG@5} = 7.000 + 1.893 + 0.000 + 0.431 + 0.000 = 9.324 $$

ステップ 2:理想順位を作り IDCG@5 を求める #

IDCG(Ideal DCG)は、関連度を降順に並べ替えた 理想的なランキング に対して DCG を計算したものです。

関連度を降順に並べると $[3, 2, 1, 0, 0]$ になります。

順位 $i$$rel_i$(理想)利得 $2^{rel_i}-1$割引 $\log_2(i+1)$寄与
1371.000$7.000$
2231.585$1.893$
3112.000$0.500$
4002.322$0.000$
5002.585$0.000$
$$ \mathrm{IDCG@5} = 7.000 + 1.893 + 0.500 + 0.000 + 0.000 = 9.393 $$

ステップ 3:NDCG@5 を算出 #

$$ \mathrm{NDCG@5} = \frac{\mathrm{DCG@5}}{\mathrm{IDCG@5}} = \frac{9.324}{9.393} \approx 0.9927 $$

この結果は「理想の約 99.3% の品質」を意味します。実際のランキングでは、関連度 1 のアイテム D が 4 位に沈んでいるだけで、上位 2 件は理想と同じ並びのため、高い NDCG が得られました。


対数割引の直感的な理解 #

なぜ割引に $\log_2(i+1)$ を使うのか、押さえておきたい性質が 3 つあります。第一に、1 位ほど重要という前提を数にしています。ユーザーは検索結果の上位から順に見るので、1 位の割引は $\log_2 2 = 1$(割引なし)ですが、10 位の割引は $\log_2 11 \approx 3.46$ で、利得が約 3.5 分の 1 に減衰します。第二に、減衰が線形割引 $1/i$ より緩やかです。10 位の重みは $1/i$ なら 10%、対数なら 29% で、「5 位と 6 位の差」より「1 位と 2 位の差」のほうがはるかに大きいというユーザー行動の経験則に合致します。第三に、対数の底を変えても(例:$\log_2$ を $\ln$ に変えても)DCG の値がスケーリングされるだけで、NDCG の値は変わりません。IDCG にも同じ底が使われ、割り算で吸収されるためです。

下の表で、順位が 1 位から 100 位へ深くなるにつれて割引がどう変化するかを確認できます。

順位 $i$$\log_2(i+1)$割引率 $1/\log_2(i+1)$
11.000100.0%
21.58563.1%
32.00050.0%
52.58538.7%
103.45928.9%
204.39222.8%
505.67217.6%
1006.65815.0%

1 位では利得が 100% 反映されますが、100 位ではわずか 15% しか反映されません。NDCG は「上位に高関連のアイテムを置くことがとくに重要」という直感を、この割引率の列で数値化しています。


Python で計算 #

ndcg_score にクエリごとの関連度行列とモデルのスコア行列を渡すと、上の手計算と同じ NDCG@k が返ります。

1
2
3
4
5
6
7
8
9
from sklearn.metrics import ndcg_score

# y_true: shape (n_samples, n_labels) に関連度を格納

# y_score: モデルが出力したスコア

score = ndcg_score(y_true, y_score, k=10)

print("NDCG@10:", round(score, 4))

ndcg_score は関連度を 0/1 だけでなく、段階的な整数スコアでも扱えます。Ground Truth として「候補ごとの関連度」の配列を用意するのがポイントで、そこが揃えば残りは 1 行です。


@k での評価(NDCG@k) #

実務では全件ではなく、ランキングの 上位 k 件だけ を対象に評価する NDCG@k が一般的です。

なぜ打ち切りが必要か #

ユーザーが実際に閲覧するのは上位数件〜数十件で、100 位以降の品質はユーザー体験に影響しません。全件で評価すると下位の無関係アイテムが大量に含まれ、1 位と 2 位を入れ替えたような上位の改善がスコアにほとんど反映されなくなります。k を 10 に固定しておけば、候補数の違うモデル同士も同じ条件で比較できます。

k の値による違い #

先ほどの計算例(関連度: [3, 2, 0, 1, 0])を使い、k を変えたときの NDCG 値を比較します。

kDCG@kIDCG@kNDCG@k
17.0007.0001.0000
28.8938.8931.0000
38.8939.3930.9468
59.3249.3930.9927

k=1 と k=2 では実際のランキングと理想が一致するため NDCG = 1.0 です。k=3 にすると、3 位に関連度 0 のアイテム C が入っているのに対し、理想では関連度 1 の D が 3 位に来るため差が現れ、0.9468 に下がります。k=5 では 4 位の D が寄与 0.431 を取り戻し、0.9927 まで回復します。

典型的な k の選び方 #

ユースケース推奨k理由
Web検索10検索結果1ページ分(10件)が標準
レコメンド5〜20UIに表示されるカルーセルやリストの件数
広告ランキング3〜5ファーストビューに収まる枠数
学術ベンチマーク10, 100TREC等の標準的な評価設定

上の表の 4 つの k を、同じ関連度配列で ndcg_score に順に渡して再現します。

1
2
3
4
5
6
7
8
9
from sklearn.metrics import ndcg_score
import numpy as np

y_true = np.array([[3, 2, 0, 1, 0]])
y_score = np.array([[5, 4, 3, 2, 1]])  # 順位に対応するスコア

for k in [1, 2, 3, 5]:
    score = ndcg_score(y_true, y_score, k=k)
    print(f"NDCG@{k} = {score:.4f}")

設定で迷うところ #

k は、ユーザーに提示する件数に合わせて @5 や @10 を選びます。関連度スコアは 0/1 の二値でも計算できますが、「非常に関連」「やや関連」のような段階ラベルを 0〜3 で付けると、利得 \(2^{rel}-1\) が 7 と 1 のように差を付けるため精度の高い評価になります。log の底は定義によって \(\log_2\) 以外を使うこともありますが、IDCG にも同じ底が入るので NDCG の値は変わりません。

使いどころと落とし穴 #

検索結果の評価では、人手で付けた関連度ラベルを使い NDCG@10 を主指標にする例が多く、レコメンドでは閲覧や購買などの Implicit Feedback を関連度 1、それ以外を 0 として扱い、ランキング改善の KPI にします。A/B テストの前には、オフラインで NDCG@10 を 0.02 上げた変更がオンラインのクリック率にどう影響したかを突き合わせ、指標の信頼度を確かめます。

落とし穴は 3 つあります。Ground Truth の関連度を 0〜3 で整備するコストが高く、Implicit Feedback で代用するとクリックしたが無関係だったようなノイズが関連度に混ざります。候補生成とランキングの二段階構成では、候補生成は Recall@k、並べ替えは NDCG のように、フェーズごとに適した指標を分けて設定します。NDCG だけでは「正解を何件取りこぼしたか」が見えないので、Recall@k や MAP を併用してユーザー体験を多面的に評価します。


まとめ #

NDCG は、関連度 \(rel_i\) の利得 \(2^{rel_i}-1\) を順位の割引 \(\log_2(i+1)\) で割って足した DCG を、理想順の IDCG で割って 0〜1 にした指標で、上位に高関連度が並ぶほど 1.0 に近づきます。5 件の例では DCG@5 = 9.324、IDCG@5 = 9.393 で NDCG@5 ≈ 0.9927、k=3 で切ると 0.9468 に下がるように、k の置き方で評価が変わります。ndcg_score(y_true, y_score, k=10) の 1 行で計算でき、関連度を段階的に付けるほど、画面の表示件数に k を合わせるほど、評価が実際のユーザー体験に近づきます。取りこぼしの量は Recall@k、正解の順位の集まり方は MAP と役割が分かれるので、並べて読んでランキング品質を総合的に判断します。