Jina AI「テキスト埋め込みによる語順の捕捉の失敗とその修正方法」
「一生懸命働いている」("Working hard")か「ほとんど働いていない」("hardly working")か?文の中の単語をシャッフルしても、そのテキスト埋め込み間のコサイン類似度は、元の文に比べて驚くほど高いままです。これは、jina-embeddings-v3/v2のようなmean-pooling-based embeddingsだけでなく、bge-1.5のようなCLS-based embeddingsでも同じことが言えます。つまりエンベッディング・モデルは、まだ単なる単語の集まりなのでしょうか?
私たちは次のことを発見しました:
- 短い文は、実際には長い文よりも、埋め込みにおける語順情報の保持が劣っています。 直感的には、短いテキストの方がモデルが適切に処理しやすいと思われるかもしれませんが、私たちの実験では、3~5 トークンの文では、シャッフルされたバージョンとシャッフルされていないバージョン間のコサイン類似度 (約 0.95) が、30 トークンの文 (約 0. 87)よりも高いことが示されています。
- より大きなモデルでは、語順の問題は実際には解決されません。BGE-small (33M パラメータ) から BGE-large (335M パラメータ) にスケールアップしても、語順の捕捉の改善は最小限でした。
幸いなことに、この問題を解決するのに必要な労力は驚くほど少なく、小さなデータセット (11,000 サンプル) を 5 分間微調整するだけで、語順の感度が劇的に向上します。詳細については、以下の記事をご覧ください。
https://jina.ai/news/text-embeddings-fail-to-capture-word-order-and-how-to-fix-it/
詳細はブログ記事。
ざっくりまとめが下にあったので、少しだけ補足追加。
- 短いテキストは単語の順序を捉えるのに失敗しやすい、長いテキストのほうが単語の順序を捉えやすい
- テキスト埋め込みモデルのサイズを大きくしても、単語の順序の理解は微々たる改善にとどまり、単に大きなモデルを使用することだけでは解決しない
- 対照学習がこれらの問題に対する潜在的な解決策となりうる
- 順序が重要になるのは以下のカテゴリー(注: 例文は日本語訳)
-
方向性
- 例1: 彼女は**パリから東京まで**飛行機で行った
- 例2: 彼女は東京からパリまで車で行った
-
時間的
- 例1: 彼女は映画を見る前に夕食を食べた
- 例2: 彼女は夕食を食べる前に映画を見た
-
因果関係
- 例1: 気温が上がって (→) 雪が溶けた
- 例2: 雪が溶けて (→) 気温が下がった
-
比較
- 例1: コーヒーは紅茶よりおいしい
- 例2: 紅茶はコーヒーよりおいしい
-
否定
- 例1: 彼はテーブルのそばに立っている
- 例2: 彼はテーブルから離れたところに立っています
-
方向性
- 以下のようなトリプレットを用意してFTする
- アンカー: 川は山から海へと流れる
- 肯定: 山から海へ水が流れる
- 否定: 川は海から山へ流れる川
- 順序が重要になるのは以下のカテゴリー(注: 例文は日本語訳)
日本語の場合でもこれは該当するのかな?
タイムリーな記事
ちょっと気になったので実際に試してみた。なおjina-embeddings-v3
を使用。
import requests
from google.colab import userdata
import numpy as np
def cosine_similarity(vec_list):
vec1 = np.array(vec_list[0])
vec2 = np.array(vec_list[1])
dot_product = np.dot(vec1, vec2)
norm_vec1 = np.linalg.norm(vec1)
norm_vec2 = np.linalg.norm(vec2)
if norm_vec1 == 0 or norm_vec2 == 0:
raise ValueError("ゼロベクトルが含まれているため、コサイン類似度は計算できません。")
return dot_product / (norm_vec1 * norm_vec2)
def get_embedding_pair(sentence_list):
url = 'https://api.jina.ai/v1/embeddings'
JINA_API_KEY = userdata.get('JINA_API_KEY')
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {JINA_API_KEY}'
}
data = {
"model": "jina-embeddings-v3",
"task": "text-matching",
"late_chunking": False,
"dimensions": 1024,
"embedding_type": "float",
"input": sentence_list
}
response = requests.post(url, json=data, headers=headers)
return [d["embedding"] for d in response.json()["data"]]
英語
sentence_pairs_en = {
"direction": [
"She flew from Paris to Tokyo.",
"She drove from Tokyo to Paris.",
],
"temporal": [
"She ate dinner before watching the movie.",
"She watched the movie before eating dinner.",
],
"causal": [
"The rising temperature melted the snow.",
"The melting snow cooled the temperature.",
],
"comparative": [
"Coffee tastes better than tea.",
"Tea tastes better than coffee.",
],
"negation": [
"He is standing by the table.",
"He is standing far from the table.",
],
}
for k, sentences in sentence_pairs_en.items():
print(f"#### {k} ####")
print("\nsentences:")
print("\n".join([f"- {s}" for s in sentences]))
print("\nscore:", cosine_similarity(get_embedding_pair(sentences)))
print()
#### direction ####
sentences:
- She flew from Paris to Tokyo.
- She drove from Tokyo to Paris.
score: 0.8810929055764013
#### temporal ####
sentences:
- She ate dinner before watching the movie.
- She watched the movie before eating dinner.
score: 0.9581018576771294
#### causal ####
sentences:
- The rising temperature melted the snow.
- The melting snow cooled the temperature.
score: 0.7663670043382221
#### comparative ####
sentences:
- Coffee tastes better than tea.
- Tea tastes better than coffee.
score: 0.9538634701296309
#### negation ####
sentences:
- He is standing by the table.
- He is standing far from the table.
score: 0.8089181733708365
日本語
sentence_pairs_ja = {
"direction": [
"彼女はパリから東京まで行った",
"彼女は東京からパリまで行った",
],
"temporal": [
"彼女は映画を見る前に夕食を食べた",
"彼女は夕食を食べる前に映画を見た",
],
"causal": [
"気温が上がって雪が溶けた",
"雪が溶けて気温が下がった",
],
"comparative": [
"コーヒーは紅茶よりおいしい",
"紅茶はコーヒーよりおいしい",
],
"negation": [
"彼はテーブルのそばに立っている",
"彼はテーブルから離れたところに立っています",
],
}
for k, sentences in sentence_pairs_ja.items():
print(f"#### {k} ####")
print("\nsentences:")
print("\n".join([f"- {s}" for s in sentences]))
print("\nscore:", cosine_similarity(get_embedding_pair(sentences)))
print()
#### direction ####
sentences:
- 彼女はパリから東京まで行った
- 彼女は東京からパリまで行った
score: 0.9849495181142561
#### temporal ####
sentences:
- 彼女は映画を見る前に夕食を食べた
- 彼女は夕食を食べる前に映画を見た
score: 0.9429784007932432
#### causal ####
sentences:
- 気温が上がって雪が溶けた
- 雪が溶けて気温が下がった
score: 0.8876550504181947
#### comparative ####
sentences:
- コーヒーは紅茶よりおいしい
- 紅茶はコーヒーよりおいしい
score: 0.9245611137311809
#### negation ####
sentences:
- 彼はテーブルのそばに立っている
- 彼はテーブルから離れたところに立っています
score: 0.8468101146022733
日本語の方は多少恣意的に似通うような翻訳にしたのだけど、それにしても英語に比べるとやや高めに出ているかなーという感はある。
ちなみに「否定」というのは自分的には「打ち消し」のイメージなので、サンプルの文章に少し違和感がある。そこで、文章を変えてみた。
sentence_pairs_negation = {
"negation_en": [
"He is standing by the table.",
"He is not standing by the table.",
],
"negation_ja": [
"彼はテーブルのそばに立っている",
"彼はテーブルのそばに立っていない",
],
}
for k, sentences in sentence_pairs_negation.items():
print(f"#### {k} ####")
print("\nsentences:")
print("\n".join([f"- {s}" for s in sentences]))
print("\nscore:", cosine_similarity(get_embedding_pair(sentences)))
print()
#### negation_en ####
sentences:
- He is standing by the table.
- He is not standing by the table.
score: 0.8280262605748225
#### negation_ja ####
sentences:
- 彼はテーブルのそばに立っている
- 彼はテーブルのそばに立っていない
score: 0.8206575665424007
日本語/英語、共に、最初の例と比べてこちらのほうが低く出ている。その意味では明確に否定的表現があったほうが類似性は下がるように見える。