8つの感情を分析できるように言語モデルLUKEをファインチューニングしてみた

こんにちにゃんです。
水色桜（みずいろさくら）です。
今回は言語モデルLUKEをファインチューニングして、８つの感情を分析できるようにしたいと思います。
今回やりたいことは文章が与えられたとき、８つの感情（喜び、悲しみ、期待、驚き、怒り、恐れ、嫌悪、信頼）のうちどの感情が最も強く含まれているのかを判別できるようにすることです。
つまり、多クラス分類と考えて、分析を行います。
今回作成したモデルは[こちら(Hugging Face)](https://huggingface.co/Mizuiro-sakura/luke-japanese-large-sentiment-analysis-wrime)で配布していますので、ぜひ利用してみてください。

# LUKE
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2847349/99114ada-239d-9577-e773-b8f789d23b7d.png)


2020年4月当時、5つのタスクで世界最高精度を達成した新しい言語モデル。
日本語バージョンのLUKE-baseモデルは執筆現在（2022年12月）も4つのタスク(MARC-ja, JSTS, JNLI, JCommonsenseQA)で最高スコアを有しています。LUKEはRoBERTaを元として構成され、entity-aware self-attentionという独自のメカニズムを用いています。LUKEに関して詳しくは下記記事をご覧ください。

https://qiita.com/Mizuiro__sakura/items/9ccbd655501e78df5cc6

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2847349/b09faef2-10d8-5052-b3b1-730cf1901792.png)

# 環境
pandas 1.4.4
numpy 1.23.4
torch 1.12.1
transformer 4.24.0
Python 3.9.13
sentencepiece 0.1.97

# [wrimeデータセット](https://www.jstage.jst.go.jp/article/jnlp/28/3/28_907/_pdf/-char/ja)
愛媛大学梶原智之先生が作成された感情分析のためのデータセット。テキストの書き手と読み手の両方の立場から感情強度が付与されています。ver1は43200文、ver2は35000文が収録されています。

```ruby: wrimeデータセットに含まれるデータの例
{
    "sentence": "ぼけっとしてたらこんな時間｡チャリあるから食べにでたいのに…",
    "user_id": "1",
    "datetime": "2012/07/31 23:48",
    "writer": {
        "joy": 0,
        "sadness": 1,
        "anticipation": 2,
        "surprise": 1,
        "anger": 1,
        "fear": 0,
        "disgust": 0,
        "trust": 1
    },
    "reader1": {
        "joy": 0,
        "sadness": 2,
        "anticipation": 0,
        "surprise": 0,
        "anger": 0,
        "fear": 0,
        "disgust": 0,
        "trust": 0
    },
    "reader2": {
        "joy": 0,
        "sadness": 2,
        "anticipation": 0,
        "surprise": 1,
        "anger": 0,
        "fear": 0,
        "disgust": 0,
        "trust": 0
    },
    "reader3": {
        "joy": 0,
        "sadness": 2,
        "anticipation": 0,
        "surprise": 0,
        "anger": 0,
        "fear": 1,
        "disgust": 1,
        "trust": 0
    },
    "avg_readers": {
        "joy": 0,
        "sadness": 2,
        "anticipation": 0,
        "surprise": 0,
        "anger": 0,
        "fear": 0,
        "disgust": 0,
        "trust": 0
    }
}
```

# 準備（ライブラリのインストール）

```
pip install datasets
pip install torch
pip install transformers
pip install sentencepiece
```

# データのロードと学習

