プログラミング

Embeddingモデルを用いて文をベクトルに変換する

在宅勤務とリモートワークはどちらも同じような意味ですが、文字列の一致度で探すキーワード検索では、片方のワードで検索したときにもう一方のワードは探すことができません。

embeddingは、文章を意味を表す数値の並び(ベクトル)に変換したもので、意味が近い文章どうしはベクトルも近くなります。この特性を用いて意味の近いワードを検索することで、文字列は異なるけれども意味が近い言葉を検索することができます。

この記事では、embeddingがどういうもので、意味の近さをどう数値で測るのかを、実際に文章をベクトル化して近い文を探しながら確認します。なお、アルゴリズムの中身までは踏み込まず、既存のライブラリを使用します。

動作確認

Python 3.12/sentence-transformers 5.6.0/torch 2.13.0/numpy 2.5.1で確認しています。

本文の記述は2026年7月時点のものです。

目次

はじめに

この記事の目的は、文章をベクトルに変換し、文章どうしの意味の近さを数値で出せるようになることです。この技術は、意味で文章を探す意味検索や、その検索を土台にするRAGの中核になります。

前準備として、次のライブラリを使用します。

pip install sentence-transformers

# uvの場合
uv add sentence-transformers

Embeddingとは

embedding(埋め込み)は、テキストを、意味を表す数値ベクトルに変換したものです。ベクトルとは数値の組です。

\begin{equation} \mathbf{x} = [x_1,x_2,\cdots,x_n] \tag{1} \end{equation}

ベクトルを構成する数値の個数は次元といい、上式の場合はn次元と呼びます。

各次元に対応する軸の数値は、「丁寧さ」「話題」のように人間が意味を与えられるものではなく、モデルが学習によって決めた抽象的な値です。したがって、意味は軸を一本ずつ読み取ることではなく、ベクトル全体が空間のどこに位置するか(他のベクトルとの距離や方向)に現れます。

embeddingの一番大事な性質は「意味が近い文章どうしはベクトルも近くなる」ということです。

例えば、「在宅勤務」と「リモートワーク」は、この空間では近くに置かれます。あとは、2つのベクトルの距離を数値で測れれば、意味の近さを評価できます。

文章を埋め込んで意味の近さを測る

モデルを読み込んで1文を埋め込む

embeddingを作るには、テキストをベクトルに変換する埋め込みモデルが必要です。埋め込みモデル自体も大量のテキストを処理することで作成します。古典的な方法は統計処理を用いますが、現在はニューラルネットワークによる学習がよく使われます。

モデルの概念を確かめるために手軽に使えるライブラリとして、sentence-transformersと多言語に対応した小型のモデルparaphrase-multilingual-MiniLM-L12-v2を使います。

モデルを読み込んで、1つの文章をベクトルに変換するには、次のようにします。SentenceTransformerでモデルを読み込むときに、初回だけモデルのダウンロードが行われます。

from sentence_transformers import SentenceTransformer
import numpy as np

# 埋め込みモデルを読み込む
model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")

# 1文をベクトルにする
vec = model.encode("在宅勤務は週3日まで認められています。")
print("次元数:", vec.shape[0])
print("先頭5要素:", np.round(vec[:5], 4))

実行すると、次の結果が得られました。

次元数: 384
先頭5要素: [ 0.0739 -0.3273  0.0347 -0.1958  0.1846]

上記のように、model.encode(文章)と呼ぶことで、その文章のベクトルが返されます。ベクトルの要素数は384個と多いので、最初の5個のみを取り出して、小数点4桁になるように四捨五入した後に表示しています。なお、ベクトルはnumpy.ndarrayで返されます。

ベクトルの近さを測る(コサイン類似度)

ベクトル同士の近さの測り方にはいくつかありますが、RAGで最もよく使われるのはコサイン類似度です。

コサイン類似度は2つのベクトルの成す角度の余弦を計算したものです。

\begin{equation} \cos\theta = \frac{\mathbf{x}_1\cdot\mathbf{x}_2}{|\mathbf{x}_1||\mathbf{x}_2|} \tag{2} \end{equation}

2つのベクトルの向きがどのくらいそろっているかを表す数値で-1から1の値をとります。向きが同じ(意味が近い)ほど1に近く、無関係だと0に近く、反対方向(意味が逆)だと-1に近くなります。

コサイン類似度で文章の意味の近さを評価するコードの例を示します。この例では4つの文章をそれぞれのベクトルに変換して、コサイン類似度を計算してみます。

from sentence_transformers import SentenceTransformer
import numpy as np

def cosine(a, b):
    # コサイン類似度
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

docs = [
    "在宅勤務は週3日まで認められています。", # 文章①
    "リモートワークは週に3回まで可能です。", # 文章②
    "経費精算は毎月20日が締め切りです。", # 文章③
    "会社の休日はカレンダー通りです。", # 文章④
]
model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
emb = model.encode(docs)

print("在宅勤務① vs リモートワーク②:", round(cosine(emb[0], emb[1]), 3))
print("在宅勤務① vs 経費精算③      :", round(cosine(emb[0], emb[2]), 3))
print("在宅勤務① vs 会社の休日④    :", round(cosine(emb[0], emb[3]), 3))

結果は次のようになりました。

在宅勤務① vs リモートワーク②: 0.771
在宅勤務① vs 経費精算③      : 0.165
在宅勤務① vs 会社の休日④    : 0.198

在宅勤務とリモートワークの文章は文字が重なりませんが、コサイン類似度は高い値を示しています。これはキーワード検索ではできなかったことで、「意味が近い=ベクトルが近い」を確認できました。

ベクトルの正規化

コサイン類似度は、ベクトルの内積をそれぞれのベクトルの長さの積で割りますが、あらかじめベクトルの長さを1にしておけば、内積はそのままコサイン類似度と一致します。このように、ベクトルの長さを1にそろえる処理を正規化とよびます。

sentence-transformersでは、encodenormalize_embeddings=Trueを渡すと、長さ1にそろえたベクトルが得られます。

from sentence_transformers import SentenceTransformer
import numpy as np

docs = [
    "在宅勤務は週3日まで認められています。", 
    "リモートワークは週に3回まで可能です。", 
]
model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
emb = model.encode(docs)

emb_n = model.encode(docs, normalize_embeddings=True) # ベクトルの正規化を実施
print("正規化前の長さ:", round(float(np.linalg.norm(emb[0])), 3))
print("正規化後の長さ:", round(float(np.linalg.norm(emb_n[0])), 3))
print("正規化後の内積:", round(float(np.dot(emb_n[0], emb_n[1])), 3))

在宅勤務とリモートワークの2つの文に対して、結果は次のようになりました。正規化後の内積は前回計算したコサイン類似度と一致しています。

正規化前の長さ: 4.153
正規化後の長さ: 1.0
正規化後の内積: 0.771

正規化によってベクトルの長さをそろえておけば、内積という軽い計算で意味の近さを測れるため、ベクトルを大量に保存して高速に検索する場面では、この違いが効いてきます。

モデルによっては最初から正規化された状態でベクトルを返すものもあります。

対称検索と非対称検索

実際の検索では「短い質問文」と「長い文章」のように性質の違うものを比べることが多くなります。性質の似たものを比べることを対称検索とよぶのに対して、性質の違うものを比べることを非対称検索とよびます。モデルによっては、非対称検索向けにフォーマットが指定されている場合もあるため、用途に合ったモデルと使い方を選ぶ上で留意します。

クエリに近い文を探す

意味の近さが測れれば、「質問(クエリ)に一番近い文章を選ぶ」ということができるようになります。クエリを埋め込みモデルでベクトル化して、各文章との類似度を測り、高い順に並べます。

from sentence_transformers import SentenceTransformer
import numpy as np

# モデルの読み込み
model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")

# コサイン類似度
def cosine(a, b):
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

# クエリとの間の類似度の高い順に文章を並べて返す
def search(docs, q_emb, doc_emb, top_k=2):
    scored = [(cosine(q_emb, doc_emb[i]), docs[i]) for i in range(len(docs))]
    scored.sort(reverse=True)  # 類似度が高い順に並べる
    return scored[:top_k]

# クエリの定義
query = "テレワークは何日できますか?"

# 文章の定義
docs = [
    "在宅勤務は週3日まで認められています。",
    "リモートワークは週に3回まで可能です。",
    "経費精算は毎月20日が締め切りです。",
    "会社の休日はカレンダー通りです。",
]

# ベクトル化
q_emb = model.encode(query)
doc_emb = model.encode(docs)

for score, d in search(docs, q_emb, doc_emb):
    print(f"{score:.3f}  {d}")

結果は次のようになりました。

0.606  リモートワークは週に3回まで可能です。
0.473  在宅勤務は週3日まで認められています。

質問(クエリ)はテレワークという文章に含まれていない文字を使用していますが、リモートワークと在宅勤務という類似した意味の文章にヒットしていることを確認できます。

なお、embeddingモデルはLLMと異なり、同じモデルと同じ入力を使用した場合には同じ出力が得られます。

クエリと文章は同じモデルで埋め込む

embeddingのベクトル空間「意味の空間」は、モデルごとに作られ方が異なります。あるモデルが作った空間と別のモデルが作った空間は、座標軸の意味が異なります。

このため、文章をモデルAで埋め込み、クエリをモデルBで埋め込んで類似度を測っても、意味の近さを正しく評価できません。このため、「クエリと文章は同じモデルで埋め込む」ことが必須です。

同じ理由で、後から埋め込みモデルを別のものに変える場合には、保存済みの文章もすべて新しいモデルで埋め込み直す必要があります。古いモデルと新しいモデルのベクトルは、混ぜて比べられないためです。

Embeddingモデルの選択

Embeddingモデルは無料のものから有料のものまで多数公開されています。このため、いくつかの軸で用途に応じてモデルを選択します。

項目内容
精度意図した文章をどれだけ正しく引けるかで最も重要です。最初は公開ベンチマークなどを参考にして、最終的には自分のデータで試して決定します。
対応言語英語中心のモデルにするか、多言語・日本語に対応したモデルにするかなどです。
最大入力長一度に埋め込めるトークン数の上限です。長い文章を1つのベクトルにしたい場合に効きます。これを超える文章は、分割して埋め込むことになります。
次元数ベクトルの要素数です。大きいほど表現力が上がりやすい一方で、保存に必要な容量が増え、検索も重くなります。
コストAPI型は入力トークン量に応じて課金され、セルフホスト型はGPUなどの自前のインフラ費用がかかります。
ライセンスオープンなモデルは自分の環境で動かせますが、商用利用の可否はライセンスによります。なお、今回使ったparaphrase-multilingual-MiniLM-L12-v2はApache-2.0で、商用利用できます。
API型/セルフホスト型APIを呼ぶだけの手軽さを採るか、自前で動かしてデータを外に出さないことや大量処理時のコストを抑えるかのトレードオフです。

その他、次のような機能をもっているかもモデル選択時の参考になります。

項目内容
MRL
(Matryoshka Representation Learning)
ベクトルの次元の切り詰めを行う仕組みです。必要に応じてベクトルの一部を使うなどです。保存コストと検索速度を調整するために使います。
マルチモーダルembeddingテキストだけではなく、画像なども同じベクトル空間に埋め込む技術です。画像も意味検索の対象にしたいときに用います。

モデルは次節の公開ベンチマークを参考にして選ぶのがよいと思います。

モデルの公開ベンチマーク

モデルが多くて選べない場合に、比較の出発点になるのが公開ベンチマークです。

代表的なものにMTEB(Massive Text Embedding Benchmark)と、その多言語版のMMTEB(Massive Multilingual Text Embedding Benchmark)があります。

これらは、多数のモデルをさまざまなタスクで評価し、スコアを一覧で比べられるようにしたもので、Hugging Faceのリーダーボードで公開されています。

実際のモデルは、次のリーダーボードで調べられます。ページ上でMTEB(英語)とMMTEB(多言語)を切り替えられ、スコアだけでなく次元数・最大入力長・ライセンス・モデルの種類などで見比べられます。

https://huggingface.co/spaces/mteb/leaderboard

順位の高いものが自分のデータに対して最もよいとは限らないため、ランキング上位のものから候補をいくつか選んで、自分のデータと質問で検索の精度を測って決めるのがよいと思います。

まとめ

embeddingがどういうものであり、意味の近さをどのように数値で測るかを、既存のモデルを実際に動かしながら確かめました。要点を以下にまとめます。

  • embeddingは、テキストを意味を表す数値ベクトルに変換したもので、その数値の個数を次元と呼びます。
  • 一番大事な性質は「意味が近い文章どうしはベクトルも近くなる」ことです。
  • ベクトルの近さは、コサイン類似度がよく使われます。正規化した場合は内積と一致します。
  • クエリと文書は必ず同じモデルで埋め込みます。モデルを変えたら、保存済みの文書も埋め込み直す必要があります。
  • モデル候補の絞り込みにはMTEB・MMTEBを使い、最終判断は自分のデータで行います。

参考資料

参考記事

-プログラミング
-,