はじめに
先日開催した社内勉強会では、書籍『Pythonではじめる 情報検索プログラミング』を教材として、情報検索の基本的な考え方について学びました。
本記事では、その中でも「検索システムの評価」にまつわる内容を整理します。
検索システムでは、文字列検索やベクトル空間モデルなど、さまざまな方法で文書を検索できます。しかし、検索結果が得られたとしても、それが本当に良い結果なのかを判断できなければ、検索システムを改善することはできません。
そこで重要になるのが、適合率・再現率・F値・平均適合率といった評価指標です。
今回は、検索結果と正解文書の関係を確認した上で、検索システムの評価方法、ランキング結果の評価、そして評価に必要となるテストコレクションについて整理していきます。
書籍リンク:https://www.morikita.co.jp/books/mid/081861
適合率と再現率
検索システムを評価するには、まず「検索結果」と「本当に正解である文書」の関係を見る必要があります。
検索システムが返した文書集合を S、人手で判断した正解文書の集合を A とします。
理想は、検索結果Sと正解集合Aが完全に一致することです。(図5.1を参照。)

適合率
適合率は、検索結果の中にどれだけ正解が含まれているかを表す指標です。
$$
P = \frac{|S \cap A|}{|S|}
$$
検索結果にノイズが少ないほど、適合率は高くなります。
再現率
再現率は、正解文書をどれだけ取りこぼさずに拾えたかを表す指標です。
$$
R = \frac{|S \cap A|}{|A|}
$$
たとえば特許検索のように、見落としが大きな問題になる検索では、再現率が特に重要になります。
F値
適合率と再現率は、基本的にトレードオフの関係にあります。
広く拾えば再現率は上がりますが、その分ノイズも混ざりやすくなり、適合率は下がる可能性があります。
そこで、適合率と再現率のバランスを見る指標として F値 を使います。
$$
F = \frac{2PR}{P+R}
$$
F値は、適合率と再現率の調和平均です。
どちらか一方だけが高いのではなく、両方のバランスが取れているかを見るための指標です。
コードでの例
ここでは、簡単な例として「人工知能」というクエリで検索した場合を考えます。
検索対象の文書が5件あり、それぞれについて、人手で判断した正解ラベルと、検索システムが計算した類似度スコアがあるとします。
| id | 文書 | 正解ラベル | 類似度スコア |
|---|---|---|---|
| 0 | 人工知能の入門 | 1 | 0.90 |
| 1 | 人工島の観光案内 | 0 | 0.55 |
| 2 | AIチャットボットの仕組み | 1 | 0.45 |
| 3 | 犬の知能について | 0 | 0.25 |
| 4 | 深層学習の基本 | 1 | 0.20 |
正解ラベルが1の文書は、検索要求に合っている文書です。
正解ラベルが0の文書は、検索要求には合っていない文書です。
一方、類似度スコアは検索システムが計算した値です。
スコアが高いほど、クエリに近い文書だと判断されていることを表します。
この例をPythonで表すと、次のようになります。
docs = [
{"id": 0, "text": "人工知能の入門", "answer": 1, "score": 0.90},
{"id": 1, "text": "人工島の観光案内", "answer": 0, "score": 0.55},
{"id": 2, "text": "AIチャットボットの仕組み", "answer": 1, "score": 0.45},
{"id": 3, "text": "犬の知能について", "answer": 0, "score": 0.25},
{"id": 4, "text": "深層学習の基本", "answer": 1, "score": 0.20},
]
次に、類似度スコアが一定以上の文書を検索結果として採用します。
ここでは、閾値を変えながら、適合率・再現率・F値がどのように変わるかを確認します。
def evaluate(docs, threshold):
y_true = [doc["answer"] for doc in docs]
y_pred = [1 if doc["score"] >= threshold else 0 for doc in docs]
tp = sum(1 for t, p in zip(y_true, y_pred) if t == 1 and p == 1)
fp = sum(1 for t, p in zip(y_true, y_pred) if t == 0 and p == 1)
fn = sum(1 for t, p in zip(y_true, y_pred) if t == 1 and p == 0)
precision = tp / (tp + fp) if tp + fp > 0 else 0
recall = tp / (tp + fn) if tp + fn > 0 else 0
f1 = 2 * precision * recall / (precision + recall) if precision + recall > 0 else 0
return precision, recall, f1
for threshold in [0.5, 0.3]:
precision, recall, f1 = evaluate(docs, threshold)
print(f"threshold = {threshold}")
print(f"precision = {precision:.3f}")
print(f"recall = {recall:.3f}")
print(f"f1 = {f1:.3f}")
print()
出力例は次のようになります。
threshold = 0.5
precision = 0.500
recall = 0.333
f1 = 0.400
threshold = 0.3
precision = 0.667
recall = 0.667
f1 = 0.667
閾値を0.5にした場合、検索結果として採用されるのはスコアが0.5以上の文書です。
このとき、「人工知能の入門」は正解ですが、「人工島の観光案内」は検索要求には合っていません。
そのため、検索結果にノイズが混ざり、適合率は0.500になります。
一方で、閾値を0.3に下げると、「AIチャットボットの仕組み」も検索結果に含まれるようになります。
この文書は正解ラベルが1なので、取りこぼしが減り、再現率が上がります。
このように、どこまでを検索結果として採用するかによって、適合率・再現率・F値は変化します。
また、この例では「人工知能」というクエリに対して、「人工島の観光案内」も高いスコアを持っています。
これは、「人工」という語が一致しているためです。
一方で、「AIチャットボットの仕組み」や「深層学習の基本」は、内容としては人工知能に関係していますが、「人工知能」という語そのものは含まれていません。
そのため、単純な単語一致に基づく検索では、内容的に近い文書をうまく拾えない場合があります。
このような例から、検索システムの評価では、単にスコアを見るだけではなく、人手で判断した正解と照らし合わせることが重要だとわかります。
適合率 - 再現率曲線
検索結果にランキングがある場合は、「正解文書が上位に来ているか」も重要です。
Web検索を考えると、正解に近いページが上位に表示されるほど、使いやすい検索システムだといえます。
そのようなランキングの良さを評価するために使われるのが、適合率-再現率曲線です。
検索結果の上位1件、上位2件、上位3件……というように、採用する件数を増やしながら、その時点での適合率と再現率を計算します。
一般に、上位だけを見ると適合率は高くなりやすいです。
しかし、検索結果に含める文書を増やしていくと、正解文書を多く拾える一方で、ノイズも混ざりやすくなります。
この変化をグラフにしたものが、適合率-再現率曲線です。(図5.2を参照。)

