0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RAGのRecall@3、重複チャンクで150%の誤集計

0
Posted at

Recall@3が1.50になった。今日、同じ文書のIDを3回並べた最小ケースをPythonで作っていて、正解文書を拾うたびに加算するコードが、150%を返した。

RAGの検索器を比べる前に、評価コードの重複カウントを潰したい。チャンクを細かくしただけで成績が上がる集計を使うと、改善の方向まで間違えるんだよね。

Recall@3の分子と分母を文書にそろえる

今回は、検索結果を元の文書IDへ変換して評価する。正解は manual と faq の2文書。検索結果は順位順に manual, manual, manual, faq, terms とする。先頭3件は、同じマニュアルから切り出した別チャンクという想定だ。

ここで測る文書単位のRecallは「正解文書のうち、上位kチャンクに何文書含まれるか」。上位k件に含まれる正解文書IDの種類数 ÷ 正解文書IDの種類数で計算する。

先頭3件に正解のIDが3回あるから、素朴な加算では3÷2で1.50。実際に見つかった正解文書は manual だけなので、文書単位なら1÷2で0.50になる。

manual の3チャンクがどれほど有用かは、まだ測っていない。

Pythonで再現する

以下を recall_check.py に保存し、python3 recall_check.py で実行する。外部ライブラリは不要。手元ではPython 3.14.8で、出力とassertを確認した。

hits = ["manual", "manual", "manual", "faq", "terms"]
relevant = {"manual", "faq"}

def doc_recall(hits, relevant, k):
    if k < 1:
        raise ValueError("k must be positive")
    if not relevant:
        return None
    return len(set(hits[:k]) & relevant) / len(relevant)

unique_hits = list(dict.fromkeys(hits))
naive = sum(x in relevant for x in hits[:3]) / len(relevant)
print(f"naive: {naive:.2f}")
print(f"chunk_top3: {doc_recall(hits, relevant, 3):.2f}")
print(f"doc_top3: {doc_recall(unique_hits, relevant, 3):.2f}")

assert doc_recall(["manual"] * 3, {"manual"}, 3) == 1
assert doc_recall(["terms"], relevant, 3) == 0
assert doc_recall([], relevant, 3) == 0
assert doc_recall(hits, set(), 3) is None

出力はこうなった。

naive: 1.50
chunk_top3: 0.50
doc_top3: 1.00

正解IDはsetで渡す契約にした。検索結果側も、先頭k件を切ってからsetへ変換する。これで、同じ文書が何チャンクあっても分子への加算は1回になる。正解があるのに検索結果が空なら0、正解IDの集合が空なら None を返す。

正解が空の設問はRecallの分母が0になる。この実装では平均から除外し、除外件数を別に残す方針だ。正解未付与の設問と、本当に回答できない設問はデータ側で区別しておきたい。後者の「検索しない判断」は別途評価する。

重複除去を先にすると、別の上位3件になる

出力の doc_top3 は1.00。同じ重複除去でも、順番を変えると数字が違う。

dict.fromkeys で文書IDの初出順を残すと、候補は manual, faq, terms になる。その先頭3文書を評価するため、正解2文書が両方入る。元の検索順位では4件目だった faq まで拾っている。

計算順序 評価する対象 Recall
先頭3件を切ってから重複除去 上位3チャンク内の文書 0.50
候補全体を重複除去してから先頭3件 初出順の上位3文書 1.00

LLMへ3チャンクだけ渡す実装なら、前者がその入力に対応する。後者を使うなら、検索側も文書をまとめ、実際に渡すコンテキストを組み直す必要がある。評価時だけ4件目を繰り上げると、LLMが読めなかった文書を成績に含めてしまう。

実データでは、チャンクIDと元文書IDをログに残す。同名の別文書を混ぜないよう、文書IDは検索インデックス側のIDにそろえる。文書の版も評価データと合わせる。

検索の改善に持ち帰るもの

自分は、チャンク数を変える比較では、この重複ケースを評価コードの確認に残したい。まず、実際にLLMへ渡した上位k件を固定して文書単位のRecallを計算する。その数字だけでは、回答に必要な段落が入ったかまでは分からないので、正解チャンクへの到達も別に見る。チャンク分割を細かくするほど、文書の網羅率と必要な段落への到達率が違う動きをするケースは増えると見ている。今回の5件だけでも、同じ検索結果から0.50と1.00が出る理由は切り分けられた。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?