この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんちには。
データアナリティクス事業本部 機械学習チームの中村です。
Hugging Faceのライブラリの使い方紹介記事第4弾です。
今回は、各レイヤ毎に別のLearning Rateで学習するためには、どのようにすればよいかを理解していきます。
前回のSchedulerと似た部分の内容になり、コードも一部流用しているため、併せて前回記事も参照ください。
層毎に別のLearning Rateで学習するとは
代表的な例としては、事前学習済みのBERT部分と、分類ヘッドで学習ヘッドを変更する例があります。
- 事前学習済みのBERT部分:Lerning Rateを小さく設定し、学習時に少しだけ更新する。
- 分類ヘッド:Lerning Rateを大きく設定し、学習時により大きく更新する。
今回は、この例にそって実装する方法を見ていきます。
実行環境
今回はGoogle Colaboratory環境で実行しました。
ハードウェアなどの情報は以下の通りです。
- GPU: Tesla P100 (GPUメモリ16GB搭載)
- CUDA: 11.1
- メモリ: 13GB
主なライブラリのバージョンは以下となります。
- transformers: 4.24.0
- datasets: 2.6.1
インストール
transformersとdatasetsをインストールします。
!pip install transformers datasets
また事前学習モデルの依存モジュールをインストールします。
!pip install fugashi
!pip install ipadic
!pip install sentencepiece
ベースとするコード
今回のベースとするコードは、前回を踏襲した以下のコードとします。
49-55行目の部分がSchedulerに関する部分でした。今回はLearning Rateをすべてのstepで一定とするconstantをSchedulerにしています。
from datasets import load_dataset
from transformers import AutoTokenizer
from transformers import AutoModelForSequenceClassification
from transformers import TrainingArguments
from transformers import Trainer
from transformers import AdamW, get_constant_schedule
from sklearn.metrics import accuracy_score, f1_score
import torch
# データセットのロード
dataset = load_dataset("tyqiangz/multilingual-sentiments", "japanese")
# # 実験のためデータセットを縮小したい場合はコチラを有効化
# from datasets import DatasetDict
# 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"
# modelから学習すべきパラメータを抽出
params = filter(lambda x: x.requires_grad, model.parameters())
# 今回はoptimizerにAdamWを使用
optimizer = AdamW(params, lr=2e-5)
scheduler = get_constant_schedule(optimizer)
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()
この内容についての解説は前回の記事を参照ください。
各レイヤでLearning Rateを変更する方法
ポイントは、ベースとするコードにもある以下の部分です。
# modelから学習すべきパラメータを抽出
params = filter(lambda x: x.requires_grad, model.parameters())
ここで、今はmodel全体のパラメータを全て抽出していますが、これをレイヤごとに分けて取得できれば、異なるLearning Rateを適用することができます。
各パラメータの確認
model.parameters()
では、各パラメータの名前が分かりませんが、代わりにmodel.named_parameters()
を使用することでパラメータ名を確認できます。
list(name for name, param in model.named_parameters())
出力は以下となります。(長いため一部省略しています)
['bert.embeddings.word_embeddings.weight',
'bert.embeddings.position_embeddings.weight',
'bert.embeddings.token_type_embeddings.weight',
'bert.embeddings.LayerNorm.weight',
'bert.embeddings.LayerNorm.bias',
'bert.encoder.layer.0.attention.self.query.weight',
'bert.encoder.layer.0.attention.self.query.bias',
'bert.encoder.layer.0.attention.self.key.weight',
'bert.encoder.layer.0.attention.self.key.bias',
'bert.encoder.layer.0.attention.self.value.weight',
'bert.encoder.layer.0.attention.self.value.bias',
'bert.encoder.layer.0.attention.output.dense.weight',
'bert.encoder.layer.0.attention.output.dense.bias',
'bert.encoder.layer.0.attention.output.LayerNorm.weight',
'bert.encoder.layer.0.attention.output.LayerNorm.bias',
'bert.encoder.layer.0.intermediate.dense.weight',
'bert.encoder.layer.0.intermediate.dense.bias',
'bert.encoder.layer.0.output.dense.weight',
'bert.encoder.layer.0.output.dense.bias',
'bert.encoder.layer.0.output.LayerNorm.weight',
'bert.encoder.layer.0.output.LayerNorm.bias',
'bert.encoder.layer.1.attention.self.query.weight',
'bert.encoder.layer.1.attention.self.query.bias',
'bert.encoder.layer.1.attention.self.key.weight',
'bert.encoder.layer.1.attention.self.key.bias',
'bert.encoder.layer.1.attention.self.value.weight',
'bert.encoder.layer.1.attention.self.value.bias',
'bert.encoder.layer.1.attention.output.dense.weight',
'bert.encoder.layer.1.attention.output.dense.bias',
'bert.encoder.layer.1.attention.output.LayerNorm.weight',
'bert.encoder.layer.1.attention.output.LayerNorm.bias',
'bert.encoder.layer.1.intermediate.dense.weight',
'bert.encoder.layer.1.intermediate.dense.bias',
'bert.encoder.layer.1.output.dense.weight',
'bert.encoder.layer.1.output.dense.bias',
'bert.encoder.layer.1.output.LayerNorm.weight',
'bert.encoder.layer.1.output.LayerNorm.bias',
'bert.encoder.layer.2.attention.self.query.weight',
'bert.encoder.layer.2.attention.self.query.bias',
'bert.encoder.layer.2.attention.self.key.weight',
'bert.encoder.layer.2.attention.self.key.bias',
'bert.encoder.layer.2.attention.self.value.weight',
'bert.encoder.layer.2.attention.self.value.bias',
'bert.encoder.layer.2.attention.output.dense.weight',
'bert.encoder.layer.2.attention.output.dense.bias',
'bert.encoder.layer.2.attention.output.LayerNorm.weight',
'bert.encoder.layer.2.attention.output.LayerNorm.bias',
'bert.encoder.layer.2.intermediate.dense.weight',
'bert.encoder.layer.2.intermediate.dense.bias',
'bert.encoder.layer.2.output.dense.weight',
'bert.encoder.layer.2.output.dense.bias',
'bert.encoder.layer.2.output.LayerNorm.weight',
'bert.encoder.layer.2.output.LayerNorm.bias',
...中略...
'bert.encoder.layer.11.attention.self.query.weight',
'bert.encoder.layer.11.attention.self.query.bias',
'bert.encoder.layer.11.attention.self.key.weight',
'bert.encoder.layer.11.attention.self.key.bias',
'bert.encoder.layer.11.attention.self.value.weight',
'bert.encoder.layer.11.attention.self.value.bias',
'bert.encoder.layer.11.attention.output.dense.weight',
'bert.encoder.layer.11.attention.output.dense.bias',
'bert.encoder.layer.11.attention.output.LayerNorm.weight',
'bert.encoder.layer.11.attention.output.LayerNorm.bias',
'bert.encoder.layer.11.intermediate.dense.weight',
'bert.encoder.layer.11.intermediate.dense.bias',
'bert.encoder.layer.11.output.dense.weight',
'bert.encoder.layer.11.output.dense.bias',
'bert.encoder.layer.11.output.LayerNorm.weight',
'bert.encoder.layer.11.output.LayerNorm.bias',
'bert.pooler.dense.weight',
'bert.pooler.dense.bias',
'classifier.weight',
'classifier.bias']
簡単に説明すると、以下の部分が先頭の各種埋め込み層とLayerNormになります。
['bert.embeddings.word_embeddings.weight',
'bert.embeddings.position_embeddings.weight',
'bert.embeddings.token_type_embeddings.weight',
'bert.embeddings.LayerNorm.weight',
'bert.embeddings.LayerNorm.bias',
以下はEncoderの1層分です。baseではこれが12層あるため、layer.0~layer.11まであります。
'bert.encoder.layer.0.attention.self.query.weight',
'bert.encoder.layer.0.attention.self.query.bias',
'bert.encoder.layer.0.attention.self.key.weight',
'bert.encoder.layer.0.attention.self.key.bias',
'bert.encoder.layer.0.attention.self.value.weight',
'bert.encoder.layer.0.attention.self.value.bias',
'bert.encoder.layer.0.attention.output.dense.weight',
'bert.encoder.layer.0.attention.output.dense.bias',
'bert.encoder.layer.0.attention.output.LayerNorm.weight',
'bert.encoder.layer.0.attention.output.LayerNorm.bias',
'bert.encoder.layer.0.intermediate.dense.weight',
'bert.encoder.layer.0.intermediate.dense.bias',
'bert.encoder.layer.0.output.dense.weight',
'bert.encoder.layer.0.output.dense.bias',
'bert.encoder.layer.0.output.LayerNorm.weight',
'bert.encoder.layer.0.output.LayerNorm.bias',
以下はEncoderの最終層の先頭(CLSトークン相当部分)に線形層を通すpoolerの部分です。
'bert.pooler.dense.weight',
'bert.pooler.dense.bias',
最後に分類のためのヘッド(線形層)があります。
'classifier.weight',
'classifier.bias']
構造の詳細は、BERTの元論文やTransformerの解説をご覧ください。
設定方法
事前学習モデルの部分には"bert"という文字列、ヘッドには"classifier"という文字列で見分けて設定します。
body_params \
= [p for n,p in model.named_parameters() if n.startswith("bert") and p.requires_grad]
head_params \
= [p for n,p in model.named_parameters() if n.startswith("classifier") and p.requires_grad]
# 別々にLearning Rateを設定するには以下のようにする
optimizer = AdamW([
{"params": body_params, 'lr': 0.0},
{"params": head_params, 'lr': 2e-5}
])
scheduler = get_constant_schedule(optimizer)
動作確認
動作確認をします。念のため、先頭と末尾のパラメータをバックアップして学習前後の変化を確認します。
from datasets import load_dataset
from transformers import AutoTokenizer
from transformers import AutoModelForSequenceClassification
from transformers import TrainingArguments
from transformers import Trainer
from transformers import AdamW, get_constant_schedule
from sklearn.metrics import accuracy_score, f1_score
import torch
# データセットのロード
dataset = load_dataset("tyqiangz/multilingual-sentiments", "japanese")
# # 実験のためデータセットを縮小したい場合はコチラを有効化
# from datasets import DatasetDict
# 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))
backup1 = list(model.parameters())[0].to('cpu').detach().numpy().copy()
backup2 = list(model.parameters())[-1].to('cpu').detach().numpy().copy()
# メトリクスの定義
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"
# 事前学習モデルの部分には"bert"という文字列、ヘッドには"classifier"という文字列で見分けて設定
body_params = [p for n,p in model.named_parameters() if n.startswith("bert") and p.requires_grad]
head_params = [p for n,p in model.named_parameters() if n.startswith("classifier") and p.requires_grad]
# 別々にLearning Rateを設定するには以下のようにする
optimizer = AdamW([
{"params": body_params, 'lr': 0},
{"params": head_params, 'lr': 2e-5}
])
scheduler = get_constant_schedule(optimizer)
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()
以下のように、先頭の層は値が変わらないことを確認できました。
backup1 - list(model.parameters())[0].to('cpu').detach().numpy().copy()
array([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
...,
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]], dtype=float32)
また以下のように、末尾の層はパラメータが更新されていることが分かります。
backup2 - list(model.parameters())[-1].to('cpu').detach().numpy().copy()
array([ 0.00139277, 0.00139004, -0.00139202], dtype=float32)
今回のように複数のLearning Rateを設定した場合、trainerのログには先頭のLearning Rateが記載されるようですので、ご注意ください。
train_log = [i for i in trainer.state.log_history if "loss" in i]
train_log
[{'loss': 0.9356, 'learning_rate': 0, 'epoch': 0.14, 'step': 1},
{'loss': 0.9791, 'learning_rate': 0, 'epoch': 0.29, 'step': 2},
{'loss': 0.9827, 'learning_rate': 0, 'epoch': 0.43, 'step': 3},
{'loss': 0.9548, 'learning_rate': 0, 'epoch': 0.57, 'step': 4},
{'loss': 0.8873, 'learning_rate': 0, 'epoch': 0.71, 'step': 5},
{'loss': 0.9497, 'learning_rate': 0, 'epoch': 0.86, 'step': 6},
{'loss': 0.844, 'learning_rate': 0, 'epoch': 1.0, 'step': 7},
{'loss': 0.903, 'learning_rate': 0, 'epoch': 1.14, 'step': 8},
{'loss': 0.9116, 'learning_rate': 0, 'epoch': 1.29, 'step': 9},
{'loss': 0.8593, 'learning_rate': 0, 'epoch': 1.43, 'step': 10},
{'loss': 0.9059, 'learning_rate': 0, 'epoch': 1.57, 'step': 11},
{'loss': 0.8507, 'learning_rate': 0, 'epoch': 1.71, 'step': 12},
{'loss': 0.8737, 'learning_rate': 0, 'epoch': 1.86, 'step': 13},
...
より進んだ応用例
より進んだ応用例としては以下も参考にされてください。weight_decayなども各層で変更している例となります。
まとめ
いかがでしたでしょうか?
実際にBERTなどのfine-tuningは、より大規模モデル(largeなど)になるにつれて学習が難しくなるタスクもあるようです。 そういった際に、Learning Rateの細かい調整が必要になるシーンがあるため記事にしてみました。
本記事がHugging Faceを使われる方の参考になれば幸いです。