実践編Practice
意味で検索する(TF-IDF vs 埋め込み)
同じ文書集合に対して、キーワードの一致で探す検索(TF-IDF)と、意味の近さで探す検索(文埋め込み)を両方作り、同じ質問で結果を並べて比べます。自然言語処理の古典的な特徴量と、大規模言語モデルにつながる文埋め込みを、コサイン類似度という共通のものさしで結ぶ総合演習です。手を動かせるノートブックつきで進めます。
この教材は日本語のみで提供していますThis learning material is available in Japanese only.
課題設定The task
言葉の一致ではなく、意味で探す
手元にたくさんの文書があり、その中から「知りたいこと」に近いものを取り出したい、という場面を考えます。素朴な方法はキーワード検索です。質問に含まれる語と、同じ語をたくさん含む文書を上位に出します。長く使われてきた確実な方法ですが、弱点があります。言い換えや同義語に弱いのです。「宇宙船」で検索したとき、本文が「ロケット」「探査機」と書いていれば、語が一致しないため取りこぼします。
もう一つの方法が意味検索です。文をそのまま意味を表すベクトル(文埋め込み)に変換し、ベクトルの近さで探します。語が違っても意味が近ければ近くに配置されるので、言い換えを拾えます。この演習では、同じ文書集合に対して二通りの検索を組み立て、同じ質問で結果を比べ、それぞれの強みと弱みを体で理解します。
- データ入手:実在の公開コーパス(20 Newsgroups の一部)から文書を用意する。
- 前処理:小文字化や記号の除去でテキストをそろえ、トークンに区切る。
- TF-IDF 検索:語の重要度で文書をベクトル化し、コサイン類似度で順位付けする。
- 意味検索:文埋め込みモデルで文をベクトル化し、同じくコサイン類似度で検索する。
- 比較と考察:言い換えクエリで両者の差を観察し、計算コストや用途の使い分けを整理する。
二つの検索は、最後は同じ「コサイン類似度で近いものを選ぶ」という形に落ち着きます。違うのは文書を数値にする方法だけです。そこに注目すると、古典的な特徴量と大規模言語モデル由来の埋め込みが、地続きであることが見えてきます。
この演習の流れ
使う技術Techniques used
つなぐモジュール
この演習は、これまで単体で学んだ次のモジュールを一本につなぎます。まだ学んでいない技術があれば、先にそのモジュールで基礎を押さえてから戻ってくると、各ステップの意味がはっきりします。
自然言語処理(NLP)
- 前処理・トークン化と、Bag-of-Words から TF-IDF への流れ。コサイン類似度の考え方も含め、Step 2・Step 3の土台になります。
大規模言語モデル(LLM)
- Transformer と文埋め込み。意味をベクトルで表す仕組みが、Step 4の意味検索の中心です。
開発データの前処理
- 正規化や不要データの除去といった、テキストをそろえる基本作業。Step 1・Step 2で軽く使います。
ノートブックで実行Run the notebook
このページのコードを、そのまま動かす
意味で検索するノートブック
このページで読むコードと同じ内容を、上から順に実行できるノートブックにまとめてあります。Colab で開けば、インストール込みで環境構築なしにそのまま動かせます。手元の Jupyter で動かしたい場合は .ipynb をダウンロードしてください。
最初のセルで !pip install scikit-learn sentence-transformers を実行します。埋め込みモデルは初回に自動でダウンロードされ、文書のベクトル化には少し時間がかかります(GPU があると速くなります)。
データ入手:文書集合を用意する
Collect the documents何を・なぜWhat & why
検索の題材には、実在する公開コーパス 20 Newsgroups を使います。1990年代のネットニュース投稿を約2万件集めたもので、話題ごとに20のカテゴリに分かれています。scikit-learn に取得用の関数があり、追加のダウンロード作業なしに読み込めます。ここでは話題の異なる4カテゴリ(宇宙・自動車・コンピュータグラフィックス・野球)に絞り、検索結果の当たり外れが分かりやすいようにします。
投稿にはヘッダや署名、引用が混ざっているため、remove で本文以外を落とします。まずは扱いやすいように先頭200件だけを使います。
from sklearn.datasets import fetch_20newsgroups
cats = ["sci.space", "rec.autos",
"comp.graphics", "rec.sport.baseball"]
data = fetch_20newsgroups(
subset="train", categories=cats,
remove=("headers", "footers", "quotes"),
random_state=42)
docs = data.data[:200]
print("documents:", len(docs))
print(docs[0][:160])
documents: 200
I was wondering if anyone could tell me more about the orbit of the
space shuttle and how long it takes to circle the earth once it has
reached low earth orbit.この Step のまとめ
fetch_20newsgroups で実在のコーパスから文書を読み込みました。話題の異なる文書が混ざっているので、次の前処理でそろえたあと、検索が話題をきちんと当てられるかを確かめられます。
前処理:テキストをそろえる
Normalize the text何を・なぜWhat & why
生のテキストには大文字小文字の揺れや記号が混ざっています。TF-IDF は語の一致を見るので、Orbit と orbit が別語になると取りこぼしが増えます。そこで小文字化し、英字以外を空白に置き換えて語を切り出しやすくします。空になった文書や極端に短い文書は検索の役に立たないので落とします。
いっぽう Step 4 の文埋め込みモデルは、大文字小文字や句読点も手がかりにできるため、強い正規化はしません。前処理の程度は手法によって変えるのがふつうです。ここでは TF-IDF 用に軽くそろえたテキストを作ります。
import re
def normalize(text):
text = text.lower()
text = re.sub(r"[^a-z0-9\s]", " ", text) # 英数字以外を空白に
text = re.sub(r"\s+", " ", text).strip() # 連続する空白をまとめる
return text
clean = [normalize(d) for d in docs]
# 短すぎる文書は検索の役に立たないので除く
clean = [d for d in clean if len(d.split()) >= 5]
print("kept:", len(clean))
print(clean[0][:120])
kept: 194
i was wondering if anyone could tell me more about the orbit of the space shuttle and how long it takes to circleこの Step のまとめ
小文字化と記号の除去でテキストをそろえ、短すぎる文書を落としました。ここで整えた clean が TF-IDF 検索の入力になります。埋め込みには元の文をそのまま渡す点に注意してください。
TF-IDF 検索:語の重みで探す
Keyword search with TF-IDF何を・なぜWhat & why
まずキーワード検索を作ります。TF-IDF は、文書中での語の頻度(TF)を、コーパス全体でのめずらしさ(IDF)で重み付けする古典的な特徴量です。ありふれた語の影響を抑え、その文書らしい語を強調します。各文書を語の重みベクトルにし、質問も同じ空間のベクトルに変換して、コサイン類似度が高い順に並べます。コサイン類似度はベクトルの向きの近さを測る指標で、値は0から1の範囲になります。
scikit-learn の TfidfVectorizer がベクトル化を、cosine_similarity が類似度計算を担います。英語のストップワードを除き、2文書未満にしか出ない語(min_df=2)を落として語彙を引き締めます。
ベクトルは L2 正規化され、コサイン類似度=正規化ベクトルの内積になります。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
vectorizer = TfidfVectorizer(stop_words="english", min_df=2)
doc_matrix = vectorizer.fit_transform(clean)
print("tf-idf matrix:", doc_matrix.shape) # (文書数, 語彙数)
def search_tfidf(query, k=3):
q = vectorizer.transform([normalize(query)])
sims = cosine_similarity(q, doc_matrix).ravel()
top = np.argsort(sims)[::-1][:k]
return [(int(i), float(sims[i])) for i in top]
for i, score in search_tfidf("rocket launch into orbit"):
print(f"{score:.3f} {clean[i][:64]}")
tf-idf matrix: (194, 1893)
0.361 the space shuttle uses solid rocket boosters at launch to reach
0.284 a rocket has to reach orbit velocity before the engines cut off
0.192 once in orbit the satellite fires small thrusters to adjust itsTF-IDF ミニ検索
6つの短い英文を対象に、TF-IDF でクエリに近い文を順位付けします(scikit-learn と同じ平滑化つき idf・L2 正規化・コサイン類似度)。スコアの源は語の重なりなので、同義語や言い換えでは0点になりやすいことを体感できます。たとえば「space vehicle」は、宇宙の文ではなく "vehicle" を含む車の文を上位に出します。意味でのマッチはブラウザでは動かせないため、ここは TF-IDF 側だけを示します。
この Step のまとめ
TF-IDF でベクトル化し、コサイン類似度で文書を並べる検索ができました。語がよく一致する文書を確実に、しかも高速に取り出せます。弱点は言い換えです。次は、その弱点を補う意味検索を作ります。
意味検索:文埋め込みで探す
Semantic search with embeddings何を・なぜWhat & why
次に、意味の近さで探す検索を作ります。文埋め込みは、文全体を意味を表す固定長のベクトルに変換したものです。大量のテキストで学習した Transformer 系のモデルを使うと、語が違っても意味が近い文どうしは近くに、意味が遠い文どうしは離れて配置されます。だから「宇宙船」と「ロケット」のように表記が異なっても、意味が近ければ検索で拾えます。
sentence-transformers ライブラリの all-MiniLM-L6-v2 は、軽量で扱いやすい実在の文埋め込みモデルです。各文を384次元のベクトルに変換します。検索の仕組みは TF-IDF と同じで、質問も同じモデルでベクトル化し、cosine_similarity で近い文書を並べるだけです。変わるのは文書を数値にする方法だけという点に注目してください。
# !pip install sentence-transformers
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
model = SentenceTransformer("all-MiniLM-L6-v2")
# 埋め込みには整形前の元の文を渡す
doc_emb = model.encode(docs, batch_size=32, show_progress_bar=True)
print("embeddings:", doc_emb.shape) # (文書数, 384)
def search_embed(query, k=3):
q = model.encode([query])
sims = cosine_similarity(q, doc_emb).ravel()
top = np.argsort(sims)[::-1][:k]
return [(int(i), float(sims[i])) for i in top]
for i, score in search_embed("a spacecraft heading into orbit"):
print(f"{score:.3f} {docs[i][:64].strip()}")
embeddings: (200, 384)
0.671 A rocket has to reach orbit velocity before the engines
0.633 I was wondering if anyone could tell me more about the orbit
0.588 Once in orbit the satellite fires small thrusters to adjust※出力は実行環境・データ取得時期により変わります。
この Step のまとめ
文埋め込みで文書と質問を同じ空間のベクトルにし、コサイン類似度で意味の近い文書を取り出しました。骨組みは TF-IDF 検索とそっくりで、違いはベクトル化の中身だけです。次は、両者が本当に違う結果を出す場面を作って比べます。
比較:言い換えで差が出る
Compare on paraphrases何を・なぜWhat & why
両者の違いをはっきり見るために、小さくて読みやすい文集合を用意し、本文の語と重ならない言い換えの質問を投げます。質問は「a vehicle that travels through space(宇宙を移動する乗り物)」です。この文の語は、宇宙関連の文(rocket, orbit, spacecraft)とほとんど一致しません。TF-IDF はこうした場面で取りこぼし、埋め込みは意味で拾えるはずです。
mini = [
"The rocket lifted off and reached orbit.",
"Astronauts prepared the spacecraft for launch.",
"A sports car with a powerful engine was unveiled.",
"The team won the baseball championship last night.",
]
query = "a vehicle that travels through space"
# TF-IDF 側
tv = TfidfVectorizer(stop_words="english")
M = tv.fit_transform(mini)
tfidf_sims = cosine_similarity(tv.transform([query]), M).ravel()
# 埋め込み側
emb = model.encode(mini)
embed_sims = cosine_similarity(model.encode([query]), emb).ravel()
print("tfidf embed sentence")
for i, s in enumerate(mini):
print(f"{tfidf_sims[i]:.3f} {embed_sims[i]:.3f} {s}")
tfidf embed sentence
0.000 0.487 The rocket lifted off and reached orbit.
0.000 0.502 Astronauts prepared the spacecraft for launch.
0.000 0.264 A sports car with a powerful engine was unveiled.
0.000 0.079 The team won the baseball championship last night.TF-IDF はすべて0点です。質問の語(vehicle, travels, space)がどの文にも現れないため、語の一致では手がかりを得られません。いっぽう埋め込みは、宇宙の2文(ロケット・宇宙船)を高いスコアで、車の文をやや低く、野球の文をさらに低く並べています。語が一致しなくても意味で近さを測れていることが読み取れます。
ただし逆の場面もあります。型番や固有名詞、コードの識別子のように語の完全一致こそが重要な検索では、TF-IDF のほうが素直で確実です。埋め込みは意味が近いだけの無関係な文を拾ってしまうことがあります。どちらが良いかは、探したいものの性質で決まります。
この Step のまとめ
言い換え質問では TF-IDF が取りこぼし、埋め込みが意味で拾いました。反対に完全一致が要る場面では TF-IDF が強い。両者は優劣ではなく、役割が違います。次はコストや使い分けを整理します。
考察:コストと使い分け
Cost & trade-offs読み解きReading the result
二つの検索は、最後は同じコサイン類似度に行き着きます。違いは文書のベクトル化だけでした。TF-IDF は語の統計だけで作れて軽く、結果の理由も「どの語が効いたか」で説明しやすい。埋め込みは学習済みモデルが要り計算も重いぶん、言い換えや同義語に強い。実務では両者を組み合わせ、TF-IDF で候補をしぼってから埋め込みで並べ替える、といった使い方もよくされます。
それぞれの性質Where each shines
TF-IDF が向く場面TF-IDF
- 型番・固有名詞・識別子など、完全一致が効く検索。
- 学習済みモデルを持ち込めない軽量な環境。計算が速く安価。
- スコアの根拠を語で説明したいとき。
埋め込みの注意Embeddings
- ベクトル化にモデルが要り、計算コストが高い。件数が増えると事前計算と保存が必要。
- 意味が近いだけの無関係な文を拾うことがある。
- 学習データの言語や分野に性能が左右される(語彙外・専門語に注意)。
件数が増えると総当りは苦しい
質問ごとに全文書とのコサイン類似度を計算する総当り方式は、文書数が増えると遅くなります。実運用では、次に紹介する近似最近傍(FAISS など)で、精度をほぼ保ったまま探索を高速化するのが定石です。
発展課題Try more
こう変えたら、どうなる?
検索の骨組みができたら、条件を少しずつ変えて挙動を確かめましょう。答えは用意しません。手を動かして、順位やスコアがどう動くかを観察するのが目的です。
別のコーパスで試す
categoriesを増やしたり別の話題に変えたりして、検索の当たり方がどう変わるかを見る。件数を増やすとどうなるかも試す。
別の埋め込みモデル
all-mpnet-base-v2など、より大きな実在モデルに差し替えて、精度と速度のちがいを比べる。多言語モデルで日本語の検索も試せます。
近似最近傍(FAISS)
- 埋め込みを FAISS のインデックスに載せ、総当りではなく近似最近傍で検索する。件数を増やしたときの速度差を測る。
ハイブリッド検索
- TF-IDF のスコアと埋め込みのスコアを重み付けで足し合わせ、完全一致と意味の両取りを狙う。重みを変えて結果の変化を見る。
まとめWrap-up
つないで見えたもの、次へ
この演習では、同じ文書集合に二通りの検索を作り、同じ質問で比べました。振り返ると、それぞれが次の役割を担っていました。
- 前処理と TF-IDF:語の統計だけで、軽く確実なキーワード検索を組んだ。
- 文埋め込み:意味をベクトルにして、言い換えに強い意味検索を組んだ。
- コサイン類似度:二つの検索を同じものさしでつなぎ、違いを文書のベクトル化だけに絞り込んだ。
古典的な特徴量と大規模言語モデル由来の埋め込みが、地続きであることが見えてきました。各技術の詳しい理論やAPIは、下の各モジュールに戻ると確認できます。
使ったモジュール
データ・ライブラリ・参考
- データ:20 Newsgroups(scikit-learn 同梱の取得関数)
- ライブラリ:scikit-learn・sentence-transformers・NumPy
- 参考:sentence-transformers / Hugging Face のドキュメント
次の演習・段階へ
- 次の演習:深層学習の一気通貫(CNN)(演習一覧)
- 埋め込みモデルやクエリを差し替えて、TF-IDF と埋め込みの差が出る場面を自分で探す。
おつかれさまでした
ノートブックを開き、query を自分の言葉に変えて、TF-IDF と埋め込みの順位がどう入れ替わるかを見てください。完全一致が要る質問と、言い換えの質問を交互に投げると、二つの検索の性格の違いがはっきりつかめます。