WRIMEデータベースを学習素材として８感情分析ができるモデルを作成する【2024年版】

# はじめに
本記事ではTransformerを用いた８感情分析を行うAIモデルを紹介します。

# 参考記事
ベースとなる記事はこちらです。2024年時点だと、バージョンが変わっているためこの記事をそのまま転写だと上手く実装できません。ですのでこちらの記事から少し作業する必要があります。

https://qiita.com/izaki_shin/items/2b4573ee7fbea5ec8ed6

WRIMEデータベースは以下から。無償で貴重なデータベースを公開してくださったことに感謝しかありません。利用用途は研究用途。再配布不可となっています。

https://github.com/ids-cv/wrime/tree/master

# Python環境
VSCode + venv - Python(3.12.1)
CPU: AMD Ryzen 7 5800X
GPU: NVIDIA GeForce RTX 3070Ti

# 全体の流れ
TransformerにWRIMEデータを食べてもらって学習してもらい、その結果を使って８感情分析を行うという流れです。

なお、8感情分析はアメリカの心理学者Robert Plutchik氏が提唱した人間の感情の分類型です。もっと細かい27種類にも分けられたモデルもあります。

# code
以下に実際に学習させたコードをご紹介します。

## ライブラリインストール
ライブラリは次の項目を用います。これ以外にも動作させるためにfugashiとipadicのインストールが必要です。
``` python
# 基本形
import requests
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# transformers系
from transformers import AutoTokenizer
from transformers import AutoModelForSequenceClassification
from tqdm.notebook import tqdm

# transfomersを使って学習訓練を実行
from transformers import TrainingArguments, Trainer
# from datasets import load_metric
import evaluate
from datasets import Dataset
```

## WRIMEのインストール
こちらもurlからダウンロードしておきます。v2は35000行用意されています。

``` python
# データセットのURL
url = "https://github.com/ids-cv/wrime/raw/master/wrime-ver2.tsv"

# ダウンロードしたファイルの保存先
file_name = "wrime-ver2.tsv"

# HTTP GETリクエストを送信してファイルをダウンロード
response = requests.get(url)

# ファイルを保存
with open(file_name, 'wb') as file:
    file.write(response.content)

print(f"{file_name} downloaded successfully.")

# データフレーム型で保存
df_wrime = pd.read_table('wrime-ver2.tsv')
print(df_wrime.info())
```

## データ整形
参考記事にも書かれているように感情測定結果の最頻値が0なので学習させるには扱いにくいです。したがって私もそれに倣って扱うデータを選定しました。

``` python
# Plutchikの8つの基本感情
# （喜び・悲しみ・期待・驚き・怒り・恐れ・嫌悪・信頼）
emotion_names = ['Joy', 'Sadness', 'Anticipation', 'Surprise', 'Anger','Fear','Disgust','Trust']
num_labels = len(emotion_names)

# 客観感情の平均（"Avg. Readers_*"） の値をlist化し、新しい列として定義する
df_wrime['readers_emotion_intensities'] = df_wrime.apply(lambda x: [x['Avg. Readers_' + name] for name in emotion_names], axis=1)

# 感情強度が低いサンプルは除外する
# (readers_emotion_intensities の max が２以上のサンプルのみを対象とする)
is_target = df_wrime['readers_emotion_intensities'].map(lambda x: max(x) >= 2)
df_wrime_target = df_wrime[is_target]

```

TrainデータとTestデータに分割します。扱うデータはTrainが12662行で、Testデータが2102行になりました。
``` python
# train / test に分割する
df_groups = df_wrime_target.groupby('Train/Dev/Test')
df_train = df_groups.get_group('train')
df_test = pd.concat([df_groups.get_group('dev'), df_groups.get_group('test')])
print('train :', len(df_train))  # train : 12662
print('test :', len(df_test))    # test : 2102
```

## BERTの訓練
Transfomersだけでは感情分析ができないので、感情分析させたモデルをWRIMEを使って作成します。まずは前処理として学習させたい文字のTokenizeを行います。こちらの作業は3秒くらいで終わります。

``` python
# 1. Transformers用のデータセット形式に変換
# pandas.DataFrame -> datasets.Dataset
target_columns = ['Sentence', 'readers_emotion_intensities']
train_dataset = Dataset.from_pandas(df_train[target_columns])
test_dataset = Dataset.from_pandas(df_test[target_columns])

# 2. Tokenizerを適用（モデル入力のための前処理）
def tokenize_function(batch):
    """Tokenizerを適用 （感情強度の正規化も同時に実施する）."""
    tokenized_batch = tokenizer(batch['Sentence'], truncation=True, padding='max_length')
    tokenized_batch['labels'] = [x / np.sum(x) for x in batch['readers_emotion_intensities']]  # 総和=1に正規化
    return tokenized_batch

# 前処理（tokenize_function） を適用
train_tokenized_dataset = train_dataset.map(tokenize_function, batched=True)
test_tokenized_dataset = test_dataset.map(tokenize_function, batched=True)
```

## メトリクスのロード
参考記事ではload_metricを利用していますが、versionが古く、相性が合わないため以下のコードで評価指標を定義しています。

``` python
# 評価指標を定義
# https://huggingface.co/docs/transformers/training

# メトリクスのロード
metric = evaluate.load("accuracy")

# versionが古いのでload_metricは利用しない
# metric = load_metric("accuracy")

def compute_metrics(eval_pred):
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)
    label_ids = np.argmax(labels, axis=-1)
    return metric.compute(predictions=predictions, references=label_ids)
```

## テンソルの構築設定
どうやらバージョンが変わって、Pytorchでは非連続なテンソル配置にするとエラーが発生する様子。なのでテンソルに対して .contiguous() メソッドを適用して、連続的なメモリ配置に変換してから保存を試みました。

