BERTで分類(Classification)タスクのファインチューニングを行う

# はじめに
`BERT`で`Classification`タスクのファインチューニンングをColab(とMac)で行う時のメモです。

素の`BERT`だと事前学習済みで公開されているモデルの仕様が古いのか、最近のバージョンの環境では実行できなかったため、`BERT`の改良版である`RoBERTa`の事前学習済みモデルにて実施します。


# 事前に必要なものを入れる
``` sh
!pip install sentencepiece
!pip install datasets
!pip install transformers
```

Macで動かすときは以下も実施。

``` sh
$ brew install jumanpp 
$ pip install protobuff 
```

# 学習用データを用意する

以下のような形式でJSONファイルを作成する。

``` json
[
    {
        "text": "東京は日本の首都です", // 本文
        "label": 2, // 分類番号
    },
    {
        "text": "明日の天気は晴れでしょう",
        "label": 3,
    },
    ...
]
```

# 学習用のコードを用意する

## トークナイザーを取得する

``` python
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("nlp-waseda/roberta-base-japanese")
```

## 学習用データを読み込む
``` python
# 学習用データの読み込み
from google.colab import drive
from sklearn.model_selection import train_test_split
from datasets import Dataset, DatasetDict
import pandas as pd

drive.mount('/mount')
data = pd.read_json('/mount/MyDrive/xxxx.json')

train, valid = train_test_split(data, test_size=0.25)

ds_train = Dataset.from_pandas(train)
ds_valid = Dataset.from_pandas(valid)

dataset = DatasetDict({
    "train": ds_train,
    "validation": ds_valid,
})
```

## 読み込んだデータを学習用に変換する

文章をトークンに変換する。

``` python
import torch

# 変換関数
def preprocess_function(data):
    texts = [q.strip() for q in data["text"]]
    inputs = tokenizer(
        texts,
        max_length=450,
        truncation=True,
        padding=True,
    )

    inputs['labels'] = torch.tensor(data['label'])

    return inputs


# 変換
tokenized_data = dataset.map(preprocess_function, batched=True)
```

## 分類用のモデルを取得する

``` python
from transformers import AutoModelForSequenceClassification

num_labels = 5 # 分類する種類の数

# デバイス判定
device = "cuda:0" if torch.cuda.is_available() else "cpu"

model = AutoModelForSequenceClassification.from_pretrained("nlp-waseda/roberta-base-japanese", num_labels=num_labels).to(device)
```

## パラメータを設定する

EarlyStoppingしたいので、`load_best_model_at_end`をTrueにする。

``` python
from transformers import TrainingArguments

training_args = TrainingArguments(
    output_dir="/mount/MyDrive/yyyy",
    evaluation_strategy="epoch",
    save_strategy="epoch",
    save_total_limit=1,
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    num_train_epochs=200,
    weight_decay=0.01,
    load_best_model_at_end=True,
)

from transformers import default_data_collator
data_collator = default_data_collator
```

## 学習を実行する

``` python
from sklearn.model_selection import train_test_split
from transformers import Trainer, EarlyStoppingCallback

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_data["train"],
    eval_dataset=tokenized_data["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
    callbacks=[EarlyStoppingCallback(early_stopping_patience=30)],
)

trainer.train()
```

# 学習後の動作確認

``` python
from torch import nn

def predict(text):
    inputs = tokenizer(text, add_special_tokens=True, return_tensors="pt").to(device)
    outputs = model(**inputs)
    ps = nn.Softmax(1)(outputs.logits)

    max_p = torch.max(ps)
    result = torch.argmax(ps).item() if max_p > 0.8 else -1
    return result

result = predict('明日の天気は晴れでしょう')

print(result)
```

`3`が表示されるはず。

# 参考サイト

- https://huggingface.co/docs/transformers/v4.19.2/en/tasks/question_answering