```python:train.py
from datasets import load_dataset

dataset = load_dataset("shunk031/wrime", name="ver1") 
dataset2 = load_dataset("shunk031/wrime", name="ver2")

# DatasetDict({
#     train: Dataset({
#         features: ['sentence', 'user_id', 'datetime', 'writer', 'reader1', 'reader2', 'reader3', 'avg_readers'],
#         num_rows: 40000
#     })
#     validation: Dataset({
#         features: ['sentence', 'user_id', 'datetime', 'writer', 'reader1', 'reader2', 'reader3', 'avg_readers'],
#         num_rows: 1200
#     })
#     test: Dataset({
#         features: ['sentence', 'user_id', 'datetime', 'writer', 'reader1', 'reader2', 'reader3', 'avg_readers'],
#         num_rows: 2000
#     })
# })

print(dataset['train']['avg_readers'][0])
import numpy as np
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("studio-ousia/luke-japanese-large-lite")
from tqdm import tqdm
tqdm.pandas()

# データセットを作成するための下準備
data_dict=dataset['train']['sentence']
max_seq_length = 128
data=tokenizer(
        data_dict,
        truncation=True,
        max_length=max_seq_length,
        padding="max_length"
    )
print('fini')
import torch
import re
tensor_input_ids = torch.tensor(data["input_ids"])
tensor_attention_masks = torch.tensor(data["attention_mask"])
answer=np.zeros((len(dataset['train']),8))
pre=np.zeros(8)
dataset_t=dataset['train']['avg_readers']
for i in range(len(dataset['train'])):
   pre=str(dataset_t[i]).split(',')
   for j in range(8):
      pre[j]=re.sub(r"\D","",pre[j])
   max_index=np.argmax(pre)
   for j in range(8):
      if j==max_index:
         answer[i][j]=1
      else:
         answer[i][j]=0
print('fini')
data_dict_val=dataset['validation']['sentence']
max_seq_length = 128
data_val=tokenizer(
        data_dict_val,
        truncation=True,
        max_length=max_seq_length,
        padding="max_length"
    )

import torch
tensor_input_ids_val = torch.tensor(data_val["input_ids"])
tensor_attention_masks_val = torch.tensor(data_val["attention_mask"])
answer_val=np.zeros((len(dataset['validation']),8))
dataset_v = dataset['validation']['avg_readers']
for i in range(len(dataset['validation'])):
   pre=str(dataset_v[i]).split(',')
   for j in range(8):
      pre[j]=re.sub(r"\D","",pre[j])
   max_index=np.argmax(pre)
   for j in range(8):
      if j==max_index:
         answer_val[i][j]=1
      else:
         answer_val[i][j]=0

# 正解データの取得
tensor_labels = torch.tensor(answer)

# 正解データの取得
tensor_labels_val = torch.tensor(answer_val)

from torch.utils.data import Dataset

# データセットの作成
class CQADataset(Dataset):
  def __init__(self, input,attention,label, is_test=False):
    self.input = input
    self.attention=attention
    self.label=label
    self.is_test = is_test

  def __getitem__(self, idx):
    data = {'input_ids': torch.tensor(self.input[idx])}
    data['attention_mask']= torch.tensor(self.attention[idx])
    data['label']= torch.tensor(self.label[idx])
    return data

  def __len__(self):
    return len(self.input)

dataset_train = CQADataset(tensor_input_ids,tensor_attention_masks,tensor_labels)
indices = np.arange(len(dataset_train))
dataset_valid = CQADataset(tensor_input_ids_val, tensor_attention_masks_val, tensor_labels_val)

from transformers import AutoModelForSequenceClassification
from transformers import Trainer, TrainingArguments
model=AutoModelForSequenceClassification.from_pretrained("studio-ousia/luke-japanese-large-lite", num_labels = 8)


from sklearn.metrics import accuracy_score, precision_recall_fscore_support

def compute_metrics(pred):
    labels = pred.label_ids
    preds = pred.predictions.argmax(-1)
    #2値分類ならaverage='binary'とする
    precision, recall, f1, _ = precision_recall_fscore_support(labels, preds, average='weighted', zero_division=0)
    acc = accuracy_score(labels, preds)
    return {
        'accuracy': acc,
        'precision': precision,
        'recall': recall,
        'f1': f1,
    }


# トレーニングの設定を記述する
training_config = TrainingArguments(
  output_dir = 'C://Users//tomot//desktop//Python//luke_wrime',
  num_train_epochs = 3, 
  per_device_train_batch_size = 8,
  per_device_eval_batch_size = 8,
  learning_rate = 1e-5,
  weight_decay = 0.1,
  save_steps = 5000,
  eval_steps=15000,
  do_eval = True
  
)

# トレーニング
trainer = Trainer(
    model = model,                         
    args = training_config,
    tokenizer = tokenizer,
    train_dataset = dataset_train,
    eval_dataset = dataset_valid,
    compute_metrics = compute_metrics, #compute_metrics
)

trainer.train()
trainer.evaluate()
print('saved')
```

# 推論の実行
以下のコードを実行することで、文章中に含まれている感情を分析することができます。

```python:example.py
from transformers import AutoTokenizer, AutoModelForSequenceClassification, LukeConfig
import torch
tokenizer = AutoTokenizer.from_pretrained("Mizuiro-sakura/luke-japanese-large-sentiment-analysis-wrime")
config = LukeConfig.from_pretrained('Mizuiro-sakura/luke-japanese-large-sentiment-analysis-wrime', output_hidden_states=True)    
model = AutoModelForSequenceClassification.from_pretrained('Mizuiro-sakura/luke-japanese-large-sentiment-analysis-wrime', config=config)

text='すごく楽しかった。また行きたい。'

max_seq_length=512
token=tokenizer(text,
        truncation=True,
        max_length=max_seq_length,
        padding="max_length")
output=model(torch.tensor(token['input_ids']).unsqueeze(0), torch.tensor(token['attention_mask']).unsqueeze(0))
max_index=torch.argmax(torch.tensor(output.logits))

if max_index==0:
    print('joy、うれしい')
elif max_index==1:
    print('sadness、悲しい')
elif max_index==2:
    print('anticipation、期待')
elif max_index==3:
    print('surprise、驚き')
elif max_index==4:
    print('anger、怒り')
elif max_index==5:
    print('fear、恐れ')
elif max_index==6:
    print('disgust、嫌悪')
elif max_index==7:
    print('trust、信頼')
```



# 終わりに
今回はwrimeデータセットを用いて感情分析を行ってみました。
ぜひ皆さんも感情分析をしてみてください。
では、ばいにゃん～。