huggingface/transformersのTrainerの使い方と挙動

　Trainerは便利だが，中で何がどう動いているか分からないと怖くて使えないので，メモ。公式ドキュメントでの紹介は[ここ](https://huggingface.co/docs/transformers/training#finetuning-in-pytorch-with-the-trainer-api)。

# 基本的な使い方

```py
from transformers import Trainer, TrainingArguments 

tokenizer=AutoTokenizer.from_pretrained('bert-base-uncased')
model=AutoModel.from_pretrained('bert-base-uncased')

args=TrainingArguments(
    output_dir='ディレクトリパス'
)

trainer=Trainer(
    args=args,
    model=model,
    tokenizer=tokenizer,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset
)

trainer.train()
trainer.evaluate()
```

まず，TrainerとTrainingArgumentsをimportする。また，モデルとトークナイザとして`AutoModel`と`AutoTokenizer`を例示するが，モデルは`BertModel`でも`BertForSequenceClassification`でも何でも良いし，トークナイザも同様である。

次に，`TrainingArguments`を作成する。必須なのは`output_dir`のみである。ここに学習の途中経過が保存される。

続いて，`Trainer`を作成する。評価をしないならばeval_datasetは不要。訓練データ・評価データともに，`torch.utils.data.Dataset`である必要がある（`Dataset`については[ここ](https://pytorch.org/docs/stable/data.html#torch.utils.data.Dataset)が一番分かりやすい）。

最後に，`train()`で学習し，`evaluate()`で評価する。評価はしなくても良い。なお，`evaluate()`の結果はコンソールには表示されず，TensorBoardで確認する羽目になる。標準出力で確認したければ，`Trainer`の`compute_metrics`にCallableを設定して，そのCallable（関数など）で何か表示させるようにプログラムを書く必要がある。

BERTはエポック数，バッチサイズ，学習率の3つをチューニングする必要がある（BERTの元論文）が，以下の通り設定できる。

# パラメータの説明

## Trainer

Trainerの引数でよく使うのは以下。

|パラメタ|引数|説明|
|-|-|-|
|args|TrainingArguments|下記参照。|
|train_dataset|Dataset|訓練用データ。バッチ処理は自動的に行われる。また，データはランダムに並べ替えられる。|
|eval_dataset|Dataset|評価用データ。バッチ処理は自動的に行われる。データは並べ替えられない。|
|tokenizer|PreTrainedTokenizerBase|ここでtokenizerを指定しない場合，後からモデルを保存してもtokenizerは保存されない。|
|compute_metrics|Callable|評価指標を自由に設定できる。デフォルトだと結果は全てログに流れていくので，ここで`print`しないと標準（エラー）出力には何も出てこない。
|model|PreTrainedModel|BERTなど好きなモデルを指定する。BertModelを派生させて自分で作ったものでもOK。|

## TrainingArguments

TrainingArgumentsの引数でよく使うのは以下。

|パラメタ|引数|説明|
|-|-|-|
|output_dir|str|ログや学習途中のモデルが出力される。必須。|
|do_train|bool|指定する必要がない。|
|do_eval|bool|指定する必要がない。|
|learning_rate|float|`5e-5`などと指定する。デフォルトは`5e-5`。|
|num_train_epochs|float|エポック数を指定する。デフォルトは`3.0`。|
|per_device_train_batch_size|int|訓練時のGPU1枚ごとのバッチサイズを指定する。デフォルトは8。|
|per_device_eval_batch_size|int|評価時のGPU1枚ごとのバッチサイズを指定する。デフォルトは8。|
|evaluation_strategy|str|1エポック毎に評価してほしい場合は`'epoch'`を指定する。こうすると画面に進捗だけでなく正解率などがエポック毎に表示されるようになる。デフォルトは`'no'`。|

GPUの数に応じた最終的なバッチサイズは以下で取得できる。

```py
args.train_batch_size
args.eval_batch_size
```


# 挙動

## データの読み込まれ方

`Trainer`クラス内での挙動について説明する。以下の`get_train_dataloader()`と`_get_train_sampler()`はTrainerクラス内に定義されている。

`train()`時は，`train_dataset`が読み込まれるが，この際に`get_train_dataloader()`によって`DataLoader`が読み込まれる。ここで，`DataLoader`の`sampler`として，`_get_train_sampler()`内で`torch.utils.data.RandomSampler`が指定されている（ただし，`TrainingArguments`で`group_by_length`をTrueにした場合は除く）。従って，訓練データはランダムにモデルへ投入される。

`evaluate()`時は，`eval_dataset`が読み込まれるが，この時には`RandomSampler`は使われない。

## compute_metrics

※2022年4月7日にEvalPredictionが大幅に変更された（[詳細](https://github.com/huggingface/transformers/pull/16461)）。なんと入力が使えるようになっている。

`evaluate()`から呼び出される`evaluation_loop()`内にて，`compute_metrics`で指定したCallableが呼び出される。この時Callableに渡される引数は，`EvalPrediction(predictions=all_preds, label_ids=all_labels)`である。

`EvalPrediction`の`__getitem__`は，次のように定義されている：

|インデックス|戻り値|説明|
|---|---|---|
|0|Union[np.ndarray, Tuple[np.ndarray]]|モデルの出力|
|1|Union[np.ndarray, Tuple[np.ndarray]]|正解ラベル|
|2|Optional[Union[np.ndarray, Tuple[np.ndarray]]]|入力。ただし，`TrainingArguments`で`include_inputs_for_metrics`を`True`にした場合のみ。していない場合はIndexError。|
|それ以外|IndexError|これが出ることによってunpackでの代入ができる。|

よって，

```py
def compute_metrics(res):
    logits, labels=res
```

という書き方ができる。logitsには出力（ただし，損失関数にはかけられていないので，通常のコードであればsoftmax等は適用されていない），labelsにはラベルが入っているので，これらを比較してやれば良い。分類問題であれば，logitsのそれぞれのデータに対してargmaxをして推定ラベルを算出すればよい。いずれも`numpy.ndarray`に変換されていることに注意する（つまり，torch.Tensorの関数は使えず，numpyの関数を使うことになる）。

簡易的に結果を出力したければ，ここで`print()`などすると良い。

compute_metricsは戻り値として`Optional[Dict[str, float]]`が要求されているので，何か返すのであれば，辞書にして返す。例えば，

```py
return {
    'precision': precision, # precisionはfloat
    'recall': recall # recallもfloat
}
```

といった感じである。
　ここで指定したkeyは，この後`metric_key_prefix`によって修飾される（`evaluation_loop`関数内の`# Prefix all keys with metric_key_prefix + '_'`コメントの後）。したがって，最終出力は`'eval_precision': 0.15`のようになる。