【生成AI】記事やアンケートをLLMで分類できないか考える

## はじめに

アンケートや記事を手で分類するのは大変な作業です。クラスタリングなどの手法を用いて分類することもありますが、今回はLLMを活用して自動的に分類する方法を検討しました。本記事では、カテゴリが未定の場合を想定し、LLMを使ってカテゴリを生成し、それに基づいて記事を分類するアプローチで考えました。

:::note warn
初学者につき、内容に誤りを含む場合があります。
:::

## 概要

1. サンプル記事からカテゴリ案を生成
2. 生成されたカテゴリを整理・統合
3. 各記事を新しいカテゴリに分類
4. 結果の可視化と評価

## 環境設定

Google Colabを使用し、必要なライブラリをインストールします。
OPENAI_API_KEYは予め左ペインのシークレット（鍵アイコン）にて登録しておきます。

```python
!pip install datasets
!pip install openai
!pip install japanize-matplotlib
```
```python
import os
import pandas as pd
import numpy as np
from datasets import load_dataset
from openai import OpenAI
from google.colab import userdata
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.decomposition import TruncatedSVD
import matplotlib.pyplot as plt
import japanize_matplotlib

# フォントの設定
plt.rcParams['font.family'] = 'IPAexGothic'

# OpenAI APIキーの設定
os.environ["OPENAI_API_KEY"] = userdata.get('OPENAI_API_KEY')
client = OpenAI()
```

### データの準備
実務のデータを使うわけにもいかないので、Hugging Faceで良さそうなデータセットを探して検証してみました。英語のニュース記事のデータセットで、予めカテゴリが割り振られているので精度検証もできそうだということが選定理由です。

```python
dataset = load_dataset("okite97/news-data")

df = pd.DataFrame({
    'title': dataset['train']['Title'],
    'excerpt': dataset['train']['Excerpt'],
    'category': dataset['train']['Category']
})
```

### ヘルパー関数の定義
```python
#サンプル抽出した記事群のテキストを並べる関数（LLMに事前情報として与えるため）
def pickuped_articles(df):
  texts = ""
  for index, row in df.iterrows():
    texts += "\n article  \n "
    texts += row["excerpt"]
  return texts

#元のデータセットから任意の数をサンプル抽出する関数
def sample_df(df, num):
  df_sample = df.sample(num)
  return df_sample

#指定した質問に対するLLMの回答を得る関数
def chat_res(system, query):
  res = client.chat.completions.create(
      model = "gpt-4",
      messages = [
          {"role": "system", "content": system},
           {"role": "user", "content": query}
          ]
      )
  res_content = res.choices[0].message.content
  return res_content
```
### カテゴリの生成
まず、サンプル記事からカテゴリ案を生成します。
100記事ランダム抽出した記事をLLMに見せ、どんなカテゴリが考えられるかを問います。
これを20回繰り返し幅広くアイデアを集めます。

```python
system = """
あなたは優秀なニュース編集員です。
ユーザーがいくつかのニュース記事を入力します。
これらの内容を見て、分類に使用できそうなカテゴリ名を検討してください。
例のように、カテゴリ名だけをカンマ区切りで返してください。重複したものは書かないでください。

###回答例
スポーツ,芸能,音楽
"""

topics = []
for i in range(20):
  _df = sample_df(df, 100)
  texts = pickuped_articles(_df)
  res = chat_res(system, texts)
  topics.append(res)

```
```
#topics
['政治,エンタメ,スポーツ,ビジネス,経済,ヘルス,テクノロジー',
 'スポーツ, 政治, 経済, ビジネス, テクノロジー, 健康, エンターテイメント, 国際, 環境',
 '政治, 経済, スポーツ, テクノロジー, 国際, 健康, 法律, エンタメ',
 '経済, 政治, スポーツ, テクノロジー, ヘルスケア, エンターテイメント, 国際関係, 教育, 法律, 環境, 
 (以下略)
 ]
```


次に、生成されたカテゴリを整理・統合します。
上で作られたtopicsを分析して、10個以内のカテゴリに絞ります。
```python
system02 = """
あなたは優秀なニュース編集員です。
様々なニュースを分類する必要があります。
カテゴリ名の案を、重複も含めてユーザーが入力します。10個以内に収まるよう、内包関係にあるカテゴリはまとめていく必要があります。
例えば、音楽はエンターテインメントにまとめます。技術とテクノロジーは一つにまとめられるでしょう。
最終的にどのカテゴリで分類すべきかを整理し、例のようにカンマ区切りで返してください。

###回答例(7個以内)
ビジネス,スポーツ,エンタメ,政治,テクノロジー
"""
new_categories = chat_res(system02, str(topics))
print(new_categories)
```
```
#結果
政治,経済,エンターテインメント,スポーツ,テクノロジー,健康,国際,環境
```


### 記事の分類
生成されたカテゴリを使用して、各記事を分類します。

