こんちには。
データアナリティクス事業本部 機械学習チームの中村です。
Hugging Faceのライブラリの使い方紹介記事第3弾です。
今回は、Learning Rateを調整するためのSchedulerについて深堀し、理解を深めていきます。
Schedulerの種類
Hugging FaceのTransformersでは、Learning Rateを調整するためのいくつかのSchedulerが準備されています。
Schedulerの種類 | 内容 |
---|---|
"constant" | 設定したLearning Rateで一定値 |
"linear" | 設定したLearning Rateをピークとして線形にepochの終わりで0となるよう減衰 |
"cosine" | 設定したLearning Rateをピークとしてcosineカーブでepochの終わりで0となるよう減衰 |
"cosine_with_restarts" | 設定したLearning Rateをピークとしてcosineカーブで0となるよう減衰を周期的に |
"polynomial" | 設定したLearning Rateをピークとして多項式曲線でepochの終わりで指定値となるよう減衰 |
上記にそれぞれ、warmup用の期間を追加で設定することが可能です。
これらは特に意識して設定しない場合、デフォルトでは"linear"が使用されます。
設定方法としては以下の2パターンがあります。
- lr_scheduler_typeを指定する方法
- get関数を使用してSchedulerを作成する方法
前者は、warmupなしの"constant"、"linear"、"cosine"を使用したい場合に簡易な設定として使用可能です。
より詳細に設定したい場合は後者の方法で使用します。
以降でそれぞれの方法について確認していきます。
実行環境
今回はGoogle Colaboratory環境で実行しました。
ハードウェアなどの情報は以下の通りです。
- GPU: Tesla P100 (GPUメモリ16GB搭載)
- CUDA: 11.1
- メモリ: 13GB
主なライブラリのバージョンは以下となります。
- transformers: 4.22.1
- datasets: 2.4.0
インストール
transformersとdatasetsをインストールします。
!pip install transformers datasets
また事前学習モデルの依存モジュールをインストールします。
!pip install fugashi
!pip install ipadic
!pip install sentencepiece
ベースとするコード
今回のベースとするコードは以下のとおりです。
from datasets import load_dataset
from transformers import AutoTokenizer
from transformers import AutoModelForSequenceClassification
from transformers import TrainingArguments
from transformers import Trainer
from sklearn.metrics import accuracy_score, f1_score
import torch
# データセットのロード
dataset = load_dataset("tyqiangz/multilingual-sentiments", "japanese")
# # 実験のためデータセットを縮小したい場合はコチラを有効化
# dataset = DatasetDict({
# "train": dataset['train'].select(range(100)),
# "validation": dataset['validation'].select(range(100)),
# "test": dataset['test'].select(range(100)),
# })
# トークナイザのロード
model_ckpt = "cl-tohoku/bert-base-japanese-whole-word-masking"
tokenizer = AutoTokenizer.from_pretrained(model_ckpt)
# トークナイズ処理
def tokenize(batch):
return tokenizer(batch["text"], padding=True, truncation=True)
dataset_encoded = dataset.map(tokenize, batched=True, batch_size=None)
# 事前学習モデルのロード
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
num_labels = 3
model = (AutoModelForSequenceClassification
.from_pretrained(model_ckpt, num_labels=num_labels)
.to(device))
# メトリクスの定義
def compute_metrics(pred):
labels = pred.label_ids
preds = pred.predictions.argmax(-1)
f1 = f1_score(labels, preds, average="weighted")
acc = accuracy_score(labels, preds)
return {"accuracy": acc, "f1": f1}
# 学習パラメータの設定
batch_size = 16
model_name = "sample-text-classification-bert"
training_args = TrainingArguments(
output_dir=model_name,
num_train_epochs=10,
learning_rate=2e-5,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
weight_decay=0.01,
evaluation_strategy="epoch",
logging_strategy="steps",
disable_tqdm=False,
logging_steps=1,
push_to_hub=False,
log_level="error",
)
# Trainerの定義
trainer = Trainer(
model=model,
args=training_args,
compute_metrics=compute_metrics,
train_dataset=dataset_encoded["train"],
eval_dataset=dataset_encoded["validation"],
tokenizer=tokenizer
)
# トレーニング実行
trainer.train()
この内容についての解説は以下の記事を参照ください。
上記の記事と違う点として以下の変更を加えています。
- Learning Rateのstep毎の変化を見るため、TrainingArgumentsに
logging_strategy="steps"
を指定- ここでstepとは、ミニバッチ単位のこと
logging_strategy="steps"
としてもlogging_steps以下の粒度にならないため、logging_steps=1
と最小で指定- 変化を分かりやすくするために
num_train_epochs=10
とエポックを増加
また、デフォルトのSchedulerはwarmupなしのlinearですので、このコードの場合はそちらが適用されます。
lr_scheduler_typeを指定する方法
設定方法
TrainingArgumentにlr_scheduler_typeという引数があり、ここでSchedulerを文字列で設定することができます。
以下はconstantに指定する例です。
training_args = TrainingArguments(
output_dir=model_name,
num_train_epochs=10,
learning_rate=2e-5,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
weight_decay=0.01,
evaluation_strategy="epoch",
logging_strategy="steps",
disable_tqdm=False,
logging_steps=1,
push_to_hub=False,
log_level="error",
lr_scheduler_type="constant",
)
lr_scheduler_typeには、以下に定義されているSchedulerTypeを指定可能です。
種類は6つありますが、lr_scheduler_typeで使い分けができるのは、"constant", "linear", "cosine"です。
それ以外の"constant_with_warmup", "cosine_with_restarts", "polynomial"はそれぞれ以下のような動作となりますので注意が必要です。
- "constant_with_warmup"は、"constant"と同じ動作
- "cosine_with_restarts"は、"cosine"と同じ動作
- "polynomial"は、"linear"と同じ動作
これらをきちんと区別して使用したい場合は、後述の「get関数を使用してSchedulerを作成する方法」で実施が必要です。 また、それぞれwarmupの設定をしたい場合も、後述の方法で実施が必要です。
逆に簡易な設定で良い場合は、lr_scheduler_typeの指定で対応できます。
それぞれの結果を比較
一旦、lr_scheduler_typeで使い分けが可能な3種類をそれぞれ確認ます。
import pandas as pd
learning_rate_history = {}
for lr_scheduler_type in ["constant", "linear", "cosine"]:
model = (AutoModelForSequenceClassification
.from_pretrained(model_ckpt, num_labels=num_labels)
.to(device))
training_args = TrainingArguments(
output_dir=model_name,
num_train_epochs=10,
learning_rate=2e-5,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
weight_decay=0.01,
evaluation_strategy="epoch",
logging_strategy="steps",
disable_tqdm=False,
logging_steps=1,
push_to_hub=False,
log_level="error",
lr_scheduler_type=lr_scheduler_type,
)
# Trainerの定義
trainer = Trainer(
model=model,
args=training_args,
compute_metrics=compute_metrics,
train_dataset=dataset_encoded["train"],
eval_dataset=dataset_encoded["validation"],
tokenizer=tokenizer
)
# トレーニング実行
trainer.train()
# Learning Rateの変化を抽出
train_log = [i for i in trainer.state.log_history if "loss" in i]
learning_rate_history[lr_scheduler_type] = pd.DataFrame(train_log)["learning_rate"]
学習時のログがtrainer.state.log_historyに格納されているため、ここからLearning Rateの変化を抽出します。
結果は以下のコードで可視化します。
learning_rate_history["epoch"] = pd.DataFrame(train_log)["epoch"]
df = pd.DataFrame(learning_rate_history)
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 3, figsize=(11, 5), dpi=100)
axes = axes.reshape(-1)
scheduler_types = ["constant", "linear", "cosine"]
for i, c in enumerate(scheduler_types):
ax = axes[i]
ax.plot(df["epoch"], df, linewidth=0.8)
ax.grid(visible=True, linestyle=":")
ax.set_xlim([-1, 11])
ax.set_ylim([-0.1e-5, 2.1e-5])
ax.set_title(c)
ax.set_xlabel("epoch")
ax.set_ylabel("learning rate")
plt.tight_layout()
Learning Rateの変化はそれぞれ以下のようになりました。
get関数を使用してSchedulerを作成する方法
get関数の種類
より詳細なLearning Rateのschedulerを使用するためには、get関数を使用してSchedulerを作成する必要があります。
関数はそれぞれ以下のように準備されています。
Schedulerの種類 | getするための関数 |
---|---|
"constant" | transformers.get_constant_schedule |
"constant_with_warmup" | transformers.get_constant_schedule_with_warmup |
"linear" | transformers.get_linear_schedule_with_warmup |
"cosine" | transformers.get_cosine_schedule_with_warmup |
"cosine_with_restarts" | transformers.get_cosine_with_hard_restarts_schedule_with_warmup |
"polynomial" | transformers.get_polynomial_decay_schedule_with_warmup |
transformers.get_scheduler
というものもありますが、すべての設定ができるわけではないため、カスタマイズのためにはこれらのget関数を使用します。
公式ドキュメント上は以下を確認してください。
それぞれのget関数について使用方法を確認していきます。
get_constant_schedule
まずはconstantを例にget関数を使う場合の手順を確認していきます。
get関数を使う場合はoptimizerを定義します。
今回はoptimizerにAdamWを使用しました。これはtransformersのTrainerで学習する際のデフォルトです。
from transformers import AdamW, get_constant_schedule
# modelから学習すべきパラメータを抽出
params = filter(lambda x: x.requires_grad, model.parameters())
# 今回はoptimizerにAdamWを使用
optimizer = AdamW(params, lr=2e-5)
scheduler = get_constant_schedule(optimizer)
これらをTrainerのoptimizers引数に以下のように与えて学習すればOKです。
この際、TrainingArgumentsのlearning_rateとlr_scheduler_typeは削除しておきます。
model = (AutoModelForSequenceClassification
.from_pretrained(model_ckpt, num_labels=num_labels)
.to(device))
training_args = TrainingArguments(
output_dir=model_name,
num_train_epochs=10,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
weight_decay=0.01,
evaluation_strategy="epoch",
logging_strategy="steps",
disable_tqdm=False,
logging_steps=1,
push_to_hub=False,
log_level="error",
)
# Trainerの定義
trainer = Trainer(
model=model,
args=training_args,
compute_metrics=compute_metrics,
train_dataset=dataset_encoded["train"],
eval_dataset=dataset_encoded["validation"],
tokenizer=tokenizer,
optimizers=[optimizer, scheduler]
)
# トレーニング実行
trainer.train()
以降のget関数では、trainingの部分は同様ですので省略します。
get_constant_schedule_with_warmup
こちらは、constantにwarmup期間を設けることができるSchedulerを作成できます。
引数としてnum_warmup_stepsを使用してstep単位(ミニバッチ単位)で指定が可能です。
今回は1エポック相当で指定しました。
import math
from transformers import get_constant_schedule_with_warmup
params = filter(lambda x: x.requires_grad, model.parameters())
optimizer = AdamW(params, lr=2e-5)
# 1epoch分をwarmupとするための記述
num_warmup_steps = math.ceil(dataset["train"].num_rows / batch_size) * 1
scheduler = get_constant_schedule_with_warmup(optimizer, num_warmup_steps=num_warmup_steps)
結果は以下のように1エポックをかけて2e-5
まで上昇し、そこから一定となるような形となっています。
get_linear_schedule_with_warmup
こちらは、先ほどのlr_scheduler_typeでも指定できた、線形に減衰するSchedulerを作成できます。
引数としてnum_warmup_stepsが使用できるので、warmupの設定が可能です。
またnum_training_stepsが引数として存在し、ここは通常エポックの最後相当となるようなstepsを指定します。
from transformers import get_linear_schedule_with_warmup
params = filter(lambda x: x.requires_grad, model.parameters())
optimizer = AdamW(params, lr=2e-5)
num_warmup_steps = math.ceil(dataset["train"].num_rows / batch_size) * 1
# Learning Rateを0にする点を指定する、今回はepoch=10なので10
num_training_steps = math.ceil(dataset["train"].num_rows / batch_size) * 10
scheduler = get_linear_schedule_with_warmup(optimizer,
num_warmup_steps=num_warmup_steps, num_training_steps=num_training_steps)
結果は、warmup後に線形に減衰する形となっています。
num_training_stepsについての補足です。
num_training_stepsは、最後のepoch相当より小さい値をに指定することも可能です。
学習を途中のエポックで止めたい場合などに使用します。
何の意味があるのか一見分からないかもしれませんが、途中の層は一定エポックで学習を止めるなど、層に応じて動作を変える際に使用するケースがあるためと考えられます。
get_cosine_schedule_with_warmup
こちらも、先ほどのlr_scheduler_typeでも指定できた、cosine波形で減衰するSchedulerを作成できます。
同様にnum_warmup_stepsが使用できるので、warmupの設定が可能です。
またnum_training_stepsも同様に指定が必要です。
from transformers import get_cosine_schedule_with_warmup
params = filter(lambda x: x.requires_grad, model.parameters())
optimizer = AdamW(params, lr=2e-5)
num_warmup_steps = math.ceil(dataset["train"].num_rows / batch_size) * 1
num_training_steps = math.ceil(dataset["train"].num_rows / batch_size) * 10
scheduler = get_cosine_schedule_with_warmup(optimizer,
num_warmup_steps=num_warmup_steps, num_training_steps=num_training_steps)
結果は、warmup後にcosine波形で減衰する形となっています。
get_cosine_with_hard_restarts_schedule_with_warmup
こちらはcosine減衰の更に応用編で、cosine減衰を周期的に減衰させることが可能です。
今回は周期数をnum_cycles=5
と指定しています。
from transformers import get_cosine_with_hard_restarts_schedule_with_warmup
params = filter(lambda x: x.requires_grad, model.parameters())
optimizer = AdamW(params, lr=2e-5)
num_warmup_steps = math.ceil(dataset["train"].num_rows / batch_size) * 1
num_training_steps = math.ceil(dataset["train"].num_rows / batch_size) * 10
# cosineの周期を指定
num_cycles = 5
scheduler = get_cosine_with_hard_restarts_schedule_with_warmup(optimizer,
num_warmup_steps=num_warmup_steps, num_training_steps=num_training_steps,
num_cycles=num_cycles)
結果は、warmup後にcosine波形の減衰が周期的に繰り返される形となっています。
get_polynomial_decay_schedule_with_warmup
こちらはlinear減衰の応用といえるのですが、多項式で減衰させることが可能です。
lr_endで最終的なLearning Rateを指定し、powerで多項式の次数を指定します。
powerが増加するほど、減衰が急峻となります。
from transformers import get_polynomial_decay_schedule_with_warmup
params = filter(lambda x: x.requires_grad, model.parameters())
optimizer = AdamW(params, lr=2e-5)
num_warmup_steps = math.ceil(dataset["train"].num_rows / batch_size) * 1
num_training_steps = math.ceil(dataset["train"].num_rows / batch_size) * 10
# 最終的なLearning Rateを指定
lr_end = 2e-6
# 次数を指定
power = 2
scheduler = get_polynomial_decay_schedule_with_warmup(optimizer,
num_warmup_steps=num_warmup_steps, num_training_steps=num_training_steps,
lr_end=lr_end, power=power)
比較のため、powerをいくつかのパターンで試してプロットしました。
warmup後に多項式の次数に応じて急峻となる減衰になっています。
powerには負の数は指定できませんが、このように1.0以下を指定することも可能です。
まとめ
いかがでしたでしょうか?
最初は、デフォルトでSchedulerがlinearとなっていることに気が付き、設定方法を調べていくと Schedulerの指定方法にいくつかのパターンがあることが分かったため、詳細を理解するために深堀して記事にしてみました。
本記事がHugging Faceを使われる方の参考になれば幸いです。