📙

テキストエンベディング/類似検索の比較

2025/02/04に公開

日本語対応のテキストエンベディングモデル

文章の類似性を見るためにテキストの埋め込みベクトルを利用するというシーンは多々あると思いますが、ふと「日本語StaticEmbeddingモデルがすごそう」という情報を聞いたので実際に類似文章検索を試してみます。

日本語モデルの記事はこちら。
https://secon.dev/entry/2025/01/21/060000-static-embedding-japanese/
↑こちらの記事によると

  • CPUでもかなり高速に実行できる
  • 性能としてもTransformer系のモデルの最高性能には及ばないものの、実用的なレベル

とのことで、期待のモデルです。

今回は、上記の記事にも出ているE5モデルと速度や類似検索性能を比較していきます。

エンコード速度

まず、エンコード速度を比較します。
比較は、

  • E5の多言語版
    • multilingual-e5-small(ベクトル長384)
    • multilingual-e5-large(ベクトル長1024)
  • 日本語StaticEmbeddingモデル ※下の表ではse-jpと略してます
    • ベクトル長1024で取得(デフォルト)
    • ベクトル長384で取得

の4つです。

E5はHugginfFaceページはこちら:
https://huggingface.co/intfloat/multilingual-e5-small

入力している文章は、RSSで配信されているニュースなどのタイトルを保存していたものが手元にあったのでそういうデータです。文字長としては平均50文字程度。

データ数 E5-small(秒) E5-large(秒) se-jp_384(秒) se-jp_1024(秒)
10 1.537 6.883 0.020 0.370
100 11.103 (72.813) 0.035 0.817
1,000 (109.59) (1057.17) 0.846 1.164
10,000 (570.36) - 3.880 5.791

カッコ付きのものは一気には処理はできず、分割処理した合計値。

本当にめちゃくちゃ速いな・・・。

一応フォロー?ですが、E5-smallもCPUでかなり高速に処理できてなかなかに精度が良い!というタイプのモデルです。。。

メモリ使用量

速度を試していてメモリの使用の仕方が少し気になりました。
速度の表で、カッコ付きの部分がありますが、これはメモリ容量が足りなくなって一気に処理できなかったものです。

メモリの使用量を調べてみました。
まず、static-embedding-japaneseに10,000行のデータを入れた場合のメモリ使用量推移はこちら。

モデルのロード等で500~700MB程度使用し、その後の処理で徐々に増えて、トータル1.5GBぐらいのメモリを使用したようです。

対して、E5-smallに300行のデータを入れた場合、

処理を始めるとガンガンメモリが消費され、8GBを超えていったところでColab上では実行不可となりました。
分割して処理して後で結合するようにすると問題なかったので、利用する場合は一定の長さで分割するようにしておくと良さそう。

結果としては、ある程度の量のデータを一気に流し込んでも日本語StaticEmbeddingはメモリそこまで多く食わずに処理できています。すごい。

メモリ使用量調査について

メモリ使用量を調べるのにいいツールないかと調べていたら、memrayというのを見つけたので使ってみました。
上に張ってあるグラフはこれを使って出力したものです。
https://github.com/bloomberg/memray

使い方や紹介の記事:
https://gihyo.jp/article/2023/06/monthly-python-2306

今回は以下のようなシンプルにエンコードするだけのプログラムを用意しました。

se-jp.pyという名前でファイルを作ったとして...

import time
import pandas as pd

from sentence_transformers import SentenceTransformer

# static embedding japanese
model_name = "hotchpotch/static-embedding-japanese"
model = SentenceTransformer(model_name, device="cpu")

# 何らかのデータがあるとします。
rss = pd.read_csv("rss.tsv.gz", sep='\t', dtype=str)

size = 10000

inp = rss["title"].tolist()[:size]
inp = [str(tmp) for tmp in inp] #たまに数字が混ざってたので文字に変換してるだけ

start_time = time.time()  # 実行開始時間を記録

vec = model.encode(inp)

end_time = time.time()  # 実行終了時間を記録
execution_time = end_time - start_time

print(f"StaticEmbeddingJapanese: サイズ:{len(inp)} 実行時間{execution_time:.3f}秒")

time()で時間計測している部分は秒数を表示するためだけのものなので、なくてもmemrayに影響ありません。

このプログラムをコマンドラインから実行します。

# 実行&計測
!memray run se-jp.py
# 実行が終わるとbinファイルが生成されます

# 結果をHTML化
!memray flamegraph memray-se-jp.py.00000.bin

このようにするだけでメモリの状況をビジュアライズできます。

HTMLにするだけでなく、リアルタイムでモニタリングしたり、より詳細にどの処理がメモリ食ってるかも見たりできるようなのでご興味のある方はどうぞ。

類似文章検索

duckdbを使った検索の準備

ではベクトル化できたので、ベクトル検索を使って類似文章を探してみます。
なお、ベクトル検索の実装はduckDB使うのが最速だと思いますので今回もそれで。

サンプルコードを載せておきます。
dfというデータフレームにベクトルが入っているとします。

