【No.L008】大規模言語モデルのファインチューニング方法:LoRAを使って文の意味を判定する手順

はじめに

今回は大規模言語モデルをファインチューニングする方法を紹介。モデルはパラメータ数が重いと、ファインチューニングに膨大なメモリが必要になるため、LoRA(Low-Rank Adaptation)という技術を使って2つの文が同じ意味かどうかを判定するタスクを実施してみた。具体的には、以下サイトのコードをGoogle Colab(GPU:V100)で実施した際のポイントを記載。
※Windows11(Ubuntu)、GPU:NVIDIA Geforce GTX 1660 Tiのローカル環境でも動作確認済
peft/examples/sequence_classification/LoRA.ipynb at main · huggingface/peft · GitHub

★関連リンク
業務で活用できるAI技集のまとめはこちら

内容

①必要なライブラリをインストール [1分]
Google Colabを起動して、GPUを設定後に以下コマンドを実行。GPUの設定方法は[編集]→[ノートブックの設定]で任意のGPUが選択可能。※今回はGPU動作前提のプログラムなので必ず選択。

!pip install peft==0.9.0
!pip install transformers datasets accelerate
!pip install evaluate
!pip install sentencepiece bitsandbytes
!pip install scipy
!pip install fugashi
!pip install ipadic
[参考] 動作時のライブラリのバージョン
peft 0.9.0
transformers 4.40.0
evaluate 0.4.1
datasets 2.19.0
accelerate 0.29.3
sentencepiece 0.1.99
bitsandbytes 0.43.1
scipy 1.11.4
fugashi 1.3.2
ipadic 1.00

②ライブラリのインポート [1分]

import argparse
import os

import torch
from torch.optim import AdamW
from torch.utils.data import DataLoader
from peft import (
    get_peft_config,
    get_peft_model,
    get_peft_model_state_dict,
    set_peft_model_state_dict,
    LoraConfig,
    PeftType,
    PrefixTuningConfig,
    PromptEncoderConfig,
)

import evaluate
from datasets import load_dataset
from transformers import AutoModelForSequenceClassification, AutoTokenizer, get_linear_schedule_with_warmup, set_seed
from tqdm import tqdm

③各種設定 [0秒]
ファインチューニングに使用するパラメータを指定。今回は事前学習済みの日本語BERTモデル(東北大学の乾研究室)を使って実施。GPU前提なのでdeviceは”cuda”で問題ないが、”cpu”でも動作するかは未確認。

batch_size = 32
model_name_or_path = "tohoku-nlp/bert-base-japanese-whole-word-masking"
task = "mrpc"
peft_type = PeftType.LORA
device = "cuda"
num_epochs = 20
peft_config = LoraConfig(task_type="SEQ_CLS", inference_mode=False, r=8, lora_alpha=16, lora_dropout=0.1)
lr = 3e-4
[参考] mrpcとは
Microsoft Research Paraphrase Corpusの略で、2つの文章の意味が近しいかどうかを評価するタスク。文章Aと文章Bが同じような内容であれば1(一致)、そうでなければ0(不一致)を返す。
[参考] peftとは
Parameter-Efficient Fine Tuningの略で、通常のFine Tuningとは違い、一部のパラメータのみを調整することで計算コストを削減しつつ精度を維持しようとする手法のこと。LoRA(Low-Rank Adaptation)もpeftの一種。

④データセットの準備 [20秒]
モデルの設定。汎用性をもたせるために、条件式で”padding_side”や”pad_token_id”に値を代入。

if any(k in model_name_or_path for k in ("gpt", "opt", "bloom")):
    padding_side = "left"
else:
    padding_side = "right"

tokenizer = AutoTokenizer.from_pretrained(model_name_or_path, padding_side=padding_side)

if getattr(tokenizer, "pad_token_id") is None:
    tokenizer.pad_token_id = tokenizer.eos_token_id

Hugging Faceのデータセットライブラリを使用して、指定されたGLUEタスク(General Language Understanding Evaluation、汎用言語理解評価)のデータセットをロード。

datasets = load_dataset("glue", task)
metric = evaluate.load("glue", task)

def tokenize_function(examples):
    # max_length=None => use the model max length (it's actually the default)
    outputs = tokenizer(examples["sentence1"], examples["sentence2"], truncation=True, max_length=None)
    return outputs

tokenized_datasets = datasets.map(
    tokenize_function,
    batched=True,
    remove_columns=["idx", "sentence1", "sentence2"],
)

# We also rename the 'label' column to 'labels' which is the expected name for labels by the models of the
# transformers library
tokenized_datasets = tokenized_datasets.rename_column("label", "labels")
[参考] datasetsの中身
train, validation, testの3分類が存在。それぞれの分類にsentence1, sentence2, label, idxのデータセットが数百~千程存在している。中身の例は以下のとおり。labelはsentense1と2の中身が一致しているかの正解データで、1の場合は一致。
sen1: Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .
sen2: Referring to him as only " the witness " , Amrozi accused his brother of deliberately distorting his evidence .
label: 1
idx: 0
[参考] tokenized_datasetsの中身
datasetsと同じくtrain, validation, testの3分類が存在。それぞれの分類にlabels, input_ids, token_type_ids, attention_maskがデータセット数分存在する。それぞれの意味は以下の通り。
labels: 正解のラベル(0 or 1)
input_ids: sentence1と2を結合し、単語毎にtokenized(数値化)した数値列
token_type_ids:どの単語がsentence1 or 2かを判断する数値列
attention_mask: 単語が存在するかを判断する数値列

トレーニングおよび評価のためのDataLoaderをインスタンス化。DataLoaderは、ミニバッチを処理するためのイテレータを作成している。

def collate_fn(examples):
    return tokenizer.pad(examples, padding="longest", return_tensors="pt")


# Instantiate dataloaders.
train_dataloader = DataLoader(tokenized_datasets["train"], shuffle=True, collate_fn=collate_fn, batch_size=batch_size)
eval_dataloader = DataLoader(
    tokenized_datasets["validation"], shuffle=False, collate_fn=collate_fn, batch_size=batch_size
)

⑤モデル・最適化の準備 [10秒]
Modelを設定し、最適化にはAdamWを採用。学習率lrの設定も後半で実施。

model = AutoModelForSequenceClassification.from_pretrained(model_name_or_path, return_dict=True)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()

optimizer = AdamW(params=model.parameters(), lr=lr)

# Instantiate scheduler
lr_scheduler = get_linear_schedule_with_warmup(
    optimizer=optimizer,
    num_warmup_steps=0.06 * (len(train_dataloader) * num_epochs),
    num_training_steps=(len(train_dataloader) * num_epochs),
)

⑥学習・検証 [7分]

model.to(device)
for epoch in range(num_epochs):
    model.train()
    for step, batch in enumerate(tqdm(train_dataloader)):
        batch.to(device)
        outputs = model(**batch)
        loss = outputs.loss
        loss.backward()
        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()

    model.eval()
    for step, batch in enumerate(tqdm(eval_dataloader)):
        batch.to(device)
        with torch.no_grad():
            outputs = model(**batch)
        predictions = outputs.logits.argmax(dim=-1)
        predictions, references = predictions, batch["labels"]
        metric.add_batch(
            predictions=predictions,
            references=references,
        )

    eval_metric = metric.compute()
    print(f"epoch {epoch}:", eval_metric)

Appendix

当記事に記載のあるコードを纏めたipynbファイルも併せて展開。

コメント