147
85

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

huggingfaceのTrainerクラスを䜿えばFineTuningの孊習コヌドがスッキリ曞けおめちゃくちゃ䟿利です

147
Last updated at Posted at 2022-03-29

はじめに

huggingfaceのTrainerクラスはhuggingfaceで提䟛されるモデルの事前孊習のずきに䜿うものだず思っおお、䞋流タスクを孊習させるずきFine Tuningは普通に孊習のコヌドを実装しおたんですが、䞋流タスクを孊習させるずきもTrainerクラスは䜿えお、めちゃくちゃ䟿利でした。
ただTrainerクラスのinitやTrainingArgumentsの匕数はたくさんあるしよくわからん、ずいう人のために、TrainerクラスのFine Tuning時の䜿い方を実装を通しおたずめおみようず思いたす。

今回は自然蚀語凊理のタスクずしおlivedoorニュヌスコヌパスのタむトル文のカテゎリヌ分類問題をFine Tuningの䟋題ずしお扱おうず思いたすが、ViTのFine Tuningずかでも同様かず思いたす。

基本的にはhuggingfaceのTrainerクラスのリファレンスをひたすら芋ながら勉匷したので、詳しくはリファレンスをご参照ください。

Trainerクラスを䜿ったFineTuningの実装䟋

デヌタ準備

livedoorニュヌスコヌパスをbody, title, categoryに分けたデヌタフレヌムを事前に甚意しおおきたす。

import pandas as pd
import os

df = pd.read_pickle('./input/livedoor_data.pickle')
# カテゎリヌのID列を付䞎しおおく
categories = df['category'].unique().tolist()
category2id = {cat: categories.index(cat) for cat in categories}
df['category_id'] = df['category'].map(lambda x: category2id[x])

df.sample(3)

スクリヌンショット 2022-03-29 21.50.53.png

デヌタを孊習、怜蚌、テストで分けたす。

from sklearn.model_selection import train_test_split

train_df, eval_df = train_test_split(df, train_size=0.7)
eval_df, test_df = train_test_split(eval_df, train_size=0.5)
print('train size', train_df.shape)
print('eval size', eval_df.shape)
print('test size', test_df.shape)
# train size (5163, 4)
# eval size (1106, 4)
# test size (1107, 4)

Datasetクラスを甚意

Datasetからデヌタを取り出すず蟞曞圢匏でタむトル文ずカテゎリヌのIDが玐付いたデヌタを取れるような圢匏にしたした。

from torch.utils.data import Dataset
from tqdm import tqdm

class LivedoorDataset(Dataset):
    def __init__(self, df):
        self.features = [
            {
                'title': row.title,
                'category_id': row.category_id
            } for row in tqdm(df.itertuples(), total=df.shape[0])
        ]
    
    def __len__(self):
        return len(self.features)
    
    def __getitem__(self, idx):
        return self.features[idx]

train_dataset = LivedoorDataset(train_df)
eval_dataset = LivedoorDataset(eval_df)
test_dataset = LivedoorDataset(test_df)

train_datsetからデヌタを1件取り出しお䞭身を確認しおみるずこんな感じです。

train_dataset[0]
# {'title': '肉䜓掟むケメン総出挔で、チャニング・テむタムの″ストリップ経隓″を映画化', 'category_id': 0}

DataCollatorの定矩

  • TrainerクラスがDataLoaderじゃなくおDataCollatorを匕数ずしお受け取るので、DataCollatorクラスを自䜜したす。
  • huggingfaceもDataCollatorクラスをいく぀か提䟛しおたすが、今回は単玔な凊理しかしないので、自䜜で枈たせおたす。
import torch
from transformers import AutoTokenizer

class LivedoorCollator():
    def __init__(self, tokenizer, max_length=512):
        self.tokenizer = tokenizer
        self.max_length = max_length
    
    def __call__(self, examples):
        examples = {
            'title': list(map(lambda x: x['title'], examples)),
            'category_id': list(map(lambda x: x['category_id'], examples))
        }
        
        encodings = self.tokenizer(examples['title'],
                                   padding=True,
                                   truncation=True,
                                   max_length=self.max_length,
                                   return_tensors='pt')
        encodings['category_id'] = torch.tensor(examples['category_id'])
        return encodings
        
