multilingual-e5で文章をベクトル化して意味の近さを測る

はじめに

文章を「意味を表す数値のベクトル」に変換する技術を**文埋め込み(sentence embedding)**と呼びます。 今回は、その中でも今のKaggle上位解法で定番になっている multilingual-e5 というモデルを使って、日本語の文章をベクトル化し、意味の近さを測ってみます。

このブログには以前、TF-IDFを使ったテキストベクトル化pythonによるコサイン類似度の計算という記事を書きました。今回はその正統進化版という位置づけです。

文埋め込みとは(TF-IDFとの違い)

コンピュータは文字そのものの意味を理解できないので、文章を数値ベクトルに変換する必要があります。 以前紹介したTF-IDFも「文章をベクトルにする」手法の一つですが、TF-IDFは基本的にどの単語が何回出たかを数える方式です。そのため、

  • 単語が違うと、意味が同じでも「似ていない」と判定されがち(同義語・言い換えに弱い)
  • 語順や文脈を考慮しない

という弱点がありました。

一方、multilingual-e5のような文埋め込みモデルは、大量のテキストで事前学習された深層学習モデルです。意味や文脈ごとベクトルに落とし込むので、単語が被っていなくても意味が近ければベクトルも近くなります。

そしてベクトル化したあとは、以前の記事と同じくコサイン類似度で「近さ」を測ります。流れはこうです。

文A → [0.12, -0.34, ...] ┐
                          ├→ コサイン類似度で近さを測る
文B → [0.11, -0.30, ...] ┘

multilingual-e5 とは

multilingual-e5はMicrosoftが公開している文埋め込みモデルE5の多言語版で、**約100言語(日本語を含む)**を1つのモデルで扱えます。 HuggingFaceでintfloat/multilingual-e5-small / base / largeの3サイズが公開されており、今回は手軽なsmallを使います。

特徴的な作法として、入力文には**query:(検索クエリ側)またはpassage:(文書側)**という接頭辞を付けて使います。ここは後で説明します。

環境

今回のバージョンは以下です。

sentence-transformers 5.6.0
torch 2.11.0

インストールはsentence-transformersを入れるだけでOKです(transformersなども一緒に入ります)。

$ pip install sentence-transformers

モデルを読み込んで文章をベクトル化する

sentence-transformersを使うと、モデルの読み込みからベクトル化まで数行で書けます。 初回はモデルが自動でダウンロードされます(smallは約470MB)。

今回は意味の関係がわかりやすいように、以下の5文を用意しました。 1・2は「猫がくつろぐ」、3・4は「AIを学習させる」で言い換えのペア、5は無関係な天気の文です。

from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer("intfloat/multilingual-e5-small")

sentences = [
    "猫がソファでくつろいでいる",
    "ネコがカウチで寝ている",
    "機械学習のモデルを学習させる",
    "AIのアルゴリズムをトレーニングする",
    "今日の東京の天気は晴れです",
]

# e5 は文書側に passage: の接頭辞を付ける作法
inputs = ["passage: " + s for s in sentences]
emb = model.encode(inputs, normalize_embeddings=True)

print(emb.shape)   # => (5, 384)

normalize_embeddings=Trueにしておくと、ベクトルの長さが1に揃います。 こうしておくと、あとで内積を取るだけでコサイン類似度になるので便利です(正規化済みベクトルの内積 = コサイン類似度)。

smallモデルの出力は384次元のベクトルでした。

類似度を計算してみる

正規化済みなので、行列の積を取るだけで全文ペアのコサイン類似度が一気に求まります。

sim = emb @ emb.T   # 内積 = コサイン類似度

np.set_printoptions(precision=3, suppress=True)
print(sim)
[[1.    0.914 0.845 0.852 0.843]
 [0.914 1.    0.835 0.839 0.855]
 [0.845 0.835 1.    0.941 0.867]
 [0.852 0.839 0.941 1.    0.863]
 [0.843 0.855 0.867 0.863 1.   ]]

ヒートマップにするとわかりやすいです。

multilingual-e5

注目してほしいのは、

  • 「猫がソファ…」と「ネコがカウチ…」→ 0.91
  • 「機械学習…学習…」と「AI…トレーニング…」→ 0.94

の2ペア。単語がほとんど被っていないのに、高い類似度になっています。 「機械学習」と「AI」、「学習」と「トレーニング」という言い換えを、ちゃんと「近い意味」として捉えられているわけです。TF-IDFではこうはいきません。

なお、e5は全体的に類似度が0.8以上の高い範囲に固まる傾向があります。絶対値そのものより、ペア間の相対的な差(言い換えペアだけ0.9台に飛び出ている点)を見るのがコツです。

意味で検索する(query: と passage:)

最後に、文埋め込みの代表的な使い道である意味検索をやってみます。 検索する側の文(クエリ)にはquery:、検索される側の文書にはpassage:の接頭辞を付けるのがe5の作法です。

query = "ニューラルネットワークの訓練方法"

q_emb = model.encode(["query: " + query], normalize_embeddings=True)[0]
scores = emb @ q_emb          # 各文書との類似度
order = np.argsort(-scores)   # 高い順に並べる

for r in order:
    print(f"{scores[r]:.3f}  {sentences[r]}")
0.840  AIのアルゴリズムをトレーニングする
0.833  機械学習のモデルを学習させる
0.778  猫がソファでくつろいでいる
0.765  ネコがカウチで寝ている
0.764  今日の東京の天気は晴れです

「ニューラルネットワークの訓練方法」というクエリに対して、その単語が1つも含まれていない「AI…トレーニング」「機械学習…学習」の2文がちゃんと上位に来ました。 キーワードの一致ではなく、意味で検索できているのがわかります。RAGの検索部分など、応用範囲はとても広いです。

まとめ

  • multilingual-e5は日本語を含む多言語対応の文埋め込みモデルで、sentence-transformersで数行から使える
  • 単語の一致ではなく意味の近さを捉えるので、TF-IDFが苦手だった同義語・言い換えに強い
  • normalize_embeddings=Trueにしておくと、内積=コサイン類似度で近さを測れる
  • 検索側はquery:、文書側はpassage:の接頭辞を付けるのがe5の作法
  • 類似度の絶対値は高めに固まるので、相対差で判断するのがコツ

事前学習済みモデルを落としてくるだけで、これだけ「意味」を扱えるのは強力です。以前のTF-IDFコサイン類似度の記事と合わせて、テキストのベクトル化の引き出しにぜひ加えてみてください。

参考リンク