「jina-embeddings-v4」を試す
気づかずスルーしていた・・・なお、翻訳はGrokによるもの。
今日、jina-embeddings-v4をリリースします。これは、テキスト、画像、ビジュアルドキュメント、コードの取得を目的とした新しい38億パラメータの汎用埋め込みモデルです。V4は、MTEB、MMTEB、CoIR、LongEmbed、STS、Jina-VDR、CLIP、ViDoReベンチマークにおいて、マルチモーダルおよび多言語タスクで最先端の検索パフォーマンスを達成しています。特に、テーブル、チャート、ダイアグラム、またはそれらの混合物などのビジュアル的にリッチなコンテンツの処理に優れています。このモデルは、シングルベクターおよびマルチベクター埋め込みの両方をサポートしています。
jina-embeddings-v4は、これまでで最も野心的なエンベディングモデルです。オープンソースモデルとして、v4は主要プロバイダーの主要なクローズドソースエンベディングモデルを上回り、多言語検索ではOpenAIのtext-embedding-3-largeよりも12%優れたパフォーマンス(66.49対59.27)、長編ドキュメントタスクでは28%の改善(67.11対52.42)、コード検索ではvoyage-3よりも15%優れ(71.59対67.23)、テキスト検索ではGoogleのgemini-embeddingと同等です。これにより、v4は現在利用可能な最も高性能なオープンソースの汎用エンベディングモデルとなり、研究者や開発者に、トレーニングプロセス、アーキテクチャの決定、モデルウェイトに関する完全な透明性を備えたエンタープライズ級のマルチモーダルエンベディング機能を提供します。詳細は、包括的な技術レポートを通じて確認できます。
jina-embeddings-v4は、Qwen2.5-VL-3B-Instructを基盤に構築されています。テキストと画像の入力は共有パスウェイを通じて処理されます:画像はまずビジョンエンコーダを介してトークンシーケンスに変換され、その後、両方のモダリティがデコーダによって共同で処理されます。3つのタスク特化型LoRAアダプター(各60Mパラメータ)は、検索、テキストマッチング、コード検索タスクのための特別な最適化を提供します。このアーキテクチャは、2つの出力モードをサポートします:(1)効率的な類似性検索のための単一ベクトル埋め込み(2048次元、MRLで128に切り詰め可能)で、平均プーリングを介して行われ、(2)後期相互作用スタイルの検索のためのマルチベクトル埋め込み(トークンごとに128次元)で、投影層を介して行われます。
v4のトレーニング、デザイン、ベンチマークについて詳しく知る
https://arxiv.org/abs/2506.18902 今日、Search Foundation APIまたはHugging Face🤗で試してみてください https://huggingface.co/jinaai/jina-embeddings-v4 ご意見をお聞かせください。
クリシェでは、量子化がパフォーマンスを損なうとされています—品質とスペースをトレードオフしなければならないというものです。実際は?スキル次第です。量子化対応トレーニング(QAT)を使用してjina-embeddings-v4の量子化バージョンをどのようにトレーニングしたかを学びましょう。この方法では、モデルが丸め処理に適応するように学習し、それに抗うことなく動作します。QATは量子化の制約を直接トレーニングに組み込み、モデルが量子化の影響に適応し、補償できるようにします。これには、低ランクQAT(LR-QAT)が含まれ、ウェイトとアクティベーションの冗長性を削減します。また、EfQATのような効率的なフレームワークは、トレーニングを最適化し、計算オーバーヘッドを抑えながらほぼ完全な精度を実現します。
量子化は正則化の役割を果たし、モデルが訓練データに過剰適合するのを防ぐことができます。モデルを低精度で動作させることにより、QATは一般化性能と堅牢性を向上させることができます。
https://jina.ai/news/quantization-aware-training-of-jina-embeddings-v4/ では、QATモデルが特定のタスクにおいてフル精度のベースラインを上回る場合があることを示しています。
jina-embeddings-v4-GGUFがさまざまな量子化で登場しました
https://github.com/jina-ai/jina-embeddings-v4-gguf Unslothのような動的量子化も近日公開予定です。
とりあえず情報モリモリなので、Dia にまとめてもらった
- jina-embeddings-v4は3.8Bパラメータのユニバーサル埋め込みモデルで、テキスト、画像、ビジュアルドキュメント、コードなど多様なデータを高精度で扱える
- マルチモーダル&マルチリンガル対応で、画像もテキストも多言語も一つのモデルで処理できる
- MTEB、MMTEB、CoIR、LongEmbed、STS、Jina-VDR、CLIP、ViDoReなど、複数のベンチマークで最先端の検索性能を達成している
- 特に表、グラフ、図、ミックス系のビジュアル情報の処理が得意
- シングルベクトル(2048次元、MRLで128次元まで切り詰め可能)とマルチベクトル(トークンごとに128次元)の両方の出力モードに対応している
- Qwen2.5-VL-3B-Instructをベースに、画像はビジョンエンコーダでトークン列に変換し、テキストと共通経路でデコーダ処理する
- 検索、テキストマッチング、コード検索それぞれに特化した3つのLoRAアダプタ(各6000万パラメータ)を搭載している
- オープンソース(ただし商用利用は不可)で、トレーニング過程や設計、重みなどの詳細も技術レポートで公開されている
- OpenAIのtext-embedding-3-largeより多言語検索で12%高性能(66.49 vs 59.27)、長文ドキュメントタスクで28%高性能(67.11 vs 52.42)、コード検索でvoyage-3より15%高性能(71.59 vs 67.23)、Googleのgemini-embeddingと同等のテキスト検索性能を持つ
- Search Foundation APIやHugging Faceで即利用可能で、フィードバックも歓迎している
- 量子化(quantization)にも注力し、Quantization-Aware Training(QAT)やLow-Rank QAT(LR-QAT)、EfQATなどを活用して、ほぼフル精度に近い性能と効率化を両立している
- 量子化は正則化(regularizer)としても機能し、モデルが訓練データに過剰適合(オーバーフィット)するのを防ぎ、汎化性能やロバスト性も向上させている。QATモデルは場合によってはフル精度モデルより良い結果を出すこともある
- jina-embeddings-v4-GGUFとして、GGUF形式で複数の量子化バージョンをGitHubで配布中。今後Unsloth系のダイナミック量子化も追加予定
ライセンスはちょっと残念だけど、色々便利に使えそうなので試してみる。
上にある通り、3つのLoRAアダプタが用意されていて、transformers / sentence-transformers で使う場合はタスクを切り替えて使うことができる。(なお、Jina AI Embeddings API も同様)
vLLM / GGUF(llama.cpp)の場合にはタスクごとに別々にモデルが提供されている。
vLLM向け: 検索
vLLM向け: テキストマッチング
vLLM向け: コード検索
GGUF向け: 検索
GGUF向け: テキストマッチング
GGUF向け: コード検索
Colaboratory L4で。パッケージ要件は以下。
transformers>=4.52.0
torch>=2.6.0
peft>=0.15.2
torchvision
pillow
自分が確認した限りはColaboratory L4のデフォルトで全部満たしているように思える。
!pip freeze | egrep "transformers|torch|peft|pillow"
peft==0.16.0
pillow==11.2.1
sentence-transformers==4.1.0
torch @ https://download.pytorch.org/whl/cu124/torch-2.6.0%2Bcu124-cp311-cp311-linux_x86_64.whl
torchao==0.10.0
torchaudio @ https://download.pytorch.org/whl/cu124/torchaudio-2.6.0%2Bcu124-cp311-cp311-linux_x86_64.whl
torchdata==0.11.0
torchsummary==1.5.1
torchtune==0.6.1
torchvision @ https://download.pytorch.org/whl/cu124/torchvision-0.21.0%2Bcu124-cp311-cp311-linux_x86_64.whl
transformers==4.53.2
あと、以下がオプションで推奨されていた。必須ではないが、ここで足りないのは flash-attention
のみ。
flash-attention
sentence-transformers
必須ではないが推論速度や効率が向上するので flash-attention
をインストール(ランタイムL4を使用したのはこのため。T4はアーキテクチャが新しすぎて今でも対応していないんじゃなかろうか。)ただ、今だと flash-attn==2.8
がインストールされるのだが、これがColaboratoryだとundefined symbol
になってしまう(原因はわかってないが、2.8.xだと必ずエラーになる)ので、2.7.xの最新を入れている。
!pip install "flash-attn==2.7.4.post1" --no-build-isolation
モデルロード
from transformers import AutoModel
import torch
# モデルの初期化
model = AutoModel.from_pretrained(
"jinaai/jina-embeddings-v4",
trust_remote_code=True,
torch_dtype=torch.float16
)
model.to("cuda")
VRAM消費は 8.3GBほど。
Fri Jul 18 14:09:01 2025
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.54.15 Driver Version: 550.54.15 CUDA Version: 12.4 |
|-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 NVIDIA L4 Off | 00000000:00:03.0 Off | 0 |
| N/A 53C P0 30W / 72W | 8271MiB / 23034MiB | 31% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
ここからは各タスクを見ていく。
検索
検索の場合は以下のパラメータを付与する。
-
task="retrieval"
を指定 - embeddings生成時に
prompt_name
を指定。クエリと文書で異なる。- クエリ:
prompt_name="query"
- 文書:
prompt_name="passage"
- クエリ:
passages = [
"木の温もりあふれるブックカフェで、自家焙煎の深煎りコーヒーと季節のタルトを味わいながら、窓辺から路面電車をのんびり眺められるんだ。",
"庭にハーブが茂るガーデンカフェでは、ハンドドリップの浅煎りとフレッシュハーブティーが選べて、小鳥のさえずりが BGM 代わりになるよ。",
"港直送の鯖を炙りしめ鯖にしてくれる専門店、皮目の香ばしさと酢のきりりとした酸味が口いっぱいに広がるんだ。",
"カウンター割烹の金目鯛の煮付けは、甘辛ダレが骨の隅々まで染みていて、白ご飯が思わずおかわり必至だよね。",
"昔ながらの屋台ラーメンは鶏ガラの澄んだ醤油スープと細ちぢれ麺が相性抜群で、深夜の胃袋にしみるんだ。",
"真っ白な豚骨スープに焦がしニンニク油をひと垂らしした濃厚ラーメン、替え玉が無料でつい無限ループしてしまうよ。",
"スリランカ式の混ぜて食べるプレートカレーでは、15種類のスパイスが複雑に重なって食べ進めるほど香りが花開くんだ。",
"野菜がごろごろ入った欧風ビーフカレーは、赤ワインとバターのコクが効いたシャバっとルウで後を引くよ。",
"薪窯ナポリピッツァのマルゲリータは、モッツァレラがびよーんと伸びて焼き立てを頬張る瞬間がたまらない。",
"4種のチーズをのせたクアトロフォルマッジに蜂蜜を垂らすスタイルが人気で、塩気と甘さのコントラストがクセになるんだ。",
"しゅわっととろけるバスクチーズケーキ専門店、表面の香ばしい焦げと濃厚クリーミーな中身のギャップが病みつきになるよ。",
"パリパリの薄皮たい焼きは羽根つきで端っこまで香ばしく、黒あんか白あんか毎回真剣に迷っちゃうんだよね。",
]
# passageのembeddingsを生成
passage_embeddings = model.encode_text(
texts=passages,
task="retrieval",
prompt_name="passage",
)
# クエリのembeddingsを生成
query_embedding = model.encode_text(
texts=["中華そばのオススメを教えて。"],
task="retrieval",
prompt_name="query",
)[0]
検索。コサイン類似度を求める。Diaに書いてもらった。
import torch
import torch.nn.functional as F
passage_embeddings_tensor = torch.stack(passage_embeddings) # shape: (12, D)
query_embedding_tensor = query_embedding.unsqueeze(0) # shape: (1, D)
cosine_sim = F.cosine_similarity(passage_embeddings_tensor, query_embedding_tensor, dim=1)
sorted = torch.argsort(cosine_sim, descending=True)
for rank, idx in enumerate(sorted, 1):
print("=" * 10, rank, "=" * 10)
print(f"[{cosine_sim[idx]:.4f}] {passages[idx]}\n")
結果
========== 1 ==========
[0.6666] 昔ながらの屋台ラーメンは鶏ガラの澄んだ醤油スープと細ちぢれ麺が相性抜群で、深夜の胃袋にしみるんだ。
========== 2 ==========
[0.6191] 真っ白な豚骨スープに焦がしニンニク油をひと垂らしした濃厚ラーメン、替え玉が無料でつい無限ループしてしまうよ。
========== 3 ==========
[0.5724] 港直送の鯖を炙りしめ鯖にしてくれる専門店、皮目の香ばしさと酢のきりりとした酸味が口いっぱいに広がるんだ。
========== 4 ==========
[0.5707] カウンター割烹の金目鯛の煮付けは、甘辛ダレが骨の隅々まで染みていて、白ご飯が思わずおかわり必至だよね。
========== 5 ==========
[0.5678] パリパリの薄皮たい焼きは羽根つきで端っこまで香ばしく、黒あんか白あんか毎回真剣に迷っちゃうんだよね。
========== 6 ==========
[0.5577] 4種のチーズをのせたクアトロフォルマッジに蜂蜜を垂らすスタイルが人気で、塩気と甘さのコントラストがクセになるんだ。
========== 7 ==========
[0.5487] 野菜がごろごろ入った欧風ビーフカレーは、赤ワインとバターのコクが効いたシャバっとルウで後を引くよ。
========== 8 ==========
[0.5271] しゅわっととろけるバスクチーズケーキ専門店、表面の香ばしい焦げと濃厚クリーミーな中身のギャップが病みつきになるよ。
========== 9 ==========
[0.5125] スリランカ式の混ぜて食べるプレートカレーでは、15種類のスパイスが複雑に重なって食べ進めるほど香りが花開くんだ。
========== 10 ==========
[0.5056] 木の温もりあふれるブックカフェで、自家焙煎の深煎りコーヒーと季節のタルトを味わいながら、窓辺から路面電車をのんびり眺められるんだ。
========== 11 ==========
[0.5019] 庭にハーブが茂るガーデンカフェでは、ハンドドリップの浅煎りとフレッシュハーブティーが選べて、小鳥のさえずりが BGM 代わりになるよ。
========== 12 ==========
[0.4904] 薪窯ナポリピッツァのマルゲリータは、モッツァレラがびよーんと伸びて焼き立てを頬張る瞬間がたまらない。
テキストマッチング
テキストマッチングの場合は task="text-matching"
を指定する。
import torch
import torch.nn.functional as F
# テキストリスト(多言語で「浜辺に沈む美しい夕日」)
texts = [
"A beautiful sunset over the beach", # 英語
"浜辺に沈む美しい夕日", # 日本語
"Un beau coucher de soleil sur la plage", # フランス語
"海滩上美丽的日落", # 中国語
"Ein wunderschöner Sonnenuntergang am Strand", # ドイツ語
]
# テキストマッチング用の埋め込みを生成
embeddings = model.encode_text(texts=texts, task="text-matching")
# コサイン類似度行列を計算
embeddings_tensor = torch.stack(embeddings)
similarity_matrix = F.cosine_similarity(embeddings_tensor.unsqueeze(1), embeddings_tensor.unsqueeze(0), dim=2)
# 結果
for i, text_i in enumerate(texts):
for j, text_j in enumerate(texts):
print(f"{i+1} vs {j+1}: {similarity_matrix[i, j]:.4f}")
print("-" * 30)
1 vs 2: 0.8956
1 vs 3: 0.9615
1 vs 4: 0.9067
1 vs 5: 0.9619
------------------------------
2 vs 1: 0.8956
2 vs 2: 1.0000
2 vs 3: 0.8871
2 vs 4: 0.9451
2 vs 5: 0.8990
------------------------------
3 vs 1: 0.9615
3 vs 2: 0.8871
3 vs 3: 1.0000
3 vs 4: 0.8882
3 vs 5: 0.9671
------------------------------
4 vs 1: 0.9067
4 vs 2: 0.9451
4 vs 3: 0.8882
4 vs 4: 1.0000
4 vs 5: 0.9061
------------------------------
5 vs 1: 0.9619
5 vs 2: 0.8990
5 vs 3: 0.9671
5 vs 4: 0.9061
5 vs 5: 1.0000
------------------------------
matplotlibでマトリックスをヒートマップで表示。CJK用にフォントとjapanize-matplotlibもインストール。
!apt-get -y install fonts-noto-cjk
!pip install japanize-matplotlib
import matplotlib.font_manager as fm
for f in fm.findSystemFonts(fontpaths=None, fontext='ttf'):
if 'NotoSansCJK' in f or 'Noto Sans CJK' in f:
print(f)
/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc
/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import numpy as np
font_path = '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'
prop = fm.FontProperties(fname=font_path)
sim_mat_np = similarity_matrix.cpu().detach().numpy()
plt.figure(figsize=(8, 6))
# vmin, vmaxをデータの最小・最大に合わせる
im = plt.imshow(sim_mat_np, cmap='viridis', vmin=sim_mat_np.min(), vmax=sim_mat_np.max())
ax = plt.gca()
ax.set_xticks(np.arange(len(texts)))
ax.set_yticks(np.arange(len(texts)))
ax.set_xticklabels(texts, rotation=45, ha='right', fontsize=12, fontproperties=prop)
ax.set_yticklabels(texts, fontsize=12, fontproperties=prop)
cbar = plt.colorbar(im)
cbar.set_label('コサイン類似度', fontproperties=prop)
plt.title('テキストマッチング 類似度マトリックス Matrix', fontsize=14, fontproperties=prop)
plt.tight_layout()
plt.show()
コード検索
コード検索の場合は以下のパラメータを付与する。
-
task="code"
を指定 - embeddings生成時に
prompt_name
を指定。クエリと文書で異なる。- クエリ:
prompt_name="query"
- 文書:
prompt_name="passage"
- クエリ:
codes = [
"def hello_world():\n print('ハローワールド!')",
"def add(a, b):\n return a + b",
"def factorial(n):\n return 1 if n == 0 else n * factorial(n-1)",
"def greet(name):\n print(f'こんにちは、{name} さん!')",
"def square(x):\n return x * x"
]
code_embeddings = model.encode_text(
texts=codes,
task="code",
prompt_name="passage",
)
query = "与えられた2つの数値を足し算する関数"
query_embedding = model.encode_text(
texts=[query],
task="code",
prompt_name="query",
)[0]
code_embeddings_tensor = torch.stack(code_embeddings)
query_embedding_tensor = query_embedding.unsqueeze(0)
cosine_sim = F.cosine_similarity(code_embeddings_tensor, query_embedding_tensor, dim=1)
sorted = torch.argsort(cosine_sim, descending=True)
for rank, idx in enumerate(sorted, 1):
print("=" * 10, rank, "=" * 10)
print(f"[{cosine_sim[idx]:.4f}]\n```\n{codes[idx]}\n```\n")
========== 1 ==========
[0.6260]
```
def add(a, b):
return a + b
```
========== 2 ==========
[0.4557]
```
def factorial(n):
return 1 if n == 0 else n * factorial(n-1)
```
========== 3 ==========
[0.4515]
```
def square(x):
return x * x
```
========== 4 ==========
[0.4443]
```
def greet(name):
print(f'こんにちは、{name} さん!')
```
========== 5 ==========
[0.4374]
```
def hello_world():
print('ハローワールド!')
```
その他
画像
image_embedding = model.encode_image(
images=["https://i.ibb.co/nQNGqL0/beach1.jpg"],
task="retrieval"
)[0]
print(image_embedding)
print(image_embedding.shape)
Encoding images...: 100%|██████████| 1/1 [00:00<00:00, 4.51it/s]tensor([ 0.0191, -0.0287, 0.0340, ..., -0.0236, -0.0055, 0.0160],
device='cuda:0')
torch.Size([2048])
マルチベクトル
multivector_embeddings = model.encode_text(
texts="中華そばが食べたい",
task="retrieval",
prompt_name="query",
return_multivector=True,
)
print(multivector_embeddings)
print(multivector_embeddings.shape)
tensor([[-0.0776, 0.1249, 0.0962, ..., -0.0574, 0.1367, 0.0031],
[ 0.0307, 0.1183, 0.1321, ..., -0.0540, 0.0365, 0.0362],
[-0.0456, -0.0429, 0.0382, ..., -0.1823, -0.0275, 0.1290],
...,
[ 0.1110, -0.0242, 0.0144, ..., 0.0711, 0.0675, 0.1110],
[ 0.0808, -0.0117, 0.1273, ..., -0.1286, 0.0955, 0.0967],
[ 0.0795, 0.0217, 0.2068, ..., -0.0585, 0.1125, 0.2114]],
device='cuda:0')
torch.Size([9, 128])
マルチベクトルあんまり使ったことないのでわかんないけど、ColBERTとかって、1回でシングル・マルチを両方返せてなかったっけ?BAAI/bge-m3なんかはそうだったような記憶。両方必要なら2回実行する必要があるってことかな。
画像のマルチベクトルも。
images = ["https://i.ibb.co/nQNGqL0/beach1.jpg", "https://i.ibb.co/r5w8hG8/beach2.jpg"]
multivector_image_embeddings = model.encode_image(
images=images,
task="retrieval",
return_multivector=True,
)
print(multivector_image_embeddings[0])
print(multivector_image_embeddings[0].shape)
Encoding images...: 100%|██████████| 1/1 [00:00<00:00, 2.75it/s]tensor([[-0.0979, 0.1621, 0.1018, ..., -0.0532, 0.1222, -0.0185],
[-0.0037, 0.2069, 0.1297, ..., -0.1780, 0.1201, 0.0490],
[ 0.0768, 0.0703, 0.0200, ..., -0.1602, 0.0163, -0.0435],
...,
[ 0.0703, 0.0553, 0.0344, ..., 0.0384, 0.1472, -0.0481],
[ 0.0218, 0.0283, -0.0054, ..., 0.0595, 0.1585, -0.0215],
[ 0.0913, 0.0426, 0.0012, ..., -0.0802, 0.1270, -0.0146]],
device='cuda:0')
torch.Size([341, 128])
たまたまかもしれないけど、画像のベクトル化はなんか詰まるときが結構あるような・・・
次元数削減
query_embeddings = model.encode_text(
texts="中華そばが食べたい",
task="retrieval",
prompt_name="query",
# 128, 256, 512, 1024, 2048 から指定。デフォルトは2048
truncate_dim=1024
)
print(query_embeddings)
print(query_embeddings.shape)
tensor([ 0.0418, -0.0069, 0.0058, ..., -0.0419, -0.0143, 0.0114],
device='cuda:0')
torch.Size([1024])
GGUF(llama.cpp)
冒頭に記載した通り、GGUFはタスクごとにモデルが別々になっている。一番ユースケースが多そうな検索モデルを少しだけ試す。
llama.cppはビルド済みとする。
llama-serverでHuggingFaceモデルを直接指定して起動してみた。
./build/bin/llama-server \
-hf jinaai/jina-embeddings-v4-text-retrieval-GGUF \
--embedding \
--pooling mean
Embedding生成。なお、retrievalモデルの場合、テキストのプレフィクスにクエリの場合はQuery:
、検索文書の場合はPassage:
を指定する必要がある。
curl -X POST "http://127.0.0.1:8080/v1/embeddings" \
-H "Content-Type: application/json" \
-d '{
"input": [
"Query: 中華そば食べたい。"
]
}'
{
"model": "gpt-3.5-turbo",
"object": "list",
"usage": {
"prompt_tokens": 9,
"total_tokens": 9
},
"data": [
{
"embedding": [
0.02717391960322857,
-0.012139739468693733,
0.002885047812014818,
(snip)
0.017668796703219414,
-0.014525865204632282,
0.007351672742515802
],
"index": 0,
"object": "embedding"
}
]
}
まとめ
ライセンス的に商用利用が不可なのは残念だけど(商用の場合は、JinaのAPI or GCP・Azure等のクラウド経由で使うことになる)、個人のちょっとしたプロジェクトならば十分すぎる精度と柔軟性が得られるのではなかろうか。
ところでOllamaのIssueを見ると、v3でもまだ対応がされていないようなIssueが上がっている。試してないのでわからないけど、今でもそうなのかな???