「[[.NET 開発基盤部会 Wiki>http://dotnetdevelopmentinfrastructure.osscons.jp]]」は、「[[Open棟梁Project>https://github.com/OpenTouryoProject/]]」,「[[OSSコンソーシアム .NET開発基盤部会>https://www.osscons.jp/dotNetDevelopmentInfrastructure/]]」によって運営されています。

-[[戻る>テキスト生成系(Transformer系)#j2f01d54]] > [[チューニング、拡張系>テキスト生成系(Transformer系)#j2f01d54]]
--[[LLMのPE]]
--[[LLMのRAG]]
--[[LLMのFT]]
---[[huggingface/transformers Trainer]]
---huggingface/trl SFTTrainer
--[[LLMエージェント]]

*目次 [#ndebe625]
#contents

*概要 [#tfd2a307]
huggingface/trl SFTTrainer

**huggingface/trl [#f564f186]
-Hugging Faceが提供するtransformersの拡張
-Transformer Reinforcement Learningの略で[[指示・強化学習>LLMのFT#nc47f676]]の機能を提供する。

**SFTTrainer [#a5f88e61]

***trlに含まれるトレーナー・クラス [#vf315bc4]
-教師ありファインチューニング(SFT)に特化したトレーナー。
-Trainerクラスを継承していて、SFT向けの機能を追加している。

***SFTTrainerでできること [#h31da3eb]
-CLM(Causal Language Modeling)による学習(次単語を予測するタスク):data_collatorを明示的に指定しない場合
 collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)

-MLM(Masked Language Modeling)による学習(マスクを予測するタスク):BERTのような双方向モデルが前提
 collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=True)

-Instruction Tuning:formatting_func、data_collatorをInstruction Tuning用に設定
 collator = DataCollatorForCompletionOnlyLM(response_template, tokenizer=tokenizer)

-LoRAによるチューニング
-packingによる学習
-NEFTuneによる学習

*詳細 [#ud2fe008]

**実装準備 [#cd8f6842]

***インストール [#ddf11f78]
-領域が足りないことがあるので注意
--「pip cache purge」&「TMPDIR=/mnt/tmp pip install」を使用
--必要に応じて[[ディスク追加>OSSのLLM#u0d612e8]]を行う。

-ModuleNotFoundError: No module named '_lzma'



***[[プロキシ環境>Python#y9ac7218]] [#w16e12cf]

***データセット [#vb74629b]
-kunishou/databricks-dolly-15k-ja
 from datasets import load_dataset
 
 dolly_dataset = load_dataset("kunishou/databricks-dolly-15k-ja")
 
 # 簡易化のためinputの値が空のデータに絞る
 # npakaさんの記事(https://note.com/npaka/n/nc7a4c20f98f8)を参考
 dolly_train_dataset = dolly_dataset['train'].filter(lambda example: example['input'] == '')
 
 print(dolly_train_dataset)
 # データ件数は全部で10,417件
 #Dataset({
 #    features: ['output', 'index', 'category', 'instruction', 'input'],
 #    num_rows: 10417
 #})
 
 print(dolly_train_dataset[0])
 #{'output': 'イコクエイラクブカ',
 # 'index': '1',
 # 'category': 'classification',
 # 'instruction': '魚の種類はどっち?イコクエイラクブカとロープ',
 # 'input': ''}

-自作JSON(jsonl)

--ロード
 train_file = "data/train.jsonl"
 dataset = datasets.load_dataset("json", data_files=train_file, split="train")
 dataset = datasets.DatasetDict({'train': dataset})

--JSONL~
各行が個別の JSON オブジェクトとして扱われるファイル形式
 {"answer":"XXX","question":"YYY","context":"ZZZ"}
 {"answer":"XXX","question":"YYY","context":"ZZZ"}
 {"answer":"XXX","question":"YYY","context":"ZZZ"}
 ...

--split="train"を書いた場合と書かない場合の話~
多くのHugging FaceのAPIやTrainerなどは Dataset オブジェクトを DatasetDict に格納する構造(例:{'train': Dataset, 'validation': Dataset})を想定している。

---書いた場合~
split="train" を指定することで、datasets.load_dataset() は Dataset 型を返し、それを明示的に DatasetDict にtrainと言うキー名でラップしているため、正しい構造になる。
 DatasetDict({
     'train': Dataset(...)
 })

---書かない場合~
split を指定しない場合、戻り値は DatasetDict 型になり、これは、自動的に "train" というキーを持つ辞書形式になる。~
その後、再度 DatasetDict にtrainと言うキー名でラップしているため、入れ子になった DatasetDict になる。コレは間違った構造。
 DatasetDict({
     'train': DatasetDict({
         'train': Dataset(...)
     })
 })

※ kunishou/databricks-dolly-15k-jaを使う例ではcontextを使用しないのでinput(≒context)の値が空のデータに絞っている。

***モデル [#b6baa2c4]
-モデルとトークナイザ(文字列と数値IDを相互変換する道具)

-2つの例を見ると、オプションに違いがあるが、コレは?
|オプション|説明|h
|device_map="auto"|GPUやCPUへ自動で重みを割り当て|
|torch_dtype=torch.bfloat16|省メモリ・高速化のためのデータ型(bfloat16)を明示|
|trust_remote_code=True|モデル定義にカスタムコードが含まれる場合に許可|
|force_download=True|キャッシュ無視して強制的に再取得|

-cyberagent/open-calm-large

--ダウンロード
 from transformers import AutoModelForCausalLM, AutoTokenizer
 # open-calm-largeは約7億パラメータの小さめのLLMです。
 # とはいえ、本記事で紹介するコードをそのまま使うとcolab pro+で使えるA100ギリギリかもしれません。(試してません)
 # お手元のGPUのリソースが限られている場合、動作確認が目的であれば、open-calm-smallに切り替えるなどしていただくと良いかもしれません。
 model_name = "cyberagent/open-calm-large"
 tokenizer = AutoTokenizer.from_pretrained(model_name)
 model = AutoModelForCausalLM.from_pretrained(
     model_name,
     device_map="auto",
     # torch_dtype=torch.float16,
     # torch.float16を指定すると、train時のlossが0.0になって学習がうまくいかない。
     # TrainerArgumentsを修正(bf16 = True)したところ正常に評価できる(TPUで事前学習したモデル)。
)

--ロード
 save_pretrainedしていれば、model = AutoModelForCausalLM.from_pretrained('./output', device_map="auto")でロードできる。

--print(model)の出力
 GPTNeoXForCausalLM(
   (gpt_neox): GPTNeoXModel(
     (embed_in): Embedding(52096, 1536)
     (emb_dropout): Dropout(p=0.0, inplace=False)
     (layers): ModuleList(
       (0-23): 24 x GPTNeoXLayer(
         (input_layernorm): LayerNorm((1536,), eps=1e-05, elementwise_affine=True)
         (post_attention_layernorm): LayerNorm((1536,), 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=1536, out_features=4608, bias=True)
           (dense): Linear(in_features=1536, out_features=1536, bias=True)
           (attention_dropout): Dropout(p=0.0, inplace=False)
         )
         (mlp): GPTNeoXMLP(
           (dense_h_to_4h): Linear(in_features=1536, out_features=6144, bias=True)
           (dense_4h_to_h): Linear(in_features=6144, out_features=1536, bias=True)
           (act): GELUActivation()
         )
       )
     )
     (final_layer_norm): LayerNorm((1536,), eps=1e-05, elementwise_affine=True)
   )
   (embed_out): Linear(in_features=1536, out_features=52096, bias=False)
 )

-Llama-3.2-1B-Instruct

--ダウンロード
 import transformers
 from transformers import (
   AutoTokenizer
 )
 
 import torch
 
 MODEL_URI  = "meta-llama/Llama-3.2-1B-Instruct"
 MODEL_PATH     = 'models/Llama-3.2-1B-Instruct'
 TOKENIZER_PATH = 'models/Llama-3.2-1B-Instruct'
 
 # model
 model = transformers.AutoModelForCausalLM.from_pretrained(
     MODEL_URI,
     torch_dtype=torch.bfloat16,
     trust_remote_code=True,
     force_download=True
   )
 model.save_pretrained(MODEL_PATH) 
 
 # tokenizer
 tokenizer = AutoTokenizer.from_pretrained(MODEL_URI)
 tokenizer.save_pretrained(TOKENIZER_PATH) 

--ロード
 # model
 model = AutoModelForCausalLM.from_pretrained(MODEL_PATH)
 
 # tokenizer
 tokenizer = AutoTokenizer.from_pretrained(TOKENIZER_PATH)
 if tokenizer.pad_token is None:
     tokenizer.pad_token = tokenizer.eos_token # コレは要るん?

--print(model)の出力
 ...

**Instruction Tuning [#u28e7e53]
Instruction Tuningは、言語モデルに 命令(Instruction)とその応答(Response)を学習させることで、指示に従う能力を高める手法

***変換関数 [#l190987d]
-Instruction Tuningのプロンプト・フォーマット / テンプレートみたいな(実際に使う時はスペースを詰める)。

--日本語、指示、応答
 text = f"以下は、タスクを説明する指示です。要求を適切に満たす応答を書きなさい\n\n### 指示:   \n{example['instruction'][i]}                                      \n\n### 応答: \n{example['output'][i]}<|endoftext|>"

--英語、question, context, answer~
ちなみに、コンテキストには、RAGのチャンクの様な参考情報が入るイメージ
 text = f"Please answer the question based on the given context.              \n\n### question\n{example['question'][i]}\n\n ### context\n{example['context'][i]}\n\n### answer\n{example['answer'][i]}<|endoftext|>"

-SFTTrainerのformatting_func引数に渡すプロンプトフォーマット変換用関数を定義(末尾にeos_token文字列を追加)

--kunishou/databricks-dolly-15k-ja
 print(tokenizer.eos_token)
 #'<|endoftext|>'
 
 def formatting_prompts_func(example):
     output_texts = []
     for i in range(len(example['instruction'])):
         text = f"以下は、タスクを説明する指示です。要求を適切に満たす応答を書きなさい\n\n### 指示:\n{example['instruction'][i]}\n\n### 応答:\n{example['output'][i]}<|endoftext|>"
         output_texts.append(text)
     return output_texts

--自作JSON(jsonl)
 def formatting_prompts_func(example):
     output_texts = []
     for i in range(len(example['question'])):
         text = f"Please answer the question based on the given context.\n\n### question\n{example['question'][i]}\n\n### context\n{example['context'][i]}\n\n### answer\n{example['answer'][i]}<|endoftext|>"
         output_texts.append(text)
     return output_texts

***損失計算 [#tfe911c1]
-Instruction + Responseの両方を含むプロンプトを使って学習するが、損失計算は「全トークン」vs「応答部分のみ」がある。
--「すべてのトークンを損失計算対象にする(全体を正解として学習)」vs「応答部分だけを損失計算対象にする(Instructionは条件として与えるのみ)」
--後者の方が「モデルが指示を理解し、応答を生成する能力」に焦点を絞るため、効果的とされ、多くの先行研究でも応答部分のみを損失計算するのが一般的

-SFTTrainerのdata_collator引数に渡すDataCollatorForCompletionOnlyLMを定義~
使用するにはpacking=Falseが必要で、この場合、dataset_text_field か formatting_funcを指定する必要あり。

--template

---response_templateは必須指定
 response_template = "### 応答:\n" # "### answer\n"

---instruction_templateは複数回の対話形式の場合に必要(1問1答形式の場合は不要)
 instruction_template = "### 指示:\n" # "### question\n"

--response_template以降のトークンだけを labels に設定~
(他は、PyTorch の CrossEntropyLoss で無視されるラベル = -100)
 from trl import DataCollatorForCompletionOnlyLM
 
 # response_templateは必須指定
 response_template = "### 応答:\n"
 collator = DataCollatorForCompletionOnlyLM(response_template, tokenizer=tokenizer)

***SFTTrainerの構成 [#t3a58389]
-ポイント
--formatting_funcで1問1答形式の文字列を作る。
--data_collatorで応答部分だけを損失対象にする。
--packing=Falseにより、packingせずに処理する。
 from transformers import TrainingArguments
 from trl import SFTTrainer
 
 # SFTTrainerはTrainingArgumentsを使用することができる。
 # 指定しない場合、TrainingArgumentsのデフォルトが指定される。
 args = TrainingArguments(
     output_dir='./output',
     num_train_epochs=2,
     gradient_accumulation_steps=8,
     per_device_train_batch_size=8,
     save_strategy="no",
     logging_steps=20,
     lr_scheduler_type="constant",
     save_total_limit=1,
     fp16=True,
 )
 
 trainer = SFTTrainer(
     model,
     args=args,
     train_dataset=dolly_train_dataset,
     formatting_func=formatting_prompts_func,
     max_seq_length=1024,
     data_collator=collator,
 )

-指定したデータセットがプロンプトフォーマットに変換されていることを確認
 print(trainer.train_dataset)
 # Dataset({
 #     features: ['input_ids', 'attention_mask'],
 #     num_rows: 10417
 # })
 
 print(tokenizer.decode(trainer.train_dataset[0]['input_ids']))
 # 以下は、タスクを説明する指示です。要求を適切に満たす応答を書きなさい
 #
 # ### 指示:
 # XXXXXXXXXX
 #
 # ### 応答:
 # YYYYYYYYYY<|endoftext|>

-損失対象の確認(Instruction部分やpaddingには -100、応答部分にのみ数値が入る)
 from torch.utils.data import DataLoader
 loader = DataLoader(trainer.train_dataset, collate_fn=collator, batch_size=8)
 
 batch = next(iter(loader))
 print(batch['labels'][0])
 #tensor([ -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
 #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
 #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
 #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
 #         -100,  -100,  -100,  -100,   275, 19052,  4044,  2048,   431,   367,
 #            0,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
 #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
 #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
 #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
 #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
 #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
 #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
 #         -100,  -100,  -100,  -100,  -100])

***学習・保存 [#s8e8326a]
-学習・保存
 trainer.train()
 trainer.save_model()

-学習済みモデルのロード

--cyberagent/open-calm-large
 lora_model = AutoModelForCausalLM.from_pretrained('./output', device_map="auto")

***推論の実行 [#k0be0ea6]
-ロード

--cyberagent/open-calm-large~
 model = AutoModelForCausalLM.from_pretrained(MODEL_PATH, device_map="auto", torch_dtype=torch.float16)
 tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)

-プロンプトのトークン化(pt = torch.Tensor)
 inputs = tokenizer('...プロンプト...', return_tensors="pt").to(model.device)
 print(inputs) # テンソルのディクショナリになっている。

-推論時、勾配計算しないブロックに格納、~
また、**inputsで「ディクショナリのキー・値」が「キーワード引数・引数」に展開。
 with torch.no_grad():
     tokens = model.generate(
         **inputs,
         max_new_tokens=64,
         do_sample=True,
         temperature=0.7,
         top_p=0.9,
         repetition_penalty=1.05,
         pad_token_id=tokenizer.pad_token_id,
     )

-デトークン化した推論結果の表示
 output = tokenizer.decode(tokens[0], skip_special_tokens=True)
 print(output)

**LoRAによるチューニング [#zb2bd049]

***コードの差異 [#k62a4fb0]
-peftでLoraConfigを定義して、SFTTrainerのpeft_config引数に指定~
(data_collator引数やformatting_func引数などは前述と同様のものを指定し。)
 from peft import LoraConfig
  
 peft_config = LoraConfig(
     r=8,
     lora_alpha=16,
     lora_dropout=0.05,
     bias="none",
     task_type="CAUSAL_LM",
 )
 
 args = TrainingArguments(
     output_dir='./output_lora',
     ...同上...,
 )
 
 trainer = SFTTrainer(
     ...同上...,
     peft_config=peft_config,
 )
 
 trainer.train()
 trainer.save_model()

-モデルのロード

--cyberagent/open-calm-large~
Adapter + ベース・パラメタ = adapter_config.json の base_model_name_or_path
 lora_model = AutoModelForCausalLM.from_pretrained('./output_lora', device_map="auto")

***確認事項 [#k7d159ce]
-LoRA Adapterが挿入されない層のパラメタ~
SFTTrainerにmodelを渡す前、渡した前、学習後で変化なしのハズ。

--cyberagent/open-calm-large
 # 最初の層のFFNの一部を確認する
 # この時点ではrequires_grad=Trueになっている
 first_ffn_param = model.gpt_neox.layers[0].mlp.dense_h_to_4h.weight
 print(first_ffn_param)
 #Parameter containing:
 #tensor([[-0.1072,  0.0417, -0.0432,  ..., -0.0873, -0.1708, -0.1608],
 #        [-0.0934,  0.0773,  0.0074,  ..., -0.2107,  0.0881, -0.0803],
 #        [-0.0506, -0.1282, -0.1511,  ...,  0.1120, -0.0126, -0.1172],
 #        ...,
 #        [ 0.1274, -0.0688,  0.1787,  ...,  0.1432,  0.0266, -0.1370],
 #        [-0.1108, -0.0758,  0.0035,  ..., -0.0404, -0.1801,  0.0338],
 #        [ 0.0669,  0.0399, -0.0443,  ..., -0.2275, -0.1323,  0.0034]],
 #       device='cuda:0', requires_grad=True)

--Llama-3.2-1B-Instruct

-SFTTrainerにmodelを渡した後でSFTTrainerからmodelにアクセス~
LoraConfigのtarget_modulesに指定しなくても、いい感じにAdapterが挿入される。

--cyberagent/open-calm-large~
「(attention): GPTNeoXAttention」の「(query_key_value): Linear」に「lora_dropout、lora_A、lora_B、lora_embedding_A / lora_embedding_B」が追加
 PeftModelForCausalLM(
   (base_model): LoraModel(
     (model): GPTNeoXForCausalLM(
       (gpt_neox): GPTNeoXModel(
         (embed_in): Embedding(52096, 1536)
         (emb_dropout): Dropout(p=0.0, inplace=False)
         (layers): ModuleList(
           (0-23): 24 x GPTNeoXLayer(
             (input_layernorm): LayerNorm((1536,), eps=1e-05, elementwise_affine=True)
             (post_attention_layernorm): LayerNorm((1536,), 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=1536, out_features=4608, bias=True
                 (lora_dropout): ModuleDict(
                   (default): Dropout(p=0.05, inplace=False)
                 )
                 (lora_A): ModuleDict(
                   (default): Linear(in_features=1536, out_features=8, bias=False)
                 )
                 (lora_B): ModuleDict(
                   (default): Linear(in_features=8, out_features=4608, bias=False)
                 )
                 (lora_embedding_A): ParameterDict()
                 (lora_embedding_B): ParameterDict()
               )
               (dense): Linear(in_features=1536, out_features=1536, bias=True)
               (attention_dropout): Dropout(p=0.0, inplace=False)
             )
             (mlp): GPTNeoXMLP(
               (dense_h_to_4h): Linear(in_features=1536, out_features=6144, bias=True)
               (dense_4h_to_h): Linear(in_features=6144, out_features=1536, bias=True)
               (act): GELUActivation()
             )
           )
         )
         (final_layer_norm): LayerNorm((1536,), eps=1e-05, elementwise_affine=True)
       )
       (embed_out): Linear(in_features=1536, out_features=52096, bias=False)
     )
   )
 )

--Llama-3.2-1B-Instruct

-学習されるパラメタ数を確認~
LoRAにより、trainableなパラメタは、かなり少なくなっているはず。

--cyberagent/open-calm-large
 trainer.model.print_trainable_parameters()
 #trainable params: 1,179,648 || all params: 841,178,112 || trainable%: 0.14023760047622352

--Llama-3.2-1B-Instruct

-SFTTrainerで学習した後のLoRA Adapterが挿入されない層のパラメタ

--cyberagent/open-calm-large
 # Adapterが挿入されていない層はパラメタはbaseモデルのときとまったく同じ
 (lora_model.gpt_neox.layers[0].mlp.dense_h_to_4h.weight == first_ffn_param).all()
 #tensor(True, device='cuda:0') ## GPU上にあるテンソルの全要素が一致

--Llama-3.2-1B-Instruct

**NEFTuneによる学習 [#m6d89a97]

**packingを用いた学習 [#c1f0b2f2]

**学習結果の比較 [#qe8b9bad]

*参考 [#w465ae3e]
https://github.com/OpenTouryoProject/DxCommon/blob/master/Notebook/path/LLM_Fine-tuning2.ipynb

**Qiita [#x7ffee2d]

-huggingface/TRLのSFTTrainerクラスを使えばLLMのInstruction Tuningのコードがスッキリ書けてとても便利です~
https://qiita.com/m__k/items/23ced0db6846e97d41cd
-OpenCALM-7Bのコードリーディング(基本編)~
https://zenn.dev/hijikix/articles/549180d467df3c

**Hugging Face [#n4339d0b]
The AI community building the future.

-TRL - Transformer Reinforcement Learning~
https://huggingface.co/docs/trl/index
-Supervised Fine-tuning Trainer~
https://huggingface.co/docs/trl/sft_trainer

**github.com [#aee3b3d2]
https://github.com/huggingface/trl/blob/main/trl/
-https://github.com/huggingface/trl/blob/main/trl/scripts/sft.py

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS