埋め込み

ここでいう埋め込み(embedding)とは、単語や文章を意味空間のベクトルとして表すことである。

2025年9月にGoogleがEmbeddingGemma(エンベッディング・ジェンマ)というオープンな埋め込みモデルを公開した(Introducing EmbeddingGemma: The Best-in-Class Open Model for On-Device Embeddings 参照)。ここではそれを使って埋め込みの実験をしてみよう。

モデルは Hugging Face で公開されている。4ビット量子化されたものはLM Studio用としても公開されているが、ここでは元の32ビット版 google/embeddinggemma-300m を試す。パラメータ数300M(3億)の小さいモデルなので、32ビットでもファイルサイズは1.2Gバイトだ(300M×4=1.2G)。あらかじめ google/embeddinggemma-300m をざっと読んで、(まだの場合は)使用許諾に了承しておく。

PyTorch はインストールされていると仮定する。sentence-transformers というパッケージを使う。あらかじめpipでインストールしておく。

次のコードを実行すれば、初回にモデルが自動でダウンロードされるはずである。device"cuda""cpu""mps""npu" を指定する。Macなら "mps" だ。

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("google/embeddinggemma-300m", device="mps")

モデルは ~/.cache/huggingface/hub の中に入る。

さっそくいくつかの語をエンコードしてみよう。

words = ["man", "woman", "king", "queen"]
embeddings = model.encode(words)
print(embeddings.shape)  # (4, 768)

これを見てわかるように、各単語は768次元空間に埋め込まれる。

embeddings[0][:10]
array([-0.21072112, -0.00405205,  0.0298054 , -0.00759131,  0.02110349,
        0.0053321 , -0.03235268,  0.04000835,  0.02122785, -0.05387658],
      dtype=float32)
[(e ** 2).sum() for e in embeddings]
[np.float32(1.0000001),
 np.float32(0.99999994),
 np.float32(1.0),
 np.float32(0.9999999)]

コサイン類似度を求める。

model.similarity(embeddings, embeddings)
tensor([[1.0000, 0.9541, 0.9545, 0.9396],
        [0.9541, 1.0000, 0.9476, 0.9474],
        [0.9545, 0.9476, 1.0000, 0.9715],
        [0.9396, 0.9474, 0.9715, 1.0000]])

さて、昔の word2vec というモデルでは woman - man ≒ queen - king のような線形の関係があることが知られている。これが今のモデルでも成り立つか。試してみよう。

model.similarity(embeddings[1] - embeddings[0], embeddings[3] - embeddings[2])
tensor([[0.2036]])
model.similarity(embeddings[2] + embeddings[1] - embeddings[0], embeddings)
tensor([[0.8752, 0.9569, 0.9565, 0.9433]])

あれ? king + woman - man は queen よりむしろ woman や king に近い?!

念のため、別の指標でもやってみよう。model.similarity_fn_name = "euclidean" とすればユークリッド距離になる。model.similarity_fn_name には "cosine""dot""euclidean""manhattan" が設定できる。

model.similarity_fn_name = "euclidean"
model.similarity(embeddings, embeddings)
tensor([[-0.0000, -0.3028, -0.3016, -0.3476],
        [-0.3028, -0.0000, -0.3237, -0.3244],
        [-0.3016, -0.3237, -0.0000, -0.2386],
        [-0.3476, -0.3244, -0.2386, -0.0000]])
model.similarity(embeddings[1] - embeddings[0], embeddings[3] - embeddings[2])
tensor([[-0.3453]])
model.similarity(embeddings[2] + embeddings[1] - embeddings[0], embeddings)
tensor([[-0.5104, -0.3016, -0.3028, -0.3453]])

なぜかユークリッド距離はすべて負になるようだが、絶対値は合っている。今度は king + woman - man は queen より man に近い!

ということで、さらに調べてみると、実は word2vec でも king - man + woman は queen より king に近いし、他の例についても必ずしも宣伝されているような関係が成り立つわけではなさそうだ。King - Man + Woman = King ? という記事を参照。(追記)自分でも試してみた→ word2vec

ただ、主成分分析(PCA)をしてみれば、何となくman、woman、king、queenが意味的な関係に近い位置にあることがわかる。

import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

x = PCA(n_components=2).fit_transform(embeddings)
plt.scatter(x[:, 0], x[:, 1])
for i, s in enumerate(["man", "woman", "king", "queen"]):
    plt.text(x[i, 0], x[i, 1], s)
plt.axis("scaled")
man、woman、king、queenをEmbeddingGemmaで埋め込んだベクトルをPCAで2次元にしたもの

さて、現代の埋め込みモデルの主な用途はRAGであろう。具体的には、ユーザが入力したクエリに最も関連の深い文書を見つけることだ。

そのためには、次のようなコードを使うことが推奨される。

documents = [
    "文書1",
    "文書2",
    "文書3",
    "文書4"
]
document_embeddings = model.encode_document(documents)

query = "ユーザのクエリ"
query_embeddings = model.encode_query(query)

similarities = model.similarity(query_embeddings, document_embeddings)
print(similarities)

クエリや文書の長さの上限はEmbeddingGemmaでは2048トークンである。