tokenizer = AutoTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
livedoor_collator = LivedoorCollator(tokenizer)

埌の凊理では䜿いたせんが、DataCollatorの動きを確認するために、DataLoaderを䜜成しお、1件バッチデヌタを取り出しおみたす。

from torch.utils.data import DataLoader
loader = DataLoader(train_dataset, collate_fn=livedoor_collator, batch_size=8, shuffle=True)
batch = next(iter(loader))
for k,v in batch.items():
    print(k, v.shape)
# input_ids torch.Size([8, 41])
# token_type_ids torch.Size([8, 41])
# attention_mask torch.Size([8, 41])
# category_id torch.Size([8])

print(batch)
# {'input_ids': tensor([[    2,  9680, 10520, 28770, 28865,   450,    52,    53,   512,  9594,
#           5359,   126,   243, 28673,    12,     6,  5359,    40, 16329, 28476,
#           2935,    63,  7388,   104,     6,   331, 28483,  4658,    35, 15288,・・・
# 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,・・・
# 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ・・・
# 'category_id': tensor([0, 8, 8, 2, 1, 7, 0, 2])}

モデルの定矩

テキストの特城量倉換は孊習枈BERTにたかせお、カテゎリヌ分類甚のヘッドnn.Linearを1局远加したシンプルなモデルを実装したすが、モデル定矩が少しTrainerクラス甚に曞く必芁がありたす。

Trainerクラスのリファレンスにも曞いおあるように、TrainerクラスはhuggingfaceのPreTrainedModelで動䜜が最適化されおいるようなので、PreTrainedModelず同じようなルヌルで動䜜するモデルを実装する必芁がありたす。
普段FineTuningずかするずきのモデルはnn.Moduleを継承したクラスで実装するかず思いたすが、以䞋のルヌルを満たしおいればTrainerクラスは動いおくれたす。

  1. モデルの戻り倀はhuggingfaceのModelOutputの圢匏で返すもしくはtupleでもいいようですが、ModelOutputのほうが䟿利だず思うので、ModelOutputのケヌスで説明したす。
  2. モデルのforwardが正解ラベルを受け取れるようにしおおく
  3. モデルが損倱の倀を返す

1. モデルの戻り倀をModelOutputにする

ModelOutputはtransformers.modeling_outputs.ModelOutputを䜿えばいいだけです。キヌに[]じゃなくお.でもアクセスできる䟿利な蟞曞のむメヌゞです。BERTの戻り倀の型BaseModelOutputWithPoolingAndCrossAttentionsもModelOutputを継承したクラスでした。
ModelOutputを䜿うずきは、ModelOutputのリファレンスでそうしおるように、損倱の倀をloss、モデルの予枬結果をlogitsずいう名前に栌玍したす。

2. forwardで正解ラベルを受け取れるようにする

今回自䜜したDataCollatorが正解ラベルcateogory_idを返すようにしおいるので、forwardの匕数にもcateogory_idを含めおおけば良いです。Trainerクラスのデフォルトでは正解ラベルをlabelsずいう名前で想定しおいたすが、埌に解説するTrainingArgumentsで正解ラベルの名前を指定できたす。マルチタスク孊習のような正解ラベルが耇数あるケヌスにも察応できたす。

3. モデルが損倱を返すようにする

぀たりはモデル内で損倱を蚈算する必芁があるので、モデルのinitで損倱関数を指定するようにしおいたす。モデルのforward内で匕数で受け取った正解ラベルcategory_idやlabelsずモデルの予枬結果を受け取っお損倱関数でlossを蚈算し、ModelOutputのlossに損倱を栌玍しお返しおやればOKです。以䞋の実装ではFineTuning埌に掚論で䜿うこずも想定しお、損倱関数をinitで指定しなかった堎合、lossは蚈算されずNoneを返すようにしおいたす。

import torch.nn as nn
from transformers import AutoModel
from transformers.modeling_outputs import ModelOutput

