EmbeddingとK-means法を使って、問い合わせをクラスタリング

# 概要
問い合わせの傾向から必要なFAQを考えたいとき、レビューやアンケートから共通する意見を見つけたいとき、ニュースをジャンルやトピックごとに分けたいときなど、文章を適切に分類できると便利です。

分類のためのカテゴリが事前にわかっている場合は、カテゴリを自動で付与する方法を考えれば良いですが、網羅的なカテゴリが作れていない文章群に対して分類をしたかったので、クラスタリングに取り組みました。

# クラスタリングの流れ
[OpenAI CookBook](https://cookbook.openai.com/)の[Clustering](https://cookbook.openai.com/examples/clustering)という記事を参考に、下記手順で分析を行いました。

1. OpenAIの[Embeddings API](https://platform.openai.com/docs/api-reference/embeddings)を使って、文章をEmbeddingする
    - Embeddingとは、自然言語を高次元の数値ベクトルとして表現する技術で、これにより文章間の類似性を数値的に扱えるようになります
1. エルボー法でクラスタ数を決定する
    - エルボー法は、クラスタ数ごとの歪み（クラスタ内のデータのばらつき）をプロットし、そのプロットの"肘"の部分を見つけることで、適切なクラスタ数を決定する手法です
1. Embeddingの結果をK-means法でクラスタリングする
    - K-means法は、指定したクラスタ数に基づいてデータをグループ分けする手法で、各データポイントを最も近いクラスタに割り当てることで、類似したデータ同士をまとめていきます 

# 使用するデータ
[東京都くらしWEB](https://www.shouhiseikatu.metro.tokyo.lg.jp/)に掲載されている[消費生活相談FAQ一覧](https://www.shouhiseikatu.metro.tokyo.lg.jp/sodan/faq/menu/)を擬似的な問い合わせデータをとらえ、これを分析することにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704675/20637ebd-011a-e5eb-a4d4-6c66ad0f98bc.png)

「街頭で声をかけられ、やせるお茶とサプリを契約した。解約したい。」といった消費生活に関する相談が、310件掲載されています。

# 実行環境
[Google Colab](https://colab.research.google.com/)を使います。

# 実装
## Embedding
まずFAQを読み込みます。
```python
import pandas as pd

# データの読み込み
datafile_path = "/content/消費生活相談FAQ一覧.csv"
df = pd.read_csv(datafile_path)
```
続いて、質問をEmbeddingします。
```python
from google.colab import userdata
from openai import OpenAI

# 環境変数の取得
openai_api_key = userdata.get("OPENAI_API_KEY")
client = OpenAI(
  api_key=openai_api_key,
)

def get_embedding(text: str):
  """
  テキストのEmbeddingを取得する
  :param text: テキスト
  :return: embedding
  """
  try:
      response = client.embeddings.create(model="text-embedding-3-small", input=text)
      return response.data[0].embedding
  except Exception as e:
      raise Exception(f"Error get_embedding: {e}")

# Embeddingを実行
df["embedding"] = df["question"].apply(get_embedding)
```

## エルボー法でクラスタ数を決定する

```python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans

# 'embedding_array' 列からデータを抽出し、2次元のNumPy配列に変換
X = np.vstack(df['embedding_array'].values)

# クラスタ数の範囲を定義
k_values = range(5, 30)

# SSEを格納するリストを初期化
sse = []

# エルボー法を実行
# 各kについてK-meansを実行し、SSEを計算
for k in k_values:
  # KMeansモデルを初期化（random_stateは再現性のために設定）
  kmeans = KMeans(n_clusters=k, random_state=42)

  # モデルをデータに適合させる
  kmeans.fit(X)

  # SSE（inertia_）をリストに追加
  sse.append(kmeans.inertia_)

# エルボー法のプロット
# プロットのサイズを設定
plt.figure(figsize=(8, 5))

# kに対するSSEをプロット（青い点と線で表示）
plt.plot(k_values, sse, 'bo-')

# グラフのラベルとタイトルを設定
plt.xlabel('Number of Clusters (k)')
plt.ylabel('Sum of Squared Errors (SSE)')
plt.title('Determining the Optimal Number of Clusters Using the Elbow Method')

# x軸の目盛りを整数に設定
plt.xticks(k_values)

# グラフを表示
plt.show()
```
実行結果は下記のようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704675/73111309-897a-09d1-866e-b1b6aea7ee9c.png)

このグラフの解釈をChatGPTに聞いてみると、適切なクラスタ数は、15または16とのことなので、クラスタ数は15で決定しました。

>グラフの解釈:
>- グラフの形状からわかるように、クラスタ数が増加するにつれてSSEが減少しています。これは、クラスタ数が増えると各クラスタの内部のデータ点がより密集するようになり、クラスタ内の誤差が減少するためです
>- エルボー法の目的は、「肘」のような形をしているポイントを見つけることです。このポイントは、クラスタ数を増やしてもSSEの減少が緩やかになり始める場所を示します。これは、クラスタ数をそれ以上増やしても、データの構造をあまり改善しないことを示しています
>
>このグラフの具体的な解釈:
>- グラフを見てわかるように、SSEが急激に減少している部分がいくつかありますが、特に顕著な「肘」の形状が見られるのは、15〜16クラスタあたりです。このポイント以降、SSEの減少率が緩やかになっています
>- したがって、適切なクラスタ数の候補としては、15または16が考えられます。この範囲でK-meansクラスタリングを行うことで、データの構造を適切に反映するクラスタリングができる可能性が高いです

## K-means法でクラスタリング
`n_clusters`を15に設定し、K-means法でクラスタリングします。
```python
from sklearn.cluster import KMeans

n_clusters = 15

kmeans = KMeans(n_clusters=n_clusters, init="k-means++", random_state=42)
kmeans.fit(matrix)
labels = kmeans.labels_
df["Cluster"] = labels
```

FAQのベクトルを2次元に圧縮し、クラスタごとに色分けして可視化します。

```python
from sklearn.manifold import TSNE
import matplotlib
import matplotlib.pyplot as plt

# t-SNEのパラメータ設定
tsne = TSNE(n_components=2, perplexity=15, random_state=42, init="random", learning_rate=200)
vis_dims2 = tsne.fit_transform(matrix)

x = [x for x, y in vis_dims2]
y = [y for x, y in vis_dims2]

# 15種類の色を定義
colors = [
    "purple", "green", "red", "blue", "yellow",
    "orange", "pink", "brown", "gray", "cyan",
    "magenta", "lime", "indigo", "gold", "teal"
]

# クラスタごとにデータポイントをプロット
for category in range(15):  # 15クラスタを前提に
    xs = np.array(x)[df.Cluster == category]
    ys = np.array(y)[df.Cluster == category]
    plt.scatter(xs, ys, color=colors[category], alpha=0.3)

    avg_x = xs.mean()
    avg_y = ys.mean()

    plt.scatter(avg_x, avg_y, marker="x", color=colors[category], s=100)

plt.title("Clusters identified visualized in 2D using t-SNE")
plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704675/e3982cf8-53b7-1ae0-44e9-847dc377a196.png)

このままでは、各クラスタにどのようなFAQが含まれているかわからないので、各クラスタの名前をChatGPTに命名してもらいます。
`rev_per_cluster = 5`とし、各クラスタから5つのFAQをGPTに渡して、命名を頼みます

```python
rev_per_cluster = 5

# 新しい列 'cluster_name' を追加し、空の値で初期化
df['cluster_name'] = ""

for i in range(n_clusters):
  print(f"Cluster {i} Theme:", end=" ")

  questions = "\n".join(
    df[df.Cluster == i]
    .question.str.replace("Questions: ", "")
    .str.replace("\n\nContent: ", ":  ")
    .sample(rev_per_cluster, random_state=42)
    .values
  )

  messages = [{
      "role": "user",
      "content": f"あなたはVoC分析アナリストです。同じカテゴリの「消費者生活相談」に関する質問を読み、どのようなトラブルかの観点で具体的なカテゴリ名を考えてください。出力は余計なものを加えずカテゴリ名のみを回答し、日本語で答えてください。\n\n質問:\n{questions}\n"
      }]

  response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    temperature=0,
    max_tokens=64,
    top_p=1,
    frequency_penalty=0,
    presence_penalty=0
    )
  cluster_name = response.choices[0].message.content.replace("\n", "")

  print(cluster_name)

  # クラスタ名をdfに格納
  df.loc[df.Cluster == i, 'cluster_name'] = cluster_name

  sample_cluster_rows = df[df.Cluster == i].sample(rev_per_cluster, random_state=42)

  for j in range(rev_per_cluster):
    print(sample_cluster_rows.question.str[:70].values[j])

  print("-" * 100)
```

実行結果は下記の通りです。

```
Cluster 0 Theme: 住宅関連トラブル
住宅ローンを支払い中のマンションを、手放さずに債務整理はできるか。
賃貸マンションをきれいに使い明け渡す前に掃除した。修繕費の請求は妥当か。
住宅リフォームのトラブルを防ぐポイントを知りたい。
賃貸アパートを退去したが、特約の内容を超える修理代を請求され不満。
アパートの更新をするが、更新料のほかに更新手数料を払う必要があるか。
----------------------------------------------------------------------------------------------------
Cluster 1 Theme: 契約トラブル
クレジットを組んで、化粧品を買ってネットワークビジネスに入会したが、会社が倒産した。
アンケートに答えたら、その後化粧品の勧誘電話が頻繁にくる。やめてほしい。
街頭で声をかけられ、やせるお茶とサプリを契約した。解約したい。
３日前、エステティックサロンに行き、化粧品とエステを契約した。解約したい。
痩身エステ100回の契約。10回施術したが中途解約できるか。
----------------------------------------------------------------------------------------------------
Cluster 2 Theme: 個人情報管理・詐欺対策・クレジットカードトラブル・キャンセル料トラブル
町内会で名簿を作成し会員に配布するが、注意することはあるか。
知らない会社から外国の宝くじが当選したような手紙が来た。どうしたらよいか。
クレジットカードが見当たらない。不正使用が心配。
申し込んだ覚えのないクレジットカードが届いた。不審に思う。対処方法は。
ホテルをキャンセルしたのに、クレジットカード代金が引き落とされた。
----------------------------------------------------------------------------------------------------
Cluster 3 Theme: 詐欺・悪質商法関連トラブル
友人に儲かるサイドビジネスを勧められたが、信用できるか。
貴金属の買取り業者にネックレスを持っていかれた。返してほしい。
高額で買い取ると勧められ、外貨を購入したが換金できない。
ネットワークビジネスのために健康食品を買ったが、誰も誘えない。
毎日いろいろな業者からしつこい電話勧誘があって困っている。名簿業者から名簿を買うのは違法ではないのか。
----------------------------------------------------------------------------------------------------
Cluster 4 Theme: 契約トラブル
家庭教師の指導方法が悪いので解約したところ、違約金を請求。不満。
友人に誘われて自己啓発講座を契約。学生ローンから借りて払ったがやめたい。
家庭教師を解約したら、翌月分の月謝を請求された。払いたくない。
「3年前に契約した旅行資格講座代金が未納」と電話があった。クーリング・オフしたのに不審。
書店で呼び止められ英会話学校を契約。レベルが低く解約したが解約料が高い。
----------------------------------------------------------------------------------------------------
Cluster 5 Theme: 修理費用に関するトラブル
テレビの修理を頼んだら、部品は安かったが出張費と技術料が高すぎる。
古い指輪をリフォームしたいが、注意点を教えてほしい。
携帯電話を3回修理したが、また故障。新しい電話機と交換してほしい。
スマートフォンの電源が落ちる不具合があり、新品と交換しても同じだ。
事故車ではないことを確認して買ったのに事故車だった。返金してほしい。
----------------------------------------------------------------------------------------------------
Cluster 6 Theme: クリーニングトラブル
茶色の皮製の飾りの色がアイボリー地ににじみ出た。元に戻してほしい。
クリーニングに出したら、白いスーツが灰色っぽく薄汚れた。弁償だけでなくスーツも返してほしい。
クリーニングから戻った合成皮革のズボンでやけど。責任を取ってほしい。
信頼できるクリーニング店の選び方を、知りたい。
母の形見のコートをクリーニングに出したら風合いが変わってしまった。
----------------------------------------------------------------------------------------------------
Cluster 7 Theme: 借金・返済問題
借金が増えて返済が苦しい。費用が安いと聞いた「特定調停」の方法は。
クレジット払いで買ったリビングボードが届かない。支払いたくない。
所在不明の息子に未納分のサービス会費の請求があった。親が支払うべきか。
クレジットカード払いのスポーツクラブ会費。現金で払うのを拒否され不満。
借金を整理した夫に「融資する」とＤＭが届く。送らせない方法があるか。
----------------------------------------------------------------------------------------------------
Cluster 8 Theme: 高齢者の契約トラブル
認知症の父親が、近所の店で次々に健康食品を契約したが、返品したい。
17歳のときに包茎手術を受け、成人後もクレジット契約の支払いを続けているがやめたい。
高齢の父のためホームヘルパーを頼んだが、約束が守られなかった。
高齢の母がリースで電話機を契約した。解約させたい。
自分が補助人を務める姉が健康食品や健康器具を次々に買う。取り消したい。
----------------------------------------------------------------------------------------------------
Cluster 9 Theme: インターネット取引に関するトラブル
インターネットオークションで売買したい。どんな注意が必要か。
インターネットでスニーカーを注文したら海外から全く別の粗悪な商品が届いた。
インターネットで子犬を購入。到着後から具合が悪く死亡した。返金してほしい。
ペットショップで買った子犬に病気が見つかった。払った代金を返金してほしい。
お得な海外パックツアーをインターネットで見つけたが信用できるだろうか。
----------------------------------------------------------------------------------------------------
Cluster 10 Theme: 契約トラブル
新車を解約したら解約料を請求された。クーリング・オフできないのか。
ネット広告を見てオーディションに応募して合格。高額なプロモーションＤＶＤの契約となり不満。
友達と合わせて休みを取ったのに温泉パックツアーが中止になった。不満。
婚活サイトで知り合った男性に勧められて投資用マンションを購入したら、連絡が途絶えた。
数年前に購入した墓を解約する。永代使用料を返金してほしい。
----------------------------------------------------------------------------------------------------
Cluster 11 Theme: 金融トラブル
以前に商品先物取引で出した損失を返金すると電話があったが、本当か。
息子から会社のお金を使い込んだと泣き声で電話があった。どうしたらよいか。
保険会社の破綻が心配。私が加入している保険会社は大丈夫だろうか。
LCC（格安航空会社）から海外航空券代を二重請求され、引き落とされた。
投資顧問会社から儲けさせてあげると勧誘され契約したが、やめたい。
----------------------------------------------------------------------------------------------------
Cluster 12 Theme: 配達トラブル
宅配便で母の日に花を贈ったが、当日に配達されなかった。弁償してほしい。
宅配業者から代金引換で荷物が届いていると電話があったが、覚えがない。
宅配便でスノーボードを送ったら割れていた。弁償してもらえるか。
デパートから買った覚えがないのに、時計が届いた。不審。対処方法は。
引越しで、鏡台の鏡にヒビが入った。弁償してほしい。
----------------------------------------------------------------------------------------------------
Cluster 13 Theme: 自己破産・借金問題相談
自己破産をすれば、依頼している保証人の借金もなくなるか。
ギャンブルで作った借金が返せない。自己破産はできるか。
17歳の息子がマルチ商法をしているようだ。どうしたらよいか。
家族や勤務先に内緒で自己破産手続きをしたいが、できるだろうか。
無認可保育園を中途退園したいが、払ったお金は返してもらえるか。
----------------------------------------------------------------------------------------------------
Cluster 14 Theme: 通信サービス・契約トラブル
スマートフォンでモバイルＷｉ－Fiルーターを使うと通信料が安くなると言われたが不審。
スマートフォンでアプリをダウンロードすると個人情報を知られてしまうか。
コンピューターウイルスとはどんなものか。予防策を教えてほしい。
スマートフォンを買ったが、クーリング・オフしたい。
タブレット型端末でアクセスしたアダルトサイトで登録の画面。請求が心配。
----------------------------------------------------------------------------------------------------
```

# 所感
非常にシンプルな実装ですが、完璧とは言わないまでも精度良くクラスタリングできていると感じました。各クラスタには、特定のトピックやトラブルに関する問い合わせがまとまっており、クラスタリングの結果を通して、クラスタごとに消費者がどのような問題で悩んでいるかを把握することができると思います。

一方で、Cluster 1と4のように同じ名前をつけられてしまうクラスタがあったり、Cluster 2のように少し統一感がなかったりするケースもあり、まだまだ改善の余地があるとも感じました。

# 参考資料
https://cookbook.openai.com/examples/clustering

https://www.shouhiseikatu.metro.tokyo.lg.jp/sodan/faq/menu/

https://qiita.com/panda531/items/4b5504a3949eacb7ab19