🥇

Mistral-adapter

2024/02/13に公開

はじめに

こんにちは、@raksparadoxです。
今日はSpiral.AI株式会社のエンジニアブログとして、Mistralモデル にllama-adapter ファインチュニングしていきたいと思います。
Mistralモデルは、2023年9月にMistral AIによってリリースされた強力な言語モデルです。これは、多くのタスクでLlama13bのようなより大きなモデルを上回る、SOTAの7bモデルでした。後に、Stability AIは同じモデルの日本語指示にファインチューニングされたバージョンであるjapanese-stablelm-instruct-gamma-7bをリリースしました。今回は、実験的にこの日本語チューニングされたinstructモデルをhuggingfaceのPEFTライブラリを使用してファインチューニングしてみたいと思います。

Peftには、低リソースでLLMをファインチューニングための多くの方法があります。その方法の一つがllama-adapterです。この論文で説明されているように、Llama-adapterは顕著な注目を集め、GitHubで5千以上のスターを獲得しました。これは、モデルを指示に沿って調整するための有望な方法でした。しかし、他の多くの方法やモデルと同様に、これはLlamaモデルとのみ互換性があります。直感としては、LlamaとMistralが非常に似たアーキテクチャを持っているため、MistralにLlama-adapterを簡単に使用できると考えられましたが、実際にはもう少し複雑でした。このブログでは、MistralモデルでLlama-adapterを使用する方法を示します。Peftライブラリをローカルにインストールし、それに変更を加えることになります。

Llama-adapterをMistralモデルで使用しようとするとどうなるか見てみましょう。

Traceback (most recent call last):
  File "/home/ec2-user/mistral-adapter/train.py", line 45, in <module>
    model = get_peft_model(model, config)
  File "/opt/conda/envs/llama2/lib/python3.10/site-packages/peft/mapping.py", line 133, in get_peft_model
    return MODEL_TYPE_TO_PEFT_MODEL_MAPPING[peft_config.task_type](model, peft_config, adapter_name=adapter_name)
  File "/opt/conda/envs/llama2/lib/python3.10/site-packages/peft/peft_model.py", line 1043, in __init__
    super().__init__(model, peft_config, adapter_name)
  File "/opt/conda/envs/llama2/lib/python3.10/site-packages/peft/peft_model.py", line 125, in __init__
    self.base_model = cls(model, {adapter_name: peft_config}, adapter_name)
  File "/opt/conda/envs/llama2/lib/python3.10/site-packages/peft/tuners/adaption_prompt/model.py", line 59, in __init__
    self.add_adapter(adapter_name, configs[adapter_name])
  File "/opt/conda/envs/llama2/lib/python3.10/site-packages/peft/tuners/adaption_prompt/model.py", line 64, in add_adapter
    config = prepare_config(config, self.model)
  File "/opt/conda/envs/llama2/lib/python3.10/site-packages/peft/tuners/adaption_prompt/config.py", line 67, in prepare_config
    raise ValueError("Unsupported model type for adaption prompt: '{model.config.model_type}'.")
ValueError: Unsupported model type for adaption prompt: '{model.config.model_type}'.

llama-adapterはmistralをサポートしていません。

PEFTの編集

mistralをllama-adapterで使用できるように、peftライブラリをローカルでインストールして編集しましょう。
これは仮想環境で行うことをお勧めします。

git clone https://github.com/huggingface/peft
cd peft
pip install -e .

これにより、システムにライブラリが編集可能モードでクローンされ、インストールされます。

llama-adapterのコードはpeft/src/peft/tuners/adaption-promptにあります。
主要なファイルは4つあります。

- adaption_prompt/
  - __pycache__/
  - __init__.py
  - config.py *M*
  - layer.py *M*
  - model.py 
  - utils.py *M*

config.py、layer.py、utils.pyを修正します。
では、前のエラーは

raise ValueError("Unsupported model type for adaption prompt: '{model.config.model_type}'.")

config.pyファイル内にありますので、config.pyを編集してmistralも受け入れるようにしましょう。config.pyの57行目に移動し、TRANSFORMERS_MODEL_CONFIGに以下を追加します。mistralはllamaと似ているため、大きな変更は必要ありません。