```python
system03 = """
あなたは優秀なニュース編集員です。
様々なニュースを分類する必要があります。
ユーザーが記事を入力します。それを読み、下記のカテゴリのどれに当てはまるかを回答してください。
回答はカテゴリだけでいいです。当てはまるものがない場合は「その他」としてください。

### カテゴリ（これのどれに当てはまるかを考える。ない場合は「その他」）
{}

### ユーザーの入力例
Uefa has opened disciplinary proceedings against Barcelona, Juventus and Real Madrid over their involvement in the proposed European Super League.
### 回答例
スポーツ
""".format(new_categories)

predicted_categories_list = []
df_test = sample_df(df, 200)
for index, row in df_test.iterrows():
  predicted_category = chat_res(system03, row["excerpt"])
  predicted_categories_list.append(predicted_category)
df_test["predicted_category"] = predicted_categories_list
print(df_test[["category","predicted_category"]])
```
これにより、`df_test`に`predicted_category`という、LLMによって予測されたカテゴリを追記できました。

### 結果の可視化
結果を散布図で可視化します。
テキストデータをベクトル化し、それを2次元に圧縮しています。もっと当てはまりの良い方法はあると思いますが、今回あまりこだわっていません。

```python
def scatter_plot(df, text_col, category_col, title='Excerpt Vectors Visualization'):
    # フォントの設定
    plt.rcParams['font.family'] = 'IPAexGothic'

    # テキストデータをベクトル化
    vectorizer = TfidfVectorizer()
    X = vectorizer.fit_transform(df[text_col])

    # 2次元に圧縮
    svd = TruncatedSVD(n_components=2)
    X_2d = svd.fit_transform(X)

    # 散布図を描画
    plt.figure(figsize=(12, 8))
    categories = df[category_col].unique()
    colors = plt.cm.rainbow(np.linspace(0, 1, len(categories)))

    for category, color in zip(categories, colors):
        mask = df[category_col] == category
        plt.scatter(X_2d[mask, 0], X_2d[mask, 1], c=[color], label=category, alpha=0.7)

    plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0)
    plt.title(title, fontsize=16)
    plt.xlabel('First SVD component', fontsize=12)
    plt.ylabel('Second SVD component', fontsize=12)
    plt.tight_layout()
    plt.show()

# 全体の確認
scatter_plot(df,"excerpt", "category")

# テストデータの元のカテゴリ
scatter_plot(df_test,"excerpt", "category")

# 予測されたカテゴリ
scatter_plot(df_test,"excerpt", "predicted_category")
```

![ダウンロード (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/b0efe5ed-f051-18b7-7b10-dbb25eb87ed6.png)
元々のデータセットの分布。政治記事が右上に、エンターテインメントが中央付近、ビジネスが下に広がっていくなど、記事のカテゴリによって一定の分布傾向が見られます。

![ダウンロード (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/85dace8e-ff3e-d5c3-732b-332da83e2fad.png)
こちらがサンプル抽出したデータ200件の、もともと付与されていたカテゴリでの分布。ラベルの色が異なっているので少々わかりにくいですが政治が右上に広がっていくところなどは親データと同様の傾向を持ちます。スポーツの分布が親ほど明確でないので少し偏ったかもしれません。

![ダウンロード (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/6040cf94-f368-e4ac-7ffc-1744bff8c01c.png)
LLMでカテゴライズしたもの。カテゴリ自体が新たに作られたものであり親データとは一致しないですが一つ一つのプロットを見ると概ね同様のカテゴリになっているようです。

### 生データの観察
生データも観察します。
元々のラベルと予測ラベルで異なっているのは例えば下記のようなものです。
| テキスト | カテゴリ | 予測カテゴリ |
|---------|---------|-------------|
| South African President Cyril Ramaphosa on Sunday announced that the nation will move from coronavirus alert level three to level one in | health | 政治 |
| Mexican President Andrés Manuel López Obrador has tested positive for COVID-19, a development that comes as his country registers the | health | 政治 |
| Bitcoin slipped 4% on Friday after Tesla boss Elon Musk was at it again - this time firing off a | tech | 経済 |
| Sunday's attack on St Francis Catholic Church, Owa-luwa Street, Owo Kingdom, Ondo State, by criminal terrorists, leaving scores dead in | politics | 国際 |

コロナウイルス関連がヘルスよりも政治関連だと分類されることがあるということでしょうか。少し意外な面が見られました。


### 結果と考察
正解データがあるので粗も目立ちますが、データの前処理などを特段行うことなくただデータを流し込んでLLMに指示を与えるだけである程度の分類ならできそうです。実務では、どの程度の精度を求めるかを明確にし、必要に応じて細かい設定をしていく形になるかと思います。

### まとめ
本記事では、生成AIを使って記事の自動分類を行う方法を紹介しました。カテゴリの生成から個別の記事の分類まで、生成AIを活用することで、柔軟かつ効率的な分類が可能になることが示唆されました。
今後の課題としては、生成AIの性能向上に伴う精度の改善、より大規模なデータセットでの検証、アンケートの様な記述自由度の高い文書への適用などが挙げられます。

### この記事を書いた人
https://x.com/RckLVnPtRv61824