E5で文章をベクトル化たものを次元圧縮して平面上に可視化する

最近文章をEmbeddingして遊ぶのにハマっています。
今回は文章をEmbeddingしてできた多次元ベクトルを視覚的に分かりやすいように2次元平面上にプロットします。

# 環境
- WSL2
- DockerのUbuntu22.04コンテナ
- Python 3.12.3

# 使ったもの
## Embeddingモデル
[intfloat/multilingual-e5-large](https://huggingface.co/intfloat/multilingual-e5-large)
文章を1024次元へ埋め込みしてくれるモデル。
無料かつローカルで動作します。
MITライセンス。
それほど重くないのに結構いい感じにEmbeddingしてくれる。
多分CPUでも十分早い。

## 多次元ベクトル→2次元ベクトルの圧縮
- PCA
- MDS(ユークリッド距離)
- MDS(コサイン距離)

# 下準備
```bash
pip install sentence_transformers pandas scikit-learn
pip install matplotlib matplotlib-fontja
```

# コード

## 必要なもののインポート

```python
import pandas as pd
from sentence_transformers import SentenceTransformer
from sklearn.decomposition import PCA
from sklearn.manifold import MDS
from sklearn.metrics.pairwise import cosine_distances
import matplotlib.pyplot as plt
import matplotlib_fontja
```

## 文章の用意
今回はQiitaのトレンド記事のタイトルを20件ほど持ってきました。

```python
texts = [
    'AWS から OCI に移行してコストを約半額にした話',
    '人類は気づいていた。この勉強の仕方が1番早いということを。',
    'なぜsortコマンドはuniq機能を含んでいるのか？（Unix哲学はどこ行った！？）',
    'AWS LambdaをDocker化する際の注意点と学びの備忘録',
    'Dockerのコンテナイメージを1/10以上軽量化してみた',
    'Git GUIツールについて',
    'VMware Workstation Pro と VMware Fusion Pro の個人利用版が無償になったので使ってみた (2024 年 5 月)',
    '【TypeScript】デスクトップアプリ、作りたくね？',
    '自社製品のUIデザインを改善した話【Pleasanter】',
    'M5Stack CoreMP135に早速最新のLinux 6.9を移植した & Arch Linuxもついでに',
    '【AWS】S3のストレージクラスを変更してコストカットする',
    'GPT-4o モデルに curl コマンドで画像をプロンプトとして送信する方法',
    '初海外カンファレンス, NervesConf US 参加記：飛行機編',
    'NRQL(New Relic Query Language)を実行するUIのUXが改善され、より使いやすくなりました！',
    'New Relicでログ解析してますか？しっかり使って、楽するところは楽しませんか？',
    'PLATEAUでSTYLYの都市XRを作る！',
    'カスタマーサクセスなら見ておくべき4つの先行指標',
    '【個人開発】プレゼントして欲しいものを謎解きで伝えることができるアプリを開発しました',
    'Docker composeのコマンドの挙動も図解にして覚えやすくしてみた',
    'Dockerユーザー必見！データ消失を防ぐマウント方法ガイド',
]
```

## E5でのEmbedding

```python
model = SentenceTransformer('intfloat/multilingual-e5-large')
vectors = pd.DataFrame(model.encode(texts), index=texts)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3568254/b82ecd5c-60e3-4d86-3c88-95cd82f55666.png)

こんな感じに縦方向は文章、横方向は1024次元ベクトルになります。

## PCAで圧縮

```python
pca = PCA(n_components=2, random_state=0)
pca_vectors = pd.DataFrame(pca.fit_transform(vectors), index=texts)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3568254/e40d23b7-2eb9-b4fd-1ad5-3fe262b915b8.png)

二次元ベクトルになっていますね。

## PCA結果の可視化

```python
plt.figure()
plt.scatter(pca_vectors[0], pca_vectors[1], s=10, color='r')
for text, pos in pca_vectors.iterrows():
    plt.text(*pos, text, ha='center', size=6)
plt.title('PCA')
plt.grid()
plt.show()
plt.close()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3568254/d03aa83e-6653-3cb3-d857-3e17024a1a3e.png)

なるほど。

## MDS(ユークリッド距離)で圧縮

```python
mds = MDS(n_components=2, dissimilarity='euclidean', random_state=0)
mds_vectors = pd.DataFrame(mds.fit_transform(vectors), index=texts)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3568254/b50a74bd-cfb1-3ebb-81ac-14704c91d647.png)

## MDS(ユークリッド距離)結果の可視化

```python
plt.figure()
plt.scatter(mds_vectors[0], mds_vectors[1], s=10, color='r')
for text, pos in mds_vectors.iterrows():
    plt.text(*pos, text, ha='center', size=6)
plt.title('MDS euclidean')
plt.grid()
plt.show()
plt.close()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3568254/dee3a0ac-8c22-1411-0909-641a23f02fcc.png)

たまたまだと思いますが、PCAより散らばっていて見やすくなっているように感じますね。

## MDS(コサイン距離)で圧縮

scikit-learnのMDSモジュールでユークリッド距離以外の距離をもとにMDSをする場合、事前に自分で距離を計算しておく必要があります。
今回はコサイン距離を求めておきます。

```python
pd.DataFrame(cosine_distances(vectors), index=texts, columns=texts)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3568254/faf77b2e-a7c1-0921-ec95-415ce06fdef9.png)

こんな感じで、コサイン距離はベクトル同士が似ていると小さな値、似ていないと大きな値になります。
同一のベクトル間の距離(対角成分)は0になります。

これをもとにMDSを求めます。
既に距離は求められているので`dissimilarity`の値を`'precomputed'`とします。

```python
mds_cosine = MDS(n_components=2, dissimilarity='precomputed', random_state=0)
mds_cosine_vectors = pd.DataFrame(mds_cosine.fit_transform(cosine_distances(vectors)), index=texts)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3568254/92136046-0ac2-4a11-e48c-44ea101000d2.png)

## MDS(コサイン距離)結果の可視化

```python
plt.figure()
plt.scatter(mds_cosine_vectors[0], mds_cosine_vectors[1], s=10, color='r')
for text, pos in mds_cosine_vectors.iterrows():
    plt.text(*pos, text, ha='center', size=6)
plt.title('MDS cosine')
plt.grid()
plt.show()
plt.close()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3568254/0b8dd600-d0f4-c1ab-467e-3bdd16227c69.png)

下半分にはDockerとかAWSとかのツールにまつわること、右上にはポエムっぽいことが分布していて良い感じですね。

# おわり

個人的にはコサイン距離を使ったMDSが一番良いと思いました。
ベクトルといえばコサイン類似度ですし。