//Lines 58-64
TRANSFORMERS_MODEL_CONFIG  = {
"llama": ModelTypeConfig(
compute_query_states=llama_compute_query_states,
target_modules="self_attn",
k_proj_layer="k_proj",
v_proj_layer="v_proj",
o_proj_layer="o_proj",
),
+"mistral": ModelTypeConfig( # same as llama,
+compute_query_states=llama_compute_query_states,
+target_modules="self_attn",
+k_proj_layer="k_proj",
+v_proj_layer="v_proj",
+o_proj_layer="o_proj",
+),
+}

では、コードを実行してみましょう。MistralとLlamaのアーキテクチャは同じであるため、これで問題なく動作するはずですよね?

RuntimeError: shape '[1, 10, 32, 128]' is invalid for input of size 10240

おや、これはどんなエラーでしょうか!
MistralとLlama (Llama2) のアーキテクチャについてもっと詳しく調査しましょう。

MistralForCausalLM(
  (model): MistralModel(
    (embed_tokens): Embedding(32000, 4096)
    (layers): ModuleList(
      (0-31): 32 x MistralDecoderLayer(
        (self_attn): MistralAttention(
          (q_proj): Linear(in_features=4096, out_features=4096, bias=False)
          (k_proj): Linear(in_features=4096, out_features=1024, bias=False)
          (v_proj): Linear(in_features=4096, out_features=1024, bias=False)
          (o_proj): Linear(in_features=4096, out_features=4096, bias=False)
          (rotary_emb): MistralRotaryEmbedding()
        )
        (mlp): MistralMLP(
          (gate_proj): Linear(in_features=4096, out_features=14336, bias=False)
          (up_proj): Linear(in_features=4096, out_features=14336, bias=False)
          (down_proj): Linear(in_features=14336, out_features=4096, bias=False)
          (act_fn): SiLU()
        )
        (input_layernorm): MistralRMSNorm()
        (post_attention_layernorm): MistralRMSNorm()
      )
    )
    (norm): MistralRMSNorm()
  )
  (lm_head): Linear(in_features=4096, out_features=32000, bias=False)
)

----------------------------------------------------------------------------------------------------------------------------------------------------------

LlamaForCausalLM(
  (model): LlamaModel(
    (embed_tokens): Embedding(32000, 4096)
    (layers): ModuleList(
      (0-31): 32 x LlamaDecoderLayer(
        (self_attn): LlamaSdpaAttention(
          (q_proj): Linear(in_features=4096, out_features=4096, bias=False)
          (k_proj): Linear(in_features=4096, out_features=4096, bias=False)
          (v_proj): Linear(in_features=4096, out_features=4096, bias=False)
          (o_proj): Linear(in_features=4096, out_features=4096, bias=False)
          (rotary_emb): LlamaRotaryEmbedding()
        )
        (mlp): LlamaMLP(
          (gate_proj): Linear(in_features=4096, out_features=11008, bias=False)
          (up_proj): Linear(in_features=4096, out_features=11008, bias=False)
          (down_proj): Linear(in_features=11008, out_features=4096, bias=False)
          (act_fn): SiLU()
        )
        (input_layernorm): LlamaRMSNorm()
        (post_attention_layernorm): LlamaRMSNorm()
      )
    )
    (norm): LlamaRMSNorm()
  )
  (lm_head): Linear(in_features=4096, out_features=32000, bias=False)
)

llama-adapterがattention周りをラップしているため、最初に見た時、両方のself attentionモジュールは同じに見えますが、より近くで検討すると:

Mistral
(k_proj): Linear(in_features=4096, out_features=1024, bias=False)
          (v_proj): Linear(in_features=4096, out_features=1024, bias=False)

Llama
      (k_proj): Linear(in_features=4096, out_features=4096, bias=False)
          (v_proj): Linear(in_features=4096, out_features=4096, bias=False)

Mistralのk_projとv_projはLlamaとは異なるサイズの出力を持っています。その理由は、MistralがGrouped Query attentionを使用しているのに対し、Llama2-7bがMultihead Attentionを使用しているからです。それについての詳細はこちらで読むことができます。

layer.pyから始めましょう。
forward関数の中で、新しい変数を追加しましょう。

 // Lines 78-80
