下書きにあったものをとりあえず放出。
2023年8月おわりくらいの下書き。無加工なので、組織名などが当時のものになっています。
ココカラ。
LINEのNLP Foundation Devチームから36億パラメータの日本語言語モデルが公開されています。
https://engineering.linecorp.com/ja/blog/3.6-billion-parameter-japanese-language-model
そして、対話用にチューニングしたモデルも出ています。
https://engineering.linecorp.com/ja/blog/3.6b-japanese-language-model-with-improved-dialog-performance-by-instruction-tuning
なのだけど、対話モデルが出る前に、ベースモデルを調整して対話できるようにして、あとlm-evaluation-harnessで性能評価もしてみてたので、まとめておきます。
Instructionチューニング
言語モデルは、文章の続きを生成する仕組みになっています。
なので例えば「言語モデルは、」を与えると、「言語モデルは、音声を解釈して、 音声を復号化する。」のような文章が生成されます。
LLMでのチャットの多くは、次のような対話履歴を渡して続きを生成させるようになっています。
ユーザー: 日本の首都は?
システム: 東京です。
ユーザー: 東京には何がある?
システム:
けれども、これを対話用にチューニングされてないベースモデルに渡すと、次のように勝手に続きの対話を埋めていきがちです。もちろん、たまたまちゃんと返答になることもあります。

