何でe5-large-v2の日本語対応を調べようと思ったか?
Hugging Faceには日本語に未対応と記載
e5-large-v2 の Hugging Faceを見ると、一番下のLimitaionsに、以下のように書かれています。この時点で、あー、英語しか使えないのね。日本語は使えないのねーと思うと思います。
This model only works for English texts. Long texts will be truncated to at most 512 tokens.
なので、最初に考えた仮説として、「日本語をいれるとTokenizationでエラーがでて、そもそも動かないんでしょ〜」でした。
なぜか日本語をいれても動いてしまう
しかし、以下の5つを、e5-large-v2でembeddingしFAISSにいれます。
input_texts = [
"好きな食べ物は何ですか?",
"どこにお住まいですか?",
"朝の電車は混みますね",
"今日は良いお天気ですね",
"最近景気悪いですね"
]
queryとして以下をいれます。
query_text = ["今日は雨が振らなくてよかった"]
そうすると、以下の文章をちゃんと選んでくれます。
今日は良いお天気ですね
あれ、ちゃんと動いてね?
もっとも適切な文章を選んでいるようにみえるけどー
この記事の目的
何で動くのか不思議なので、e5-large-v2が日本語でなぜ動くのか、その理由を明らかにすること〜
Short Summary
なぜ英語しか対応していないe5-large-v2に日本語をいれても、エラー出ずに動いてしまうのか?
結論からいうと以下の2点です。
- e5-large-v2は、少しだけ日本語も対応している (マニュアルには書いていない)
- 知らない単語はすべてTokenizationでUNKとして置き換えられる
この状態でEmbeddingまですると、UNKに置き換えられた知らない単語はすべて同じTokenに変換され解析されるため、エラーが出ずに動きます。
上記の理由で精度が悪くなるのは直感的にわかりますね。日本語を使うには、おとなしくmultilitual-e5を使いましょう
ここからテストの詳細
e5-large-v5に日本語をいれてテストしてみる
e5-large-v5に日本語をぶち込みEmbedding後、ベクトル情報をFAISSにインポートして、実際にベクター検索してみました
テスト環境の構築
# 必要なライブラリをインポートします。
import torch.nn.functional as F
from torch import Tensor
import faiss # FAISSライブラリをインポート
import numpy as np # NumPyライブラリをインポート
from transformers import AutoTokenizer, AutoModel
# 最後の隠れ層の状態を平均プーリングする関数を定義します。
def average_pool(last_hidden_states: Tensor, attention_mask: Tensor) -> Tensor:
last_hidden = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0)
return last_hidden.sum(dim=1) / attention_mask.sum(dim=1)[..., None]
# 解析したいテキストのリストを定義します。
input_texts = [
"好きな食べ物は何ですか?",
"どこにお住まいですか?",
"朝の電車は混みますね",
"今日は良いお天気ですね",
"最近景気悪いですね"
]
# 使用するモデルとトークナイザーの事前学習済みのパスを指定してロードします。
#tokenizer = AutoTokenizer.from_pretrained('intfloat/multilingual-e5-small')
#model = AutoModel.from_pretrained('intfloat/multilingual-e5-small')
tokenizer = AutoTokenizer.from_pretrained('intfloat/e5-large-v2')
model = AutoModel.from_pretrained('intfloat/e5-large-v2')
# テキストをモデルが処理できる形式にトークン化します。
batch_dict = tokenizer(input_texts, max_length=512, padding=True, truncation=True, return_tensors='pt')
# batch_dict内の各テンソルのサイズを確認する
for key, tensor in batch_dict.items():
print(f"batch_dict {key}: {tensor.size()}")
# モデルを実行して出力を取得します。
outputs = model(**batch_dict)
# batch_dict内の各テンソルのサイズを確認する
for key, tensor in outputs.items():
print(f"outputs {key}: {tensor.size()}")
# `average_pool`関数を使って、最終的な埋め込みベクトルを計算します。
print("attention_mask:", batch_dict['attention_mask'].sum(dim=1))
print("last_hidden_states:", outputs.last_hidden_state.sum(dim=1))
embeddings = average_pool(outputs.last_hidden_state, batch_dict['attention_mask'])
# PyTorchのTensorからNumPy配列に変換します。
embeddings_np = embeddings.cpu().detach().numpy()
# FAISSインデックスを初期化し、データを追加します。
# ここでは、ベクトルの次元数を取得し、L2距離(ユークリッド距離)を使用するインデックスを作成します。
dimension = embeddings_np.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(embeddings_np)
# FAISSインデックスにデータが追加された状態です。
print("Number of vectors in the FAISS index:", index.ntotal)
テストQueryの作成
# クエリテキストを定義します。
query_text = ["今日は雨が振らなくてよかった"]
# クエリテキストをトークナイズし、モデルが処理できる形式にします。
query_dict = tokenizer(query_text, max_length=512, padding=True, truncation=True, return_tensors='pt')
# モデルを実行して出力を取得します。
query_output = model(**query_dict)
# `average_pool`関数を使って、クエリの埋め込みベクトルを計算します。
query_embeddings = average_pool(query_output.last_hidden_state, query_dict['attention_mask'])
# クエリの埋め込みベクトルをNumPy配列に変換します。
query_embeddings_np = query_embeddings.cpu().detach().numpy()
テストQueryの実行
# FAISSを使用して近傍を検索します。ここでは最も類似した1つのベクトルを取得します。
k = 3 # 近傍の数
D, I = index.search(query_embeddings_np, k) # Dは距離、Iはインデックス
# 検索結果を表示します。
print("Query:", query_text[0])
for i in range(k):
print(f"---- {i} ----")
selected_input_id = I[0][i]
print("Selected Input ID:", selected_input_id)
print("Closest text in the index:", input_texts[selected_input_id])
print("Distance:", D[0][i])
実行結果は以下です。動いているように見えますね
Query: 今日は雨が振らなくてよかった
---- 0 ----
Selected Input ID: 3
Closest text in the index: 今日は良いお天気ですね
Distance: 149.77528
---- 1 ----
Selected Input ID: 4
Closest text in the index: 最近景気悪いですね
Distance: 179.69614
---- 2 ----
Selected Input ID: 2
Closest text in the index: 朝の電車は混みますね
Distance: 190.99731
----------
なんで動いたか調査してみる
うごいちゃうんですけど、ひとつひとつ順番にみてみます。
Tokenaiztion確認用コード
以下のコードを実行しTokenization後の状態をチェックしてみます。
# トークン化された結果を表示します。
tokenized_input = tokenizer.batch_encode_plus(input_texts, max_length=512, padding=True, truncation=True, return_tensors='pt')
# トークンIDからトークン文字列への変換と、[UNK]のチェック
decoded_tokens = [tokenizer.convert_ids_to_tokens(ids) for ids in tokenized_input['input_ids']]
unk_tokens = [[token for token in tokens if token == tokenizer.unk_token] for tokens in decoded_tokens]
# それぞれの入力テキストと対応するトークン、[UNK]トークンの有無を表示
for text, tokens, unk in zip(input_texts, decoded_tokens, unk_tokens):
print(f"Input Text: {text}")
e5-large-v2 のTokenization
おー、UNKがたくさんある。
Input Text: 好きな食べ物は何ですか?
Tokens: ['[CLS]', '[UNK]', 'き', '##な', '食', 'へ', '[UNK]', 'は', '[UNK]', 'て', '##す', '##か', '?', '[SEP]']
UNK Tokens: ['[UNK]', '[UNK]', '[UNK]']
----------
Input Text: どこにお住まいですか?
Tokens: ['[CLS]', 'と', '##こ', '##に', '##お', '[UNK]', 'ま', '##い', '##て', '##す', '##か', '?', '[SEP]', '[PAD]']
UNK Tokens: ['[UNK]']
----------
Input Text: 朝の電車は混みますね
Tokens: ['[CLS]', '朝', 'の', '[UNK]', '車', 'は', '[UNK]', 'み', '##ま', '##す', '##ね', '[SEP]', '[PAD]', '[PAD]']
UNK Tokens: ['[UNK]', '[UNK]']
----------
Input Text: 今日は良いお天気ですね
Tokens: ['[CLS]', '[UNK]', '日', 'は', '良', 'い', '##お', '天', '[UNK]', 'て', '##す', '##ね', '[SEP]', '[PAD]']
UNK Tokens: ['[UNK]', '[UNK]']
----------
Input Text: 最近景気悪いですね
Tokens: ['[CLS]', '[UNK]', '[UNK]', '[UNK]', '[UNK]', '[UNK]', 'い', '##て', '##す', '##ね', '[SEP]', '[PAD]', '[PAD]', '[PAD]']
UNK Tokens: ['[UNK]', '[UNK]', '[UNK]', '[UNK]', '[UNK]']
----------
各入力テキストで [UNK]
が出現している箇所を詳しく見てみましょう。
-
好きな食べ物は何ですか?
- 「好き」、「物」、「何」といった一般的な日本語の単語が
[UNK]
として処理
- 「好き」、「物」、「何」といった一般的な日本語の単語が
-
どこにお住まいですか?
- 「住」が
[UNK]
- 「住」が
-
朝の電車は混みますね
- 「電」、「混」などが
[UNK]
- 「電」、「混」などが
-
今日は良いお天気ですね
- 「今」や「気」が
[UNK]
- 「今」や「気」が
-
最近景気悪いですね
- ほとんどの単語が
[UNK]
として処理
- ほとんどの単語が
日常会話程度の言葉が対応できていなさそうです。ひらがなは認識してそうですね。
全部認識できている
multilingual-e5-large のTokenizationをみて比較してみる
Input Text: 好きな食べ物は何ですか?
Tokens: ['<s>', '▁', '好きな', '食べ物', 'は何', 'ですか', '?', '</s>', '<pad>', '<pad>']
UNK Tokens: []
----------
Input Text: どこにお住まいですか?
Tokens: ['<s>', '▁', 'どこに', 'お', '住', 'まい', 'ですか', '?', '</s>', '<pad>']
UNK Tokens: []
----------
Input Text: 朝の電車は混みますね
Tokens: ['<s>', '▁', '朝', 'の', '電車', 'は', '混', 'みます', 'ね', '</s>']
UNK Tokens: []
----------
Input Text: 今日は良いお天気ですね
Tokens: ['<s>', '▁今日は', '良い', 'お', '天気', 'ですね', '</s>', '<pad>', '<pad>', '<pad>']
UNK Tokens: []
----------
Input Text: 最近景気悪いですね
Tokens: ['<s>', '▁最近', '景', '気', '悪い', 'ですね', '</s>', '<pad>', '<pad>', '<pad>']
UNK Tokens: []
----------
結論
なぜ英語しか対応していないe5-large-v2に日本語をいれても、エラー出ずに動いてしまうのか調べてました。
その理由は以下の2つです。
- e5-large-v2は、少しだけ日本語も対応している
- 知らない単語はすべてTokenizationでUNKとして置き換えられる
1ですが、論文 "Text Embeddings by Weakly-SupervisedContrastive Pre-training"を見ると学習用のデータにCCPairsというデータを利用しているとの記載がありますが、その中に日本語が含まれていることが予測されます(このデータは公開されていないようです)
次に2ですが、この状態でEmbeddingまですると、UNKに置き換えられた知らない単語はすべて同じTokenに変換され解析されるためエラーが出ずに動きます。
これらの結果を踏まえると、e5-large-v2 は日本語のテキストに対して適切なモデルではありません。このモデルでは、日本語の多くの重要な要素が未知のトークンとして処理されてしまうため、テキストの意味を理解するための情報が大幅に失われています。
上記の理由で精度が悪くなるのは直感的にわかりますね。日本語を使うには、おとなしくmultilitual-e5を使いましょう。