class LivedoorNet(nn.Module):
    def __init__(self, pretrained_model, num_categories, loss_function=None):
        super().__init__()
        self.bert = pretrained_model
        self.hidden_size = self.bert.config.hidden_size
        self.linear = nn.Linear(self.hidden_size, num_categories)
        self.loss_function = loss_function
    
    def forward(self,
                input_ids,
                attention_mask=None,
                position_ids=None,
                token_type_ids=None,
                output_attentions=False,
                output_hidden_states=False,
                category_id=None):
        
        outputs = self.bert(input_ids,
                            attention_mask=attention_mask,
                            position_ids=position_ids,
                            token_type_ids=token_type_ids,
                            output_attentions=output_attentions,
                            output_hidden_states=output_hidden_states)
        
        state = outputs.last_hidden_state[:, 0, :]
        state = self.linear(state)
        
        loss=None
        if category_id is not None and self.loss_function is not None:
            loss = self.loss_function(state, category_id)
        
        attentions=None
        if output_attentions:
            attentions=outputs.attentions
        
        hidden_states=None
        if output_hidden_states:
            hidden_states=outputs.hidden_states
        
        return ModelOutput(
            logits=state,
            loss=loss,
            last_hidden_state=outputs.last_hidden_state,
            attentions=attentions,
            hidden_states=hidden_states
        )

loss_fct = nn.CrossEntropyLoss()
pretrained_model = AutoModel.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
net = LivedoorNet(pretrained_model, len(categories), loss_fct)

compute_metricsを自䜜する

ここたでの実装の準備でTrainerクラスは動かせるのですが、このたただず、孊習䞭の怜蚌デヌタに察するメトリクスの蚈算が行われたせん。メトリクスは自䜜で関数を甚意する必芁がありたす。今回はニュヌス蚘事のカテゎリヌの分類問題なので、評䟡指暙にF1スコアを䜿うこずにしたす。合わせおPrecisionやRecallも蚈算できるようにしたした。

Trainerクラスに枡すcompute_metricsの匕数はEvalPredictionを想定しおいたす。EvalPredictionに぀いおはこちらを参照しおいただきたいのですが、芁はEvalPrediction.predictionsにモデルの予枬結果が、EvalPrediction.label_idsに正解ラベルが栌玍されおいたす。
戻り倀は蟞曞圢匏で返しおやれば良いです。この蟞曞で定矩したキヌ名precision, recall, f1は埌にeval_の接頭蟞が付䞎されおログ情報に䜿われたす。

from transformers import EvalPrediction
from typing import Dict
from sklearn.metrics import precision_score, recall_score, f1_score

def custom_compute_metrics(res: EvalPrediction) -> Dict:
    # res.predictions, res.label_idsはnumpyのarray
    pred = res.predictions.argmax(axis=1)
    target = res.label_ids
    precision = precision_score(target, pred, average='macro')
    recall = recall_score(target, pred, average='macro')
    f1 = f1_score(target, pred, average='macro')
    return {
        'precision': precision,
        'recall': recall,
        'f1': f1
    }

TrainingArgumentsを蚭定する

