📝

Embeddingsを使ってローカルでテキストをクラスタリングする(Multilingual-E5)

2023/08/13に公開

EmbeddingsとSentence Transformers

Sentence Transformersは、テキストをEmbeddings(埋め込み)と呼ばれるベクトル表現に変換するためのライブラリです。OpenAIの "text-embedding-ada002" も、Embeddingsを生成するモデルです。
https://www.sbert.net/
テキストの意味をベクトルで表現すると、コサイン類似度などで意味の類似度が簡単に計算できるため、下記のようなタスクが容易になります。

  • テキストの類似度算出
  • 分類(Classifying)
  • クラスタリング
  • セマンティック検索(意味に基づいた検索)

今回は、ローカルで動作させることができる "Multilingual-E5" というモデルを使って、短いテキストを分類してみます。
https://huggingface.co/intfloat/multilingual-e5-base
このモデルは、Leaderboradでも好成績を収めています。
https://huggingface.co/spaces/mteb/leaderboard
largeモデルは、"text-embedding-ada002"よりハイスコアです。ただし扱えるシーケンス長は512トークンまでで、ada(8192)より短くなります。また、多言語モデルなので、言語が違っても、翻訳することなく意味の類似度などを計算できます。

環境

Windows 11, RTX3060 12GB
Python 3.10.11
sentence-transformers 2.2.2
torch 2.0.1+cu118

事前準備

パッケージのインストール

GPUを使う場合は事前にPytorchをインストールします。

# pip install torch --index-url https://download.pytorch.org/whl/cu118
pip install sentence_transformers

ダミーデータの作成

今回はPCに関するクレームを短文で表したものを用意します。GPT-4に作成してもらいました。
(n = 75)

# data/claim.txt
ディスプレイがつかない。
画面が真っ暗です。
モニターのライトが点かない。
PCの画面が起動しない。
スクリーンが点灯しません。
ディスプレイに何も映らない。
モニターが真っ黒のままです。
画面が反応しません。
電源は入るが、画面だけ映らない。
スクリーンが暗いままです。
...

Embeddingsの作成

データの読み込み

import pandas as pd
df = pd.read_table("data/claim.txt", header=None, names=["claim"])
df.head()
claim
0 ディスプレイがつかない。
1 画面が真っ暗です。
2 モニターのライトが点かない。
3 PCの画面が起動しない。
4 スクリーンが点灯しません。
. ...

モデルの指定

PCのスペックやデータ数に応じてモデルを選択します。small, base, largeの3つのモデルが提供されています。Embeddingsの処理は軽量なので、smallなら低スペックのノートPCでも高速に動作します。GPUがあるならlargeでも余裕で動くと思います。

# model_name = "intfloat/multilingual-e5-small"
# model_name = "intfloat/multilingual-e5-base"
model_name = "intfloat/multilingual-e5-large"

# モデルの読み込み
from sentence_transformers import SentenceTransformer
model = SentenceTransformer(model_name)

参考

今回の環境では、smallとbaseでベクトル化の時間に差はありませんでした。
モデルはパフォーマンス要件と性能のトレードオフで選択すると良いでしょう。

モデル モデルサイズ 次元 ベクトル化にかかった時間※
small 471MB 384 8.5s
base 1.11GB 768 8.5s
large 2.24GB 1024 16.0s

※環境
GPU: RTX3060 12GB
データ数: 1000

ベクトル化

テキストをmodel.encode()に渡すだけでベクトル化できます。超簡単。

