pytorch-transformersを触ってみる③
はじめに
前回は日本語でのpytorch-transformersの扱い方についてまとめました。
kento1109.hatenablog.com
これまでpytorch-transformersの基本的なところを英語・日本語で動かしてみました。
今回はもう一歩進んでfine tuningについてまとめてみます。
ドキュメントについては、以下を参考とします。
Examples — pytorch-transformers 1.0.0 documentation
実行
ものすごく簡単です。run_bert_classifier.pyに適切な引数を与えて実行するだけです。
と思ったのですが、github上にこのようなファイル名のスクリプトは存在しません。
このissueについては、既に以下で解決済です。
github.com
run_bert_classifier.pyは前のpytorch-pretrained-bertの時のスクリプトのようで、
今は、run_glue.pyという名称に代わっているみたいです。
※githubのREADMEにもGLUEのfine tuningはrun_glue.pyと書いていますね。。
export GLUE_DIR=/path/to/glue export TASK_NAME=MRPC python ./examples/run_glue.py \ --model_type bert \ --model_name_or_path bert-base-uncased \ --task_name $TASK_NAME \ --do_train \ --do_eval \ --do_lower_case \ --data_dir $GLUE_DIR/$TASK_NAME \ --max_seq_length 128 \ --per_gpu_eval_batch_size=8 \ --per_gpu_train_batch_size=8 \ --learning_rate 2e-5 \ --num_train_epochs 3.0 \ --output_dir /tmp/$TASK_NAME/
GPU環境の場合、1時間もしないうちに学習が終わります。
最後に、以下のような評価結果が表示されればOKかと思います。
08/22/2019 10:59:29 - INFO - __main__ - ***** Eval results ***** 08/22/2019 10:59:29 - INFO - __main__ - acc = 0.9231651376146789
環境設定などでハマらなければとても簡単です。
run_glue.py
GLUEのタスクでは、用意されたスクリプトを実行するだけで結果が得られました。
ほとんど頭で何か考える必要はありませんでした。
しかし、GLUE以外のタスク(自身で用意したデータセットなど)に適用する場合、このスクリプトは使えません。
なので、run_glue.pyでどうやってfine tuningを行ったかをコードを少し見て勉強しようと思います。
具体的には、run_glue.py内でこれまでに見たpretrained modelをどのように扱っているのか学びます。
※distributed training(分散学習)に関する部分は今回は扱いません。
model Instantiate
スクリプト内では、辞書形式で引数に応じたクラスを定義しています。
MODEL_CLASSES = { 'bert': (BertConfig, BertForSequenceClassification, BertTokenizer), 'xlnet': (XLNetConfig, XLNetForSequenceClassification, XLNetTokenizer), 'xlm': (XLMConfig, XLMForSequenceClassification, XLMTokenizer), 'roberta': (RobertaConfig, RobertaForSequenceClassification, RobertaTokenizer), }
引数model_typeにてbertと与えられたら、bertのクラスが呼ばれるようになっています。
valueに存在するクラスについては、以前に扱いました。
BertForSequenceClassificationの出力層の数は「2」なので、分類問題用のモデルとなっています。
これらのクラスを以下のようにして変数にセットします。
config_class, model_class, tokenizer_class = MODEL_CLASSES[args.model_type]
まず、config_classの設定です。
config = config_class.from_pretrained(args.config_name if args.config_name else args.model_name_or_path, num_labels=num_labels, finetuning_task=args.task_name)
デフォルトのハイパーパラメータ以外に以下を設定します。
- num_labels
- finetuning_task
finetuning_taskはドキュメントによると、checkpointの名前で用いるみたいです。
次に、tokenizerを設定します。
tokenizer = tokenizer_class.from_pretrained(args.tokenizer_name if args.tokenizer_name else args.model_name_or_path, do_lower_case=args.do_lower_case)
対象はbert-base-uncasedなので、do_lower_case=Trueとします。
最後にclass methodのfrom_pretrained()により、モデルをインスタンス化します。
model = model_class.from_pretrained(args.model_name_or_path, from_tf=bool('.ckpt' in args.model_name_or_path), config=config)
dataset
次に、datasetについてみていきます。
BERTなどは入力のdatasetが特殊なので、工夫が必要となります。
ちなみに元々の入力データは以下のようになっています。
it 's a charming and often affecting journey . 1 unflinchingly bleak and desperate 0
これをどのように変換するか確認します。
訓練データのload & tokenize & id変換はload_and_cache_examples()で行います。
train_dataset = load_and_cache_examples(args, args.task_name, tokenizer, evaluate=False)
load_and_cache_examplesを読んでみます。
examples = processor.get_dev_examples(args.data_dir) if evaluate else processor.get_train_examples(args.data_dir) features = convert_examples_to_features(examples, label_list, args.max_seq_length, tokenizer, output_mode, cls_token_at_end=bool(args.model_type in ['xlnet']), # xlnet has a cls token at the end cls_token=tokenizer.cls_token, cls_token_segment_id=2 if args.model_type in ['xlnet'] else 0, sep_token=tokenizer.sep_token, sep_token_extra=bool(args.model_type in ['roberta']), # roberta uses an extra separator b/w pairs of sentences, cf. github.com/pytorch/fairseq/commit/1684e166e3da03f5b600dbb7855cb98ddfcd0805 pad_on_left=bool(args.model_type in ['xlnet']), # pad on the left for xlnet pad_token=tokenizer.convert_tokens_to_ids([tokenizer.pad_token])[0], pad_token_segment_id=4 if args.model_type in ['xlnet'] else 0, )
1行目で入力データからexamplesを作成します。
2行目でexamplesを引数として、各入力の形式に変換します。
2つ目のlabel_listはラベルが取り得る値のリストです。
label_list = ["0", "1"]
各exampleの処理を確認します。(一部コードを省略しています。)
features = [] for (ex_index, example) in enumerate(examples): tokens_a = tokenizer.tokenize(example.text_a) # The convention in BERT is: # (a) For sequence pairs: # tokens: [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP] # type_ids: 0 0 0 0 0 0 0 0 1 1 1 1 1 1 # (b) For single sequences: # tokens: [CLS] the dog is hairy . [SEP] # type_ids: 0 0 0 0 0 0 0 tokens = tokens_a + [sep_token] # sep_token = '[SEP]' segment_ids = [sequence_a_segment_id] * len(tokens) tokens = [cls_token] + tokens segment_ids = [cls_token_segment_id] + segment_ids input_ids = tokenizer.convert_tokens_to_ids(tokens) # The mask has 1 for real tokens and 0 for padding tokens. input_mask = [1 if mask_padding_with_zero else 0] * len(input_ids) input_ids = input_ids + ([pad_token] * padding_length) # pad_token=0 input_mask = input_mask + ([0 if mask_padding_with_zero else 1] * padding_length) segment_ids = segment_ids + ([pad_token_segment_id] * padding_length) label_id = label_map[example.label] features.append( InputFeatures(input_ids=input_ids, input_mask=input_mask, segment_ids=segment_ids, label_id=label_id)) return features class InputFeatures(object): """A single set of features of data.""" def __init__(self, input_ids, input_mask, segment_ids, label_id): self.input_ids = input_ids self.input_mask = input_mask self.segment_ids = segment_ids self.label_id = label_id
やっていることは、大まかに言うと
1. tokenize
2. [SEP], [CLS]の挿入
3. id変換
4. padding
5. mask
です。(2.を除いて一般的なNLPタスクとほとんど同じです。)
Training
変換した訓練データと定義済のモデルでfine tuningを行います。
global_step, tr_loss = train(args, train_dataset, model, tokenizer)
train()内では、samplerとdataloaderを定義します。
train_sampler = RandomSampler(train_dataset) if args.local_rank == -1 else DistributedSampler(train_dataset) train_dataloader = DataLoader(train_dataset, sampler=train_sampler, batch_size=args.train_batch_size)
samplerとdataloaderについては以下が分かりやすいです。
PytorchのDataloaderとSamplerの使い方 - Qiita
そっから、ごにょごにょした後、エポックを回します。
イテレーションの内容は、一般的な学習とほとんど同じです。(一部コードを省略しています。)
tr_loss, logging_loss = 0.0, 0.0 model.zero_grad() train_iterator = trange(int(args.num_train_epochs), desc="Epoch", disable=args.local_rank not in [-1, 0]) set_seed(args) # Added here for reproductibility (even between python 2 and 3) for _ in train_iterator: epoch_iterator = tqdm(train_dataloader, desc="Iteration", disable=args.local_rank not in [-1, 0]) for step, batch in enumerate(epoch_iterator): model.train() batch = tuple(t.to(args.device) for t in batch) inputs = {'input_ids': batch[0], 'attention_mask': batch[1], 'token_type_ids': batch[2] if args.model_type in ['bert', 'xlnet'] else None, # XLM don't use segment_ids 'labels': batch[3]} outputs = model(**inputs) loss = outputs[0] # model outputs are always tuple in pytorch-transformers (see doc) loss = loss.mean() # mean() to average on multi-gpu parallel training loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), args.max_grad_norm) # default=1.0 tr_loss += loss.item()
modelはBertForSequenceClassificationクラスですので、outputsには「(loss), logits, (hidden_states), (attentions)」が格納されています。
モデルの出力に関しては、ここを確認しておくと良いと思います。
Migrating from pytorch-pretrained-bert — pytorch-transformers 1.0.0 documentation
Saving best-practices
fine tuning後のモデルなどを保存しておきます。
model_to_save = model.module if hasattr(model, 'module') else model # Take care of distributed/parallel training model_to_save.save_pretrained(args.output_dir) tokenizer.save_pretrained(args.output_dir) # torch.save(model_to_save.state_dict(), output_model_file) # Good practice: save your training arguments together with the trained model torch.save(args, os.path.join(args.output_dir, 'training_args.bin'))
save_pretrainedでは、torch.save()を呼んで保存しています。
Evaluation
基本的に訓練時と同じです。
ただ、Samplerのところはランダムではなくシーケンシャルです。
# Note that DistributedSampler samples randomly eval_sampler = SequentialSampler(eval_dataset) if args.local_rank == -1 else DistributedSampler(eval_dataset) eval_dataloader = DataLoader(eval_dataset, sampler=eval_sampler, batch_size=args.eval_batch_size)
あと、lossと一緒にlogitも取り出す必要があります。
eval_loss = 0.0 nb_eval_steps = 0 preds = None for batch in tqdm(eval_dataloader, desc="Evaluating"): outputs = model(**inputs) eval_loss += tmp_eval_loss.mean().item() if preds is None: preds = logits.detach().cpu().numpy() out_label_ids = inputs['labels'].detach().cpu().numpy() else: preds = np.append(preds, logits.detach().cpu().numpy(), axis=0) out_label_ids = np.append(out_label_ids, inputs['labels'].detach().cpu().numpy(), axis=0) eval_loss = eval_loss / nb_eval_steps preds = np.argmax(preds, axis=1) result = compute_metrics(eval_task, preds, out_label_ids) results.update(result)
ここらへんが理解できれば、別のタスクでもfine tuningできると思いました。
おわりに
run_glue.pyを走らせて、fine tuningを触ってみました。
run_glue.pyは動かすだけで、GLUEのfine tuningが出来るので凄く簡単です。
しかし、別のタスクに取り組むためには、run_glue.pyに頼らないコーディングが必要です。
次回は、run_glue.pyに存在しないタスク(CoNLL)に挑戦したいと思います。