target se_vector e5_vector
0 テキスト... [-0.174996390, 0.44765535, -0.... [0.184379901, -0.039089251, -...
import duckdb

con = duckdb.connect()
con.install_extension("vss")
con.load_extension("vss")

# dfからduckDBのduck_mainにデータを登録
con.sql("drop table IF EXISTS duck_main;")
con.sql("create table duck_main AS SELECT * FROM df;")

for c in ['se_vector', 'e5_vector']:
  # ベクトル長取得
  vec_len = len(df[c][0]) #ベクトル長を何らか手段で取得
  # index作る
  con.sql(f"DROP INDEX IF EXISTS idx_{c};")
  con.sql(f"ALTER TABLE duck_main ALTER {c} TYPE FLOAT[{vec_len}]")

for c in ['se_vector', 'e5_vector']:
  con.sql(f"CREATE INDEX idx_{c} ON duck_main USING HNSW ({c});")

検索するのも簡単!

vec = model.encode(query).tolist()

con.sql(f"""SELECT *, array_distance(se_vector, {vec}::FLOAT[1024]) as dist
FROM duck_main
ORDER BY array_distance(se_vector, {vec}::FLOAT[1024])
LIMIT 10;""").df()

実験

いくつか検索ワードを与えてみて、E5-smallと日本語StaticEmbedding(1024次元)それぞれの応答を見てみます。
適当なデータなので、似たようなタイトルもありますがあえてそのままにします(割とモデルによって差が出たので)

  • query: "フリーランスの給料事情"

E5-small

順番 本文 距離
0 職種別「ITフリーランスの平均月額単価ランキング」 2位は「プロマネ」約77万円 1位は ... 2.162377
1 職種別「ITフリーランスの平均月額単価ランキング」 2位は「プロマネ」約 77万円 1位は ... 2.168080
2 職種別「ITフリーランスの平均月額単価ランキング」 2位は「プロマネ」約77万円 1位は……... 2.289336
3 そんな給料で働けるか、バーカ! 2.327567
4 フリーランス“C#エンジニア”の平均年収「903万円」 高いか安いか 2.379486

se-jp

順番 本文 距離
0 職種別「ITフリーランスの平均月額単価ランキング」 2位は「プロマネ」約 77万円 1位は ... 59.828434
1 理由の1位は「めんどくさい」… ITフリーランスの44%が過去2年間健康診断を受けず 60.606678
2 副業に「<b>生成AI</b>を利用している」人は約4割…副業での年間収入は利用していない人... 61.877949
3 職種別「ITフリーランスの平均月額単価ランキング」 2位は「プロマネ」約77万円 1位は……... 62.108547
4 「俺、転職したら<b>生成AI</b>で仕事するんだ」 パーソルキャリアが転職活動に関する調... 62.241253

E5の方は上3つに類似の記事が固まっているのに対して、se-jpの方はバラけています。
E5の方がより意味合いでの検索や表記ゆれに強そうに見えます。

  • query: "スタートアップ投資動向"

E5-small

順番 本文 距離
0 AIスタートアップニュースレター 2.126278
1 <b>AI</b>スタートアップ米投資は全体の35%に(CB調査)——グローバルスタートアッ... 2.176250
2 生成AI スタートアップ企業に大手企業から出資相次ぐ - nhk.or.jp 2.201689
3 ディープテックとレイターステージを強化する スタートアップ育成5ヵ年計画のこれから 2.205705
4 2024年 <b>AI</b> 関連企業への米投資は全体の35%に——グローバルスタートアッ... 2.215788

se-jp

順番 本文 距離
0 ETF投資は株式投資で有効か 60.969990
1 積み立て投資でリターンを最大化したいなら…“経費率の低い”投資信託はコレ! - MONEY ... 64.057198
2 知らないと損する! 投資のきほんと心得 ~リスクを抑えて賢く運用 64.582069
3 動画<b>生成AI</b>領域で新興スタートアップ続々 HedraやCaptionなどの動向... 64.893890
4 動画<b>生成 AI</b>領域で新興スタートアップ続々、Hedraや Captionなどの... 65.250267

"投資"に引っ張られ度合いが違いそう。

  • query: "AIは人間の仕事を奪うのか?"

E5-small

順番 本文 距離
0 AI があなたの仕事を奪う 1.808731
1 AIがあなたの仕事を奪う可能性は50/50 - ARAB NEWS 1.878756
2 <b>AI</b>は人間の仕事を奪うのか、現実は既に動き出している - Bloomberg 1.898820
3 AIに仕事を奪われる前に:生成AIを使うべき理由 1.925818
4 8月30日 AIが技術者の仕事を奪うのはもう少し先 1.930183

se-jp

順番 本文 距離
0 AI があなたの仕事を奪う 17.089600
1 AI が人間を超えることはない 19.527472
2 8月30日 AIが技術者の仕事を奪うのはもう少し先 21.501383
3 ある日 AI が来て、仕事がなくなった 02 23.180807
4 AIに仕事を奪われそうなライターが、AI研究者にとことん聞いてみた 23.434408

数値的な評価ではなく、あくまで主観的評価ですが、E5の方が類似の意味を持つ文章を見つける能力が高いように感じます。
また、他のいろいろなクエリで遊んでみましたが、両者とも最も意味合いの近い1位のものはしっかり選べていると感じるのに対して2位以下ではE5の方がかなり納得感があると感じました。

まとめ

日本語StaticEmbeddingは、体感の性能としては若干E5に劣るものの、超高速に埋め込みベクトルが得られるのは非常に嬉しいポイントです。
これまではサクっと文章を処理したいときはまずE5を使ってましたが、これからはこっち使おうかなと思っている程度には魅力を感じております。

NaviPlusテックブログ

Discussion