df["vector"] = df["claim"].apply(model.encode)
df.head()
claim vector
0 ディスプレイがつかない。 [0.030703284, 0.0027665994, -0.02013304, -0.06...
1 画面が真っ暗です。 [0.030521175, 0.017222878, -0.012971448, -0.05...
2 モニターのライトが点かない。 [0.029229984, 0.0129130315, -0.013014274, -0.0...
3 PCの画面が起動しない。 [0.040510327, 0.003165508, -0.021918021, -0.05...
4 スクリーンが点灯しません。 [0.03320634, -0.0026000645, -0.025136635, -0.0...
. ... ...
# ベクトルの次元数
print(len(df.at[0, "vector"]))
# 1024

1024次元のベクトルデータが生成されていることがわかります。

クラスタリング

今回は、一般的なK-Means法を使います。

シルエット分析

最適なクラスター数を探る方法はいくつかあるようです。今回はシルエット分析を使いました。
https://qiita.com/deaikei/items/11a10fde5bb47a2cf2c2

from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

vectors = df["vector"].tolist()

# 探索するクラスタ数の範囲を設定
range_n_clusters = range(2, 30)  # 例: 2から30まで

best_score = -1
best_k = None
scores = []  # シルエットスコアを保存するためのリスト

for k in range_n_clusters:
    kmeans = KMeans(n_clusters=k, n_init="auto", random_state=0)
    labels = kmeans.fit_predict(vectors)
    
    # シルエットスコアを計算
    score = silhouette_score(vectors, labels)
    scores.append(score)
    
    # ベストスコアの更新
    if score > best_score:
        best_score = score
        best_k = k

print("Best k:", best_k)
print("Best silhouette score:", best_score)
# Best k: 22
# Best silhouette score: 0.077055216
# シルエットスコアのプロット
import matplotlib.pyplot as plt
plt.figure(figsize=(10,6))
plt.plot(range_n_clusters, scores, marker='o')
plt.xlabel('Number of clusters')
plt.ylabel('Silhouette Score')
plt.title('Silhouette Score for Different Number of Clusters')
plt.show()

クラスタリング

n = 75 なので、k = 22だとクラスタリングする意味が薄いです。シルエットスコアを見ながらクラスター数を探り、今回は10に決定しました。答えはないと思うので、データの性質や業務要件によって選択するしかないでしょう。
色々試してみた感想では、人間の感覚の倍くらいのクラスター数にするといい感じになる気がします。それより少ないと、意味の違う文が混ざってしまう印象でした。

# k値の指定
k = 10

# クラスタリング
kmeans = KMeans(n_clusters=k, n_init="auto", random_state=0)
df["label"] = kmeans.fit_predict(vectors)

# 保存
df.sort_values("label").drop(columns="vector").to_csv("data/output.csv", index=None)
claim label
ビデオ再生がカクカクする。 0
インターネットのアイコンにエクスクラメーションマークがついてる。 0
ソフトがスムーズに動作しない。 1
アプリケーションが遅く開く。 1
処理速度がかなり落ちてきた。 1
突然の遅延が頻繁に発生。 1
タスクの完了が遅くなった。 1
起動に時間がかかる。 1
キーボードエラーが表示される。 2
接続エラーが出る。 2
DNSエラーが頻繁に発生。 2
ネットワークトラブルのポップアップが出る。 2
ノートPCのキーボードが故障してる? 3
キータイプしても文字が入力されない。 3
キーの一部が使えない。 3
キーボードがフリーズしている。 3
文字入力ができない状態。 3
キーボードからの入力が受け付けられない。 3
特定のキーだけ動かない。 3
USBキーボードも認識しない。 3
キーボードのライトは点いてるけど動かない。 3
キータッチ音はするのに、文字が出ない。 3
キーボードのドライバトラブルか、反応が悪い。 3
キーボードが動作しない。 3

少し怪しいところもありますが、それなりにうまく行っています。

推論

sklearn.cluster.KMeans.predict()を使うと、新しいテキストデータのラベルを推論できます。

new_claims = ["動画再生がカクカクする。",
              "PCの起動が遅い",
              "キーボードが効かない"]

new_vectors = list(map(model.encode, new_claims))

print(kmeans.predict(new_vectors))
# [0 1 3]

色々応用ができそうですね!

Discussion