大規模言語モデルのファインチューニング：感情分析モデル

## はじめに

本記事では、WRIMEデータセットを用いて感情分析モデルのファインチューニング手順を解説します。感情分析は、テキストデータから感情的な傾向を抽出する重要なタスクであり、ビジネスや研究において広く利用されています。WRIMEデータセットは、日本語のツイートを対象とした感情分析に適したデータセットです。

## 環境の準備

まず、必要なライブラリをインストールし、乱数シードを固定します。

```python
!pip install transformers[ja,torch] datasets matplotlib japanize-matplotlib
from transformers.trainer_utils import set_seed

# 乱数シードを42に固定（再現性確保のため）
set_seed(42)
```

## データセットの準備

WRIMEデータセットをHugging Face Hubから読み込みます。

```python
from pprint import pprint
from datasets import load_dataset

# WRIMEデータセットの読み込み
train_dataset = load_dataset("llm-book/wrime-sentiment", split="train")
valid_dataset = load_dataset("llm-book/wrime-sentiment", split="validation")

# データセットの内容表示
pprint(train_dataset[0])
pprint(train_dataset.features)
```

**出力:**

```python
{'datetime': '2012/7/31 23:48',
 'label': 1,
 'sentence': 'ぼけっとしてたらこんな時間。チャリあるから食べにでたいのに…',
 'user_id': 1}
{'datetime': Value(dtype='string', id=None),
 'label': ClassLabel(names=['positive', 'negative'], id=None),
 'sentence': Value(dtype='string', id=None),
 'user_id': Value(dtype='int64', id=None)}
```

WRIMEデータセットは、日本語のツイートを含み、ラベルはポジティブとネガティブの二値分类です。

## トークナイザ

日本語BERTモデルのトークナイザを読み込み、テキストをトークン化します。

```python
from transformers import AutoTokenizer

# トークナイザの読み込み
model_name = "cl-tohoku/bert-base-japanese-v3"
tokenizer = AutoTokenizer.from_pretrained(model_name)

# トークナイザの動作確認
print(type(tokenizer).__name__)
tokenizer.tokenize("これはテストです。")
encoded_input = tokenizer("これはテストです。")
print(type(encoded_input).__name__)
pprint(encoded_input)
tokenizer.convert_ids_to_tokens(encoded_input["input_ids"])
```

**出力:**

```python
BertJapaneseTokenizer
BatchEncoding
{'attention_mask': [1, 1, 1, 1, 1, 1, 1],
 'input_ids': [2, 12538, 465, 14985, 13037, 385, 3],
 'token_type_ids': [0, 0, 0, 0, 0, 0, 0]}
['[CLS]', 'これ', 'は', 'テスト', 'です', '。', '[SEP]']
```

日本語特化のBERTモデルのトークナイザを使用し、入力テキストをトークン化します。

## データセット統計の可視化

データセット内のテキスト長とラベル分布を可視化します。

```python
from collections import Counter
import japanize_matplotlib
import matplotlib.pyplot as plt
from datasets import Dataset
from tqdm import tqdm

plt.rcParams["font.size"] = 18  # 文字サイズを大きくする

def visualize_text_length(dataset: Dataset):
    """テキストのトークン数分布を可視化"""
    length_counter = Counter()
    for data in tqdm(dataset):
        length = len(tokenizer.tokenize(data["sentence"]))
        length_counter[length] += 1
    plt.bar(length_counter.keys(), length_counter.values(), width=1.0)
    plt.xlabel("トークン数")
    plt.ylabel("事例数")
    plt.show()

visualize_text_length(train_dataset)
visualize_text_length(valid_dataset)
```

![image-20250119134624559.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3932781/e7b24e50-edf5-091c-8967-4ec0b10b5c13.png)

![image-20250119134639944.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3932781/c6769d57-1d4d-b880-f2a8-f9c7ab4e0aa1.png)


```
def visualize_labels(dataset: Dataset):
    """ラベル分布を可視化"""
    label_counter = Counter()
    for data in dataset:
        label_id = data["label"]
        label_name = dataset.features["label"].names[label_id]
        label_counter[label_name] += 1
    plt.bar(label_counter.keys(), label_counter.values(), width=1.0)
    plt.xlabel("ラベル")
    plt.ylabel("事例数")
    plt.show()

visualize_labels(train_dataset)
visualize_labels(valid_dataset)
```

![image-20250119134706430.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3932781/b412951e-c2ef-e73e-0303-0e60debeb435.png)

![image-20250119134726190.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3932781/5dc6966a-41d0-df3e-226e-0e83ff78e4ac.png)
## データセットの前処理

テキストをトークン化し、モデルの入力形式に変換します。