``` python
# モデル内の全てのテンソルを連続化する関数
def make_model_contiguous(model):
    for param in model.parameters():
        if not param.is_contiguous():
            param.data = param.data.contiguous()

# Trainerの実行前にこの関数を呼び出して、モデルのすべてのテンソルを連続化
make_model_contiguous(model)
```

## モデルの学習
以下のコードで8感情分析を学習させます。私の環境だとこの仕様で完了まで4時間半ほどかかりました。気長に待ちましょう。

``` python
# 訓練時の設定
# https://huggingface.co/docs/transformers/v4.21.1/en/main_classes/trainer#transformers.TrainingArguments
training_args = TrainingArguments(
    output_dir="test_trainer",
    per_device_train_batch_size=8, # バッチサイズ　大きいと処理速度が上がるがGPU次第 標準は8
    num_train_epochs=1.0,
    fp16=True,
    evaluation_strategy="steps", eval_steps=200)  # 200ステップ毎にテストデータで評価する

# Trainerの生成
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_tokenized_dataset,
    eval_dataset=test_tokenized_dataset,
    compute_metrics=compute_metrics,
)

# モデルの全パラメータを連続化してから訓練を実行
make_model_contiguous(trainer.model)
trainer.train()

# モデルの保存時にもテンソルを連続化
make_model_contiguous(trainer.model)
trainer.save_model("path_to_save_model")

```

## モデルの推論テスト
学習が終わったら実際に推論がうまくいっているか確認します。

``` python
# https://www.delftstack.com/ja/howto/numpy/numpy-softmax/
def np_softmax(x):
    f_x = np.exp(x) / np.sum(np.exp(x))
    return f_x

def analyze_emotion(text, show_fig=False, ret_prob=False):
    # 推論モードを有効
    model.eval()

    # 入力データ変換 + 推論
    tokens = tokenizer(text, truncation=True, return_tensors="pt")
    tokens.to(model.device)
    preds = model(**tokens)
    prob = np_softmax(preds.logits.cpu().detach().numpy()[0])
    out_dict = {n: p for n, p in zip(emotion_names, prob)}

    # 棒グラフを描画
    if show_fig:
        plt.figure(figsize=(8, 3))
        df = pd.DataFrame(out_dict.items(), columns=['name', 'prob'])
        sns.barplot(x='name', y='prob', data=df)
        plt.title('Text : ' + text, fontsize=15)

    if ret_prob:
        return out_dict

# 動作確認
analyze_emotion('What are you doing? That is s a so terrible accident!', show_fig=True)
```

## モデルの保存
推論が問題なさそうであれば、モデルを保存します。保存先はどこでも問題ありませんが、念のためデフォルトの保存先が良いと思います。どんな事前学習モデルを使って何で学習させたかが分かるように保存します。今回はmodelとtokenizerを分けて保存しましたが同じ場所でも問題ありません。

デフォルト保存先: C:\Users\user_name\\.cache\huggingface\transformers\

``` python
# 訓練が完了したモデルを保存
file_path_model = r"C:\Users\user_name\.cache\huggingface\transformers\8-motion-sentiment-anaylsis_model"
file_path_tokenizer = r"C:\Users\user_name\.cache\huggingface\transformers\8-motion-sentiment-anaylsis_tokenizer"
model.save_pretrained(file_path_model)
tokenizer.save_pretrained(file_path_tokenizer)
```
保存出来たら、保存したmodelとtokenizerを呼び出して推論できるかどうか確認してください。

# 想定事例
データフレーム構造にテキストが入っているとして、そのデータフレームのテキストを一度に分析するサンプルコードです。短い文章であれば英文でもある程度精度はあるように感じます。

``` python
def analyze_emotion_bulk(df, text_column='sentence', id_column='ID', emotion_columns=None):
    # 結果を格納するリスト
    results = []
    
    # 感情の名前を列として使用する
    if emotion_columns is None:
        emotion_columns = ['Joy', 'Trust', 'Fear', 'Surprise', 'Sadness', 'Disgust', 'Anger', 'Anticipation']

    # DataFrameの各行に対して感情分析を実行
    for index, row in df.iterrows():
        text = row[text_column]  # センテンス列から文章を取得
        out_dict = analyze_emotion(text, ret_prob=True)  # 感情の確率分布を取得
        
        # 結果の辞書を感情名に対応するリストに変換
        emotion_probabilities = [out_dict[emotion] for emotion in emotion_columns]
        
        # 元のIDと感情確率を合わせた結果を保存
        results.append([row[id_column]] + emotion_probabilities)
    
    # 結果を新しいDataFrameとして作成
    results_df = pd.DataFrame(results, columns=[id_column] + emotion_columns)
    
    # 元のDataFrameに結果をマージ（ID列で結合）
    df_with_emotions = pd.merge(df, results_df, on=id_column)
    
    return df_with_emotions

# サンプルデータの作成
data = {
    'ID': [1, 2, 3, 4, 5],
    'sentence': [
        'I am so happy today!',
        'This is a terrible event.',
        'I am excited and nervous about the surprise.',
        '昨日の雨は酷かった。川が冠水してたら家まで浸水してたかもしれない',
        '昨日の赤から鍋はとてもおいしかったけど、最後の料理はチーズリゾットよりラーメンのほうがおいしかった。'
    ]
}

df = pd.DataFrame(data)

# 感情の確率をDataFrameに追加
df_with_emotions = analyze_emotion_bulk(df, text_column='sentence', id_column='ID')

# 結果を表示
display(df_with_emotions)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3883350/3d5d0b3d-bbb5-a39e-6e67-6fa111e6df05.png)

3行目が特に興味深く喜びと期待の２トップの感情になっており文章もそれを伺うことができます。