+ factor = (
+ self.model.k_proj.in_features // self.model.k_proj.out_features
+ )

これはk_projとv_projレイヤーの入力と出力の次元の比率です(ここでは4です)。

次に、key tensorがadapter_kとadapter_vに再形成されている行で、それを変更します。

// Lines 88-98
# (bsz, num_key_value_heads, adapter_len, head_dim)
        adapter_k = (
-           key.view(1, self.adapter_len, self.model.num_heads, self.model.head_dim)
+           key.view(1, self.adapter_len, (self.model.num_heads // factor), self.model.head_dim)
            .repeat(bsz, 1, 1, 1)
            .transpose(1, 2)
        )
        adapter_v = (
-           value.view(1, self.adapter_len, self.model.num_heads, self.model.head_dim)
+           value.view(1, self.adapter_len, (self.model.num_heads // factor), self.model.head_dim)
            .repeat(bsz, 1, 1, 1)
            .transpose(1, 2)
        )

ここでは、factorによってnum_headsを再形成しています。

次に、adapter_kとadapter_vを繰り返す必要があります。

//Lines 99-102
+ # Below is taken from https://github.com/huggingface/transformers/blob/e547458c43dfdbbb8f6a7757237e234c44e20a8f/src/transformers/models/mistral/modeling_mistral.py#L181
+ # (bsz, num_heads, adapter_len, head_dim)
+ adapter_k  =  torch.repeat_interleave(adapter_k, repeats=factor, dim=1)
+ adapter_v  =  torch.repeat_interleave(adapter_v, repeats=factor, dim=1)

これでlayer.pyの編集は終わりです。
最後にutils.pyに移りましょう。ここで変更する必要があるのは1つだけで、llama_compute_query_states関数に変数factorを追加し、valueの状態を変更します。

// Lines 72-76
+ factor = (
+ self.model.k_proj.in_features // self.model.k_proj.out_features
+ )
- value_states = model.v_proj(hidden_states).view(bsz, q_len, model.num_heads, model.head_dim).transpose(1, 2)
+ value_states = model.v_proj(hidden_states).view(bsz, q_len, (model.num_heads // factor), model.head_dim).transpose(1, 2)

素晴らしい!これで準備は完了です、テストしましょう!

学習と推論

train.pyを作成し、以下のコードを書いてください(ご自身のデータセットを使用してください)。
今回はデータセットは 'saldra/sakura_japanese_dataset' 使います。

import  os
import  torch
import  transformers
from datasets import load_dataset, concatenate_datasets
from transformers import AutoTokenizer, AutoModelForCausalLM, Trainer, TrainingArguments
from peft import prepare_model_for_kbit_training, get_peft_model
from peft import AdaptionPromptConfig

# Seed
seed  =  42
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)

# Best current model for Japanese is Mistral based Gamma trained by Stability Ai
tokenizer  = AutoTokenizer.from_pretrained("stabilityai/japanese-stablelm-instruct-gamma-7b")
model  = AutoModelForCausalLM.from_pretrained(
"stabilityai/japanese-stablelm-instruct-gamma-7b",
load_in_8bit=True,
device_map="auto",
torch_dtype=torch.float16
)
print(model)

# Prepare model for k-bit training
model  = prepare_model_for_kbit_training(model)
tokenizer.pad_token =  "<unk>"

# Configure Adapter
config  = AdaptionPromptConfig(
adapter_layers=30,
adapter_len=10,
task_type="CAUSAL_LM"  
)
model  = get_peft_model(model, config)
print(model)

# Function to print trainable parameters
def  print_trainable_parameters(m):
	trainable_params  =  sum(p.numel() for  p  in  m.parameters() if  p.requires_grad)

	all_params  =  sum(p.numel() for  p  in  m.parameters())

	print(f"trainable params: {trainable_params} || all params: {all_params} || trainable%: {100  *  trainable_params  /  all_params}")
print_trainable_parameters(model)

# Load dataset
dataset  = load_dataset('saldra/sakura_japanese_dataset')

dataset  =  dataset['train']
print(dataset)

# The prompt is taken from (https://huggingface.co/stabilityai/japanese-stablelm-instruct-gamma-7b)

def  generate_prompt(user_query, sep="\n\n### "):
    sys_msg  =  "以下は、タスクを説明する指示と、文脈のある入力の組み合わせです。要求を適切に満たす応答を書きなさい。"
    p  =  sys_msg
    roles  = ["指示", "応答"]
    msgs  = [": \n"  +  user_query['input'], ": \n"]
    for  role, msg  in  zip(roles, msgs):
        p  += sep +  role  +  msg
    return  p  +  user_query['output']

# Function to tokenize
def  tokenize(prompt):
	return  tokenizer(
        prompt  +  tokenizer.eos_token,
        truncation=True,
        max_length=512,
        padding="max_length"
        )
	
# Process and tokenize the data
train_data  =  dataset .shuffle().map(lambda  x: tokenize(generate_prompt(x)), remove_columns=["input","output","instruction"])

log_eval_step  =  5
print(log_eval_step)
trainer  = Trainer(
model=model,
train_dataset=train_data,
args=TrainingArguments(
remove_unused_columns=False,
per_device_train_batch_size=4, 
gradient_accumulation_steps=8,
num_train_epochs=6,
learning_rate=1e-3,
logging_steps=log_eval_step,
optim="adamw_torch",
save_strategy="epoch",
output_dir="llama-adapter-mistral-rachel",
),
data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False)
)
model.config.use_cache =  False

# Start training
trainer.train()

Mistralモデルをllama-adapterを使用してトレーニングしました。
さて、結果をテストしましょう。

データセットからの入力に似た3つのinputをGPT4に生成させ、オリジナルのモデルとチューニングされたモデルの出力を比較します。

============================== ORIGINAL ==============================
### 指示: 
ある学校で、全生徒が参加したマラソン大会がありました。生徒たちは合計で800kmを走りました。男子生徒は全体の距離の60%を、女子生徒は残りの距離を走りました。女子生徒は何km走ったでしょうか?

### 応答: 
女子生徒は、800kmのうち400kmを走ったことになります。</s>

--------------------------------------------------
### 指示: 
小林さんはレモネードを作るために、10%の濃度のレモンジュース500mlを使用しました。これを、2%の濃度の飲み物に希釈したいと考えています。何mlの水を加えればよいでしょうか?

### 応答: 
レモンジュースには500mlの水が必要です。</s>

--------------------------------------------------
### 指示: 
あるクラスで、生徒たちは植樹活動に参加しました。4年1組は15本の木を、4年2組は4年1組の木の数の50%、4年3組は4年2組の木の数の150%を植えました。4年1組と4年3組ではどちらが多くの木を植えたでしょうか? (2通りで比較してください)。

### 応答: 
4年1組は4年2組の木の数の75%、4年3組は4年2組の木の数の225%を植えました。よって4年1組が多くの木を植えたことになります。</s>

--------------------------------------------------
============================== FINETUNED ==============================
### 指示: 
ある学校で、全生徒が参加したマラソン大会がありました。生徒たちは合計で800kmを走りました。男子生徒は全体の距離の60%を、女子生徒は残りの距離を走りました。女子生徒は何km走ったでしょうか?

### 応答: 
200km</s>

--------------------------------------------------
### 指示: 
小林さんはレモネードを作るために、10%の濃度のレモンジュース500mlを使用しました。これを、2%の濃度の飲み物に希釈したいと考えています。何mlの水を加えればよいでしょうか?

### 応答: 
3500ml</s>

--------------------------------------------------
### 指示: 
あるクラスで、生徒たちは植樹活動に参加しました。4年1組は15本の木を、4年2組は4年1組の木の数の50%、4年3組は4年2組の木の数の150%を植えました。4年1組と4年3組ではどちらが多くの木を植えたでしょうか? (2通りで比較してください)。

### 応答: 
4年1組</s>

--------------------------------------------------

感想

モデルは短い回答をするように学習しました!
モデルは9エポックで学習され、学習率は1e-3です。
llama-adapterメソッドに関する私の観察:
高い学習率と多くのエポック数が必要です。公式リポジトリでは、デフォルトの学習率が1e-3で、デフォルトのエポック数は400です!

それを試してみて、何か面白いことがあればコメントで教えてください。
PS, これらの変更についてPRを出しましたので、これからはpeftから直接使用できるようになります。

SpiralAIテックブログ

Discussion