2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

e5-large-v2は英語のみ対応なのに日本語をいれてもエラーがでない理由 (使ってはいけない理由)

Last updated at Posted at 2024-05-05

何で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つです。

  1. e5-large-v2は、少しだけ日本語も対応している
  2. 知らない単語はすべてTokenizationでUNKとして置き換えられる

1ですが、論文 "Text Embeddings by Weakly-SupervisedContrastive Pre-training"を見ると学習用のデータにCCPairsというデータを利用しているとの記載がありますが、その中に日本語が含まれていることが予測されます(このデータは公開されていないようです)

次に2ですが、この状態でEmbeddingまですると、UNKに置き換えられた知らない単語はすべて同じTokenに変換され解析されるためエラーが出ずに動きます。

これらの結果を踏まえると、e5-large-v2 は日本語のテキストに対して適切なモデルではありません。このモデルでは、日本語の多くの重要な要素が未知のトークンとして処理されてしまうため、テキストの意味を理解するための情報が大幅に失われています。

上記の理由で精度が悪くなるのは直感的にわかりますね。日本語を使うには、おとなしくmultilitual-e5を使いましょう。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?