本文へスキップ
BecomeCoder

RAGコース · 第5章 精度を上げる · レッスン20

リランキング ― 候補を並べ直す2段構え

ブラウザで完結

導入

検索の精度を上げるもう1つの定番テクニックが2段構えの検索です。まず速くて軽い検索で候補を多めに取ってきて(見逃しを減らす=recall重視)、その候補だけを、今度は重くて丁寧なモデルで採点し直します(正確さを上げる=precision重視)。全文書を毎回重いモデルで採点するのは遅すぎるので、「まず絞る→あとで丁寧に選ぶ」という役割分担がポイントです。

本物のリランカーにはCross-Encoder(質問と文書のペアをまとめて読み込み、関連度を直接スコア化するモデル)がよく使われますが、ここでは自作の簡易リランカーで「候補が並べ直される」感覚を体験します。

説明

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

docs = [
    "Python is a programming language used for data science and AI.",
    "The python is a large snake found in Asia and Africa.",
    "Monty Python is a famous British comedy group.",
    "Pandas is a Python library for data analysis.",
]
query = "python data analysis library"
vec = TfidfVectorizer().fit(docs)
mat = vec.transform(docs)

# 1段目: 速い検索で候補を多めに取る(recall重視)
scores = cosine_similarity(vec.transform([query]), mat)[0]
candidates = list(np.argsort(scores)[::-1][:3])
print("Stage 1 (retriever):")
for i in candidates:
    print(f"  {scores[i]:.3f}  {docs[i]}")

# 2段目: 候補だけを採点し直す(簡易リランカー = 質問語との重なり数)
q_terms = set(query.lower().split())
def rerank_score(doc):
    d_terms = doc.lower().replace(".", "").split()
    return sum(1 for w in d_terms if w in q_terms)

reranked = sorted(candidates, key=lambda i: rerank_score(docs[i]), reverse=True)
print("Stage 2 (reranked):")
for i in reranked:
    print(f"  overlap={rerank_score(docs[i])}  {docs[i]}")

最初の実行は少し待ちます:scikit-learn を使う回は、初回だけライブラリの読み込みに数十秒かかることがあります(2回目以降は速くなります)。「読み込み中…」と出たら、そのまま待ってください。

1段目(Stage 1)はTF-IDFのコサイン類似度で上位3件を候補にしています。この時点では、スネークの話(The python is a large snake...)のようにキーワードは似ていても内容が的外れな文書が紛れ込むことがあります。2段目(Stage 2)では、その候補だけを対象に、質問の単語がどれだけ重なっているかで採点し直しています。実行結果を見ると、関連の薄い候補が下位に沈み、質問に本当に近い文書が上に来ていることが確認できます。

本物のリランカー(Cohere Rerank や各種 Cross-Encoder モデル)は、この「重なり数」よりもずっと賢い方法で関連度を判定します。ただし処理が重いため、候補数(k)を絞ってから使うのが定石です。全文書に対して毎回実行していては遅すぎます。

やってみよう

1段目の候補数を [:3] から [:4](全件)に増やしてみましょう。2段目のリランキングで、増えた候補がどのように並び替えられるか確認してください。

演習

1段目の候補数を [:3] から [:2] に絞って、2段目のリランキング結果がどう変わるか確認してください。candidates の計算し直しから reranked の表示までをもう一度書いて print しましょう。

ヒント1を見る

np.argsort(scores)[::-1][:2] のように、末尾のスライスの数字を2に変えるだけです。

ヒント2を見る

新しい candidates を使って rerank_score で並べ替え、for i in reranked: print(docs[i]) のように表示します。

まとめ

  • リランキングは「速い検索で多めに候補を取る→重いモデルで候補だけを採点し直す」の2段構え。
  • 1段目はrecall(見逃しを減らす)重視、2段目はprecision(正確さ)重視。
  • 本物はCross-Encoderなどを使うが、候補数(k)を絞ることでコストを現実的にする。

実際に動かしてみよう

下のエディタにPythonを書いて「実行」を押すと、scikit-learn(TF-IDF)とnumpyで本物の「検索」をその場で動かせます。RAGの検索フェーズ――文書のチャンク分割・ベクトル化・類似度計算・並べ替え――が実際に動きます。scikit-learn を使う回は初回の読み込みに数十秒かかることがあります。検索デモのサンプル文書は英語です(TF-IDFの既定トークナイザが日本語を分割できないため。日本語検索の対処は読み物で解説します)。LLMの生成や本物の埋め込みAPIは、APIキーが要るため読み物の回でコードを示します。

Python — ライブラリ付きで実行(Pyodide)

numpy / pandas / matplotlib が使える本物のPython(Pyodide)を読み込みます。初回のみ読み込みに少し時間がかかります(以降はブラウザにキャッシュされます)。
スクロールして表示された時点でも自動で読み込まれます。