そこで対話用の返答のやりかたを仕込んであげる必要があるのですが、そのようなチューニングをInstructionチューニングと呼ぶようです。
LINEの対話モデルもInstructionチューニングされていますが、今回はそれを自分でやってみようという感じ。
Instructionチューニング用データセット
そうすると、学習のためのデータセットをどうするかということになります。
ここで、DatabricksがEleutherAIのpyhiaをInstructionチューニングした[Dolly 2.0]があります。
Free Dolly: Introducing the World's First Open and Commercially Viable Instruction-Tuned LLM - The Databricks Blog
このチューニングに使われたデータセットが公開されています。
databricks/databricks-dolly-15k · Datasets at Hugging Face
そして、このデータセットをkunishouさんが日本語化されていますので、これを使いましょう。
kunishou/databricks-dolly-15k-ja · Datasets at Hugging Face
LoRAファインチューニング
ということでチューニングをするのだけど、36億パラメータを全部更新するようなファインチューニングには読み込んだパラメータの数倍のVRAMが必要になります。
そんなメモリはない!
そこで、パラメータの一部を更新していい感じにチューニングを行うLoRAというテクニックを使います。
といっても、PEFT(Parameter-Efficient Fine-Tuning)というライブラリがあるのでそれを使うだけだけど。
https://github.com/huggingface/peft
ただ、PEFTではどこのパラメータを更新するのか指定する必要があります。
それを確認するためにこういうコードを動かす。モデルを読み込んで表示するだけ。
from transformers import AutoModelForCausalLM
model_name = "line-corporation/japanese-large-lm-3.6b"
model = AutoModelForCausalLM.from_pretrained(
model_name, device_map='cpu')
print(model)
こんな感じに表示されます。GPTNeoXというモデルになってることがわかります。
GPTNeoXForCausalLM(
(gpt_neox): GPTNeoXModel(
(embed_in): Embedding(51200, 3072)
(emb_dropout): Dropout(p=0.0, inplace=False)
(layers): ModuleList(
(0-29): 30 x GPTNeoXLayer(
(input_layernorm): LayerNorm((3072,), eps=1e-05, elementwise_affine=True)
(post_attention_layernorm): LayerNorm((3072,), eps=1e-05, elementwise_affine=True)
(post_attention_dropout): Dropout(p=0.0, inplace=False)
(post_mlp_dropout): Dropout(p=0.0, inplace=False)
(attention): GPTNeoXAttention(
(rotary_emb): GPTNeoXRotaryEmbedding()
(query_key_value): Linear(in_features=3072, out_features=9216, bias=True)
(dense): Linear(in_features=3072, out_features=3072, bias=True)
(attention_dropout): Dropout(p=0.0, inplace=False)
)
(mlp): GPTNeoXMLP(
(dense_h_to_4h): Linear(in_features=3072, out_features=12288, bias=True)
(dense_4h_to_h): Linear(in_features=12288, out_features=3072, bias=True)
(act): GELUActivation()
)
)
)
(final_layer_norm): LayerNorm((3072,), eps=1e-05, elementwise_affine=True)
)
(embed_out): Linear(in_features=3072, out_features=51200, bias=False)
)
LoRAでは基本的にアテンションの線形レイヤーを対象にすればいいということなのでLinearを探します。(attention)というところを見ると(query_key_value)と(dense)という層があるのがわかります。そして、だいたい`(query_key_value)'だけを対象にすればいいっぽい。
import torch
import datasets
from transformers import AutoTokenizer, AutoModelForCausalLM, Trainer, TrainingArguments
from peft import get_peft_model, LoraConfig, TaskType, PeftModel, PeftConfig
model_name = "line-corporation/japanese-large-lm-1.7b"
peft_model_name = "peft_model"
dataset_name = "kunishou/databricks-dolly-15k-ja"
targets = ["query_key_value"]
prompt_template_cqa = """ユーザー: 次の情報を元に質問に答えてください。{input}
システム: わかりました。
ユーザー: {instruction}
システム: """
prompt_template_oqa = """ユーザー: {instruction}
システム: """
def encode(sample):
if (sample["input"]):
prompt = prompt_template_cqa.format(instruction=sample["instruction"], input=sample["input"])
else:
prompt = prompt_template_oqa.format(instruction=sample["instruction"])
target = sample["output"] + tokenizer.eos_token
input_ids_prompt, input_ids_target = tokenizer([prompt, target]).input_ids
input_ids = input_ids_prompt + input_ids_target
labels = input_ids.copy()
labels[:len(input_ids_prompt)] = [-100] * len(input_ids_prompt)
return {"input_ids": input_ids, "labels": labels}
def get_collator(tokenizer, max_length):
def collator(batch):
batch = [{ key: value[:max_length] for key, value in sample.items() } for sample in batch ]
batch = tokenizer.pad(batch, padding=True)
batch["labels"] = [ e + [-100] * (len(batch["input_ids"][0]) - len(e)) for e in batch["labels"] ]
batch = { key: torch.tensor(value) for key, value in batch.items() }
return batch
return collator
# prepare dataset
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)
dataset = datasets.load_dataset(dataset_name)
dataset = dataset.map(encode)
dataset = dataset["train"].train_test_split(0.2)
train_dataset = dataset["train"]
val_dataset = dataset["test"]
# load model
base_model = AutoModelForCausalLM.from_pretrained(model_name, device_map={"": 0}, torch_dtype=torch.float16)
peft_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
inference_mode=False,
target_modules=targets,
r=16,
lora_alpha=32,
lora_dropout=0.05
)
model = get_peft_model(base_model, peft_config)
model.print_trainable_parameters()
training_args = TrainingArguments(
output_dir="./train_results",
learning_rate=2e-4,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
per_device_eval_batch_size=16,
num_train_epochs=1,
logging_strategy='steps',
logging_steps=10,
save_strategy='epoch',
evaluation_strategy='epoch',
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
greater_is_better=False,
save_total_limit=2
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=val_dataset,
data_collator=get_collator(tokenizer, 512)
)
trainer.train()
model = trainer.model
model.save_pretrained(peft_model_name)
評価
https://github.com/Stability-AI/lm-evaluation-harness
https://github.com/Stability-AI/lm-evaluation-harness/blob/jp-stable/docs/prompt_templates.md
ユーザー: 与えられた選択肢の中から、最適な答えを選んでください。
システム: 分かりました。
ユーザー: 質問:{question}
選択肢:
- {choice0}
- {choice1}
...
- {choice4}
システム: {answer}
open file in utf-8 by kishida · Pull Request #11 · shunk031/huggingface-datasets_JGLUE · GitHub
Set pad_token_id for hugging face model to suppress warning by kishida · Pull Request #82 · Stability-AI/lm-evaluation-harness · GitHub
make dir only if directory is specified in 'output_path' by kishida · Pull Request #79 · Stability-AI/lm-evaluation-harness · GitHub
3 epochでチューニング
先ほどは各データ1回ずつという1 epochで学習を行ったのだけど、3 epochほどまわしてみました。
9時間半かかって終了。
A100 80GBを8台使い、3epoch学習しました。1epochの学習にかかる時間はおおむね10分程度でした
というのを見ると「GPU力こそパワー」というのを実感しますね。


LoRAモデルのマージ
# https://note.com/__olender/n/n7913ac32c18c#f6bc06e7-c594-42ff-a8f6-72c1a5aab972
import torch
from peft import PeftConfig, PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "line-corporation/japanese-large-lm-3.6b"
peft_name = "lora/dolly-line-3.6b"
output_dir = peft_name + "-merged"
# PEFT(LoRA)の指定
peft_config = PeftConfig.from_pretrained(peft_name)
print (f"lora loaded {peft_name}")
# ベースモデルの読み込み
model = AutoModelForCausalLM.from_pretrained(
model_name,
return_dict=True,
torch_dtype=torch.float16,
)
print (f"model loaded {model_name}")
# Rinnaのトークナイザーでは、「use_fast=False」も必要になる
try:
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast = False)
except ValueError:
tokenizer = AutoTokenizer.from_pretrained(model_name)
# PEFT(LoRA)の読み込み
model = PeftModel.from_pretrained(model, peft_name)
# マージモデル作成
merged_model = model.merge_and_unload()
# 出力
merged_model.save_pretrained(output_dir)
tokenizer.save_pretrained(output_dir)
print(f"Saving to {output_dir}")
RLHF
https://huggingface.co/datasets/kunishou/hh-rlhf-49k-ja