```python
from transformers import BatchEncoding

def preprocess_text_classification(example: dict[str, str | int]) -> BatchEncoding:
    """テキストをトークン化し、ラベルを追加"""
    encoded_example = tokenizer(example["sentence"], max_length=512)
    encoded_example["labels"] = example["label"]
    return encoded_example

encoded_train_dataset = train_dataset.map(preprocess_text_classification, remove_columns=train_dataset.column_names)
encoded_valid_dataset = valid_dataset.map(preprocess_text_classification, remove_columns=valid_dataset.column_names)
print(encoded_train_dataset[0])
```

**出力:**

```python
{'input_ids': [2, 478, 7139, 15269, 441, 456, 13123, 21311, 12671, 385, 12651, 7065, 12485, 12488, 13781, 461, 457, 13030, 464, 461, 29, 29, 29, 3], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'labels': 1}
```

## ミニバッチ構築

データセットをミニバッチに分割します。

```python
from transformers import DataCollatorWithPadding

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
batch_inputs = data_collator(encoded_train_dataset[0:4])
pprint({name: tensor.size() for name, tensor in batch_inputs.items()})
```

**出力:**

```python
{'attention_mask': torch.Size([4, 36]),
 'input_ids': torch.Size([4, 36]),
 'labels': torch.Size([4]),
 'token_type_ids': torch.Size([4, 36])}
```

## モデルの準備

感情分析用のモデルを読み込みます。

```python
from transformers import AutoModelForSequenceClassification

class_label = train_dataset.features["label"]
label2id = {label: id for id, label in enumerate(class_label.names)}
id2label = {id: label for id, label in enumerate(class_label.names)}

model = AutoModelForSequenceClassification.from_pretrained(
    model_name,
    num_labels=class_label.num_classes,
    label2id=label2id,
    id2label=id2label,
)

# パラメータをメモリ上に隣接した形で配置
for param in model.parameters():
    param.data = param.data.contiguous()

print(model.forward(**data_collator(encoded_train_dataset[0:4])))
```

**出力:**

```python
SequenceClassifierOutput(loss=tensor(0.8075, grad_fn=<NllLossBackward0>), logits=tensor([[ 0.2869, -0.2644],
        [ 0.1715,  0.0095],
        [ 0.0040,  0.1524],
        [ 0.2233, -0.2968]], grad_fn=<AddmmBackward0>), hidden_states=None, attentions=None)
```

## 訓練の実行

モデルの訓練を実行します。

```python
from transformers import TrainingArguments

training_args = TrainingArguments(
    output_dir="output_wrime",
    per_device_train_batch_size=32,
    per_device_eval_batch_size=32,
    learning_rate=2e-5,
    lr_scheduler_type="linear",
    warmup_ratio=0.1,
    num_train_epochs=3,
    save_strategy="epoch",
    logging_strategy="epoch",
    evaluation_strategy="epoch",
    load_best_model_at_end=True,
    metric_for_best_model="accuracy",
    fp16=True,
    report_to="none",
)

import numpy as np

def compute_accuracy(eval_pred: tuple[np.ndarray, np.ndarray]) -> dict[str, float]:
    """正解率を計算"""
    predictions, labels = eval_pred
    predictions = np.argmax(predictions, axis=1)
    return {"accuracy": (predictions == labels).mean()}

from transformers import Trainer

trainer = Trainer(
    model=model,
    train_dataset=encoded_train_dataset,
    eval_dataset=encoded_valid_dataset,
    data_collator=data_collator,
    args=training_args,
    compute_metrics=compute_accuracy,
)

trainer.train()
```

## 訓練後のモデルの評価

訓練後のモデルを検証セットで評価します。

```python
eval_metrics = trainer.evaluate(encoded_valid_dataset)
pprint(eval_metrics)
```

## モデルの保存

訓練済みモデルをGoogle DriveまたはHugging Face Hubに保存します。

### Google Driveへの保存

```python
from google.colab import drive

drive.mount("drive")
!mkdir -p drive/MyDrive/llm-book
!cp -r output_wrime drive/MyDrive/llm-book
```

### Hugging Face Hubへの保存

```python
from huggingface_hub import login

login()
repo_name = "YOUR-ACCOUNT/bert-base-japanese-v3-wrime-sentiment"
tokenizer.push_to_hub(repo_name)
model.push_to_hub(repo_name)
```

## まとめ

本記事では、WRIMEデータセットを用いた感情分析モデルのファインチューニング手順を解説しました。WRIMEデータセットの特徴とモデルファインチューニングの重要性を理解し、実装手法を学ぶことができました。ぜひ自身の環境で試してみてください。

---

参考資料：

- [https://github.com/ghmagazine/llm-book](https://github.com/ghmagazine/llm-book)