LLama2の訓練可能な全層をQLoRAで学習する
はじめに
LLama2はMetaが23年7月に公開した、GPT-3に匹敵するレベルのオープンソース大規模言語モデル(LLM)です。
最近はFalcon 180bのような、より大きなモデルも出ていますが、デファクトスタンダードとして定着している感があります
LLMに新たな情報を加える手法として、ファインチューニング、特にQLoRAが注目されています。
しかしQLoRA、特に初期設定では一部のパラメータしか更新できません
本記事ではQLoRAの状況について整理しつつ、学習可能なパラメータを増やしてファインチューニングをしてみます
QLoRAの関連情報
LoRAの課題(?)
QLoRA(あるいはLoRA)はモデルの一部しか学習しないのが特徴です。
(ちなみにQはquantized, 量子化 の略で、 LoRAとは直接関係ありません)
そのため、QLoRAで、どこまで情報を加えられるかどうかが、あまり分かっておらず、諸説入り乱れる状態です。
例えば、、
LoRAの論文
LoRAはフルパラメータのファインチューニングに匹敵するとの報告
QLoRAではうまく知識を入れられなかった例
(そもそもファインチューニングは、事前学習で得た知識を吸い出すための補助に過ぎないという主張)
理論: LoRAとフルパラメータファインチューニングではどこが異なるのか?
LoRAはどこを更新するか?
例えばtransformersのpeftライブラリの場合、llamaにおいてはattention 層のquery (q), value (v)のみをデフォルト設定で更新しているようです。
しかし、llama2-7bには300近い層(named_parameters)が存在します。
次のコマンドで、層を確認してみます。
from transformers import pipeline
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch
#モデル読み込み
model_name = "meta-llama/Llama-2-7b-chat-hf"
#load base model
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name,
quantization_config=bnb_config,
device_map="auto",
)
#層の表示
for name, param in model.named_parameters():
print(name)
出力結果
model.embed_tokens.weight
model.layers.0.self_attn.q_proj.weight
model.layers.0.self_attn.k_proj.weight
model.layers.0.self_attn.v_proj.weight
model.layers.0.self_attn.o_proj.weight
model.layers.0.mlp.gate_proj.weight
model.layers.0.mlp.up_proj.weight
model.layers.0.mlp.down_proj.weight
model.layers.0.input_layernorm.weight
model.layers.0.post_attention_layernorm.weight
model.layers.1.self_attn.q_proj.weight
...
各層について、GPT-4に意味を聞いてみます。
llama2の構造については以下の記事がわかりやすそうです。
元のLoRA論文で更新されなかった層
peftライブラリでは,q,vしか更新しないデフォルト設定であることがわかりました。
一方、元の論文では、attention層のQuery, Key, Valueに加えて、層全体の出力に関わる(?) outputも一緒に更新すると、最高性能が得られるとの報告でした。
しかし、LLMの中には、LoRAの論文で対象としたattention層に加えて、tokenに対するembedding層やMLPが、訓練可能なレイヤーとして存在します。
誰かが既に試しているかもしれませんが、これらも一緒に更新する価値は、あるかもしれません。
[メモ]
embedding: 単語の「意味」を更新 (ファインチューニングで新たなドメインや単語を学習させたい時?)
MLP: モデル全般の処理能力に関連?
備考1: LoRA≒フルパラメータのファインチューニングとなる条件
LoRAですべての学習可能なパラメータを更新する設定にした上で、ハイパーパラメータr (詳細は割愛; どこかのweb記事などを参照)を十分に大きくすれば、フルパラメータのファインチューニングに挙動が収束するように思います(間違っていたら、教えてください。)
備考2: トークナイザーの影響
文章をどのように分割するかという、tokenizerも学習モデルの性能に影響します。
tokenizerを工夫することで、推論・学習速度が上がるとの報告がありました。
一方、性能自体に向上は見られなかったようなので、今回はtokenizerについては扱いません。
実践: すべての学習可能な層をQLoRAする
Adapter層の追加
peftライブラリで訓練可能なパラメータ*に、adapterを付けていきます
(*dense, conv1d層)
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch
#load base model
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
#4bitで読み込みたいときは、quantization_configを指定する。
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name,
quantization_config=bnb_config,
device_map="auto",
)
#peftモデルの定義
from peft import LoraConfig, get_peft_model
#adapter層を付けられるレイヤー名
target_modules=[
"embed_tokens",
"lm_head",
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj",
]
#rはハイパーパラメータ
peft_config = LoraConfig(
task_type="CAUSAL_LM", inference_mode=False, r=16, lora_alpha=32,
lora_dropout=0.1,
target_modules=target_modules,
)
model = get_peft_model(model, peft_config)
追加されたレイヤーを含む全ての層は、先程と同じコマンドで確認できます。
for name, param in model.named_parameters():
print(name)
実行すると、adapter層(lora_A, lora_B)が付加されているのがわかります。
base_model.model.model.embed_tokens.weight
base_model.model.model.embed_tokens.lora_embedding_A.default
base_model.model.model.embed_tokens.lora_embedding_B.default
base_model.model.model.layers.0.self_attn.q_proj.weight
base_model.model.model.layers.0.self_attn.q_proj.lora_A.default.weight
base_model.model.model.layers.0.self_attn.q_proj.lora_B.default.weight
base_model.model.model.layers.0.self_attn.k_proj.weight
base_model.model.model.layers.0.self_attn.k_proj.lora_A.default.weight
base_model.model.model.layers.0.self_attn.k_proj.lora_B.default.weight
base_model.model.model.layers.0.self_attn.v_proj.weight
...
ちなみに、target_modules=Noneとした初期設定では、想定通り、q,vに対するadapterしか生成されていませんでした。
base_model.model.model.embed_tokens.weight
base_model.model.model.layers.0.self_attn.q_proj.weight
base_model.model.model.layers.0.self_attn.q_proj.lora_A.default.weight
base_model.model.model.layers.0.self_attn.q_proj.lora_B.default.weight
base_model.model.model.layers.0.self_attn.k_proj.weight
base_model.model.model.layers.0.self_attn.v_proj.weight
base_model.model.model.layers.0.self_attn.v_proj.lora_A.default.weight
base_model.model.model.layers.0.self_attn.v_proj.lora_B.default.weight
base_model.model.model.layers.0.self_attn.o_proj.weight
base_model.model.model.layers.0.mlp.gate_proj.weight
base_model.model.model.layers.0.mlp.up_proj.weight
base_model.model.model.layers.0.mlp.down_proj.weight
base_model.model.model.layers.0.input_layernorm.weight
base_model.model.model.layers.0.post_attention_layernorm.weight
...
訓練可能なパラメータ数は以下のコマンドで確認できます。
model.print_trainable_parameters()
出力例
trainable params: 41,132,032 || all params: 6,779,547,648 || trainable%: 0.6067076173162401
ハイパーパラメータ rを変えながら、trainable%をプロットした結果は以下のとおりです。rを増やすと、どんどん値が大きくなっていきます。
rは通常、16程度とされていますが、2000まで増やすと、43%まで増加しました。
コードを読む限り、この値は、
((元のパラメータ数)+(LoRAのパラメータ数))/(総パラメータ数)
で定義されているようなので、rを十分に大きくすると50%に収束すると思われます(100%ではない)。
学習
学習データ
学習のお題は、夏目漱石の「こころ」です。
はじめに、学習前のモデルで「あなたは腹の底から真面目ですか?」に対する回答をみてみます。
pipe= pipeline("text-generation", model=model, tokenizer=tokenizer,max_new_tokens=100)
text="Q: あなたはそのたった一人になれますか。なってくれますか。あなたははらの底から真面目ですか。 A: "
def ask(pipe,text):
out=pipe(text)
out=out[0]["generated_text"][len(text):]
return out
ask(pipe,text)
意味不明の回答が得られました。
learning rateが高めの条件で、正解となる会話をひたすら学習させてみます。
Q: あなたはそのたった一人になれますか。なってくれますか。あなたははらの底から真面目ですか。
A: もし私の命が真面目なものなら、私の今いった事も真面目です
10/16修正 不要なコードをコメントアウト
import transformers
from datasets import load_dataset,Dataset
#from scoring import generate_prompt
import random
#データセット
context_list=["Q: あなたはそのたった一人になれますか。なってくれますか。あなたははらの底から真面目ですか。 A: もし私の命が真面目なものなら、私の今いった事も真面目です"]
data_list=[{"text":i} for i in context_list]
dataset = Dataset.from_dict({"text": [item["text"] for item in data_list]})
train_dataset=dataset.map(lambda samples: tokenizer(samples['text']), batched=True)
#10 epoch回してみる
tokenizer.pad_token = tokenizer.eos_token
train_args=transformers.TrainingArguments(
per_device_train_batch_size=1,
gradient_accumulation_steps=1,
warmup_steps=0,
num_train_epochs=1,
learning_rate=2e-4,
fp16=True,
logging_steps=1,
output_dir='outputs',
)
trainer = transformers.Trainer(
model=model,
train_dataset=train_dataset,
args=train_args,
data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False)
)
ans_dict={}
for i in range(10):
training_result=trainer.train()
res=ask(pipe,text)
ans_dict[i]={"out":res,"loss":training_result.training_loss }
print(res)
#回答の確認
import pandas as pd
pd.DataFrame.from_dict(ans_dict).T
学習結果
全層でLoraした場合、query, value層のみで学習した場合のlossの違いは次の通り。
全層をQLoRAで学習させた時の回答の変遷
q,v層のみをQLoRAで学習させた時の回答の変遷
Lossの下がり方からも想像できる通り、全層学習させた方が、早く訓練データに適合することがわかりました。
今後、もう少し真面目な研究タスクで、違いを検討する予定です。
この記事が気に入ったらサポートをしてみませんか?