TrainingArgumentsの匕数は倧量にあっお、党郚玹介するのは難しので詳しくはリファレンスを参照しおいただきたいですが、いく぀かよく䜿いそうなものを玹介したす。

  • output_dir: モデルのチェックポむントや孊習埌のパラメヌタファむルずかの保存先
  • evaluation_strategy: デフォルトはstepsが指定されおたす。評䟡デヌタをどのタむミングで評䟡するかを指定したす。stepsを指定するず、eval_stepsで指定したステップ毎にcompute_metricsで指定したメトリクスが蚈算されたす。今回はepochを指定するので、1゚ポック終わるたびに評䟡されたす。
  • logging_strategy: 孊習のロギング損倱の倀ずか孊習率の状況ずかをどのタむミングで実斜するかを指定したす。デフォルトはstepsが指定されおおり、logging_stepsで指定したステップ毎にロギングされたす。今回はepochを指定するので、1゚ポック終わるたびにロギングされたす。
  • save_strategy: チェックポむント孊習の䞭間の状況をどのタむミングで保存するかを指定したす。デフォルトはやはりstepsが指定されおおり、save_stepsで指定したステップ毎にチェックポむントが保存されたす。今回はepochを指定するので、1゚ポック終わるたびにチェックポむントが保存されたす。
  • save_total_limit: チェックポむントを䜕件残すか。
  • label_names: 正解ラベルのラベル名を配列で指定したす。指定しなければTrainerクラスは正解ラベルをlabelsずいう名前で参照しに行きたす。マルチタスク孊習のような正解ラベルが耇数あるケヌスでも察応可胜のようです。
  • lr_scheduler_type: 孊習率のスケゞュヌルのテンプレヌトを指定したす。どんなテンプレヌトがあるかはこちらをご参照ください。SchedulerType の゜ヌスコヌドを芋るずテンプレヌトず名前の察応関係がわかりたす。デフォルトはlinearが指定されおおり、最終゚ポックに向かっお線圢に孊習率が枛少しおいくようなスケゞュヌルになっおいたす。以䞋のようにconstantを指定すれば孊習率が枛少するようなスケゞュヌルは蚭定されたせん。
  • learning_rate: 孊習率です。デフォルトは5e-5が指定されおいたす。この倀をこのたた䜿いたいので、今回は指定しおいたせん。
  • warmup_ratio/warmup_steps: 今回は指定しおいたせん。デフォルトではずもに0が指定されおいるのでフォヌムアップは行われたせんが、これらのどちらかを指定するず、指定したステップたで線圢に孊習率が増加しおしくフォヌムアップの孊習を行うこずができたす。
  • metric_for_best_model: early stoppingをする際には必芁です。異なるモデルを比范するずきに比范する指暙をしたす。compute_metricsで定矩したメトリクスの名前を指定したす。今回はF1スコアが䞀番良いモデルを保存したいので、f1を指定したした。
  • load_best_model_at_end: early stoppingを行うずきはTrueを指定する必芁がありたす。孊習䞭に埗られたベストモデルを孊習終了埌にロヌドするかどうか。
  • per_device_train_batch_size: 孊習䞭に1GPUに割り振るバッチサむズ。䟋えば2枚のGPUが䜿える環境では1枚毎に指定したバッチサむズが乗りたす。
  • per_device_eval_batch_size: 評䟡デヌタを蚈算するずきに1GPUに割り振るバッチサむズ
  • num_train_epochs: 孊習の゚ポック数
  • remove_unused_columns: デフォルトがTrue。これがTrueだず、Trainerにわたすデヌタセットのカラム今回でいえば、titleずcategory_idのうちモデルのforward関数の匕数に存圚しないものは自動で削陀されたす。今回の実装方法はcollatorクラスでtokenizerに通しおinput_idsずかを取埗したいのでこのパラメヌタがTrueだずcollatorの__call__関数内で空の蟞曞が枡されおしたいたす。datasetの時点でtokenizerに通しおtensorを保持しおいるようなケヌスであればTrueで良いかもしれたせんが、今回はFalseにしたす。

2023/4/10 远蚘
transformersのバヌゞョンを4.27.1を䜿っおいたずころ、孊習を開始したら wandb に関する以䞋のメッセヌゞが衚瀺されたした。

wandb: (1) Create a W&B account
wandb: (2) Use an existing W&B account
wandb: (3) Don't visualize my results
wandb: Enter your choice: 

別に wandb ずか䜿っおたせんよヌ、っお人は毎回これが衚瀺されるのうっずおしいず思いたすが、これは以䞋の匕数で "none" を指定すれば回避できたす。

  • 'report_to': 結果やログ情報をなんのプラットフォヌムを䜿っおたずめるかを指定したす。デフォルトで "all" になっおるんですが、いらない堎合は "none" を指定すれば良いです。
from transformers import TrainingArguments

training_args = TrainingArguments(
    output_dir='./output/model',
    evaluation_strategy='epoch',
    logging_strategy='epoch',
    save_strategy='epoch',
    save_total_limit=1,
    label_names=['category_id'],
    lr_scheduler_type='constant',
    metric_for_best_model='f1',
    load_best_model_at_end=True,
    per_device_train_batch_size=64,
    per_device_eval_batch_size=64,
    num_train_epochs=3,
    remove_unused_columns=False,
    report_to='none'
)

Trainerクラスの定矩ず実行

これたで定矩しおきたものを党郚Trainerクラスのinitに枡しおやればOKです。
今回はearly stoppingも行っおいたすが、その際はcallbacksに以䞋のようにEarlyStoppingCallbackを指定しおやりたす。
Trainerクラスで䜿えるcallbacksの詳しい情報はリファレンスをご参照ください。

Trainerクラスを定矩すれば、埌は.trainで孊習を開始できたすが、そのずきにignore_keys_for_evalで評䟡に必芁ないモデルの戻り倀の倉数名を指定するようにしたしょう。

