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

# 何でe5-large-v2の日本語対応を調べようと思ったか？

## Hugging Faceには日本語に未対応と記載

[e5-large-v2 の Hugging Face](https://huggingface.co/intfloat/e5-large-v2)を見ると、一番下の**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にいれます。

```text
input_texts = [
    "好きな食べ物は何ですか?",
    "どこにお住まいですか?",
    "朝の電車は混みますね",
    "今日は良いお天気ですね",
    "最近景気悪いですね"
]
```

queryとして以下をいれます。

```text
query_text = ["今日は雨が振らなくてよかった"]
```

そうすると、以下の文章をちゃんと選んでくれます。

```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にインポートして、実際にベクター検索してみました

### テスト環境の構築

```python
# 必要なライブラリをインポートします。
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の作成

```python
# クエリテキストを定義します。
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の実行

```python:テストクエリの実行
# 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])
```

実行結果は以下です。動いているように見えますね

```terminal:結果
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後の状態をチェックしてみます。

```python
# トークン化された結果を表示します。
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がたくさんある。

```terminal
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をみて比較してみる

```terminal
Input Text: 好きな食べ物は何ですか?
Tokens: ['
', '▁', '好きな', '食べ物', 'は何', 'ですか', '?', '
', '<pad>', '<pad>']
UNK Tokens: []
----------
Input Text: どこにお住まいですか?
Tokens: ['
', '▁', 'どこに', 'お', '住', 'まい', 'ですか', '?', '
', '<pad>']
UNK Tokens: []
----------
Input Text: 朝の電車は混みますね
Tokens: ['
', '▁', '朝', 'の', '電車', 'は', '混', 'みます', 'ね', '
']
UNK Tokens: []
----------
Input Text: 今日は良いお天気ですね
Tokens: ['
', '▁今日は', '良い', 'お', '天気', 'ですね', '
', '<pad>', '<pad>', '<pad>']
UNK Tokens: []
----------
Input Text: 最近景気悪いですね
Tokens: ['
', '▁最近', '景', '気', '悪い', 'ですね', '
', '<pad>', '<pad>', '<pad>']
UNK Tokens: []
----------
```

# 結論

なぜ英語しか対応していないe5-large-v2に日本語をいれても、エラー出ずに動いてしまうのか調べてました。
その理由は以下の2つです。

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

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

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

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

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