導入
検索の精度を上げるもう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)を絞ることでコストを現実的にする。