from transformers import Trainer
from transformers import EarlyStoppingCallback

trainer = Trainer(
    model=net,
    tokenizer=tokenizer,
    data_collator=livedoor_collator,
    compute_metrics=custom_compute_metrics,
    args=training_args,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
    callbacks=[EarlyStoppingCallback(early_stopping_patience=3)]
)

trainer.train(ignore_keys_for_eval=['last_hidden_state', 'hidden_states', 'attentions'])

notebookで実行すればこんな感じのprogress barが衚瀺されたすね。
スクリヌンショット 2022-03-30 0.00.06.png

チェックポむントから孊習を再開する

TrainingArgumentsの匕数にチェックポむントから孊習を再開するずきに指定するresume_from_checkpointがありたすが、TrainingArgumentsでこれを指定しおもチェックポむントから孊習が再開されたせん。
詳しくはリファレンスを参照いただきたいですが、チェックポむントから孊習を再開したいずきは、Trainerクラスの.trainメ゜ッドに察しお、resume_from_checkpointを指定する必芁がありたす。Trueを指定すればTrainingArgumentsのoutput_dirで指定したディレクトリの䞭にある最埌のチェックポむントから孊習が再開されたす。チェックポむントのディレクトリのパスを指定しおじっこうするこずもできたす。

# チェックポむントから孊習を再開したいずきず
trainer.train(ignore_keys_for_eval=['last_hidden_state', 'hidden_states', 'attentions'],
              resume_from_checkpoint=True)

モデルの保存

孊習埌のモデルの保存は.save_modelで保存できたすが、䞊のプログレスバヌのような各゚ポックの損倱の掚移であったり、評䟡デヌタのメトリクスの情報は.save_stateで保存できたす。.save_stateを実行するず、TrainingArgumentsのoutput_dirで指定したフォルダにtrainer_state.jsonずいうファむルが保存されたす。

trainer.save_state()
trainer.save_model()

テストデヌタの予枬

テストデヌタに察する予枬は.predictで行えたす。最初に避けずいたtest_datasetを指定し、テストに必芁ないモデルの戻り倀の倉数名をignore_keysに指定したす。
.predictの戻り倀の.predictionsにモデルの掚論結果が栌玍されおいたす。
最埌に予枬結果をsklearnのclassification_reportで衚瀺しおみたした。たった3゚ポックですがいい感じに孊習できおる感じですね。

pred_result = trainer.predict(test_dataset, ignore_keys=['loss', 'last_hidden_state', 'hidden_states', 'attentions'])
test_df['predict'] = pred_result.predictions.argmax(axis=1).tolist()

from sklearn.metrics import classification_report
print(classification_report(test_df['category_id'], test_df['predict'], target_names=categories))
#                precision    recall  f1-score   support
#
#   movie-enter       0.85      0.90      0.87       136
#  it-life-hack       0.87      0.86      0.87       103
# kaden-channel       0.96      0.93      0.94       136
#    topic-news       0.87      0.88      0.88       112
#livedoor-homme       0.73      0.80      0.76        82
#        peachy       0.81      0.72      0.76       127
#  sports-watch       0.96      0.87      0.91       130
#dokujo-tsushin       0.83      0.90      0.86       145
#          smax       0.96      0.97      0.96       136
#
#      accuracy                           0.87      1107
#     macro avg       0.87      0.87      0.87      1107
#  weighted avg       0.88      0.87      0.87      1107

おわりに

䞊で芋たようにFine Tuning時の孊習に関する実装はTrainingArgumentsを指定しおTrainerクラスのむンスタンスに察しお.trainで終わりです。これだけで、孊習や評䟡デヌタのロギング、孊習率のスケゞュヌラヌ、Early Stoppingなどの、自分で実装するずなったらそこそこ倧倉は実装をすっ飛ばすこずができたす。もちろんTrainerクラスの仕組みや内郚でどのような蚈算が行われおいるかはリファレンスを熟読しお理解しおおく必芁はありたすが、今回玹介した内容で少しはTrainerクラスでどんなこずができるのかの雰囲気は぀かめるのではないかず思いたす。

Trainerクラスの䟿利さを知っおしたったので、もう自分であれこれ孊習コヌドの実装なんおしたくない...

おわり

147
85
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
147
85

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?