図5.2 適合率 - 再現率曲線の描画
図5.2を見ると、再現率が上がるにつれて、適合率が上下しながら変化していることがわかります。
これは、検索結果を上位から順に追加していく中で、正解文書だけでなく不適合な文書も混ざってくるためです。
一方、理想的なランキングでは、正解文書がすべて上位に並びます。
その場合、しばらく適合率1.0を維持したまま、再現率だけが上がっていきます。(図5.3を参照。)

図5.3 適合率 - 再現率曲線の描画(理想的なランキングな場合)
図5.3のように、理想的なランキングでは、曲線の下の面積が大きくなります。
つまり、正解文書が上位に集まっているほど、ランキングとして良い結果だと考えられます。
平均適合率とMAP
適合率-再現率曲線を見ると、ランキングの良し悪しを視覚的に確認できます。
さらに、その曲線の下の面積を使うと、ランキングの良さを数値として評価できます。
この値を 平均適合率(Average Precision; AP) と呼びます。
平均適合率は、正解文書が上位に集まっているほど高くなります。
簡単なコードで確認すると、次のようになります。
def average_precision(docs):
ranking = sorted(docs, key=lambda doc: doc["score"], reverse=True)
correct_count = 0
precision_sum = 0
total_correct = sum(doc["answer"] for doc in docs)
for rank, doc in enumerate(ranking, start=1):
if doc["answer"] == 1:
correct_count += 1
precision_at_rank = correct_count / rank
precision_sum += precision_at_rank
return precision_sum / total_correct
ap = average_precision(docs)
print(f"average precision = {ap:.3f}")
出力例は次のようになります。
average precision = 0.756
この値が高いほど、正解文書がランキングの上位に集まっているといえます。
理想的なランキングでは、平均適合率は1.0になります。
ただし、平均適合率はクエリごとに変わります。
あるクエリでは良い結果が出ても、別のクエリではうまくいかないことがあります。
そこで、複数のクエリで平均適合率を計算し、それらをさらに平均した MAP(Mean Average Precision) が使われます。
MAPを使うことで、1つのクエリだけではなく、検索システム全体としてのランキング性能を評価できます。
5.5 テストコレクション
検索システムを評価するには、評価用のデータセットが必要です。
具体的には、次のような情報が必要になります。
- 検索対象となる文書集合
- クエリ
- 各クエリに対する正解文書の集合
このような評価用データセットを テストコレクション と呼びます。
複数の検索システムを公平に比較するには、同じテストコレクションを使うことが重要です。
代表的な取り組みとして、米国NISTなどが主催する TREC があります。
また、日本語文書を対象とした検索評価では、BMIR-J1、BMIR-J2、NTCIRなどのテストコレクションが使われてきました。
ただし、大規模な文書集合では、人手ですべての適合文書を確認するのは現実的ではありません。
そのため、検索評価では、正解データをどのように作るかも重要な課題になります。